Skip to content

Commit 8ada243

Browse files
SuaYooemma-sg
andauthored
feat: Filter browser profile list by name (#3017)
- Allows users to filter browser profile list by name - Fixes layout inconsistencies with browser profiles list --------- Co-authored-by: Emma Segal-Grossman <[email protected]>
1 parent 937a563 commit 8ada243

File tree

4 files changed

+158
-92
lines changed

4 files changed

+158
-92
lines changed

frontend/src/layouts/pageHeader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ export function pageHeader({
123123
return html`
124124
<header
125125
class=${clsx(
126-
tw`mt-5 flex flex-row flex-wrap gap-3`,
127-
border && tw`border-b pb-3`,
126+
tw`mt-5 flex flex-row flex-wrap gap-3 pb-3`,
127+
border && tw`border-b`,
128128
classNames,
129129
)}
130130
>

frontend/src/pages/org/browser-profiles-list.ts

Lines changed: 140 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Task } from "@lit/task";
33
import { html, type PropertyValues } from "lit";
44
import { customElement, state } from "lit/decorators.js";
55
import { when } from "lit/directives/when.js";
6+
import omit from "lodash/fp/omit";
67
import queryString from "query-string";
78

89
import type { Profile } from "./types";
@@ -20,7 +21,7 @@ import { ClipboardController } from "@/controllers/clipboard";
2021
import { SearchParamsValue } from "@/controllers/searchParamsValue";
2122
import { originsWithRemainder } from "@/features/browser-profiles/templates/origins-with-remainder";
2223
import { emptyMessage } from "@/layouts/emptyMessage";
23-
import { page } from "@/layouts/page";
24+
import { pageHeader } from "@/layouts/pageHeader";
2425
import { OrgTab } from "@/routes";
2526
import type {
2627
APIPaginatedList,
@@ -30,6 +31,7 @@ import type {
3031
import { SortDirection as SortDirectionEnum } from "@/types/utils";
3132
import { isApiError } from "@/utils/api";
3233
import { isArchivingDisabled } from "@/utils/orgs";
34+
import { toSearchItem, type SearchValues } from "@/utils/searchValues";
3335

3436
const SORT_DIRECTIONS = ["asc", "desc"] as const;
3537
type SortDirection = (typeof SORT_DIRECTIONS)[number];
@@ -45,7 +47,7 @@ const sortableFields: Record<
4547
> = {
4648
name: {
4749
label: msg("Name"),
48-
defaultDirection: "desc",
50+
defaultDirection: "asc",
4951
},
5052
url: {
5153
label: msg("Primary Site"),
@@ -66,7 +68,17 @@ const DEFAULT_SORT_BY = {
6668
direction: sortableFields.modified.defaultDirection || "desc",
6769
} as const satisfies SortBy;
6870
const INITIAL_PAGE_SIZE = 20;
69-
const FILTER_BY_CURRENT_USER_STORAGE_KEY = "btrix.filterByCurrentUser.crawls";
71+
const FILTER_BY_CURRENT_USER_STORAGE_KEY =
72+
"btrix.filterByCurrentUser.browserProfiles";
73+
const SEARCH_KEYS = ["name"] as const;
74+
const DEFAULT_TAGS_TYPE = "or";
75+
76+
type FilterBy = {
77+
name?: string;
78+
tags?: string[];
79+
tagsType?: "and" | "or";
80+
mine?: boolean;
81+
};
7082

7183
const columnsCss = [
7284
"min-content", // Status
@@ -124,84 +136,84 @@ export class BrowserProfilesList extends BtrixElement {
124136
},
125137
);
126138

127-
private readonly filterByTags = new SearchParamsValue<string[] | undefined>(
128-
this,
129-
(value, params) => {
130-
params.delete("tags");
131-
value?.forEach((v) => {
132-
params.append("tags", v);
133-
});
134-
return params;
135-
},
136-
(params) => params.getAll("tags"),
137-
);
138-
139-
private readonly filterByTagsType = new SearchParamsValue<"and" | "or">(
139+
private readonly filterBy = new SearchParamsValue<FilterBy>(
140140
this,
141141
(value, params) => {
142-
if (value === "and") {
143-
params.set("tagsType", value);
142+
if ("name" in value && value["name"]) {
143+
params.set("name", value["name"]);
144+
} else {
145+
params.delete("name");
146+
}
147+
if ("tags" in value) {
148+
params.delete("tags");
149+
value["tags"]?.forEach((v) => {
150+
params.append("tags", v);
151+
});
152+
} else {
153+
params.delete("tags");
154+
}
155+
if ("tagsType" in value && value["tagsType"] === "and") {
156+
params.set("tagsType", value["tagsType"]);
144157
} else {
145158
params.delete("tagsType");
146159
}
147-
return params;
148-
},
149-
(params) => (params.get("tagsType") === "and" ? "and" : "or"),
150-
);
151-
152-
private readonly filterByCurrentUser = new SearchParamsValue<boolean>(
153-
this,
154-
(value, params) => {
155-
if (value) {
160+
if ("mine" in value && value["mine"]) {
156161
params.set("mine", "true");
162+
window.sessionStorage.setItem(
163+
FILTER_BY_CURRENT_USER_STORAGE_KEY,
164+
"true",
165+
);
157166
} else {
158167
params.delete("mine");
168+
window.sessionStorage.removeItem(FILTER_BY_CURRENT_USER_STORAGE_KEY);
159169
}
160170
return params;
161171
},
162-
(params) => params.get("mine") === "true",
172+
(params) => ({
173+
name: params.get("name") || undefined,
174+
tags: params.getAll("tags"),
175+
tagsType: params.get("tagsType") === "and" ? "and" : "or",
176+
mine: params.get("mine") === "true",
177+
}),
163178
{
164-
initial: (initialValue) =>
165-
window.sessionStorage.getItem(FILTER_BY_CURRENT_USER_STORAGE_KEY) ===
166-
"true" ||
167-
initialValue ||
168-
false,
179+
initial: (initialValue) => ({
180+
...initialValue,
181+
mine:
182+
window.sessionStorage.getItem(FILTER_BY_CURRENT_USER_STORAGE_KEY) ===
183+
"true" ||
184+
initialValue?.["mine"] ||
185+
false,
186+
}),
169187
},
170188
);
171189

172190
private get hasFiltersSet() {
173-
return [
174-
this.filterByCurrentUser.value || undefined,
175-
this.filterByTags.value?.length || undefined,
176-
].some((v) => v !== undefined);
191+
const filterBy = this.filterBy.value;
192+
return (
193+
filterBy.name ||
194+
filterBy.tags?.length ||
195+
(filterBy.tagsType ?? DEFAULT_TAGS_TYPE) !== DEFAULT_TAGS_TYPE ||
196+
filterBy.mine
197+
);
177198
}
178199

179200
get isCrawler() {
180201
return this.appState.isCrawler;
181202
}
182203

183204
private clearFilters() {
184-
this.filterByCurrentUser.setValue(false);
185-
this.filterByTags.setValue([]);
205+
this.filterBy.setValue({});
186206
}
187207

188208
private readonly profilesTask = new Task(this, {
189-
task: async (
190-
[
191-
pagination,
192-
orderBy,
193-
filterByCurrentUser,
194-
filterByTags,
195-
filterByTagsType,
196-
],
197-
{ signal },
198-
) => {
209+
task: async ([pagination, orderBy, filterBy], { signal }) => {
199210
return this.getProfiles(
200211
{
201212
...pagination,
202-
userid: filterByCurrentUser ? this.userInfo?.id : undefined,
203-
tags: filterByTags,
204-
tagMatch: filterByTagsType,
213+
userid: filterBy.mine ? this.userInfo?.id : undefined,
214+
name: filterBy.name || undefined,
215+
tags: filterBy.tags,
216+
tagMatch: filterBy.tagsType,
205217
sortBy: orderBy.field,
206218
sortDirection:
207219
orderBy.direction === "desc"
@@ -212,39 +224,33 @@ export class BrowserProfilesList extends BtrixElement {
212224
);
213225
},
214226
args: () =>
215-
[
216-
this.pagination,
217-
this.orderBy.value,
218-
this.filterByCurrentUser.value,
219-
this.filterByTags.value,
220-
this.filterByTagsType.value,
221-
] as const,
227+
[this.pagination, this.orderBy.value, this.filterBy.value] as const,
228+
});
229+
230+
private readonly searchOptionsTask = new Task(this, {
231+
task: async (_args, { signal }) => {
232+
const data = await this.getSearchValues(signal);
233+
234+
return [...data.names.map(toSearchItem("name"))];
235+
},
236+
args: () => [] as const,
222237
});
223238

224239
protected willUpdate(changedProperties: PropertyValues): void {
225240
if (
226241
changedProperties.has("orderBy.internalValue") ||
227-
changedProperties.has("filterByCurrentUser.internalValue") ||
228-
changedProperties.has("filterByTags.internalValue") ||
229-
changedProperties.has("filterByTagsType.internalValue")
242+
changedProperties.has("filterBy.internalValue")
230243
) {
231244
this.pagination = {
232245
...this.pagination,
233246
page: 1,
234247
};
235248
}
236-
237-
if (changedProperties.has("filterByCurrentUser.internalValue")) {
238-
window.sessionStorage.setItem(
239-
FILTER_BY_CURRENT_USER_STORAGE_KEY,
240-
this.filterByCurrentUser.value.toString(),
241-
);
242-
}
243249
}
244250

245251
render() {
246-
return page(
247-
{
252+
return html`
253+
${pageHeader({
248254
title: msg("Browser Profiles"),
249255
border: false,
250256
actions: this.isCrawler
@@ -266,9 +272,9 @@ export class BrowserProfilesList extends BtrixElement {
266272
</sl-button>
267273
`
268274
: undefined,
269-
},
270-
this.renderPage,
271-
);
275+
})}
276+
${this.renderPage()}
277+
`;
272278
}
273279

274280
private readonly renderPage = () => {
@@ -377,41 +383,77 @@ export class BrowserProfilesList extends BtrixElement {
377383

378384
private renderControls() {
379385
return html`
380-
<div class="flex flex-wrap items-center justify-between gap-2">
386+
<div class="flex flex-wrap items-center gap-2 md:gap-4">
387+
<div class="grow basis-2/3">${this.renderSearch()}</div>
388+
389+
<div class="flex items-center">
390+
<label
391+
class="mr-2 whitespace-nowrap text-sm text-neutral-500"
392+
for="sort-select"
393+
>
394+
${msg("Sort by:")}
395+
</label>
396+
${this.renderSortControl()}
397+
</div>
398+
381399
<div class="flex flex-wrap items-center gap-2">
382400
<span class="whitespace-nowrap text-neutral-500">
383401
${msg("Filter by:")}
384402
</span>
385403
${this.renderFilterControls()}
386404
</div>
387-
388-
<div class="flex flex-wrap items-center gap-2">
389-
<label class="whitespace-nowrap text-neutral-500" for="sort-select">
390-
${msg("Sort by:")}
391-
</label>
392-
${this.renderSortControl()}
393-
</div>
394405
</div>
395406
`;
396407
}
397408

409+
private renderSearch() {
410+
return html`
411+
<btrix-search-combobox
412+
.searchKeys=${SEARCH_KEYS}
413+
.searchOptions=${this.searchOptionsTask.value || []}
414+
.searchByValue=${this.filterBy.value["name"] || ""}
415+
placeholder=${msg("Search by name")}
416+
@btrix-select=${(e: CustomEvent) => {
417+
const { key, value } = e.detail;
418+
this.filterBy.setValue({
419+
...this.filterBy.value,
420+
[key]: value,
421+
});
422+
}}
423+
@btrix-clear=${() => {
424+
const otherFilters = omit(SEARCH_KEYS, this.filterBy.value);
425+
this.filterBy.setValue(otherFilters);
426+
}}
427+
>
428+
</btrix-search-combobox>
429+
`;
430+
}
431+
398432
private renderFilterControls() {
433+
const filterBy = this.filterBy.value;
434+
399435
return html`
400436
<btrix-tag-filter
401437
tagType="profile"
402-
.tags=${this.filterByTags.value}
403-
.type=${this.filterByTagsType.value}
438+
.tags=${filterBy.tags}
439+
.type=${filterBy.tagsType || DEFAULT_TAGS_TYPE}
404440
@btrix-change=${(e: BtrixChangeTagFilterEvent) => {
405-
this.filterByTags.setValue(e.detail.value?.tags || []);
406-
this.filterByTagsType.setValue(e.detail.value?.type || "or");
441+
this.filterBy.setValue({
442+
...this.filterBy.value,
443+
tags: e.detail.value?.tags || [],
444+
tagsType: e.detail.value?.type || DEFAULT_TAGS_TYPE,
445+
});
407446
}}
408447
></btrix-tag-filter>
409448
410449
<btrix-filter-chip
411-
?checked=${this.filterByCurrentUser.value}
450+
?checked=${filterBy.mine}
412451
@btrix-change=${(e: BtrixFilterChipChangeEvent) => {
413452
const { checked } = e.target as FilterChip;
414-
this.filterByCurrentUser.setValue(Boolean(checked));
453+
this.filterBy.setValue({
454+
...this.filterBy.value,
455+
mine: checked,
456+
});
415457
}}
416458
>
417459
${msg("Mine")}
@@ -652,6 +694,7 @@ export class BrowserProfilesList extends BtrixElement {
652694
private async getProfiles(
653695
params: {
654696
userid?: string;
697+
name?: string;
655698
tags?: string[];
656699
tagMatch?: string;
657700
} & APIPaginationQuery &
@@ -674,4 +717,13 @@ export class BrowserProfilesList extends BtrixElement {
674717

675718
return data;
676719
}
720+
721+
private async getSearchValues(signal: AbortSignal) {
722+
return this.api.fetch<SearchValues>(
723+
`/orgs/${this.orgId}/profiles/search-values`,
724+
{
725+
signal,
726+
},
727+
);
728+
}
677729
}

0 commit comments

Comments
 (0)