~james/core

core/web/src/lib/api/labels.ts -rw-r--r-- 4.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
			}
		];
	});
};