/** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ import type React from 'react'; import type { Field } from '@byline/core'; import type { DocumentPatch } from '@byline/core/patches'; interface FormError { field: string; message: string; } /** * Represents a file that has been selected but not yet uploaded. * The file is held locally until form submission. */ export interface PendingUpload { /** The actual File object to upload */ file: File; /** Blob URL for local preview (must be revoked on cleanup) */ previewUrl: string; /** The collection path for the upload endpoint */ collectionPath: string; } type FieldListener = (value: any) => void; type ErrorsListener = (errors: FormError[]) => void; type MetaListener = () => void; type SystemPathListener = (value: string | null) => void; type SystemAvailableLocalesListener = (value: string[]) => void; type FieldUploadingListener = (uploading: boolean) => void; /** * Why the form is dirty, partitioned by write semantics — drives the single * Save button. `content` mints a new version (normal workflow). `direct-write` * is an immediate, non-versioned write of the document-grain system fields * (path / advertised locales) that does NOT reset workflow status. `both` does * each through its own write path. See docs/08-internationalization/index.md. */ export type DirtyReason = 'none' | 'content' | 'direct-write' | 'both'; export interface DirtyBreakdown { reason: DirtyReason; /** Document field data / patches changed → versioned write. */ contentDirty: boolean; /** Path widget changed → non-versioned direct write. */ pathDirty: boolean; /** Available-locales widget changed → non-versioned direct write. */ availableLocalesDirty: boolean; } interface FormContextType { /** * The persisted document id when the form edits an existing document, * `null` while the document is unsaved (create mode). Upload widgets use * this to honour `upload.requireSavedDocument` (see `UploadConfig` in * `@byline/core`). */ documentId: string | null; /** * Path of the collection this form edits, `null` when the form is rendered * without one. Upload widgets need it to address the upload endpoint. * * It lives here rather than being passed down because it is constant for * the whole form: threading it as a prop meant every nesting-capable * container (`array`, `group`, `blocks`) had to remember to forward it, * and a container that forgot silently rendered upload fields read-only — * no error, just a missing drop zone. That happened twice, in `array` / * `group` and then in `blocks`. A value read from context cannot be * dropped by a container that never carries it. */ collectionPath: string | null; setFieldValue: (name: string, value: any) => void; setFieldStore: (name: string, value: any) => void; getFieldValue: (name: string) => any; getFieldValues: () => Record; getPatches: () => DocumentPatch[]; appendPatch: (patch: DocumentPatch) => void; resetPatches: () => void; hasChanges: () => boolean; resetHasChanges: () => void; runFieldHooks: (fields: Field[]) => Promise; validateForm: (fields: Field[], additionalErrors?: FormError[]) => FormError[]; trackFieldChange: (pending: Promise) => () => void; waitForFieldChanges: () => Promise; errors: FormError[]; getErrors: () => FormError[]; clearErrors: () => void; setFieldError: (field: string, message: string) => void; clearFieldError: (field: string) => void; isDirty: (fieldName: string) => boolean; /** * Partition the current dirty state into content vs. system-field (path / * advertised-locales) writes so the Save button can branch. See * docs/08-internationalization/index.md. */ getDirtyBreakdown: () => DirtyBreakdown; subscribeField: (name: string, listener: FieldListener) => () => void; subscribeErrors: (listener: ErrorsListener) => () => void; subscribeMeta: (listener: MetaListener) => () => void; addPendingUpload: (fieldPath: string, upload: PendingUpload) => boolean; removePendingUpload: (fieldPath: string) => void; removePendingUploadsUnder: (itemPath: string) => void; getPendingUploads: () => Map; hasPendingUploads: () => boolean; clearPendingUploads: () => void; setFieldUploading: (fieldPath: string, uploading: boolean) => void; getIsFieldUploading: (fieldPath: string) => boolean; subscribeFieldUploading: (fieldPath: string, listener: FieldUploadingListener) => () => void; getSystemPath: () => string | null; setSystemPath: (value: string | null) => void; subscribeSystemPath: (listener: SystemPathListener) => () => void; getSystemAvailableLocales: () => string[]; setSystemAvailableLocales: (value: string[]) => void; subscribeSystemAvailableLocales: (listener: SystemAvailableLocalesListener) => () => void; } export declare const useFormContext: () => FormContextType; export declare const FormProvider: ({ children, initialData, documentId, collectionPath, }: { children: React.ReactNode; initialData?: Record; /** * The persisted document id (edit mode); `null` while unsaved. Exposed on * the context for upload widgets honouring `upload.requireSavedDocument`. */ documentId?: string | null; /** * Path of the collection being edited. Exposed on the context so upload * widgets can reach the upload endpoint from any nesting depth without * every container forwarding it — see `FormContextType.collectionPath`. */ collectionPath?: string | null; }) => React.JSX.Element; /** * Subscribe to the system `path` slot edited by the path widget. * Returns the current value (or `null` when no override is set). */ export declare const useSystemPath: () => string | null; export declare const useSystemAvailableLocales: () => string[]; export declare const useFormStore: () => FormContextType; export declare const useFieldError: (name: string) => string | undefined; export declare const useFormMeta: () => { hasChanges: boolean; }; export declare const useIsDirty: (name: string) => boolean; export declare const useFieldValue: (name: string) => T | undefined; export declare const useIsFieldUploading: (fieldPath: string) => boolean; export {};