Skip to content

Commit c0fcc20

Browse files
emma-sgSuaYoo
andauthored
Refactor workflows list to use Task & SearchParamsValue (#2898)
Co-authored-by: sua yoo <[email protected]> Co-authored-by: sua yoo <[email protected]>
1 parent 832e693 commit c0fcc20

File tree

9 files changed

+509
-445
lines changed

9 files changed

+509
-445
lines changed

frontend/src/components/ui/pagination.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export class Pagination extends LitElement {
163163
@property({ type: Number })
164164
set page(page: number) {
165165
if (page !== this._page) {
166-
this.setPage(page);
166+
this.#setPage(page);
167167
this._page = page;
168168
}
169169
}
@@ -232,7 +232,7 @@ export class Pagination extends LitElement {
232232
if (parsedPage != this._page) {
233233
const page = parsePage(this.searchParams.searchParams.get(this.name));
234234
const constrainedPage = Math.max(1, Math.min(this.pages, page));
235-
this.onPageChange(constrainedPage, { dispatch: false });
235+
this.setPage(constrainedPage, { dispatch: false });
236236
}
237237
}
238238

@@ -242,7 +242,7 @@ export class Pagination extends LitElement {
242242
(this.page > this.pages || this.page < 1)
243243
) {
244244
const constrainedPage = Math.max(1, Math.min(this.pages, this.page));
245-
this.onPageChange(constrainedPage, { dispatch: true });
245+
this.setPage(constrainedPage, { dispatch: true });
246246
}
247247

248248
if (changedProperties.get("_page")) {
@@ -337,7 +337,7 @@ export class Pagination extends LitElement {
337337
nextPage = page;
338338
}
339339
340-
this.onPageChange(nextPage);
340+
this.setPage(nextPage);
341341
}}
342342
@focus=${(e: Event) => {
343343
// Select text on focus for easy typing
@@ -390,26 +390,32 @@ export class Pagination extends LitElement {
390390
.active=${isCurrent}
391391
.size=${"x-small"}
392392
.align=${"center"}
393-
@click=${() => this.onPageChange(page)}
393+
@click=${() => this.setPage(page)}
394394
aria-disabled=${isCurrent}
395395
>${localize.number(page)}</btrix-navigation-button
396396
>
397397
</li>`;
398398
};
399399

400400
private onPrev() {
401-
this.onPageChange(this._page > 1 ? this._page - 1 : 1);
401+
this.setPage(this._page > 1 ? this._page - 1 : 1);
402402
}
403403

404404
private onNext() {
405-
this.onPageChange(this._page < this.pages ? this._page + 1 : this.pages);
405+
this.setPage(this._page < this.pages ? this._page + 1 : this.pages);
406406
}
407407

408-
private onPageChange(page: number, opts = { dispatch: true }) {
408+
setPage(
409+
page: number,
410+
options: { dispatch?: boolean; replace?: boolean } = {
411+
dispatch: true,
412+
replace: false,
413+
},
414+
) {
409415
if (this._page !== page) {
410-
this.setPage(page);
416+
this.#setPage(page, { replace: options.replace });
411417

412-
if (opts.dispatch) {
418+
if (options.dispatch) {
413419
this.dispatchEvent(
414420
new CustomEvent<PageChangeDetail>("page-change", {
415421
detail: { page: page, pages: this.pages },
@@ -421,12 +427,12 @@ export class Pagination extends LitElement {
421427
this._page = page;
422428
}
423429

424-
private setPage(page: number) {
430+
#setPage(page: number, options: { replace?: boolean } = {}) {
425431
if (!this.disablePersist) {
426432
if (page === 1) {
427-
this.searchParams.delete(this.name);
433+
this.searchParams.delete(this.name, options);
428434
} else {
429-
this.searchParams.set(this.name, page.toString());
435+
this.searchParams.set(this.name, page.toString(), options);
430436
}
431437
} else {
432438
this._page = page;

frontend/src/controllers/searchParams.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export class SearchParamsController implements ReactiveController {
1414

1515
public update(
1616
update: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),
17-
options: { replace?: boolean; data?: unknown } = { replace: false },
17+
options: { replace?: boolean; data?: unknown } = {
18+
replace: false,
19+
},
1820
) {
1921
this.prevParams = new URLSearchParams(this.searchParams);
2022
const url = new URL(location.toString());
@@ -35,7 +37,9 @@ export class SearchParamsController implements ReactiveController {
3537
public set(
3638
name: string,
3739
value: string,
38-
options: { replace?: boolean; data?: unknown } = { replace: false },
40+
options: { replace?: boolean; data?: unknown } = {
41+
replace: false,
42+
},
3943
) {
4044
this.prevParams = new URLSearchParams(this.searchParams);
4145
const url = new URL(location.toString());

frontend/src/controllers/searchParamsValue.ts

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { type ReactiveController, type ReactiveControllerHost } from "lit";
1+
import { type ReactiveController, type ReactiveElement } from "lit";
2+
import isEqual from "lodash/isEqual";
23

34
import { SearchParamsController } from "@/controllers/searchParams";
45

@@ -23,13 +24,32 @@ import { SearchParamsController } from "@/controllers/searchParams";
2324
* `setValue` will update the URL search params and push state to the history
2425
* stack, and reading it is cached, so performance should be good no matter what.
2526
*
26-
* There is currently one option available via a third parameter, `initial`,
27-
* which can be used to modify the initial value based on the URL search params
28-
* at the time of initialization. This is useful if there's a default value that
29-
* should be used if the URL search params are empty, but otherwise the value
30-
* from the URL should be prioritized, for example.
27+
* Updates to the value via `setValue` will request an update from the host
28+
* with the name set to the property name of the controller on the host followed
29+
* by `.setValue`. This allows you to use lifecycle hooks such as `willUpdate`
30+
* you would with any other state or property.
3131
*
32-
* Example usage:
32+
* Similarly, updates to the value originating from the browser (e.g. user
33+
* navigation, URL changes, etc.) will also trigger updates to the value, but
34+
* suffixed with `.value`. This allows you to use lifecycle hooks to react to
35+
* changes originating in different places differently.
36+
*
37+
* ## Options
38+
*
39+
* ### `initial`
40+
* `options.initial` can be used to modify the initial value based on the URL
41+
* search params at the time of initialization. This is useful if there's a
42+
* default value that should be used if the URL search params are empty, but
43+
* otherwise the value from the URL should be prioritized, for example.
44+
*
45+
* ### `propertyName`
46+
* `options.propertyName` can be used to specify the name of the property in the
47+
* host so that this controller can correctly request updates in the host. This
48+
* shouldn't need to be set if the controller is initialized in the host class
49+
* body, but if it's initialized in the host class constructor, it should be set
50+
* to the name of the property that holds the controller instance.
51+
*
52+
* ## Example usage
3353
* ```ts
3454
* class MyComponent extends LitElement {
3555
* query = new SearchParamsValue<string>(
@@ -45,49 +65,96 @@ import { SearchParamsController } from "@/controllers/searchParams";
4565
* (params) => params.get("q") ?? ""
4666
* );
4767
* render() {
48-
* return html`<input .value=${this.query.value} @input=${(e) => {this.query.setValue(e.target.value)}} />`;
68+
* return html`<input
69+
* .value=${this.query.value}
70+
* @input=${(e) => {this.query.setValue(e.target.value)}}
71+
* />`;
4972
* }
5073
* }
5174
* ```
5275
*/
5376
export class SearchParamsValue<T> implements ReactiveController {
54-
host: ReactiveControllerHost;
77+
readonly #host: ReactiveElement;
78+
79+
#value: T;
5580

56-
private _value: T;
81+
readonly #searchParams;
82+
readonly #encoder: (value: T, params: URLSearchParams) => URLSearchParams;
83+
readonly #decoder: (params: URLSearchParams) => T;
5784

58-
private readonly searchParams;
59-
private readonly encoder: (
60-
value: T,
61-
params: URLSearchParams,
62-
) => URLSearchParams;
63-
private readonly decoder: (params: URLSearchParams) => T;
85+
readonly #options: {
86+
initial?: (valueFromSearchParams?: T) => T;
87+
propertyKey?: PropertyKey;
88+
};
6489

6590
public get value(): T {
66-
return this._value;
91+
return this.#value;
6792
}
93+
/**
94+
* Set value and request update if deep comparison with current value is not equal.
95+
*/
6896
public setValue(value: T) {
69-
this._value = value;
70-
this.searchParams.update((params) => this.encoder(value, params));
71-
this.host.requestUpdate();
97+
if (isEqual(value, this.#value)) return;
98+
99+
const oldValue = this.#value;
100+
this.#value = value;
101+
this.#searchParams.update((params) => this.#encoder(value, params));
102+
this.#host.requestUpdate(this.#getPropertyName("setValue"), oldValue);
103+
}
104+
// Little bit hacky/metaprogramming-y, but this lets us auto-detect property
105+
// name from the host element's properties without needing to repeat the name
106+
// in a string passed into options in order to have `requestUpdate` be called
107+
// with the correct property name. Ideally, eventually we'd use a decorator,
108+
// which would allow us to avoid this hacky approach — though that might make
109+
// differentiating between internally-triggered ("setValue") and
110+
// externally-triggered ("value") updates a bit more complex.
111+
#getPropertyName(type: "value" | "setValue"): PropertyKey | undefined {
112+
// Use explicit property name if provided
113+
if (this.#options.propertyKey)
114+
return `${this.#options.propertyKey.toString()}.${type}`;
115+
116+
try {
117+
for (const prop of Reflect.ownKeys(this.#host)) {
118+
const descriptor = Object.getOwnPropertyDescriptor(this.#host, prop);
119+
if (descriptor && descriptor.value === this) {
120+
this.#options.propertyKey = prop;
121+
return `${prop.toString()}.${type}`;
122+
}
123+
}
124+
} catch (error) {
125+
console.debug(
126+
"SearchParamsValue: Failed to auto-detect property name",
127+
error,
128+
);
129+
}
130+
return undefined;
72131
}
73132

74133
constructor(
75-
host: ReactiveControllerHost,
134+
host: ReactiveElement,
76135
encoder: (value: T, params: URLSearchParams) => URLSearchParams,
77136
decoder: (params: URLSearchParams) => T,
78-
options?: { initial?: (valueFromSearchParams?: T) => T },
137+
options?: {
138+
initial?: (valueFromSearchParams?: T) => T;
139+
propertyKey?: PropertyKey;
140+
},
79141
) {
80-
(this.host = host).addController(this);
81-
this.encoder = encoder;
82-
this.decoder = decoder;
83-
this.searchParams = new SearchParamsController(this.host, (params) => {
84-
this._value = this.decoder(params);
85-
this.host.requestUpdate();
142+
(this.#host = host).addController(this);
143+
this.#encoder = encoder;
144+
this.#decoder = decoder;
145+
this.#options = options || {};
146+
this.#searchParams = new SearchParamsController(this.#host, (params) => {
147+
const oldValue = this.#value;
148+
this.#value = this.#decoder(params);
149+
150+
if (isEqual(oldValue, this.#value)) return;
151+
152+
this.#host.requestUpdate(this.#getPropertyName("value"), oldValue);
86153
});
87154

88-
this._value = options?.initial
89-
? options.initial(this.decoder(this.searchParams.searchParams))
90-
: this.decoder(this.searchParams.searchParams);
155+
this.#value = options?.initial
156+
? options.initial(this.#decoder(this.#searchParams.searchParams))
157+
: this.#decoder(this.#searchParams.searchParams);
91158
}
92159

93160
hostConnected() {}

frontend/src/features/crawl-workflows/workflow-profile-filter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export class WorkflowProfileFilter extends BtrixElement {
7272
}
7373

7474
protected updated(changedProperties: PropertyValues<this>): void {
75-
if (changedProperties.has("selected")) {
75+
if (changedProperties.get("selected")) {
7676
const selectedProfiles = [];
7777

7878
for (const [profile, value] of this.selected) {
@@ -339,7 +339,7 @@ export class WorkflowProfileFilter extends BtrixElement {
339339
@sl-change=${async (e: SlChangeEvent) => {
340340
const { checked, value } = e.target as SlCheckbox;
341341
342-
this.selected = new Map(this.selected.set(value, checked));
342+
this.selected = new Map([...this.selected, [value, checked]]);
343343
}}
344344
>
345345
${repeat(

frontend/src/features/crawl-workflows/workflow-schedule-filter.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { localized, msg } from "@lit/localize";
22
import type { SlChangeEvent, SlRadioGroup } from "@shoelace-style/shoelace";
3-
import { html, nothing, type PropertyValues } from "lit";
3+
import { html, nothing } from "lit";
44
import { customElement, property } from "lit/decorators.js";
55

66
import { BtrixElement } from "@/classes/BtrixElement";
@@ -25,19 +25,6 @@ export class WorkflowScheduleFilter extends BtrixElement {
2525
@property({ type: Boolean })
2626
schedule?: boolean;
2727

28-
protected updated(changedProperties: PropertyValues<this>): void {
29-
if (changedProperties.has("schedule")) {
30-
this.dispatchEvent(
31-
new CustomEvent<BtrixChangeWorkflowScheduleFilterEvent["detail"]>(
32-
"btrix-change",
33-
{
34-
detail: { value: this.schedule },
35-
},
36-
),
37-
);
38-
}
39-
}
40-
4128
render() {
4229
return html`
4330
<btrix-filter-chip
@@ -73,6 +60,14 @@ export class WorkflowScheduleFilter extends BtrixElement {
7360
class="part-[label]:px-0"
7461
@click=${() => {
7562
this.schedule = undefined;
63+
64+
this.dispatchEvent(
65+
new CustomEvent<
66+
BtrixChangeWorkflowScheduleFilterEvent["detail"]
67+
>("btrix-change", {
68+
detail: { value: this.schedule },
69+
}),
70+
);
7671
}}
7772
>${msg("Clear")}</sl-button
7873
>`
@@ -101,6 +96,14 @@ export class WorkflowScheduleFilter extends BtrixElement {
10196
this.schedule = undefined;
10297
break;
10398
}
99+
100+
this.dispatchEvent(
101+
new CustomEvent<
102+
BtrixChangeWorkflowScheduleFilterEvent["detail"]
103+
>("btrix-change", {
104+
detail: { value: this.schedule },
105+
}),
106+
);
104107
}}
105108
>
106109
<sl-radio

frontend/src/features/crawl-workflows/workflow-tag-filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class WorkflowTagFilter extends BtrixElement {
7474
}
7575

7676
protected updated(changedProperties: PropertyValues<this>): void {
77-
if (changedProperties.has("selected") || changedProperties.has("type")) {
77+
if (changedProperties.get("selected") || changedProperties.get("type")) {
7878
const selectedTags = Array.from(this.selected.entries())
7979
.filter(([_tag, selected]) => selected)
8080
.map(([tag]) => tag);

0 commit comments

Comments
 (0)