Include tag counts in tag filter & tag input autocomplete (#2711)
This commit is contained in:
parent
80a225c677
commit
74c72ce551
@ -25,6 +25,7 @@ from .models import (
|
||||
ConfigRevision,
|
||||
CrawlConfig,
|
||||
CrawlConfigOut,
|
||||
CrawlConfigTags,
|
||||
CrawlOut,
|
||||
UpdateCrawlConfig,
|
||||
Organization,
|
||||
@ -976,8 +977,20 @@ class CrawlConfigOps:
|
||||
|
||||
async def get_crawl_config_tags(self, org):
|
||||
"""get distinct tags from all crawl configs for this org"""
|
||||
tags = await self.crawl_configs.distinct("tags", {"oid": org.id})
|
||||
return list(tags)
|
||||
return await self.crawl_configs.distinct("tags", {"oid": org.id})
|
||||
|
||||
async def get_crawl_config_tag_counts(self, org):
|
||||
"""get distinct tags from all crawl configs for this org"""
|
||||
tags = await self.crawl_configs.aggregate(
|
||||
[
|
||||
{"$match": {"oid": org.id}},
|
||||
{"$unwind": "$tags"},
|
||||
{"$group": {"_id": "$tags", "count": {"$sum": 1}}},
|
||||
{"$project": {"tag": "$_id", "count": "$count", "_id": 0}},
|
||||
{"$sort": {"count": -1, "tag": 1}},
|
||||
]
|
||||
).to_list()
|
||||
return tags
|
||||
|
||||
async def get_crawl_config_search_values(self, org):
|
||||
"""List unique names, first seeds, and descriptions from all workflows in org"""
|
||||
@ -1399,10 +1412,17 @@ def init_crawl_config_api(
|
||||
)
|
||||
return paginated_format(crawl_configs, total, page, pageSize)
|
||||
|
||||
@router.get("/tags", response_model=List[str])
|
||||
@router.get("/tags", response_model=List[str], deprecated=True)
|
||||
async def get_crawl_config_tags(org: Organization = Depends(org_viewer_dep)):
|
||||
"""
|
||||
Deprecated - prefer /api/orgs/{oid}/crawlconfigs/tagCounts instead.
|
||||
"""
|
||||
return await ops.get_crawl_config_tags(org)
|
||||
|
||||
@router.get("/tagCounts", response_model=CrawlConfigTags)
|
||||
async def get_crawl_config_tag_counts(org: Organization = Depends(org_viewer_dep)):
|
||||
return {"tags": await ops.get_crawl_config_tag_counts(org)}
|
||||
|
||||
@router.get("/search-values", response_model=CrawlConfigSearchValues)
|
||||
async def get_crawl_config_search_values(
|
||||
org: Organization = Depends(org_viewer_dep),
|
||||
|
||||
@ -577,11 +577,19 @@ class CrawlConfigAddedResponse(BaseModel):
|
||||
execMinutesQuotaReached: bool
|
||||
|
||||
|
||||
# ============================================================================
|
||||
class CrawlConfigTagCount(BaseModel):
|
||||
"""Response model for crawlconfig tag count"""
|
||||
|
||||
tag: str
|
||||
count: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
class CrawlConfigTags(BaseModel):
|
||||
"""Response model for crawlconfig tags"""
|
||||
|
||||
tags: List[str]
|
||||
tags: List[CrawlConfigTagCount]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -50,6 +50,22 @@ def test_get_config_by_tag_1(admin_auth_headers, default_org_id):
|
||||
assert sorted(data) == ["tag-1", "tag-2", "wr-test-1", "wr-test-2"]
|
||||
|
||||
|
||||
def test_get_config_by_tag_counts_1(admin_auth_headers, default_org_id):
|
||||
r = requests.get(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/tagCounts",
|
||||
headers=admin_auth_headers,
|
||||
)
|
||||
data = r.json()
|
||||
assert data == {
|
||||
"tags": [
|
||||
{"tag": "wr-test-2", "count": 2},
|
||||
{"tag": "tag-1", "count": 1},
|
||||
{"tag": "tag-2", "count": 1},
|
||||
{"tag": "wr-test-1", "count": 1},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_create_new_config_2(admin_auth_headers, default_org_id):
|
||||
r = requests.post(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/",
|
||||
@ -84,6 +100,24 @@ def test_get_config_by_tag_2(admin_auth_headers, default_org_id):
|
||||
]
|
||||
|
||||
|
||||
def test_get_config_by_tag_counts_2(admin_auth_headers, default_org_id):
|
||||
r = requests.get(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/tagCounts",
|
||||
headers=admin_auth_headers,
|
||||
)
|
||||
data = r.json()
|
||||
assert data == {
|
||||
"tags": [
|
||||
{"tag": "wr-test-2", "count": 2},
|
||||
{"tag": "tag-0", "count": 1},
|
||||
{"tag": "tag-1", "count": 1},
|
||||
{"tag": "tag-2", "count": 1},
|
||||
{"tag": "tag-3", "count": 1},
|
||||
{"tag": "wr-test-1", "count": 1},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_get_config_2(admin_auth_headers, default_org_id):
|
||||
r = requests.get(
|
||||
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/{new_cid_2}",
|
||||
|
||||
@ -11,7 +11,7 @@ export type BadgeVariant =
|
||||
| "danger"
|
||||
| "neutral"
|
||||
| "primary"
|
||||
| "blue"
|
||||
| "cyan"
|
||||
| "high-contrast";
|
||||
|
||||
/**
|
||||
@ -27,6 +27,12 @@ export class Badge extends TailwindElement {
|
||||
@property({ type: String })
|
||||
variant: BadgeVariant = "neutral";
|
||||
|
||||
@property({ type: Boolean })
|
||||
outline = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
pill = false;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
role: string | null = "status";
|
||||
|
||||
@ -40,16 +46,32 @@ export class Badge extends TailwindElement {
|
||||
return html`
|
||||
<span
|
||||
class=${clsx(
|
||||
tw`h-4.5 inline-flex items-center justify-center rounded-sm px-2 align-[1px] text-xs`,
|
||||
tw`inline-flex h-[1.125rem] items-center justify-center align-[1px] text-xs`,
|
||||
this.outline
|
||||
? [
|
||||
tw`ring-1`,
|
||||
{
|
||||
success: tw`bg-success-500 text-success-500 ring-success-500`,
|
||||
warning: tw`bg-warning-600 text-warning-600 ring-warning-600`,
|
||||
danger: tw`bg-danger-500 text-danger-500 ring-danger-500`,
|
||||
neutral: tw`g-neutral-100 text-neutral-600 ring-neutral-600`,
|
||||
"high-contrast": tw`bg-neutral-600 text-neutral-0 ring-neutral-0`,
|
||||
primary: tw`bg-white text-primary ring-primary`,
|
||||
cyan: tw`bg-cyan-50 text-cyan-600 ring-cyan-600`,
|
||||
blue: tw`bg-blue-50 text-blue-600 ring-blue-600`,
|
||||
}[this.variant],
|
||||
]
|
||||
: {
|
||||
success: tw`bg-success-500 text-neutral-0`,
|
||||
warning: tw`bg-warning-600 text-neutral-0`,
|
||||
danger: tw`bg-danger-500 text-neutral-0`,
|
||||
neutral: tw`bg-neutral-100 text-neutral-600`,
|
||||
"high-contrast": tw`bg-neutral-600 text-neutral-0`,
|
||||
primary: tw`bg-primary text-neutral-0`,
|
||||
blue: tw`bg-cyan-50 text-cyan-600`,
|
||||
cyan: tw`bg-cyan-50 text-cyan-600`,
|
||||
blue: tw`bg-blue-50 text-blue-600`,
|
||||
}[this.variant],
|
||||
this.pill ? tw`min-w-[1.125rem] rounded-full px-1` : tw`rounded px-2`,
|
||||
)}
|
||||
part="base"
|
||||
>
|
||||
|
||||
@ -17,6 +17,7 @@ import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import debounce from "lodash/fp/debounce";
|
||||
|
||||
import type { UnderlyingFunction } from "@/types/utils";
|
||||
import { type WorkflowTag } from "@/types/workflow";
|
||||
import { dropdown } from "@/utils/css";
|
||||
|
||||
export type Tags = string[];
|
||||
@ -80,7 +81,7 @@ export class TagInput extends LitElement {
|
||||
}
|
||||
|
||||
sl-popup::part(popup) {
|
||||
z-index: 3;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.shake {
|
||||
@ -116,7 +117,7 @@ export class TagInput extends LitElement {
|
||||
initialTags?: Tags;
|
||||
|
||||
@property({ type: Array })
|
||||
tagOptions: Tags = [];
|
||||
tagOptions: WorkflowTag[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
disabled = false;
|
||||
@ -224,6 +225,7 @@ export class TagInput extends LitElement {
|
||||
@paste=${this.onPaste}
|
||||
?required=${this.required && !this.tags.length}
|
||||
placeholder=${placeholder}
|
||||
autocomplete="off"
|
||||
role="combobox"
|
||||
aria-controls="dropdown"
|
||||
aria-expanded="${this.dropdownIsOpen === true}"
|
||||
@ -258,10 +260,14 @@ export class TagInput extends LitElement {
|
||||
>
|
||||
${this.tagOptions
|
||||
.slice(0, 3)
|
||||
.filter(({ tag }) => !this.tags.includes(tag))
|
||||
.map(
|
||||
(tag) => html`
|
||||
({ tag, count }) => html`
|
||||
<sl-menu-item role="option" value=${tag}
|
||||
>${tag}</sl-menu-item
|
||||
>${tag}
|
||||
<btrix-badge pill variant="cyan" slot="suffix"
|
||||
>${count}</btrix-badge
|
||||
></sl-menu-item
|
||||
>
|
||||
`,
|
||||
)}
|
||||
|
||||
@ -17,6 +17,7 @@ import type {
|
||||
TagsChangeEvent,
|
||||
} from "@/components/ui/tag-input";
|
||||
import { type CollectionsChangeEvent } from "@/features/collections/collections-add";
|
||||
import { type WorkflowTag, type WorkflowTags } from "@/types/workflow";
|
||||
import { APIError } from "@/utils/api";
|
||||
import { maxLengthValidator } from "@/utils/form";
|
||||
|
||||
@ -70,7 +71,7 @@ export class FileUploader extends BtrixElement {
|
||||
private collectionIds: string[] = [];
|
||||
|
||||
@state()
|
||||
private tagOptions: Tags = [];
|
||||
private tagOptions: WorkflowTag[] = [];
|
||||
|
||||
@state()
|
||||
private tagsToSave: Tags = [];
|
||||
@ -85,7 +86,8 @@ export class FileUploader extends BtrixElement {
|
||||
private readonly form!: Promise<HTMLFormElement>;
|
||||
|
||||
// For fuzzy search:
|
||||
private readonly fuse = new Fuse([], {
|
||||
private readonly fuse = new Fuse<WorkflowTag>([], {
|
||||
keys: ["tag"],
|
||||
shouldSort: false,
|
||||
threshold: 0.2, // stricter; default is 0.6
|
||||
});
|
||||
@ -361,8 +363,8 @@ export class FileUploader extends BtrixElement {
|
||||
|
||||
private async fetchTags() {
|
||||
try {
|
||||
const tags = await this.api.fetch<never>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tags`,
|
||||
const { tags } = await this.api.fetch<WorkflowTags>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tagCounts`,
|
||||
);
|
||||
|
||||
// Update search/filter collection
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
} from "@/components/ui/tag-input";
|
||||
import { type CollectionsChangeEvent } from "@/features/collections/collections-add";
|
||||
import type { ArchivedItem } from "@/types/crawler";
|
||||
import { type WorkflowTag, type WorkflowTags } from "@/types/workflow";
|
||||
import { maxLengthValidator } from "@/utils/form";
|
||||
import LiteElement, { html } from "@/utils/LiteElement";
|
||||
|
||||
@ -46,7 +47,7 @@ export class CrawlMetadataEditor extends LiteElement {
|
||||
private includeName = false;
|
||||
|
||||
@state()
|
||||
private tagOptions: Tags = [];
|
||||
private tagOptions: WorkflowTag[] = [];
|
||||
|
||||
@state()
|
||||
private tagsToSave: Tags = [];
|
||||
@ -55,7 +56,8 @@ export class CrawlMetadataEditor extends LiteElement {
|
||||
private collectionsToSave: string[] = [];
|
||||
|
||||
// For fuzzy search:
|
||||
private readonly fuse = new Fuse<string>([], {
|
||||
private readonly fuse = new Fuse<WorkflowTag>([], {
|
||||
keys: ["tag"],
|
||||
shouldSort: false,
|
||||
threshold: 0.2, // stricter; default is 0.6
|
||||
});
|
||||
@ -164,8 +166,8 @@ export class CrawlMetadataEditor extends LiteElement {
|
||||
private async fetchTags() {
|
||||
if (!this.crawl) return;
|
||||
try {
|
||||
const tags = await this.apiFetch<string[]>(
|
||||
`/orgs/${this.crawl.oid}/crawlconfigs/tags`,
|
||||
const { tags } = await this.apiFetch<WorkflowTags>(
|
||||
`/orgs/${this.crawl.oid}/crawlconfigs/tagCounts`,
|
||||
);
|
||||
|
||||
// Update search/filter collection
|
||||
|
||||
@ -88,7 +88,11 @@ import {
|
||||
type WorkflowParams,
|
||||
} from "@/types/crawler";
|
||||
import type { UnderlyingFunction } from "@/types/utils";
|
||||
import { NewWorkflowOnlyScopeType } from "@/types/workflow";
|
||||
import {
|
||||
NewWorkflowOnlyScopeType,
|
||||
type WorkflowTag,
|
||||
type WorkflowTags,
|
||||
} from "@/types/workflow";
|
||||
import { track } from "@/utils/analytics";
|
||||
import { isApiError, isApiErrorDetail } from "@/utils/api";
|
||||
import { DEPTH_SUPPORTED_SCOPES, isPageScopeType } from "@/utils/crawler";
|
||||
@ -258,7 +262,7 @@ export class WorkflowEditor extends BtrixElement {
|
||||
private showCrawlerChannels = false;
|
||||
|
||||
@state()
|
||||
private tagOptions: string[] = [];
|
||||
private tagOptions: WorkflowTag[] = [];
|
||||
|
||||
@state()
|
||||
private isSubmitting = false;
|
||||
@ -293,7 +297,8 @@ export class WorkflowEditor extends BtrixElement {
|
||||
});
|
||||
|
||||
// For fuzzy search:
|
||||
private readonly fuse = new Fuse<string>([], {
|
||||
private readonly fuse = new Fuse<WorkflowTag>([], {
|
||||
keys: ["tag"],
|
||||
shouldSort: false,
|
||||
threshold: 0.2, // stricter; default is 0.6
|
||||
});
|
||||
@ -2532,8 +2537,8 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
||||
private async fetchTags() {
|
||||
this.tagOptions = [];
|
||||
try {
|
||||
const tags = await this.api.fetch<string[]>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tags`,
|
||||
const { tags } = await this.api.fetch<WorkflowTags>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tagCounts`,
|
||||
);
|
||||
|
||||
// Update search/filter collection
|
||||
|
||||
@ -21,6 +21,7 @@ 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 { tw } from "@/utils/tailwind";
|
||||
|
||||
const MAX_TAGS_IN_LABEL = 5;
|
||||
@ -47,7 +48,9 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
@queryAll("sl-checkbox")
|
||||
private readonly checkboxes!: NodeListOf<SlCheckbox>;
|
||||
|
||||
private readonly fuse = new Fuse<string>([]);
|
||||
private readonly fuse = new Fuse<WorkflowTag>([], {
|
||||
keys: ["tag"],
|
||||
});
|
||||
|
||||
private selected = new Map<string, boolean>();
|
||||
|
||||
@ -63,8 +66,8 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
|
||||
private readonly orgTagsTask = new Task(this, {
|
||||
task: async () => {
|
||||
const tags = await this.api.fetch<string[]>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tags`,
|
||||
const { tags } = await this.api.fetch<WorkflowTags>(
|
||||
`/orgs/${this.orgId}/crawlconfigs/tagCounts`,
|
||||
);
|
||||
|
||||
this.fuse.setCollection(tags);
|
||||
@ -235,18 +238,18 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderList(opts: { item: string }[]) {
|
||||
const tag = (tag: string) => {
|
||||
const checked = this.selected.get(tag) === true;
|
||||
private renderList(opts: { item: WorkflowTag }[]) {
|
||||
const tag = (tag: WorkflowTag) => {
|
||||
const checked = this.selected.get(tag.tag) === true;
|
||||
|
||||
return html`
|
||||
<li role="option" aria-checked=${checked}>
|
||||
<sl-checkbox
|
||||
class="w-full part-[base]:w-full part-[base]:rounded part-[base]:p-2 part-[base]:hover:bg-primary-50 part-[base]:focus:bg-primary-50"
|
||||
value=${tag}
|
||||
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"
|
||||
value=${tag.tag}
|
||||
?checked=${checked}
|
||||
tabindex="0"
|
||||
>${tag}
|
||||
>${tag.tag}
|
||||
<btrix-badge pill variant="cyan">${tag.count}</btrix-badge>
|
||||
</sl-checkbox>
|
||||
</li>
|
||||
`;
|
||||
@ -264,36 +267,6 @@ export class WorkflowTagFilter extends BtrixElement {
|
||||
|
||||
this.selected.set(value, checked);
|
||||
}}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (!this.checkboxes.length) return;
|
||||
|
||||
// Enable focus trapping
|
||||
const options = Array.from(this.checkboxes);
|
||||
const focused = options.findIndex((opt) => opt.matches(":focus"));
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown": {
|
||||
e.preventDefault();
|
||||
options[
|
||||
focused === -1 || focused === options.length - 1
|
||||
? 0
|
||||
: focused + 1
|
||||
].focus();
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
e.preventDefault();
|
||||
options[
|
||||
focused === -1 || focused === 0
|
||||
? options.length - 1
|
||||
: focused - 1
|
||||
].focus();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
${repeat(
|
||||
opts,
|
||||
|
||||
@ -5,3 +5,12 @@ export enum NewWorkflowOnlyScopeType {
|
||||
}
|
||||
|
||||
export const WorkflowScopeType = { ...ScopeType, ...NewWorkflowOnlyScopeType };
|
||||
|
||||
export type WorkflowTag = {
|
||||
tag: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type WorkflowTags = {
|
||||
tags: WorkflowTag[];
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user