diff --git a/packages/toolpad-app/src/toolpad/AppEditor/BindingEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/BindingEditor.tsx index d6970035c10..772ce694816 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/BindingEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/BindingEditor.tsx @@ -30,7 +30,7 @@ import { JsExpressionAction, } from '@mui/toolpad-core'; import { TabContext, TabList } from '@mui/lab'; -import { Maybe, WithControlledProp, GlobalScopeMeta } from '../../utils/types'; +import { Maybe, WithControlledProp } from '../../utils/types'; import { JsExpressionEditor } from './PageEditor/JsExpressionEditor'; import JsonView from '../../components/JsonView'; import { tryFormatExpression } from '../../utils/prettier'; @@ -44,6 +44,7 @@ import * as appDom from '../../appDom'; import { usePageEditorState } from './PageEditor/PageEditorProvider'; import GlobalScopeExplorer from './GlobalScopeExplorer'; import TabPanel from '../../components/TabPanel'; +import { GlobalScopeMeta } from '../../types'; interface BindingEditorContext { label: string; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/GlobalScopeExplorer.tsx b/packages/toolpad-app/src/toolpad/AppEditor/GlobalScopeExplorer.tsx index 3f3dcfc3603..17fa4a5ff45 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/GlobalScopeExplorer.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/GlobalScopeExplorer.tsx @@ -1,7 +1,7 @@ import { Typography, Divider, Box, SxProps } from '@mui/material'; import * as React from 'react'; import ObjectInspector from '../../components/ObjectInspector'; -import { GlobalScopeMeta } from '../../utils/types'; +import { GlobalScopeMeta } from '../../types'; export interface GlobalScopeExplorerProps { value?: Record; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx index af4ec954693..84349a014dd 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx @@ -2,7 +2,8 @@ import { Stack, SxProps } from '@mui/material'; import * as React from 'react'; import { BindableAttrValue, PropValueType, LiveBinding } from '@mui/toolpad-core'; import { BindingEditor } from '../BindingEditor'; -import { WithControlledProp, GlobalScopeMeta } from '../../../utils/types'; +import { WithControlledProp } from '../../../utils/types'; +import { GlobalScopeMeta } from '../../../types'; import { getDefaultControl } from '../../propertyControls'; function renderDefaultControl(params: RenderControlParams) { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/JsExpressionEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/JsExpressionEditor.tsx index ba24a85c59c..03bef9c654c 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/JsExpressionEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/JsExpressionEditor.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import jsonToTs from 'json-to-ts'; import { Skeleton, styled, SxProps } from '@mui/material'; -import { WithControlledProp, GlobalScopeMeta } from '../../../utils/types'; +import { WithControlledProp } from '../../../utils/types'; +import { GlobalScopeMeta } from '../../../types'; import lazyComponent from '../../../utils/lazyComponent'; import { hasOwnProperty } from '../../../utils/collections'; import ElementContext from '../ElementContext'; diff --git a/packages/toolpad-app/src/toolpadDataSources/rest/client.tsx b/packages/toolpad-app/src/toolpadDataSources/rest/client.tsx index a0f429f902e..68e34c88ffb 100644 --- a/packages/toolpad-app/src/toolpadDataSources/rest/client.tsx +++ b/packages/toolpad-app/src/toolpadDataSources/rest/client.tsx @@ -23,6 +23,7 @@ import { ConnectionEditorProps, ExecFetchFn, QueryEditorProps, + GlobalScopeMeta, } from '../../types'; import { FetchPrivateQuery, @@ -41,7 +42,7 @@ import { useEvaluateLiveBindingEntries, } from '../../toolpad/AppEditor/useEvaluateLiveBinding'; import MapEntriesEditor from '../../components/MapEntriesEditor'; -import { Maybe, GlobalScopeMeta } from '../../utils/types'; +import { Maybe } from '../../utils/types'; import AuthenticationEditor from './AuthenticationEditor'; import { isSaveDisabled, validation } from '../../utils/forms'; import * as appDom from '../../appDom'; diff --git a/packages/toolpad-app/src/types.ts b/packages/toolpad-app/src/types.ts index 686ac921b0b..4bc8e4328c9 100644 --- a/packages/toolpad-app/src/types.ts +++ b/packages/toolpad-app/src/types.ts @@ -193,3 +193,11 @@ export interface RuntimeState { appId: string; modules: Record; } + +export interface MetaField { + description?: string; + deprecated?: boolean | string; + tsType?: string; + value?: any; +} +export type GlobalScopeMeta = Record; diff --git a/packages/toolpad-app/src/utils/bindings.ts b/packages/toolpad-app/src/utils/bindings.ts deleted file mode 100644 index 4b231b1d4f1..00000000000 --- a/packages/toolpad-app/src/utils/bindings.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { BindingAttrValueFormat } from '@mui/toolpad-core'; -import invariant from 'invariant'; - -type ParsedBinding = string[]; - -export function parse(expr: string): ParsedBinding { - return expr.split(/{{\s*([a-zA-Z0-9.]+?)\s*}}/); -} - -export function getInterpolations(parts: ParsedBinding): string[] { - return parts.filter((part, i) => i % 2 === 1); -} - -function toTemplateStringExpression(parts: ParsedBinding): string { - if (parts.length === 3 && !parts[0] && !parts[2]) { - return parts[1]; - } - const transformedParts = parts.map((part, i) => - i % 2 === 0 ? part.replaceAll('`', '\\`') : `\${${part}}`, - ); - return `\`${transformedParts.join('')}\``; -} - -export function formatExpression( - expr: ParsedBinding, - bindingFormat: BindingAttrValueFormat = 'default', -): string { - switch (bindingFormat) { - case 'stringLiteral': - return toTemplateStringExpression(expr); - case 'default': - return expr.join(''); - default: - return invariant(false, `Unrecognized binding format "${bindingFormat}"`); - } -} - -export function formatStringValue(expr: ParsedBinding): any { - return expr.join(''); -} - -export function resolve( - expr: ParsedBinding, - transform: (interpolation: string) => string, -): ParsedBinding { - return expr.map((part, i) => { - if (i % 2 === 1) { - return transform(part); - } - return part; - }); -} diff --git a/packages/toolpad-app/src/utils/collections.ts b/packages/toolpad-app/src/utils/collections.ts index d6d8bb45864..e6c6ff1d0f1 100644 --- a/packages/toolpad-app/src/utils/collections.ts +++ b/packages/toolpad-app/src/utils/collections.ts @@ -8,6 +8,10 @@ type Require = T & { [P in K]-?: T[P] }; type Ensure = K extends keyof U ? Require : U & Record; +/** + * Type aware version of Object.protoype.hasOwnProperty. + * See https://fettblog.eu/typescript-hasownproperty/ + */ export function hasOwnProperty( obj: X, prop: Y, @@ -15,6 +19,10 @@ export function hasOwnProperty( return obj.hasOwnProperty(prop); } +/** + * Maps `obj` to a new object. The `mapper` function receices an entry array of jey and value and + * is allowed to manipulate both. It can also return `null` to omit a property from the result. + */ export function mapProperties( obj: P, mapper: >(old: [K, P[K]]) => [L, U] | null, @@ -31,6 +39,9 @@ export function mapProperties( ); } +/** + * Maps an objects' property keys. The result is a new object with the mapped keys, but the same values. + */ export function mapKeys( obj: Record, mapper: (old: string) => string, @@ -38,13 +49,19 @@ export function mapKeys( return mapProperties(obj, ([key, value]) => [mapper(key), value]); } +/** + * Maps an objects' property values. The result is a new object with the same keys, but mapped values. + */ export function mapValues( obj: P, mapper: (old: P[PropertiesOf

], key: PropertiesOf

) => V, ): Record, V> { return mapProperties(obj, ([key, value]) => [key, mapper(value, key)]); } - +/** + * Filters an objects' property values. Similar to `array.filter` but for objects. The result is a new + * object with all the properties removed for which `filter` returned `false`. + */ export function filterValues

(obj: P, filter: (old: P[keyof P]) => boolean): Partial

; export function filterValues( obj: Record, diff --git a/packages/toolpad-app/src/utils/crypto.ts b/packages/toolpad-app/src/utils/crypto.ts index e1ecb772eb0..92792839964 100644 --- a/packages/toolpad-app/src/utils/crypto.ts +++ b/packages/toolpad-app/src/utils/crypto.ts @@ -6,4 +6,9 @@ const crypto = (invariant(!!global.crypto, 'Remove the crypto polyfill'), require('crypto')) : global.crypto; +/** + * Isomorphic version of web `crypto`. + * In anticipation of `globalThis.crypto` becoming stable in Node.js. + * See https://nodejs.org/api/globals.html#crypto_1 + */ export default crypto; diff --git a/packages/toolpad-app/src/utils/errors.ts b/packages/toolpad-app/src/utils/errors.ts index d551d5fe566..d0c35482831 100644 --- a/packages/toolpad-app/src/utils/errors.ts +++ b/packages/toolpad-app/src/utils/errors.ts @@ -7,6 +7,20 @@ export function serializeError(error: Error): SerializedError { return { message, name, stack }; } +/** + * Creates a javascript `Error` from an unkown value if it's not already an error. + * Does a best effort at inferring a message. Intended to be used typically in `catch` + * blocks, as there is no way to enforce only `Error` objects being thrown. + * + * ``` + * try { + * // ... + * } catch (rawError) { + * const error = errorFrom(rawError); + * console.assert(error instancof Error); + * } + * ``` + */ export function errorFrom(maybeError: unknown): Error { if (maybeError instanceof Error) { return maybeError; diff --git a/packages/toolpad-app/src/utils/evalExpression.ts b/packages/toolpad-app/src/utils/evalExpression.ts index 202c7e93c7d..c34102a86c2 100644 --- a/packages/toolpad-app/src/utils/evalExpression.ts +++ b/packages/toolpad-app/src/utils/evalExpression.ts @@ -1,4 +1,8 @@ let iframe: HTMLIFrameElement; + +/** + * Evaluates a javascript expression with global scope in an iframe. + */ export default function evalExpression(code: string, globalScope: Record) { // TODO: investigate https://www.npmjs.com/package/ses if (!iframe) { diff --git a/packages/toolpad-app/src/utils/fields.ts b/packages/toolpad-app/src/utils/fields.ts index 3e179643c8f..11a682bf461 100644 --- a/packages/toolpad-app/src/utils/fields.ts +++ b/packages/toolpad-app/src/utils/fields.ts @@ -1,6 +1,6 @@ const SINGLE_ACTION_INPUT_TYPES = ['checkbox', 'radio', 'range', 'color']; -export const hasFieldFocus = (documentTarget = document) => { +export function hasFieldFocus(documentTarget = document): boolean { const activeElement = documentTarget.activeElement as HTMLElement | HTMLInputElement; if (!activeElement) { @@ -15,4 +15,4 @@ export const hasFieldFocus = (documentTarget = document) => { const focusedContentEditable = contentEditable === 'true'; return focusedInput || focusedTextarea || focusedContentEditable; -}; +} diff --git a/packages/toolpad-app/src/utils/forms.ts b/packages/toolpad-app/src/utils/forms.ts index 60f92fcb6aa..244c0e75d17 100644 --- a/packages/toolpad-app/src/utils/forms.ts +++ b/packages/toolpad-app/src/utils/forms.ts @@ -12,6 +12,9 @@ function errorMessage(error: FieldError) { } } +/** + * Translates `react-hook-form` `formState` into error/helpText properties for UI components. + */ export function validation( formState: FormState, field: keyof T, @@ -24,6 +27,9 @@ export function validation( }; } +/** + * Reads `react-hook-form` `formState` and checks whether the state can and needs to be saved. + */ export function isSaveDisabled(formState: FormState): boolean { // Always destructure formState to trigger underlying react-hook-form Proxy object const { isValid, isDirty } = formState; diff --git a/packages/toolpad-app/src/utils/fs.ts b/packages/toolpad-app/src/utils/fs.ts index b4f352baebe..b2a562cbfc8 100644 --- a/packages/toolpad-app/src/utils/fs.ts +++ b/packages/toolpad-app/src/utils/fs.ts @@ -1,5 +1,8 @@ import * as fs from 'fs/promises'; +/** + * Like `fs.readFile`, but for JSON files specifically. Will throw on malformed JSON. + */ export async function readJsonFile(path: string): Promise { const content = await fs.readFile(path, { encoding: 'utf-8' }); return JSON.parse(content); diff --git a/packages/toolpad-app/src/utils/geometry.ts b/packages/toolpad-app/src/utils/geometry.ts index 3688050fd7b..3a6be4a461b 100644 --- a/packages/toolpad-app/src/utils/geometry.ts +++ b/packages/toolpad-app/src/utils/geometry.ts @@ -7,10 +7,16 @@ export interface Rectangle { height: number; } +/** + * Calculates the Euclidian distance between two points + */ export function distanceToPoint(x1: number, y1: number, x2: number, y2: number): number { return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); } - +/** + * Calculates the shortest Euclidian distance from a point to a rectangle. Returns `0` if the + * point falss within the rectangle. + */ export function distanceToRect(rect: Rectangle, x: number, y: number) { const left = rect.x; const top = rect.y; @@ -43,8 +49,11 @@ export function distanceToRect(rect: Rectangle, x: number, y: number) { return 0; } -// All credit goes to https://stackoverflow.com/a/6853926/419436 -// I was too lazy to figure out the math +/** + * Calculates the shortes Euclidian distance from a point to a line segment. + * All credit goes to https://stackoverflow.com/a/6853926/419436 + * I was too lazy to figure out the math + */ export function distanceToLine( x1: number, y1: number, @@ -85,6 +94,9 @@ export function distanceToLine( return Math.sqrt(dx * dx + dy * dy); } +/** + * Translates a `Rectangle` into CSS properties for absolutely positioned elements. + */ export function absolutePositionCss({ x, y, width, height }: Rectangle): React.CSSProperties { return { left: x, top: y, width, height }; } @@ -101,7 +113,9 @@ export function isReverseFlow(flowDirection: FlowDirection): boolean { return flowDirection === 'row-reverse' || flowDirection === 'column-reverse'; } -// Returns the bounding client rect of an element against another element. +/** + * Returns the bounding client rect of an element relative to its container. + */ export function getRelativeBoundingRect(containerElm: Element, childElm: Element): Rectangle { const containerRect = containerElm.getBoundingClientRect(); const childRect = childElm.getBoundingClientRect(); @@ -114,8 +128,10 @@ export function getRelativeBoundingRect(containerElm: Element, childElm: Element }; } -// Returns the bounding box of an element against another element. -// Considers the box model to return the full dimensions, including padding/border/margin. +/** + * Returns the bounding box of an element against another element. + * Considers the box model to return the full dimensions, including padding/border/margin. + */ export function getRelativeOuterRect(containerElm: Element, childElm: Element): Rectangle { const { x, y, width, height } = getRelativeBoundingRect(containerElm, childElm); const styles = window.getComputedStyle(childElm); @@ -140,6 +156,9 @@ export function getRelativeOuterRect(containerElm: Element, childElm: Element): }; } +/** + * Checks whether a point falls within a reactangle + */ export function rectContainsPoint(rect: Rectangle, x: number, y: number): boolean { return rect.x <= x && rect.x + rect.width >= x && rect.y <= y && rect.y + rect.height >= y; } diff --git a/packages/toolpad-app/src/utils/har.ts b/packages/toolpad-app/src/utils/har.ts index 88142eb0824..17e1759b71b 100644 --- a/packages/toolpad-app/src/utils/har.ts +++ b/packages/toolpad-app/src/utils/har.ts @@ -1,5 +1,8 @@ import { Har } from 'har-format'; +/** + * Initializes an empty HAR object. + */ export function createHarLog(): Har { return { log: { @@ -21,6 +24,9 @@ function oldestFirst(a: WithStartedDateTime, b: WithStartedDateTime): number { return new Date(a.startedDateTime).valueOf() - new Date(b.startedDateTime).valueOf(); } +/** + * Merge two HAR files into a new one. + */ export function mergeHar(target: Har, ...src: Har[]): Har { for (const har of src) { if (har.log.pages) { diff --git a/packages/toolpad-app/src/utils/immutability.ts b/packages/toolpad-app/src/utils/immutability.ts index 6b74d84cba2..0e93f22fc1c 100644 --- a/packages/toolpad-app/src/utils/immutability.ts +++ b/packages/toolpad-app/src/utils/immutability.ts @@ -1,19 +1,7 @@ -type ArrayUpdate = { - [index: number]: T; -}; - -export function updateArray(dest: T[], src: ArrayUpdate): T[] { - let result: T[] | undefined; - Object.entries(src).forEach(([strIdx, value]) => { - const idx = Number(strIdx); - if (!Number.isNaN(idx) && idx < dest.length && idx >= 0 && dest[idx] !== value) { - result = result || [...dest]; - (result as any)[idx] = value; - } - }); - return result || dest; -} - +/** + * Applies changes to an object in an immutable way. The `dest` object will adopt the properties of + * the `src` object. Object identity is preserved if the operation results in a no-op. + */ export function update(dest: T, src: Partial): T { let result: T | undefined; Object.entries(src).forEach(([key, value]) => { @@ -24,15 +12,26 @@ export function update(dest: T, src: Partial): T { }); return result || dest; } - +/** + * Applies changes to an object in an immutable way. The `dest` object will adopt the properties of + * the `src` object. If `dest` is undefined, `src` will be used. Object identity is preserved if + * the operation results in a no-op. + */ export function updateOrCreate(dest: T | null | undefined, src: NonNullable): T { return dest ? update(dest, src) : src; } +/** + * Inserts a value in an immutable array. + */ export function insert(array: readonly T[], value: T, index: number): T[] { return [...array.slice(0, index), value, ...array.slice(index)]; } +/** + * Removes a set of properties from an object in an immutable way. Object identity is preserved if + * the operation results in a no-op. + */ export function omit(obj: T, ...keys: readonly K[]): Omit { let result: T | undefined; @@ -48,6 +47,10 @@ export function omit(obj: T, ...keys: readonly K[]): Omit< return result || obj; } +/** + * Returns an object created from `obj` with only the specified `keys`. Object identity is preserved if + * the operation results in a no-op. + */ export function take>( obj: T, ...keys: readonly K[] @@ -66,7 +69,10 @@ export function take>( return result || obj; } - +/** + * Returns an array without any of its items equal to `value`. Object identity is preserved if + * the operation results in a no-op. + */ export function without(array: readonly T[], value: T): readonly T[] { const result: T[] = []; diff --git a/packages/toolpad-app/src/utils/lazyComponent.tsx b/packages/toolpad-app/src/utils/lazyComponent.tsx index 1a6a02336c5..1cfe87f4295 100644 --- a/packages/toolpad-app/src/utils/lazyComponent.tsx +++ b/packages/toolpad-app/src/utils/lazyComponent.tsx @@ -6,6 +6,9 @@ interface LazyComponentOptions { fallback?: React.ReactNode; } +/** + * Extends React.lazy with automatic fallback support and adds the ability to exclude SSR. + */ export default function lazyComponent>( importComponent: () => Promise<{ default: T }>, { noSsr, fallback }: LazyComponentOptions = {}, diff --git a/packages/toolpad-app/src/utils/prettier.ts b/packages/toolpad-app/src/utils/prettier.ts index 25af85de14c..e30a22201de 100644 --- a/packages/toolpad-app/src/utils/prettier.ts +++ b/packages/toolpad-app/src/utils/prettier.ts @@ -6,10 +6,16 @@ const DEFAULT_OPTIONS = { plugins: [parserBabel], }; +/** + * Formats a javascript source with `prettier`. + */ export function format(code: string): string { return prettier.format(code, DEFAULT_OPTIONS); } +/** + * Formats a javascript expression with `prettier`. + */ export function formatExpression(code: string): string { const formatted = prettier.format(code, { ...DEFAULT_OPTIONS, @@ -21,6 +27,9 @@ export function formatExpression(code: string): string { return formatted.replace(/^;/, ''); } +/** + * Formats a javascript source with `prettier`, if it's valid. + */ export function tryFormat(code: string): string { try { return format(code); @@ -29,6 +38,9 @@ export function tryFormat(code: string): string { } } +/** + * Formats a javascript expression with `prettier`, if it's valid. + */ export function tryFormatExpression(code: string): string { try { return formatExpression(code); diff --git a/packages/toolpad-app/src/utils/prisma.ts b/packages/toolpad-app/src/utils/prisma.ts index 584543a45af..55db01bcf9f 100644 --- a/packages/toolpad-app/src/utils/prisma.ts +++ b/packages/toolpad-app/src/utils/prisma.ts @@ -1,4 +1,7 @@ -// See https://github.com/prisma/prisma/issues/5042#issuecomment-1104679760 +/** + * Excludes a set of keys from a javascript object + * See https://github.com/prisma/prisma/issues/5042#issuecomment-1104679760 + */ export function excludeFields( fields: T, excluded: K, diff --git a/packages/toolpad-app/src/utils/react.tsx b/packages/toolpad-app/src/utils/react.tsx index d3bb2b3930b..bc2283965e7 100644 --- a/packages/toolpad-app/src/utils/react.tsx +++ b/packages/toolpad-app/src/utils/react.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; +/** + * Context that throws when used outside of a provider. + */ export function createProvidedContext( name: string, ): [() => T, React.ComponentType>] { @@ -16,34 +19,9 @@ export function createProvidedContext( return [useContext, context.Provider as React.ComponentType>]; } -export function suspendPromise(promise: Promise): () => T { - let status = 'pending'; - let error: Error; - let response: T; - - const suspender = promise.then( - (res) => { - status = 'success'; - response = res; - }, - (err: Error) => { - status = 'error'; - error = err; - }, - ); - - return () => { - switch (status) { - case 'pending': - throw suspender; - case 'error': - throw error; - default: - return response; - } - }; -} - +/** + * Like `Array.prototype.join`, but for React nodes. + */ export function interleave(items: React.ReactNode[], separator: React.ReactNode): React.ReactNode { const result: React.ReactNode[] = []; diff --git a/packages/toolpad-app/src/utils/readableDuration.ts b/packages/toolpad-app/src/utils/readableDuration.ts index 1554920573d..3f7c96ca72b 100644 --- a/packages/toolpad-app/src/utils/readableDuration.ts +++ b/packages/toolpad-app/src/utils/readableDuration.ts @@ -1,3 +1,6 @@ +/** + * Converts a Date into a human readable duration relative to the current time. + */ const getReadableDuration = (editedAt: Date) => { const duration = new Date().getTime() - editedAt.getTime(); const delta = Math.floor(duration / 1000); diff --git a/packages/toolpad-app/src/utils/router.ts b/packages/toolpad-app/src/utils/router.ts index debbd1098f8..52961a1cc48 100644 --- a/packages/toolpad-app/src/utils/router.ts +++ b/packages/toolpad-app/src/utils/router.ts @@ -18,7 +18,11 @@ type NavigationContextWithBlock = React.ContextType & }; /** - * @source https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874 + * Blocks all navigation attempts. This is useful for preventing the page from + * changing until some condition is met, like saving form data. + * + * In anticipation of this functionality being brought back to react-router + * See https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874 */ export function useBlocker(blocker: Blocker, when = true) { const { navigator } = React.useContext(NavigationContext) as NavigationContextWithBlock; diff --git a/packages/toolpad-app/src/utils/server.ts b/packages/toolpad-app/src/utils/server.ts deleted file mode 100644 index e7e70cf3eab..00000000000 --- a/packages/toolpad-app/src/utils/server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextApiHandler } from 'next'; - -export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'; -export type MethodMap = Partial>; - -export function createEndpoint(handlers: MethodMap): NextApiHandler { - return async (req, res): Promise => { - const handler = handlers[req.method as Method]; - if (handler) { - await handler(req, res); - return; - } - res.status(404).end(); - }; -} diff --git a/packages/toolpad-app/src/utils/streams.ts b/packages/toolpad-app/src/utils/streams.ts index b6e117d4e83..3fd47864351 100644 --- a/packages/toolpad-app/src/utils/streams.ts +++ b/packages/toolpad-app/src/utils/streams.ts @@ -1,6 +1,9 @@ import { Readable } from 'stream'; -export function streamToString(stream: Readable) { +/** + * Reads a readable stream to the end. Returns a promise that resolves with the combined string. + */ +export function streamToString(stream: Readable): Promise { const chunks: Buffer[] = []; return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); diff --git a/packages/toolpad-app/src/utils/strings.ts b/packages/toolpad-app/src/utils/strings.ts index b9dc2a3360f..99c2eb622c0 100644 --- a/packages/toolpad-app/src/utils/strings.ts +++ b/packages/toolpad-app/src/utils/strings.ts @@ -25,6 +25,9 @@ export function camelCase(...parts: string[]): string { return ''; } +/** + * Generates a string for `base` by add a number until it's unique amongst a set of predefined names. + */ export function generateUniqueString(base: string, existingNames: Set) { let i = 1; if (!existingNames.has(base)) { @@ -39,6 +42,9 @@ export function generateUniqueString(base: string, existingNames: Set) { return suggestion; } +/** + * Escape string for use in HTML. + */ export function escapeHtml(unsafe: string): string { return unsafe .replace(/&/g, '&') @@ -48,8 +54,12 @@ export function escapeHtml(unsafe: string): string { .replace(/'/g, '''); } +/** + * Normalizes and removes all diacritics from a javascript string. + * + * See https://stackoverflow.com/a/37511463 + */ export function removeDiacritics(input: string): string { - // See https://stackoverflow.com/a/37511463 return input.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } @@ -61,18 +71,30 @@ export function isAbsoluteUrl(maybeUrl: string) { } } +/** + * Removes a prefix from a string if it starts with it. + */ export function removePrefix(input: string, prefix: string): string { return input.startsWith(prefix) ? input.slice(prefix.length) : input; } +/** + * Removes a suffix from a string if it ends with it. + */ export function removeSuffix(input: string, suffix: string): string { return input.endsWith(suffix) ? input.slice(0, input.length - suffix.length) : input; } +/** + * Adds a prefix to a string if it doesn't start with it. + */ export function ensurePrefix(input: string, prefix: string): string { return input.startsWith(prefix) ? input : prefix + input; } +/** + * Adds a suffix to a string if it doesn't end with it. + */ export function ensureSuffix(input: string, suffix: string): string { return input.endsWith(suffix) ? input : input + suffix; } @@ -99,10 +121,19 @@ export function ensureSuffix(input: string, suffix: string): string { const IMPORT_STATEMENT_REGEX = /^\s*import(?:["'\s]*([\w*{}\n, ]+)from\s*)?["'\s]*([^"']+)["'\s].*/gm; +/** + * Statically analyses a javascript source code for import statements and return the specifiers. + * + * NOTE: This function does a best effort without parsing the code. The result may contain false + * positives + */ export function findImports(src: string): string[] { return Array.from(src.matchAll(IMPORT_STATEMENT_REGEX), (match) => match[2]); } +/** + * Limits the length of a string and adds ellipsis if necessary. + */ export function truncate(str: string, maxLength: number, dots: string = '...') { if (str.length <= maxLength) { return str; diff --git a/packages/toolpad-app/src/utils/tests.ts b/packages/toolpad-app/src/utils/tests.ts index 25344fd0e02..df4d7e9ceae 100644 --- a/packages/toolpad-app/src/utils/tests.ts +++ b/packages/toolpad-app/src/utils/tests.ts @@ -1,9 +1,14 @@ import * as http from 'http'; import getPort from 'get-port'; +/** + * A convenience wrapper around node.js createServer + listen for testing purposes. + * - starts a http server using `handler` on a free port + * - returns the port and stopServer method for cleanup. + */ export async function startServer(handler?: http.RequestListener) { const server = http.createServer(handler); - const port = await getPort({ port: 3000 }); + const port = await getPort(); let app: http.Server | undefined; await new Promise((resolve, reject) => { app = server.listen(port); diff --git a/packages/toolpad-app/src/utils/types.ts b/packages/toolpad-app/src/utils/types.ts index e899aebef5d..83754c7a60f 100644 --- a/packages/toolpad-app/src/utils/types.ts +++ b/packages/toolpad-app/src/utils/types.ts @@ -6,6 +6,9 @@ export type WithControlledProp< export type ExactEntriesOf

= Exclude<{ [K in keyof P]: [K, P[K]] }[keyof P], undefined>[]; +/** + * The inverse of Awaited. + */ export type Awaitable = T | Promise | PromiseLike; /** @@ -64,6 +67,7 @@ export type CapitalizeTail = T extends [] : never; /** + * sString template type that converts snake-case to camel-case * @example * type T0 = SnakeToCamel<'foo-bar-baz'>; // 'fooBarBaz' * type T1 = CapitalizeAll<'foo'>; // 'foo' @@ -71,6 +75,9 @@ export type CapitalizeTail = T extends [] */ export type SnakeToCamel = Join>, ''>; +/** + * The inverso of NonNullable + */ export type Maybe = T | undefined | null; export interface MetaField { diff --git a/packages/toolpad-app/src/utils/useBoolean.ts b/packages/toolpad-app/src/utils/useBoolean.ts index f6f3c7512b8..8f02e9b2f8f 100644 --- a/packages/toolpad-app/src/utils/useBoolean.ts +++ b/packages/toolpad-app/src/utils/useBoolean.ts @@ -1,5 +1,8 @@ import * as React from 'react'; +/** + * A utility with shortcuts to manipulate boolean values. + */ export default function useBoolean(initialValue: boolean) { const [value, setValue] = React.useState(initialValue); const toggle = React.useCallback(() => setValue((existing) => !existing), []); diff --git a/packages/toolpad-app/src/utils/useDebounced.ts b/packages/toolpad-app/src/utils/useDebounced.ts index 7723c769bd2..5ecfd4cd1c1 100644 --- a/packages/toolpad-app/src/utils/useDebounced.ts +++ b/packages/toolpad-app/src/utils/useDebounced.ts @@ -1,5 +1,12 @@ import * as React from 'react'; +/** + * This hook allows you to debounce any fast changing value. The debounced value will only + * reflect the latest value when the useDebounce hook has not been called for the specified + * time period. + * + * Inspired by https://usehooks.com/useDebounce/ + */ export default function useDebounced(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = React.useState(() => value); const timeoutIdRef = React.useRef(null); diff --git a/packages/toolpad-app/src/utils/useDebouncedHandler.ts b/packages/toolpad-app/src/utils/useDebouncedHandler.ts index aa1bf7d6807..08ff850b893 100644 --- a/packages/toolpad-app/src/utils/useDebouncedHandler.ts +++ b/packages/toolpad-app/src/utils/useDebouncedHandler.ts @@ -30,6 +30,9 @@ function defer

(fn: React.MutableRefObject>, params: /** * Creates a debounced version of the handler that is passed. The invocation of [fn] is * delayed for [delay] milliseconds from the last invocation of the debounced function. + * + * This implementation adds on the lodash implementation in that it handles updates to the + * delay value. */ export default function useDebouncedHandler

( fn: Handler

, diff --git a/packages/toolpad-app/src/utils/useDialog.tsx b/packages/toolpad-app/src/utils/useDialog.tsx deleted file mode 100644 index 65ecc5d03e9..00000000000 --- a/packages/toolpad-app/src/utils/useDialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; - -interface DialogProps { - data?: D; - open: boolean; - onClose: (result?: R) => void; -} - -interface UseDialog { - element: React.ReactNode; - show: (props: D) => Promise; -} - -interface DialogState { - data: D; - resolve: (result?: R) => void; -} - -export default function useDialog( - Component: React.ComponentType>, -): UseDialog { - const [state, setState] = React.useState | null>(null); - - const show = React.useCallback(async (data: D) => { - return new Promise((resolve) => { - setState({ - data, - resolve, - }); - }); - }, []); - - const handleClose = React.useCallback( - (result?: R) => { - if (!state) { - return; - } - const { resolve } = state; - resolve(result); - setState(null); - }, - [state], - ); - - return { show, element: }; -} diff --git a/packages/toolpad-app/src/utils/useEvent.ts b/packages/toolpad-app/src/utils/useEvent.ts index d09db7b4138..9a46a4489ac 100644 --- a/packages/toolpad-app/src/utils/useEvent.ts +++ b/packages/toolpad-app/src/utils/useEvent.ts @@ -20,6 +20,8 @@ function isInRender() { } /** + * A Hook to define an event handler with an always-stable function identity. + * In anticipation of a react native solution * See https://github.com/reactjs/rfcs/pull/220 */ export default function useEvent void>(handler: F): F { diff --git a/packages/toolpad-app/src/utils/useLatest.ts b/packages/toolpad-app/src/utils/useLatest.ts index 88b1afe5d94..e75ab3be40b 100644 --- a/packages/toolpad-app/src/utils/useLatest.ts +++ b/packages/toolpad-app/src/utils/useLatest.ts @@ -1,5 +1,8 @@ import * as React from 'react'; +/** + * Returns the latest non-null, non-undefiend value that has been passed to it. + */ function useLatest(value: T): T; function useLatest(value: T | null | undefined): T | null | undefined; function useLatest(value: T | null | undefined): T | null | undefined { diff --git a/packages/toolpad-app/src/utils/useLocalStorageState.ts b/packages/toolpad-app/src/utils/useLocalStorageState.ts index 7f04569ca91..b8be21ccb94 100644 --- a/packages/toolpad-app/src/utils/useLocalStorageState.ts +++ b/packages/toolpad-app/src/utils/useLocalStorageState.ts @@ -59,6 +59,12 @@ function setValue(key: string, value: T) { * * Since the local storage API isn't available in server-rendering environments, we * return initialValue during SSR and hydration. + * + * Things this hook does different from existing solutions: + * - SSR-capable: it shows initial value during SSR and hydration, but immediately + * initiallizes when clientside mounted. + * - Sync state across tabs: When another tab changes the value in local storage, the + * current tab follows suit. */ export default function useLocalStorageState( key: string, diff --git a/packages/toolpad-app/src/utils/useMenu.ts b/packages/toolpad-app/src/utils/useMenu.ts index 538296c40a7..70f7e61b26e 100644 --- a/packages/toolpad-app/src/utils/useMenu.ts +++ b/packages/toolpad-app/src/utils/useMenu.ts @@ -1,6 +1,9 @@ import { ButtonProps, MenuProps } from '@mui/material'; import * as React from 'react'; +/** + * Abstracts MUI menus opening logic and some of the a11y. + */ export default function useMenu() { const buttonId = React.useId(); const menuId = React.useId(); diff --git a/packages/toolpad-app/src/utils/usePageTitle.ts b/packages/toolpad-app/src/utils/usePageTitle.ts index 9c36069193a..e0dfd74c738 100644 --- a/packages/toolpad-app/src/utils/usePageTitle.ts +++ b/packages/toolpad-app/src/utils/usePageTitle.ts @@ -1,5 +1,8 @@ import * as React from 'react'; +/** + * Sets the current document title. + */ export default function usePageTitle(title: string) { React.useEffect(() => { const original = document.title; diff --git a/packages/toolpad-app/src/utils/useRisingEdge.ts b/packages/toolpad-app/src/utils/useRisingEdge.ts index 8f82c107c57..f91331c0628 100644 --- a/packages/toolpad-app/src/utils/useRisingEdge.ts +++ b/packages/toolpad-app/src/utils/useRisingEdge.ts @@ -1,6 +1,10 @@ import * as React from 'react'; import useEvent from './useEvent'; +/** + * Detects rising edges of boolean values. `handler` will be called any time `value` changes + * from `false` to `true`. + */ export default function useRisingEdge(value: boolean, handler: () => void) { const prevValue = React.useRef(value); const stableHandler = useEvent(handler); diff --git a/packages/toolpad-app/src/utils/useThottled.ts b/packages/toolpad-app/src/utils/useThottled.ts index aa1bfe1dbe7..adcbc5936f0 100644 --- a/packages/toolpad-app/src/utils/useThottled.ts +++ b/packages/toolpad-app/src/utils/useThottled.ts @@ -1,5 +1,9 @@ import * as React from 'react'; - +/** + * This hook allows you to throttle any fast changing value. The throttle value will only + * update once in the defined time period. Multiple changes to the value within this time + * period are throttled until the delay has passed. + */ export default function useThrottled(value: T, throttle: number): T { const [throttledValue, setThrottledValue] = React.useState(value); const lastUpdate = React.useRef(-Infinity); diff --git a/packages/toolpad-app/src/utils/uuid.ts b/packages/toolpad-app/src/utils/uuid.ts index eb4fc9e7fd7..befbb5e748e 100644 --- a/packages/toolpad-app/src/utils/uuid.ts +++ b/packages/toolpad-app/src/utils/uuid.ts @@ -1,6 +1,9 @@ import crypto from './crypto'; -// credit: https://stackoverflow.com/a/2117523 +/** + * Isomorphic UUID generator, based on web crypto + * credit: https://stackoverflow.com/a/2117523 + */ export function uuidv4() { // @ts-ignore return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>