stackpress
Version:
Incept is a content management framework.
556 lines (555 loc) • 17.8 kB
JavaScript
import { VariableDeclarationKind } from 'ts-morph';
export default function generate(directory, registry) {
for (const model of registry.model.values()) {
model.columns.forEach(column => column.field.method === 'relation'
? generateRelation(directory, model, column)
: column.field.method === 'fieldset'
? generateFieldset(directory, model, column)
: column.field.component
&& ['Checkbox', 'Switch'].indexOf(column.field.component) !== -1
? generateBoolean(directory, model, column)
: generateField(directory, model, column));
}
for (const fieldset of registry.fieldset.values()) {
fieldset.columns.forEach(column => column.field.method === 'fieldset'
? generateFieldset(directory, fieldset, column)
: column.field.component
&& ['Checkbox', 'Switch'].indexOf(column.field.component) !== -1
? generateBoolean(directory, fieldset, column)
: generateField(directory, fieldset, column));
}
}
export function generateRelation(directory, model, column) {
if (typeof column.field.component !== 'string')
return;
const path = `${model.name}/components/fields/${column.title}Field.tsx`;
const source = directory.createSourceFile(path, '', { overwrite: true });
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/view/client',
namedImports: ['FieldProps', 'ControlProps']
});
source.addImportDeclaration({
moduleSpecifier: 'mustache',
defaultImport: 'mustache'
});
source.addImportDeclaration({
moduleSpecifier: 'r22n',
namedImports: ['useLanguage']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/form/Control',
defaultImport: 'Control'
});
source.addImportDeclaration({
moduleSpecifier: `frui/field/${column.field.component}`,
defaultImport: column.field.component
});
source.addFunction({
isExported: true,
name: `${column.title}Field`,
parameters: [
{ name: 'props', type: 'FieldProps' }
],
statements: (`
//props
const {
className,
name = '${column.name}${column.multiple ? '[]' : ''}',
value,
change,
error = false
} = props;
//render
return (
<${column.field.component}
name={name}
className={className}
error={error}
defaultValue={value}
searchable={true}
onQuery={async (query, update) => {
const response = await fetch(\`${column.field.attributes.search?.includes('?')
? column.field.attributes.search + '&q=${query}'
: column.field.attributes.search + '?q=${query}'}\`);
const json = await response.json();
const options = json.results.map(row => ({
label: mustache.render('${column.field.attributes.template}', row),
value: row.${column.field.attributes.id}
}));
update(options);
}}
/>
);
`)
});
source.addFunction({
isExported: true,
name: `${column.title}FieldControl`,
parameters: [
{ name: 'props', type: 'ControlProps' }
],
statements: (`
//props
const { className, name, value, change, error } = props;
//hooks
const { _ } = useLanguage();
//determine label
const label = ${column.required && !column.multiple
? `\`\${_('${column.label}')}*\`;`
: `_('${column.label}');`}
//render
return (
<Control label={label} error={error} className={className}>
<${column.title}Field
error={!!error}
name={name}
value={value}
change={change}
/>
</Control>
);
`)
});
}
export function generateFieldset(directory, model, column) {
if (!column.fieldset)
return;
const path = `${model.name}/components/fields/${column.title}Field.tsx`;
const source = directory.createSourceFile(path, '', { overwrite: true });
const fieldset = column.fieldset;
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'react',
namedImports: ['ReactNode', 'CSSProperties']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'frui/element/Fieldset',
namedImports: ['FieldsProps', 'FieldsetProps']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: `../../../${fieldset.name}/types.js`,
namedImports: [`${column.title}Input`]
});
source.addImportDeclaration({
moduleSpecifier: 'r22n',
namedImports: ['useLanguage']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/form/Fieldset',
defaultImport: 'make'
});
fieldset.fields.forEach(column => {
if (typeof column.field.component !== 'string')
return;
if (column.field.method === 'fieldset') {
source.addImportDeclaration({
moduleSpecifier: `../../../${fieldset.name}/components/fields/${column.title}Field.js`,
namedImports: [`${column.title}FieldsetControl`]
});
return;
}
source.addImportDeclaration({
moduleSpecifier: `../../../${fieldset.name}/components/fields/${column.title}Field.js`,
namedImports: [`${column.title}FieldControl`]
});
});
source.addTypeAlias({
isExported: true,
name: `${column.title}Config`,
type: `{
type?: string,
values?: (${column.title}Input|undefined)[],
index: number,
set: (values: (${column.title}Input|undefined)[]) => void
}`
});
source.addTypeAlias({
isExported: true,
name: `${column.title}FieldsetProps`,
type: `FieldsetProps<${column.title}Input> & { errors?: Record<string, any>[] }`
});
source.addTypeAlias({
isExported: true,
name: `${column.title}ControlProps`,
type: `{
label?: string,
error?: string,
value?: ${column.title}Input|${column.title}Input[],
errors?: Record<string, any>|Record<string, any>[],
style?: CSSProperties,
className?: string
}`
});
source.addFunction({
isExported: true,
name: `use${column.title}Fieldset`,
parameters: [
{ name: 'config', type: `${column.title}Config` }
],
statements: (`
//props
const { values, index, set } = config;
//handlers
const handlers = {
update: (value: string) => {
const newValues = [ ...(values || []) ]
newValues[index] = value;
set(newValues);
},
remove: () => {
const newValues = [ ...(values || []) ];
newValues[index] = undefined;
set(newValues);
}
};
return { handlers };
`)
});
source.addFunction({
isExported: true,
name: `${column.title}Fields`,
parameters: [
{ name: 'props', type: `FieldsProps<${column.title}Input>` }
],
statements: (`
const {
name,
config,
values,
index,
set,
limit
} = props;
const { _ } = useLanguage();
const { errors = [] } = config;
//handlers
const { handlers } = use${column.title}Fieldset({ values, index, set });
//variables
const prefix = !limit && name
? \`\${name}[\${index}]\`
: limit && name
? \`\${name}\`
: undefined;
const value = values ? values[index]: undefined;
const error = errors[index] || {};
const classNames = [
errors[index] && 'field-fieldset-error',
!limit && 'field-fieldset-multiple',
(limit || !config.required) && 'field-fieldset-optional'
].filter(Boolean).join(' ');
return (
<div className={\`field-fieldset \${classNames}\`}>
{!limit ? (
<header>
<h6>
{_('${fieldset.singular} %s', index + 1)}
</h6>
<a onClick={handlers.remove}>
×
</a>
</header>
) : !config.required ? (
<header>
<a onClick={handlers.remove}>
×
</a>
</header>
) : null}
<main>
${fieldset.fields.map(column => {
return column.field.method === 'fieldset'
? (`
<${column.title}FieldsetControl
className="field-fieldset-control"
name={prefix ? \`\${prefix}[${column.name}]\` : undefined}
errors={error['${column.name}']}
value={value['${column.name}']}
/>
`)
: column.field.component
? (`
<${column.title}FieldControl
className="field-fieldset-control"
name={prefix ? \`\${prefix}[${column.name}]\` : undefined}
error={error['${column.name}']}
value={value['${column.name}']}
/>
`)
: false;
}).filter(Boolean).join('\n')}
</main>
</div>
);
`)
});
source.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
declarations: [{
name: 'Fieldset',
initializer: `make<${column.title}Input>(${column.title}Fields)`,
}]
});
source.addFunction({
isExported: true,
name: `${column.title}Fieldset`,
parameters: [
{ name: 'props', type: `${column.title}FieldsetProps` }
],
statements: (`
//config gets passed straight to the fields
const config = {
required: ${column.required ? 'true' : 'false'},
errors: props.errors || []
};
const add = 'Add ${fieldset.singular}';
const limit = ${column.multiple ? '0' : '1'};
const defaults = ${JSON.stringify(fieldset.defaults)};
const value = ${column.multiple
? 'props.value || []'
: column.required
? 'props.value || [ defaults ]'
: 'props.value || []'}
//render
return (
<Fieldset
{...props}
add={add}
limit={limit}
config={config}
defaultValue={value}
emptyValue={defaults}
/>
);
`)
});
source.addFunction({
isExported: true,
name: `${column.title}FieldsetControl`,
parameters: [
{ name: 'props', type: `${column.title}ControlProps` }
],
statements: (`
//hooks
const { _ } = useLanguage();
//props
let {
label = ${column.required && !column.multiple
? `\`\${_('${column.label}')}*\``
: `_('${column.label}')`},
error = 'Invalid ${column.label}',
className,
name,
value,
errors = [],
...attributes
} = props;
//format value
value = Array.isArray(value)
? value
: value && typeof value === 'object'
? [ value ]
: undefined;
//format errors
errors = Array.isArray(errors)
? errors
: errors && typeof errors === 'object'
? [ errors ]
: [];
//determine classnames
const classNames = ['frui-control'];
if (className) {
classNames.push(className);
}
//render
return (
<div className={classNames.join(' ')} {...attributes}>
{!!label && (
<label className="frui-control-label">{label}</label>
)}
<div className="frui-control-field">
<${column.title}Fieldset
error={errors.length > 0}
errors={errors}
name={name}
value={value}
/>
</div>
{errors.length > 0 && (
<div className="frui-control-error">{error || ''}</div>
)}
</div>
);
`)
});
}
export function generateBoolean(directory, fieldset, column) {
if (typeof column.field.component !== 'string')
return;
const path = `${fieldset.name}/components/fields/${column.title}Field.tsx`;
const source = directory.createSourceFile(path, '', { overwrite: true });
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/view/client',
namedImports: ['FieldProps', 'ControlProps']
});
source.addImportDeclaration({
moduleSpecifier: 'r22n',
namedImports: ['useLanguage']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/form/Control',
defaultImport: 'Control'
});
source.addImportDeclaration({
moduleSpecifier: `frui/field/${column.field.component}`,
defaultImport: column.field.component
});
source.addFunction({
isExported: true,
name: `${column.title}Field`,
parameters: [
{ name: 'props', type: 'FieldProps' }
],
statements: (`
//props
const {
className,
name = '${column.name}${column.multiple ? '[]' : ''}',
value,
change,
error = false
} = props;
const attributes = ${JSON.stringify(column.field.attributes)};
//render
return (
<${column.field.component}
{...attributes}
name={name}
className={className}
error={error}
defaultValue="1"
defaultChecked={!!value}
onUpdate={value => change && change('${column.name}${column.multiple ? '[]' : ''}', value)}
/>
);
`)
});
source.addFunction({
isExported: true,
name: `${column.title}FieldControl`,
parameters: [
{ name: 'props', type: 'ControlProps' }
],
statements: (`
//props
const {
className,
name = '${column.name}${column.multiple ? '[]' : ''}',
value${column.default ? ' = ' + JSON.stringify(column.default) : ''},
change,
error
} = props;
//hooks
const { _ } = useLanguage();
//determine label
const label = ${column.required && !column.multiple
? `\`\${_('${column.label}')}*\`;`
: `_('${column.label}');`}
//render
return (
<Control label={label} error={error} className={className}>
<input type="hidden" name="${column.name}" value="0" />
<${column.title}Field
error={!!error}
name={name}
value={value}
change={change}
/>
</Control>
);
`)
});
}
export function generateField(directory, fieldset, column) {
if (typeof column.field.component !== 'string')
return;
const path = `${fieldset.name}/components/fields/${column.title}Field.tsx`;
const source = directory.createSourceFile(path, '', { overwrite: true });
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/view/client',
namedImports: ['FieldProps', 'ControlProps']
});
source.addImportDeclaration({
moduleSpecifier: 'r22n',
namedImports: ['useLanguage']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/form/Control',
defaultImport: 'Control'
});
source.addImportDeclaration({
moduleSpecifier: `frui/field/${column.field.component}`,
defaultImport: column.field.component
});
source.addFunction({
isExported: true,
name: `${column.title}Field`,
parameters: [
{ name: 'props', type: 'FieldProps' }
],
statements: (`
//props
const {
className,
name = '${column.name}${column.multiple ? '[]' : ''}',
value${column.default ? ' = ' + JSON.stringify(column.default) : ''},
change,
error = false
} = props;
const attributes = ${JSON.stringify(column.field.attributes)};
//render
return (
<${column.field.component}
{...attributes}
name={name}
className={className}
error={error}
defaultValue={value}
onUpdate={value => change && change('${column.name}${column.multiple ? '[]' : ''}', value)}
/>
);
`)
});
source.addFunction({
isExported: true,
name: `${column.title}FieldControl`,
parameters: [
{ name: 'props', type: 'ControlProps' }
],
statements: (`
//props
const { className, name, value, change, error } = props;
//hooks
const { _ } = useLanguage();
//determine label
const label = ${column.required && !column.multiple
? `\`\${_('${column.label}')}*\`;`
: `_('${column.label}');`}
//render
return (
<Control label={label} error={error} className={className}>
<${column.title}Field
error={typeof error === 'string'}
name={name}
value={value}
change={change}
/>
</Control>
);
`)
});
}