import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { awaitDeletedRecord, awaitIndexedOrSaved } from "#lib/api/awaitRecord.ts"; import { createBobbinClient, type BobbinContext, type XrpcRequestInit } from "#lib/api/client.ts"; import { createCob, eraseCob, readCob } from "#lib/api/cob.ts"; import type * as OrgTangledLabelDefinition from "#lib/api/lexicons/types/org/tangled/label/definition.ts"; import type * as OrgTangledLabelDefs from "#lib/api/lexicons/types/org/tangled/label/defs.ts"; import { collect } from "#lib/api/pagination.ts"; import type { ResolvedPathname } from "$app/types"; import type { Label } from "#lib/components/repo/LabelPanel.svelte"; import { labelHref } from "#lib/ticketQuery.ts"; import { atUriOf, datetimeOf, didOf, labelKeyOf, nsidOf, recordKeyOf, type AtUri, type Did, type KnotOrigin, type LabelKey } from "#lib/api/syntax.ts"; export type LabelDefinition = OrgTangledLabelDefinition.Main; export type LabelToken = OrgTangledLabelDefs.LabelToken; const LABEL_DEFINITION_NSID = "org.tangled.label.definition"; const LABEL_DEFINITION_COLLECTION = nsidOf(LABEL_DEFINITION_NSID); export const BASIC_LABEL_VALUE = "null"; export interface LabelView { uri: string; value: LabelDefinition; } export const listLabelDefinitions = async ( ctx: BobbinContext, repoDid: string, init?: XrpcRequestInit ): Promise => { const items = await collect( ctx, "org.tangled.label.listDefinitions", { subject: didOf(repoDid), limit: 100 }, { ...init, maxPages: 5 } ); return items.map((item) => ({ uri: item.uri, value: item.value as LabelDefinition })); }; export interface CustomLabelInput { name: string; color: string; } // the knot wants a slug as a label def's rkey and refuses a tid const labelSlugOf = (name: string): LabelKey => labelKeyOf( name .trim() .toLowerCase() .replace(/[^a-z0-9._~-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 64) ); // only the owner can make these, and an erased key can't be used again export const createRepoLabel = async ( agent: OAuthUserAgent, knot: KnotOrigin, repoDid: Did, bobbinUrl: string, { name, color }: CustomLabelInput ): Promise => { const value: LabelDefinition = { $type: "org.tangled.label.definition", name, color, createdAt: datetimeOf(new Date().toISOString()) }; const written = await createCob( agent, knot, repoDid, LABEL_DEFINITION_COLLECTION, value, recordKeyOf(labelSlugOf(name)) ); await awaitIndexedOrSaved(createBobbinClient({ serviceUrl: bobbinUrl }), written); return { uri: written.uri, value }; }; export const deleteRepoLabel = async ( agent: OAuthUserAgent, knot: KnotOrigin, bobbinUrl: string, uri: string ): Promise => { const current = await readCob(knot, atUriOf(uri)); await eraseCob(agent, knot, current); await awaitDeletedRecord(createBobbinClient({ serviceUrl: bobbinUrl }), current.uri); }; export type AppliedLabels = Record; export interface SubjectLabels { definitions: LabelView[]; applied: AppliedLabels; } export const NO_SUBJECT_LABELS: SubjectLabels = { definitions: [], applied: {} }; export const labelDefinitionUri = (repoDid: string, key: string): AtUri => atUriOf(`at://${repoDid}/${LABEL_DEFINITION_NSID}/${key}`); export const loadSubjectLabels = async ( ctx: BobbinContext, repoDid: string, tokens: readonly LabelToken[], init?: XrpcRequestInit ): Promise => ({ definitions: await listLabelDefinitions(ctx, repoDid, init), applied: Object.fromEntries( tokens.map((token) => [labelDefinitionUri(repoDid, token.key), [BASIC_LABEL_VALUE]]) ) }); export const labelsOfTokens = ( definitions: readonly LabelView[], repoDid: string, tokens: readonly LabelToken[] | undefined, listHref: string ): Label[] => { const byUri = new Map(definitions.map((definition) => [definition.uri, definition])); return (tokens ?? []).flatMap((token) => { const definition = byUri.get(labelDefinitionUri(repoDid, token.key)); if (!definition) return []; return [ { key: definition.uri, name: definition.value.name, color: definition.value.color ?? "", href: labelHref(listHref, definition.value.name) as ResolvedPathname } ]; }); };