diff options
author | Chocobozzz <me@florianbigard.com> | 2020-11-16 14:47:05 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2020-11-16 14:47:05 +0100 |
commit | 5ed46c1bce29affbe101f126d58657ab484bffe7 (patch) | |
tree | 019b1649facdefea85eb2cb490a485a5fbf83c64 /client/src/app | |
parent | 7706b3703aeb2bea686b12089959b963a7dd89f4 (diff) | |
download | PeerTube-5ed46c1bce29affbe101f126d58657ab484bffe7.tar.gz PeerTube-5ed46c1bce29affbe101f126d58657ab484bffe7.tar.zst PeerTube-5ed46c1bce29affbe101f126d58657ab484bffe7.zip |
Refactor rest table search filter
Diffstat (limited to 'client/src/app')
9 files changed, 61 insertions, 115 deletions
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html index 96293ae5e..7250b1368 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html | |||
@@ -27,7 +27,7 @@ | |||
27 | </div> | 27 | </div> |
28 | <input | 28 | <input |
29 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 29 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
30 | (keyup)="onBlockSearch($event)" | 30 | (keyup)="onSearch($event)" |
31 | > | 31 | > |
32 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | 32 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> |
33 | <span class="sr-only" i18n>Clear filters</span> | 33 | <span class="sr-only" i18n>Clear filters</span> |
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 2b1ef663c..82818547c 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 | |||
@@ -1,5 +1,5 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { filter, switchMap } from 'rxjs/operators' | 2 | import { switchMap } from 'rxjs/operators' |
3 | import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' | 3 | import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils' |
4 | import { environment } from 'src/environments/environment' | 4 | import { environment } from 'src/environments/environment' |
5 | import { AfterViewInit, Component, OnInit } from '@angular/core' | 5 | import { AfterViewInit, Component, OnInit } from '@angular/core' |
@@ -25,16 +25,16 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
25 | videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] | 25 | videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] |
26 | 26 | ||
27 | constructor ( | 27 | constructor ( |
28 | protected route: ActivatedRoute, | ||
29 | protected router: Router, | ||
28 | private notifier: Notifier, | 30 | private notifier: Notifier, |
29 | private serverService: ServerService, | 31 | private serverService: ServerService, |
30 | private confirmService: ConfirmService, | 32 | private confirmService: ConfirmService, |
31 | private videoBlocklistService: VideoBlockService, | 33 | private videoBlocklistService: VideoBlockService, |
32 | private markdownRenderer: MarkdownService, | 34 | private markdownRenderer: MarkdownService, |
33 | private sanitizer: DomSanitizer, | 35 | private sanitizer: DomSanitizer, |
34 | private videoService: VideoService, | 36 | private videoService: VideoService |
35 | private route: ActivatedRoute, | 37 | ) { |
36 | private router: Router | ||
37 | ) { | ||
38 | super() | 38 | super() |
39 | 39 | ||
40 | this.videoBlocklistActions = [ | 40 | this.videoBlocklistActions = [ |
@@ -104,14 +104,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV | |||
104 | }) | 104 | }) |
105 | 105 | ||
106 | this.initialize() | 106 | this.initialize() |
107 | 107 | this.listenToSearchChange() | |
108 | this.route.queryParams | ||
109 | .pipe(filter(params => params.search !== undefined && params.search !== null)) | ||
110 | .subscribe(params => { | ||
111 | this.search = params.search | ||
112 | this.setTableFilter(params.search) | ||
113 | this.loadData() | ||
114 | }) | ||
115 | } | 108 | } |
116 | 109 | ||
117 | ngAfterViewInit () { | 110 | ngAfterViewInit () { |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html index 5c932739b..330ee2478 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html | |||
@@ -31,7 +31,7 @@ | |||
31 | </div> | 31 | </div> |
32 | <input | 32 | <input |
33 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 33 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
34 | (keyup)="onInputSearch($event)" | 34 | (keyup)="onSearch($event)" |
35 | > | 35 | > |
36 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | 36 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> |
37 | <span class="sr-only" i18n>Clear filters</span> | 37 | <span class="sr-only" i18n>Clear filters</span> |
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 d26047125..284ec541d 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 | |||
@@ -14,6 +14,8 @@ import { FeedFormat, UserRight } from '@shared/models' | |||
14 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] | 14 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] |
15 | }) | 15 | }) |
16 | export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit { | 16 | export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit { |
17 | baseRoute = '/admin/moderation/video-comments/list' | ||
18 | |||
17 | comments: VideoCommentAdmin[] | 19 | comments: VideoCommentAdmin[] |
18 | totalRecords = 0 | 20 | totalRecords = 0 |
19 | sort: SortMeta = { field: 'createdAt', order: -1 } | 21 | sort: SortMeta = { field: 'createdAt', order: -1 } |
@@ -44,13 +46,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
44 | } | 46 | } |
45 | 47 | ||
46 | constructor ( | 48 | constructor ( |
49 | protected router: Router, | ||
50 | protected route: ActivatedRoute, | ||
47 | private auth: AuthService, | 51 | private auth: AuthService, |
48 | private notifier: Notifier, | 52 | private notifier: Notifier, |
49 | private confirmService: ConfirmService, | 53 | private confirmService: ConfirmService, |
50 | private videoCommentService: VideoCommentService, | 54 | private videoCommentService: VideoCommentService, |
51 | private markdownRenderer: MarkdownService, | 55 | private markdownRenderer: MarkdownService, |
52 | private route: ActivatedRoute, | ||
53 | private router: Router, | ||
54 | private bulkService: BulkService | 56 | private bulkService: BulkService |
55 | ) { | 57 | ) { |
56 | super() | 58 | super() |
@@ -75,39 +77,13 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
75 | 77 | ||
76 | ngOnInit () { | 78 | ngOnInit () { |
77 | this.initialize() | 79 | this.initialize() |
78 | 80 | this.listenToSearchChange() | |
79 | this.route.queryParams | ||
80 | .pipe(filter(params => params.search !== undefined && params.search !== null)) | ||
81 | .subscribe(params => { | ||
82 | this.search = params.search | ||
83 | this.setTableFilter(params.search) | ||
84 | this.loadData() | ||
85 | }) | ||
86 | } | 81 | } |
87 | 82 | ||
88 | ngAfterViewInit () { | 83 | ngAfterViewInit () { |
89 | if (this.search) this.setTableFilter(this.search) | 84 | if (this.search) this.setTableFilter(this.search) |
90 | } | 85 | } |
91 | 86 | ||
92 | onInputSearch (event: Event) { | ||
93 | this.onSearch(event) | ||
94 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
95 | } | ||
96 | |||
97 | setQueryParams (search: string) { | ||
98 | const queryParams: Params = {} | ||
99 | |||
100 | if (search) Object.assign(queryParams, { search }) | ||
101 | this.router.navigate([ '/admin/moderation/video-comments/list' ], { queryParams }) | ||
102 | } | ||
103 | |||
104 | resetTableFilter () { | ||
105 | this.setTableFilter('') | ||
106 | this.setQueryParams('') | ||
107 | this.resetSearch() | ||
108 | } | ||
109 | /* END Table filter functions */ | ||
110 | |||
111 | getIdentifier () { | 87 | getIdentifier () { |
112 | return 'VideoCommentListComponent' | 88 | return 'VideoCommentListComponent' |
113 | } | 89 | } |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 63144502c..38445cee7 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -30,7 +30,7 @@ | |||
30 | </div> | 30 | </div> |
31 | <input | 31 | <input |
32 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 32 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
33 | (keyup)="onUserSearch($event)" | 33 | (keyup)="onSearch($event)" |
34 | > | 34 | > |
35 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | 35 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> |
36 | <span class="sr-only" i18n>Clear filters</span> | 36 | <span class="sr-only" i18n>Clear filters</span> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 86812f73d..9f92358a0 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -35,14 +35,14 @@ export class UserListComponent extends RestTable implements OnInit { | |||
35 | private serverConfig: ServerConfig | 35 | private serverConfig: ServerConfig |
36 | 36 | ||
37 | constructor ( | 37 | constructor ( |
38 | protected route: ActivatedRoute, | ||
39 | protected router: Router, | ||
38 | private notifier: Notifier, | 40 | private notifier: Notifier, |
39 | private confirmService: ConfirmService, | 41 | private confirmService: ConfirmService, |
40 | private serverService: ServerService, | 42 | private serverService: ServerService, |
41 | private userService: UserService, | ||
42 | private auth: AuthService, | 43 | private auth: AuthService, |
43 | private route: ActivatedRoute, | 44 | private userService: UserService |
44 | private router: Router | 45 | ) { |
45 | ) { | ||
46 | super() | 46 | super() |
47 | } | 47 | } |
48 | 48 | ||
@@ -68,14 +68,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
68 | .subscribe(config => this.serverConfig = config) | 68 | .subscribe(config => this.serverConfig = config) |
69 | 69 | ||
70 | this.initialize() | 70 | this.initialize() |
71 | 71 | this.listenToSearchChange() | |
72 | this.route.queryParams | ||
73 | .subscribe(params => { | ||
74 | this.search = params.search || '' | ||
75 | |||
76 | this.setTableFilter(this.search) | ||
77 | this.loadData() | ||
78 | }) | ||
79 | 72 | ||
80 | this.bulkUserActions = [ | 73 | this.bulkUserActions = [ |
81 | [ | 74 | [ |
@@ -170,26 +163,6 @@ export class UserListComponent extends RestTable implements OnInit { | |||
170 | this.loadData() | 163 | this.loadData() |
171 | } | 164 | } |
172 | 165 | ||
173 | /* Table filter functions */ | ||
174 | onUserSearch (event: Event) { | ||
175 | this.onSearch(event) | ||
176 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
177 | } | ||
178 | |||
179 | setQueryParams (search: string) { | ||
180 | const queryParams: Params = {} | ||
181 | if (search) Object.assign(queryParams, { search }) | ||
182 | |||
183 | this.router.navigate([ '/admin/users/list' ], { queryParams }) | ||
184 | } | ||
185 | |||
186 | resetTableFilter () { | ||
187 | this.setTableFilter('') | ||
188 | this.setQueryParams('') | ||
189 | this.resetSearch() | ||
190 | } | ||
191 | /* END Table filter functions */ | ||
192 | |||
193 | switchToDefaultAvatar ($event: Event) { | 166 | switchToDefaultAvatar ($event: Event) { |
194 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | 167 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() |
195 | } | 168 | } |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index 7e7e6f4f7..50f6bf39d 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 1 | import * as debug from 'debug' |
2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' | 2 | import { LazyLoadEvent, SortMeta } from 'primeng/api' |
3 | import { RestPagination } from './rest-pagination' | ||
4 | import { Subject } from 'rxjs' | 3 | import { Subject } from 'rxjs' |
5 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
6 | import * as debug from 'debug' | 5 | import { ActivatedRoute, Params, Router } from '@angular/router' |
6 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | ||
7 | import { RestPagination } from './rest-pagination' | ||
7 | 8 | ||
8 | const logger = debug('peertube:tables:RestTable') | 9 | const logger = debug('peertube:tables:RestTable') |
9 | 10 | ||
@@ -18,8 +19,13 @@ export abstract class RestTable { | |||
18 | rowsPerPage = this.rowsPerPageOptions[0] | 19 | rowsPerPage = this.rowsPerPageOptions[0] |
19 | expandedRows = {} | 20 | expandedRows = {} |
20 | 21 | ||
22 | baseRoute: string | ||
23 | |||
21 | protected searchStream: Subject<string> | 24 | protected searchStream: Subject<string> |
22 | 25 | ||
26 | protected route: ActivatedRoute | ||
27 | protected router: Router | ||
28 | |||
23 | abstract getIdentifier (): string | 29 | abstract getIdentifier (): string |
24 | 30 | ||
25 | initialize () { | 31 | initialize () { |
@@ -80,6 +86,33 @@ export abstract class RestTable { | |||
80 | onSearch (event: Event) { | 86 | onSearch (event: Event) { |
81 | const target = event.target as HTMLInputElement | 87 | const target = event.target as HTMLInputElement |
82 | this.searchStream.next(target.value) | 88 | this.searchStream.next(target.value) |
89 | |||
90 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
91 | } | ||
92 | |||
93 | setQueryParams (search: string) { | ||
94 | if (!this.baseRoute) return | ||
95 | |||
96 | const queryParams: Params = {} | ||
97 | |||
98 | if (search) Object.assign(queryParams, { search }) | ||
99 | this.router.navigate([ this.baseRoute ], { queryParams }) | ||
100 | } | ||
101 | |||
102 | resetTableFilter () { | ||
103 | this.setTableFilter('') | ||
104 | this.setQueryParams('') | ||
105 | this.resetSearch() | ||
106 | } | ||
107 | |||
108 | listenToSearchChange () { | ||
109 | this.route.queryParams | ||
110 | .subscribe(params => { | ||
111 | this.search = params.search || '' | ||
112 | |||
113 | this.setTableFilter(this.search) | ||
114 | this.loadData() | ||
115 | }) | ||
83 | } | 116 | } |
84 | 117 | ||
85 | onPage (event: { first: number, rows: number }) { | 118 | onPage (event: { first: number, rows: number }) { |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index 90638d176..1c4149147 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -25,7 +25,7 @@ | |||
25 | </div> | 25 | </div> |
26 | <input | 26 | <input |
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
28 | (keyup)="onAbuseSearch($event)" | 28 | (keyup)="onSearch($event)" |
29 | > | 29 | > |
30 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | 30 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> |
31 | <span class="sr-only" i18n>Clear filters</span> | 31 | <span class="sr-only" i18n>Clear filters</span> |
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 3bc527684..ca0d23699 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 | |||
@@ -5,7 +5,7 @@ import { buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/uti | |||
5 | import { environment } from 'src/environments/environment' | 5 | import { environment } from 'src/environments/environment' |
6 | import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core' | 6 | import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core' |
7 | import { DomSanitizer } from '@angular/platform-browser' | 7 | import { DomSanitizer } from '@angular/platform-browser' |
8 | import { ActivatedRoute, Params, Router } from '@angular/router' | 8 | import { ActivatedRoute, Router } from '@angular/router' |
9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | 9 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' | 11 | import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation' |
@@ -37,6 +37,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
37 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] | 37 | abuseActions: DropdownAction<ProcessedAbuse>[][] = [] |
38 | 38 | ||
39 | constructor ( | 39 | constructor ( |
40 | protected route: ActivatedRoute, | ||
41 | protected router: Router, | ||
40 | private notifier: Notifier, | 42 | private notifier: Notifier, |
41 | private abuseService: AbuseService, | 43 | private abuseService: AbuseService, |
42 | private blocklistService: BlocklistService, | 44 | private blocklistService: BlocklistService, |
@@ -45,9 +47,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
45 | private videoBlocklistService: VideoBlockService, | 47 | private videoBlocklistService: VideoBlockService, |
46 | private confirmService: ConfirmService, | 48 | private confirmService: ConfirmService, |
47 | private markdownRenderer: MarkdownService, | 49 | private markdownRenderer: MarkdownService, |
48 | private sanitizer: DomSanitizer, | 50 | private sanitizer: DomSanitizer |
49 | private route: ActivatedRoute, | ||
50 | private router: Router | ||
51 | ) { | 51 | ) { |
52 | super() | 52 | super() |
53 | } | 53 | } |
@@ -66,16 +66,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
66 | ] | 66 | ] |
67 | 67 | ||
68 | this.initialize() | 68 | this.initialize() |
69 | 69 | this.listenToSearchChange() | |
70 | this.route.queryParams | ||
71 | .subscribe(params => { | ||
72 | this.search = params.search || '' | ||
73 | |||
74 | logger('On URL change (search: %s).', this.search) | ||
75 | |||
76 | this.setTableFilter(this.search) | ||
77 | this.loadData() | ||
78 | }) | ||
79 | } | 70 | } |
80 | 71 | ||
81 | ngAfterViewInit () { | 72 | ngAfterViewInit () { |
@@ -98,26 +89,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV | |||
98 | this.loadData() | 89 | this.loadData() |
99 | } | 90 | } |
100 | 91 | ||
101 | /* Table filter functions */ | ||
102 | onAbuseSearch (event: Event) { | ||
103 | this.onSearch(event) | ||
104 | this.setQueryParams((event.target as HTMLInputElement).value) | ||
105 | } | ||
106 | |||
107 | setQueryParams (search: string) { | ||
108 | const queryParams: Params = {} | ||
109 | if (search) Object.assign(queryParams, { search }) | ||
110 | |||
111 | this.router.navigate([ this.baseRoute ], { queryParams }) | ||
112 | } | ||
113 | |||
114 | resetTableFilter () { | ||
115 | this.setTableFilter('') | ||
116 | this.setQueryParams('') | ||
117 | this.resetSearch() | ||
118 | } | ||
119 | /* END Table filter functions */ | ||
120 | |||
121 | isAbuseAccepted (abuse: AdminAbuse) { | 92 | isAbuseAccepted (abuse: AdminAbuse) { |
122 | return abuse.state.id === AbuseState.ACCEPTED | 93 | return abuse.state.id === AbuseState.ACCEPTED |
123 | } | 94 | } |