stackpress
Version:
Incept is a content management framework.
434 lines (432 loc) • 15.8 kB
JavaScript
import { VariableDeclarationKind } from 'ts-morph';
export default function searchPage(directory, _registry, model) {
const file = `${model.name}/admin/views/search.tsx`;
const source = directory.createSourceFile(file, '', { overwrite: true });
const ids = model.ids.map(column => column.name);
const path = ids.map(name => `\${row.${name}}`).join('/');
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'react',
namedImports: ['ChangeEvent', 'MouseEventHandler', 'SetStateAction']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/sql',
namedImports: ['SearchParams']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/view/client',
namedImports: ['ServerPageProps', 'SessionPermission']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: 'stackpress/admin/types',
namedImports: ['AdminConfigProps']
});
source.addImportDeclaration({
isTypeOnly: true,
moduleSpecifier: '../../types',
namedImports: [`${model.title}Extended`]
});
source.addImportDeclaration({
moduleSpecifier: 'react',
namedImports: ['useState']
});
source.addImportDeclaration({
moduleSpecifier: 'r22n',
namedImports: ['useLanguage']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/element/Table',
namedImports: ['Table', 'Thead', 'Trow', 'Tcol']
});
source.addImportDeclaration({
moduleSpecifier: 'frui/element/Alert',
defaultImport: 'Alert'
});
source.addImportDeclaration({
moduleSpecifier: 'frui/form/Button',
defaultImport: 'Button'
});
source.addImportDeclaration({
moduleSpecifier: 'frui/field/Input',
defaultImport: 'Input'
});
source.addImportDeclaration({
moduleSpecifier: 'stackpress/view/client',
namedImports: [
'paginate', 'filter', 'order', 'notify', 'flash',
'useServer', 'useStripe', 'Crumbs', 'Pagination', 'LayoutAdmin'
]
});
source.addImportDeclaration({
moduleSpecifier: 'stackpress/view/import',
namedImports: ['batchAndSend']
});
model.lists.forEach(column => {
if (typeof column.list.component !== 'string')
return;
source.addImportDeclaration({
moduleSpecifier: `../../components/lists/${column.title}ListFormat`,
defaultImport: `${column.title}ListFormat`
});
});
model.filters.forEach(column => {
if (typeof column.filter.component !== 'string')
return;
source.addImportDeclaration({
moduleSpecifier: `../../components/filters/${column.title}Filter`,
namedImports: [`${column.title}FilterControl`]
});
});
model.spans.forEach(column => {
if (typeof column.span.component !== 'string')
return;
source.addImportDeclaration({
moduleSpecifier: `../../components/spans/${column.title}Span`,
namedImports: [`${column.title}SpanControl`]
});
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchCrumbs`,
statements: (`
//hooks
const { _ } = useLanguage();
//variables
const crumbs = [{
label: _('${model.plural}'),
icon: '${model.icon}'
}];
return (<Crumbs crumbs={crumbs} />);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchFilters`,
parameters: [{
name: 'props',
type: `{
query: SearchParams,
close: MouseEventHandler<HTMLElement>
}`
}],
statements: (`
//props
const { query, close } = props;
//hooks
const { _ } = useLanguage();
return (
<aside className="theme-bg-bg2 theme-bc-bd1 flex flex-col px-w-100-0 px-h-100-0 border-r">
<header className="theme-bg-bg3 px-px-10 px-py-14 uppercase">
<i className="fas fa-chevron-right px-mr-10 cursor-pointer" onClick={close}></i>
{_('Filters')}
</header>
<form className="flex-grow overflow-auto px-p-10">
${Array.from(model.columns.values()).map(column => {
if (column.filter.component) {
return (`
<${column.title}FilterControl
className="px-mb-20"
value={query.filter?.${column.name}}
/>
`);
}
else if (column.span.component) {
return (`
<${column.title}SpanControl
className="px-mb-20"
value={query.span?.${column.name}}
/>
`);
}
return '';
}).join('\n')}
<Button
className="theme-bc-primary theme-bg-primary border !px-px-14 !px-py-8"
type="submit"
>
<i className="text-sm fas fa-fw fa-filter"></i>
{_('Filter')}
</Button>
</form>
</aside>
);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchForm`,
parameters: [{
name: 'props',
type: (`{
base: string,
token: string,
open: (value: SetStateAction<boolean>) => void,
can: (...permits: SessionPermission[]) => boolean
}`)
}],
statements: (`
const { base, token, open, can } = props;
const upload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
//get the input
const input = e.currentTarget;
//get the first file
const file = input.files?.[0];
//skip if we can't find the file
if (!file) return;
//proceed to send
batchAndSend('import', token, file, notify).then(() => {
flash('success', 'File imported successfully');
window.location.reload();
});
return false;
};
return (
<div className="flex items-center">
<Button
className="border theme-bc-bd2 theme-bg-bg2 !px-px-14 !px-py-8"
type="button"
onClick={() => open((opened: boolean) => !opened)}
>
<i className="text-sm fas fa-fw fa-filter"></i>
</Button>
<div className="flex-grow">
${model.searchables.length > 0 ? (`
<form className="flex items-center">
<Input className="!theme-bc-bd2" />
<Button className="theme-bc-bd2 theme-bg-bg2 border-r border-l-0 border-y !px-px-14 !px-py-8" type="submit">
<i className="text-sm fas fa-fw fa-search"></i>
</Button>
</form>
`) : ''}
</div>
{can({ method: 'GET', route: \`\${base}/${model.dash}/export\` }) ?(
<Button info className="px-px-16 px-py-9" href="export">
<i className="fas fa-download"></i>
</Button>
): null}
{can({ method: 'GET', route: \`\${base}/${model.dash}/import\` }) ?(
<Button warning type="button" className="relative !px-px-16 !px-py-9">
<i className="cursor-pointer fas fa-upload"></i>
<input className="cursor-pointer opacity-0 absolute px-b-0 px-l-0 px-r-0 px-t-0" type="file" onChange={upload} />
</Button>
): null}
{can({ method: 'GET', route: \`\${base}/${model.dash}/create\` }) ? (
<Button success className="px-px-16 px-py-9" href="create">
<i className="fas fa-plus"></i>
</Button>
): null}
</div>
);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchResults`,
parameters: [{
name: 'props',
type: (`{
base: string,
query: Partial<SearchParams>,
results: ProfileExtended[],
can: (...permits: SessionPermission[]) => boolean
}`)
}],
statements: (`
const { can, base, query, results } = props;
const { sort = {} } = query;
const { _ } = useLanguage();
const stripe = useStripe('theme-bg-bg0', 'theme-bg-bg1');
return (
<Table>
${model.lists.filter(column => column.list.method !== 'hide').map(column => column.sortable ? (`
<Thead noWrap stickyTop className="theme-info theme-bg-bg2 !theme-bc-bd2 text-right">
<span
className="cursor-pointer"
onClick={() => order('sort[${column.name}]')}
>
{_('${column.label}')}
</span>
{!sort.${column.name} ? (
<i className="px-ml-2 text-xs fas fa-sort"></i>
) : null}
{sort.${column.name} === 'asc' ? (
<i className="px-ml-2 text-xs fas fa-sort-up"></i>
) : null}
{sort.${column.name} === 'desc' ? (
<i className="px-ml-2 text-xs fas fa-sort-down"></i>
) : null}
</Thead>
`) : (`
<Thead noWrap stickyTop className="!theme-bc-bd2 theme-bg-bg2 text-left">
{_('${column.label}')}
</Thead>
`)).join('\n')}
<Thead stickyTop stickyRight className="!theme-bc-bd2 theme-bg-bg2" />
{results.map((row, index) => (
<Trow key={index}>
${model.lists.filter(column => column.list.method !== 'hide').map(column => {
const value = column.required && column.list.method === 'none'
? `{row.${column.name}.toString()}`
: column.required && column.list.method !== 'none'
? `<${column.title}ListFormat data={row} value={row.${column.name}} />`
: !column.required && column.list.method === 'none'
? `{row.${column.name} ? row.${column.name}.toString() : ''}`
: `{row.${column.name} ? (<${column.title}ListFormat data={row} value={row.${column.name}} />) : ''}`;
const align = column.sortable ? 'text-right' : 'text-left';
return column.filter.method !== 'none' ? (`
<Tcol noWrap className={\`!theme-bc-bd2 ${align} \${stripe(index)}\`}>
<span
className="cursor-pointer theme-info"
onClick={() => filter('filter[${column.name}]', row.${column.name})}
>
${value}
</span>
</Tcol>
`) : (`
<Tcol noWrap className={\`!theme-bc-bd2 ${align} \${stripe(index)}\`}>
${value}
</Tcol>
`);
}).join('\n')}
<Tcol stickyRight className={\`!theme-bc-bd2 text-center \${stripe(index)}\`}>
{can({ method: 'GET', route: \`\${base}/${model.dash}/detail/${path}\`}) ? (
<Button info className="px-p-2" href={\`detail/\${row.id}\`}>
<i className="fas fa-fw fa-caret-right"></i>
</Button>
) : null}
</Tcol>
</Trow>
))}
</Table>
);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchBody`,
statements: (`
//props
const { config, session, request, response } = useServer<${[
'AdminConfigProps',
'Partial<SearchParams>',
`${model.title}Extended[]`
].join(', ')}>();
const base = config.path('admin.base', '/admin');
const can = session.can.bind(session);
const query = request.data();
const skip = query.skip || 0;
const take = query.take || 50;
const results = response.results as ${model.title}Extended[];
const total = response.total || 0;
//hooks
const { _ } = useLanguage();
const [ opened, open ] = useState(false);
//handlers
const page = (skip: number) => paginate('skip', skip);
//render
return (
<main className="flex flex-col px-h-100-0 theme-bg-bg0 relative">
<div className="px-px-10 px-py-14 theme-bg-bg2">
<Admin${model.title}SearchCrumbs />
</div>
<div className={\`absolute px-t-0 px-b-0 px-w-360 px-z-10 duration-200 \${opened? 'px-r-0': 'px-r--360' }\`}>
<Admin${model.title}SearchFilters query={query} close={() => open(false)} />
</div>
<div className="px-p-10">
<Admin${model.title}SearchForm
base={base}
token={session.data.token}
open={open}
can={can}
/>
</div>
{!!results?.length && (
<h1 className="px-px-10 px-mb-10">{_(
'Showing %s - %s of %s',
(skip + 1).toString(),
(skip + results.length).toString(),
total.toString()
)}</h1>
)}
<div className="flex-grow px-w-100-0 relative bottom-0 overflow-auto">
{!results?.length ? (
<Alert info>
<i className="fas fa-fw fa-info-circle px-mr-5"></i>
{_('No results found.')}
</Alert>
): (
<Admin${model.title}SearchResults
base={base}
can={can}
query={query}
results={results}
/>
)}
</div>
<Pagination
total={total}
take={take}
skip={skip}
paginate={page}
/>
</main>
);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchHead`,
parameters: [{
name: 'props',
type: 'ServerPageProps<AdminConfigProps>'
}],
statements: (`
const { data, styles = [] } = props;
const { favicon = '/favicon.ico' } = data?.brand || {};
const { _ } = useLanguage();
const mimetype = favicon.endsWith('.png')
? 'image/png'
: favicon.endsWith('.svg')
? 'image/svg+xml'
: 'image/x-icon';
return (
<>
<title>{_('Search ${model.plural}')}</title>
{favicon && <link rel="icon" type={mimetype} href={favicon} />}
<link rel="stylesheet" type="text/css" href="/styles/global.css" />
{styles.map((href, index) => (
<link key={index} rel="stylesheet" type="text/css" href={href} />
))}
</>
);
`)
});
source.addFunction({
isExported: true,
name: `Admin${model.title}SearchPage`,
parameters: [{
name: 'props',
type: 'ServerPageProps<AdminConfigProps>'
}],
statements: (`
return (
<LayoutAdmin {...props}>
<Admin${model.title}SearchBody />
</LayoutAdmin>
);
`)
});
source.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [{
name: 'Head',
initializer: `Admin${model.title}SearchHead`
}]
});
source.addStatements(`export default Admin${model.title}SearchPage;`);
}