@colixsystems/widget-sdk 0.168.0 → 0.170.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,7 +73,31 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
73
73
 
74
74
  ## Status
75
75
 
76
- `v0.167.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
76
+ `v0.170.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
77
+
78
+ ### What's new in 0.170.0 (contract 1.131.0)
79
+
80
+ **An action script receives the parameters a Run action event sent — `input` joins `CONTRACT.actionScriptGlobals` (sc-9031).** A button or widget event bound to "Run action" could start an Action but could not tell it anything, so authors wrote the value into a row and switched to a record trigger. The binding now carries `params`, and a widget event's payload is forwarded with them:
81
+
82
+ ```js
83
+ if (!input || typeof input.client_id !== "string") return;
84
+ await connectors.call("crm", { path: `/clients/${encodeURIComponent(input.client_id)}/sync`, method: "POST" });
85
+ ```
86
+
87
+ - **Deep-frozen, null elsewhere.** `input` is `null` on `schedule`, `record_*`, `manual` and `http_post` runs, so `if (input)` is the guard.
88
+ - **Untrusted.** It comes from the app user's device; validate it. The run still executes as the Action's bound integration identity.
89
+ - **Widget side unchanged.** A widget passes data with the emitter it already has: `useWidgetEvent("slotChosen")({ slot })`. The author binds that event to Run action.
90
+
91
+ ### What's new in 0.169.0 (contract 1.130.0)
92
+
93
+ **Widgets can create folders and copy files, including between spaces.** Two new hooks over `ctx.filestore`:
94
+
95
+ - `useFilestoreCreateFolder({ spaceType, parentFolderId? })` → `{ createFolder, creating, error, lastCreated }`. `createFolder(name, { parentFolderId? })` resolves to the new folder row; `parentFolderId` omitted or `null` places it at the space root. Refetch your `useFilestoreFolders` afterwards to show it.
96
+ - `useFilestoreCopy()` → `{ copy, copying, error, lastCopied }`. `copy(fileId, { spaceType, folderId?, name? })` duplicates a file into any space the user may add files to — project, public, or their own personal space — and resolves to the **new** file row with its own `id` and `url`. `spaceType` names the destination and is required. The bytes are duplicated server-side, so the copy and its source are independent afterwards. A name already taken at the destination lands as `name (2).ext` and never replaces the existing file.
97
+
98
+ The public space keeps its usual gate: copying into it needs the same upload access the workspace operator grants for uploads, and creating a folder there needs the right to manage that space, so an app user's public `createFolder()` is refused unless they were given it. Copying into the user's own personal space works whenever they may download the source.
99
+
100
+ Refusals keep the usual shape: branch on `err.status`, with the server's code at `err.details.code`. The copy-specific codes are `FILE_COPY_NOT_PERMITTED` (the user may only view the source), `UPLOAD_NOT_PERMITTED` (the destination refuses the add), `STORAGE_QUOTA_EXCEEDED` (413) and `FILE_PROCESSING` (409, a video still being processed). Both hooks need the `files.write:*` scope and behave identically on the web Player and the native export. Needs `@colixsystems/filestore-client` 0.10.0, which adds `files.copy(id, body)`.
77
101
 
78
102
  ### What's new in 0.167.0 (contract 1.129.0)
79
103
 
@@ -1593,7 +1617,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
1593
1617
  **Filestore browsing + BankID file signing for widgets (REQ-FS / REQ-SIGN).** Three new hooks read a newly-injected `ctx.filestore` (the `@colixsystems/filestore-client`, now constructed by both the web and native hosts):
1594
1618
  - `useFilestoreFiles({ spaceType, folderId?, q?, type? })` → `{ files, loading, error, refetch }` — browses the end-user's Filestore space. The hook resolves `owner_id` from the host context (tenant for a project space, the app user for a personal space), so the widget only picks the space. Every row carries a ready-to-render `url` (its absolutized `presigned_url`), so a gallery renders `files.map(f => f.url)` directly — no per-row `useFilestoreFile` call. Every row ALSO carries `urls` — the size ladder `{ thumbnail, card, large, hero }` at 128 / 512 / 1024 / 2048px longest edge — so a thumbnail grid should render `f.urls.thumbnail` rather than pulling the full-size original for every tile.
