Add browser profile filter to workflow list & add link to filtered list to profile detail pages (#2727)
This commit is contained in:
parent
f91bfda42e
commit
b3c8cc5994
@ -236,7 +236,7 @@ export class ConfigDetails extends BtrixElement {
|
||||
() =>
|
||||
html`<a
|
||||
class="text-blue-500 hover:text-blue-600"
|
||||
href=${`/orgs/${crawlConfig!.oid}/browser-profiles/profile/${
|
||||
href=${`${this.navigate.orgBasePath}/browser-profiles/profile/${
|
||||
crawlConfig!.profileid
|
||||
}`}
|
||||
@click=${this.navigate.link}
|
||||
|
@ -9,3 +9,4 @@ import("./workflow-editor");
|
||||
import("./workflow-list");
|
||||
import("./workflow-schedule-filter");
|
||||
import("./workflow-tag-filter");
|
||||
import("./workflow-profile-filter");
|
||||
|
357
frontend/src/features/crawl-workflows/workflow-profile-filter.ts
Normal file
357
frontend/src/features/crawl-workflows/workflow-profile-filter.ts
Normal file
@ -0,0 +1,357 @@
|
||||
import { localized, msg, str } from "@lit/localize";
|
||||
import { Task } from "@lit/task";
|
||||
import type {
|
||||
SlChangeEvent,
|
||||
SlCheckbox,
|
||||
SlInput,
|
||||
SlInputEvent,
|
||||
} from "@shoelace-style/shoelace";
|
||||
import clsx from "clsx";
|
||||
import Fuse from "fuse.js";
|
||||
import { html, nothing, type PropertyValues } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import queryString from "query-string";
|
||||
import { isFocusable } from "tabbable";
|
||||
|
||||
import { BtrixElement } from "@/classes/BtrixElement";
|
||||
import type { BtrixChangeEvent } from "@/events/btrix-change";
|
||||
import { type APIPaginatedList } from "@/types/api";
|
||||
import { type Profile } from "@/types/crawler";
|
||||
import { pluralOf } from "@/utils/pluralize";
|
||||
import { richText } from "@/utils/rich-text";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
const MAX_PROFILES_IN_LABEL = 2;
|
||||
const MAX_ORIGINS_IN_LIST = 5;
|
||||
|
||||
export type BtrixChangeWorkflowProfileFilterEvent = BtrixChangeEvent<
|
||||
string[] | undefined
|
||||
>;
|
||||
|
||||
/**
|
||||
* @fires btrix-change
|
||||
*/
|
||||
@customElement("btrix-workflow-profile-filter")
|
||||
@localized()
|
||||
export class WorkflowProfileFilter extends BtrixElement {
|
||||
@property({ type: Array })
|
||||
profiles?: string[];
|
||||
|
||||
@state()
|
||||
private searchString = "";
|
||||
|
||||
@query("sl-input")
|
||||
private readonly input?: SlInput | null;
|
||||
|
||||
@queryAll("sl-checkbox")
|
||||
private readonly checkboxes!: NodeListOf<SlCheckbox>;
|
||||
|
||||
private readonly fuse = new Fuse<Profile>([], {
|
||||
keys: ["id", "name", "description", "origins"],
|
||||
});
|
||||
|
||||
private selected = new Map<string, boolean>();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("profiles")) {
|
||||
if (this.profiles) {
|
||||
this.selected = new Map(this.profiles.map((tag) => [tag, true]));
|
||||
} else if (changedProperties.get("profiles")) {
|
||||
this.selected = new Map();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly profilesTask = new Task(this, {
|
||||
task: async () => {
|
||||
const query = queryString.stringify(
|
||||
{
|
||||
pageSize: 1000,
|
||||
page: 1,
|
||||
},
|
||||
{
|
||||
arrayFormat: "comma",
|
||||
},
|
||||
);
|
||||
const { items } = await this.api.fetch<APIPaginatedList<Profile>>(
|
||||
`/orgs/${this.orgId}/profiles?${query}`,
|
||||
);
|
||||
|
||||
this.fuse.setCollection(items);
|
||||
|
||||
// Match fuse shape
|
||||
return items.map((item) => ({ item }));
|
||||
},
|
||||
args: () => [] as const,
|
||||
});
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<btrix-filter-chip
|
||||
?checked=${!!this.profiles?.length}
|
||||
selectFromDropdown
|
||||
stayOpenOnChange
|
||||
@sl-after-show=${() => {
|
||||
if (this.input && !this.input.disabled) {
|
||||
this.input.focus();
|
||||
}
|
||||
}}
|
||||
@sl-after-hide=${() => {
|
||||
this.searchString = "";
|
||||
|
||||
const selectedProfiles = [];
|
||||
|
||||
for (const [profile, value] of this.selected) {
|
||||
if (value) {
|
||||
selectedProfiles.push(profile);
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<BtrixChangeEvent["detail"]>("btrix-change", {
|
||||
detail: {
|
||||
value: selectedProfiles.length ? selectedProfiles : undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
${this.profiles?.length
|
||||
? html`<span class="opacity-75"
|
||||
>${msg(
|
||||
str`Using ${pluralOf("profiles", this.profiles.length)}`,
|
||||
)}</span
|
||||
>
|
||||
${this.renderProfilesInLabel(this.profiles)}`
|
||||
: msg("Browser Profile")}
|
||||
|
||||
<div
|
||||
slot="dropdown-content"
|
||||
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"
|
||||
>
|
||||
<header
|
||||
class=${clsx(
|
||||
this.profilesTask.value && tw`border-b`,
|
||||
tw`flex-shrink-0 flex-grow-0 overflow-hidden rounded-t bg-white pb-3`,
|
||||
)}
|
||||
>
|
||||
<sl-menu-label
|
||||
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"
|
||||
>
|
||||
<div
|
||||
id="profile-list-label"
|
||||
class="leading-[var(--sl-input-height-small)]"
|
||||
>
|
||||
${msg("Filter by Browser Profile")}
|
||||
</div>
|
||||
${this.profiles?.length
|
||||
? html`<sl-button
|
||||
variant="text"
|
||||
size="small"
|
||||
class="part-[label]:px-0"
|
||||
@click=${() => {
|
||||
this.checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<BtrixChangeEvent["detail"]>(
|
||||
"btrix-change",
|
||||
{
|
||||
detail: {
|
||||
value: undefined,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}}
|
||||
>${msg("Clear")}</sl-button
|
||||
>`
|
||||
: nothing}
|
||||
</sl-menu-label>
|
||||
|
||||
<div class="px-3">${this.renderSearch()}</div>
|
||||
</header>
|
||||
|
||||
${this.profilesTask.render({
|
||||
complete: (profiles) => {
|
||||
let options = profiles;
|
||||
|
||||
if (profiles.length && this.searchString) {
|
||||
options = this.fuse.search(this.searchString);
|
||||
}
|
||||
|
||||
if (options.length) {
|
||||
return this.renderList(options);
|
||||
}
|
||||
|
||||
return html`<div class="p-3 text-neutral-500">
|
||||
${this.searchString
|
||||
? msg("No matching profiles found.")
|
||||
: msg("No profiles found.")}
|
||||
</div>`;
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</btrix-filter-chip>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderProfilesInLabel(profiles: string[]) {
|
||||
const formatter2 = this.localize.list(
|
||||
profiles.length > MAX_PROFILES_IN_LABEL
|
||||
? [
|
||||
...profiles.slice(0, MAX_PROFILES_IN_LABEL),
|
||||
msg(
|
||||
str`${this.localize.number(profiles.length - MAX_PROFILES_IN_LABEL)} more`,
|
||||
),
|
||||
]
|
||||
: profiles,
|
||||
{ type: "disjunction" },
|
||||
);
|
||||
|
||||
return formatter2.map((part, index, array) =>
|
||||
part.type === "literal"
|
||||
? html`<span class="opacity-75">${part.value}</span>`
|
||||
: profiles.length > MAX_PROFILES_IN_LABEL && index === array.length - 1
|
||||
? html`<span class="text-primary-500"> ${part.value} </span>`
|
||||
: html`<span class="inline-block max-w-48 truncate"
|
||||
>${this.profilesTask.value?.find(
|
||||
({ item }) => item.id === part.value,
|
||||
)?.item.name}</span
|
||||
>`,
|
||||
);
|
||||
}
|
||||
|
||||
private renderSearch() {
|
||||
return html`
|
||||
<label for="profile-search" class="sr-only"
|
||||
>${msg("Filter profiles")}</label
|
||||
>
|
||||
<sl-input
|
||||
class="min-w-[30ch]"
|
||||
id="profile-search"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="true"
|
||||
aria-controls="profile-listbox"
|
||||
aria-activedescendant="profile-selected-option"
|
||||
value=${this.searchString}
|
||||
placeholder=${msg("Search for profile")}
|
||||
size="small"
|
||||
?disabled=${!this.profilesTask.value?.length}
|
||||
@sl-input=${(e: SlInputEvent) =>
|
||||
(this.searchString = (e.target as SlInput).value)}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
// Prevent moving to next tabbable element since dropdown should close
|
||||
if (e.key === "Tab") e.preventDefault();
|
||||
if (e.key === "ArrowDown" && isFocusable(this.checkboxes[0])) {
|
||||
this.checkboxes[0].focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.profilesTask.render({
|
||||
pending: () => html`<sl-spinner slot="prefix"></sl-spinner>`,
|
||||
complete: () => html`<sl-icon slot="prefix" name="search"></sl-icon>`,
|
||||
})}
|
||||
</sl-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderList(opts: { item: Profile }[]) {
|
||||
const profile = (profile: Profile) => {
|
||||
const checked = this.selected.get(profile.id) === true;
|
||||
|
||||
return html`
|
||||
<li role="option" aria-checked=${checked}>
|
||||
<sl-checkbox
|
||||
class="w-full part-[label]:grid part-[base]:w-full part-[label]:w-full part-[label]:items-center part-[label]:justify-between part-[label]:gap-x-2 part-[label]:gap-y-1 part-[base]:rounded part-[base]:p-2 part-[base]:hover:bg-primary-50"
|
||||
value=${profile.id}
|
||||
?checked=${checked}
|
||||
?disabled=${!profile.inUse}
|
||||
>
|
||||
<span class="mb-1 inline-block min-w-0 max-w-96 truncate"
|
||||
>${profile.name}</span
|
||||
>
|
||||
<btrix-format-date
|
||||
class="col-start-2 ml-auto text-xs text-stone-600"
|
||||
date=${profile.modified ?? profile.created}
|
||||
></btrix-format-date>
|
||||
${profile.inUse
|
||||
? html`${profile.description &&
|
||||
html`<div
|
||||
class="col-span-2 min-w-0 truncate text-xs text-stone-600 contain-inline-size"
|
||||
>
|
||||
${profile.description}
|
||||
</div>`}
|
||||
<div
|
||||
class="col-span-2 min-w-0 max-w-full text-xs text-stone-400 contain-inline-size"
|
||||
>
|
||||
${this.localize
|
||||
.list(
|
||||
profile.origins.length > MAX_ORIGINS_IN_LIST
|
||||
? [
|
||||
...profile.origins.slice(0, MAX_ORIGINS_IN_LIST),
|
||||
msg(
|
||||
str`${this.localize.number(profile.origins.length - MAX_ORIGINS_IN_LIST)} more`,
|
||||
),
|
||||
]
|
||||
: profile.origins,
|
||||
)
|
||||
.map((part) =>
|
||||
part.type === "literal"
|
||||
? part.value
|
||||
: richText(part.value, {
|
||||
shortenOnly: true,
|
||||
linkClass: tw`inline-block max-w-[min(theme(spacing.72),100%)] truncate font-medium text-stone-600`,
|
||||
}),
|
||||
)}
|
||||
</div> `
|
||||
: html`<div class="col-span-2 text-xs">
|
||||
${msg("Not in use")}
|
||||
</div>`}
|
||||
</sl-checkbox>
|
||||
</li>
|
||||
`;
|
||||
};
|
||||
|
||||
// TODO for if/when we correctly handle `inUse` in the profile list endpoint
|
||||
|
||||
// const sortedProfiles = opts.sort(({ item: a }, { item: b }) =>
|
||||
// b.inUse === a.inUse ? 0 : b.inUse ? -1 : 1,
|
||||
// );
|
||||
|
||||
// For now, we just hardcode `inUse` to be true
|
||||
const sortedProfiles = opts.map(({ item }) => ({
|
||||
item: { ...item, inUse: true },
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ul
|
||||
id="profile-listbox"
|
||||
class="flex-1 overflow-auto p-1"
|
||||
role="listbox"
|
||||
aria-labelledby="profile-list-label"
|
||||
aria-multiselectable="true"
|
||||
@sl-change=${async (e: SlChangeEvent) => {
|
||||
const { checked, value } = e.target as SlCheckbox;
|
||||
|
||||
this.selected.set(value, checked);
|
||||
}}
|
||||
>
|
||||
${repeat(
|
||||
sortedProfiles,
|
||||
({ item }) => item,
|
||||
({ item }) => profile(item),
|
||||
)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
@ -22,13 +22,17 @@ import { isFocusable } from "tabbable";
|
||||
import { BtrixElement } from "@/classes/BtrixElement";
|
||||
import type { BtrixChangeEvent } from "@/events/btrix-change";
|
||||
import { type WorkflowTag, type WorkflowTags } from "@/types/workflow";
|
||||
import { stopProp } from "@/utils/events";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
const MAX_TAGS_IN_LABEL = 5;
|
||||
|
||||
export type BtrixChangeWorkflowTagFilterEvent = BtrixChangeEvent<
|
||||
string[] | undefined
|
||||
>;
|
||||
type ChangeWorkflowTagEventDetails =
|
||||
| { tags: string[]; type: "and" | "or" }
|
||||
| undefined;
|
||||
|
||||
export type BtrixChangeWorkflowTagFilterEvent =
|
||||
BtrixChangeEvent<ChangeWorkflowTagEventDetails>;
|
||||
|
||||
/**
|
||||
* @fires btrix-change
|
||||
@ -52,9 +56,19 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
keys: ["tag"],
|
||||
});
|
||||
|
||||
@state()
|
||||
private get selectedTags() {
|
||||
return Array.from(this.selected.entries())
|
||||
.filter(([_tag, selected]) => selected)
|
||||
.map(([tag]) => tag);
|
||||
}
|
||||
|
||||
private selected = new Map<string, boolean>();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
@state()
|
||||
private type: "and" | "or" = "or";
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("tags")) {
|
||||
if (this.tags) {
|
||||
this.selected = new Map(this.tags.map((tag) => [tag, true]));
|
||||
@ -92,17 +106,17 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
@sl-after-hide=${() => {
|
||||
this.searchString = "";
|
||||
|
||||
const selectedTags = [];
|
||||
|
||||
for (const [tag, value] of this.selected) {
|
||||
if (value) {
|
||||
selectedTags.push(tag);
|
||||
}
|
||||
}
|
||||
console.log("after hide");
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<BtrixChangeEvent["detail"]>("btrix-change", {
|
||||
detail: { value: selectedTags.length ? selectedTags : undefined },
|
||||
new CustomEvent<
|
||||
BtrixChangeEvent<ChangeWorkflowTagEventDetails>["detail"]
|
||||
>("btrix-change", {
|
||||
detail: {
|
||||
value: this.selectedTags.length
|
||||
? { tags: this.selectedTags, type: this.type }
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}}
|
||||
@ -141,15 +155,16 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
this.type = "or";
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<BtrixChangeEvent["detail"]>(
|
||||
"btrix-change",
|
||||
{
|
||||
detail: {
|
||||
value: undefined,
|
||||
},
|
||||
new CustomEvent<
|
||||
BtrixChangeEvent<ChangeWorkflowTagEventDetails>["detail"]
|
||||
>("btrix-change", {
|
||||
detail: {
|
||||
value: undefined,
|
||||
},
|
||||
),
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>${msg("Clear")}</sl-button
|
||||
@ -157,7 +172,32 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
: nothing}
|
||||
</sl-menu-label>
|
||||
|
||||
<div class="px-3">${this.renderSearch()}</div>
|
||||
<div class="flex gap-2 px-3">
|
||||
${this.renderSearch()}
|
||||
<sl-radio-group
|
||||
size="small"
|
||||
value=${this.type}
|
||||
@sl-change=${(event: SlChangeEvent) => {
|
||||
this.type = (event.target as HTMLInputElement).value as
|
||||
| "or"
|
||||
| "and";
|
||||
}}
|
||||
@sl-after-hide=${stopProp}
|
||||
>
|
||||
<sl-tooltip hoist content=${msg("Any of the selected tags")}>
|
||||
<sl-radio-button value="or" checked>
|
||||
<sl-icon name="union" slot="prefix"></sl-icon>
|
||||
${msg("Any")}
|
||||
</sl-radio-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip hoist content=${msg("All of the selected tags")}>
|
||||
<sl-radio-button value="and">
|
||||
<sl-icon name="intersect" slot="prefix"></sl-icon>
|
||||
${msg("All")}
|
||||
</sl-radio-button>
|
||||
</sl-tooltip>
|
||||
</sl-radio-group>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
${this.orgTagsTask.render({
|
||||
@ -194,6 +234,7 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
),
|
||||
]
|
||||
: tags,
|
||||
{ type: this.type === "and" ? "conjunction" : "disjunction" },
|
||||
);
|
||||
|
||||
return formatter2.map((part, index, array) =>
|
||||
@ -266,6 +307,7 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
const { checked, value } = e.target as SlCheckbox;
|
||||
|
||||
this.selected.set(value, checked);
|
||||
this.requestUpdate("selectedTags");
|
||||
}}
|
||||
>
|
||||
${repeat(
|
||||
|
@ -160,6 +160,9 @@ export class BrowserProfilesDetail extends BtrixElement {
|
||||
<div class="mb-7 flex flex-col gap-5 lg:flex-row">
|
||||
<section class="flex-1">
|
||||
<header class="flex items-center gap-2">
|
||||
<h2 class="text-lg font-medium leading-none">
|
||||
${msg("Browser Profile")}
|
||||
</h2>
|
||||
<sl-tooltip
|
||||
content=${isBackedUp ? msg("Backed Up") : msg("Not Backed Up")}
|
||||
?disabled=${!this.profile}
|
||||
@ -167,7 +170,7 @@ export class BrowserProfilesDetail extends BtrixElement {
|
||||
<sl-icon
|
||||
class="${isBackedUp
|
||||
? "text-success"
|
||||
: "text-neutral-500"} text-base"
|
||||
: "text-neutral-500"} ml-auto text-base"
|
||||
name=${this.profile
|
||||
? isBackedUp
|
||||
? "clouds-fill"
|
||||
@ -175,9 +178,36 @@ export class BrowserProfilesDetail extends BtrixElement {
|
||||
: "clouds"}
|
||||
></sl-icon>
|
||||
</sl-tooltip>
|
||||
<h2 class="text-lg font-medium leading-none">
|
||||
${msg("Browser Profile")}
|
||||
</h2>
|
||||
|
||||
${this.profile?.inUse
|
||||
? html`
|
||||
<sl-tooltip
|
||||
content=${msg(
|
||||
"View Crawl Workflows using this Browser Profile",
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href=${`${this.navigate.orgBasePath}/workflows?profiles=${this.profile.id}`}
|
||||
@click=${this.navigate.link}
|
||||
class="ml-2 flex items-center gap-2 text-sm font-medium text-primary-500 transition-colors hover:text-primary-600"
|
||||
>
|
||||
${msg("In Use")}
|
||||
<sl-icon
|
||||
class="text-base"
|
||||
name="arrow-right-circle"
|
||||
></sl-icon>
|
||||
</a>
|
||||
</sl-tooltip>
|
||||
`
|
||||
: html`<sl-tooltip
|
||||
content=${msg("Not In Use")}
|
||||
?disabled=${!this.profile}
|
||||
>
|
||||
<sl-icon
|
||||
class="text-base text-neutral-500"
|
||||
name=${this.profile ? "slash-circle" : "clouds"}
|
||||
></sl-icon>
|
||||
</sl-tooltip>`}
|
||||
</header>
|
||||
|
||||
${when(this.isCrawler, () =>
|
||||
@ -249,12 +279,12 @@ export class BrowserProfilesDetail extends BtrixElement {
|
||||
</header>
|
||||
<!-- display: inline -->
|
||||
<div
|
||||
class="leading whitespace-pre-line rounded border p-5 leading-relaxed first-line:leading-[0]"
|
||||
class="leading whitespace-pre-line rounded border p-5 leading-relaxed"
|
||||
>${this.profile
|
||||
? this.profile.description
|
||||
? richText(this.profile.description)
|
||||
: html`
|
||||
<div class="text-center text-neutral-400">
|
||||
<div class="text-center leading-[0] text-neutral-400">
|
||||
${msg("No description added.")}
|
||||
</div>
|
||||
`
|
||||
|
@ -25,6 +25,7 @@ import { type SelectEvent } from "@/components/ui/search-combobox";
|
||||
import { ClipboardController } from "@/controllers/clipboard";
|
||||
import { SearchParamsController } from "@/controllers/searchParams";
|
||||
import type { SelectJobTypeEvent } from "@/features/crawl-workflows/new-workflow-dialog";
|
||||
import { type BtrixChangeWorkflowProfileFilterEvent } from "@/features/crawl-workflows/workflow-profile-filter";
|
||||
import type { BtrixChangeWorkflowScheduleFilterEvent } from "@/features/crawl-workflows/workflow-schedule-filter";
|
||||
import type { BtrixChangeWorkflowTagFilterEvent } from "@/features/crawl-workflows/workflow-tag-filter";
|
||||
import { pageHeader } from "@/layouts/pageHeader";
|
||||
@ -131,6 +132,12 @@ export class WorkflowsList extends BtrixElement {
|
||||
@state()
|
||||
private filterByTags?: string[];
|
||||
|
||||
@state()
|
||||
private filterByTagsType: "and" | "or" = "or";
|
||||
|
||||
@state()
|
||||
private filterByProfiles?: string[];
|
||||
|
||||
@query("#deleteDialog")
|
||||
private readonly deleteDialog?: SlDialog | null;
|
||||
|
||||
@ -173,6 +180,12 @@ export class WorkflowsList extends BtrixElement {
|
||||
this.filterByTags = undefined;
|
||||
}
|
||||
|
||||
if (params.has("profiles")) {
|
||||
this.filterByProfiles = params.getAll("profiles");
|
||||
} else {
|
||||
this.filterByProfiles = undefined;
|
||||
}
|
||||
|
||||
// add filters present in search params
|
||||
for (const [key, value] of params) {
|
||||
// Filter by current user
|
||||
@ -180,6 +193,10 @@ export class WorkflowsList extends BtrixElement {
|
||||
this.filterByCurrentUser = value === "true";
|
||||
}
|
||||
|
||||
if (key === "tagsType") {
|
||||
this.filterByTagsType = value === "and" ? "and" : "or";
|
||||
}
|
||||
|
||||
// Sorting field
|
||||
if (key === "sortBy") {
|
||||
if (value in sortableFields) {
|
||||
@ -200,7 +217,18 @@ export class WorkflowsList extends BtrixElement {
|
||||
}
|
||||
|
||||
// Ignored params
|
||||
if (["page", "mine", "tags", "sortBy", "sortDir"].includes(key)) continue;
|
||||
if (
|
||||
[
|
||||
"page",
|
||||
"mine",
|
||||
"tags",
|
||||
"tagsType",
|
||||
"profiles",
|
||||
"sortBy",
|
||||
"sortDir",
|
||||
].includes(key)
|
||||
)
|
||||
continue;
|
||||
|
||||
// Convert string bools to filter values
|
||||
if (value === "true") {
|
||||
@ -239,6 +267,8 @@ export class WorkflowsList extends BtrixElement {
|
||||
const resetToFirstPageProps = [
|
||||
"filterByCurrentUser",
|
||||
"filterByTags",
|
||||
"filterByTagsType",
|
||||
"filterByProfiles",
|
||||
"filterByScheduled",
|
||||
"filterBy",
|
||||
"orderBy",
|
||||
@ -278,15 +308,14 @@ export class WorkflowsList extends BtrixElement {
|
||||
changedProperties.has("filterBy") ||
|
||||
changedProperties.has("filterByCurrentUser") ||
|
||||
changedProperties.has("filterByTags") ||
|
||||
changedProperties.has("filterByTagsType") ||
|
||||
changedProperties.has("filterByProfiles") ||
|
||||
changedProperties.has("orderBy")
|
||||
) {
|
||||
this.searchParams.update((params) => {
|
||||
// Reset page
|
||||
params.delete("page");
|
||||
|
||||
// Existing tags
|
||||
const tags = params.getAll("tags");
|
||||
|
||||
const newParams = [
|
||||
// Known filters
|
||||
...USED_FILTERS.map<[string, undefined]>((f) => [f, undefined]),
|
||||
@ -299,6 +328,13 @@ export class WorkflowsList extends BtrixElement {
|
||||
|
||||
["tags", this.filterByTags],
|
||||
|
||||
[
|
||||
"tagsType",
|
||||
this.filterByTagsType !== "or" ? this.filterByTagsType : undefined,
|
||||
],
|
||||
|
||||
["profiles", this.filterByProfiles],
|
||||
|
||||
// Sorting fields
|
||||
[
|
||||
"sortBy",
|
||||
@ -319,7 +355,8 @@ export class WorkflowsList extends BtrixElement {
|
||||
if (value !== undefined) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => {
|
||||
if (!tags.includes(v)) {
|
||||
// Only add new array values to URL
|
||||
if (!params.getAll(filter).includes(v)) {
|
||||
params.append(filter, v);
|
||||
}
|
||||
});
|
||||
@ -626,10 +663,18 @@ export class WorkflowsList extends BtrixElement {
|
||||
<btrix-workflow-tag-filter
|
||||
.tags=${this.filterByTags}
|
||||
@btrix-change=${(e: BtrixChangeWorkflowTagFilterEvent) => {
|
||||
this.filterByTags = e.detail.value;
|
||||
this.filterByTags = e.detail.value?.tags;
|
||||
this.filterByTagsType = e.detail.value?.type || "or";
|
||||
}}
|
||||
></btrix-workflow-tag-filter>
|
||||
|
||||
<btrix-workflow-profile-filter
|
||||
.profiles=${this.filterByProfiles}
|
||||
@btrix-change=${(e: BtrixChangeWorkflowProfileFilterEvent) => {
|
||||
this.filterByProfiles = e.detail.value;
|
||||
}}
|
||||
></btrix-workflow-profile-filter>
|
||||
|
||||
<btrix-filter-chip
|
||||
?checked=${this.filterBy.isCrawlRunning === true}
|
||||
@btrix-change=${(e: BtrixFilterChipChangeEvent) => {
|
||||
@ -976,6 +1021,8 @@ export class WorkflowsList extends BtrixElement {
|
||||
INITIAL_PAGE_SIZE,
|
||||
userid: this.filterByCurrentUser ? this.userInfo?.id : undefined,
|
||||
tag: this.filterByTags || undefined,
|
||||
tagMatch: this.filterByTagsType,
|
||||
profileIds: this.filterByProfiles || undefined,
|
||||
sortBy: this.orderBy.field,
|
||||
sortDirection: this.orderBy.direction === "desc" ? -1 : 1,
|
||||
},
|
||||
|
@ -221,6 +221,32 @@ const plurals = {
|
||||
id: "browserWindows.plural.other",
|
||||
}),
|
||||
},
|
||||
profiles: {
|
||||
zero: msg("profiles", {
|
||||
desc: 'plural form of "profiles" for zero profiles',
|
||||
id: "profiles.plural.zero",
|
||||
}),
|
||||
one: msg("profile", {
|
||||
desc: 'singular form for "profile"',
|
||||
id: "profiles.plural.one",
|
||||
}),
|
||||
two: msg("profiles", {
|
||||
desc: 'plural form of "profiles" for two profiles',
|
||||
id: "profiles.plural.two",
|
||||
}),
|
||||
few: msg("profiles", {
|
||||
desc: 'plural form of "profiles" for few profiles',
|
||||
id: "profiles.plural.few",
|
||||
}),
|
||||
many: msg("profiles", {
|
||||
desc: 'plural form of "profiles" for many profiles',
|
||||
id: "profiles.plural.many",
|
||||
}),
|
||||
other: msg("profiles", {
|
||||
desc: 'plural form of "profiles" for multiple/other profiles',
|
||||
id: "profiles.plural.other",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const pluralOf = (word: keyof typeof plurals, count: number) => {
|
||||
|
Loading…
Reference in New Issue
Block a user