From 5d89c2b1f0f5fdfab13547ebda889a4fab6d6ab6 Mon Sep 17 00:00:00 2001 From: Tate Date: Fri, 12 Jun 2026 10:57:34 +1200 Subject: [PATCH 01/10] Little type fixes --- src/index.d.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 2af532e..61ebd5d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -252,12 +252,11 @@ export interface GetInputConfigOptions { slug: string; } -export interface FileNotFoundError extends Error { - message: 'File not found'; -} - -export interface CollectionNotFoundError extends Error { - message: 'Collection not found'; +export interface FileMetadata { + file_size: number | null; + created_at: string | null; + last_modified: string | Date | null; + data: any; } export interface CloudCannonVisualEditorAPIV1FileContent { @@ -279,7 +278,7 @@ export interface CloudCannonVisualEditorAPIV1FileContent { * @throws {FileNotFoundError} If the file is not found * @returns Promise that resolves when the body content is set */ - set(options: any): Promise; + set(content: string): Promise; addEventListener( event: 'change', @@ -304,10 +303,7 @@ export interface CloudCannonVisualEditorAPIV1FileData { * const value = await CloudCannon.data(); * ``` */ - get(options?: { - slug?: string; - rewriteUrls?: boolean; - }): Promise | any[] | undefined>; + get(options?: { slug?: string }): Promise | any[] | undefined>; /** * Sets data for a specific field @@ -439,7 +435,7 @@ export interface CloudCannonVisualEditorAPIV1File { * @throws {FileNotFoundError} If the file is not found * @returns Promise that resolves with the metadata of the file */ - metadata(): Promise; + metadata(): Promise; // /** // * Deletes a file @@ -565,7 +561,7 @@ export interface CloudCannonVisualEditorAPIV1 { * @param loadingData - Optional loading state message * @returns Promise that resolves when loading state is updated */ - setLoading(loadingData: string | undefined): Promise; + setLoading(loadingData: string | undefined): Promise; /** * Uploads a file to the editor From 89188ef435b099227c4b2dce3cb76daeb4244c4d Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:31:11 +1200 Subject: [PATCH 02/10] first pass JSDocs --- src/index.d.ts | 792 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 655 insertions(+), 137 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 61ebd5d..655a2be 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -9,13 +9,47 @@ import type { UrlInput, } from '@cloudcannon/configuration-types'; +/** + * Options for `createCustomDataPanel`, which opens a custom Data Panel in the + * Visual Editor using CloudCannon's Input types. + */ export interface CreateCustomDataPanelOptions { + /** + * A stable identifier for the panel, returned as the `panelId` and passed to + * `destroyCustomDataPanel` to close it. When omitted, CloudCannon generates a + * seven-character base-36 id (digits `0`-`9` and lowercase `a`-`z`, e.g. + * `k4j92xq`). Treat it as an opaque token. + */ id?: string; + /** The heading shown at the top of the Data Panel. */ title: string; + /** + * Called whenever someone changes a value in the panel. Receives the full + * updated data object, not a diff. + */ onChange: (data?: Record | unknown[]) => void; + /** + * Initial values for the panel, keyed by Input name. Each key becomes an + * editable field configured by `config`. + */ data?: Record | unknown[]; + /** + * Input configuration for the fields in `data`, using the same `_inputs` + * shape as a CloudCannon configuration file. + */ config?: Cascade; + /** + * A `DOMRect` (for example from `getBoundingClientRect()`) used to anchor the + * panel next to the control that opened it. When omitted, CloudCannon + * positions the panel. + */ position?: DOMRect; + /** + * When `true`, Inputs resolve against the previewed file and the Site + * configuration the same way hosted Data Panels do (for example, for + * Structure matching). When `false` (the default), only the `data` and + * `config` passed here are used. + */ allowFullDataCascade?: boolean; } @@ -178,24 +212,24 @@ export interface CloudCannonVisualEditorAPIV0 { } /** - * Options for setting data in the v2 API + * Options for `data.set()`. */ export interface SetOptions { - /** The identifier of the field to set */ + /** The slug of the field to set. */ slug: string; - /** The value to set */ + /** The new value for the field. */ value: any; } /** - * Options for editing a field in the v2 API + * Options for `data.edit()`. */ export interface EditOptions { - /** The identifier of the field to edit */ + /** The slug of the field to open for editing. */ slug: string; - /** Optional style information */ + /** Optional style hint for the editing surface. */ style?: string | null; - /** The coordinates of the edit, and the bounding rectangle of the element being edited */ + /** The click coordinates and the bounding rectangle of the element being edited, used to position the panel. */ position?: { x: number; y: number; @@ -207,84 +241,122 @@ export interface EditOptions { } /** - * Options for array operations in the v2 API + * Base options for the array-field operations. */ export interface ArrayOptions { - /** The identifier of the array field */ + /** The slug of the array field. */ slug: string; } /** - * Options for adding an array item in the v2 API + * Options for `data.addArrayItem()`. */ export interface AddArrayItemOptions extends ArrayOptions { - /** The position to insert at (null for end) */ + /** The position to insert at. Pass `null` to append to the end. */ index: number | null; - /** The value to insert */ + /** The value to insert. */ value: any; - /** The index to clone from if value isnt provided */ + /** The index to clone the new item from when `value` is not provided. */ sourceIndex?: number; } /** - * Options for moving an array item in the v2 API + * Options for `data.moveArrayItem()`. */ export interface MoveArrayItemOptions { - /** the identifier of the array field to move from */ + /** The slug of the array field to move the item from. */ fromSlug: string; - /** the identifier of the array field to move to, defaults to fromSlug if not provided */ + /** The slug of the array field to move the item to. Defaults to `fromSlug`. */ toSlug?: string; - /** The current index of the item */ + /** The current index of the item. */ fromIndex: number; - /** The target index for the item */ + /** The target index for the item. */ toIndex: number; } /** - * Options for moving an array item in the v2 API + * Options for `data.removeArrayItem()`. */ export interface RemoveArrayItemOptions extends ArrayOptions { - /** The current index of the item */ + /** The index of the item to remove. */ index: number; } +/** + * Options for `getInputConfig()`. + */ export interface GetInputConfigOptions { + /** The slug of the field whose Input configuration to resolve. */ slug: string; } +/** Metadata describing a file, returned by `File.metadata()`. */ export interface FileMetadata { + /** The file's size in bytes (the length of its raw source), or `null` if unknown. */ file_size: number | null; + /** An ISO 8601 timestamp of when the file was created, or `null` if unknown. */ created_at: string | null; + /** A timestamp of the file's most recent change, or `null` if unknown. */ last_modified: string | Date | null; + /** + * The file's resolved output data: its front matter merged with the data + * CloudCannon's build produces for it. The shape depends on the file, so this + * is loosely typed. + */ data: any; } +/** + * Body-content access for a file: everything after the front matter. Read or + * replace it as a string. + */ export interface CloudCannonVisualEditorAPIV1FileContent { /** - * Gets the body content of a file. This is the content of the file without the front matter as a string. - * @param options - Optional configuration for the value retrieval - * @returns Promise that resolves with the body content of the file - * @throws {FileNotFoundError} If the file is not found + * Returns the file's body content (everything after the front matter) as a + * string. Resolves to `undefined` if the file does not exist. + * @returns A promise for the body content string. * @example + * In this example, we read the body content of the file open in the editor. * ```javascript - * const value = await CloudCannon.content(); + * const content = await api.currentFile().content.get(); * ``` */ get(): Promise; /** - * Sets the body content of a file - * @param options - Configuration options for setting body content - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the body content is set + * Replaces the file's body content with the given string. This marks the file + * as having unsaved changes; a Team Member must save the Site to persist it. + * Resolves to `undefined` if the file does not exist. + * @param content The new body content, as a string. + * @returns A promise that resolves once the change is applied to the editor. + * @example + * In this example, we append a paragraph to the file's current body content. + * ```javascript + * const file = api.currentFile(); + * const content = await file.content.get(); + * await file.content.set(`${content}\n\nAppended paragraph.`); + * ``` */ set(content: string): Promise; + /** + * Listens for `change` events on this file's body content, fired whenever the + * content is updated in the editor. Remove the listener with + * `removeEventListener` when your integration is torn down. + * @example + * In this example, we log a message whenever the body content changes. + * ```javascript + * api.currentFile().content.addEventListener('change', () => { + * console.log('Body content changed'); + * }); + * ``` + */ addEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` listener previously added with `addEventListener`. */ removeEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, @@ -292,106 +364,142 @@ export interface CloudCannonVisualEditorAPIV1FileContent { ): void; } +/** + * Structured-data access for a file: its front matter, or the full contents of a + * data file. Read and write fields, and add, remove, or reorder array items. + */ export interface CloudCannonVisualEditorAPIV1FileData { /** - * Gets the data of a file. This will be a JSON object. This is either the data from the file or the data from front matter. - * @param options - Optional configuration for the value retrieval - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves with the data of the file + * Returns the file's structured data: the front matter of a content file, or + * the full contents of a data file (JSON, YAML, TOML). The result is an object, + * or an array when the file's top-level value is a list. Pass `slug` to read a single + * field's value instead of the whole object. Resolves to `undefined` if the + * file does not exist. + * @param options Optional `{ slug }` to read a single field. + * @returns A promise for the data: an object, an array, or `undefined`. * @example + * In this example, we read the whole front matter object, then read a single field by slug. * ```javascript - * const value = await CloudCannon.data(); + * const data = await api.currentFile().data.get(); + * const title = await api.currentFile().data.get({ slug: 'title' }); * ``` */ get(options?: { slug?: string }): Promise | any[] | undefined>; /** - * Sets data for a specific field - * @param options - Configuration options for setting data - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the data is set + * Sets a single structured-data field. This marks the file as having unsaved + * changes; a Team Member must save the Site to persist it. Resolves to + * `undefined` if the file does not exist. + * @param options The field `slug` and its new `value`. + * @returns A promise that resolves once the change is applied (with no value). * @example + * In this example, we set the `title` field to a new value. * ```javascript - * await CloudCannon.set({ - * slug: 'title', - * value: 'My Title', - * }); + * await api.currentFile().data.set({ slug: 'title', value: 'My Title' }); * ``` */ set(options: SetOptions): Promise; /** - * Initiates editing of a specific field. This will open a data panel for the field. - * @param options - Configuration options for editing - * @throws {FileNotFoundError} If the file is not found + * Opens the hosted Data Panel for a single field so a Team Member can edit it. + * This is fire-and-forget: it returns immediately and does not wait for, or + * report, the result of the edit. + * @param options The field `slug`, with optional `style` and `position`. * @example + * In this example, we open the Data Panel for the `title` field. * ```javascript - * CloudCannon.edit({ - * slug: 'title', - * style: 'panel', - * e: event, - * }); + * api.currentFile().data.edit({ slug: 'title' }); * ``` */ edit(options: EditOptions): void; /** - * Uploads a file to an input + * Uploads a file to a specific field (for example, an image or file Input) and + * returns the uploaded file's path. Resolves to `undefined` if the file does + * not exist or the upload produces no path. For a general asset upload that is + * not tied to a field, use the top-level `uploadFile`. + * @param file The file to upload. + * @param options The target field `slug`, with optional `style` and `position`. + * @returns A promise for the uploaded file's path, or `undefined`. + * @example + * In this example, we upload a file into the `hero_image` field and read its path. + * ```javascript + * const path = await api.currentFile().data.upload(file, { slug: 'hero_image' }); + * ``` */ upload(file: File, options: EditOptions): Promise; /** - * Adds an item to an array field - * @param options - Configuration options for adding an array item - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the item is added + * Adds an item to an array field. This marks the file as having unsaved + * changes; a Team Member must save the Site to persist it. Resolves to + * `undefined` if the file does not exist. + * @param options The field `slug`, the `index` to insert at (`null` to append), + * and the `value`. + * @returns A promise that resolves once the item is added. * @example + * In this example, we append a new item to the `items` array field. * ```javascript - * await CloudCannon.addArrayItem({ + * await api.currentFile().data.addArrayItem({ * slug: 'items', - * value: { title: 'New Item' }, - * e: event, + * index: null, + * value: { title: 'New item' }, * }); * ``` */ addArrayItem(options: AddArrayItemOptions): Promise; /** - * Removes an item from an array field - * @param options - Configuration options for removing an array item - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the item is removed + * Removes an item from an array field by index. This marks the file as having + * unsaved changes; a Team Member must save the Site to persist it. Resolves to + * `undefined` if the file does not exist. + * @param options The field `slug` and the `index` to remove. + * @returns A promise that resolves once the item is removed. * @example + * In this example, we remove the item at index 1 from the `items` array field. * ```javascript - * await CloudCannon.removeArrayItem({ - * slug: 'items', - * index: 1, - * }); + * await api.currentFile().data.removeArrayItem({ slug: 'items', index: 1 }); * ``` */ removeArrayItem(options: RemoveArrayItemOptions): Promise; /** - * Moves an item within an array field - * @param options - Configuration options for moving an array item - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the item is moved + * Moves an item within an array field, or between two array fields when + * `toSlug` differs from `fromSlug`. This marks the file as having unsaved + * changes; a Team Member must save the Site to persist it. Resolves to + * `undefined` if the file does not exist. + * @param options `fromSlug` and `fromIndex` for the source, and `toIndex` + * (with optional `toSlug`) for the destination. + * @returns A promise that resolves once the item is moved. * @example + * In this example, we move an item from index 1 to index 2 within the `items` array field. * ```javascript - * await CloudCannon.moveArrayItem({ - * slug: 'items', - * index: 1, + * await api.currentFile().data.moveArrayItem({ + * fromSlug: 'items', + * fromIndex: 1, * toIndex: 2, * }); * ``` */ moveArrayItem(options: MoveArrayItemOptions): Promise; + /** + * Listens for `change` events on this file's structured data, fired whenever a + * field is updated in the editor. Remove the listener with `removeEventListener` + * when your integration is torn down. + * @example + * In this example, we log a message whenever any field's data changes. + * ```javascript + * api.currentFile().data.addEventListener('change', () => { + * console.log('Data changed'); + * }); + * ``` + */ addEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` listener previously added with `addEventListener`. */ removeEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, @@ -399,121 +507,212 @@ export interface CloudCannonVisualEditorAPIV1FileData { ): void; } +/** + * A single file in your Site. Read and write its raw source, body content, and + * structured data, read its metadata, and lock it while you edit. + */ export interface CloudCannonVisualEditorAPIV1File { /** - * The path of the file + * The file's source path, relative to the Site root. + * @example + * In this example, we read a file's source path. + * ```javascript + * const path = api.currentFile().path; + * ``` */ path: string; /** - * The data of the file + * Structured-data access for this file (front matter, or a data file's contents). + * @example + * In this example, we read a field through the file's `data` object. + * ```javascript + * const title = await api.currentFile().data.get({ slug: 'title' }); + * ``` */ data: CloudCannonVisualEditorAPIV1FileData; /** - * The content of the file + * Body-content access for this file (everything after the front matter). + * @example + * In this example, we read the body through the file's `content` object. + * ```javascript + * const body = await api.currentFile().content.get(); + * ``` */ content: CloudCannonVisualEditorAPIV1FileContent; /** - * Gets the body content of a file - * @returns Promise that resolves with the body content of the file - * @throws {FileNotFoundError} If the file is not found + * Returns the file's entire raw source (front matter and body) as a string. + * Use `content.get()` for the body alone, or `data.get()` for the parsed front + * matter. Resolves to `undefined` if the file does not exist. + * @returns A promise for the raw source string. + * @example + * In this example, we read the raw source of the file open in the editor. + * ```javascript + * const raw = await api.currentFile().get(); + * ``` */ get(): Promise; /** - * Sets the raw content of a file - * @param value - The raw content to set - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves when the raw content is set + * Replaces the file's entire raw source with the given string. Use this for + * files that aren't edited through structured data, such as a + * `robots.txt`. This marks the file as having unsaved changes; a Team Member + * must save the Site to persist it. Resolves to `undefined` if the file does + * not exist. + * @param value The new raw source, as a string. + * @returns A promise that resolves once the change is applied. + * @example + * In this example, we read a file's raw source, replace some text, and write it back. + * ```javascript + * const file = api.file('/public/robots.txt'); + * const raw = await file.get(); + * await file.set(raw.replace('old text', 'new text')); + * ``` */ set(value: string): Promise; /** - * Gets the metadata of a file - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves with the metadata of the file + * Returns the file's metadata: `{ file_size, created_at, last_modified, data }`. + * Metadata is read-only and cannot be written through the API. Resolves to + * `undefined` if the file does not exist. + * @returns A promise for the file's metadata. + * @example + * In this example, we read the metadata of the file open in the editor. + * ```javascript + * const meta = await api.currentFile().metadata(); + * ``` */ metadata(): Promise; - // /** - // * Deletes a file - // * @throws {FileNotFoundError} If the file is not found - // * @returns Promise that resolves when the file is deleted - // */ - // delete(): Promise; - - // /** - // * Moves a file - // * @param options - Configuration options for moving the file - // * @throws {FileNotFoundError} If the file is not found - // * @returns Promise that resolves when the file is moved - // */ - // move(options: any): Promise; - - // /** - // * Copies a file - // * @param options - Configuration options for copying the file - // * @throws {FileNotFoundError} If the file is not found - // * @returns Promise that resolves when the file is copied - // */ - // duplicate(options: any): Promise; + // Not yet implemented: delete(), move(), duplicate(). /** - * Claims a lock on a file - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves with the lock status + * Claims an editing lock on this file so other Team Members cannot change it + * while your integration writes to it. Resolves `{ readOnly }`: when `readOnly` + * is `true`, another Team Member already holds the lock and you should not + * write. Resolves to `undefined` if the file does not exist. Release the lock + * with `releaseLock()` when you are done. + * @returns A promise for the lock status, `{ readOnly: boolean }`. + * @example + * In this example, we claim the lock, write a field only if no one else holds it, then release it. + * ```javascript + * const file = api.file('/_data/settings.yml'); + * const { readOnly } = await file.claimLock(); + * if (!readOnly) { + * await file.data.set({ slug: 'status', value: 'in-progress' }); + * await file.releaseLock(); + * } + * ``` */ claimLock(): Promise<{ readOnly: boolean }>; /** - * Releases a lock on a file - * @throws {FileNotFoundError} If the file is not found - * @returns Promise that resolves with the lock status + * Releases a lock claimed with `claimLock()`, letting other Team Members edit + * the file again. Resolves to `undefined` if the file does not exist. + * @returns A promise for the lock status, `{ readOnly: boolean }`. + * @example + * In this example, we release a lock previously claimed on a file. + * ```javascript + * await api.file('/_data/settings.yml').releaseLock(); + * ``` */ releaseLock(): Promise<{ readOnly: boolean }>; + /** + * Listens for `change` and `delete` events on this file. `change` fires when + * the file is created or updated (`event.detail.isNew` is `true` for a newly + * created file); `delete` fires when it is removed. Remove the listener with + * `removeEventListener` when your integration is torn down. + * @example + * In this example, we log messages when the file changes or is deleted. + * ```javascript + * const file = api.currentFile(); + * file.addEventListener('change', () => console.log('Changed')); + * file.addEventListener('delete', () => console.log('Deleted')); + * ``` + */ addEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** + * Returns the resolved Input configuration for a field, or `undefined` when the + * field has no configuration (or the file does not exist). + * @param options The field `slug`. + * @returns A promise for the field's Input configuration, or `undefined`. + * @example + * In this example, we read the resolved Input configuration for the `hero_image` field. + * ```javascript + * const config = await api.currentFile().getInputConfig({ slug: 'hero_image' }); + * ``` + */ getInputConfig(options: GetInputConfigOptions): Promise; } +/** + * A Collection of files, as configured under `collections_config`. List its + * files and listen for changes to them. + */ export interface CloudCannonVisualEditorAPIV1Collection { /** - * The key of the collection + * The Collection's key, as configured under `collections_config`. + * @example + * In this example, we read a Collection's key from its handle. + * ```javascript + * const collection = api.collection('posts'); + * const key = collection.collectionKey; // 'posts' + * ``` */ collectionKey: string; /** - * Gets the items in a collection - * @throws {CollectionNotFoundError} If the collection is not found - * @returns Promise that resolves with the items in the collection + * Returns every file in the Collection as an array of File objects. The array + * is empty when the Collection key isn't configured. Note: if the key is falsy, + * the returned promise never resolves, so always pass a real Collection key. + * @returns A promise for the Collection's files. + * @example + * In this example, we list every file in the `posts` Collection and log each path. + * ```javascript + * const posts = await api.collection('posts').items(); + * for (const file of posts) { + * console.log(file.path); + * } + * ``` */ items(): Promise; - // /** - // * Adds an item to a collection or triggers an add modal if the provided items are not available. - // * @param options - Configuration options for adding an item to a collection - // * @throws {CollectionNotFoundError} If the collection is not found - // * @returns Promise that resolves with the added item - // */ - // add(options: any): Promise; + // Not yet implemented: add(). + /** + * Listens for `change` and `delete` events on any file in this Collection. + * `change` fires when a file is created or updated (`event.detail.isNew` is + * `true` for a newly created file); `delete` fires when one is removed. + * `event.detail.sourcePath` identifies the file. Remove the listener with + * `removeEventListener` when your integration is torn down. + * @example + * In this example, we log the path of any post that changes. + * ```javascript + * api.collection('posts').addEventListener('change', (event) => { + * console.log('A post changed:', event.detail.sourcePath); + * }); + * ``` + */ addEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, @@ -521,23 +720,57 @@ export interface CloudCannonVisualEditorAPIV1Collection { ): void; } +/** + * A Dataset, as configured under `data_config`. Read its file or files and + * listen for changes to them. + */ export interface CloudCannonVisualEditorAPIV1Dataset { /** - * The key of the dataset + * The Dataset's key, as configured under `data_config`. + * @example + * In this example, we read a Dataset's key from its handle. + * ```javascript + * const dataset = api.dataset('locales'); + * const key = dataset.datasetKey; // 'locales' + * ``` */ datasetKey: string; /** - * Gets the items in a dataset - * @returns Promise that resolves with the items in the collection + * Returns the Dataset's file or files: a single File when the Dataset is + * configured as one file, or an array of File objects when it's a folder. + * Note: if the key is falsy or invalid, the returned promise never resolves, so + * always pass a real Dataset key. + * @returns A promise for the Dataset's file, or array of files. + * @example + * In this example, we read the `locales` Dataset's file or files, normalised to an array. + * ```javascript + * const result = await api.dataset('locales').items(); + * const items = Array.isArray(result) ? result : [result]; + * ``` */ items(): Promise; + /** + * Listens for `change` and `delete` events on any file in this Dataset. + * `change` fires when a file is created or updated (`event.detail.isNew` is + * `true` for a newly created file); `delete` fires when one is removed. + * `event.detail.sourcePath` identifies the file. Remove the listener with + * `removeEventListener` when your integration is torn down. + * @example + * In this example, we log the path of any locale that changes. + * ```javascript + * api.dataset('locales').addEventListener('change', (event) => { + * console.log('A locale changed:', event.detail.sourcePath); + * }); + * ``` + */ addEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, @@ -545,88 +778,373 @@ export interface CloudCannonVisualEditorAPIV1Dataset { ): void; } +/** + * An editable region in the page preview, created with `createTextEditableRegion`. + * Update its content programmatically with `setContent`. + */ export interface CloudCannonVisualEditorAPIV1TextEditableRegion { + /** + * Replaces the editable region's content programmatically. + * @param content The new content. Pass `null` or omit to clear it. + * @example + * In this example, we mount an editable region, then set its content programmatically. + * ```javascript + * const region = await api.createTextEditableRegion(element, onChange); + * region.setContent('

New content

'); + * ``` + */ setContent: (content?: string | null) => void; } export interface CloudCannonVisualEditorAPIV1 { /** - * Gets prefetched files - * @returns Promise that resolves with a record of file blobs + * Returns the files CloudCannon has prefetched for the current editing + * session, as a map of source path to `Blob`. + * @returns A promise for the prefetched files. + * @example + * In this example, we read the files CloudCannon prefetched for this session. + * ```javascript + * const files = await api.prefetchedFiles(); + * ``` */ prefetchedFiles(): Promise>; /** - * Sets the loading state of the editor - * @param loadingData - Optional loading state message - * @returns Promise that resolves when loading state is updated + * Shows or hides the Visual Editor's loading overlay. Pass a message to show + * the overlay while your integration performs async setup, or `undefined` to + * clear it. + * @param loadingData The message to show, or `undefined` to hide the overlay. + * @returns A promise that resolves once the loading state is applied. + * @example + * In this example, we show the loading overlay during async work, then clear it. + * ```javascript + * await api.setLoading('Loading data…'); + * // …async work… + * await api.setLoading(undefined); + * ``` */ setLoading(loadingData: string | undefined): Promise; /** - * Uploads a file to the editor - * @param file - The file to upload - * @param inputConfig - Optional configuration for the input - * @returns Promise that resolves with the path of the uploaded file + * Uploads a file through CloudCannon's asset handling and returns its path. + * Pass `undefined` as the second argument for default behaviour, or an Input + * configuration to control where the file is uploaded and which asset sources + * or DAMs are offered. To upload into a specific field, use `file.data.upload` + * instead. Resolves to `undefined` if the upload produces no path. + * @param file The file to upload. + * @param inputConfig Optional Input configuration, or `undefined` for defaults. + * @returns A promise for the uploaded file's path, or `undefined`. + * @example + * In this example, we upload an image with default asset handling. + * ```javascript + * const path = await api.uploadFile(new File([blob], 'image.png'), undefined); + * ``` */ uploadFile( file: File, inputConfig: RichTextInput | UrlInput | FileInput | undefined ): Promise; + /** + * Returns the file currently open in the Visual Editor. Not every page has an + * associated file; this throws when the open page has none, so wrap it in a + * `try`/`catch`. + * @returns The file open in the preview. + * @throws {Error} `'No current file path'` when no file is open. + * @example + * In this example, we read the open file, handling the case where the page has none. + * ```javascript + * try { + * const file = api.currentFile(); + * } catch (err) { + * // The open page has no associated file. + * } + * ``` + */ currentFile(): CloudCannonVisualEditorAPIV1File; + /** + * Returns the file at a source path, relative to the Site root, regardless of + * which page is open. This never throws; calling a method on a file whose path + * doesn't exist resolves to `undefined`. + * @param path The file's source path (for example, `/content/pages/about.md`). + * @returns The file at that path. + * @example + * In this example, we reference a file by its source path. + * ```javascript + * const about = api.file('/content/pages/about.md'); + * ``` + */ file(path: string): CloudCannonVisualEditorAPIV1File; + /** + * Returns the Collection with the given key, as configured under + * `collections_config` in your CloudCannon configuration file. + * @param key The Collection key. + * @returns The Collection. + * @example + * In this example, we reference the `posts` Collection by key. + * ```javascript + * const posts = api.collection('posts'); + * ``` + */ collection(key: string): CloudCannonVisualEditorAPIV1Collection; + /** + * Returns the Dataset with the given key, as configured under `data_config` in + * your CloudCannon configuration file. + * @param key The Dataset key. + * @returns The Dataset. + * @example + * In this example, we reference the `locales` Dataset by key. + * ```javascript + * const locales = api.dataset('locales'); + * ``` + */ dataset(key: string): CloudCannonVisualEditorAPIV1Dataset; + /** + * Returns every file in the Site as an array of File objects. + * @returns A promise for all files. + * @example + * In this example, we list every file in the Site and log each path. + * ```javascript + * const files = await api.files(); + * for (const file of files) console.log(file.path); + * ``` + */ files(): Promise; + /** + * Returns every configured Collection in the Site as an array of Collection + * objects. + * @returns A promise for all Collections. + * @example + * In this example, we list every Collection in the Site and log each key. + * ```javascript + * const collections = await api.collections(); + * for (const collection of collections) console.log(collection.collectionKey); + * ``` + */ collections(): Promise; + /** + * Listens for `change` and `delete` events across the entire Site. `change` + * fires when any file is created or updated (`event.detail.isNew` is `true` for + * a newly created file); `delete` fires when any file is removed. + * `event.detail.sourcePath` identifies the file. Remove the listener with + * `removeEventListener` when your integration is torn down. + * @example + * In this example, we log the path of any file that changes anywhere in the Site. + * ```javascript + * api.addEventListener('change', (event) => { + * console.log('Changed:', event.detail.sourcePath); + * }); + * ``` + */ addEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; + /** + * Type guard that returns `true` when `obj` is a File object. + * @example + * In this example, we narrow an unknown value to a File before using it. + * ```javascript + * if (api.isAPIFile(obj)) { + * const data = await obj.data.get(); + * } + * ``` + */ isAPIFile(obj: unknown): obj is CloudCannonVisualEditorAPIV1File; + /** + * Type guard that returns `true` when `obj` is a Collection object. + * @example + * In this example, we narrow an unknown value to a Collection before using it. + * ```javascript + * if (api.isAPICollection(obj)) { + * const items = await obj.items(); + * } + * ``` + */ isAPICollection(obj: unknown): obj is CloudCannonVisualEditorAPIV1Collection; + /** + * Type guard that returns `true` when `obj` is a Dataset object. + * @example + * In this example, we narrow an unknown value to a Dataset before using it. + * ```javascript + * if (api.isAPIDataset(obj)) { + * const items = await obj.items(); + * } + * ``` + */ isAPIDataset(obj: unknown): obj is CloudCannonVisualEditorAPIV1Dataset; + /** + * Finds the Structure value whose conditions match a given data object, the + * same way CloudCannon picks a Structure entry for a value. + * @param structure The Structure to search. + * @param value The data object to match against the Structure's values. + * @returns The matching Structure value, or `undefined` if none match. + * @example + * In this example, we find the Structure value that matches a data object. + * ```javascript + * const match = api.findStructure(structure, { type: 'hero' }); + * ``` + */ findStructure(structure: Structure, value: any): StructureValue | undefined; + /** + * Returns the Input type CloudCannon would use for a field (such as `text`, + * `image`, or `select`), based on its key, value, and any Input configuration. + * @param key The field key. + * @param value The field value. + * @param inputConfig Optional Input configuration for the field. + * @returns The resolved Input type. + * @example + * In this example, we resolve the Input type CloudCannon would use for a field. + * ```javascript + * const type = api.getInputType('hero_image', '/img.png'); + * ``` + */ getInputType(key: string | undefined, value?: unknown, inputConfig?: Input): InputType; + /** + * Makes a supported HTML element directly editable in the Visual Editor page + * preview. Clicking the element opens a rich text toolbar and edits its content + * in place. Returns a region object whose `setContent` updates the content + * programmatically. + * @param element The element to make editable. + * @param onChange Called with the updated content whenever it changes. + * @param options Optional configuration for the editable region. + * @returns A promise for the editable region. + * @throws {Error} `'Parent window not yet initialized'` when called before the editor is ready. + * @example + * In this example, we make a heading element editable in the page preview. + * ```javascript + * const region = await api.createTextEditableRegion( + * document.querySelector('#hero-heading'), + * (content) => console.log('Updated:', content), + * ); + * ``` + */ createTextEditableRegion( element: HTMLElement, onChange: (content?: string | null) => void, options?: { + /** + * The family of HTML element being edited: `text`, `block`, `span`, + * `image`, or `link`. Inferred from the tag name if omitted (for + * example, `h2` as `text`, `div` as `block`). + */ elementType?: string; + /** + * The editing mode. Use the same value as `elementType`, or `content` + * when editing body content. + */ editableType?: string; + /** Controls which rich text toolbar options are available. */ inputConfig?: RichTextInput; + /** + * The file extension used when saving the edited fragment (for example, + * `.html` or `.md`). Match it to the format of the underlying file. + */ extension?: string; } ): Promise; + /** + * Opens a custom Data Panel in the Visual Editor and resolves with its + * `panelId`. When `options.id` is omitted, CloudCannon generates a + * seven-character base-36 id. Pass the returned `panelId` to + * `destroyCustomDataPanel` to close the panel. + * @param options Configuration for the panel and its Inputs. + * @returns A promise for the panel's `panelId`. + * @example + * In this example, we open a custom Data Panel and log its data whenever it changes. + * ```javascript + * const panelId = await api.createCustomDataPanel({ + * title: 'Image SEO', + * onChange: (data) => console.log(data), + * }); + * ``` + */ createCustomDataPanel(options: CreateCustomDataPanelOptions): Promise; + /** + * Closes the custom Data Panel with the given `panelId`. + * @param id The `panelId` returned by `createCustomDataPanel`. + * @returns A promise that resolves once the panel is closed. + * @example + * In this example, we close a custom Data Panel by its id. + * ```javascript + * await api.destroyCustomDataPanel(panelId); + * ``` + */ destroyCustomDataPanel(id: string): Promise; + /** + * Returns a URL that works inside the Visual Editor for a file that may not yet + * be committed to the Site, such as an image uploaded in the current editing + * session. Use this instead of the source path when displaying media. + * @param originalUrl The source path to rewrite. + * @param inputConfig Optional Input configuration. + * @returns A promise for a preview URL. + * @throws {Error} `'Parent window not yet initialized'` when called before the editor is ready. + * @example + * In this example, we read a preview URL for an image that may not be committed yet. + * ```javascript + * const url = await api.getPreviewUrl('/images/hero.jpg'); + * ``` + */ getPreviewUrl(originalUrl: string, inputConfig?: Input): Promise; } export type CloudCannonVisualEditorAPIVersions = 'v0' | 'v1'; +/** + * The `detail` of the `cloudcannon:load` event, and the shape mixed into + * `window`: the API router on `CloudCannonAPI`, plus the v0 handler on + * `CloudCannon` for backwards compatibility. + */ export interface CloudCannonVisualEditorAPIEventDetails { CloudCannonAPI?: CloudCannonVisualEditorAPIRouter; CloudCannon?: CloudCannonVisualEditorAPIV0 | CloudCannonVisualEditorAPIV1; } +/** + * A `Window` augmented with CloudCannon's globals. Declare `window` as this type + * in modules that talk to the API to get typed access to `CloudCannonAPI`. + */ export interface CloudCannonVisualEditorWindow extends Window, CloudCannonVisualEditorAPIEventDetails {} +/** + * The router exposed on `window.CloudCannonAPI`. Call `useVersion` to get a + * versioned API object. + */ export interface CloudCannonVisualEditorAPIRouter { + /** + * Returns the v0 API handler. + * @deprecated Use `useVersion('v1', true)` instead. + * @param key The API version, `'v0'`. + * @param preventGlobalInstall When `true`, the handler is not assigned to + * `window.CloudCannon`. + */ useVersion(key: 'v0', preventGlobalInstall?: boolean): CloudCannonVisualEditorAPIV0; + /** + * Returns the v1 API object used to interact with the Visual Editor. + * @param key The API version, `'v1'`. + * @param preventGlobalInstall When `true`, the handler is not assigned to + * `window.CloudCannon`. Pass `true` to avoid version conflicts with other + * integrations (such as Bookshop). + * @returns The v1 API object. + * @example + * In this example, we obtain the v1 API object without installing it on the global window. + * ```javascript + * const api = window.CloudCannonAPI.useVersion('v1', true); + * ``` + */ useVersion(key: 'v1', preventGlobalInstall?: boolean): CloudCannonVisualEditorAPIV1; } From eef7e036e5e7f1a87c181e71dbf4f634f9b4f93c Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:25:53 +1200 Subject: [PATCH 03/10] Refine VE API JSDoc: object/property descriptions, event listeners, examples --- src/index.d.ts | 154 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 113 insertions(+), 41 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 655a2be..3b0ecd1 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -290,25 +290,29 @@ export interface GetInputConfigOptions { slug: string; } -/** Metadata describing a file, returned by `File.metadata()`. */ +/** + * Represents metadata describing a file. This object is returned by the + * `metadata()` method on a File. + */ export interface FileMetadata { - /** The file's size in bytes (the length of its raw source), or `null` if unknown. */ + /** This property holds the file's size in bytes (the length of its raw source), or `null` if unknown. */ file_size: number | null; - /** An ISO 8601 timestamp of when the file was created, or `null` if unknown. */ + /** This property holds an ISO 8601 timestamp of when the file was created, or `null` if unknown. */ created_at: string | null; - /** A timestamp of the file's most recent change, or `null` if unknown. */ + /** This property holds a timestamp of the file's most recent change, or `null` if unknown. */ last_modified: string | Date | null; /** - * The file's resolved output data: its front matter merged with the data - * CloudCannon's build produces for it. The shape depends on the file, so this - * is loosely typed. + * This property holds the file's resolved output data: its front matter merged + * with the data CloudCannon's build produces for it. The shape depends on the + * file, so this is loosely typed. */ data: any; } /** - * Body-content access for a file: everything after the front matter. Read or - * replace it as a string. + * Provides body-content access for a file: everything after the front matter. + * This object is accessed through a File's `content` property. Additionally, you + * can read or replace a file's body as a string. */ export interface CloudCannonVisualEditorAPIV1FileContent { /** @@ -356,7 +360,17 @@ export interface CloudCannonVisualEditorAPIV1FileContent { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` listener previously added with `addEventListener`. */ + /** + * Removes a `change` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for body content changes on teardown. + * ```javascript + * const content = api.currentFile().content; + * const onChange = () => console.log('Body content changed'); + * content.addEventListener('change', onChange); + * content.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, @@ -365,8 +379,10 @@ export interface CloudCannonVisualEditorAPIV1FileContent { } /** - * Structured-data access for a file: its front matter, or the full contents of a - * data file. Read and write fields, and add, remove, or reorder array items. + * Provides structured-data access for a file: its front matter, or the full + * contents of a data file. This object is accessed through a File's `data` + * property. Additionally, you can read and write a file's fields, and add, + * remove, or reorder array items. */ export interface CloudCannonVisualEditorAPIV1FileData { /** @@ -499,7 +515,17 @@ export interface CloudCannonVisualEditorAPIV1FileData { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` listener previously added with `addEventListener`. */ + /** + * Removes a `change` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for data changes on teardown. + * ```javascript + * const data = api.currentFile().data; + * const onChange = () => console.log('Data changed'); + * data.addEventListener('change', onChange); + * data.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change', listener: EventListenerOrEventListenerObject | null, @@ -508,12 +534,15 @@ export interface CloudCannonVisualEditorAPIV1FileData { } /** - * A single file in your Site. Read and write its raw source, body content, and - * structured data, read its metadata, and lock it while you edit. + * Represents a single file in your Site. This object is returned by the + * `currentFile()` and `file()` methods, and by a Collection's or Dataset's + * `items()` method. Additionally, you can read and write a File's raw source, + * body content, and structured data, read its metadata, and lock it while you + * edit. */ export interface CloudCannonVisualEditorAPIV1File { /** - * The file's source path, relative to the Site root. + * This property holds the file's source path, relative to the Site root. * @example * In this example, we read a file's source path. * ```javascript @@ -523,7 +552,7 @@ export interface CloudCannonVisualEditorAPIV1File { path: string; /** - * Structured-data access for this file (front matter, or a data file's contents). + * This property provides structured-data access for this file (front matter, or a data file's contents). * @example * In this example, we read a field through the file's `data` object. * ```javascript @@ -533,7 +562,7 @@ export interface CloudCannonVisualEditorAPIV1File { data: CloudCannonVisualEditorAPIV1FileData; /** - * Body-content access for this file (everything after the front matter). + * This property provides body-content access for this file (everything after the front matter). * @example * In this example, we read the body through the file's `content` object. * ```javascript @@ -623,8 +652,8 @@ export interface CloudCannonVisualEditorAPIV1File { /** * Listens for `change` and `delete` events on this file. `change` fires when * the file is created or updated (`event.detail.isNew` is `true` for a newly - * created file); `delete` fires when it is removed. Remove the listener with - * `removeEventListener` when your integration is torn down. + * created file), while `delete` fires when it is removed. Remove the listener + * with `removeEventListener` when your integration is torn down. * @example * In this example, we log messages when the file changes or is deleted. * ```javascript @@ -638,7 +667,17 @@ export interface CloudCannonVisualEditorAPIV1File { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ + /** + * Removes a `change` or `delete` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for changes to the file on teardown. + * ```javascript + * const file = api.currentFile(); + * const onChange = () => console.log('Changed'); + * file.addEventListener('change', onChange); + * file.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, @@ -660,12 +699,13 @@ export interface CloudCannonVisualEditorAPIV1File { } /** - * A Collection of files, as configured under `collections_config`. List its - * files and listen for changes to them. + * Represents a Collection of files, as configured under `collections_config` in your CloudCannon Configuration File in your CloudCannon Configuration File. + * This object is returned by the `collection()` and `collections()` methods. Additionally, you can call `items()` to list a Collection's + * files, or `addEventListener` to react to changes. */ export interface CloudCannonVisualEditorAPIV1Collection { /** - * The Collection's key, as configured under `collections_config`. + * This property holds the Collection's key, as configured under `collections_config` in your CloudCannon Configuration File. * @example * In this example, we read a Collection's key from its handle. * ```javascript @@ -696,9 +736,9 @@ export interface CloudCannonVisualEditorAPIV1Collection { /** * Listens for `change` and `delete` events on any file in this Collection. * `change` fires when a file is created or updated (`event.detail.isNew` is - * `true` for a newly created file); `delete` fires when one is removed. - * `event.detail.sourcePath` identifies the file. Remove the listener with - * `removeEventListener` when your integration is torn down. + * `true` for a newly created file), while `delete` fires when one is removed. + * `event.detail.sourcePath` holds the changed file's path. Remove the listener + * with `removeEventListener` when your integration is torn down. * @example * In this example, we log the path of any post that changes. * ```javascript @@ -712,7 +752,17 @@ export interface CloudCannonVisualEditorAPIV1Collection { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ + /** + * Removes a `change` or `delete` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for changes to posts on teardown. + * ```javascript + * const posts = api.collection('posts'); + * const onChange = (event) => console.log('A post changed:', event.detail.sourcePath); + * posts.addEventListener('change', onChange); + * posts.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, @@ -721,12 +771,14 @@ export interface CloudCannonVisualEditorAPIV1Collection { } /** - * A Dataset, as configured under `data_config`. Read its file or files and - * listen for changes to them. + * Represents a Dataset, as configured under `data_config` in your CloudCannon + * Configuration File. This object is returned by the `dataset()` and `datasets()` + * methods. Additionally, you can call `items()` to read a Dataset's file or + * files, or `addEventListener` to react to changes. */ export interface CloudCannonVisualEditorAPIV1Dataset { /** - * The Dataset's key, as configured under `data_config`. + * This property holds the Dataset's key, as configured under `data_config` in your CloudCannon Configuration File. * @example * In this example, we read a Dataset's key from its handle. * ```javascript @@ -754,9 +806,9 @@ export interface CloudCannonVisualEditorAPIV1Dataset { /** * Listens for `change` and `delete` events on any file in this Dataset. * `change` fires when a file is created or updated (`event.detail.isNew` is - * `true` for a newly created file); `delete` fires when one is removed. - * `event.detail.sourcePath` identifies the file. Remove the listener with - * `removeEventListener` when your integration is torn down. + * `true` for a newly created file), while `delete` fires when one is removed. + * `event.detail.sourcePath` holds the changed file's path. Remove the listener + * with `removeEventListener` when your integration is torn down. * @example * In this example, we log the path of any locale that changes. * ```javascript @@ -770,7 +822,17 @@ export interface CloudCannonVisualEditorAPIV1Dataset { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ + /** + * Removes a `change` or `delete` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for changes to locales on teardown. + * ```javascript + * const locales = api.dataset('locales'); + * const onChange = (event) => console.log('A locale changed:', event.detail.sourcePath); + * locales.addEventListener('change', onChange); + * locales.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, @@ -779,8 +841,9 @@ export interface CloudCannonVisualEditorAPIV1Dataset { } /** - * An editable region in the page preview, created with `createTextEditableRegion`. - * Update its content programmatically with `setContent`. + * Represents an editable region in the page preview. This object is returned by + * the `createTextEditableRegion()` method. Additionally, you can call + * `setContent` to update the region's content programmatically. */ export interface CloudCannonVisualEditorAPIV1TextEditableRegion { /** @@ -926,9 +989,9 @@ export interface CloudCannonVisualEditorAPIV1 { /** * Listens for `change` and `delete` events across the entire Site. `change` * fires when any file is created or updated (`event.detail.isNew` is `true` for - * a newly created file); `delete` fires when any file is removed. - * `event.detail.sourcePath` identifies the file. Remove the listener with - * `removeEventListener` when your integration is torn down. + * a newly created file), while `delete` fires when any file is removed. + * `event.detail.sourcePath` holds the changed file's path. Remove the listener + * with `removeEventListener` when your integration is torn down. * @example * In this example, we log the path of any file that changes anywhere in the Site. * ```javascript @@ -942,7 +1005,16 @@ export interface CloudCannonVisualEditorAPIV1 { listener: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean ): void; - /** Removes a `change` or `delete` listener previously added with `addEventListener`. */ + /** + * Removes a `change` or `delete` listener previously added with `addEventListener`. + * @example + * In this example, we stop listening for Site-wide changes on teardown. + * ```javascript + * const onChange = (event) => console.log('Changed:', event.detail.sourcePath); + * api.addEventListener('change', onChange); + * api.removeEventListener('change', onChange); + * ``` + */ removeEventListener( event: 'change' | 'delete', listener: EventListenerOrEventListenerObject | null, From 71abb9655b1d1b2a7ffd1e1ace8aa9c2bf98cdfc Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:59:24 +1200 Subject: [PATCH 04/10] temporary handoff file --- HANDOFF.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..68b0611 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,51 @@ +# Visual Editor API — docs handoff + +## How the documentation consumes this package + +The JSDoc in `src/index.d.ts` **is** the CloudCannon developer reference for the Visual Editor API. The `platform-documentation` site parses it at build time (with ts-morph) into: + +- the reference section (`/documentation/developer-reference/visual-editor-api/`) — an overview with an auto-generated methods table, plus one page per object type, and +- the reference data tables embedded in the VE API how-to articles. + +**All of that machinery lives on the docs side.** `platform-documentation` fetches `src/index.d.ts` from the npm CDN (jsDelivr), pinned by version, so the reference builds in CI without this repo checked out. This repo stays lean: types + JSDoc only — no parser, no build step beyond the existing `cp src/* dist/`. + +### To ship a documentation change + +1. Edit the JSDoc in `src/index.d.ts`. +2. Tag a release (`v*`) — CI publishes to npm as today. +3. The docs team bumps the pinned version in `platform-documentation` (`_lib/veapi-docs.ts` → `VEAPI_VERSION`). That single edit is the whole re-pin. + +There is nothing else to wire up on this side. + +## What's in the `docs/jsdocs-review` branch + +A full JSDoc rewrite, written from the implementation (the previous JSDoc was AI-generated and unreliable — e.g. `@throws {FileNotFoundError}` / `{CollectionNotFoundError}` that are never thrown). Highlights: + +- Hallucinated `@throws` removed; only the four real `Error` cases documented. Real failure modes (resolve-`undefined`, the `items()` falsy-key hang) described instead. +- `@example` on every method; consistent present-tense voice; CloudCannon terminology capitalized; no em dashes. +- Object interface descriptions name their origin ("This object is returned by … Additionally, you can …"); property descriptions are complete sentences ("This property holds/provides …"). +- Event listeners: `addEventListener`/`removeEventListener` documented on all six interfaces, each with an example (using a named handler, since an anonymous listener can't be removed). `event.detail` (`isNew`, `sourcePath`) documented; `sourcePath` noted as present only on Site/Collection/Dataset events. + +Comments only — no type/signature changes in this branch. + +## Open type follow-ups (maintainer) + +These need type changes the docs pass can't make, and would each improve the generated reference. + +### 1. Give `event.detail` a typed shape (new) + +`addEventListener`/`removeEventListener` use the standard DOM listener type, so `event.detail` is untyped (`any`). The docs describe it in prose, but the reference can't surface it as a first-class type the way it now does for `FileMetadata`. + +The payload has two fields: +- `isNew: boolean` — `true` when a `change` fired for a newly created file, `false` for an update. +- `sourcePath: string` — the changed file's path. Present on the Site-wide (`CloudCannonVisualEditorAPIV1`), `Collection`, and `Dataset` listeners. The `File`, `FileData`, and `FileContent` listeners are already scoped to a single known file, so their events don't include it. + +Suggested: define a `CustomEvent`-style detail interface (e.g. `FileChangeEventDetail { sourcePath: string; isNew: boolean }`, plus a narrower variant without `sourcePath` for the file-scoped listeners if worth distinguishing) and type the listener signatures against it. Low breaking risk — it narrows an untyped value to a real shape. Parallel in spirit to the `FileMetadata` typing. + +### 2. V0 `'create'` event in the listener union (low priority, app-internal) + +The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. + +## Already handled (for the record) + +Resolved in earlier maintainer pushes, noted so they aren't re-flagged: `set(content: string)`, `get(options?: { slug?: string })` (dead `rewriteUrls` removed), `metadata(): Promise` + the `FileMetadata` interface, `setLoading(): Promise`, and removal of the unused error interfaces. From 11c45a85a4834a32851cbb835920b755ebf933ee Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:56:23 +1200 Subject: [PATCH 05/10] drafting --- HANDOFF.md | 35 ++++++++++++- src/index.d.ts | 132 ++++++++++++++++++++++++++----------------------- 2 files changed, 103 insertions(+), 64 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 68b0611..c386458 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -24,7 +24,7 @@ A full JSDoc rewrite, written from the implementation (the previous JSDoc was AI - Hallucinated `@throws` removed; only the four real `Error` cases documented. Real failure modes (resolve-`undefined`, the `items()` falsy-key hang) described instead. - `@example` on every method; consistent present-tense voice; CloudCannon terminology capitalized; no em dashes. - Object interface descriptions name their origin ("This object is returned by … Additionally, you can …"); property descriptions are complete sentences ("This property holds/provides …"). -- Event listeners: `addEventListener`/`removeEventListener` documented on all six interfaces, each with an example (using a named handler, since an anonymous listener can't be removed). `event.detail` (`isNew`, `sourcePath`) documented; `sourcePath` noted as present only on Site/Collection/Dataset events. +- Event listeners: `addEventListener`/`removeEventListener` documented on all six interfaces, each with an example (using a named handler, since an anonymous listener can't be removed). Each description focuses on the listener itself (events, scope, teardown); the `event.detail` payload (`isNew`, `sourcePath`) is documented once, independently, in the reference's Events section rather than repeated in every listener. Comments only — no type/signature changes in this branch. @@ -42,7 +42,38 @@ The payload has two fields: Suggested: define a `CustomEvent`-style detail interface (e.g. `FileChangeEventDetail { sourcePath: string; isNew: boolean }`, plus a narrower variant without `sourcePath` for the file-scoped listeners if worth distinguishing) and type the listener signatures against it. Low breaking risk — it narrows an untyped value to a real shape. Parallel in spirit to the `FileMetadata` typing. -### 2. V0 `'create'` event in the listener union (low priority, app-internal) +This pairs with a deliberate docs decision: the per-interface `addEventListener` descriptions now document only the listener (events, scope, teardown) and treat `event.detail` as an independent detail, defined once in the reference's Events section. Typing `event.detail` would give that detail a real home — each listener could link to the `event.detail` type the same way return types already link to their object pages, instead of relying on the prose Events section. + +### 2. `File` methods that resolve `undefined` on a missing file (done in this branch — please confirm) + +`File.get()`, `claimLock()`, and `releaseLock()` document resolving `undefined` when the file doesn't exist, but their return types didn't reflect it. To keep the types accurate, this branch narrowed them: +- `get(): Promise` +- `claimLock(): Promise<{ readOnly: boolean } | undefined>` +- `releaseLock(): Promise<{ readOnly: boolean } | undefined>` + +This is the one place the docs pass touched types (everything else is comments only), done because the prose already promised the behavior and `metadata()`/`getInputConfig()` already model it. Please confirm it matches the implementation — and if `FileContent.get()` / `FileData.get()` resolve `undefined` on a missing file too, they likely want the same treatment (not changed here). + +### 3. Give `FileData.get` a named options interface + +`FileData.get` (`data.get`) is the only option-taking method whose options are an **inline literal** (`get(options?: { slug?: string })`) rather than a named, documented interface like every other option method (`SetOptions`, `AddArrayItemOptions`, `GetInputConfigOptions`, …). Because the inline `slug` carries no JSDoc, the reference now renders its parameter as `slug` (`string`, optional) **with no description**, while `getInputConfig` shows `slug` *with* one. It's the one inconsistent option method. + +Give it a named interface with a documented field: + +```ts +export interface GetDataOptions { + /** The slug of a single field to read, instead of the whole object. */ + slug?: string; +} +// get(options?: GetDataOptions): Promise | any[] | undefined> +``` + +Then its parameter gets a real description and matches the rest. Low breaking risk — same shape, just named. (Lighter alternative: add a doc comment to the inline field.) + +### 4. `AddArrayItemOptions.value` is typed required but documented as optional + +`value: any` is required in the type, but `data.addArrayItem` documents it as an either/or with `sourceIndex` ("Provide either `value` for a new item, or `sourceIndex` to clone an existing one"). If that's the real behavior, `value` should be `value?: any`. Please confirm against the implementation. + +### 5. V0 `'create'` event in the listener union (low priority, app-internal) The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. diff --git a/src/index.d.ts b/src/index.d.ts index 3b0ecd1..c7351c7 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -254,9 +254,9 @@ export interface ArrayOptions { export interface AddArrayItemOptions extends ArrayOptions { /** The position to insert at. Pass `null` to append to the end. */ index: number | null; - /** The value to insert. */ + /** The value to insert. Provide either `value` for a new item, or `sourceIndex` to clone an existing one. */ value: any; - /** The index to clone the new item from when `value` is not provided. */ + /** The index of an existing array item to clone, used instead of `value`. */ sourceIndex?: number; } @@ -291,18 +291,18 @@ export interface GetInputConfigOptions { } /** - * Represents metadata describing a file. This object is returned by the + * Represents metadata describing a file. Returned by the * `metadata()` method on a File. */ export interface FileMetadata { - /** This property holds the file's size in bytes (the length of its raw source), or `null` if unknown. */ + /** Holds the file's size in bytes (the length of its raw source), or `null` if unknown. */ file_size: number | null; - /** This property holds an ISO 8601 timestamp of when the file was created, or `null` if unknown. */ + /** Holds an ISO 8601 timestamp of when the file was created, or `null` if unknown. */ created_at: string | null; - /** This property holds a timestamp of the file's most recent change, or `null` if unknown. */ + /** Holds a timestamp of the file's most recent change, or `null` if unknown. */ last_modified: string | Date | null; /** - * This property holds the file's resolved output data: its front matter merged + * Holds the file's resolved output data: its front matter merged * with the data CloudCannon's build produces for it. The shape depends on the * file, so this is loosely typed. */ @@ -311,7 +311,7 @@ export interface FileMetadata { /** * Provides body-content access for a file: everything after the front matter. - * This object is accessed through a File's `content` property. Additionally, you + * Accessed through a File's `content` property. Additionally, you * can read or replace a file's body as a string. */ export interface CloudCannonVisualEditorAPIV1FileContent { @@ -344,7 +344,7 @@ export interface CloudCannonVisualEditorAPIV1FileContent { set(content: string): Promise; /** - * Listens for `change` events on this file's body content, fired whenever the + * Listens for `change` events on the file's body content, fired whenever the * content is updated in the editor. Remove the listener with * `removeEventListener` when your integration is torn down. * @example @@ -380,7 +380,7 @@ export interface CloudCannonVisualEditorAPIV1FileContent { /** * Provides structured-data access for a file: its front matter, or the full - * contents of a data file. This object is accessed through a File's `data` + * contents of a data file. Accessed through a File's `data` * property. Additionally, you can read and write a file's fields, and add, * remove, or reorder array items. */ @@ -499,7 +499,7 @@ export interface CloudCannonVisualEditorAPIV1FileData { moveArrayItem(options: MoveArrayItemOptions): Promise; /** - * Listens for `change` events on this file's structured data, fired whenever a + * Listens for `change` events on the file's structured data, fired whenever a * field is updated in the editor. Remove the listener with `removeEventListener` * when your integration is torn down. * @example @@ -534,7 +534,7 @@ export interface CloudCannonVisualEditorAPIV1FileData { } /** - * Represents a single file in your Site. This object is returned by the + * Represents a single file in your Site. Returned by the * `currentFile()` and `file()` methods, and by a Collection's or Dataset's * `items()` method. Additionally, you can read and write a File's raw source, * body content, and structured data, read its metadata, and lock it while you @@ -542,31 +542,34 @@ export interface CloudCannonVisualEditorAPIV1FileData { */ export interface CloudCannonVisualEditorAPIV1File { /** - * This property holds the file's source path, relative to the Site root. + * Holds the file's source path, relative to the Site root. * @example - * In this example, we read a file's source path. + * In this example, we read a file's source path and log it. * ```javascript * const path = api.currentFile().path; + * console.log(path); * ``` */ path: string; /** - * This property provides structured-data access for this file (front matter, or a data file's contents). + * Provides structured-data access for the file (front matter, or a data file's contents). * @example - * In this example, we read a field through the file's `data` object. + * In this example, we read the file's data through its `data` object and log it. * ```javascript - * const title = await api.currentFile().data.get({ slug: 'title' }); + * const data = await api.currentFile().data.get(); + * console.log(data); * ``` */ data: CloudCannonVisualEditorAPIV1FileData; /** - * This property provides body-content access for this file (everything after the front matter). + * Provides body-content access for the file (everything after the front matter). * @example - * In this example, we read the body through the file's `content` object. + * In this example, we read the body through the file's `content` object and log it. * ```javascript * const body = await api.currentFile().content.get(); + * console.log(body); * ``` */ content: CloudCannonVisualEditorAPIV1FileContent; @@ -577,12 +580,13 @@ export interface CloudCannonVisualEditorAPIV1File { * matter. Resolves to `undefined` if the file does not exist. * @returns A promise for the raw source string. * @example - * In this example, we read the raw source of the file open in the editor. + * In this example, we read the raw source of the file open in the editor and log it. * ```javascript * const raw = await api.currentFile().get(); + * console.log(raw); * ``` */ - get(): Promise; + get(): Promise; /** * Replaces the file's entire raw source with the given string. Use this for @@ -608,9 +612,10 @@ export interface CloudCannonVisualEditorAPIV1File { * `undefined` if the file does not exist. * @returns A promise for the file's metadata. * @example - * In this example, we read the metadata of the file open in the editor. + * In this example, we read the metadata of the file open in the editor and log it. * ```javascript * const meta = await api.currentFile().metadata(); + * console.log(meta); * ``` */ metadata(): Promise; @@ -618,16 +623,16 @@ export interface CloudCannonVisualEditorAPIV1File { // Not yet implemented: delete(), move(), duplicate(). /** - * Claims an editing lock on this file so other Team Members cannot change it - * while your integration writes to it. Resolves `{ readOnly }`: when `readOnly` - * is `true`, another Team Member already holds the lock and you should not - * write. Resolves to `undefined` if the file does not exist. Release the lock - * with `releaseLock()` when you are done. + * Claims an editing lock on the file so other Team Members cannot change it + * while your integration writes to it. Resolves to `{ readOnly }`. When + * `readOnly` is `true`, another Team Member already holds the lock for that + * file and your integration should not write. Resolves to `undefined` if the + * file does not exist. Release the lock with `releaseLock()` when you are done. * @returns A promise for the lock status, `{ readOnly: boolean }`. * @example - * In this example, we claim the lock, write a field only if no one else holds it, then release it. + * In this example, we claim the lock on the file we're editing, write a field only if no one else holds it, then release it. * ```javascript - * const file = api.file('/_data/settings.yml'); + * const file = api.currentFile(); * const { readOnly } = await file.claimLock(); * if (!readOnly) { * await file.data.set({ slug: 'status', value: 'in-progress' }); @@ -635,25 +640,24 @@ export interface CloudCannonVisualEditorAPIV1File { * } * ``` */ - claimLock(): Promise<{ readOnly: boolean }>; + claimLock(): Promise<{ readOnly: boolean } | undefined>; /** * Releases a lock claimed with `claimLock()`, letting other Team Members edit * the file again. Resolves to `undefined` if the file does not exist. * @returns A promise for the lock status, `{ readOnly: boolean }`. * @example - * In this example, we release a lock previously claimed on a file. + * In this example, we release a lock on the file we're editing. * ```javascript - * await api.file('/_data/settings.yml').releaseLock(); + * await api.currentFile().releaseLock(); * ``` */ - releaseLock(): Promise<{ readOnly: boolean }>; + releaseLock(): Promise<{ readOnly: boolean } | undefined>; /** - * Listens for `change` and `delete` events on this file. `change` fires when - * the file is created or updated (`event.detail.isNew` is `true` for a newly - * created file), while `delete` fires when it is removed. Remove the listener - * with `removeEventListener` when your integration is torn down. + * Listens for `change` and `delete` events on the file. `change` fires when + * the file is created or updated, and `delete` when it is removed. Remove the + * listener with `removeEventListener` when your integration is torn down. * @example * In this example, we log messages when the file changes or is deleted. * ```javascript @@ -688,24 +692,27 @@ export interface CloudCannonVisualEditorAPIV1File { * Returns the resolved Input configuration for a field, or `undefined` when the * field has no configuration (or the file does not exist). * @param options The field `slug`. - * @returns A promise for the field's Input configuration, or `undefined`. + * @returns A promise for the field's Input configuration. * @example - * In this example, we read the resolved Input configuration for the `hero_image` field. + * In this example, we read the resolved Input configuration for the `hero_image` field and log it. * ```javascript * const config = await api.currentFile().getInputConfig({ slug: 'hero_image' }); + * console.log(config); * ``` */ getInputConfig(options: GetInputConfigOptions): Promise; } /** - * Represents a Collection of files, as configured under `collections_config` in your CloudCannon Configuration File in your CloudCannon Configuration File. - * This object is returned by the `collection()` and `collections()` methods. Additionally, you can call `items()` to list a Collection's - * files, or `addEventListener` to react to changes. + * Represents a Collection of files, as configured under `collections_config` in + * your CloudCannon Configuration File. Returned by the `collection()` and + * `collections()` methods. Additionally, you can call `items()` to list a + * Collection's files, or `addEventListener` to react to changes. */ export interface CloudCannonVisualEditorAPIV1Collection { /** - * This property holds the Collection's key, as configured under `collections_config` in your CloudCannon Configuration File. + * Holds the Collection's key, as configured under `collections_config` in + * your CloudCannon Configuration File. * @example * In this example, we read a Collection's key from its handle. * ```javascript @@ -717,7 +724,7 @@ export interface CloudCannonVisualEditorAPIV1Collection { /** * Returns every file in the Collection as an array of File objects. The array - * is empty when the Collection key isn't configured. Note: if the key is falsy, + * is empty when the Collection key isn't configured. If the key is falsy, * the returned promise never resolves, so always pass a real Collection key. * @returns A promise for the Collection's files. * @example @@ -735,10 +742,9 @@ export interface CloudCannonVisualEditorAPIV1Collection { /** * Listens for `change` and `delete` events on any file in this Collection. - * `change` fires when a file is created or updated (`event.detail.isNew` is - * `true` for a newly created file), while `delete` fires when one is removed. - * `event.detail.sourcePath` holds the changed file's path. Remove the listener - * with `removeEventListener` when your integration is torn down. + * `change` fires when a file is created or updated, and `delete` when one is + * removed. Remove the listener with `removeEventListener` when your integration + * is torn down. * @example * In this example, we log the path of any post that changes. * ```javascript @@ -772,13 +778,14 @@ export interface CloudCannonVisualEditorAPIV1Collection { /** * Represents a Dataset, as configured under `data_config` in your CloudCannon - * Configuration File. This object is returned by the `dataset()` and `datasets()` + * Configuration File. Returned by the `dataset()` and `datasets()` * methods. Additionally, you can call `items()` to read a Dataset's file or * files, or `addEventListener` to react to changes. */ export interface CloudCannonVisualEditorAPIV1Dataset { /** - * This property holds the Dataset's key, as configured under `data_config` in your CloudCannon Configuration File. + * Holds the Dataset's key, as configured under `data_config` in your + * CloudCannon Configuration File. * @example * In this example, we read a Dataset's key from its handle. * ```javascript @@ -791,24 +798,26 @@ export interface CloudCannonVisualEditorAPIV1Dataset { /** * Returns the Dataset's file or files: a single File when the Dataset is * configured as one file, or an array of File objects when it's a folder. - * Note: if the key is falsy or invalid, the returned promise never resolves, so + * If the key is falsy or invalid, the returned promise never resolves, so * always pass a real Dataset key. * @returns A promise for the Dataset's file, or array of files. * @example - * In this example, we read the `locales` Dataset's file or files, normalised to an array. + * In this example, we read the `locales` Dataset's file or files, normalized to an array. * ```javascript * const result = await api.dataset('locales').items(); * const items = Array.isArray(result) ? result : [result]; + * for (const file of items) { + * console.log(file.path); + * } * ``` */ items(): Promise; /** * Listens for `change` and `delete` events on any file in this Dataset. - * `change` fires when a file is created or updated (`event.detail.isNew` is - * `true` for a newly created file), while `delete` fires when one is removed. - * `event.detail.sourcePath` holds the changed file's path. Remove the listener - * with `removeEventListener` when your integration is torn down. + * `change` fires when a file is created or updated, and `delete` when one is + * removed. Remove the listener with `removeEventListener` when your integration + * is torn down. * @example * In this example, we log the path of any locale that changes. * ```javascript @@ -841,7 +850,7 @@ export interface CloudCannonVisualEditorAPIV1Dataset { } /** - * Represents an editable region in the page preview. This object is returned by + * Represents an editable region in the page preview. Returned by * the `createTextEditableRegion()` method. Additionally, you can call * `setContent` to update the region's content programmatically. */ @@ -890,7 +899,7 @@ export interface CloudCannonVisualEditorAPIV1 { /** * Uploads a file through CloudCannon's asset handling and returns its path. - * Pass `undefined` as the second argument for default behaviour, or an Input + * Pass `undefined` as the second argument for default behavior, or an Input * configuration to control where the file is uploaded and which asset sources * or DAMs are offered. To upload into a specific field, use `file.data.upload` * instead. Resolves to `undefined` if the upload produces no path. @@ -988,10 +997,9 @@ export interface CloudCannonVisualEditorAPIV1 { /** * Listens for `change` and `delete` events across the entire Site. `change` - * fires when any file is created or updated (`event.detail.isNew` is `true` for - * a newly created file), while `delete` fires when any file is removed. - * `event.detail.sourcePath` holds the changed file's path. Remove the listener - * with `removeEventListener` when your integration is torn down. + * fires when any file is created or updated, and `delete` when any file is + * removed. Remove the listener with `removeEventListener` when your integration + * is torn down. * @example * In this example, we log the path of any file that changes anywhere in the Site. * ```javascript From 7eaa969f02879afd4aec0a7df8e2ff6886d8d73a Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:24:22 +1200 Subject: [PATCH 06/10] finish jsdocs review --- HANDOFF.md | 90 ++++++++++++++++++++++++--- src/index.d.ts | 164 +++++++++++++++++++++++++++++++------------------ 2 files changed, 186 insertions(+), 68 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index c386458..11db379 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,5 +1,27 @@ # Visual Editor API — docs handoff +## Previewing your JSDoc edits in the docs locally + +You can see how your `src/index.d.ts` changes will render in the reference before publishing, as long as both repos are checked out as siblings in the same parent folder: + +``` +your-workspace/ +├── visual-editor-api/ ← this repo (edit src/index.d.ts here) +└── platform-documentation/ ← the docs site +``` + +Then, from the `platform-documentation` repo: + +```sh +VEAPI_LOCAL=1 deno task serve +``` + +Open the reference at **http://localhost:9010/documentation/developer-reference/visual-editor-api/**. + +- `VEAPI_LOCAL=1` tells the docs parser to read your local `../visual-editor-api/src/index.d.ts` working tree instead of the published npm version, so your unpublished JSDoc edits show up live. Without it, the docs build against the pinned release (see "To ship a documentation change" below) and you'll see the shipped reference, not your edits. +- The JSDoc is parsed once when the serve task starts. The sibling repo sits outside the docs site's watch tree, so if you edit `src/index.d.ts` while the server is running and the page doesn't update, restart the serve task to re-parse. +- Don't run `deno task build` and `deno task serve` at the same time — they both write to `_site` and will clash. + ## How the documentation consumes this package The JSDoc in `src/index.d.ts` **is** the CloudCannon developer reference for the Visual Editor API. The `platform-documentation` site parses it at build time (with ts-morph) into: @@ -51,23 +73,24 @@ This pairs with a deliberate docs decision: the per-interface `addEventListener` - `claimLock(): Promise<{ readOnly: boolean } | undefined>` - `releaseLock(): Promise<{ readOnly: boolean } | undefined>` -This is the one place the docs pass touched types (everything else is comments only), done because the prose already promised the behavior and `metadata()`/`getInputConfig()` already model it. Please confirm it matches the implementation — and if `FileContent.get()` / `FileData.get()` resolve `undefined` on a missing file too, they likely want the same treatment (not changed here). +This is the one place the docs pass touched types (everything else is comments only), done because the prose already promised the behavior and `metadata()`/`getInputConfig()` already model it. Please confirm it matches the implementation. + +One specific inconsistency the JSDoc review surfaced: **`FileContent.get()` is typed `Promise` but its prose says it resolves to `undefined` when the file doesn't exist.** `FileData.get()` already returns `Record | any[] | undefined`, so `FileContent.get()` is the outlier — narrow it to `Promise` to match its own prose and `FileData.get()` (confirm against the implementation). -### 3. Give `FileData.get` a named options interface +### 3. Give `FileData.get` a named options interface (optional cleanup) -`FileData.get` (`data.get`) is the only option-taking method whose options are an **inline literal** (`get(options?: { slug?: string })`) rather than a named, documented interface like every other option method (`SetOptions`, `AddArrayItemOptions`, `GetInputConfigOptions`, …). Because the inline `slug` carries no JSDoc, the reference now renders its parameter as `slug` (`string`, optional) **with no description**, while `getInputConfig` shows `slug` *with* one. It's the one inconsistent option method. +`FileData.get` (`data.get`) is the only option-taking method whose options are an **inline literal** (`get(options?: { slug?: string })`) rather than a named, documented interface like every other option method (`SetOptions`, `AddArrayItemOptions`, `GetInputConfigOptions`, …). -Give it a named interface with a documented field: +The missing-description symptom is **already fixed in this branch** by adding a doc comment to the inline field, so the reference now renders `slug` with a description like every other option method: ```ts -export interface GetDataOptions { +get(options?: { /** The slug of a single field to read, instead of the whole object. */ slug?: string; -} -// get(options?: GetDataOptions): Promise | any[] | undefined> +}): Promise | any[] | undefined>; ``` -Then its parameter gets a real description and matches the rest. Low breaking risk — same shape, just named. (Lighter alternative: add a doc comment to the inline field.) +This is purely optional cleanup now: extracting the literal into a named `GetDataOptions` interface would make it consistent in *shape* with the other option methods (and easier to reuse), but it's no longer needed to get a documented parameter. Low breaking risk — same shape, just named. ### 4. `AddArrayItemOptions.value` is typed required but documented as optional @@ -77,6 +100,57 @@ Then its parameter gets a real description and matches the rest. Low breaking ri The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. +### 6. `FileMetadata.last_modified` is typed `string | Date` (likely serialization-inaccurate) + +`last_modified` is typed `string | Date | null`, but `created_at` (the sibling field) is `string | null`. Metadata crosses the API/`postMessage` boundary, where a `Date` instance wouldn't survive serialization, so `last_modified` is almost certainly a `string` (or `null`) in practice too. If so, drop the `Date` so it matches `created_at`. The JSDoc also diverges as a result (`created_at` is documented as an "ISO 8601 timestamp", `last_modified` only as "a timestamp"); once the type is fixed, the descriptions can match. Please confirm against the implementation. + +### 7. `data.upload` reuses `EditOptions`, surfacing edit-only fields + +`upload(file: File, options: EditOptions)` reuses the `edit()` options interface, so the generated reference lists `style` and `position` as `upload` parameters with edit-specific descriptions ("the field to open for editing", "used to position the panel"). But the handler (`file:upload-asset-file` in the app) only reads `slug` from those options. `style` and `position` are forwarded by the API and silently ignored on upload. + +Give `upload` its own options interface with just the field it uses: + +```ts +export interface UploadOptions { + /** The slug of the field to upload to. */ + slug: string; +} +// upload(file: File, options: UploadOptions): Promise +``` + +Then the reference stops showing `style`/`position` (which don't apply) and `slug` gets an upload-appropriate description. The docs pass can't fix this from comments alone, because the rendered fields come from `EditOptions`. Please confirm against the implementation that `style`/`position` are genuinely unused for uploads. (Low breaking risk — it narrows an options object that already only needs `slug`.) + +### 8. Name the inline `position` object-literal type + +`EditOptions.position` (also reached via `data.upload` and `createTextEditableRegion`-adjacent option shapes) is an inline literal: + +```ts +position?: { x: number; y: number; left: number; width: number; top: number; height: number }; +``` + +Because it has no name, the reference prints the whole six-field literal as the parameter's type, which renders as a long, wrapping code block that looks out of place next to short types like `string`. Extracting it into a named interface: + +```ts +export interface PanelPosition { + x: number; + y: number; + left: number; + width: number; + top: number; + height: number; +} +``` + +would make the parameter's type render as a short `PanelPosition` token (and link to its own entry), like every other named type. Pure readability improvement; no behavior change. + +### 9. No `datasets()` to match `collections()` + +The API Object has `collections()` (lists every Collection) but no `datasets()` equivalent — there's no way to list all Datasets. The `Dataset` JSDoc previously claimed it was "Returned by the `dataset()` and `datasets()` methods"; since `datasets()` exists in neither the types nor the app, that reference has been corrected to `dataset()` only. + +Decide which way to resolve the asymmetry: +- If listing all Datasets should be supported, add `datasets(): Promise` (mirroring `collections()`), and the `Dataset` description can list it again. +- If it's intentional that Datasets aren't listable, no code change needed — just confirming the asymmetry is by design. + ## Already handled (for the record) Resolved in earlier maintainer pushes, noted so they aren't re-flagged: `set(content: string)`, `get(options?: { slug?: string })` (dead `rewriteUrls` removed), `metadata(): Promise` + the `FileMetadata` interface, `setLoading(): Promise`, and removal of the unused error interfaces. diff --git a/src/index.d.ts b/src/index.d.ts index c7351c7..86bec81 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -24,7 +24,7 @@ export interface CreateCustomDataPanelOptions { /** The heading shown at the top of the Data Panel. */ title: string; /** - * Called whenever someone changes a value in the panel. Receives the full + * Called whenever a Team Member changes a value in the panel. Receives the full * updated data object, not a diff. */ onChange: (data?: Record | unknown[]) => void; @@ -35,7 +35,7 @@ export interface CreateCustomDataPanelOptions { data?: Record | unknown[]; /** * Input configuration for the fields in `data`, using the same `_inputs` - * shape as a CloudCannon configuration file. + * shape as a CloudCannon Configuration File. */ config?: Cascade; /** @@ -225,7 +225,7 @@ export interface SetOptions { * Options for `data.edit()`. */ export interface EditOptions { - /** The slug of the field to open for editing. */ + /** The slug of the target field. */ slug: string; /** Optional style hint for the editing surface. */ style?: string | null; @@ -295,24 +295,54 @@ export interface GetInputConfigOptions { * `metadata()` method on a File. */ export interface FileMetadata { - /** Holds the file's size in bytes (the length of its raw source), or `null` if unknown. */ + /** + * Holds the file's size in bytes (the length of its raw source), or `null` if unknown. + * @example + * In this example, we read the size of the file open in the editor. + * ```javascript + * const { file_size } = await api.currentFile().metadata(); + * console.log(file_size); + * ``` + */ file_size: number | null; - /** Holds an ISO 8601 timestamp of when the file was created, or `null` if unknown. */ + /** + * Holds an ISO 8601 timestamp of when the file was created, or `null` if unknown. + * @example + * In this example, we read when the file was created. + * ```javascript + * const { created_at } = await api.currentFile().metadata(); + * console.log(created_at); + * ``` + */ created_at: string | null; - /** Holds a timestamp of the file's most recent change, or `null` if unknown. */ + /** + * Holds a timestamp of the file's most recent change, or `null` if unknown. + * @example + * In this example, we read when the file was last changed. + * ```javascript + * const { last_modified } = await api.currentFile().metadata(); + * console.log(last_modified); + * ``` + */ last_modified: string | Date | null; /** * Holds the file's resolved output data: its front matter merged * with the data CloudCannon's build produces for it. The shape depends on the * file, so this is loosely typed. + * @example + * In this example, we read the file's resolved output data. + * ```javascript + * const { data } = await api.currentFile().metadata(); + * console.log(data); + * ``` */ data: any; } /** - * Provides body-content access for a file: everything after the front matter. - * Accessed through a File's `content` property. Additionally, you - * can read or replace a file's body as a string. + * Accessed through a File's `content` property. Provides body-content access for + * a file: everything after the front matter. Additionally, you can read or + * replace a file's body as a string. */ export interface CloudCannonVisualEditorAPIV1FileContent { /** @@ -320,9 +350,10 @@ export interface CloudCannonVisualEditorAPIV1FileContent { * string. Resolves to `undefined` if the file does not exist. * @returns A promise for the body content string. * @example - * In this example, we read the body content of the file open in the editor. + * In this example, we read the body content of the file open in the editor and log it. * ```javascript * const content = await api.currentFile().content.get(); + * console.log(content); * ``` */ get(): Promise; @@ -330,7 +361,7 @@ export interface CloudCannonVisualEditorAPIV1FileContent { /** * Replaces the file's body content with the given string. This marks the file * as having unsaved changes; a Team Member must save the Site to persist it. - * Resolves to `undefined` if the file does not exist. + * Has no effect if the file does not exist. * @param content The new body content, as a string. * @returns A promise that resolves once the change is applied to the editor. * @example @@ -379,10 +410,9 @@ export interface CloudCannonVisualEditorAPIV1FileContent { } /** - * Provides structured-data access for a file: its front matter, or the full - * contents of a data file. Accessed through a File's `data` - * property. Additionally, you can read and write a file's fields, and add, - * remove, or reorder array items. + * Accessed through a File's `data` property. Provides structured-data access for + * a file: its front matter, or the full contents of a data file. Additionally, + * you can read and write a file's fields, and add, remove, or reorder array items. */ export interface CloudCannonVisualEditorAPIV1FileData { /** @@ -394,18 +424,22 @@ export interface CloudCannonVisualEditorAPIV1FileData { * @param options Optional `{ slug }` to read a single field. * @returns A promise for the data: an object, an array, or `undefined`. * @example - * In this example, we read the whole front matter object, then read a single field by slug. + * In this example, we read the whole front matter object, then a single field by slug, and log both. * ```javascript * const data = await api.currentFile().data.get(); * const title = await api.currentFile().data.get({ slug: 'title' }); + * console.log(data, title); * ``` */ - get(options?: { slug?: string }): Promise | any[] | undefined>; + get(options?: { + /** The slug of a single field to read, instead of the whole object. */ + slug?: string; + }): Promise | any[] | undefined>; /** * Sets a single structured-data field. This marks the file as having unsaved - * changes; a Team Member must save the Site to persist it. Resolves to - * `undefined` if the file does not exist. + * changes; a Team Member must save the Site to persist it. Has no effect if + * the file does not exist. * @param options The field `slug` and its new `value`. * @returns A promise that resolves once the change is applied (with no value). * @example @@ -418,8 +452,8 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Opens the hosted Data Panel for a single field so a Team Member can edit it. - * This is fire-and-forget: it returns immediately and does not wait for, or - * report, the result of the edit. + * The method returns immediately and does not wait for or report the result of + * the edit. * @param options The field `slug`, with optional `style` and `position`. * @example * In this example, we open the Data Panel for the `title` field. @@ -431,15 +465,18 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Uploads a file to a specific field (for example, an image or file Input) and - * returns the uploaded file's path. Resolves to `undefined` if the file does - * not exist or the upload produces no path. For a general asset upload that is - * not tied to a field, use the top-level `uploadFile`. + * returns the uploaded file's path. Setting the field marks the file as having + * unsaved changes; a Team Member must save the Site to persist it. Resolves to + * `undefined` if the file does not exist or the upload produces no path. For a + * general asset upload that is not tied to a field, use the top-level + * `uploadFile`. * @param file The file to upload. - * @param options The target field `slug`, with optional `style` and `position`. + * @param options The target field `slug`. * @returns A promise for the uploaded file's path, or `undefined`. * @example - * In this example, we upload a file into the `hero_image` field and read its path. + * In this example, we upload the file chosen in a file input into the `hero_image` field and read its path. * ```javascript + * const [file] = document.querySelector('input[type="file"]').files; * const path = await api.currentFile().data.upload(file, { slug: 'hero_image' }); * ``` */ @@ -447,8 +484,8 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Adds an item to an array field. This marks the file as having unsaved - * changes; a Team Member must save the Site to persist it. Resolves to - * `undefined` if the file does not exist. + * changes; a Team Member must save the Site to persist it. Has no effect if + * the file does not exist. * @param options The field `slug`, the `index` to insert at (`null` to append), * and the `value`. * @returns A promise that resolves once the item is added. @@ -466,8 +503,8 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Removes an item from an array field by index. This marks the file as having - * unsaved changes; a Team Member must save the Site to persist it. Resolves to - * `undefined` if the file does not exist. + * unsaved changes; a Team Member must save the Site to persist it. Has no + * effect if the file does not exist. * @param options The field `slug` and the `index` to remove. * @returns A promise that resolves once the item is removed. * @example @@ -481,8 +518,8 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Moves an item within an array field, or between two array fields when * `toSlug` differs from `fromSlug`. This marks the file as having unsaved - * changes; a Team Member must save the Site to persist it. Resolves to - * `undefined` if the file does not exist. + * changes; a Team Member must save the Site to persist it. Has no effect if + * the file does not exist. * @param options `fromSlug` and `fromIndex` for the source, and `toIndex` * (with optional `toSlug`) for the destination. * @returns A promise that resolves once the item is moved. @@ -535,8 +572,8 @@ export interface CloudCannonVisualEditorAPIV1FileData { /** * Represents a single file in your Site. Returned by the - * `currentFile()` and `file()` methods, and by a Collection's or Dataset's - * `items()` method. Additionally, you can read and write a File's raw source, + * `currentFile()`, `file()`, and `files()` methods, and by a Collection's or + * Dataset's `items()` method. Additionally, you can read and write a File's raw source, * body content, and structured data, read its metadata, and lock it while you * edit. */ @@ -553,23 +590,23 @@ export interface CloudCannonVisualEditorAPIV1File { path: string; /** - * Provides structured-data access for the file (front matter, or a data file's contents). + * Provides structured-data access for the file (front matter, or a data file's + * contents). Use the methods on the FileData object to read and write the data. * @example - * In this example, we read the file's data through its `data` object and log it. + * In this example, we get the FileData object for the file open in the editor. * ```javascript - * const data = await api.currentFile().data.get(); - * console.log(data); + * const data = api.currentFile().data; * ``` */ data: CloudCannonVisualEditorAPIV1FileData; /** * Provides body-content access for the file (everything after the front matter). + * Use the methods on the FileContent object to read and write the body. * @example - * In this example, we read the body through the file's `content` object and log it. + * In this example, we get the FileContent object for the file open in the editor. * ```javascript - * const body = await api.currentFile().content.get(); - * console.log(body); + * const content = api.currentFile().content; * ``` */ content: CloudCannonVisualEditorAPIV1FileContent; @@ -592,8 +629,7 @@ export interface CloudCannonVisualEditorAPIV1File { * Replaces the file's entire raw source with the given string. Use this for * files that aren't edited through structured data, such as a * `robots.txt`. This marks the file as having unsaved changes; a Team Member - * must save the Site to persist it. Resolves to `undefined` if the file does - * not exist. + * must save the Site to persist it. Has no effect if the file does not exist. * @param value The new raw source, as a string. * @returns A promise that resolves once the change is applied. * @example @@ -607,9 +643,11 @@ export interface CloudCannonVisualEditorAPIV1File { set(value: string): Promise; /** - * Returns the file's metadata: `{ file_size, created_at, last_modified, data }`. - * Metadata is read-only and cannot be written through the API. Resolves to - * `undefined` if the file does not exist. + * Returns the file's metadata: `{ file_size, created_at, last_modified, data }`, + * where `data` is the file's resolved output data (its front matter merged with + * the data CloudCannon's build produces for it), not the same as the File's + * `data` object. Metadata is read-only and cannot be written through the API. + * Resolves to `undefined` if the file does not exist. * @returns A promise for the file's metadata. * @example * In this example, we read the metadata of the file open in the editor and log it. @@ -691,7 +729,6 @@ export interface CloudCannonVisualEditorAPIV1File { /** * Returns the resolved Input configuration for a field, or `undefined` when the * field has no configuration (or the file does not exist). - * @param options The field `slug`. * @returns A promise for the field's Input configuration. * @example * In this example, we read the resolved Input configuration for the `hero_image` field and log it. @@ -723,9 +760,11 @@ export interface CloudCannonVisualEditorAPIV1Collection { collectionKey: string; /** - * Returns every file in the Collection as an array of File objects. The array - * is empty when the Collection key isn't configured. If the key is falsy, - * the returned promise never resolves, so always pass a real Collection key. + * Returns every file in the Collection as an array of File objects. A + * non-matching key (a valid string that matches no configured Collection) + * resolves to an empty array. A falsy key (an empty string or `undefined`) + * never resolves the returned promise, so always pass a real Collection key. + * A Dataset differs here: its `items()` never resolves for a non-matching key. * @returns A promise for the Collection's files. * @example * In this example, we list every file in the `posts` Collection and log each path. @@ -778,9 +817,9 @@ export interface CloudCannonVisualEditorAPIV1Collection { /** * Represents a Dataset, as configured under `data_config` in your CloudCannon - * Configuration File. Returned by the `dataset()` and `datasets()` - * methods. Additionally, you can call `items()` to read a Dataset's file or - * files, or `addEventListener` to react to changes. + * Configuration File. Returned by the `dataset()` method. Additionally, you can + * call `items()` to read a Dataset's file or files, or `addEventListener` to + * react to changes. */ export interface CloudCannonVisualEditorAPIV1Dataset { /** @@ -798,8 +837,10 @@ export interface CloudCannonVisualEditorAPIV1Dataset { /** * Returns the Dataset's file or files: a single File when the Dataset is * configured as one file, or an array of File objects when it's a folder. - * If the key is falsy or invalid, the returned promise never resolves, so - * always pass a real Dataset key. + * A falsy key (an empty string or `undefined`) or a non-matching key (a valid + * string that matches no configured Dataset) never resolves the returned + * promise, so always pass a real Dataset key. A Collection differs here: its + * `items()` resolves to an empty array for a non-matching key. * @returns A promise for the Dataset's file, or array of files. * @example * In this example, we read the `locales` Dataset's file or files, normalized to an array. @@ -919,12 +960,12 @@ export interface CloudCannonVisualEditorAPIV1 { /** * Returns the file currently open in the Visual Editor. Not every page has an - * associated file; this throws when the open page has none, so wrap it in a - * `try`/`catch`. + * associated file, and this throws when the open page has none. * @returns The file open in the preview. * @throws {Error} `'No current file path'` when no file is open. * @example - * In this example, we read the open file, handling the case where the page has none. + * In this example, we read the open file, wrapping the call in a `try`/`catch` + * to handle a page with no associated file. * ```javascript * try { * const file = api.currentFile(); @@ -949,7 +990,7 @@ export interface CloudCannonVisualEditorAPIV1 { file(path: string): CloudCannonVisualEditorAPIV1File; /** * Returns the Collection with the given key, as configured under - * `collections_config` in your CloudCannon configuration file. + * `collections_config` in your CloudCannon Configuration File. * @param key The Collection key. * @returns The Collection. * @example @@ -961,7 +1002,7 @@ export interface CloudCannonVisualEditorAPIV1 { collection(key: string): CloudCannonVisualEditorAPIV1Collection; /** * Returns the Dataset with the given key, as configured under `data_config` in - * your CloudCannon configuration file. + * your CloudCannon Configuration File. * @param key The Dataset key. * @returns The Dataset. * @example @@ -1031,6 +1072,7 @@ export interface CloudCannonVisualEditorAPIV1 { /** * Type guard that returns `true` when `obj` is a File object. + * @param obj The value to check. * @example * In this example, we narrow an unknown value to a File before using it. * ```javascript @@ -1042,6 +1084,7 @@ export interface CloudCannonVisualEditorAPIV1 { isAPIFile(obj: unknown): obj is CloudCannonVisualEditorAPIV1File; /** * Type guard that returns `true` when `obj` is a Collection object. + * @param obj The value to check. * @example * In this example, we narrow an unknown value to a Collection before using it. * ```javascript @@ -1053,6 +1096,7 @@ export interface CloudCannonVisualEditorAPIV1 { isAPICollection(obj: unknown): obj is CloudCannonVisualEditorAPIV1Collection; /** * Type guard that returns `true` when `obj` is a Dataset object. + * @param obj The value to check. * @example * In this example, we narrow an unknown value to a Dataset before using it. * ```javascript From 69dd02b498b1a880f13fef551b23d3ec7c67d310 Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:03:37 +1200 Subject: [PATCH 07/10] handoff doc --- HANDOFF.md | 133 ++++++++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 63 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 11db379..6bdc530 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -26,7 +26,7 @@ Open the reference at **http://localhost:9010/documentation/developer-reference/ The JSDoc in `src/index.d.ts` **is** the CloudCannon developer reference for the Visual Editor API. The `platform-documentation` site parses it at build time (with ts-morph) into: -- the reference section (`/documentation/developer-reference/visual-editor-api/`) — an overview with an auto-generated methods table, plus one page per object type, and +- the reference section (`/documentation/developer-reference/visual-editor-api/`) — an overview, an "API Object" page listing the top-level methods, and one page per object type, and - the reference data tables embedded in the VE API how-to articles. **All of that machinery lives on the docs side.** `platform-documentation` fetches `src/index.d.ts` from the npm CDN (jsDelivr), pinned by version, so the reference builds in CI without this repo checked out. This repo stays lean: types + JSDoc only — no parser, no build step beyond the existing `cp src/* dist/`. @@ -45,68 +45,44 @@ A full JSDoc rewrite, written from the implementation (the previous JSDoc was AI - Hallucinated `@throws` removed; only the four real `Error` cases documented. Real failure modes (resolve-`undefined`, the `items()` falsy-key hang) described instead. - `@example` on every method; consistent present-tense voice; CloudCannon terminology capitalized; no em dashes. -- Object interface descriptions name their origin ("This object is returned by … Additionally, you can …"); property descriptions are complete sentences ("This property holds/provides …"). +- Object interface descriptions name their origin ("Represents … Returned by / Accessed through … Additionally, you can …"); property descriptions are blunt, verb-first sentences ("Holds … / Provides …"). - Event listeners: `addEventListener`/`removeEventListener` documented on all six interfaces, each with an example (using a named handler, since an anonymous listener can't be removed). Each description focuses on the listener itself (events, scope, teardown); the `event.detail` payload (`isNew`, `sourcePath`) is documented once, independently, in the reference's Events section rather than repeated in every listener. -Comments only — no type/signature changes in this branch. +Comments only, apart from one verified return-type narrowing (the `undefined`-on-missing-file change, detailed in Tier 1 below). ## Open type follow-ups (maintainer) -These need type changes the docs pass can't make, and would each improve the generated reference. +Grouped by priority. **Tier 1** are real inaccuracies worth fixing. **Tier 2** need a quick check against the implementation. **Tier 3** is optional polish — leaving these as-is is a reasonable design choice, especially where a value is intentionally open-shaped. -### 1. Give `event.detail` a typed shape (new) +### Tier 1 — Correctness (real inaccuracies) -`addEventListener`/`removeEventListener` use the standard DOM listener type, so `event.detail` is untyped (`any`). The docs describe it in prose, but the reference can't surface it as a first-class type the way it now does for `FileMetadata`. +#### `File` / `FileContent` methods resolve `undefined` on a missing file -The payload has two fields: -- `isNew: boolean` — `true` when a `change` fired for a newly created file, `false` for an update. -- `sourcePath: string` — the changed file's path. Present on the Site-wide (`CloudCannonVisualEditorAPIV1`), `Collection`, and `Dataset` listeners. The `File`, `FileData`, and `FileContent` listeners are already scoped to a single known file, so their events don't include it. - -Suggested: define a `CustomEvent`-style detail interface (e.g. `FileChangeEventDetail { sourcePath: string; isNew: boolean }`, plus a narrower variant without `sourcePath` for the file-scoped listeners if worth distinguishing) and type the listener signatures against it. Low breaking risk — it narrows an untyped value to a real shape. Parallel in spirit to the `FileMetadata` typing. - -This pairs with a deliberate docs decision: the per-interface `addEventListener` descriptions now document only the listener (events, scope, teardown) and treat `event.detail` as an independent detail, defined once in the reference's Events section. Typing `event.detail` would give that detail a real home — each listener could link to the `event.detail` type the same way return types already link to their object pages, instead of relying on the prose Events section. - -### 2. `File` methods that resolve `undefined` on a missing file (done in this branch — please confirm) - -`File.get()`, `claimLock()`, and `releaseLock()` document resolving `undefined` when the file doesn't exist, but their return types didn't reflect it. To keep the types accurate, this branch narrowed them: +`File.get()`, `claimLock()`, and `releaseLock()` document resolving `undefined` when the file doesn't exist, but their return types didn't reflect it. This branch narrowed them (the one place the docs pass touched types): - `get(): Promise` - `claimLock(): Promise<{ readOnly: boolean } | undefined>` - `releaseLock(): Promise<{ readOnly: boolean } | undefined>` -This is the one place the docs pass touched types (everything else is comments only), done because the prose already promised the behavior and `metadata()`/`getInputConfig()` already model it. Please confirm it matches the implementation. +The behavior is grounded in the app, not assumed from prose: the message handler short-circuits *before* dispatching any file action and posts back `undefined` whenever the `sourcePath` can't be resolved to a file (`app/assets/javascripts/views/file/visual-iframe.view.ts`): -One specific inconsistency the JSDoc review surfaced: **`FileContent.get()` is typed `Promise` but its prose says it resolves to `undefined` when the file doesn't exist.** `FileData.get()` already returns `Record | any[] | undefined`, so `FileContent.get()` is the outlier — narrow it to `Promise` to match its own prose and `FileData.get()` (confirm against the implementation). - -### 3. Give `FileData.get` a named options interface (optional cleanup) - -`FileData.get` (`data.get`) is the only option-taking method whose options are an **inline literal** (`get(options?: { slug?: string })`) rather than a named, documented interface like every other option method (`SetOptions`, `AddArrayItemOptions`, `GetInputConfigOptions`, …). - -The missing-description symptom is **already fixed in this branch** by adding a doc comment to the inline field, so the reference now renders `slug` with a description like every other option method: - -```ts -get(options?: { - /** The slug of a single field to read, instead of the whole object. */ - slug?: string; -}): Promise | any[] | undefined>; +```js +if (!file?.commitChange) { + this.postMessage(`${message.action}-response`, undefined, message.editorCallbackId); + return; +} ``` -This is purely optional cleanup now: extracting the literal into a named `GetDataOptions` interface would make it consistent in *shape* with the other option methods (and easier to reuse), but it's no longer needed to get a documented parameter. Low breaking risk — same shape, just named. +Every method routed through this handler (`get`, `set`, `metadata`, `claimLock`, `releaseLock`, `getInputConfig`, and the `data`/`content` methods) therefore receives `undefined` for a non-existent file. Most client methods pass that straight through (`File.get` uses `typeof value === 'string' ? … : value`; `claimLock`/`releaseLock` resolve the raw value), so they resolve to `undefined` cleanly. The narrowed return types reflect that; `metadata()`/`getInputConfig()` already modelled it. -### 4. `AddArrayItemOptions.value` is typed required but documented as optional +**Still to do — and it's a client bug, not just a type:** `FileContent.get()` does **not** resolve `undefined` cleanly. Its client code is `resolve(value.content)` with no guard, unlike every sibling getter — so on a missing file `value` is `undefined`, `value.content` throws, and the promise never resolves. Its documented "Resolves to `undefined` if the file does not exist" is therefore currently false. Two ways to fix: +- **Make it behave like the others** — change the client to `resolve(value?.content)`, then it resolves `undefined`, and the published type should become `Promise` (currently `Promise`). This is the consistent option. +- **Or document reality** — if `FileContent.get` is meant to throw on a missing file, change the prose to say so (it would then differ from `File.get`/`FileData.get`, which resolve `undefined`). -`value: any` is required in the type, but `data.addArrayItem` documents it as an either/or with `sourceIndex` ("Provide either `value` for a new item, or `sourceIndex` to clone an existing one"). If that's the real behavior, `value` should be `value?: any`. Please confirm against the implementation. +(`FileData.get` already resolves `undefined` cleanly and returns `Record | any[] | undefined`; `FileContent.get` is the lone outlier.) -### 5. V0 `'create'` event in the listener union (low priority, app-internal) +#### `data.upload` reuses `EditOptions`, surfacing fields it ignores -The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. - -### 6. `FileMetadata.last_modified` is typed `string | Date` (likely serialization-inaccurate) - -`last_modified` is typed `string | Date | null`, but `created_at` (the sibling field) is `string | null`. Metadata crosses the API/`postMessage` boundary, where a `Date` instance wouldn't survive serialization, so `last_modified` is almost certainly a `string` (or `null`) in practice too. If so, drop the `Date` so it matches `created_at`. The JSDoc also diverges as a result (`created_at` is documented as an "ISO 8601 timestamp", `last_modified` only as "a timestamp"); once the type is fixed, the descriptions can match. Please confirm against the implementation. - -### 7. `data.upload` reuses `EditOptions`, surfacing edit-only fields - -`upload(file: File, options: EditOptions)` reuses the `edit()` options interface, so the generated reference lists `style` and `position` as `upload` parameters with edit-specific descriptions ("the field to open for editing", "used to position the panel"). But the handler (`file:upload-asset-file` in the app) only reads `slug` from those options. `style` and `position` are forwarded by the API and silently ignored on upload. +`upload(file: File, options: EditOptions)` reuses the `edit()` options interface, so the generated reference lists `style` and `position` as `upload` parameters with edit-specific descriptions ("the field to open for editing", "used to position the panel"). But the handler (`file:upload-asset-file` in the app) only reads `slug` from those options — `style` and `position` are forwarded by the API and silently ignored on upload (verified against the implementation). Give `upload` its own options interface with just the field it uses: @@ -118,32 +94,35 @@ export interface UploadOptions { // upload(file: File, options: UploadOptions): Promise ``` -Then the reference stops showing `style`/`position` (which don't apply) and `slug` gets an upload-appropriate description. The docs pass can't fix this from comments alone, because the rendered fields come from `EditOptions`. Please confirm against the implementation that `style`/`position` are genuinely unused for uploads. (Low breaking risk — it narrows an options object that already only needs `slug`.) +Then the reference stops showing `style`/`position` (which don't apply) and `slug` gets an upload-appropriate description. The docs pass can't fix this from comments alone, because the rendered fields come from `EditOptions`. (The fix is your call — a dedicated `UploadOptions` is the clean option; keeping the shared `EditOptions` is also fine, the docs just live with the extra fields. Low breaking risk either way.) -### 8. Name the inline `position` object-literal type +#### `FileMetadata.last_modified` is typed `string | Date`, but the client always receives a string -`EditOptions.position` (also reached via `data.upload` and `createTextEditableRegion`-adjacent option shapes) is an inline literal: +The published `FileMetadata.last_modified` is `string | Date | null`, while its sibling `created_at` is `string | null`. The `Date` is inaccurate for what the VE API client receives: the metadata handler serializes the payload with `JSON.stringify(file.metadata)` and the client `JSON.parse`s it, so any `Date` is converted to an ISO 8601 string in transit (`JSON.stringify` invokes `Date.prototype.toJSON`). The client therefore always receives `string | null`, never a `Date`. -```ts -position?: { x: number; y: number; left: number; width: number; top: number; height: number }; -``` +The `Date` most likely leaked from the app's internal model (`app/assets/javascripts/models/site-file.ts`: `last_modified: string | Date | null`), which is the *pre-serialization* type. Drop `Date` from the published `FileMetadata` so it matches `created_at` and reality; the descriptions can then align (`created_at` is documented as an "ISO 8601 timestamp", `last_modified` currently just "a timestamp"). -Because it has no name, the reference prints the whole six-field literal as the parameter's type, which renders as a long, wrapping code block that looks out of place next to short types like `string`. Extracting it into a named interface: +#### `FileData.addEventListener` / `FileContent.addEventListener` never fire -```ts -export interface PanelPosition { - x: number; - y: number; - left: number; - width: number; - top: number; - height: number; -} -``` +The reference documents `change` listeners on `FileData` (`data`) and `FileContent` (`content`), each with an example — but they never receive events. The client registers them on `CloudCannon:file:{path}:data:change` and `…:content:change` (`APIFileData`/`APIFileContent.eventPrefix` append `:data` / `:content`), but `triggerFileEvent` only dispatches: -would make the parameter's type render as a short `PanelPosition` token (and link to its own entry), like every other named type. Pure readability improvement; no behavior change. +- `CloudCannon:file:{path}:{change|delete}` — the File-level listener +- `CloudCannon:{change|delete}` — Site-wide +- `CloudCannon:collection:{key}:{change|delete}` and `CloudCannon:dataset:{key}:{change|delete}` -### 9. No `datasets()` to match `collections()` +There is no `:data:` or `:content:` dispatch anywhere in the app — `APIEvents.dispatchEvent` is only called in those four places (in `cloudcannon-v1-api.ts`). So the `data`/`content` `change` listeners are dead; only `File.addEventListener` fires for file changes. + +Resolve one way or the other: +- **If they should work:** dispatch matching `:data:`/`:content:` events (from `triggerFileEvent`, or wherever data/content changes are detected). +- **If not:** remove them from the API and the docs, and point integrators to `File.addEventListener`, which already fires on any change to the file. + +### Tier 2 — Confirm against the implementation + +#### `AddArrayItemOptions.value` — required, or optional vs `sourceIndex`? + +`value: any` is required in the type, but `data.addArrayItem` documents it as an either/or with `sourceIndex` ("Provide either `value` for a new item, or `sourceIndex` to clone an existing one"). If that's the real behavior, `value` should be `value?: any`. This is about the *optionality* (`?`), not the `any` — keeping `value` loosely typed is correct. Please confirm against the implementation. + +#### No `datasets()` to match `collections()` — intended? The API Object has `collections()` (lists every Collection) but no `datasets()` equivalent — there's no way to list all Datasets. The `Dataset` JSDoc previously claimed it was "Returned by the `dataset()` and `datasets()` methods"; since `datasets()` exists in neither the types nor the app, that reference has been corrected to `dataset()` only. @@ -151,6 +130,34 @@ Decide which way to resolve the asymmetry: - If listing all Datasets should be supported, add `datasets(): Promise` (mirroring `collections()`), and the `Dataset` description can list it again. - If it's intentional that Datasets aren't listable, no code change needed — just confirming the asymmetry is by design. +### Tier 3 — Optional (readability / consistency; fine to leave by design) + +#### Give `event.detail` a typed shape + +`addEventListener`/`removeEventListener` use the standard DOM listener type, so `event.detail` is untyped (`any`). The docs describe it in prose, but the reference can't surface it as a first-class type the way it now does for `FileMetadata`. The payload has two fields: +- `isNew: boolean` — `true` when a `change` fired for a newly created file, `false` for an update. +- `sourcePath: string` — the changed file's path. Present on the Site-wide (`CloudCannonVisualEditorAPIV1`), `Collection`, and `Dataset` listeners. The `File`, `FileData`, and `FileContent` listeners are already scoped to a single known file, so their events don't include it. + +If you want it typed, define a `CustomEvent`-style detail interface (e.g. `FileChangeEventDetail { sourcePath: string; isNew: boolean }`, plus a narrower variant without `sourcePath` for the file-scoped listeners) and type the listener signatures against it — then each listener could link to the `event.detail` type instead of relying on the prose Events section. **But if event payloads are meant to stay open or evolve, leaving `event.detail` as `any` is a reasonable choice** — this is a docs-rendering nicety, not a correctness issue. + +#### Name `FileData.get`'s inline options (`GetDataOptions`) + +`FileData.get` (`data.get`) is the only option-taking method whose options are an inline literal (`get(options?: { slug?: string })`) rather than a named interface like every other option method. The missing-description symptom is **already fixed in this branch** by adding a doc comment to the inline field, so `slug` now renders with a description. Extracting it into a named `GetDataOptions` interface would only make it consistent in *shape* with the others (and easier to reuse) — purely optional now. + +#### Name the inline `position` type (`PanelPosition`) + +`EditOptions.position` is an inline literal: + +```ts +position?: { x: number; y: number; left: number; width: number; top: number; height: number }; +``` + +Because it has no name, the reference prints the whole six-field literal as the parameter's type, which renders as a long, wrapping block next to short types like `string`. Extracting it into a named interface (e.g. `PanelPosition`) would make the parameter render as a short token that links to its own entry. Pure readability; no behavior change. + +#### V0 `'create'` event in the listener union (app-internal) + +The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. + ## Already handled (for the record) Resolved in earlier maintainer pushes, noted so they aren't re-flagged: `set(content: string)`, `get(options?: { slug?: string })` (dead `rewriteUrls` removed), `metadata(): Promise` + the `FileMetadata` interface, `setLoading(): Promise`, and removal of the unused error interfaces. From 507c41afe63e0e0c67a2d521178272fedef4fe9e Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:17:04 +1200 Subject: [PATCH 08/10] Restore package.json and lock to main versions --- package-lock.json | 80 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfb5ca8..b6d7053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@cloudcannon/configuration-types": "0.0.58" + "@cloudcannon/configuration-types": "0.0.59" }, "devDependencies": { - "@biomejs/biome": "2.4.16", + "@biomejs/biome": "2.5.0", "typescript": "6.0.3" } }, "node_modules/@biomejs/biome": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.16.tgz", - "integrity": "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -33,20 +33,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.16", - "@biomejs/cli-darwin-x64": "2.4.16", - "@biomejs/cli-linux-arm64": "2.4.16", - "@biomejs/cli-linux-arm64-musl": "2.4.16", - "@biomejs/cli-linux-x64": "2.4.16", - "@biomejs/cli-linux-x64-musl": "2.4.16", - "@biomejs/cli-win32-arm64": "2.4.16", - "@biomejs/cli-win32-x64": "2.4.16" + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.16.tgz", - "integrity": "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", "cpu": [ "arm64" ], @@ -61,9 +61,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.16.tgz", - "integrity": "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", "cpu": [ "x64" ], @@ -78,9 +78,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.16.tgz", - "integrity": "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", "cpu": [ "arm64" ], @@ -95,9 +95,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.16.tgz", - "integrity": "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", "cpu": [ "arm64" ], @@ -112,9 +112,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.16.tgz", - "integrity": "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", "cpu": [ "x64" ], @@ -129,9 +129,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.16.tgz", - "integrity": "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", "cpu": [ "x64" ], @@ -146,9 +146,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.16.tgz", - "integrity": "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", "cpu": [ "arm64" ], @@ -163,9 +163,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.16.tgz", - "integrity": "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", "cpu": [ "x64" ], @@ -180,9 +180,9 @@ } }, "node_modules/@cloudcannon/configuration-types": { - "version": "0.0.58", - "resolved": "https://registry.npmjs.org/@cloudcannon/configuration-types/-/configuration-types-0.0.58.tgz", - "integrity": "sha512-W6Vfyq1Z18aewej8fVB7qHrzKaup+vrc1K7EsxCoLaUJ6f92FYIP5O78Zl2tST1Tocgw3Y5b4vaGD7e1T/YI4w==", + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@cloudcannon/configuration-types/-/configuration-types-0.0.59.tgz", + "integrity": "sha512-DyC91Ddw6NIiajHFT4MIIRM8VdKayiZHjLrAznWovnMvBHzqowHYulRqHq5L5fc/9DBPcpbaW+ZjNydj5MzUNQ==", "license": "MIT", "dependencies": { "zod": "4.4.3" diff --git a/package.json b/package.json index 6a63c18..c4e9c2c 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,10 @@ "src/**/*" ], "dependencies": { - "@cloudcannon/configuration-types": "0.0.58" + "@cloudcannon/configuration-types": "0.0.59" }, "devDependencies": { - "@biomejs/biome": "2.4.16", + "@biomejs/biome": "2.5.0", "typescript": "6.0.3" } } From 2d9fdc0c6ca1850fcd419d66ea3d6c73fbdf8f2f Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:21:33 +1200 Subject: [PATCH 09/10] respond to review notes --- src/index.d.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 86bec81..33bc28b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -15,10 +15,9 @@ import type { */ export interface CreateCustomDataPanelOptions { /** - * A stable identifier for the panel, returned as the `panelId` and passed to - * `destroyCustomDataPanel` to close it. When omitted, CloudCannon generates a - * seven-character base-36 id (digits `0`-`9` and lowercase `a`-`z`, e.g. - * `k4j92xq`). Treat it as an opaque token. + * A stable identifier for the panel. Pass it to `destroyCustomDataPanel` to + * close the panel. When omitted, CloudCannon generates a seven-character + * base-36 id (digits `0`-`9` and lowercase `a`-`z`, e.g. `k4j92xq`). */ id?: string; /** The heading shown at the top of the Data Panel. */ @@ -227,7 +226,7 @@ export interface SetOptions { export interface EditOptions { /** The slug of the target field. */ slug: string; - /** Optional style hint for the editing surface. */ + /** Set to `"sidebar"` to open the field in the Data Editor sidebar instead of a floating Data Panel. */ style?: string | null; /** The click coordinates and the bounding rectangle of the element being edited, used to position the panel. */ position?: { @@ -311,7 +310,7 @@ export interface FileMetadata { * In this example, we read when the file was created. * ```javascript * const { created_at } = await api.currentFile().metadata(); - * console.log(created_at); + * console.log(`Created at ${new Date(created_at).toLocaleString()}`); * ``` */ created_at: string | null; @@ -989,10 +988,12 @@ export interface CloudCannonVisualEditorAPIV1 { */ file(path: string): CloudCannonVisualEditorAPIV1File; /** - * Returns the Collection with the given key, as configured under - * `collections_config` in your CloudCannon Configuration File. + * Returns a Collection object for the given key, as configured under + * `collections_config` in your CloudCannon Configuration File. The object + * provides methods to list the Collection's items and listen for changes, + * not the items themselves. * @param key The Collection key. - * @returns The Collection. + * @returns A Collection object with methods to list items and subscribe to changes. * @example * In this example, we reference the `posts` Collection by key. * ```javascript @@ -1179,12 +1180,12 @@ export interface CloudCannonVisualEditorAPIV1 { ): Promise; /** - * Opens a custom Data Panel in the Visual Editor and resolves with its - * `panelId`. When `options.id` is omitted, CloudCannon generates a - * seven-character base-36 id. Pass the returned `panelId` to + * Opens a custom Data Panel in the Visual Editor and resolves with the + * panel's id. When `options.id` is omitted, CloudCannon generates a + * seven-character base-36 id. Pass the returned id to * `destroyCustomDataPanel` to close the panel. * @param options Configuration for the panel and its Inputs. - * @returns A promise for the panel's `panelId`. + * @returns A promise for the panel's id. * @example * In this example, we open a custom Data Panel and log its data whenever it changes. * ```javascript @@ -1196,8 +1197,8 @@ export interface CloudCannonVisualEditorAPIV1 { */ createCustomDataPanel(options: CreateCustomDataPanelOptions): Promise; /** - * Closes the custom Data Panel with the given `panelId`. - * @param id The `panelId` returned by `createCustomDataPanel`. + * Closes the custom Data Panel with the given id. + * @param id The id returned by `createCustomDataPanel`. * @returns A promise that resolves once the panel is closed. * @example * In this example, we close a custom Data Panel by its id. From 40ea223e62881dff3f10c63e29c90393e419e3a8 Mon Sep 17 00:00:00 2001 From: Ella <141297015+EllaCloudCannon@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:37:47 +1200 Subject: [PATCH 10/10] remove handoff doc --- HANDOFF.md | 163 ----------------------------------------------------- 1 file changed, 163 deletions(-) delete mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 6bdc530..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,163 +0,0 @@ -# Visual Editor API — docs handoff - -## Previewing your JSDoc edits in the docs locally - -You can see how your `src/index.d.ts` changes will render in the reference before publishing, as long as both repos are checked out as siblings in the same parent folder: - -``` -your-workspace/ -├── visual-editor-api/ ← this repo (edit src/index.d.ts here) -└── platform-documentation/ ← the docs site -``` - -Then, from the `platform-documentation` repo: - -```sh -VEAPI_LOCAL=1 deno task serve -``` - -Open the reference at **http://localhost:9010/documentation/developer-reference/visual-editor-api/**. - -- `VEAPI_LOCAL=1` tells the docs parser to read your local `../visual-editor-api/src/index.d.ts` working tree instead of the published npm version, so your unpublished JSDoc edits show up live. Without it, the docs build against the pinned release (see "To ship a documentation change" below) and you'll see the shipped reference, not your edits. -- The JSDoc is parsed once when the serve task starts. The sibling repo sits outside the docs site's watch tree, so if you edit `src/index.d.ts` while the server is running and the page doesn't update, restart the serve task to re-parse. -- Don't run `deno task build` and `deno task serve` at the same time — they both write to `_site` and will clash. - -## How the documentation consumes this package - -The JSDoc in `src/index.d.ts` **is** the CloudCannon developer reference for the Visual Editor API. The `platform-documentation` site parses it at build time (with ts-morph) into: - -- the reference section (`/documentation/developer-reference/visual-editor-api/`) — an overview, an "API Object" page listing the top-level methods, and one page per object type, and -- the reference data tables embedded in the VE API how-to articles. - -**All of that machinery lives on the docs side.** `platform-documentation` fetches `src/index.d.ts` from the npm CDN (jsDelivr), pinned by version, so the reference builds in CI without this repo checked out. This repo stays lean: types + JSDoc only — no parser, no build step beyond the existing `cp src/* dist/`. - -### To ship a documentation change - -1. Edit the JSDoc in `src/index.d.ts`. -2. Tag a release (`v*`) — CI publishes to npm as today. -3. The docs team bumps the pinned version in `platform-documentation` (`_lib/veapi-docs.ts` → `VEAPI_VERSION`). That single edit is the whole re-pin. - -There is nothing else to wire up on this side. - -## What's in the `docs/jsdocs-review` branch - -A full JSDoc rewrite, written from the implementation (the previous JSDoc was AI-generated and unreliable — e.g. `@throws {FileNotFoundError}` / `{CollectionNotFoundError}` that are never thrown). Highlights: - -- Hallucinated `@throws` removed; only the four real `Error` cases documented. Real failure modes (resolve-`undefined`, the `items()` falsy-key hang) described instead. -- `@example` on every method; consistent present-tense voice; CloudCannon terminology capitalized; no em dashes. -- Object interface descriptions name their origin ("Represents … Returned by / Accessed through … Additionally, you can …"); property descriptions are blunt, verb-first sentences ("Holds … / Provides …"). -- Event listeners: `addEventListener`/`removeEventListener` documented on all six interfaces, each with an example (using a named handler, since an anonymous listener can't be removed). Each description focuses on the listener itself (events, scope, teardown); the `event.detail` payload (`isNew`, `sourcePath`) is documented once, independently, in the reference's Events section rather than repeated in every listener. - -Comments only, apart from one verified return-type narrowing (the `undefined`-on-missing-file change, detailed in Tier 1 below). - -## Open type follow-ups (maintainer) - -Grouped by priority. **Tier 1** are real inaccuracies worth fixing. **Tier 2** need a quick check against the implementation. **Tier 3** is optional polish — leaving these as-is is a reasonable design choice, especially where a value is intentionally open-shaped. - -### Tier 1 — Correctness (real inaccuracies) - -#### `File` / `FileContent` methods resolve `undefined` on a missing file - -`File.get()`, `claimLock()`, and `releaseLock()` document resolving `undefined` when the file doesn't exist, but their return types didn't reflect it. This branch narrowed them (the one place the docs pass touched types): -- `get(): Promise` -- `claimLock(): Promise<{ readOnly: boolean } | undefined>` -- `releaseLock(): Promise<{ readOnly: boolean } | undefined>` - -The behavior is grounded in the app, not assumed from prose: the message handler short-circuits *before* dispatching any file action and posts back `undefined` whenever the `sourcePath` can't be resolved to a file (`app/assets/javascripts/views/file/visual-iframe.view.ts`): - -```js -if (!file?.commitChange) { - this.postMessage(`${message.action}-response`, undefined, message.editorCallbackId); - return; -} -``` - -Every method routed through this handler (`get`, `set`, `metadata`, `claimLock`, `releaseLock`, `getInputConfig`, and the `data`/`content` methods) therefore receives `undefined` for a non-existent file. Most client methods pass that straight through (`File.get` uses `typeof value === 'string' ? … : value`; `claimLock`/`releaseLock` resolve the raw value), so they resolve to `undefined` cleanly. The narrowed return types reflect that; `metadata()`/`getInputConfig()` already modelled it. - -**Still to do — and it's a client bug, not just a type:** `FileContent.get()` does **not** resolve `undefined` cleanly. Its client code is `resolve(value.content)` with no guard, unlike every sibling getter — so on a missing file `value` is `undefined`, `value.content` throws, and the promise never resolves. Its documented "Resolves to `undefined` if the file does not exist" is therefore currently false. Two ways to fix: -- **Make it behave like the others** — change the client to `resolve(value?.content)`, then it resolves `undefined`, and the published type should become `Promise` (currently `Promise`). This is the consistent option. -- **Or document reality** — if `FileContent.get` is meant to throw on a missing file, change the prose to say so (it would then differ from `File.get`/`FileData.get`, which resolve `undefined`). - -(`FileData.get` already resolves `undefined` cleanly and returns `Record | any[] | undefined`; `FileContent.get` is the lone outlier.) - -#### `data.upload` reuses `EditOptions`, surfacing fields it ignores - -`upload(file: File, options: EditOptions)` reuses the `edit()` options interface, so the generated reference lists `style` and `position` as `upload` parameters with edit-specific descriptions ("the field to open for editing", "used to position the panel"). But the handler (`file:upload-asset-file` in the app) only reads `slug` from those options — `style` and `position` are forwarded by the API and silently ignored on upload (verified against the implementation). - -Give `upload` its own options interface with just the field it uses: - -```ts -export interface UploadOptions { - /** The slug of the field to upload to. */ - slug: string; -} -// upload(file: File, options: UploadOptions): Promise -``` - -Then the reference stops showing `style`/`position` (which don't apply) and `slug` gets an upload-appropriate description. The docs pass can't fix this from comments alone, because the rendered fields come from `EditOptions`. (The fix is your call — a dedicated `UploadOptions` is the clean option; keeping the shared `EditOptions` is also fine, the docs just live with the extra fields. Low breaking risk either way.) - -#### `FileMetadata.last_modified` is typed `string | Date`, but the client always receives a string - -The published `FileMetadata.last_modified` is `string | Date | null`, while its sibling `created_at` is `string | null`. The `Date` is inaccurate for what the VE API client receives: the metadata handler serializes the payload with `JSON.stringify(file.metadata)` and the client `JSON.parse`s it, so any `Date` is converted to an ISO 8601 string in transit (`JSON.stringify` invokes `Date.prototype.toJSON`). The client therefore always receives `string | null`, never a `Date`. - -The `Date` most likely leaked from the app's internal model (`app/assets/javascripts/models/site-file.ts`: `last_modified: string | Date | null`), which is the *pre-serialization* type. Drop `Date` from the published `FileMetadata` so it matches `created_at` and reality; the descriptions can then align (`created_at` is documented as an "ISO 8601 timestamp", `last_modified` currently just "a timestamp"). - -#### `FileData.addEventListener` / `FileContent.addEventListener` never fire - -The reference documents `change` listeners on `FileData` (`data`) and `FileContent` (`content`), each with an example — but they never receive events. The client registers them on `CloudCannon:file:{path}:data:change` and `…:content:change` (`APIFileData`/`APIFileContent.eventPrefix` append `:data` / `:content`), but `triggerFileEvent` only dispatches: - -- `CloudCannon:file:{path}:{change|delete}` — the File-level listener -- `CloudCannon:{change|delete}` — Site-wide -- `CloudCannon:collection:{key}:{change|delete}` and `CloudCannon:dataset:{key}:{change|delete}` - -There is no `:data:` or `:content:` dispatch anywhere in the app — `APIEvents.dispatchEvent` is only called in those four places (in `cloudcannon-v1-api.ts`). So the `data`/`content` `change` listeners are dead; only `File.addEventListener` fires for file changes. - -Resolve one way or the other: -- **If they should work:** dispatch matching `:data:`/`:content:` events (from `triggerFileEvent`, or wherever data/content changes are detected). -- **If not:** remove them from the API and the docs, and point integrators to `File.addEventListener`, which already fires on any change to the file. - -### Tier 2 — Confirm against the implementation - -#### `AddArrayItemOptions.value` — required, or optional vs `sourceIndex`? - -`value: any` is required in the type, but `data.addArrayItem` documents it as an either/or with `sourceIndex` ("Provide either `value` for a new item, or `sourceIndex` to clone an existing one"). If that's the real behavior, `value` should be `value?: any`. This is about the *optionality* (`?`), not the `any` — keeping `value` loosely typed is correct. Please confirm against the implementation. - -#### No `datasets()` to match `collections()` — intended? - -The API Object has `collections()` (lists every Collection) but no `datasets()` equivalent — there's no way to list all Datasets. The `Dataset` JSDoc previously claimed it was "Returned by the `dataset()` and `datasets()` methods"; since `datasets()` exists in neither the types nor the app, that reference has been corrected to `dataset()` only. - -Decide which way to resolve the asymmetry: -- If listing all Datasets should be supported, add `datasets(): Promise` (mirroring `collections()`), and the `Dataset` description can list it again. -- If it's intentional that Datasets aren't listable, no code change needed — just confirming the asymmetry is by design. - -### Tier 3 — Optional (readability / consistency; fine to leave by design) - -#### Give `event.detail` a typed shape - -`addEventListener`/`removeEventListener` use the standard DOM listener type, so `event.detail` is untyped (`any`). The docs describe it in prose, but the reference can't surface it as a first-class type the way it now does for `FileMetadata`. The payload has two fields: -- `isNew: boolean` — `true` when a `change` fired for a newly created file, `false` for an update. -- `sourcePath: string` — the changed file's path. Present on the Site-wide (`CloudCannonVisualEditorAPIV1`), `Collection`, and `Dataset` listeners. The `File`, `FileData`, and `FileContent` listeners are already scoped to a single known file, so their events don't include it. - -If you want it typed, define a `CustomEvent`-style detail interface (e.g. `FileChangeEventDetail { sourcePath: string; isNew: boolean }`, plus a narrower variant without `sourcePath` for the file-scoped listeners) and type the listener signatures against it — then each listener could link to the `event.detail` type instead of relying on the prose Events section. **But if event payloads are meant to stay open or evolve, leaving `event.detail` as `any` is a reasonable choice** — this is a docs-rendering nicety, not a correctness issue. - -#### Name `FileData.get`'s inline options (`GetDataOptions`) - -`FileData.get` (`data.get`) is the only option-taking method whose options are an inline literal (`get(options?: { slug?: string })`) rather than a named interface like every other option method. The missing-description symptom is **already fixed in this branch** by adding a doc comment to the inline field, so `slug` now renders with a description. Extracting it into a named `GetDataOptions` interface would only make it consistent in *shape* with the others (and easier to reuse) — purely optional now. - -#### Name the inline `position` type (`PanelPosition`) - -`EditOptions.position` is an inline literal: - -```ts -position?: { x: number; y: number; left: number; width: number; top: number; height: number }; -``` - -Because it has no name, the reference prints the whole six-field literal as the parameter's type, which renders as a long, wrapping block next to short types like `string`. Extracting it into a named interface (e.g. `PanelPosition`) would make the parameter render as a short token that links to its own entry. Pure readability; no behavior change. - -#### V0 `'create'` event in the listener union (app-internal) - -The app's `addEventListener` union accepts `'change' | 'delete' | 'create'`, but `triggerFileEvent()` only ever dispatches `'change'` or `'delete'` (creation is a `'change'` with `event.detail.isNew === true`). The published `index.d.ts` is already correct (`'change' | 'delete'` only) — this is just app-side cleanup: drop `'create'` or actually dispatch it. - -## Already handled (for the record) - -Resolved in earlier maintainer pushes, noted so they aren't re-flagged: `set(content: string)`, `get(options?: { slug?: string })` (dead `rewriteUrls` removed), `metadata(): Promise` + the `FileMetadata` interface, `setLoading(): Promise`, and removal of the unused error interfaces.