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<LabelView[]> => {
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<LabelView> => {
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<void> => {
const current = await readCob<LabelDefinition>(knot, atUriOf(uri));
await eraseCob(agent, knot, current);
await awaitDeletedRecord(createBobbinClient({ serviceUrl: bobbinUrl }), current.uri);
};
export type AppliedLabels = Record<string, string[]>;
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<SubjectLabels> => ({
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
}
];
});
};