mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
import { Args, ArgTypes } from '@storybook/addons';
|
|
import { once } from '@storybook/client-logger';
|
|
import isPlainObject from 'lodash/isPlainObject';
|
|
import dedent from 'ts-dedent';
|
|
|
|
type ValueType = { name: string; value?: ObjectValueType | ValueType };
|
|
type ObjectValueType = Record<string, ValueType>;
|
|
|
|
const INCOMPATIBLE = Symbol('incompatible');
|
|
const map = (arg: unknown, type: ValueType): any => {
|
|
if (arg === undefined || arg === null || !type) return arg;
|
|
switch (type.name) {
|
|
case 'string':
|
|
return String(arg);
|
|
case 'enum':
|
|
return arg;
|
|
case 'number':
|
|
return Number(arg);
|
|
case 'boolean':
|
|
return arg === 'true';
|
|
case 'array':
|
|
if (!type.value || !Array.isArray(arg)) return INCOMPATIBLE;
|
|
return arg.reduce((acc, item) => {
|
|
const mapped = map(item, type.value as ValueType);
|
|
return mapped === INCOMPATIBLE ? acc : acc.concat([mapped]);
|
|
}, []);
|
|
case 'object':
|
|
if (!type.value || typeof arg !== 'object') return INCOMPATIBLE;
|
|
return Object.entries(arg).reduce((acc, [key, val]) => {
|
|
const mapped = map(val, (type.value as ObjectValueType)[key]);
|
|
return mapped === INCOMPATIBLE ? acc : Object.assign(acc, { [key]: mapped });
|
|
}, {} as Args);
|
|
default:
|
|
return INCOMPATIBLE;
|
|
}
|
|
};
|
|
|
|
export const mapArgsToTypes = (args: Args, argTypes: ArgTypes): Args => {
|
|
return Object.entries(args).reduce((acc, [key, value]) => {
|
|
if (!argTypes[key]) return acc;
|
|
const mapped = map(value, argTypes[key].type);
|
|
return mapped === INCOMPATIBLE ? acc : Object.assign(acc, { [key]: mapped });
|
|
}, {});
|
|
};
|
|
|
|
export const combineArgs = (value: any, update: any): Args => {
|
|
if (!isPlainObject(value) || !isPlainObject(update)) return update;
|
|
return Object.keys({ ...value, ...update }).reduce((acc, key) => {
|
|
if (key in update) {
|
|
const combined = combineArgs(value[key], update[key]);
|
|
if (combined !== undefined) acc[key] = combined;
|
|
} else {
|
|
acc[key] = value[key];
|
|
}
|
|
return acc;
|
|
}, {} as any);
|
|
};
|
|
|
|
export const validateOptions = (args: Args, argTypes: ArgTypes): Args => {
|
|
return Object.entries(argTypes).reduce((acc, [key, { options }]) => {
|
|
if (!options) {
|
|
acc[key] = args[key];
|
|
return acc;
|
|
}
|
|
|
|
if (!Array.isArray(options)) {
|
|
once.error(dedent`
|
|
Invalid argType: '${key}.options' should be an array.
|
|
|
|
More info: https://storybook.js.org/docs/react/api/argtypes
|
|
`);
|
|
acc[key] = args[key];
|
|
return acc;
|
|
}
|
|
|
|
if (options.some((opt) => opt && ['object', 'function'].includes(typeof opt))) {
|
|
once.error(dedent`
|
|
Invalid argType: '${key}.options' should only contain primitives. Use a 'mapping' for complex values.
|
|
|
|
More info: https://storybook.js.org/docs/react/writing-stories/args#mapping-to-complex-arg-values
|
|
`);
|
|
acc[key] = args[key];
|
|
return acc;
|
|
}
|
|
|
|
const isArray = Array.isArray(args[key]);
|
|
const invalidIndex = isArray && args[key].findIndex((val: any) => !options.includes(val));
|
|
const isValidArray = isArray && invalidIndex === -1;
|
|
|
|
if (args[key] === undefined || options.includes(args[key]) || isValidArray) {
|
|
acc[key] = args[key];
|
|
return acc;
|
|
}
|
|
|
|
const field = isArray ? `${key}[${invalidIndex}]` : key;
|
|
const supportedOptions = options
|
|
.map((opt: any) => (typeof opt === 'string' ? `'${opt}'` : String(opt)))
|
|
.join(', ');
|
|
once.warn(`Received illegal value for '${field}'. Supported options: ${supportedOptions}`);
|
|
|
|
return acc;
|
|
}, {} as Args);
|
|
};
|