Skip to content

Commit 6013d6a

Browse files
committed
create separate filter
1 parent fb86353 commit 6013d6a

File tree

5 files changed

+287
-26
lines changed

5 files changed

+287
-26
lines changed

frontend/src/features/archived-items/archived-item-state-filter.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { CrawlStatus } from "./crawl-status";
2121

2222
import { BtrixElement } from "@/classes/BtrixElement";
2323
import type { BtrixChangeEvent } from "@/events/btrix-change";
24-
import { CRAWL_STATES, type CrawlState } from "@/types/crawlState";
24+
import { type CrawlState } from "@/types/crawlState";
2525
import { finishedCrawlStates } from "@/utils/crawler";
2626
import { isNotEqual } from "@/utils/is-not-equal";
2727
import { tw } from "@/utils/tailwind";
@@ -42,9 +42,6 @@ export class ArchivedItemStateFilter extends BtrixElement {
4242
@property({ type: Array })
4343
states?: CrawlState[];
4444

45-
@property({ type: Boolean })
46-
showUnfinishedStates = false;
47-
4845
@state()
4946
private searchString = "";
5047

@@ -54,19 +51,11 @@ export class ArchivedItemStateFilter extends BtrixElement {
5451
@queryAll("sl-checkbox")
5552
private readonly checkboxes!: NodeListOf<SlCheckbox>;
5653

57-
private fuse = new Fuse<CrawlState>(finishedCrawlStates);
54+
private readonly fuse = new Fuse<CrawlState>(finishedCrawlStates);
5855

5956
@state({ hasChanged: isNotEqual })
6057
selected = new Map<CrawlState, boolean>();
6158

62-
connectedCallback(): void {
63-
super.connectedCallback();
64-
65-
if (this.showUnfinishedStates) {
66-
this.fuse = new Fuse(CRAWL_STATES);
67-
}
68-
}
69-
7059
protected willUpdate(changedProperties: PropertyValues<this>): void {
7160
if (changedProperties.has("states")) {
7261
if (this.states) {
@@ -96,9 +85,7 @@ export class ArchivedItemStateFilter extends BtrixElement {
9685
render() {
9786
const options = this.searchString
9887
? this.fuse.search(this.searchString)
99-
: (this.showUnfinishedStates ? CRAWL_STATES : finishedCrawlStates).map(
100-
(state) => ({ item: state }),
101-
);
88+
: finishedCrawlStates.map((state) => ({ item: state }));
10289
return html`
10390
<btrix-filter-chip
10491
?checked=${!!this.states?.length}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { localized, msg, str } from "@lit/localize";
2+
import type {
3+
SlChangeEvent,
4+
SlCheckbox,
5+
SlInput,
6+
SlInputEvent,
7+
} from "@shoelace-style/shoelace";
8+
import Fuse from "fuse.js";
9+
import { html, type PropertyValues } from "lit";
10+
import {
11+
customElement,
12+
property,
13+
query,
14+
queryAll,
15+
state,
16+
} from "lit/decorators.js";
17+
import { repeat } from "lit/directives/repeat.js";
18+
import { isFocusable } from "tabbable";
19+
20+
import { CrawlStatus } from "../archived-items/crawl-status";
21+
22+
import { BtrixElement } from "@/classes/BtrixElement";
23+
import type { BtrixChangeEvent } from "@/events/btrix-change";
24+
import { CRAWL_STATES, type CrawlState } from "@/types/crawlState";
25+
import { isNotEqual } from "@/utils/is-not-equal";
26+
import { tw } from "@/utils/tailwind";
27+
28+
const MAX_STATES_IN_LABEL = 2;
29+
30+
type ChangeCrawlStateEventDetails = CrawlState[];
31+
32+
export type BtrixChangeCrawlStateFilterEvent =
33+
BtrixChangeEvent<ChangeCrawlStateEventDetails>;
34+
35+
/**
36+
* @fires btrix-change
37+
*/
38+
@customElement("btrix-crawl-state-filter")
39+
@localized()
40+
export class ArchivedItemStateFilter extends BtrixElement {
41+
@property({ type: Array })
42+
states?: CrawlState[];
43+
44+
@state()
45+
private searchString = "";
46+
47+
@query("sl-input")
48+
private readonly input?: SlInput | null;
49+
50+
@queryAll("sl-checkbox")
51+
private readonly checkboxes!: NodeListOf<SlCheckbox>;
52+
53+
private readonly fuse = new Fuse<CrawlState>(CRAWL_STATES);
54+
55+
@state({ hasChanged: isNotEqual })
56+
selected = new Map<CrawlState, boolean>();
57+
58+
protected willUpdate(changedProperties: PropertyValues<this>): void {
59+
if (changedProperties.has("states")) {
60+
if (this.states) {
61+
this.selected = new Map(this.states.map((state) => [state, true]));
62+
} else if (changedProperties.get("states")) {
63+
this.selected = new Map();
64+
}
65+
}
66+
}
67+
68+
protected updated(changedProperties: PropertyValues<this>): void {
69+
if (changedProperties.has("selected")) {
70+
this.dispatchEvent(
71+
new CustomEvent<
72+
BtrixChangeEvent<ChangeCrawlStateEventDetails>["detail"]
73+
>("btrix-change", {
74+
detail: {
75+
value: Array.from(this.selected.entries())
76+
.filter(([_tag, selected]) => selected)
77+
.map(([tag]) => tag),
78+
},
79+
}),
80+
);
81+
}
82+
}
83+
84+
render() {
85+
const options = this.searchString
86+
? this.fuse.search(this.searchString)
87+
: CRAWL_STATES.map((state) => ({ item: state }));
88+
return html`
89+
<btrix-filter-chip
90+
?checked=${!!this.states?.length}
91+
selectFromDropdown
92+
stayOpenOnChange
93+
@sl-after-show=${() => {
94+
if (this.input && !this.input.disabled) {
95+
this.input.focus();
96+
}
97+
}}
98+
@sl-after-hide=${() => {
99+
this.searchString = "";
100+
}}
101+
>
102+
${this.states?.length
103+
? html`<span class="opacity-75">${msg("Status")}</span>
104+
${this.renderStatesInLabel(this.states)}`
105+
: msg("Status")}
106+
107+
<div
108+
slot="dropdown-content"
109+
class="flex max-h-[var(--auto-size-available-height)] max-w-[var(--auto-size-available-width)] flex-col overflow-hidden rounded border bg-white text-left"
110+
>
111+
<header
112+
class="flex-shrink-0 flex-grow-0 overflow-hidden rounded-t border-b bg-white pb-3"
113+
>
114+
<sl-menu-label
115+
class="min-h-[var(--sl-input-height-small)] part-[base]:flex part-[base]:items-center part-[base]:justify-between part-[base]:gap-4 part-[base]:px-3"
116+
>
117+
<div
118+
id="tag-list-label"
119+
class="leading-[var(--sl-input-height-small)]"
120+
>
121+
${msg("Filter by Status")}
122+
</div>
123+
${this.states?.length
124+
? html`<sl-button
125+
variant="text"
126+
size="small"
127+
class="part-[label]:px-0"
128+
@click=${() => {
129+
this.checkboxes.forEach((checkbox) => {
130+
checkbox.checked = false;
131+
});
132+
133+
this.dispatchEvent(
134+
new CustomEvent<
135+
BtrixChangeEvent<ChangeCrawlStateEventDetails>["detail"]
136+
>("btrix-change", {
137+
detail: {
138+
value: [],
139+
},
140+
}),
141+
);
142+
}}
143+
>${msg("Clear")}</sl-button
144+
>`
145+
: html`<span class="opacity-50">${msg("Any")}</span>`}
146+
</sl-menu-label>
147+
148+
<div class="flex gap-2 px-3">${this.renderSearch()}</div>
149+
</header>
150+
151+
${options.length > 0
152+
? this.renderList(options)
153+
: html`<div class="p-3 text-neutral-500">
154+
${msg("No matching states found.")}
155+
</div>`}
156+
</div>
157+
</btrix-filter-chip>
158+
`;
159+
}
160+
161+
private renderStatesInLabel(states: string[]) {
162+
const formatter = this.localize.list(
163+
states.length > MAX_STATES_IN_LABEL
164+
? [
165+
...states.slice(0, MAX_STATES_IN_LABEL),
166+
msg(
167+
str`${this.localize.number(states.length - MAX_STATES_IN_LABEL)} more`,
168+
),
169+
]
170+
: states,
171+
{ type: "disjunction" },
172+
);
173+
174+
return formatter.map((part, index, array) =>
175+
part.type === "literal"
176+
? html`<span class="opacity-75">${part.value}</span>`
177+
: states.length > MAX_STATES_IN_LABEL && index === array.length - 1
178+
? html`<span class="text-primary-500"> ${part.value} </span>`
179+
: html`<span>${this.renderLabel(part.value as CrawlState)}</span>`,
180+
);
181+
}
182+
183+
private renderLabel(state: CrawlState) {
184+
const { icon, label } = CrawlStatus.getContent({
185+
state,
186+
originalState: state,
187+
});
188+
return html`<span
189+
class=${tw`inline-flex items-baseline gap-1 [&_sl-icon]:relative [&_sl-icon]:bottom-[-0.05rem]`}
190+
>${icon}${label}</span
191+
>`;
192+
}
193+
194+
private renderSearch() {
195+
return html`
196+
<label for="state-search" class="sr-only"
197+
>${msg("Filter statuses")}</label
198+
>
199+
<sl-input
200+
class="min-w-[30ch]"
201+
id="state-search"
202+
role="combobox"
203+
aria-autocomplete="list"
204+
aria-expanded="true"
205+
aria-controls="state-listbox"
206+
aria-activedescendant="state-selected-option"
207+
value=${this.searchString}
208+
placeholder=${msg("Search for status")}
209+
size="small"
210+
@sl-input=${(e: SlInputEvent) =>
211+
(this.searchString = (e.target as SlInput).value)}
212+
@keydown=${(e: KeyboardEvent) => {
213+
// Prevent moving to next tabbable element since dropdown should close
214+
if (e.key === "Tab") e.preventDefault();
215+
if (e.key === "ArrowDown" && isFocusable(this.checkboxes[0])) {
216+
this.checkboxes[0].focus();
217+
}
218+
}}
219+
>
220+
<sl-icon slot="prefix" name="search"></sl-icon>
221+
</sl-input>
222+
`;
223+
}
224+
225+
private renderList(opts: { item: CrawlState }[]) {
226+
const state = (state: CrawlState) => {
227+
const checked = this.selected.get(state) === true;
228+
229+
const { icon, label } = CrawlStatus.getContent({
230+
state,
231+
originalState: state,
232+
});
233+
234+
return html`
235+
<li role="option" aria-checked=${checked}>
236+
<sl-checkbox
237+
class="w-full part-[label]:flex part-[base]:w-full part-[label]:w-full part-[label]:items-center part-[label]:justify-between part-[base]:rounded part-[base]:p-2 part-[base]:hover:bg-primary-50"
238+
value=${state}
239+
?checked=${checked}
240+
>
241+
<span class="contents"
242+
>${label}<span class="ml-2 flex place-content-center"
243+
>${icon}</span
244+
></span
245+
>
246+
</sl-checkbox>
247+
</li>
248+
`;
249+
};
250+
251+
return html`
252+
<ul
253+
id="state-listbox"
254+
class="flex-1 overflow-auto p-1"
255+
role="listbox"
256+
aria-labelledby="tag-list-label"
257+
aria-multiselectable="true"
258+
@sl-change=${async (e: SlChangeEvent) => {
259+
const { checked, value } = e.target as SlCheckbox;
260+
261+
this.selected = new Map([
262+
...this.selected,
263+
[value as CrawlState, checked],
264+
]);
265+
}}
266+
>
267+
${repeat(
268+
opts,
269+
({ item }) => item,
270+
({ item }) => state(item),
271+
)}
272+
</ul>
273+
`;
274+
}
275+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
import("./crawl-list");
2+
import("./crawl-state-filter");

frontend/src/pages/org/crawls.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import {
2323
import type { SelectEvent } from "@/components/ui/search-combobox";
2424
import { ClipboardController } from "@/controllers/clipboard";
2525
import { SearchParamsValue } from "@/controllers/searchParamsValue";
26-
import { type BtrixChangeArchivedItemStateFilterEvent } from "@/features/archived-items/archived-item-state-filter";
2726
import {
2827
WorkflowSearch,
2928
type SearchFields,
3029
} from "@/features/crawl-workflows/workflow-search";
30+
import { type BtrixChangeCrawlStateFilterEvent } from "@/features/crawls/crawl-state-filter";
3131
import { OrgTab } from "@/routes";
3232
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
3333
import type { CrawlState } from "@/types/crawlState";
@@ -483,16 +483,15 @@ export class OrgCrawls extends BtrixElement {
483483
<span class="whitespace-nowrap text-sm text-neutral-500">
484484
${msg("Filter by:")}
485485
</span>
486-
<btrix-archived-item-state-filter
486+
<btrix-crawl-state-filter
487487
.states=${this.filterBy.value.state}
488-
showUnfinishedStates
489-
@btrix-change=${(e: BtrixChangeArchivedItemStateFilterEvent) => {
488+
@btrix-change=${(e: BtrixChangeCrawlStateFilterEvent) => {
490489
this.filterBy.setValue({
491490
...this.filterBy.value,
492491
state: e.detail.value,
493492
});
494493
}}
495-
></btrix-archived-item-state-filter>
494+
></btrix-crawl-state-filter>
496495
497496
${this.userInfo?.id
498497
? html`<btrix-filter-chip

frontend/src/pages/org/workflow-detail.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import { BtrixElement } from "@/classes/BtrixElement";
1919
import type { Alert } from "@/components/ui/alert";
2020
import { parsePage, type PageChangeEvent } from "@/components/ui/pagination";
2121
import { ClipboardController } from "@/controllers/clipboard";
22-
import type { BtrixChangeArchivedItemStateFilterEvent } from "@/features/archived-items/archived-item-state-filter";
2322
import { CrawlStatus } from "@/features/archived-items/crawl-status";
2423
import { ExclusionEditor } from "@/features/crawl-workflows/exclusion-editor";
2524
import { ShareableNotice } from "@/features/crawl-workflows/templates/shareable-notice";
2625
import {
2726
Action,
2827
type BtrixSelectActionEvent,
2928
} from "@/features/crawl-workflows/workflow-action-menu/types";
29+
import type { BtrixChangeCrawlStateFilterEvent } from "@/features/crawls/crawl-state-filter";
3030
import { pageError } from "@/layouts/pageError";
3131
import { pageNav, type Breadcrumb } from "@/layouts/pageHeader";
3232
import { WorkflowTab } from "@/routes";
@@ -1136,17 +1136,16 @@ export class WorkflowDetail extends BtrixElement {
11361136
<span class="whitespace-nowrap text-sm text-neutral-500">
11371137
${msg("Filter by:")}
11381138
</span>
1139-
<btrix-archived-item-state-filter
1139+
<btrix-crawl-state-filter
11401140
.states=${this.crawlsParams.state}
1141-
showUnfinishedStates
1142-
@btrix-change=${(e: BtrixChangeArchivedItemStateFilterEvent) => {
1141+
@btrix-change=${(e: BtrixChangeCrawlStateFilterEvent) => {
11431142
this.crawlsParams = {
11441143
...this.crawlsParams,
11451144
page: 1,
11461145
state: e.detail.value,
11471146
};
11481147
}}
1149-
></btrix-archived-item-state-filter>
1148+
></btrix-crawl-state-filter>
11501149
${when(
11511150
this.crawlsParams.state?.length,
11521151
() => html`

0 commit comments

Comments
 (0)