import type { Schema } from '$lib/common' import { twMerge } from 'tailwind-merge' import { type AppComponent } from './editor/component' import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType' import type { Output } from './rx' import type { App, ComponentCssProperty, GridItem, HorizontalAlignment, VerticalAlignment } from './types' import { gridColumns } from './gridUtils' export const BG_PREFIX = 'bg_' export function migrateApp(app: App) { ;(app?.hiddenInlineScripts ?? []).forEach((x) => { if (x.type == undefined) { //@ts-ignore x.type = 'runnableByName' } //TODO: remove after migration is done if (x.doNotRecomputeOnInputChanged != undefined) { x.recomputeOnInputChanged = !x.doNotRecomputeOnInputChanged x.doNotRecomputeOnInputChanged = undefined } }) allItems(app.grid, app.subgrids).forEach((x) => { gridColumns.forEach((column: number) => { if (x?.[column]?.fullHeight === undefined) { x[column].fullHeight = false } }) }) } export function allItems( grid: GridItem[], subgrids: Record | undefined ): GridItem[] { if (subgrids == undefined) { return grid } return [...grid, ...Object.values(subgrids).flat()] } export function schemaToInputsSpec( schema: Schema, defaultUserInput: boolean ): Record { if (schema?.properties == undefined) { return {} } return Object.keys(schema.properties).reduce((accu, key) => { const property = schema.properties[key] accu[key] = { type: defaultUserInput ? 'user' : 'static', value: property.default, fieldType: property.type, format: property.format } if (Array.isArray(property.enum)) { accu[key] = { ...accu[key], selectOptions: property.enum, fieldType: 'select' } } return accu }, {}) } export function accessPropertyByPath(object: T, path: string): any { let a = path.split('.') for (let i = 0, depth = a.length; i < depth; ++i) { let key = a[i] if (object[key] != undefined) { object = object[key] } else { // Value not found return } } return object } export function fieldTypeToTsType(inputType: InputType): string { switch (inputType) { case 'number': return 'number' case 'integer': return 'number' case 'boolean': return 'boolean' case 'object': return 'object' case 'array': return 'array' case 'any': return 'any' case 'tab-select': return 'Tab' default: return 'string' } } export function isScriptByNameDefined(appInput: AppInput | undefined): boolean { if (!appInput) { return false } if (appInput.type === 'runnable' && appInput.runnable?.type == 'runnableByName') { return appInput.runnable?.name != undefined } return false } export function isScriptByPathDefined(appInput: AppInput | undefined): boolean { if (!appInput) { return false } if (appInput.type === 'runnable' && appInput.runnable?.type == 'runnableByPath') { return Boolean(appInput.runnable?.path) } return false } export function clearResultAppInput(appInput: ResultAppInput): ResultAppInput { appInput.runnable = undefined if (Object.keys(appInput.fields ?? {}).length > 0) { appInput.fields = {} } return appInput } export function toStatic( app: App, staticExporter: Record any>, summary: string ): { app: App; summary: string } { const newApp: App = JSON.parse(JSON.stringify(app)) allItems(newApp.grid, newApp.subgrids).forEach((x) => { let c: AppComponent = x.data if (c.componentInput?.type == 'runnable') { c.componentInput.value = staticExporter[x.id]() } }) newApp.hiddenInlineScripts?.forEach((x, i) => { x.noBackendValue = staticExporter[BG_PREFIX + i]() }) return { app: newApp, summary } } export function buildExtraLib( components: Record>>, idToExclude: string, state: Record, goto: boolean ): string { const cs = Object.entries(components) .filter(([k, v]) => k != idToExclude && k != 'state') .map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))]) .map( ([k, v]) => `declare const ${k}: WidenRo<${JSON.stringify(v)}>; ` ) .join('\n') return ` type Widen = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends Array ? Array> : T extends object ? Partial<{ [K in keyof T]: Widen } & {[key: string]: any}> : T; type WidenRo = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends Array ? Readonly>> : T extends object ? Readonly<{ [K in keyof T]: WidenRo }> : T; /** The mutable state of the app */ declare const state: Widen<${JSON.stringify(state)}> & {[key: string]: any}; ${cs} ${ goto ? ` /** open a window or tab * @param path path/url to go to * @param open in a tab? */ declare async function goto(path: string, newTab?: boolean): Promise; /** set tab * @param id component's id * @param index index of the tab to set */ declare function setTab(id: string, index: number): void; /** recompute a component's runnable or background runnable * @param id component's id */ declare function recompute(id: string): void; /** get the ag grid api from an AgGridTable * @param id component's id */ declare function getAgGrid(id: string): {api: any, columnApi: any} | undefined; /** set value of a component * @param id component's id * @param value value to set */ declare function setValue(id: string, value: any): void; /** set selected index of a table * @param id component's id * @param index index to set */ declare function setSelectedIndex(id: string, index: number): void; /** close a drawer or modal * @param id component's id */ declare function open(id: string): void; /** close a drawer or modal * @param id component's id */ declare function close(id: string): void; /** validate form field property 'key' * @param id component's id * @param key property's key to validate */ declare function validate(id: string, key: string): void; /** validate form field property 'key' * @param id component's id * @param key property's key to validate */ declare function invalidate(id: string, key: string, error: string): void; /** validate all form's properties * @param id component's id */ declare function validateAll(id: string): void; /** Clear the files of a file input component * @param id component's id */ declare function clearFiles(id: string): void; /** Display a toast message * @param message message to display */ declare function showToast(message: string, error?: boolean): void; /** * Wait for a job to finish * @param id job id * @returns the result of the job */ declare async function waitJob(id: string): Promise; /** * ask user resource on a UserResourceComponent */ declare async function askNewResource(id: string): void; /** * Download a file from a url or base64 encoded string * @param input url, base64 encoded string, [dataUrl](https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data) or S3 object * @param [fileName] name of the file to download (optional) */ declare function downloadFile(input: string | {s3: string, storage?: string}, fileName?: string): void; ` : '' } /** The iterator within the context of a list */ declare const iter: {index: number, value: any}; /** The row within the context of a table */ declare const row: {index: number, value: Record, disabled: boolean}; /** The file within the s3 file input */ declare const file: File | undefined; /** The group fields within the context of a container's group */ declare const group: Record; /** The result within the context of a transformer */ declare const result: any; ` } export function getAllScriptNames(app: App): string[] { const names = allItems(app.grid, app?.subgrids).reduce((acc, gridItem: GridItem) => { const { componentInput } = gridItem.data if ( componentInput?.type === 'runnable' && componentInput?.runnable?.type === 'runnableByName' ) { acc.push(componentInput.runnable.name) } if (gridItem.data.type === 'tablecomponent') { gridItem.data.actionButtons.forEach((actionButton) => { if (actionButton.componentInput?.type === 'runnable') { if (actionButton.componentInput.runnable?.type === 'runnableByName') { acc.push(actionButton.componentInput.runnable.name) } } }) } if ( gridItem.data.type === 'aggridcomponent' || gridItem.data.type === 'aggridcomponentee' || gridItem.data.type === 'dbexplorercomponent' || gridItem.data.type === 'aggridinfinitecomponent' || gridItem.data.type === 'aggridinfinitecomponentee' ) { gridItem.data.actions?.forEach((actionButton) => { if (actionButton.componentInput?.type === 'runnable') { if (actionButton.componentInput.runnable?.type === 'runnableByName') { acc.push(actionButton.componentInput.runnable.name) } } }) } if (gridItem.data.type === 'menucomponent') { gridItem.data.menuItems.forEach((menuItem) => { if (menuItem.componentInput?.type === 'runnable') { if (menuItem.componentInput.runnable?.type === 'runnableByName') { acc.push(menuItem.componentInput.runnable.name) } } }) } return acc }, [] as string[]) const unusedNames = app.unusedInlineScripts.map((x) => x.name) const backgroundNames = app.hiddenInlineScripts?.map((x) => x.name) ?? [] return [...names, ...unusedNames, ...backgroundNames] } function clearAndUpper(text: string) { return text.replace(/-/, '').toUpperCase() } export function toPascalCase(text: string) { return text.replace(/(^\w|-\w)/g, clearAndUpper) } export function toKebabCase(text: string) { return text.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase()) } export function initCss>( appCss?: Record, componentCss?: T ): T { if (!componentCss) return {} as T return Object.fromEntries( Object.entries(componentCss).map(([key, v]) => { // This is the general style of the component type const appStyle = appCss?.[key]?.style?.trim() || '' const appEnding = appStyle?.endsWith(';') || !appStyle ? ' ' : '; ' // This is the custom style of the component instance const compStyle = v?.style?.trim() || '' const compEnding = compStyle?.endsWith(';') || !compStyle ? ' ' : ';' return [ key, { style: (appStyle + appEnding + compStyle + compEnding).trim(), class: twMerge(appCss?.[key]?.class, v?.class), evalClass: appCss?.[key]?.evalClass || v?.evalClass } ] }) ) as T } export function tailwindHorizontalAlignment(alignment?: HorizontalAlignment) { if (!alignment) return '' const classes: Record = { left: 'justify-start', center: 'justify-center', right: 'justify-end' } return classes[alignment] } export function tailwindVerticalAlignment(alignment?: VerticalAlignment) { if (!alignment) return '' const classes: Record = { top: 'items-start', center: 'items-center', bottom: 'items-end' } return classes[alignment] } export const TailwindClassPatterns = { bg: /bg-(?:[^-\s]+-)?(?:[^-\s]+)/g, height: /(h-[^\s]+)/g, width: /(w-[^\s]+)/g } export function hasTailwindClass(classes: string | undefined, pattern: RegExp) { return Boolean(classes?.match(pattern)) } export function transformBareBase64IfNecessary(source: string | undefined) { if (!source) { return source } if (/^(http|https):\/\//.test(source) || source.startsWith('/')) { return source } if (source.startsWith('data:') || source.includes(',')) { return source } else { return `data:application/octet-stream;base64,${source}` } } export function getImageDataURL(imageKind: string | undefined, image: string | undefined) { if (!imageKind || !image) { return null } switch (imageKind) { case 'png encoded as base64': return 'data:image/png;base64,' + image case 'jpeg encoded as base64': return 'data:image/jpeg;base64,' + image case 'svg encoded as base64': return 'data:image/svg+xml;base64,' + image default: return image } } export const ctxRegex = /^ctx\.(workspace|groups|username|email|author)$/