diff options
author | Chocobozzz <me@florianbigard.com> | 2021-11-03 14:23:55 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-11-03 14:23:55 +0100 |
commit | dd6d2a7ce50e7ff02e00995ccc87f8f929ad9709 (patch) | |
tree | b71ed391c2d8f99bff40dd3461010876de7bb23c | |
parent | d324756edb836672f12284cd18e642a658b273d8 (diff) | |
download | PeerTube-dd6d2a7ce50e7ff02e00995ccc87f8f929ad9709.tar.gz PeerTube-dd6d2a7ce50e7ff02e00995ccc87f8f929ad9709.tar.zst PeerTube-dd6d2a7ce50e7ff02e00995ccc87f8f929ad9709.zip |
Improve advanced input filter
14 files changed, 131 insertions, 33 deletions
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 1fe8d0f9d..dca746f4e 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -30,11 +30,11 @@ export class VideoBlockListComponent extends RestTable implements OnInit { | |||
30 | title: $localize`Advanced filters`, | 30 | title: $localize`Advanced filters`, |
31 | children: [ | 31 | children: [ |
32 | { | 32 | { |
33 | queryParams: { search: 'type:auto' }, | 33 | value: 'type:auto', |
34 | label: $localize`Automatic blocks` | 34 | label: $localize`Automatic blocks` |
35 | }, | 35 | }, |
36 | { | 36 | { |
37 | queryParams: { search: 'type:manual' }, | 37 | value: 'type:manual', |
38 | label: $localize`Manual blocks` | 38 | label: $localize`Manual blocks` |
39 | } | 39 | } |
40 | ] | 40 | ] |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts index a60b228af..25fe65133 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts | |||
@@ -47,11 +47,11 @@ export class VideoCommentListComponent extends RestTable implements OnInit { | |||
47 | title: $localize`Advanced filters`, | 47 | title: $localize`Advanced filters`, |
48 | children: [ | 48 | children: [ |
49 | { | 49 | { |
50 | queryParams: { search: 'local:true' }, | 50 | value: 'local:true', |
51 | label: $localize`Local comments` | 51 | label: $localize`Local comments` |
52 | }, | 52 | }, |
53 | { | 53 | { |
54 | queryParams: { search: 'local:false' }, | 54 | value: 'local:false', |
55 | label: $localize`Remote comments` | 55 | label: $localize`Remote comments` |
56 | } | 56 | } |
57 | ] | 57 | ] |
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts index 548e6e80f..9fba11cbd 100644 --- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts | |||
@@ -39,7 +39,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
39 | title: $localize`Advanced filters`, | 39 | title: $localize`Advanced filters`, |
40 | children: [ | 40 | children: [ |
41 | { | 41 | { |
42 | queryParams: { search: 'banned:true' }, | 42 | value: 'banned:true', |
43 | label: $localize`Banned users` | 43 | label: $localize`Banned users` |
44 | } | 44 | } |
45 | ] | 45 | ] |
diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts index b90fe22d8..f80de7acd 100644 --- a/client/src/app/+admin/overview/videos/video-admin.service.ts +++ b/client/src/app/+admin/overview/videos/video-admin.service.ts | |||
@@ -44,11 +44,11 @@ export class VideoAdminService { | |||
44 | title: $localize`Video type`, | 44 | title: $localize`Video type`, |
45 | children: [ | 45 | children: [ |
46 | { | 46 | { |
47 | queryParams: { search: 'isLive:false' }, | 47 | value: 'isLive:false', |
48 | label: $localize`VOD` | 48 | label: $localize`VOD` |
49 | }, | 49 | }, |
50 | { | 50 | { |
51 | queryParams: { search: 'isLive:true' }, | 51 | value: 'isLive:true', |
52 | label: $localize`Live` | 52 | label: $localize`Live` |
53 | } | 53 | } |
54 | ] | 54 | ] |
@@ -58,19 +58,19 @@ export class VideoAdminService { | |||
58 | title: $localize`Video files`, | 58 | title: $localize`Video files`, |
59 | children: [ | 59 | children: [ |
60 | { | 60 | { |
61 | queryParams: { search: 'webtorrent:true' }, | 61 | value: 'webtorrent:true', |
62 | label: $localize`With WebTorrent` | 62 | label: $localize`With WebTorrent` |
63 | }, | 63 | }, |
64 | { | 64 | { |
65 | queryParams: { search: 'webtorrent:false' }, | 65 | value: 'webtorrent:false', |
66 | label: $localize`Without WebTorrent` | 66 | label: $localize`Without WebTorrent` |
67 | }, | 67 | }, |
68 | { | 68 | { |
69 | queryParams: { search: 'hls:true' }, | 69 | value: 'hls:true', |
70 | label: $localize`With HLS` | 70 | label: $localize`With HLS` |
71 | }, | 71 | }, |
72 | { | 72 | { |
73 | queryParams: { search: 'hls:false' }, | 73 | value: 'hls:false', |
74 | label: $localize`Without HLS` | 74 | label: $localize`Without HLS` |
75 | } | 75 | } |
76 | ] | 76 | ] |
@@ -80,11 +80,11 @@ export class VideoAdminService { | |||
80 | title: $localize`Videos scope`, | 80 | title: $localize`Videos scope`, |
81 | children: [ | 81 | children: [ |
82 | { | 82 | { |
83 | queryParams: { search: 'isLocal:false' }, | 83 | value: 'isLocal:false', |
84 | label: $localize`Remote videos` | 84 | label: $localize`Remote videos` |
85 | }, | 85 | }, |
86 | { | 86 | { |
87 | queryParams: { search: 'isLocal:true' }, | 87 | value: 'isLocal:true', |
88 | label: $localize`Local videos` | 88 | label: $localize`Local videos` |
89 | } | 89 | } |
90 | ] | 90 | ] |
@@ -94,7 +94,7 @@ export class VideoAdminService { | |||
94 | title: $localize`Exclude`, | 94 | title: $localize`Exclude`, |
95 | children: [ | 95 | children: [ |
96 | { | 96 | { |
97 | queryParams: { search: 'excludeMuted' }, | 97 | value: 'excludeMuted', |
98 | label: $localize`Exclude muted accounts` | 98 | label: $localize`Exclude muted accounts` |
99 | } | 99 | } |
100 | ] | 100 | ] |
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts index 635552cf5..0f98a5d33 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts | |||
@@ -86,7 +86,7 @@ export class VideoListComponent extends RestTable implements OnInit { | |||
86 | } | 86 | } |
87 | 87 | ||
88 | getPrivacyBadgeClass (video: Video) { | 88 | getPrivacyBadgeClass (video: Video) { |
89 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-blue' | 89 | if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' |
90 | 90 | ||
91 | return 'badge-yellow' | 91 | return 'badge-yellow' |
92 | } | 92 | } |
diff --git a/client/src/app/+my-library/my-follows/my-followers.component.ts b/client/src/app/+my-library/my-follows/my-followers.component.ts index 4a72b983f..0dd9bf6f5 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.ts +++ b/client/src/app/+my-library/my-follows/my-followers.component.ts | |||
@@ -39,7 +39,7 @@ export class MyFollowersComponent implements OnInit { | |||
39 | this.auth.userInformationLoaded.subscribe(() => { | 39 | this.auth.userInformationLoaded.subscribe(() => { |
40 | const channelFilters = this.auth.getUser().videoChannels.map(c => { | 40 | const channelFilters = this.auth.getUser().videoChannels.map(c => { |
41 | return { | 41 | return { |
42 | queryParams: { search: 'channel:' + c.name }, | 42 | value: 'channel:' + c.name, |
43 | label: c.name | 43 | label: c.name |
44 | } | 44 | } |
45 | }) | 45 | }) |
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index a117d0915..261e87f99 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts | |||
@@ -82,7 +82,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
82 | 82 | ||
83 | const channelFilters = this.userChannels.map(c => { | 83 | const channelFilters = this.userChannels.map(c => { |
84 | return { | 84 | return { |
85 | queryParams: { search: 'channel:' + c.name }, | 85 | value: 'channel:' + c.name, |
86 | label: c.name | 86 | label: c.name |
87 | } | 87 | } |
88 | }) | 88 | }) |
@@ -92,7 +92,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { | |||
92 | title: $localize`Advanced filters`, | 92 | title: $localize`Advanced filters`, |
93 | children: [ | 93 | children: [ |
94 | { | 94 | { |
95 | queryParams: { search: 'isLive:true' }, | 95 | value: 'isLive:true', |
96 | label: $localize`Only live videos` | 96 | label: $localize`Only live videos` |
97 | } | 97 | } |
98 | ] | 98 | ] |
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts index 59152e658..b2a5a3f72 100644 --- a/client/src/app/core/rest/rest.service.ts +++ b/client/src/app/core/rest/rest.service.ts | |||
@@ -82,13 +82,11 @@ export class RestService { | |||
82 | parseQueryStringFilter <T extends QueryStringFilterPrefixes> (q: string, prefixes: T): ParseQueryStringFiltersResult<keyof T> { | 82 | parseQueryStringFilter <T extends QueryStringFilterPrefixes> (q: string, prefixes: T): ParseQueryStringFiltersResult<keyof T> { |
83 | if (!q) return {} | 83 | if (!q) return {} |
84 | 84 | ||
85 | // Tokenize the strings using spaces that are not in quotes | 85 | const tokens = this.tokenizeString(q) |
86 | const tokens = q.match(/(?:[^\s"]+|"[^"]*")+/g) | ||
87 | .filter(token => !!token) | ||
88 | 86 | ||
89 | // Build prefix array | 87 | // Build prefix array |
90 | const prefixeStrings = Object.values(prefixes) | 88 | const prefixeStrings = Object.values(prefixes) |
91 | .map(p => p.prefix) | 89 | .map(p => p.prefix) |
92 | 90 | ||
93 | logger(`Built tokens "${tokens.join(', ')}" for prefixes "${prefixeStrings.join(', ')}"`) | 91 | logger(`Built tokens "${tokens.join(', ')}" for prefixes "${prefixeStrings.join(', ')}"`) |
94 | 92 | ||
@@ -137,4 +135,12 @@ export class RestService { | |||
137 | ...additionalFilters | 135 | ...additionalFilters |
138 | } | 136 | } |
139 | } | 137 | } |
138 | |||
139 | tokenizeString (q: string) { | ||
140 | if (!q) return [] | ||
141 | |||
142 | // Tokenize the strings using spaces that are not in quotes | ||
143 | return q.match(/(?:[^\s"]+|"[^"]*")+/g) | ||
144 | .filter(token => !!token) | ||
145 | } | ||
140 | } | 146 | } |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 10f5861b9..b902726fa 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -39,23 +39,23 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
39 | title: $localize`Advanced filters`, | 39 | title: $localize`Advanced filters`, |
40 | children: [ | 40 | children: [ |
41 | { | 41 | { |
42 | queryParams: { search: 'state:pending' }, | 42 | value: 'state:pending', |
43 | label: $localize`Unsolved reports` | 43 | label: $localize`Unsolved reports` |
44 | }, | 44 | }, |
45 | { | 45 | { |
46 | queryParams: { search: 'state:accepted' }, | 46 | value: 'state:accepted', |
47 | label: $localize`Accepted reports` | 47 | label: $localize`Accepted reports` |
48 | }, | 48 | }, |
49 | { | 49 | { |
50 | queryParams: { search: 'state:rejected' }, | 50 | value: 'state:rejected', |
51 | label: $localize`Refused reports` | 51 | label: $localize`Refused reports` |
52 | }, | 52 | }, |
53 | { | 53 | { |
54 | queryParams: { search: 'videoIs:blacklisted' }, | 54 | value: 'videoIs:blacklisted', |
55 | label: $localize`Reports with blocked videos` | 55 | label: $localize`Reports with blocked videos` |
56 | }, | 56 | }, |
57 | { | 57 | { |
58 | queryParams: { search: 'videoIs:deleted' }, | 58 | value: 'videoIs:deleted', |
59 | label: $localize`Reports with deleted videos` | 59 | label: $localize`Reports with deleted videos` |
60 | } | 60 | } |
61 | ] | 61 | ] |
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.html b/client/src/app/shared/shared-forms/advanced-input-filter.component.html index c662b9bb6..7031cb53b 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.html +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.html | |||
@@ -8,9 +8,11 @@ | |||
8 | <ng-container *ngFor="let group of filters"> | 8 | <ng-container *ngFor="let group of filters"> |
9 | <h6 class="dropdown-header">{{ group.title }}</h6> | 9 | <h6 class="dropdown-header">{{ group.title }}</h6> |
10 | 10 | ||
11 | <a *ngFor="let filter of group.children" [routerLink]="[ '.' ]" [queryParams]="filter.queryParams" class="dropdown-item"> | 11 | <button *ngFor="let filter of group.children" (click)="onFilterClick(filter)" class="dropdown-item"> |
12 | <my-global-icon [ngClass]="{ 'no-visible': !isFilterEnabled(filter) }" iconName="tick"></my-global-icon> | ||
13 | |||
12 | {{ filter.label }} | 14 | {{ filter.label }} |
13 | </a> | 15 | </button> |
14 | </ng-container> | 16 | </ng-container> |
15 | </div> | 17 | </div> |
16 | </div> | 18 | </div> |
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss index 07a43761c..ee1b3b508 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .dropdown-item { | ||
5 | font-size: 14px; | ||
6 | } | ||
7 | |||
4 | input { | 8 | input { |
5 | @include peertube-input-text(250px); | 9 | @include peertube-input-text(250px); |
6 | } | 10 | } |
@@ -8,3 +12,22 @@ input { | |||
8 | .input-group-text { | 12 | .input-group-text { |
9 | background-color: transparent; | 13 | background-color: transparent; |
10 | } | 14 | } |
15 | |||
16 | my-global-icon { | ||
17 | $size: 18px; | ||
18 | $margin: 2px; | ||
19 | |||
20 | @include margin-right($margin); | ||
21 | |||
22 | opacity: 1; | ||
23 | width: 18px; | ||
24 | height: 18px; | ||
25 | |||
26 | |||
27 | &.no-visible { | ||
28 | @include margin-right($size + $margin); | ||
29 | |||
30 | width: 0; | ||
31 | height: 0; | ||
32 | } | ||
33 | } | ||
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts index a12dddf7a..d8aeaa0fa 100644 --- a/client/src/app/shared/shared-forms/advanced-input-filter.component.ts +++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.ts | |||
@@ -3,14 +3,17 @@ import { Subject } from 'rxjs' | |||
3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
4 | import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 4 | import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
5 | import { ActivatedRoute, Params, Router } from '@angular/router' | 5 | import { ActivatedRoute, Params, Router } from '@angular/router' |
6 | import { RestService } from '@app/core' | ||
6 | 7 | ||
7 | export type AdvancedInputFilter = { | 8 | export type AdvancedInputFilter = { |
8 | title: string | 9 | title: string |
9 | 10 | ||
10 | children: { | 11 | children: AdvancedInputFilterChild[] |
11 | label: string | 12 | } |
12 | queryParams: Params | 13 | |
13 | }[] | 14 | export type AdvancedInputFilterChild = { |
15 | label: string | ||
16 | value: string | ||
14 | } | 17 | } |
15 | 18 | ||
16 | const logger = debug('peertube:AdvancedInputFilterComponent') | 19 | const logger = debug('peertube:AdvancedInputFilterComponent') |
@@ -28,6 +31,8 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
28 | 31 | ||
29 | searchValue: string | 32 | searchValue: string |
30 | 33 | ||
34 | private enabledFilters = new Set<string>() | ||
35 | |||
31 | private searchStream: Subject<string> | 36 | private searchStream: Subject<string> |
32 | 37 | ||
33 | private viewInitialized = false | 38 | private viewInitialized = false |
@@ -35,6 +40,7 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
35 | 40 | ||
36 | constructor ( | 41 | constructor ( |
37 | private route: ActivatedRoute, | 42 | private route: ActivatedRoute, |
43 | private restService: RestService, | ||
38 | private router: Router | 44 | private router: Router |
39 | ) { } | 45 | ) { } |
40 | 46 | ||
@@ -62,6 +68,18 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
62 | return this.filters && this.filters.length !== 0 | 68 | return this.filters && this.filters.length !== 0 |
63 | } | 69 | } |
64 | 70 | ||
71 | isFilterEnabled (filter: AdvancedInputFilterChild) { | ||
72 | return this.enabledFilters.has(filter.value) | ||
73 | } | ||
74 | |||
75 | onFilterClick (filter: AdvancedInputFilterChild) { | ||
76 | const newSearch = this.isFilterEnabled(filter) | ||
77 | ? this.removeFilterToSearch(this.searchValue, filter) | ||
78 | : this.addFilterToSearch(this.searchValue, filter) | ||
79 | |||
80 | this.router.navigate([ '.' ], { relativeTo: this.route, queryParams: { search: newSearch.trim() } }) | ||
81 | } | ||
82 | |||
65 | private scheduleSearchUpdate (value: string) { | 83 | private scheduleSearchUpdate (value: string) { |
66 | this.searchValue = value | 84 | this.searchValue = value |
67 | this.searchStream.next(this.searchValue) | 85 | this.searchStream.next(this.searchValue) |
@@ -71,6 +89,7 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
71 | this.searchValue = value | 89 | this.searchValue = value |
72 | 90 | ||
73 | this.setQueryParams(this.searchValue) | 91 | this.setQueryParams(this.searchValue) |
92 | this.parseFilters(this.searchValue) | ||
74 | this.emitSearch() | 93 | this.emitSearch() |
75 | } | 94 | } |
76 | 95 | ||
@@ -84,6 +103,9 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
84 | if (this.searchValue === search) return | 103 | if (this.searchValue === search) return |
85 | 104 | ||
86 | this.searchValue = search | 105 | this.searchValue = search |
106 | |||
107 | this.parseFilters(this.searchValue) | ||
108 | |||
87 | this.emitSearch() | 109 | this.emitSearch() |
88 | }) | 110 | }) |
89 | } | 111 | } |
@@ -98,6 +120,7 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
98 | ) | 120 | ) |
99 | .subscribe(() => { | 121 | .subscribe(() => { |
100 | this.setQueryParams(this.searchValue) | 122 | this.setQueryParams(this.searchValue) |
123 | this.parseFilters(this.searchValue) | ||
101 | 124 | ||
102 | this.emitSearch() | 125 | this.emitSearch() |
103 | }) | 126 | }) |
@@ -120,4 +143,34 @@ export class AdvancedInputFilterComponent implements OnInit, AfterViewInit { | |||
120 | if (search) Object.assign(queryParams, { search }) | 143 | if (search) Object.assign(queryParams, { search }) |
121 | this.router.navigate([ ], { queryParams }) | 144 | this.router.navigate([ ], { queryParams }) |
122 | } | 145 | } |
146 | |||
147 | private removeFilterToSearch (search: string, removedFilter: AdvancedInputFilterChild) { | ||
148 | return search.replace(removedFilter.value, '') | ||
149 | } | ||
150 | |||
151 | private addFilterToSearch (search: string, newFilter: AdvancedInputFilterChild) { | ||
152 | const prefix = newFilter.value.split(':').shift() | ||
153 | |||
154 | // Tokenize search and remove a potential existing filter | ||
155 | const tokens = this.restService.tokenizeString(search) | ||
156 | .filter(t => !t.startsWith(prefix)) | ||
157 | |||
158 | tokens.push(newFilter.value) | ||
159 | |||
160 | return tokens.join(' ') | ||
161 | } | ||
162 | |||
163 | private parseFilters (search: string) { | ||
164 | const tokens = this.restService.tokenizeString(search) | ||
165 | |||
166 | this.enabledFilters = new Set() | ||
167 | |||
168 | for (const group of this.filters) { | ||
169 | for (const filter of group.children) { | ||
170 | if (tokens.includes(filter.value)) { | ||
171 | this.enabledFilters.add(filter.value) | ||
172 | } | ||
173 | } | ||
174 | } | ||
175 | } | ||
123 | } | 176 | } |
diff --git a/shared/core-utils/utils/array.ts b/shared/core-utils/utils/array.ts new file mode 100644 index 000000000..9e326a5aa --- /dev/null +++ b/shared/core-utils/utils/array.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | function findCommonElement <T> (array1: T[], array2: T[]) { | ||
2 | for (const a of array1) { | ||
3 | for (const b of array2) { | ||
4 | if (a === b) return a | ||
5 | } | ||
6 | } | ||
7 | |||
8 | return null | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | findCommonElement | ||
13 | } | ||
diff --git a/shared/core-utils/utils/index.ts b/shared/core-utils/utils/index.ts index a71977d88..8d16365a8 100644 --- a/shared/core-utils/utils/index.ts +++ b/shared/core-utils/utils/index.ts | |||
@@ -1 +1,2 @@ | |||
1 | export * from './array' | ||
1 | export * from './object' | 2 | export * from './object' |