main
js 254 lines 8.08 KB
Raw
1 import { showButtonFeedback } from "/components/messages/action-buttons/simple-action-buttons.js";
2 import { open as openSurface } from "/js/surfaces.js";
3
4 const DESKTOP_FORMATS = ["odt", "ods", "odp", "docx", "xlsx", "pptx"];
5
6 function basename(path = "") {
7 const value = String(path || "").split("?")[0].split("#")[0];
8 return value.split("/").filter(Boolean).pop() || "document";
9 }
10
11 function extensionFromPath(path = "") {
12 const name = basename(path);
13 const index = name.lastIndexOf(".");
14 return index > 0 ? name.slice(index + 1).toLowerCase() : "";
15 }
16
17 function parseMaybeJson(value) {
18 if (!value) return null;
19 if (typeof value === "object") return value;
20 if (typeof value !== "string") return null;
21 const trimmed = value.trim();
22 if (!trimmed.startsWith("{")) return null;
23 try {
24 const parsed = JSON.parse(trimmed);
25 return parsed && typeof parsed === "object" ? parsed : null;
26 } catch {
27 return null;
28 }
29 }
30
31 function truthy(value) {
32 if (value === true) return true;
33 if (value === false || value == null) return false;
34 if (typeof value === "number") return value !== 0;
35 return ["1", "true", "yes", "y", "on"].includes(String(value).trim().toLowerCase());
36 }
37
38 function firstValue(...values) {
39 for (const value of values) {
40 if (value != null && String(value).trim() !== "") return value;
41 }
42 return "";
43 }
44
45 export function parseDocumentResult(content) {
46 return parseMaybeJson(content) || {};
47 }
48
49 export function normalizeDocumentMetadata(args = {}, result = {}) {
50 const kvps = parseMaybeJson(args?.kvps) || args?.kvps || {};
51 const document = result?.document && typeof result.document === "object"
52 ? result.document
53 : {};
54 const path = String(firstValue(
55 result.path,
56 kvps.path,
57 args.path,
58 document.path,
59 ));
60 const title = String(firstValue(
61 result.title,
62 kvps.title,
63 kvps.basename,
64 args.title,
65 document.basename,
66 basename(path),
67 ));
68 const format = String(firstValue(
69 result.format,
70 result.extension,
71 kvps.format,
72 kvps.extension,
73 args.format,
74 document.extension,
75 extensionFromPath(path),
76 )).toLowerCase().replace(/^\./, "");
77
78 return {
79 action: String(firstValue(result.action, kvps.action, args.action)).toLowerCase(),
80 file_id: String(firstValue(result.file_id, kvps.file_id, args.file_id, document.file_id)),
81 path,
82 title,
83 format,
84 extension: format,
85 size: firstValue(result.size, kvps.size, document.size),
86 version: firstValue(result.version, kvps.version, args.version, document.version),
87 last_modified: firstValue(result.last_modified, kvps.last_modified, args.last_modified, document.last_modified),
88 exists: firstValue(result.exists, kvps.exists, document.exists),
89 open_in_canvas: truthy(firstValue(result.open_in_canvas, kvps.open_in_canvas, args.open_in_canvas)),
90 open_in_desktop: truthy(firstValue(result.open_in_desktop, kvps.open_in_desktop, args.open_in_desktop)),
91 };
92 }
93
94 export function documentFromLog(args = {}, result = {}) {
95 return normalizeDocumentMetadata(args, result);
96 }
97
98 export async function openDocumentInDesktop(document = {}) {
99 await openSurface("desktop", {
100 path: document.path || "",
101 file_id: document.file_id || "",
102 refresh: true,
103 source: "message-action",
104 });
105 }
106
107 export async function openOfficeArtifact(document = {}) {
108 await openDocumentInDesktop(document);
109 }
110
111 function usesDesktop(doc = {}) {
112 const format = String(doc.format || doc.extension || "").toLowerCase();
113 return DESKTOP_FORMATS.includes(format);
114 }
115
116 function canvasActionTitle(doc = {}) {
117 const format = String(doc.format || doc.extension || "").toLowerCase();
118 if (["odt", "docx"].includes(format)) return "Open in canvas with Writer";
119 if (["ods", "xlsx"].includes(format)) return "Open in canvas with Calc";
120 if (["odp", "pptx"].includes(format)) return "Open in canvas with Impress";
121 return "Open in canvas";
122 }
123
124 function documentIcon(doc = {}) {
125 const format = String(doc.format || doc.extension || "").toLowerCase();
126 if (["ods", "xlsx"].includes(format)) return "table_chart";
127 if (["odp", "pptx"].includes(format)) return "slideshow";
128 return usesDesktop(doc) ? "description" : "draft";
129 }
130
131 function statusLine(doc = {}) {
132 const parts = [];
133 if (doc.path) parts.push(doc.path);
134 if (doc.version) parts.push(`v${doc.version}`);
135 return parts.join(" | ");
136 }
137
138 export function buildDocumentFileCard(document = {}) {
139 const card = globalThis.document.createElement("span");
140 card.className = "document-file-card";
141 card.setAttribute("role", "button");
142 card.setAttribute("tabindex", "0");
143 card.setAttribute("aria-label", canvasActionTitle(document));
144 card.setAttribute("title", canvasActionTitle(document));
145
146 const icon = globalThis.document.createElement("x-icon");
147 icon.className = "document-file-card-icon";
148 icon.name = documentIcon(document);
149 card.appendChild(icon);
150
151 const meta = globalThis.document.createElement("span");
152 meta.className = "document-file-card-meta";
153
154 const name = globalThis.document.createElement("span");
155 name.className = "document-file-card-name";
156 name.textContent = document.title || basename(document.path);
157 meta.appendChild(name);
158
159 const detail = globalThis.document.createElement("span");
160 detail.className = "document-file-card-path";
161 detail.textContent = statusLine(document) || "Office artifact";
162 meta.appendChild(detail);
163 card.appendChild(meta);
164
165 if (document.format) {
166 const badge = globalThis.document.createElement("span");
167 badge.className = "document-file-card-badge";
168 badge.textContent = String(document.format).toUpperCase();
169 card.appendChild(badge);
170 }
171
172 if (document.path || document.file_id) {
173 card.addEventListener("click", () => openOfficeArtifact(document));
174 card.addEventListener("keydown", (event) => {
175 if (event.key !== "Enter" && event.key !== " ") return;
176 event.preventDefault();
177 void openOfficeArtifact(document);
178 });
179 } else {
180 card.setAttribute("aria-disabled", "true");
181 card.removeAttribute("tabindex");
182 }
183
184 return card;
185 }
186
187 export function downloadDocument(doc = {}) {
188 const path = String(doc.path || "");
189 if (!path) return;
190 const link = globalThis.document.createElement("a");
191 link.href = `/api/download_work_dir_file?path=${encodeURIComponent(path)}`;
192 link.download = String(doc.title || basename(path));
193 globalThis.document.body.appendChild(link);
194 link.click();
195 globalThis.document.body.removeChild(link);
196 }
197
198 export function createDocumentActionButton(icon, label, handler = null, options = {}) {
199 const button = globalThis.document.createElement("button");
200 button.type = "button";
201 button.className = ["action-button", "document-file-action", options.className]
202 .filter(Boolean)
203 .join(" ");
204 button.setAttribute("aria-label", options.ariaLabel || options.title || label);
205 button.setAttribute("title", options.title || label);
206
207 if (icon) {
208 const iconEl = globalThis.document.createElement("x-icon");
209 iconEl.name = icon;
210 button.appendChild(iconEl);
211 }
212
213 if (typeof handler === "function") {
214 button.addEventListener("click", async (event) => {
215 event.stopPropagation();
216 const iconEl = button.querySelector("x-icon");
217 const originalIcon = iconEl?.name || "";
218 try {
219 await handler();
220 if (originalIcon) showButtonFeedback(button, true, originalIcon);
221 } catch (err) {
222 console.error("Document action failed:", err);
223 if (originalIcon) showButtonFeedback(button, false, originalIcon);
224 }
225 });
226 }
227
228 return button;
229 }
230
231 export function buildDocumentFileActionButtons(document = {}) {
232 const hasTarget = Boolean(document?.path || document?.file_id);
233 const buttons = [];
234 if (hasTarget) {
235 buttons.push(
236 createDocumentActionButton(
237 "open_in_new",
238 "Open in canvas",
239 () => openOfficeArtifact(document),
240 {
241 className: "document-file-action-primary",
242 title: canvasActionTitle(document),
243 ariaLabel: canvasActionTitle(document),
244 },
245 ),
246 );
247 }
248 if (document?.path) {
249 buttons.push(
250 createDocumentActionButton("download", "Download", () => downloadDocument(document)),
251 );
252 }
253 return buttons;
254 }