diff --git a/frontend/docs/docs/user-guide/workflow-setup.md b/frontend/docs/docs/user-guide/workflow-setup.md index 5c698c7959..3f86223c5f 100644 --- a/frontend/docs/docs/user-guide/workflow-setup.md +++ b/frontend/docs/docs/user-guide/workflow-setup.md @@ -392,6 +392,12 @@ You can use a tool like [crontab.guru](https://crontab.guru/) to check Cron synt Cron schedules are always in [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). +## Collections + +### Auto-Add to Collection + +Search for and specify [collections](collection.md) that this crawl workflow should automatically add archived items to as soon as crawling finishes. Canceled and Failed crawls will not be added to collections. + ## Metadata Describe and organize your crawl workflow and the resulting archived items. @@ -407,7 +413,3 @@ Leave optional notes about the workflow's configuration. ### Tags Apply tags to the workflow. Tags applied to the workflow will propagate to every crawl created with it at the time of crawl creation. - -### Collection Auto-Add - -Search for and specify [collections](collection.md) that this crawl workflow should automatically add archived items to as soon as crawling finishes. Canceled and Failed crawls will not be added to collections. diff --git a/frontend/src/components/ui/config-details.ts b/frontend/src/components/ui/config-details.ts index b2c6a8378a..b8c84b5832 100644 --- a/frontend/src/components/ui/config-details.ts +++ b/frontend/src/components/ui/config-details.ts @@ -16,9 +16,7 @@ import { import { labelFor } from "@/strings/crawl-workflows/labels"; import scopeTypeLabel from "@/strings/crawl-workflows/scopeType"; import sectionStrings from "@/strings/crawl-workflows/section"; -import type { Collection } from "@/types/collection"; import { WorkflowScopeType, type StorageSeedFile } from "@/types/workflow"; -import { isApiError } from "@/utils/api"; import { unescapeCustomPrefix } from "@/utils/crawl-workflows/unescapeCustomPrefix"; import { DEPTH_SUPPORTED_SCOPES, isPageScopeType } from "@/utils/crawler"; import { humanizeSchedule } from "@/utils/cron"; @@ -60,13 +58,9 @@ export class ConfigDetails extends BtrixElement { maxPagesPerCrawl?: number; }; - @state() - private collections: Collection[] = []; - async connectedCallback() { super.connectedCallback(); void this.fetchOrgDefaults(); - await this.fetchCollections(); } render() { @@ -313,6 +307,24 @@ export class ConfigDetails extends BtrixElement { )} `, })} + ${when(!this.hideMetadata, () => + this.renderSection({ + id: "collection", + heading: sectionStrings.collections, + renderDescItems: () => html` + ${this.renderSetting( + html`${msg("Auto-Add to Collection")}`, + crawlConfig?.autoAddCollections.length + ? html`` + : undefined, + )} + `, + }), + )} ${when(!this.hideMetadata, () => this.renderSection({ id: "crawl-metadata", @@ -338,21 +350,6 @@ export class ConfigDetails extends BtrixElement { ) : [], )} - ${this.renderSetting( - msg("Collections"), - this.collections.length - ? this.collections.map( - (coll) => - html` - ${coll.name} - - (${this.localize.number(coll.crawlCount)} - ${pluralOf("items", coll.crawlCount)}) - - `, - ) - : undefined, - )} `, }), )} @@ -633,44 +630,6 @@ export class ConfigDetails extends BtrixElement { `; } - private async fetchCollections() { - if (this.crawlConfig?.autoAddCollections) { - try { - await this.getCollections(); - } catch (e) { - this.notify.toast({ - message: - isApiError(e) && e.statusCode === 404 - ? msg("Collections not found.") - : msg( - "Sorry, couldn't retrieve Collection details at this time.", - ), - variant: "danger", - icon: "exclamation-octagon", - id: "collection-fetch-status", - }); - } - } - } - - private async getCollections() { - const collections: Collection[] = []; - const orgId = this.crawlConfig?.oid; - - if (this.crawlConfig?.autoAddCollections && orgId) { - for (const collectionId of this.crawlConfig.autoAddCollections) { - const data = await this.api.fetch( - `/orgs/${orgId}/collections/${collectionId}`, - ); - if (data) { - collections.push(data); - } - } - } - this.collections = collections; - this.requestUpdate(); - } - // TODO Consolidate with workflow-editor private async fetchOrgDefaults() { try { diff --git a/frontend/src/features/archived-items/item-metadata-editor.ts b/frontend/src/features/archived-items/item-metadata-editor.ts index 1825f0ae2c..d719dbee37 100644 --- a/frontend/src/features/archived-items/item-metadata-editor.ts +++ b/frontend/src/features/archived-items/item-metadata-editor.ts @@ -1,8 +1,10 @@ import { localized, msg } from "@lit/localize"; import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; import Fuse from "fuse.js"; +import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import { BtrixElement } from "@/classes/BtrixElement"; import type { TagInputEvent, Tags, @@ -12,7 +14,6 @@ import { type CollectionsChangeEvent } from "@/features/collections/collections- 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"; /** * Usage: @@ -30,7 +31,7 @@ import LiteElement, { html } from "@/utils/LiteElement"; */ @customElement("btrix-item-metadata-editor") @localized() -export class CrawlMetadataEditor extends LiteElement { +export class CrawlMetadataEditor extends BtrixElement { @property({ type: Object }) crawl?: ArchivedItem; @@ -78,7 +79,7 @@ export class CrawlMetadataEditor extends LiteElement { render() { return html` (this.isDialogVisible = true)} @sl-after-hide=${() => (this.isDialogVisible = false)} @@ -125,11 +126,11 @@ export class CrawlMetadataEditor extends LiteElement { @tags-change=${(e: TagsChangeEvent) => (this.tagsToSave = e.detail.tags)} > -
+
(this.collectionsToSave = e.detail.collections)} > @@ -166,7 +167,7 @@ export class CrawlMetadataEditor extends LiteElement { private async fetchTags() { if (!this.crawl) return; try { - const { tags } = await this.apiFetch( + const { tags } = await this.api.fetch( `/orgs/${this.crawl.oid}/crawlconfigs/tagCounts`, ); @@ -220,7 +221,7 @@ export class CrawlMetadataEditor extends LiteElement { this.isSubmittingUpdate = true; try { - const data = await this.apiFetch<{ updated: boolean }>( + const data = await this.api.fetch<{ updated: boolean }>( `/orgs/${this.crawl.oid}/all-crawls/${this.crawl.id}`, { method: "PATCH", @@ -233,7 +234,7 @@ export class CrawlMetadataEditor extends LiteElement { } this.dispatchEvent(new CustomEvent("updated")); - this.notify({ + this.notify.toast({ message: msg("Successfully saved crawl details."), variant: "success", icon: "check2-circle", @@ -241,7 +242,7 @@ export class CrawlMetadataEditor extends LiteElement { }); this.requestClose(); } catch (e) { - this.notify({ + this.notify.toast({ message: msg("Sorry, couldn't save crawl details at this time."), variant: "danger", icon: "exclamation-octagon", diff --git a/frontend/src/features/collections/collections-add.ts b/frontend/src/features/collections/collections-add.ts index 63f2e69782..bf3850ec80 100644 --- a/frontend/src/features/collections/collections-add.ts +++ b/frontend/src/features/collections/collections-add.ts @@ -9,6 +9,7 @@ import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; import type { Combobox } from "@/components/ui/combobox"; +import type { BtrixRemoveLinkedCollectionEvent } from "@/features/collections/linked-collections/types"; import type { APIPaginatedList, APIPaginationQuery, @@ -51,9 +52,6 @@ export class CollectionsAdd extends BtrixElement { @property({ type: String }) emptyText?: string; - @state() - private collectionsData: { [id: string]: Collection } = {}; - @state() private collectionIds: string[] = []; @@ -89,7 +87,6 @@ export class CollectionsAdd extends BtrixElement { this.collectionIds = this.initialCollections; } super.connectedCallback(); - void this.initializeCollectionsFromIds(); } disconnectedCallback() { @@ -99,7 +96,7 @@ export class CollectionsAdd extends BtrixElement { render() { return html`
${this.renderSearch()} @@ -109,9 +106,15 @@ export class CollectionsAdd extends BtrixElement { this.collectionIds.length ? html`
-
    - ${this.collectionIds.map(this.renderCollectionItem, this)} -
+ { + const { id } = e.detail.item; + + this.removeCollection(id); + }} + >
` : this.emptyText @@ -142,12 +145,6 @@ export class CollectionsAdd extends BtrixElement { ); if (coll) { const { id } = coll; - if (!(this.collectionsData[id] as Collection | undefined)) { - this.collectionsData = { - ...this.collectionsData, - [id]: (await this.getCollection(id))!, - }; - } this.collectionIds = [...this.collectionIds, id]; void this.dispatchChange(); } @@ -225,35 +222,7 @@ export class CollectionsAdd extends BtrixElement { }); } - private renderCollectionItem(id: string) { - const collection = this.collectionsData[id] as Collection | undefined; - return html`
  • -
    -
    - ${collection?.name} -
    -
    - ${msg(str`${collection?.crawlCount || 0} items`)} -
    - - -
    -
  • `; - } - - private removeCollection(event: Event) { - const target = event.currentTarget as HTMLElement; - const collectionId = target.getAttribute("data-key"); + private removeCollection(collectionId: string) { if (collectionId) { const collIdIndex = this.collectionIds.indexOf(collectionId); if (collIdIndex > -1) { @@ -325,24 +294,6 @@ export class CollectionsAdd extends BtrixElement { return data; } - private async initializeCollectionsFromIds() { - this.collectionIds.forEach(async (id) => { - const data = await this.getCollection(id); - if (data) { - this.collectionsData = { - ...this.collectionsData, - [id]: data, - }; - } - }); - } - - private readonly getCollection = async ( - collId: string, - ): Promise => { - return this.api.fetch(`/orgs/${this.orgId}/collections/${collId}`); - }; - private async dispatchChange() { await this.updateComplete; this.dispatchEvent( diff --git a/frontend/src/features/collections/index.ts b/frontend/src/features/collections/index.ts index a4fbc66b0a..7c43e87442 100644 --- a/frontend/src/features/collections/index.ts +++ b/frontend/src/features/collections/index.ts @@ -6,6 +6,7 @@ import("./collection-edit-dialog"); import("./collection-create-dialog"); import("./collection-initial-view-dialog"); import("./collection-workflow-list"); +import("./linked-collections"); import("./select-collection-access"); import("./select-collection-page"); import("./share-collection"); diff --git a/frontend/src/features/collections/linked-collections/index.ts b/frontend/src/features/collections/linked-collections/index.ts new file mode 100644 index 0000000000..2450e9505c --- /dev/null +++ b/frontend/src/features/collections/linked-collections/index.ts @@ -0,0 +1 @@ +import "./linked-collections"; diff --git a/frontend/src/features/collections/linked-collections/linked-collections-list.ts b/frontend/src/features/collections/linked-collections/linked-collections-list.ts new file mode 100644 index 0000000000..0beadea2cf --- /dev/null +++ b/frontend/src/features/collections/linked-collections/linked-collections-list.ts @@ -0,0 +1,119 @@ +import { localized, msg } from "@lit/localize"; +import clsx from "clsx"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { until } from "lit/directives/until.js"; + +import type { + BtrixRemoveLinkedCollectionEvent, + CollectionLikeItem, +} from "./types"; +import { isActualCollection } from "./utils"; + +import { TailwindElement } from "@/classes/TailwindElement"; +import { pluralOf } from "@/utils/pluralize"; +import { tw } from "@/utils/tailwind"; + +@customElement("btrix-linked-collections-list") +@localized() +export class LinkedCollectionsList extends TailwindElement { + @property({ type: Array }) + collections: (CollectionLikeItem & { + request?: Promise; + })[] = []; + + @property({ type: String }) + baseUrl?: string; + + @property({ type: Boolean }) + removable?: boolean; + + render() { + if (!this.collections.length) { + return; + } + + return html`
      + ${this.collections.map((item) => + item.request + ? until(item.request.then(this.renderItem)) + : this.renderItem(item, { loading: true }), + )} +
    `; + } + + private readonly renderItem = ( + item: CollectionLikeItem, + { loading } = { loading: false }, + ) => { + const actual = isActualCollection(item); + + const content = [ + html`
    ${item.name}
    `, + ]; + + if (actual) { + content.push( + html`
    + ${item.crawlCount} + ${pluralOf("items", item.crawlCount)} +
    `, + ); + } + + if (this.baseUrl) { + content.push( + html`
    + + + + +
    `, + ); + } + + if (this.removable) { + content.push( + html`
    + + + this.dispatchEvent( + new CustomEvent( + "btrix-remove", + { + detail: { + item: item, + }, + bubbles: true, + composed: true, + }, + ), + )} + > + +
    `, + ); + } + + return html`
  • + ${content} +
  • `; + }; +} diff --git a/frontend/src/features/collections/linked-collections/linked-collections.ts b/frontend/src/features/collections/linked-collections/linked-collections.ts new file mode 100644 index 0000000000..fa9240a9e5 --- /dev/null +++ b/frontend/src/features/collections/linked-collections/linked-collections.ts @@ -0,0 +1,99 @@ +import { localized } from "@lit/localize"; +import { Task } from "@lit/task"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import isEqual from "lodash/fp/isEqual"; + +import type { CollectionLikeItem } from "./types"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { Collection } from "@/types/collection"; + +import "./linked-collections-list"; + +/** + * Display list of collections that are linked to a workflow or archived item by ID. + */ +@customElement("btrix-linked-collections") +@localized() +export class LinkedCollections extends BtrixElement { + /** + * List of collection IDs, checked against values so collection data is not + * unnecessarily fetched if IDs have not changed + */ + @property({ type: Array, hasChanged: (a, b) => !isEqual(a, b) }) + collectionIds: string[] = []; + + @property({ type: Boolean }) + removable?: boolean; + + // Use a custom abort controller rather than the one provided by `Task` + // to only abort on disconnect + private collectionsTaskController = new AbortController(); + + private readonly collectionsMap = new Map< + string, + Promise + >(); + + disconnectedCallback(): void { + this.collectionsTaskController.abort(); + super.disconnectedCallback(); + } + + connectedCallback(): void { + this.collectionsTaskController = new AbortController(); + super.connectedCallback(); + } + + private readonly collectionsTask = new Task(this, { + task: async ([ids]) => { + // The API doesn't currently support getting collections by a list of IDs + const collectionsWithRequest: { + id: string; + request: Promise; + }[] = []; + + ids.forEach(async (id) => { + let request = this.collectionsMap.get(id); + + if (!request) { + request = this.fetchCollection( + id, + this.collectionsTaskController.signal, + ); + + this.collectionsMap.set(id, request); + } + + collectionsWithRequest.push({ id, request }); + }); + + return collectionsWithRequest; + }, + args: () => [this.collectionIds] as const, + }); + + render() { + const collections = + this.collectionsTask.value || this.collectionIds.map((id) => ({ id })); + + return html``; + } + + private async fetchCollection(id: string, signal: AbortSignal) { + try { + return await this.api.fetch( + `/orgs/${this.orgId}/collections/${id}`, + { signal }, + ); + } catch { + return { id }; + } + } +} diff --git a/frontend/src/features/collections/linked-collections/types.ts b/frontend/src/features/collections/linked-collections/types.ts new file mode 100644 index 0000000000..e9e6943e86 --- /dev/null +++ b/frontend/src/features/collections/linked-collections/types.ts @@ -0,0 +1,8 @@ +import type { BtrixRemoveEvent } from "@/events/btrix-remove"; +import type { Collection } from "@/types/collection"; + +// NOTE Some API endpoints return only the ID for a collection +export type CollectionLikeItem = Collection | { id: string; name?: string }; + +export type BtrixRemoveLinkedCollectionEvent = + BtrixRemoveEvent; diff --git a/frontend/src/features/collections/linked-collections/utils.ts b/frontend/src/features/collections/linked-collections/utils.ts new file mode 100644 index 0000000000..82182e3b7b --- /dev/null +++ b/frontend/src/features/collections/linked-collections/utils.ts @@ -0,0 +1,7 @@ +import type { CollectionLikeItem } from "./types"; + +import { collectionSchema, type Collection } from "@/types/collection"; + +export const isActualCollection = ( + item: CollectionLikeItem, +): item is Collection => collectionSchema.safeParse(item).success; diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index f9229deca6..cf6a74b048 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -204,6 +204,10 @@ const getDefaultProgressState = (hasConfigId = false): ProgressState => { error: false, completed: hasConfigId, }, + collections: { + error: false, + completed: hasConfigId, + }, metadata: { error: false, completed: hasConfigId, @@ -2279,6 +2283,31 @@ https://archiveweb.page/images/${"logo.svg"}`} `; }; + private renderCollections() { + return html` + ${inputCol(html` + + this.updateFormState( + { + autoAddCollections: e.detail.collections, + }, + true, + )} + > + `)} + ${this.renderHelpTextCol( + msg(`Automatically add crawls from this workflow to one or more collections + as soon as they complete. + Individual crawls can be selected from within the collection later.`), + )} + `; + } + private renderJobMetadata() { const isPageScope = isPageScopeType(this.formState.scopeType); @@ -2363,25 +2392,6 @@ https://archiveweb.page/images/${"logo.svg"}`} msg(`Create or assign this crawl (and its outputs) to one or more tags to help organize your archived items.`), )} - ${inputCol(html` - - this.updateFormState( - { - autoAddCollections: e.detail.collections, - }, - true, - )} - > - `)} - ${this.renderHelpTextCol( - msg(`Automatically add crawls from this workflow to one or more collections - as soon as they complete. - Individual crawls can be selected from within the collection later.`), - )} `; } @@ -2452,9 +2462,14 @@ https://archiveweb.page/images/${"logo.svg"}`} desc: msg("Schedule recurring crawls."), render: this.renderJobScheduling, }, + { + name: "collections", + desc: msg("Add crawls from this workflow to one or more collections."), + render: this.renderCollections, + }, { name: "metadata", - desc: msg("Describe and organize crawls from this workflow."), + desc: msg("Describe and tag this workflow and its crawls."), render: this.renderJobMetadata, }, ]; diff --git a/frontend/src/layouts/collections/metadataColumn.ts b/frontend/src/layouts/collections/metadataColumn.ts index f687cf1ee9..350602c88d 100644 --- a/frontend/src/layouts/collections/metadataColumn.ts +++ b/frontend/src/layouts/collections/metadataColumn.ts @@ -15,7 +15,7 @@ export function metadataItemWithCollection( render, }: { label: string | TemplateResult; - render: (c: PublicCollection) => TemplateResult | string; + render: (c: Collection | PublicCollection) => TemplateResult | string; }) { return html` diff --git a/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts b/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts index 32a31fcfb1..9a2c9162e2 100644 --- a/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts +++ b/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts @@ -403,13 +403,13 @@ export class ArchivedItemDetail extends BtrixElement { break; default: sectionContent = html` -
    -
    +
    +
    ${this.renderPanel(msg("Overview"), this.renderOverview(), [ tw`rounded-lg border p-4`, ])}
    -
    +
    ${this.renderPanel( html` ${this.renderTitle(msg("Metadata"))} @@ -420,7 +420,7 @@ export class ArchivedItemDetail extends BtrixElement { class="text-base" name="pencil" @click=${this.openMetadataEditor} - label=${msg("Edit Metadata")} + label=${msg("Edit Archived Item")} > `, )} @@ -429,6 +429,26 @@ export class ArchivedItemDetail extends BtrixElement { [tw`rounded-lg border p-4`], )}
    +
    + ${this.renderPanel( + html` + ${this.renderTitle(msg("Collections"))} + ${when( + this.isCrawler, + () => html` + + `, + )} + `, + this.renderCollections(), + [tw`rounded-lg border p-4`], + )} +
    `; break; @@ -653,7 +673,7 @@ export class ArchivedItemDetail extends BtrixElement { }} > - ${msg("Edit Metadata")} + ${msg("Edit Archived Item")} `, @@ -959,26 +979,26 @@ export class ArchivedItemDetail extends BtrixElement { () => html``, )} - + + `; + } + + private renderCollections() { + const noneText = html`${msg("None")}`; + return html` + + ${when( this.item, - () => + (item) => when( - this.item!.collections.length, + item.collections.length, () => html` -
      - ${this.item!.collections.map( - ({ id, name }) => - html`
    • - ${name} -
    • `, - )} -
    + `, () => noneText, ), diff --git a/frontend/src/pages/org/archived-items.ts b/frontend/src/pages/org/archived-items.ts index 2393eb5713..5096a5d33f 100644 --- a/frontend/src/pages/org/archived-items.ts +++ b/frontend/src/pages/org/archived-items.ts @@ -593,7 +593,7 @@ export class CrawlsList extends BtrixElement { }} > - ${msg("Edit Metadata")} + ${msg("Edit Archived Item")} `, diff --git a/frontend/src/pages/org/collection-detail.ts b/frontend/src/pages/org/collection-detail.ts index dc37d61b19..f05a7af653 100644 --- a/frontend/src/pages/org/collection-detail.ts +++ b/frontend/src/pages/org/collection-detail.ts @@ -639,7 +639,9 @@ export class CollectionDetail extends BtrixElement { private renderDetailItem( label: string | TemplateResult, - renderContent: (collection: PublicCollection) => TemplateResult | string, + renderContent: ( + collection: Collection | PublicCollection, + ) => TemplateResult | string, ) { return metadataItemWithCollection(this.collection)({ label, diff --git a/frontend/src/pages/public/org.ts b/frontend/src/pages/public/org.ts index 7d325f15d0..91a2f122e7 100644 --- a/frontend/src/pages/public/org.ts +++ b/frontend/src/pages/public/org.ts @@ -8,7 +8,11 @@ import queryString from "query-string"; import { BtrixElement } from "@/classes/BtrixElement"; import { page, pageHeading } from "@/layouts/page"; import type { APIPaginatedList, APISortQuery } from "@/types/api"; -import { CollectionAccess, type Collection } from "@/types/collection"; +import { + CollectionAccess, + type Collection, + type PublicCollection, +} from "@/types/collection"; import type { OrgData, PublicOrgCollections } from "@/types/org"; import { SortDirection } from "@/types/utils"; import { richText } from "@/utils/rich-text"; @@ -321,7 +325,7 @@ export class PublicOrg extends BtrixElement { } private async getUserPublicCollections({ orgId }: { orgId: string }) { - const params: APISortQuery & { + const params: APISortQuery & { access: CollectionAccess; } = { sortBy: "dateLatest", @@ -330,7 +334,7 @@ export class PublicOrg extends BtrixElement { }; const query = queryString.stringify(params); - const data = await this.api.fetch>( + const data = await this.api.fetch>( `/orgs/${orgId}/collections?${query}`, ); diff --git a/frontend/src/strings/crawl-workflows/section.ts b/frontend/src/strings/crawl-workflows/section.ts index 04ebcb13c5..1b09c14064 100644 --- a/frontend/src/strings/crawl-workflows/section.ts +++ b/frontend/src/strings/crawl-workflows/section.ts @@ -8,6 +8,7 @@ const section: Record = { behaviors: msg("Page Behavior"), browserSettings: msg("Browser Settings"), scheduling: msg("Scheduling"), + collections: msg("Collections"), metadata: msg("Metadata"), }; diff --git a/frontend/src/types/collection.ts b/frontend/src/types/collection.ts index f4837eae5d..dd06b2c795 100644 --- a/frontend/src/types/collection.ts +++ b/frontend/src/types/collection.ts @@ -25,8 +25,8 @@ export const publicCollectionSchema = z.object({ orgName: z.string(), orgPublicProfile: z.boolean(), name: z.string(), - created: z.string().datetime(), - modified: z.string().datetime(), + created: z.string().datetime().nullable(), // NOTE dates may be null for older collections since we can't backfill + modified: z.string().datetime().nullable(), caption: z.string().nullable(), description: z.string().nullable(), resources: z.array(z.string()), @@ -47,13 +47,15 @@ export const publicCollectionSchema = z.object({ totalSize: z.number(), allowPublicDownload: z.boolean(), homeUrl: z.string().url().nullable(), - homeUrlPageId: z.string().url().nullable(), + homeUrlPageId: z.string().nullable(), homeUrlTs: z.string().datetime().nullable(), access: z.nativeEnum(CollectionAccess), }); export type PublicCollection = z.infer; export const collectionSchema = publicCollectionSchema.extend({ + orgName: z.string().optional(), + orgPublicProfile: z.boolean().optional(), tags: z.array(z.string()), access: z.nativeEnum(CollectionAccess), }); diff --git a/frontend/src/types/storage.ts b/frontend/src/types/storage.ts index ac53700148..dd8b6e0689 100644 --- a/frontend/src/types/storage.ts +++ b/frontend/src/types/storage.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const storageFileSchema = z.object({ - id: z.string(), + id: z.string().optional(), name: z.string(), path: z.string().url(), hash: z.string(), diff --git a/frontend/src/utils/workflow.ts b/frontend/src/utils/workflow.ts index 5a317abab3..d6930f2fae 100644 --- a/frontend/src/utils/workflow.ts +++ b/frontend/src/utils/workflow.ts @@ -39,6 +39,7 @@ export const SECTIONS = [ "behaviors", "browserSettings", "scheduling", + "collections", "metadata", ] as const; export const sectionsEnum = z.enum(SECTIONS); @@ -50,6 +51,7 @@ export enum GuideHash { Behaviors = "page-behavior", BrowserSettings = "browser-settings", Scheduling = "scheduling", + Collections = "collections", Metadata = "metadata", } @@ -64,6 +66,7 @@ export const workflowTabToGuideHash: Record = { behaviors: GuideHash.Behaviors, browserSettings: GuideHash.BrowserSettings, scheduling: GuideHash.Scheduling, + collections: GuideHash.Collections, metadata: GuideHash.Metadata, };