Skip to content

Commit 43c4dc2

Browse files
authored
task: Add dedupe form control to workflow (#2932)
- Adds new "Deduplication" section to workflows - Allows users to use a collection for deduplication - Various refactors for consistency
1 parent 2fcf6d7 commit 43c4dc2

27 files changed

+767
-169
lines changed

frontend/docs/docs/user-guide/workflow-setup.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,10 @@ You can use a tool like [crontab.guru](https://crontab.guru/) to check Cron synt
392392

393393
Cron schedules are always in [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time).
394394

395+
## Deduplication
396+
397+
Prevent duplicate content from being crawled and stored.
398+
395399
## Collections
396400

397401
### Auto-Add to Collection

frontend/src/components/ui/combobox.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ export class Combobox extends LitElement {
7979
@keyup=${this.onKeyup}
8080
@focusout=${this.onFocusout}
8181
>
82-
<div slot="anchor">
83-
<slot></slot>
84-
</div>
82+
<slot slot="anchor"></slot>
8583
<div
8684
id="dropdown"
8785
class="dropdown hidden"

frontend/src/components/ui/config-details.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { localized, msg, str } from "@lit/localize";
22
import ISO6391 from "iso-639-1";
33
import { html, nothing, type TemplateResult } from "lit";
44
import { customElement, property, state } from "lit/decorators.js";
5+
import { ifDefined } from "lit/directives/if-defined.js";
56
import { when } from "lit/directives/when.js";
67
import capitalize from "lodash/fp/capitalize";
78

@@ -171,7 +172,7 @@ export class ConfigDetails extends BtrixElement {
171172
heading: sectionStrings.behaviors,
172173
renderDescItems: (seedsConfig) => html`
173174
${this.renderSetting(
174-
labelFor.behaviors,
175+
sectionStrings.behaviors,
175176
[
176177
seedsConfig?.behaviors?.includes(Behavior.AutoScroll) &&
177178
labelFor.autoscrollBehavior,
@@ -194,7 +195,7 @@ export class ConfigDetails extends BtrixElement {
194195
),
195196
)}
196197
${this.renderSetting(
197-
labelFor.customBehaviors,
198+
labelFor.customBehavior,
198199
seedsConfig?.customBehaviors.length
199200
? html`
200201
<btrix-custom-behaviors-table
@@ -309,6 +310,16 @@ export class ConfigDetails extends BtrixElement {
309310
)}
310311
`,
311312
})}
313+
${this.renderSection({
314+
id: "deduplication",
315+
heading: sectionStrings.deduplication,
316+
renderDescItems: () => html`
317+
${this.renderSetting(
318+
html`<span class="mb-1 inline-block">${labelFor.dedupeType}</span>`,
319+
crawlConfig?.dedupeCollId ? msg("Enabled") : msg("Disabled"),
320+
)}
321+
`,
322+
})}
312323
${when(!this.hideMetadata, () =>
313324
this.renderSection({
314325
id: "collection",
@@ -321,6 +332,7 @@ export class ConfigDetails extends BtrixElement {
321332
crawlConfig?.autoAddCollections.length
322333
? html`<btrix-linked-collections
323334
.collections=${crawlConfig.autoAddCollections}
335+
dedupeId=${ifDefined(crawlConfig.dedupeCollId || undefined)}
324336
></btrix-linked-collections>`
325337
: undefined,
326338
)}
@@ -575,7 +587,7 @@ export class ConfigDetails extends BtrixElement {
575587
const selectors = this.crawlConfig?.config.selectLinks || [];
576588

577589
return this.renderSetting(
578-
labelFor.selectLink,
590+
labelFor.selectLinks,
579591
selectors.length
580592
? html`
581593
<div class="mb-2">

frontend/src/components/ui/search-combobox.ts

Lines changed: 117 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
import { localized, msg } from "@lit/localize";
2-
import type { SlInput, SlMenuItem } from "@shoelace-style/shoelace";
2+
import type {
3+
SlClearEvent,
4+
SlInput,
5+
SlMenuItem,
6+
} from "@shoelace-style/shoelace";
37
import Fuse from "fuse.js";
4-
import { html, LitElement, nothing, type PropertyValues } from "lit";
8+
import { html, nothing, type PropertyValues } from "lit";
59
import { customElement, property, query, state } from "lit/decorators.js";
10+
import { ifDefined } from "lit/directives/if-defined.js";
611
import { when } from "lit/directives/when.js";
712
import debounce from "lodash/fp/debounce";
813

14+
import { TailwindElement } from "@/classes/TailwindElement";
15+
import { defaultFuseOptions } from "@/context/search-org/connectFuse";
16+
import type { BtrixSelectEvent } from "@/events/btrix-select";
917
import { type UnderlyingFunction } from "@/types/utils";
18+
import { isNotEqual } from "@/utils/is-not-equal";
1019

11-
type SelectEventDetail<T> = {
20+
export type BtrixSearchComboboxSelectEvent = BtrixSelectEvent<{
1221
key: string | null;
13-
value?: T;
14-
};
15-
export type SelectEvent<T> = CustomEvent<SelectEventDetail<T>>;
22+
value: string;
23+
}>;
1624

1725
const MIN_SEARCH_LENGTH = 2;
18-
const MAX_SEARCH_RESULTS = 10;
26+
const MAX_SEARCH_RESULTS = 5;
1927

2028
/**
2129
* Fuzzy search through list of options
2230
*
23-
* @event btrix-select
24-
* @event btrix-clear
31+
* @fires btrix-select
32+
* @fires btrix-clear
2533
*/
2634
@customElement("btrix-search-combobox")
2735
@localized()
28-
export class SearchCombobox<T> extends LitElement {
36+
export class SearchCombobox<T> extends TailwindElement {
2937
@property({ type: Array })
3038
searchOptions: T[] = [];
3139

32-
@property({ type: Array })
40+
@property({ type: Array, hasChanged: isNotEqual })
3341
searchKeys: string[] = [];
3442

35-
@property({ type: Object })
43+
@property({ type: Object, hasChanged: isNotEqual })
3644
keyLabels?: { [key: string]: string };
3745

3846
@property({ type: String })
@@ -44,6 +52,30 @@ export class SearchCombobox<T> extends LitElement {
4452
@property({ type: String })
4553
searchByValue = "";
4654

55+
@property({ type: String })
56+
label?: string;
57+
58+
@property({ type: String })
59+
name?: string;
60+
61+
@property({ type: Number })
62+
maxlength?: number;
63+
64+
@property({ type: Boolean })
65+
required?: boolean;
66+
67+
@property({ type: Boolean })
68+
disabled?: boolean;
69+
70+
@property({ type: String })
71+
size?: SlInput["size"];
72+
73+
@property({ type: Boolean })
74+
disableSearch = false;
75+
76+
@property({ type: Boolean })
77+
createNew = false;
78+
4779
private get hasSearchStr() {
4880
return this.searchByValue.length >= MIN_SEARCH_LENGTH;
4981
}
@@ -54,10 +86,9 @@ export class SearchCombobox<T> extends LitElement {
5486
@query("sl-input")
5587
private readonly input!: SlInput;
5688

57-
private fuse = new Fuse<T>([], {
58-
keys: [],
59-
threshold: 0.2, // stricter; default is 0.6
60-
includeMatches: true,
89+
protected fuse = new Fuse<T>(this.searchOptions, {
90+
...defaultFuseOptions,
91+
keys: this.searchKeys,
6192
});
6293

6394
disconnectedCallback(): void {
@@ -72,7 +103,7 @@ export class SearchCombobox<T> extends LitElement {
72103
}
73104
if (changedProperties.has("searchKeys")) {
74105
this.onSearchInput.cancel();
75-
this.fuse = new Fuse<T>([], {
106+
this.fuse = new Fuse<T>(this.searchOptions, {
76107
...(
77108
this.fuse as unknown as {
78109
options: ConstructorParameters<typeof Fuse>[1];
@@ -106,21 +137,27 @@ export class SearchCombobox<T> extends LitElement {
106137
this.searchByValue = value;
107138
await this.updateComplete;
108139
this.dispatchEvent(
109-
new CustomEvent<SelectEventDetail<T>>("btrix-select", {
110-
detail: {
111-
key: key ?? null,
112-
value: value as T,
140+
new CustomEvent<BtrixSearchComboboxSelectEvent["detail"]>(
141+
"btrix-select",
142+
{
143+
detail: {
144+
item: { key: key ?? null, value: value },
145+
},
113146
},
114-
}),
147+
),
115148
);
116149
}}
117150
>
118151
<sl-input
119-
size="small"
120152
placeholder=${this.placeholder}
153+
label=${ifDefined(this.label)}
154+
size=${ifDefined(this.size)}
155+
maxlength=${ifDefined(this.maxlength)}
156+
?disabled=${this.disabled}
121157
clearable
122158
value=${this.searchByValue}
123-
@sl-clear=${() => {
159+
@sl-clear=${(e: SlClearEvent) => {
160+
e.stopPropagation();
124161
this.searchResultsOpen = false;
125162
this.onSearchInput.cancel();
126163
this.dispatchEvent(new CustomEvent("btrix-clear"));
@@ -139,7 +176,10 @@ export class SearchCombobox<T> extends LitElement {
139176
style="margin-left: var(--sl-spacing-3x-small)"
140177
>${this.keyLabels![this.selectedKey!]}</sl-tag
141178
>`,
142-
() => html`<sl-icon name="search" slot="prefix"></sl-icon>`,
179+
() =>
180+
this.disableSearch
181+
? nothing
182+
: html`<sl-icon name="search" slot="prefix"></sl-icon>`,
143183
)}
144184
</sl-input>
145185
${this.renderSearchResults()}
@@ -160,38 +200,65 @@ export class SearchCombobox<T> extends LitElement {
160200
limit: MAX_SEARCH_RESULTS,
161201
});
162202

163-
if (!searchResults.length) {
164-
return html`
165-
<sl-menu-item slot="menu-item" disabled
166-
>${msg("No matches found.")}</sl-menu-item
167-
>
168-
`;
169-
}
203+
const match = ({ key, value }: Fuse.FuseResultMatch) => {
204+
if (!!key && !!value) {
205+
const keyLabel = this.keyLabels?.[key];
206+
return html`
207+
<sl-menu-item slot="menu-item" data-key=${key} value=${value}>
208+
${keyLabel
209+
? html`<sl-tag slot="prefix" size="small" pill
210+
>${keyLabel}</sl-tag
211+
>`
212+
: nothing}
213+
${value}
214+
</sl-menu-item>
215+
`;
216+
}
217+
return nothing;
218+
};
219+
220+
const newName = this.searchByValue.trim();
221+
// Hide "Add" if there's a result that matches the entire string (case insensitive)
222+
const showCreateNew =
223+
this.createNew &&
224+
!searchResults.some((res) =>
225+
res.matches?.some(
226+
({ value }) =>
227+
value && value.toLocaleLowerCase() === newName.toLocaleLowerCase(),
228+
),
229+
);
170230

171231
return html`
172-
${searchResults.map(({ matches }) =>
173-
matches?.map(({ key, value }) => {
174-
if (!!key && !!value) {
175-
const keyLabel = this.keyLabels?.[key];
176-
return html`
177-
<sl-menu-item slot="menu-item" data-key=${key} value=${value}>
178-
${keyLabel
179-
? html`<sl-tag slot="prefix" size="small" pill
180-
>${keyLabel}</sl-tag
181-
>`
182-
: nothing}
183-
${value}
184-
</sl-menu-item>
185-
`;
186-
}
187-
return nothing;
188-
}),
232+
${when(
233+
searchResults.length,
234+
() => html`
235+
${searchResults.map(({ matches }) => matches?.map(match))}
236+
${showCreateNew
237+
? html`<sl-divider slot="menu-item"></sl-divider>`
238+
: nothing}
239+
`,
240+
() =>
241+
showCreateNew
242+
? nothing
243+
: html`
244+
<sl-menu-item slot="menu-item" disabled
245+
>${msg("No matches found.")}</sl-menu-item
246+
>
247+
`,
189248
)}
249+
${when(showCreateNew, () => {
250+
return html`
251+
<sl-menu-item slot="menu-item" value=${newName}>
252+
<span class="text-neutral-500">${msg("Create")}</span
253+
>${newName}<span class="text-neutral-500"></span>
254+
</sl-menu-item>
255+
`;
256+
})}
190257
`;
191258
}
192259

193260
private readonly onSearchInput = debounce(150)(() => {
194-
this.searchByValue = this.input.value.trim();
261+
this.searchByValue = this.input.value;
195262

196263
if (!this.searchResultsOpen && this.hasSearchStr) {
197264
this.searchResultsOpen = true;

frontend/src/context/search-org/WithSearchOrgContext.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { ContextConsumer } from "@lit/context";
1+
import { ContextConsumer, type ContextCallback } from "@lit/context";
22
import type { LitElement } from "lit";
33
import type { Constructor } from "type-fest";
44

5-
import { searchOrgContext, searchOrgInitialValue } from "./search-org";
5+
import {
6+
searchOrgContext,
7+
searchOrgInitialValue,
8+
type SearchOrgContext,
9+
} from "./search-org";
610
import type { SearchOrgKey } from "./types";
711

812
/**
@@ -17,8 +21,14 @@ export const WithSearchOrgContext = <T extends Constructor<LitElement>>(
1721
superClass: T,
1822
) =>
1923
class extends superClass {
24+
protected searchOrgContextUpdated: ContextCallback<SearchOrgContext> =
25+
() => {};
26+
2027
readonly #searchOrg = new ContextConsumer(this, {
2128
context: searchOrgContext,
29+
callback: (value) => {
30+
this.searchOrgContextUpdated(value);
31+
},
2232
subscribe: true,
2333
});
2434

frontend/src/context/search-org/connectFuse.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import Fuse from "fuse.js";
55

66
import { searchQueryKeys, type SearchQuery } from "./types";
77

8+
export const defaultFuseOptions: Fuse.IFuseOptions<unknown> = {
9+
threshold: 0.3, // stricter; default is 0.6
10+
useExtendedSearch: true,
11+
includeMatches: true,
12+
includeScore: true,
13+
shouldSort: false,
14+
};
15+
816
export function connectFuse(values: SearchQuery[]) {
917
return new Fuse(values, {
18+
...defaultFuseOptions,
1019
keys: searchQueryKeys,
11-
threshold: 0.3,
12-
useExtendedSearch: true,
1320
});
1421
}

frontend/src/events/btrix-select.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ export type BtrixSelectEvent<T = unknown> = CustomEvent<{ item: T }>;
22

33
declare global {
44
interface GlobalEventHandlersEventMap {
5-
"btrix-select": BtrixSelectEvent;
5+
"btrix-select": BtrixSelectEvent<never>;
66
}
77
}

frontend/src/features/archived-items/file-uploader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export class FileUploader extends BtrixElement {
237237
></btrix-tag-input>
238238
<div class="mt-4">
239239
<btrix-collections-add
240-
.initialCollections=${this.collectionIds}
240+
.collectionIds=${this.collectionIds}
241241
label=${msg("Add to Collection")}
242242
@collections-change=${(e: CollectionsChangeEvent) =>
243243
(this.collectionIds = e.detail.collections)}

0 commit comments

Comments
 (0)