/// /** Mimics FileFilter from electron without having to import Electron. */ export interface FileFilter { /** NOTE: Extensions should not contain the leading separator (dot/period). */ extensions: string[]; /** Name to be shown to the user for this filter. */ name: string; } import type { ObjectDataset, ObjectChangeset } from './objects'; import type { CommitOutcome } from './changes'; import type { IndexStatus } from './indexes'; import type { BaseAction, PersistentStateReducerHook } from '../usePersistentStateReducer'; import type { TimeTravelingPersistentStateReducerHook } from '../useTimeTravelingPersistentStateReducer'; import type { Settings, GlobalSettings } from '../settings'; import type { OpenFileDialogProps, SaveFileDialogProps } from './dialogs'; import type { SubprocessDescription } from './binary-invocation'; import type { ExportFormatInfo } from './export-formats'; import type { ObjectSpecViewID, ObjectViewProps } from './object-spec'; import type { ContextSpec as OperationContext } from '../widgets/OperationQueue/context'; /** A specification of context available to extension component. */ export interface DatasetContext extends OperationContext { /** Dataset title, as specified by dataset manifest/metadata YAML file in the repository. */ title: string; logger: { log: Console["log"]; error: Console["error"]; debug: Console["debug"]; }; /** Launches specified URI in OS’s default browser. */ openExternalLink: (opts: { uri: string; }) => Promise; /** Retrieves object data, and does its best to auto-refresh hook value if objects change. */ useObjectData: Hooks.Data.GetObjectDataset; /** * Retrieves object data, as a simple async function. * Generally it’s recommended to use the `useObjectData` hook instead. * * Request parameters are the same as to the hook. */ getObjectData: (opts: ObjectDatasetRequest) => Promise; /** * Builds (or reuses) custom dataset index for windowed data access. * * Filtered indexes are based on *query expressions*. * A query expression is a small predicate function that returns true or false for each object * in the dataset; objects for which true is returned are included in the filtered index. * * Whenever dataset contents change, * Paneron calculates (using query expression) * which filtered object indexes were affected by the change, * performs relevant updates on those indexes * and issues events triggering requisite hook refreshes & dependent GUI updates. * * NOTE: In some cases Paneron may fail to react to underlying object change, * resulting in stale indexes. In such cases, the user can refresh indexes. * * This hook will not unnecessarily create filtered index * if it already exists for given query expression. * * This hook returns filtered object index ID, * which is intended to be used for further calls: * * - Filtered index status can be obtained using `useIndexDescription()`. * * - Filtered object paths can be subsequently obtained * via `useObjectPathFromFilteredIndex()`, * which in turn can be passed to `useObjectData()` * in order to obtain actual deserialized obejct data. */ useFilteredIndex: Hooks.Indexes.GetOrCreateFiltered; /** Describes a filtered object index: provides indexing status and object count. */ useIndexDescription: Hooks.Indexes.Describe; /** * Retrieve an object from a filtered index by its position in the index. * Used for efficient data windowing. */ useObjectPathFromFilteredIndex: Hooks.Indexes.GetFilteredObject; /** Non-hook equivalent of `useObjectPathFromFilteredIndex()` as plain async function. */ getObjectPathFromFilteredIndex: (opts: FilteredObjectPathRequest) => Promise; /** * Given object path, retrieve its position in given filtered object index. * NOTE: The primary use case of this hook is workaround-ish, so it may be deprecated in future. */ useFilteredIndexPosition: Hooks.Indexes.GetFilteredObjectIndexPosition; /** Non-hook equivalent of `useFilteredIndexPosition()` as plain async function. */ getFilteredIndexPosition: (opts: FilteredObjectIndexPositionRequest) => Promise; /** * Runs a set of map-reduce chains over the entire dataset. * The final result is an object where each key is one of given chain IDs, * and the value corresponding to the output of map-reduce chain * (if `reduceFunc` is not provided, the corresponding value is always a list). * `predicateFunc` is required. * */ useMapReducedData: >(opts: { chains: Record & { predicateFunc: string; }>; }) => ValueHook; /** * As useMapReducedData, but in the shape of a regular async function. */ getMapReducedData: >(opts: { chains: Record; }) => Promise; usePersistentDatasetStateReducer: Hooks.UsePersistentDatasetStateReducer; useTimeTravelingPersistentDatasetStateReducer: Hooks.UseTimeTravelingPersistentDatasetStateReducer; /** * Invokes the bundled Metanorma binary with given arguments, and returns subprocess description. * * For ongoing monitoring of invocation status, use `useMetanormaInvocationStatus()`. * * NOTE: It may be available even if Metanorma binary does not exist, in which case invocation will throw an error. */ invokeMetanorma?: (opts: { cliArgs: string[]; }) => Promise; /** * Query Metanorma subprocess execution status. * * - Will return null if Metanorma subprocess cannot be found. * * - Will auto-refresh returned subprocess description. */ useMetanormaInvocationStatus?: () => ValueHook; operationKey?: string; /** * Provides access to remote username, as configured in Paneron settings. * * NOTE: Dataset extension *could* use it to infer author’s role by comparing with some list. * However, this is not a security measure and must be used for sensitive access control * in place of Git server-level authentication. * * This hook can be used for informative purposes to show role-appropriate GUIs * to already trusted users who will abide * despite technically being able to sidestep the comparison. */ useRemoteUsername: RemoteUsernameHook; copyObjects: (objects: ObjectDataset) => Promise; requestCopiedObjects: () => Promise; /** * Returns Paneron-global settings that may make sense for extensions * (e.g., sidebar positioning). * * TODO: Allow overriding global settings with scoped? */ useGlobalSettings: Hooks.Settings.UseGlobalSettings; /** Returns arbitrary settings scoped to current dataset. */ useSettings: Hooks.Settings.UseSettings; /** Updates a given setting in dataset scope. */ updateSetting: Hooks.Settings.UpdateSetting; /** TODO: Provisional; for the new extension architecture */ getObjectView: (opts: { objectPath: string; viewID: ObjectSpecViewID; }) => React.FC | undefined; /** Returns a string value obtained from a binary blob given as Uint8Array. */ useDecodedBlob: Hooks.UseDecodedBlob; /** Given a string, will return a binary blob as Uint8Array. */ getBlob?: (fromStringValue: string) => Promise; /** * Generates a UUID (not really useful in read-only mode, so is optional) * * @deprecated use browser’s `crypto.randomUUID()` instead? */ makeRandomID?: () => Promise; /** * Commits changes to the dataset on per-object level. */ updateObjects?: Hooks.Data.UpdateObjects; /** * Manipulates repository data (within the dataset) on subtree level (delete or move subtree). * * NOTE: Does not do integrity checks. Dangerous. */ updateTree?: Hooks.Data.UpdateObjectTree; /** * Invokes file selection dialog and returns file data as object dataset when user confirms. * This does not mutate dataset / Git repo contents, updateObjects still * must be invoked later in order to commit newly added or replaced file. */ requestFileFromFilesystem?: (opts: OpenFileDialogProps, cb?: (data: ObjectDataset) => void) => Promise; /** * Invokes file save dialog, * writes provided buffer data to chosen file path, * and returns that path. * * Note: currently this does not yet check selected path for sanity, * so the user can write to repository’s own working directory * and mess things up. */ writeFileToFilesystem?: (opts: { dialogOpts: SaveFileDialogProps; bufferData: Uint8Array; }) => Promise<{ success: true; savedToFileAtPath: string; }>; /** * List available export options. * Some may be registered by dataset extension, * some may be default provided by host app itself. */ listExporters: () => { [exporter: string]: ExportFormatInfo; }; /** * Invokes file selection dialog, * if any files were selected adds them to dataset at specified path, * and makes a commit with specified commit message. * * For performance, this function breaks the logical object layer abstraction: * it does not deserialize buffers and treats selected files * as pre-serialized low-level repository buffers. * * If object(s) with same names already exist at the path, * will overwrite them with new versions. * * `opts` can modify that behavior, * see its member documentation for details. * * Returned commit outcome can be null if no objects were selected. */ addFromFilesystem?: ( /** * Options to pass to OS file selection dialog. * * NOTE: `allowMultiple` has no effect * if `opts.replaceTarget` is set. */ dialogOpts: OpenFileDialogProps, commitMessage: string, /** * Default behavior is to preserve original filenames * and treat `targetPath` as a tree root under which * new objects will be written as leaf objects with full contents. * If target path exists and is a leaf object (file), * or a leaf object exists with the same name as one of the selected files, * the call will fail. * * Note: `opts.replaceTarget` changes this behavior. */ targetPath: string, opts: { /** * Whether to replace `targetPath`. * * Treat `targetPath` as a leaf buffer (file) and replace it. * The user will be able to select only one file in this case * (`dialogOpts.allowMultiple` will have no effect). * * Any existing object at `targetPath` will be replaced. * Original filename will be ignored. * If target path exists and is a tree (directory), the call will fail. */ replaceTarget?: true; /** * If specified as true, selected file(s) will be uploaded to LFS, * and only after successful upload LFS pointer files will be committed * in place of actual file contents. * * If upload fails, an error will be thrown. * * NOTE: If some LFS upload succeeds but subsequent commit fails * (or is later reverted), or upload for another object fails, * already uploaded data may remain on LFS and keep costing storage. * * NOTE: If repository does not have the remote configured, * this option will have no effect * (actual blobs will be stored directly in repository). * * NOTE: Regarding authentication, LFS is expected to use the same * credentials as Git. If Git authentication details * (both username and password) cannot be retrieved * from OS credential storage, the call will throw an error. */ offloadToLFS?: true; }) => Promise<{ commitOutcome: CommitOutcome | null; }>; } /** * A hook that contains the result of calling some endpoint * and a way of refreshing the result. */ export interface ValueHook { /** The value returned from the call. */ value: T; /** * A list of errors, usually one item, as strings. * Non-empty if there were errors thrown by IPC handler. */ errors: string[]; /** * Whether the value is currently being updated * (handler response is being awaited). */ isUpdating: boolean; /** * Can be used to imperatively refresh returned value * by calling handler. * * Generally, it’s better to pass the appropriate memoize arguments instead. */ refresh: () => void; _reqCounter: number; } export declare type FilteredObjectPathRequest = { /** * The ID of the filtered object index from which the object is requested. * Index ID is returned by `useFilteredIndex()` hook. */ indexID: string; /** * Numerical position of object path in this index. * * Filter description hook will return the total number of objects in this index, * and that can be used as the upper bound for position. */ position: number; }; export declare type FilteredObjectPathResponse = { /** * Object path at requested position in the index. * This path can be passed to, e.g., `useObjectData()` to obtain actual deserialized object. * * NOTE: there are some edge cases where objectPath can be empty, and those need to be documented. * However, `useObjectData()` will not cause a loud error if an empty path is passed. */ objectPath: string; }; export declare type FilteredObjectIndexPositionRequest = { /** The ID of the filtered object index from which object position is requested. */ indexID: string; /** Object path, for which position is requested. */ objectPath: string; }; export declare type FilteredObjectIndexPositionResponse = { /** * Position of given object in the index. * Can be null if object path was not found in the index. */ position: number | null; }; export declare type ObjectDatasetRequest = { /** A list of dataset-relative object paths to fetch. */ objectPaths: string[]; /** * Whether or not to attempt to resolve large file storage pointers. * * The behavior for an unresolved LFS pointer is to return * an object with a single `lfsPointerInfo` property * instead of actual object data for that path. * (You can test for that property when displaying the object.) * * With this flag, when an LFS pointer is encountered, * Paneron will attempt to download the full blob * if it is not cached yet. This may take some time. * * NOTE: this flag does not guarantee all pointers will be resolved. * * - A failed download will cause an unresolved pointer to be returned. * * - If repository does not have remote configured, * this flag will not have effect (unresolved pointers will be returned). * * NOTE: Regarding authentication, LFS is expected to use the same * credentials as Git. If Git authentication details * (both username and password) cannot be retrieved * from OS credential storage, the call will throw an error. */ resolveLFS?: true; }; export declare type ObjectDatasetResponse = { /** * A map of requested object paths to deserialized object data, except: * * - If no object data could be retrieved for given object path, * object path will be mapped to `null`. * * - If `resolveLFS` was not set, * objects that are pointers to LFS will be returned as is * (they will not contain the actual deserialized object data). */ data: ObjectDataset; }; export declare namespace Hooks { type UseDecodedBlob = (opts: { blob: Uint8Array; }) => { asString: string; }; type UsePersistentDatasetStateReducer = PersistentStateReducerHook; type UseTimeTravelingPersistentDatasetStateReducer = TimeTravelingPersistentStateReducerHook; namespace Settings { type UseGlobalSettings = () => ValueHook<{ settings: GlobalSettings; }>; type UseSettings = () => ValueHook<{ settings: Settings; }>; type UpdateSetting = (opts: { key: string; value: any; }) => Promise<{ success: true; }>; } namespace Indexes { type Describe = (opts: { /** Filtered object index ID. Is returned by `useFilteredIndex()`. */ indexID?: string; }) => ValueHook<{ status: IndexStatus; }>; type GetOrCreateFiltered = (opts: { /** * Query expression for the index. * * This must be a string containing a valid function definition. * The function will be constructed from this definition once, * and invoked against every object in default index. * * The function must return either true or false. Objects for which * `queryExpression` returns true will be included in this index. * * The function will receive two positional arguments, called: * 1. `objPath` (dataset-relative path to the object, including file extension if any) * 2. `obj` (deserialized object data, as a JavaScript object) * * Thus, the function can do simple tests against object path, * like returning all objects that end with index.yaml and are in some subdirectory: * * `return objPath.startsWith('/some/subdirectory/') && objPath.endsWith('index.yaml')` * * as well as do slightly more complex investigation of deserialized object properties, * like finding all objects which contain `published` property set to `true`): * * `return obj.published === true` * * or both. */ queryExpression: string; keyExpression?: string; }) => ValueHook<{ indexID: string | undefined; }>; type GetFilteredObject = (opts: FilteredObjectPathRequest) => ValueHook; type GetFilteredObjectIndexPosition = (opts: FilteredObjectIndexPositionRequest) => ValueHook; type ListenToFilteredIndexUpdates = (eventCallback: (evt: { indexID: string; }) => Promise, args: any[]) => void; type ListenToIndexStatusChanges = (eventCallback: (evt: { indexID?: string; status: IndexStatus; }) => Promise, args: any[]) => void; } namespace Data { type GetObjectDataset = (opts: ObjectDatasetRequest & { nounLabel?: string; }) => ValueHook; type UpdateObjects = (opts: { objectChangeset: ObjectChangeset; commitMessage: string; /** Disables the checks against oldValue keys in the changeset. */ _dangerouslySkipValidation?: true; }) => Promise; type UpdateObjectTree = (opts: { /** Old subtree root, dataset-relative. */ subtreeRoot: string; /** New subtree root, dataset-relative. If null, tree will be deleted. */ newSubtreeRoot: string | null; commitMessage: string; }) => Promise; interface MapReduceChain { predicateFunc?: string; /** * Definition of the map function as a string. * * Available arguments: * `key` (string, object path), * `value` (anything, deserialized object data), * `emit` (call instead of return, possibly multiple times). */ mapFunc: string; /** * Definition of the reduce function as a string, * complete with return statement. * Doesn’t have to be provided. * * Available arguments: * `accumulator` (data reduced so far), * `value` (next value emitted by the mapper). */ reduceFunc?: string; } type MapReduceChains = Record; } } export declare type RemoteUsernameHook = () => ValueHook<{ /** * Remote Git username, * as specified for this repository by the user in Paneron settings. */ username?: string; }>;