diff --git a/frontend/src/layouts/pageHeader.ts b/frontend/src/layouts/pageHeader.ts index 3b6f7478e2..5bd908cb2d 100644 --- a/frontend/src/layouts/pageHeader.ts +++ b/frontend/src/layouts/pageHeader.ts @@ -123,8 +123,8 @@ export function pageHeader({ return html`
diff --git a/frontend/src/pages/org/browser-profiles-list.ts b/frontend/src/pages/org/browser-profiles-list.ts index 4ace718c76..2b12e6a852 100644 --- a/frontend/src/pages/org/browser-profiles-list.ts +++ b/frontend/src/pages/org/browser-profiles-list.ts @@ -3,6 +3,7 @@ import { Task } from "@lit/task"; import { html, type PropertyValues } from "lit"; import { customElement, state } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; +import omit from "lodash/fp/omit"; import queryString from "query-string"; import type { Profile } from "./types"; @@ -20,7 +21,7 @@ import { ClipboardController } from "@/controllers/clipboard"; import { SearchParamsValue } from "@/controllers/searchParamsValue"; import { originsWithRemainder } from "@/features/browser-profiles/templates/origins-with-remainder"; import { emptyMessage } from "@/layouts/emptyMessage"; -import { page } from "@/layouts/page"; +import { pageHeader } from "@/layouts/pageHeader"; import { OrgTab } from "@/routes"; import type { APIPaginatedList, @@ -30,6 +31,7 @@ import type { import { SortDirection as SortDirectionEnum } from "@/types/utils"; import { isApiError } from "@/utils/api"; import { isArchivingDisabled } from "@/utils/orgs"; +import { toSearchItem, type SearchValues } from "@/utils/searchValues"; const SORT_DIRECTIONS = ["asc", "desc"] as const; type SortDirection = (typeof SORT_DIRECTIONS)[number]; @@ -45,7 +47,7 @@ const sortableFields: Record< > = { name: { label: msg("Name"), - defaultDirection: "desc", + defaultDirection: "asc", }, url: { label: msg("Primary Site"), @@ -66,7 +68,17 @@ const DEFAULT_SORT_BY = { direction: sortableFields.modified.defaultDirection || "desc", } as const satisfies SortBy; const INITIAL_PAGE_SIZE = 20; -const FILTER_BY_CURRENT_USER_STORAGE_KEY = "btrix.filterByCurrentUser.crawls"; +const FILTER_BY_CURRENT_USER_STORAGE_KEY = + "btrix.filterByCurrentUser.browserProfiles"; +const SEARCH_KEYS = ["name"] as const; +const DEFAULT_TAGS_TYPE = "or"; + +type FilterBy = { + name?: string; + tags?: string[]; + tagsType?: "and" | "or"; + mine?: boolean; +}; const columnsCss = [ "min-content", // Status @@ -124,56 +136,65 @@ export class BrowserProfilesList extends BtrixElement { }, ); - private readonly filterByTags = new SearchParamsValue( - this, - (value, params) => { - params.delete("tags"); - value?.forEach((v) => { - params.append("tags", v); - }); - return params; - }, - (params) => params.getAll("tags"), - ); - - private readonly filterByTagsType = new SearchParamsValue<"and" | "or">( + private readonly filterBy = new SearchParamsValue( this, (value, params) => { - if (value === "and") { - params.set("tagsType", value); + if ("name" in value && value["name"]) { + params.set("name", value["name"]); + } else { + params.delete("name"); + } + if ("tags" in value) { + params.delete("tags"); + value["tags"]?.forEach((v) => { + params.append("tags", v); + }); + } else { + params.delete("tags"); + } + if ("tagsType" in value && value["tagsType"] === "and") { + params.set("tagsType", value["tagsType"]); } else { params.delete("tagsType"); } - return params; - }, - (params) => (params.get("tagsType") === "and" ? "and" : "or"), - ); - - private readonly filterByCurrentUser = new SearchParamsValue( - this, - (value, params) => { - if (value) { + if ("mine" in value && value["mine"]) { params.set("mine", "true"); + window.sessionStorage.setItem( + FILTER_BY_CURRENT_USER_STORAGE_KEY, + "true", + ); } else { params.delete("mine"); + window.sessionStorage.removeItem(FILTER_BY_CURRENT_USER_STORAGE_KEY); } return params; }, - (params) => params.get("mine") === "true", + (params) => ({ + name: params.get("name") || undefined, + tags: params.getAll("tags"), + tagsType: params.get("tagsType") === "and" ? "and" : "or", + mine: params.get("mine") === "true", + }), { - initial: (initialValue) => - window.sessionStorage.getItem(FILTER_BY_CURRENT_USER_STORAGE_KEY) === - "true" || - initialValue || - false, + initial: (initialValue) => ({ + ...initialValue, + mine: + window.sessionStorage.getItem(FILTER_BY_CURRENT_USER_STORAGE_KEY) === + "true" || + initialValue?.["mine"] || + false, + }), }, ); private get hasFiltersSet() { - return [ - this.filterByCurrentUser.value || undefined, - this.filterByTags.value?.length || undefined, - ].some((v) => v !== undefined); + const filterBy = this.filterBy.value; + return ( + filterBy.name || + filterBy.tags?.length || + (filterBy.tagsType ?? DEFAULT_TAGS_TYPE) !== DEFAULT_TAGS_TYPE || + filterBy.mine + ); } get isCrawler() { @@ -181,27 +202,18 @@ export class BrowserProfilesList extends BtrixElement { } private clearFilters() { - this.filterByCurrentUser.setValue(false); - this.filterByTags.setValue([]); + this.filterBy.setValue({}); } private readonly profilesTask = new Task(this, { - task: async ( - [ - pagination, - orderBy, - filterByCurrentUser, - filterByTags, - filterByTagsType, - ], - { signal }, - ) => { + task: async ([pagination, orderBy, filterBy], { signal }) => { return this.getProfiles( { ...pagination, - userid: filterByCurrentUser ? this.userInfo?.id : undefined, - tags: filterByTags, - tagMatch: filterByTagsType, + userid: filterBy.mine ? this.userInfo?.id : undefined, + name: filterBy.name || undefined, + tags: filterBy.tags, + tagMatch: filterBy.tagsType, sortBy: orderBy.field, sortDirection: orderBy.direction === "desc" @@ -212,39 +224,33 @@ export class BrowserProfilesList extends BtrixElement { ); }, args: () => - [ - this.pagination, - this.orderBy.value, - this.filterByCurrentUser.value, - this.filterByTags.value, - this.filterByTagsType.value, - ] as const, + [this.pagination, this.orderBy.value, this.filterBy.value] as const, + }); + + private readonly searchOptionsTask = new Task(this, { + task: async (_args, { signal }) => { + const data = await this.getSearchValues(signal); + + return [...data.names.map(toSearchItem("name"))]; + }, + args: () => [] as const, }); protected willUpdate(changedProperties: PropertyValues): void { if ( changedProperties.has("orderBy.internalValue") || - changedProperties.has("filterByCurrentUser.internalValue") || - changedProperties.has("filterByTags.internalValue") || - changedProperties.has("filterByTagsType.internalValue") + changedProperties.has("filterBy.internalValue") ) { this.pagination = { ...this.pagination, page: 1, }; } - - if (changedProperties.has("filterByCurrentUser.internalValue")) { - window.sessionStorage.setItem( - FILTER_BY_CURRENT_USER_STORAGE_KEY, - this.filterByCurrentUser.value.toString(), - ); - } } render() { - return page( - { + return html` + ${pageHeader({ title: msg("Browser Profiles"), border: false, actions: this.isCrawler @@ -266,9 +272,9 @@ export class BrowserProfilesList extends BtrixElement { ` : undefined, - }, - this.renderPage, - ); + })} + ${this.renderPage()} + `; } private readonly renderPage = () => { @@ -377,41 +383,77 @@ export class BrowserProfilesList extends BtrixElement { private renderControls() { return html` -
+
+
${this.renderSearch()}
+ +
+ + ${this.renderSortControl()} +
+
${msg("Filter by:")} ${this.renderFilterControls()}
- -
- - ${this.renderSortControl()} -
`; } + private renderSearch() { + return html` + { + const { key, value } = e.detail; + this.filterBy.setValue({ + ...this.filterBy.value, + [key]: value, + }); + }} + @btrix-clear=${() => { + const otherFilters = omit(SEARCH_KEYS, this.filterBy.value); + this.filterBy.setValue(otherFilters); + }} + > + + `; + } + private renderFilterControls() { + const filterBy = this.filterBy.value; + return html` { - this.filterByTags.setValue(e.detail.value?.tags || []); - this.filterByTagsType.setValue(e.detail.value?.type || "or"); + this.filterBy.setValue({ + ...this.filterBy.value, + tags: e.detail.value?.tags || [], + tagsType: e.detail.value?.type || DEFAULT_TAGS_TYPE, + }); }} > { const { checked } = e.target as FilterChip; - this.filterByCurrentUser.setValue(Boolean(checked)); + this.filterBy.setValue({ + ...this.filterBy.value, + mine: checked, + }); }} > ${msg("Mine")} @@ -649,6 +691,7 @@ export class BrowserProfilesList extends BtrixElement { private async getProfiles( params: { userid?: string; + name?: string; tags?: string[]; tagMatch?: string; } & APIPaginationQuery & @@ -671,4 +714,13 @@ export class BrowserProfilesList extends BtrixElement { return data; } + + private async getSearchValues(signal: AbortSignal) { + return this.api.fetch( + `/orgs/${this.orgId}/profiles/search-values`, + { + signal, + }, + ); + } } diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts index 336eeeb88f..bedc7b7430 100644 --- a/frontend/src/pages/org/collections-list.ts +++ b/frontend/src/pages/org/collections-list.ts @@ -150,6 +150,7 @@ export class CollectionsList extends WithSearchOrgContext(BtrixElement) {
${pageHeader({ title: msg("Collections"), + border: false, actions: this.isCrawler ? html` ` : nothing, - classNames: tw`border-b-transparent`, })}
@@ -384,7 +384,7 @@ export class CollectionsList extends WithSearchOrgContext(BtrixElement) { > { this.searchResultsOpen = false; diff --git a/frontend/src/utils/searchValues.ts b/frontend/src/utils/searchValues.ts new file mode 100644 index 0000000000..267eb5c80a --- /dev/null +++ b/frontend/src/utils/searchValues.ts @@ -0,0 +1,14 @@ +export type SearchValues = { + names: string[]; +}; + +export type SearchField = "name"; + +/** + * Convert API search values to a format compatible with Fuse collections + */ +export function toSearchItem(key: T) { + return (value: string) => ({ + [key as string]: value, + }); +}