kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import {
2 findExternalLinksByTask,
3 updateExternalLink,
4} from "../../github/services/link-manager";
5import type { PluginContext, TaskTitleChangedEvent } from "../../types";
6import type { GiteaConfig } from "../config";
7import { createGiteaClient } from "../utils/gitea-api";
8
9type LinkSyncState = {
10 timestamp: string;
11 source: string;
12 value: string;
13};
14
15type LinkMetadata = {
16 lastSync?: {
17 title?: LinkSyncState;
18 };
19 [key: string]: unknown;
20};
21
22export async function handleTaskTitleChanged(
23 event: TaskTitleChangedEvent,
24 context: PluginContext,
25): Promise<void> {
26 const config = context.config as GiteaConfig;
27 if (!config.baseUrl || !config.accessToken) {
28 return;
29 }
30
31 const { repositoryOwner, repositoryName } = config;
32
33 try {
34 const links = await findExternalLinksByTask(event.taskId);
35 const issueLink = links.find(
36 (link) =>
37 link.integrationId === context.integrationId &&
38 link.resourceType === "issue",
39 );
40
41 if (!issueLink) {
42 return;
43 }
44
45 let metadata: LinkMetadata = {};
46 if (issueLink.metadata) {
47 try {
48 metadata = JSON.parse(issueLink.metadata) as LinkMetadata;
49 } catch (error) {
50 console.warn(
51 "Failed to parse Gitea issue link metadata for title sync",
52 {
53 issueLinkId: issueLink.id,
54 taskId: issueLink.taskId,
55 metadata: issueLink.metadata,
56 error,
57 },
58 );
59 }
60 }
61
62 const lastTitleSync = metadata.lastSync?.title;
63 if (lastTitleSync) {
64 if (
65 lastTitleSync.value === event.newTitle &&
66 lastTitleSync.source === "gitea"
67 ) {
68 console.log("Skipping title sync - already synced from Gitea");
69 return;
70 }
71
72 const timeSinceLastSync =
73 Date.now() - new Date(lastTitleSync.timestamp).getTime();
74 if (lastTitleSync.source === "gitea" && timeSinceLastSync < 2000) {
75 console.log(
76 `Skipping title sync - recent sync detected (${timeSinceLastSync}ms ago)`,
77 );
78 return;
79 }
80 }
81
82 const client = createGiteaClient(config);
83 const issueNumber = Number.parseInt(issueLink.externalId, 10);
84 if (Number.isNaN(issueNumber)) {
85 console.warn("Skipping Gitea title sync for invalid issue number", {
86 issueLinkId: issueLink.id,
87 externalId: issueLink.externalId,
88 taskId: issueLink.taskId,
89 });
90 return;
91 }
92
93 await client.updateIssue(repositoryOwner, repositoryName, issueNumber, {
94 title: event.newTitle,
95 });
96
97 await updateExternalLink(issueLink.id, {
98 title: event.newTitle,
99 metadata: {
100 ...metadata,
101 lastSync: {
102 ...(metadata.lastSync ?? {}),
103 title: {
104 timestamp: new Date().toISOString(),
105 source: "kaneo",
106 value: event.newTitle,
107 },
108 },
109 },
110 });
111
112 console.log(`Synced task title to Gitea issue #${issueNumber}`);
113 } catch (error) {
114 console.error("Failed to update Gitea issue title:", error);
115 }
116}