1595
1619
  - `useFilestoreFile(fileId)` → `{ file, url, urls, loading, error, refetch }` — resolves ONE file id to a displayable `url` (its `presigned_url`, absolutized for web + native) via `ctx.filestore.files.get(id)`. **Read the top-level `url`** — the returned `file` is `null` until the fetch resolves (and for an empty id), so `file.url` throws on the first render; it is correct only after a null check. **Never compose a file URL yourself** — the bytes are served only from a server-signed token URL, so a hand-built path like `/api/files/<id>` can never resolve (the linter's `no-host-api-url` rule flags it, including the `${location.origin}/api/files/…` form). A datastore `FILE` column holds — and reads back as — that **bare file-id string**; it is NOT hydrated into an object the way a `RELATION` (`{ id, label }`) or `USER` (`{ id, name }`) column is, so pass the value straight to the hook (no `{ id }` / `{ url }` unwrapping guard). Empty / deleted / not-found ids resolve to `url: null` so a display widget shows a fallback. When you render `url` in an `<Image>` that fills its container, size it with `aspectRatio` (e.g. `{ width: "100%", aspectRatio: 1 }`) or pixels — never a percentage `height`, which React Native collapses to 0 against a content-sized parent, so the image loads but is invisible. Requires `files.read:*`.
1596
- - `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
1620
+ - `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching. (Folder creation and file copy arrived later, in 0.169.0 — see above.)
1597
1621
  - `useFilestoreUpload({ spaceType, folderId?, compress? })` → `{ upload, uploading, error, lastUploaded }` — POSTs a multipart upload to `ctx.filestore.files.upload`. Resolves `owner_id` from the host context (like the read hooks) so the widget only picks the space + destination folder. Uploaded images are compressed to WebP by the backend; pass `compress: false` (on the hook, or per `upload(file, { compress })`) to store the file byte-for-byte — use it whenever the original matters. Pair with the `<FilePicker>` primitive for the visible trigger. Requires the `files.write:*` scope.
1598
1622
  - `useFileSignature(fileId)` → `{ status, qr, signerName, verdict, initiate, refresh, cancel, verify, … }` — drives a BankID signing flow for a file (the backend hashes the bytes server-side, binds the digest into the signature, and verifies the proof offline).
1599
1623
 
package/dist/contract.cjs CHANGED
@@ -1217,6 +1217,56 @@ const HOOKS = [
1217
1217
  requiredContextSlice: ["filestore.folders"],
1218
1218
  scopes: ["files.read:*"],
1219
1219
  },
1220
+ {
1221
+ name: "useFilestoreCreateFolder",
1222
+ signature: "useFilestoreCreateFolder({ spaceType, parentFolderId })",
1223
+ optionalArgs: ["parentFolderId"],
1224
+ description:
1225
+ "Create a folder in the end-user's Filestore space. The widget passes the " +
1226
+ "SPACE (`{ spaceType, parentFolderId? }`); the hook resolves owner_id from " +
1227
+ "the host context and posts `{ name, space_type, owner_id, parent_folder_id }` " +
1228
+ "through ctx.filestore.folders.create. `createFolder(name, { parentFolderId? })` " +
1229
+ "resolves to the created folder row or throws the wire error: 422 for an " +
1230
+ "invalid name, 404 for a parent that is not in this space, 403/404 when the " +
1231
+ "caller may not add a folder there (the server's FOLDER_CREATE_NOT_PERMITTED " +
1232
+ "is at `err.details.code`). In the public space only managers of that space " +
1233
+ "may create folders. Refetch useFilestoreFolders afterwards to show it.",
1234
+ returnShape: {
1235
+ createFolder: "(name, { parentFolderId? }) => Promise<FilestoreFolder>",
1236
+ creating: "boolean",
1237
+ error: "Error | null",
1238
+ lastCreated: "FilestoreFolder | null",
1239
+ },
1240
+ requiredContextSlice: ["filestore.folders"],
1241
+ scopes: ["files.write:*"],
1242
+ },
1243
+ {
1244
+ name: "useFilestoreCopy",
1245
+ signature: "useFilestoreCopy()",
1246
+ description:
1247
+ "Copy a Filestore file into any space and folder the caller may add files " +
1248
+ "to, including ACROSS spaces (project, public, and the signed-in user's " +
1249
+ "personal space). `copy(fileId, { spaceType, folderId?, name? })` names the " +
1250
+ "DESTINATION (spaceType is required); the hook resolves its owner_id from " +
1251
+ "the host context and posts through ctx.filestore.files.copy. Resolves to " +
1252
+ "the NEW file row (its own id and url) — the source is untouched and the " +
1253
+ "bytes are duplicated, so later changes to one never affect the other. A " +
1254
+ "name already taken at the destination lands as \"name (2).ext\" instead of " +
1255
+ "replacing it. Copying INTO public needs the same operator upload grant as " +
1256
+ "uploading there. Errors (code at `err.details.code`): 404 source not visible; " +
1257
+ "403 FILE_COPY_NOT_PERMITTED when the caller may only view the source; " +
1258
+ "403/404 UPLOAD_NOT_PERMITTED when the destination refuses the add; 413 " +
1259
+ "STORAGE_QUOTA_EXCEEDED when the destination space is full; 409 " +
1260
+ "FILE_PROCESSING while a video is still being processed.",
1261
+ returnShape: {
1262
+ copy: "(fileId, { spaceType, folderId?, name? }) => Promise<FilestoreFile>",
1263
+ copying: "boolean",
1264
+ error: "Error | null",
1265
+ lastCopied: "FilestoreFile | null",
1266
+ },
1267
+ requiredContextSlice: ["filestore.files"],
1268
+ scopes: ["files.write:*"],
1269
+ },
1220
1270
  {
1221
1271
  name: "useFileSignature",
1222
1272
  signature: "useFileSignature(fileId, existingSignatureId)",
@@ -2401,6 +2451,8 @@ const ACTION_SCRIPT_GLOBALS = [
2401
2451
  "triggerTableId",
2402
2452
  // sc-7885 — who initiated the run; null on `schedule` / `record_*`.
2403
2453
  "triggeredBy",
2454
+ // sc-9031 — a Run action event's parameters; null on every other trigger.
2455
+ "input",
2404
2456
  ];
2405
2457
  // Mirrors action.service.js SCRIPT_MAX_BYTES.
2406
2458
  const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
@@ -2692,10 +2744,10 @@ const WIDGET_CONTEXT_SHAPE = {
2692
2744
  filestore: {
2693
2745
  description:
2694
2746
  "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
2695
- "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), remove(id), preview(id) }, " +
2747
+ "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
2696
2748
  "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
2697
2749
  "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
2698
- "Backs useFilestoreFiles() + useFilestoreFolders() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
2750
+ "Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
2699
2751
  required: false,
2700
2752
  fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
2701
2753
  },
@@ -4553,7 +4605,15 @@ const CONTRACT = deepFreeze({
4553
4605
  // it removed every `<...>`, so an AI reply lost `Promise<void>`. The
4554
4606
  // emphasis grammar now follows CommonMark flanking rules, so `created_at`
4555
4607
  // keeps its underscore and `2 * 4` keeps its asterisk.
4556
- version: "1.129.0",
4608
+ // 1.130.0: additive (sc-9057) — `useFilestoreCreateFolder` and
4609
+ // `useFilestoreCopy`. Widgets can now create Filestore folders and copy a
4610
+ // file into another folder or space (project / public / personal); the
4611
+ // filestore client gains `files.copy`.
4612
+ // 1.131.0: additive (sc-9031) — `input` joins `actionScriptGlobals`: the
4613
+ // parameters a Run action event sent (the binding's `params` merged over
4614
+ // the emitting widget's payload), deep-frozen, null on every non-`app`
4615
+ // trigger. Additive: a script that never reads it runs unchanged.
4616
+ version: "1.131.0",
4557
4617
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4558
4618
  hooks: HOOKS,
4559
4619
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -1217,6 +1217,56 @@ const HOOKS = [
1217
1217
  requiredContextSlice: ["filestore.folders"],
1218
1218
  scopes: ["files.read:*"],
1219
1219
  },
1220
+ {
1221
+ name: "useFilestoreCreateFolder",
1222
+ signature: "useFilestoreCreateFolder({ spaceType, parentFolderId })",
1223
+ optionalArgs: ["parentFolderId"],
1224
+ description:
1225
+ "Create a folder in the end-user's Filestore space. The widget passes the " +
1226
+ "SPACE (`{ spaceType, parentFolderId? }`); the hook resolves owner_id from " +
1227
+ "the host context and posts `{ name, space_type, owner_id, parent_folder_id }` " +
1228
+ "through ctx.filestore.folders.create. `createFolder(name, { parentFolderId? })` " +
1229
+ "resolves to the created folder row or throws the wire error: 422 for an " +
1230
+ "invalid name, 404 for a parent that is not in this space, 403/404 when the " +
1231
+ "caller may not add a folder there (the server's FOLDER_CREATE_NOT_PERMITTED " +
1232
+ "is at `err.details.code`). In the public space only managers of that space " +
1233
+ "may create folders. Refetch useFilestoreFolders afterwards to show it.",
1234
+ returnShape: {
1235
+ createFolder: "(name, { parentFolderId? }) => Promise<FilestoreFolder>",
1236
+ creating: "boolean",
1237
+ error: "Error | null",
1238
+ lastCreated: "FilestoreFolder | null",
1239
+ },
1240
+ requiredContextSlice: ["filestore.folders"],
1241
+ scopes: ["files.write:*"],
1242
+ },
1243
+ {
1244
+ name: "useFilestoreCopy",
1245
+ signature: "useFilestoreCopy()",
1246
+ description:
1247
+ "Copy a Filestore file into any space and folder the caller may add files " +
1248
+ "to, including ACROSS spaces (project, public, and the signed-in user's " +
1249
+ "personal space). `copy(fileId, { spaceType, folderId?, name? })` names the " +
1250
+ "DESTINATION (spaceType is required); the hook resolves its owner_id from " +
1251
+ "the host context and posts through ctx.filestore.files.copy. Resolves to " +
1252
+ "the NEW file row (its own id and url) — the source is untouched and the " +
1253
+ "bytes are duplicated, so later changes to one never affect the other. A " +
1254
+ "name already taken at the destination lands as \"name (2).ext\" instead of " +
1255
+ "replacing it. Copying INTO public needs the same operator upload grant as " +
1256
+ "uploading there. Errors (code at `err.details.code`): 404 source not visible; " +
1257
+ "403 FILE_COPY_NOT_PERMITTED when the caller may only view the source; " +
1258
+ "403/404 UPLOAD_NOT_PERMITTED when the destination refuses the add; 413 " +
1259
+ "STORAGE_QUOTA_EXCEEDED when the destination space is full; 409 " +
1260
+ "FILE_PROCESSING while a video is still being processed.",
1261
+ returnShape: {
1262
+ copy: "(fileId, { spaceType, folderId?, name? }) => Promise<FilestoreFile>",
1263
+ copying: "boolean",
1264
+ error: "Error | null",
1265
+ lastCopied: "FilestoreFile | null",
1266
+ },
1267
+ requiredContextSlice: ["filestore.files"],
1268
+ scopes: ["files.write:*"],
1269
+ },
1220
1270
  {
1221
1271
  name: "useFileSignature",
1222
1272
  signature: "useFileSignature(fileId, existingSignatureId)",
@@ -2397,6 +2447,8 @@ const ACTION_SCRIPT_GLOBALS = [
2397
2447
  "triggerTableId",
2398
2448
  // sc-7885 — who initiated the run; null on `schedule` / `record_*`.
2399
2449
  "triggeredBy",
2450
+ // sc-9031 — a Run action event's parameters; null on every other trigger.
2451
+ "input",
2400
2452
  ];
2401
2453
  // Mirrors action.service.js SCRIPT_MAX_BYTES.
2402
2454
  const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
@@ -2688,10 +2740,10 @@ const WIDGET_CONTEXT_SHAPE = {
2688
2740
  filestore: {
2689
2741
  description:
2690
2742
  "Injected @colixsystems/filestore-client instance — the end-user file archive (project / personal / public spaces) + BankID file signing. " +
2691
- "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), remove(id), preview(id) }, " +
2743
+ "{ files: { list(query) -> Promise<{ data, meta }>, get(id), upload(formData), update(id, body), copy(id, body), remove(id), preview(id) }, " +
2692
2744
  "folders: { list, create, update, remove }, shares: { ... }, trash: { ... }, " +
2693
2745
  "signatures: { initiate(fileId), status(id), cancel(id), verify(id) }, objectUrl(token), fetchObject(token) }. " +
2694
- "Backs useFilestoreFiles() + useFilestoreFolders() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
2746
+ "Backs useFilestoreFiles() + useFilestoreFolders() + useFilestoreCreateFolder() + useFilestoreCopy() + useFileSignature(). Optional — a host without an end-user filestore omits it and those hooks throw a clear error.",
2695
2747
  required: false,
2696
2748
  fields: { files: "object", folders: "object", shares: "object", signatures: "object" },
2697
2749
  },
@@ -4549,7 +4601,15 @@ const CONTRACT = deepFreeze({
4549
4601
  // it removed every `<...>`, so an AI reply lost `Promise<void>`. The
4550
4602
  // emphasis grammar now follows CommonMark flanking rules, so `created_at`
4551
4603
  // keeps its underscore and `2 * 4` keeps its asterisk.
4552
- version: "1.129.0",
4604
+ // 1.130.0: additive (sc-9057) — `useFilestoreCreateFolder` and
4605
+ // `useFilestoreCopy`. Widgets can now create Filestore folders and copy a
4606
+ // file into another folder or space (project / public / personal); the
4607
+ // filestore client gains `files.copy`.
4608
+ // 1.131.0: additive (sc-9031) — `input` joins `actionScriptGlobals`: the
4609
+ // parameters a Run action event sent (the binding's `params` merged over
4610
+ // the emitting widget's payload), deep-frozen, null on every non-`app`
4611
+ // trigger. Additive: a script that never reads it runs unchanged.
4612
+ version: "1.131.0",
4553
4613
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
4554
4614
  hooks: HOOKS,
4555
4615
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -3850,7 +3850,8 @@ export function useAssetsByTag(tag, options) {
3850
3850
  * FILESTORE CLIENT — ctx.filestore (@colixsystems/filestore-client)
3851
3851
  *
3852
3852
  * The end-user file archive (project / personal spaces) + BankID file signing.
3853
- * Covers: useFilestoreFiles, useFileSignature.
3853
+ * Covers: useFilestoreFiles, useFilestoreFolders, useFilestoreCreateFolder,
3854
+ * useFilestoreCopy, useFilestoreUpload, useFileSignature.
3854
3855
  * ==========================================================================*/
3855
3856
 
3856
3857
  // Resolve the owner_id a filestore query needs from the host context: a PROJECT
@@ -4302,6 +4303,153 @@ export function useFilestoreFolders(options) {
4302
4303
  return { folders, loading, error, refetch };
4303
4304
  }
4304
4305
 
4306
+ /**
4307
+ * Create a folder in the end-user's Filestore space. Returns `{ createFolder,
4308
+ * creating, error, lastCreated }`. The widget passes the SPACE (`{ spaceType,
4309
+ * parentFolderId? }`); the hook resolves owner_id from the host context and
4310
+ * posts `{ name, space_type, owner_id, parent_folder_id }` through
4311
+ * `ctx.filestore.folders.create`.
4312
+ *
4313
+ * `createFolder(name, { parentFolderId? })` resolves to the created folder row
4314
+ * or throws the wire error: 422 for an invalid name, 404 for a parent that is
4315
+ * not in this space, and 403/404 when the caller may not add a folder there
4316
+ * (403 when they can see the position). The server's
4317
+ * `FOLDER_CREATE_NOT_PERMITTED` is at `err.details.code`.
4318
+ */
4319
+ export function useFilestoreCreateFolder(options) {
4320
+ const ctx = useWidgetContextOrThrow("useFilestoreCreateFolder");
4321
+ if (
4322
+ !ctx.filestore ||
4323
+ !ctx.filestore.folders ||
4324
+ typeof ctx.filestore.folders.create !== "function"
4325
+ ) {
4326
+ throw new Error(
4327
+ "useFilestoreCreateFolder: host did not inject a filestore client (ctx.filestore.folders.create)",
4328
+ );
4329
+ }
4330
+ const { spaceType = "project", parentFolderId: defaultParentId = null } =
4331
+ options || {};
4332
+ const ownerId = _filestoreOwnerId(ctx, spaceType);
4333
+
4334
+ const [creating, setCreating] = useState(false);
4335
+ const [error, setError] = useState(null);
4336
+ const [lastCreated, setLastCreated] = useState(null);
4337
+
4338
+ const foldersRef = useRef(ctx.filestore.folders);
4339
+ foldersRef.current = ctx.filestore.folders;
4340
+
4341
+ const createFolder = useCallback(
4342
+ async (name, overrides) => {
4343
+ if (typeof name !== "string" || name.trim().length === 0) {
4344
+ throw new Error("useFilestoreCreateFolder: name is required");
4345
+ }
4346
+ if (!ownerId) {
4347
+ const err = new Error("Sign in to create a folder");
4348
+ setError(err);
4349
+ throw err;
4350
+ }
4351
+ const parentFolderId =
4352
+ overrides &&
4353
+ Object.prototype.hasOwnProperty.call(overrides, "parentFolderId")
4354
+ ? overrides.parentFolderId
4355
+ : defaultParentId;
4356
+ const body = {
4357
+ name: name.trim(),
4358
+ space_type: String(spaceType || "project").toUpperCase(),
4359
+ owner_id: ownerId,
4360
+ };
4361
+ if (parentFolderId) body.parent_folder_id = parentFolderId;
4362
+ setCreating(true);
4363
+ setError(null);
4364
+ try {
4365
+ const created = await foldersRef.current.create(body);
4366
+ setLastCreated(created || null);
4367
+ setCreating(false);
4368
+ return created;
4369
+ } catch (err) {
4370
+ setError(err);
4371
+ setCreating(false);
4372
+ throw err;
4373
+ }
4374
+ },
4375
+ [ownerId, defaultParentId, spaceType],
4376
+ );
4377
+
4378
+ return { createFolder, creating, error, lastCreated };
4379
+ }
4380
+
4381
+ /**
4382
+ * Copy a Filestore file into any space and folder the caller may add files
4383
+ * to — including ACROSS spaces (project, public, the user's personal space).
4384
+ * Returns `{ copy, copying, error, lastCopied }`.
4385
+ *
4386
+ * `copy(fileId, { spaceType, folderId?, name? })` names the DESTINATION;
4387
+ * `spaceType` is required. The hook resolves the destination owner_id the same
4388
+ * way the upload hook does and posts through `ctx.filestore.files.copy`. It
4389
+ * resolves to the NEW file row (its own id and url); the source is untouched.
4390
+ * A name already taken at the destination lands as "name (2).ext" rather than
4391
+ * replacing that file.
4392
+ *
4393
+ * Errors: 404 when the source is not visible, 403 `FILE_COPY_NOT_PERMITTED`
4394
+ * when it is view-only, 403/404 `UPLOAD_NOT_PERMITTED` when the destination
4395
+ * refuses the add, 413 `STORAGE_QUOTA_EXCEEDED` when the destination space is
4396
+ * full, 409 `FILE_PROCESSING` while a video is still being processed. The code
4397
+ * is at `err.details.code`.
4398
+ */
4399
+ export function useFilestoreCopy() {
4400
+ const ctx = useWidgetContextOrThrow("useFilestoreCopy");
4401
+ if (
4402
+ !ctx.filestore ||
4403
+ !ctx.filestore.files ||
4404
+ typeof ctx.filestore.files.copy !== "function"
4405
+ ) {
4406
+ throw new Error(
4407
+ "useFilestoreCopy: host did not inject a filestore client (ctx.filestore.files.copy)",
4408
+ );
4409
+ }
4410
+
4411
+ const [copying, setCopying] = useState(false);
4412
+ const [error, setError] = useState(null);
4413
+ const [lastCopied, setLastCopied] = useState(null);
4414
+
4415
+ const ctxRef = useRef(ctx);
4416
+ ctxRef.current = ctx;
4417
+
4418
+ const copy = useCallback(async (fileId, destination) => {
4419
+ if (!fileId) throw new Error("useFilestoreCopy: fileId is required");
4420
+ const { spaceType, folderId, name } = destination || {};
4421
+ if (!spaceType) {
4422
+ throw new Error("useFilestoreCopy: destination spaceType is required");
4423
+ }
4424
+ const ownerId = _filestoreOwnerId(ctxRef.current, spaceType);
4425
+ if (!ownerId) {
4426
+ const err = new Error("Sign in to copy files");
4427
+ setError(err);
4428
+ throw err;
4429
+ }
4430
+ const body = {
4431
+ space_type: String(spaceType).toUpperCase(),
4432
+ owner_id: ownerId,
4433
+ };
4434
+ if (folderId) body.folder_id = folderId;
4435
+ if (name) body.name = name;
4436
+ setCopying(true);
4437
+ setError(null);
4438
+ try {
4439
+ const created = await ctxRef.current.filestore.files.copy(fileId, body);
4440
+ setLastCopied(created || null);
4441
+ setCopying(false);
4442
+ return created;
4443
+ } catch (err) {
4444
+ setError(err);
4445
+ setCopying(false);
4446
+ throw err;
4447
+ }
4448
+ }, []);
4449
+
4450
+ return { copy, copying, error, lastCopied };
4451
+ }
4452
+
4305
4453
  /**
4306
4454
  * Drive a BankID signing flow for one Filestore file. Returns
4307
4455
  * `{ status, qr, autoStartToken, message, signerName, signedAt, verdict,
package/dist/index.js CHANGED
@@ -44,6 +44,8 @@ export {
44
44
  useFilestoreUpload,
45
45
  usePdfExport,
46
46
  useFilestoreFolders,
47
+ useFilestoreCreateFolder,
48
+ useFilestoreCopy,
47
49
  useFileSignature,
48
50
  useFileSignatures,
49
51
  useFileRoster,
@@ -44,6 +44,8 @@ export {
44
44
  useFilestoreUpload,
45
45
  usePdfExport,
46
46
  useFilestoreFolders,
47
+ useFilestoreCreateFolder,
48
+ useFilestoreCopy,
47
49
  useFileSignature,
48
50
  useFileSignatures,
49
51
  useFileRoster,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.168.0",
3
+ "version": "0.170.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",