Feature/template builder rework
This commit is contained in:
@@ -7,6 +7,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored admin module-generator source builders to compose imports and source sections through
|
||||||
|
shared source-file helpers while preserving the existing generation payload.
|
||||||
|
- Renamed module-generator builder classes and files from template-builder terminology to
|
||||||
|
source-builder terminology.
|
||||||
|
- Updated module-generator frontend documentation to reference the new source-builder names.
|
||||||
|
|
||||||
## [1.0.1] - 2026-06-30
|
## [1.0.1] - 2026-06-30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import { ModuleAsset } from '#devtools/entities/ModuleAsset';
|
||||||
|
import { ModuleGeneratorConfig, ModuleTableColumn, ModuleTableConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
||||||
|
import { indentSource, SourceFile, source } from './shared/SourceFile';
|
||||||
|
|
||||||
|
class DataTableSourceBuilder {
|
||||||
|
build(data: ModuleGeneratorConfig) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
this.addImports(file, data);
|
||||||
|
file.addSection(this.buildColumnFilterPropsInterface());
|
||||||
|
file.addSection(this.buildDataTableComponent(data));
|
||||||
|
file.addSection(this.buildToolbarComponent(data));
|
||||||
|
file.addSection(this.buildColumnInputFilterComponent());
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private addImports(file: SourceFile, data: ModuleGeneratorConfig) {
|
||||||
|
const shouldAddEditForm = this.shouldAddEditForm(data.generate);
|
||||||
|
const icons = ['LoaderCircleIcon', 'SearchIcon', 'Settings2'];
|
||||||
|
const metaImports = ['MODULE_PERMISSION_DELETE'];
|
||||||
|
|
||||||
|
if (shouldAddEditForm) {
|
||||||
|
icons.push('SquarePen');
|
||||||
|
metaImports.unshift('MODULE_NAME_LABEL', 'MODULE_PERMISSION_CREATE');
|
||||||
|
metaImports.push('MODULE_PERMISSION_UPDATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import React from 'react';",
|
||||||
|
`
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
SortingState,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
`,
|
||||||
|
`import { ${icons.join(', ')} } from 'lucide-react';`,
|
||||||
|
"import { useIntl } from 'react-intl';",
|
||||||
|
"import { Badge } from '#shared/ui/Badge';",
|
||||||
|
"import { CopyButton } from '#shared/ui/CopyButton';",
|
||||||
|
"import { Button } from '#shared/ui/Button';",
|
||||||
|
`
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTable,
|
||||||
|
CardTitle,
|
||||||
|
CardToolbar,
|
||||||
|
} from '#shared/ui/Card';
|
||||||
|
`,
|
||||||
|
"import { DataGrid, useDataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable } from '#shared/widgets/DataGrid';",
|
||||||
|
"import { Input, InputWrapper } from '#shared/ui/Input';",
|
||||||
|
"import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';",
|
||||||
|
"import { Skeleton } from '#shared/ui/Skeleton';",
|
||||||
|
"import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';",
|
||||||
|
"import { useDebounceSearch } from '@crux-kit/hooks';",
|
||||||
|
"import { PermissionGate } from '#core/Permission';",
|
||||||
|
"import { useGetList } from '../../api/list';",
|
||||||
|
`import { ${metaImports.join(', ')} } from '../../config/meta';`,
|
||||||
|
"import { Entity } from '../../entities/Entity';",
|
||||||
|
"import { DeleteTrigger } from './DeleteTrigger';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (shouldAddEditForm) {
|
||||||
|
file.addImport("import { ManageTrigger } from './ManageTrigger';");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildColumnFilterPropsInterface() {
|
||||||
|
return `
|
||||||
|
interface IColumnFilterProps<TData, TValue> {
|
||||||
|
column: Column<TData, TValue>
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDataTableComponent(data: ModuleGeneratorConfig) {
|
||||||
|
return `
|
||||||
|
export const DataTable = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [search, setSearch] = React.useState<string>('');
|
||||||
|
|
||||||
|
const { handleChange } = useDebounceSearch({
|
||||||
|
minQueryNumber: 3,
|
||||||
|
delay: 400,
|
||||||
|
initialQuery: search,
|
||||||
|
onTimeOut: setSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
|
||||||
|
const { list, total, isFetching, isLoading, pagesCount } = useGetList(
|
||||||
|
mapTableStateToListQueryParams(pagination, sorting, search)
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = React.useMemo<ColumnDef<Entity>[]>(
|
||||||
|
() => [
|
||||||
|
${indentSource(this.createColumnsSource(data), 12)}
|
||||||
|
],
|
||||||
|
[intl]
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
columns,
|
||||||
|
autoResetPageIndex: false,
|
||||||
|
enableMultiSort: false,
|
||||||
|
data: list,
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount: pagesCount,
|
||||||
|
getRowId: (row) => String(row.id),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
sorting,
|
||||||
|
},
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGrid
|
||||||
|
table={table}
|
||||||
|
recordCount={total || 0}
|
||||||
|
emptyMessage={intl.formatMessage({ id: 'TABLE.COMMON.NO_DATA_AVAILABLE' })}
|
||||||
|
loadingMode="skeleton"
|
||||||
|
isLoading={isLoading}
|
||||||
|
tableLayout={{
|
||||||
|
columnsDraggable: false,
|
||||||
|
columnsResizable: false,
|
||||||
|
width: 'auto',
|
||||||
|
columnsPinnable: true,
|
||||||
|
columnsMovable: true,
|
||||||
|
columnsVisibility: true,
|
||||||
|
cellBorder: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<InputWrapper>
|
||||||
|
{search.length > 0 && isFetching ? (
|
||||||
|
<LoaderCircleIcon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={intl.formatMessage({ id: 'ACTION.SEARCH' })}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InputWrapper>
|
||||||
|
</CardTitle>
|
||||||
|
<Toolbar />
|
||||||
|
</CardHeader>
|
||||||
|
<CardTable>
|
||||||
|
<ScrollArea>
|
||||||
|
<DataGridTable />
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
</CardTable>
|
||||||
|
<CardFooter>
|
||||||
|
<DataGridPagination />
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</DataGrid>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildToolbarComponent(data: ModuleGeneratorConfig) {
|
||||||
|
return `
|
||||||
|
const Toolbar = () => {
|
||||||
|
const { table } = useDataGrid();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardToolbar>
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
|
${this.shouldAddEditForm(data.generate) ? indentSource(this.buildCreateButton(), 14) : ''}
|
||||||
|
<DataGridColumnVisibility
|
||||||
|
table={table}
|
||||||
|
trigger={
|
||||||
|
<Button variant="outline">
|
||||||
|
<Settings2 />
|
||||||
|
{intl.formatMessage({ id: 'TABLE.COMMON.COLUMNS' })}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildColumnInputFilterComponent() {
|
||||||
|
return `
|
||||||
|
const ColumnInputFilter = <TData, TValue>({ column }: IColumnFilterProps<TData, TValue>) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage({ id: 'FILTER' })}
|
||||||
|
value={(column.getFilterValue() as string) ?? ''}
|
||||||
|
onChange={(event) => column.setFilterValue(event.target.value)}
|
||||||
|
className="h-9 w-full max-w-40"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCreateButton() {
|
||||||
|
return `
|
||||||
|
<PermissionGate requiredPermissions={MODULE_PERMISSION_CREATE}>
|
||||||
|
<ManageTrigger
|
||||||
|
id={null}
|
||||||
|
title={intl.formatMessage(
|
||||||
|
{ id: 'MODULE.COMMON.CREATE_NEW_ENTITY' },
|
||||||
|
{ entity: MODULE_NAME_LABEL }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button>{intl.formatMessage({ id: 'ACTION.CREATE' })}</Button>
|
||||||
|
</ManageTrigger>
|
||||||
|
</PermissionGate>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildEditButton() {
|
||||||
|
return `
|
||||||
|
<PermissionGate requiredPermissions={MODULE_PERMISSION_UPDATE}>
|
||||||
|
<ManageTrigger title={\`Edit \${MODULE_NAME_LABEL\}\`} id={id}>
|
||||||
|
<Button variant="secondary" size="sm" shape="default" mode="icon">
|
||||||
|
<SquarePen />
|
||||||
|
</Button>
|
||||||
|
</ManageTrigger>
|
||||||
|
</PermissionGate>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldAddEditForm(assets: ModuleAsset[]): boolean {
|
||||||
|
return assets.includes(ModuleAsset.FORM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createColumnsSource(ctx: ModuleGeneratorConfig) {
|
||||||
|
const data: ModuleTableConfig = ctx.datatable;
|
||||||
|
const columns: string[] = [];
|
||||||
|
|
||||||
|
data.columns
|
||||||
|
.filter((column) => column.hidden === false)
|
||||||
|
.forEach((column) => {
|
||||||
|
const name = column.value;
|
||||||
|
|
||||||
|
if (column.isActionColumn) {
|
||||||
|
columns.push(this.createActionColumn(column, ctx));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === 'id' || name === 'uuid') {
|
||||||
|
columns.push(this.createIdColumn(column));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(this.createBaseColumn(column));
|
||||||
|
});
|
||||||
|
|
||||||
|
return columns.map(source).join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBaseColumn(data: ModuleTableColumn) {
|
||||||
|
const enableSorting = data.innerProps.sort ? 'true' : 'false';
|
||||||
|
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
id: '${data.value}',
|
||||||
|
accessorFn: (row) => row.${data.value},
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataGridColumnHeader
|
||||||
|
title="${data.label}"
|
||||||
|
filter={<ColumnInputFilter column={column} />}
|
||||||
|
column={column}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ${source(this.createCellContent(data))},
|
||||||
|
enableSorting: ${enableSorting},
|
||||||
|
meta: {
|
||||||
|
cellClassName: '',
|
||||||
|
skeleton: <Skeleton className="h-4" />,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createIdColumn(data: ModuleTableColumn) {
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
id: '${data.value}',
|
||||||
|
accessorFn: (row) => row.id,
|
||||||
|
header: ({ column }) => <DataGridColumnHeader title="${data.label}" column={column} />,
|
||||||
|
cell: ${source(this.createCellContent(data))},
|
||||||
|
enableSorting: true,
|
||||||
|
size: 80,
|
||||||
|
meta: {
|
||||||
|
cellClassName: '',
|
||||||
|
skeleton: <Skeleton className="h-4" />,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createActionColumn(data: ModuleTableColumn, ctx: ModuleGeneratorConfig) {
|
||||||
|
const editButtonSource = this.shouldAddEditForm(ctx.generate) ? this.buildEditButton() : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
id: '${data.value}',
|
||||||
|
header: '${data.label}',
|
||||||
|
cell: (info) => {
|
||||||
|
const data = info.row.original;
|
||||||
|
const id = data.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 justify-end">
|
||||||
|
${editButtonSource ? indentSource(editButtonSource, 14) : ''}
|
||||||
|
<PermissionGate requiredPermissions={MODULE_PERMISSION_DELETE}>
|
||||||
|
<DeleteTrigger id={id} deleteTarget={id} />
|
||||||
|
</PermissionGate>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enableSorting: false,
|
||||||
|
size: 96,
|
||||||
|
meta: {
|
||||||
|
headerClassName: '',
|
||||||
|
skeleton: <Skeleton className="h-4" />,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCellContent(data: ModuleTableColumn) {
|
||||||
|
if (data.innerProps.copy) {
|
||||||
|
return `
|
||||||
|
(info) => {
|
||||||
|
const val = String(info.getValue());
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<code className="line-clamp-1">{val}</code>
|
||||||
|
<CopyButton copyText={val} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.innerProps.badge) {
|
||||||
|
return `(info) => <Badge variant="primary" size="sm">{String(info.getValue())}</Badge>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '(info) => info.getValue()';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DataTableSourceBuilder();
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
import { ModuleAsset } from '#devtools/entities/ModuleAsset';
|
|
||||||
import { ModuleGeneratorConfig, ModuleTableColumn, ModuleTableConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
|
||||||
|
|
||||||
class DataTableTemplateBuilder {
|
|
||||||
build(data: ModuleGeneratorConfig) {
|
|
||||||
const FormButton = `
|
|
||||||
<PermissionGate requiredPermissions={MODULE_PERMISSION_CREATE}>
|
|
||||||
<ManageTrigger
|
|
||||||
id={null}
|
|
||||||
title={intl.formatMessage(
|
|
||||||
{ id: 'MODULE.COMMON.CREATE_NEW_ENTITY' },
|
|
||||||
{ entity: MODULE_NAME_LABEL }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Button>{intl.formatMessage({ id: 'ACTION.CREATE' })}</Button>
|
|
||||||
</ManageTrigger>
|
|
||||||
</PermissionGate>
|
|
||||||
`;
|
|
||||||
const raw = `
|
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Column,
|
|
||||||
ColumnDef,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
PaginationState,
|
|
||||||
SortingState,
|
|
||||||
useReactTable,
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import { LoaderCircleIcon, SearchIcon, Settings2, SquarePen } from 'lucide-react';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { Badge } from '#shared/ui/Badge';
|
|
||||||
import { CopyButton } from '#shared/ui/CopyButton';
|
|
||||||
import { Button } from '#shared/ui/Button';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTable,
|
|
||||||
CardTitle,
|
|
||||||
CardToolbar,
|
|
||||||
} from '#shared/ui/Card';
|
|
||||||
import { DataGrid, useDataGrid, DataGridColumnHeader, DataGridColumnVisibility, DataGridPagination, DataGridTable } from '#shared/widgets/DataGrid';
|
|
||||||
import { Input, InputWrapper } from '#shared/ui/Input';
|
|
||||||
import { ScrollArea, ScrollBar } from '#shared/ui/ScrollArea';
|
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
|
||||||
import { mapTableStateToListQueryParams } from '#infrastructure/reactQuery';
|
|
||||||
import { useDebounceSearch } from '@crux-kit/hooks';
|
|
||||||
import { PermissionGate } from '#core/Permission';
|
|
||||||
import { useGetList } from '../../api/list';
|
|
||||||
import {
|
|
||||||
MODULE_NAME_LABEL,
|
|
||||||
MODULE_PERMISSION_CREATE,
|
|
||||||
MODULE_PERMISSION_DELETE,
|
|
||||||
MODULE_PERMISSION_UPDATE,
|
|
||||||
} from '../../config/meta';
|
|
||||||
import { Entity } from '../../entities/Entity';
|
|
||||||
import { DeleteTrigger } from './DeleteTrigger';
|
|
||||||
${this.shouldAddEditForm(data.generate) ? "import { ManageTrigger } from './ManageTrigger';" : ''}
|
|
||||||
|
|
||||||
interface IColumnFilterProps<TData, TValue> {
|
|
||||||
column: Column<TData, TValue>
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DataTable = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [search, setSearch] = React.useState<string>('');
|
|
||||||
|
|
||||||
const { handleChange } = useDebounceSearch({
|
|
||||||
minQueryNumber: 3,
|
|
||||||
delay: 400,
|
|
||||||
initialQuery: search,
|
|
||||||
onTimeOut: setSearch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
||||||
|
|
||||||
const { list, total, isFetching, isLoading, pagesCount } = useGetList(
|
|
||||||
mapTableStateToListQueryParams(pagination, sorting, search)
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = React.useMemo<ColumnDef<Entity>[]>(
|
|
||||||
() => [${this.createColumnsHTML(data)}],
|
|
||||||
[intl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
columns,
|
|
||||||
autoResetPageIndex: false,
|
|
||||||
enableMultiSort: false,
|
|
||||||
data: list,
|
|
||||||
manualPagination: true,
|
|
||||||
pageCount: pagesCount,
|
|
||||||
getRowId: (row) => String(row.id),
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
sorting,
|
|
||||||
},
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
onSortingChange: setSorting,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataGrid
|
|
||||||
table={table}
|
|
||||||
recordCount={total || 0}
|
|
||||||
emptyMessage={intl.formatMessage({ id: 'TABLE.COMMON.NO_DATA_AVAILABLE' })}
|
|
||||||
loadingMode="skeleton"
|
|
||||||
isLoading={isLoading}
|
|
||||||
tableLayout={{
|
|
||||||
columnsDraggable: false,
|
|
||||||
columnsResizable: false,
|
|
||||||
width: 'auto',
|
|
||||||
columnsPinnable: true,
|
|
||||||
columnsMovable: true,
|
|
||||||
columnsVisibility: true,
|
|
||||||
cellBorder: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
<InputWrapper>
|
|
||||||
{search.length > 0 && isFetching ? (
|
|
||||||
<LoaderCircleIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<SearchIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder={intl.formatMessage({ id: 'ACTION.SEARCH' })}
|
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</InputWrapper>
|
|
||||||
</CardTitle>
|
|
||||||
<Toolbar />
|
|
||||||
</CardHeader>
|
|
||||||
<CardTable>
|
|
||||||
<ScrollArea>
|
|
||||||
<DataGridTable />
|
|
||||||
<ScrollBar orientation="horizontal" />
|
|
||||||
</ScrollArea>
|
|
||||||
</CardTable>
|
|
||||||
<CardFooter>
|
|
||||||
<DataGridPagination />
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</DataGrid>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
const Toolbar = () => {
|
|
||||||
const { table } = useDataGrid();
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardToolbar>
|
|
||||||
<div className="flex flex-wrap items-center gap-2.5">
|
|
||||||
${this.shouldAddEditForm(data.generate) ? FormButton : ''}
|
|
||||||
<DataGridColumnVisibility
|
|
||||||
table={table}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
<Settings2 />
|
|
||||||
{intl.formatMessage({ id: 'TABLE.COMMON.COLUMNS' })}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardToolbar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ColumnInputFilter = <TData, TValue>({ column }: IColumnFilterProps<TData, TValue>) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
placeholder={intl.formatMessage({ id: 'FILTER' })}
|
|
||||||
value={(column.getFilterValue() as string) ?? ''}
|
|
||||||
onChange={(event) => column.setFilterValue(event.target.value)}
|
|
||||||
className="h-9 w-full max-w-40"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldAddEditForm(assets: ModuleAsset[]): boolean {
|
|
||||||
return assets.includes(ModuleAsset.FORM);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createColumnsHTML(ctx: ModuleGeneratorConfig) {
|
|
||||||
const data: ModuleTableConfig = ctx.datatable;
|
|
||||||
const columns: string[] = [];
|
|
||||||
|
|
||||||
data.columns
|
|
||||||
.filter((column) => column.hidden === false)
|
|
||||||
.forEach((column) => {
|
|
||||||
const name = column.value;
|
|
||||||
|
|
||||||
if (column.isActionColumn) {
|
|
||||||
columns.push(this.createActionColumn(column, ctx));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (name === 'id' || name === 'uuid') {
|
|
||||||
columns.push(this.createIdColumn(column));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(this.createBaseColumn(column));
|
|
||||||
});
|
|
||||||
|
|
||||||
return columns.join(',\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBaseColumn(data: ModuleTableColumn) {
|
|
||||||
const enableSorting = data.innerProps.sort ? 'true' : 'false';
|
|
||||||
const baseCellContent = '(info) => info.getValue()';
|
|
||||||
|
|
||||||
let cellContent = baseCellContent;
|
|
||||||
|
|
||||||
if (data.innerProps.badge) {
|
|
||||||
const cellContentWithBadge = `(info) => <Badge variant="primary" size="sm">{String(info.getValue())}</Badge>`;
|
|
||||||
cellContent = cellContentWithBadge;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.innerProps.copy) {
|
|
||||||
const cellContentWithCopy = `
|
|
||||||
(info) => {
|
|
||||||
const val = String(info.getValue());
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<code className="line-clamp-1">{val}</code>
|
|
||||||
<CopyButton copyText={val} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
cellContent = cellContentWithCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
{
|
|
||||||
id: '${data.value}',
|
|
||||||
accessorFn: (row) => row.${data.value},
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataGridColumnHeader
|
|
||||||
title="${data.label}"
|
|
||||||
filter={<ColumnInputFilter column={column} />}
|
|
||||||
column={column}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ${cellContent},
|
|
||||||
enableSorting: ${enableSorting},
|
|
||||||
meta: {
|
|
||||||
cellClassName: '',
|
|
||||||
skeleton: <Skeleton className="h-4" />,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createIdColumn(data: ModuleTableColumn) {
|
|
||||||
const baseCellContent = '(info) => info.getValue()';
|
|
||||||
|
|
||||||
let cellContent = baseCellContent;
|
|
||||||
|
|
||||||
if (data.innerProps.badge) {
|
|
||||||
const cellContentWithBadge = `(info) => <Badge variant="primary" size="sm">{String(info.getValue())}</Badge>`;
|
|
||||||
cellContent = cellContentWithBadge;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.innerProps.copy) {
|
|
||||||
const cellContentWithCopy = `
|
|
||||||
(info) => {
|
|
||||||
const val = String(info.getValue());
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<code className="line-clamp-1">{val}</code>
|
|
||||||
<CopyButton copyText={val} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
cellContent = cellContentWithCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
{
|
|
||||||
id: '${data.value}',
|
|
||||||
accessorFn: (row) => row.id,
|
|
||||||
header: ({ column }) => <DataGridColumnHeader title="${data.label}" column={column} />,
|
|
||||||
cell: ${cellContent},
|
|
||||||
enableSorting: true,
|
|
||||||
size: 80,
|
|
||||||
meta: {
|
|
||||||
cellClassName: '',
|
|
||||||
skeleton: <Skeleton className="h-4" />,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createActionColumn(data: ModuleTableColumn, ctx: ModuleGeneratorConfig) {
|
|
||||||
const EditFormButton = `
|
|
||||||
<PermissionGate requiredPermissions={MODULE_PERMISSION_UPDATE}>
|
|
||||||
<ManageTrigger title={\`Edit \${MODULE_NAME_LABEL\}\`} id={id}>
|
|
||||||
<Button variant="secondary" size="sm" shape="default" mode="icon">
|
|
||||||
<SquarePen />
|
|
||||||
</Button>
|
|
||||||
</ManageTrigger>
|
|
||||||
</PermissionGate>
|
|
||||||
`;
|
|
||||||
const DeleteRowButton = `
|
|
||||||
<PermissionGate requiredPermissions={MODULE_PERMISSION_DELETE}>
|
|
||||||
<DeleteTrigger id={id} deleteTarget={id} />
|
|
||||||
</PermissionGate>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
{
|
|
||||||
id: '${data.value}',
|
|
||||||
header: '${data.label}',
|
|
||||||
cell: (info) => {
|
|
||||||
const data = info.row.original;
|
|
||||||
const id = data.id;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 justify-end">
|
|
||||||
${this.shouldAddEditForm(ctx.generate) && EditFormButton}
|
|
||||||
${DeleteRowButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
enableSorting: false,
|
|
||||||
size: 96,
|
|
||||||
meta: {
|
|
||||||
headerClassName: '',
|
|
||||||
skeleton: <Skeleton className="h-4" />,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new DataTableTemplateBuilder();
|
|
||||||
+47
-26
@@ -1,17 +1,46 @@
|
|||||||
import { ModuleFieldType } from '#devtools/entities/ModuleFieldType';
|
import { ModuleFieldType } from '#devtools/entities/ModuleFieldType';
|
||||||
import { ModuleGridCell, ModuleLayoutConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
import { ModuleGridCell, ModuleLayoutConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
||||||
|
import { indentSource, SourceFile, source } from './shared/SourceFile';
|
||||||
|
|
||||||
class FormTemplateBuilder {
|
class FormSourceBuilder {
|
||||||
buildComponent(data: ModuleLayoutConfig) {
|
buildComponent(data: ModuleLayoutConfig) {
|
||||||
const raw = `
|
const file = new SourceFile();
|
||||||
import React from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Form as FormProvider } from '#shared/ui/form';
|
|
||||||
import * as Component from '#shared/forms/composed';
|
|
||||||
import { FORM_ID } from './constants';
|
|
||||||
import { FormValues, SubmitFunc, validationSchema } from './form';
|
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import React from 'react';",
|
||||||
|
"import { useForm } from 'react-hook-form';",
|
||||||
|
"import { zodResolver } from '@hookform/resolvers/zod';",
|
||||||
|
"import { Form as FormProvider } from '#shared/ui/form';",
|
||||||
|
"import * as Component from '#shared/forms/composed';",
|
||||||
|
"import { FORM_ID } from './constants';",
|
||||||
|
"import { FormValues, SubmitFunc, validationSchema } from './form';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
file.addSection(this.buildMainFormComponent(data));
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDefinition(data: ModuleLayoutConfig) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import { UseFormReturn } from 'react-hook-form';",
|
||||||
|
"import z from 'zod';",
|
||||||
|
"import { Entity } from '../../../entities/Entity';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (data.grid.find((item) => item.body?.type === ModuleFieldType.PHONE)) {
|
||||||
|
file.addImport("import { e164ToMask } from '#shared/lib/phoneNumber';");
|
||||||
|
}
|
||||||
|
|
||||||
|
file.addSection(this.buildFormDefinition(data));
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildMainFormComponent(data: ModuleLayoutConfig) {
|
||||||
|
return `
|
||||||
export const MainForm: React.FC<{
|
export const MainForm: React.FC<{
|
||||||
initialValues: FormValues;
|
initialValues: FormValues;
|
||||||
onSubmit: SubmitFunc;
|
onSubmit: SubmitFunc;
|
||||||
@@ -28,25 +57,18 @@ class FormTemplateBuilder {
|
|||||||
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
|
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
|
||||||
>
|
>
|
||||||
<Component.FormGrid columns={${data.cols}}>
|
<Component.FormGrid columns={${data.cols}}>
|
||||||
${this.createFieldsHTML(data)}
|
${indentSource(this.createFieldsSource(data), 16)}
|
||||||
</Component.FormGrid>
|
</Component.FormGrid>
|
||||||
</Component.FormElement>
|
</Component.FormElement>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDefinition(data: ModuleLayoutConfig) {
|
private buildFormDefinition(data: ModuleLayoutConfig) {
|
||||||
const raw = `
|
return `
|
||||||
import { UseFormReturn } from 'react-hook-form';
|
export const validationSchema = ${source(this.createValidationSchema(data))};
|
||||||
import z from 'zod';
|
|
||||||
import { e164ToMask } from '#shared/lib/phoneNumber';
|
|
||||||
import { Entity } from '../../../entities/Entity';
|
|
||||||
|
|
||||||
export const validationSchema = ${this.createValidationSchema(data)};
|
|
||||||
|
|
||||||
export type FormValues = z.infer<typeof validationSchema>;
|
export type FormValues = z.infer<typeof validationSchema>;
|
||||||
|
|
||||||
@@ -60,15 +82,14 @@ class FormTemplateBuilder {
|
|||||||
|
|
||||||
export const initialValues: FormValues = ${this.createInitialValues(data)};
|
export const initialValues: FormValues = ${this.createInitialValues(data)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createFieldsHTML(formConfig: ModuleLayoutConfig): string {
|
private createFieldsSource(formConfig: ModuleLayoutConfig): string {
|
||||||
return formConfig.grid
|
return formConfig.grid
|
||||||
.map((cell) => this.mapGridCellToComponent(cell))
|
.map((cell) => this.mapGridCellToComponent(cell))
|
||||||
.filter((str) => str != null)
|
.filter((str): str is string => str != null)
|
||||||
.join('');
|
.map(source)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private createInitialValues(formConfig: ModuleLayoutConfig): string {
|
private createInitialValues(formConfig: ModuleLayoutConfig): string {
|
||||||
@@ -589,4 +610,4 @@ class FormTemplateBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FormTemplateBuilder();
|
export default new FormSourceBuilder();
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { ModuleTableConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
||||||
|
import { SourceFile } from './shared/SourceFile';
|
||||||
|
|
||||||
|
class ListPageSourceBuilder {
|
||||||
|
build(_data: ModuleTableConfig, params: { addTable: boolean }) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import React from 'react';",
|
||||||
|
"import { Container } from '#shared/widgets/common/Container';",
|
||||||
|
"import { PermissionPageGate } from '#core/Permission';",
|
||||||
|
"import { MODULE_PERMISSION_VIEW } from '../../config/meta';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (params.addTable) {
|
||||||
|
file.addImport("import { DataTable } from '../../view/widgets/DataTable';");
|
||||||
|
}
|
||||||
|
|
||||||
|
file.addSection(this.buildComponent(params.addTable ? '<DataTable />' : ''));
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildComponent(tableSource: string) {
|
||||||
|
return `
|
||||||
|
const ListPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<PermissionPageGate requiredPermissions={[MODULE_PERMISSION_VIEW]}>
|
||||||
|
<Container>
|
||||||
|
${tableSource}
|
||||||
|
</Container>
|
||||||
|
</PermissionPageGate>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListPage;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ListPageSourceBuilder();
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import { ModuleFormContainer } from '#devtools/entities/ModuleGeneratorTypes';
|
||||||
|
import { SourceFile } from './shared/SourceFile';
|
||||||
|
|
||||||
|
type DialogSize = 'default' | 'md' | 'fullscreen';
|
||||||
|
type SheetSide = 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
|
||||||
|
interface ManageTriggerContainerConfig {
|
||||||
|
body: string;
|
||||||
|
close: string;
|
||||||
|
content: string;
|
||||||
|
contentProps: string;
|
||||||
|
description: string;
|
||||||
|
disclosureName: string;
|
||||||
|
footer: string;
|
||||||
|
header: string;
|
||||||
|
root: string;
|
||||||
|
title: string;
|
||||||
|
trigger: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageTriggerSourceBuilder {
|
||||||
|
build(container: ModuleFormContainer) {
|
||||||
|
if (this.isSheet(container)) {
|
||||||
|
return this.buildSheet(this.getSheetSide(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.buildDialog(this.getDialogSize(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSheet(
|
||||||
|
container: ModuleFormContainer
|
||||||
|
): container is Extract<ModuleFormContainer, `sheet-${string}`> {
|
||||||
|
return container.startsWith('sheet-');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSheetSide(container: Extract<ModuleFormContainer, `sheet-${string}`>): SheetSide {
|
||||||
|
switch (container) {
|
||||||
|
case 'sheet-top':
|
||||||
|
return 'top';
|
||||||
|
case 'sheet-right':
|
||||||
|
return 'right';
|
||||||
|
case 'sheet-bottom':
|
||||||
|
return 'bottom';
|
||||||
|
case 'sheet-left':
|
||||||
|
return 'left';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDialogSize(container: Extract<ModuleFormContainer, `dialog-${string}`>): DialogSize {
|
||||||
|
switch (container) {
|
||||||
|
case 'dialog-default':
|
||||||
|
return 'default';
|
||||||
|
case 'dialog-md':
|
||||||
|
return 'md';
|
||||||
|
case 'dialog-fullscreen':
|
||||||
|
return 'fullscreen';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDialog(dialogSize: DialogSize) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import React from 'react';",
|
||||||
|
"import { LoaderCircleIcon } from 'lucide-react';",
|
||||||
|
"import { useIntl } from 'react-intl';",
|
||||||
|
"import { Button } from '#shared/ui/Button';",
|
||||||
|
`
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogBody,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '#shared/ui/Dialog';
|
||||||
|
`,
|
||||||
|
"import { Skeleton } from '#shared/ui/Skeleton';",
|
||||||
|
"import { useDisclosure } from '@crux-kit/hooks';",
|
||||||
|
"import { useGetDetailed } from '../../api/detailed';",
|
||||||
|
"import { initialValues, FORM_ID, MainForm, prepareInitialValues, useSubmit } from '../forms/MainForm';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
file.addSection(
|
||||||
|
this.buildManageTriggerComponent({
|
||||||
|
body: 'DialogBody',
|
||||||
|
close: 'DialogClose',
|
||||||
|
content: 'DialogContent',
|
||||||
|
contentProps: `variant="${dialogSize}"`,
|
||||||
|
description: 'DialogDescription',
|
||||||
|
disclosureName: 'dialog',
|
||||||
|
footer: 'DialogFooter',
|
||||||
|
header: 'DialogHeader',
|
||||||
|
root: 'Dialog',
|
||||||
|
title: 'DialogTitle',
|
||||||
|
trigger: 'DialogTrigger'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSheet(side: SheetSide) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import React from 'react';",
|
||||||
|
"import { LoaderCircleIcon } from 'lucide-react';",
|
||||||
|
"import { useIntl } from 'react-intl';",
|
||||||
|
"import { Button } from '#shared/ui/Button';",
|
||||||
|
`
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetBody,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '#shared/ui/Sheet';
|
||||||
|
`,
|
||||||
|
"import { Skeleton } from '#shared/ui/Skeleton';",
|
||||||
|
"import { useDisclosure } from '@crux-kit/hooks';",
|
||||||
|
"import { useGetDetailed } from '../../api/detailed';",
|
||||||
|
"import { initialValues, FORM_ID, MainForm, prepareInitialValues, useSubmit } from '../forms/MainForm';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
file.addSection(
|
||||||
|
this.buildManageTriggerComponent({
|
||||||
|
body: 'SheetBody',
|
||||||
|
close: 'SheetClose',
|
||||||
|
content: 'SheetContent',
|
||||||
|
contentProps: `side="${side}" className="max-h-dvh"`,
|
||||||
|
description: 'SheetDescription',
|
||||||
|
disclosureName: 'sheet',
|
||||||
|
footer: 'SheetFooter',
|
||||||
|
header: 'SheetHeader',
|
||||||
|
root: 'Sheet',
|
||||||
|
title: 'SheetTitle',
|
||||||
|
trigger: 'SheetTrigger'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildManageTriggerComponent(config: ManageTriggerContainerConfig) {
|
||||||
|
const bodyProps = config.body === 'SheetBody' ? ' className="overflow-y-auto"' : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
export const ManageTrigger: React.FC<
|
||||||
|
React.PropsWithChildren<{
|
||||||
|
title: string;
|
||||||
|
id: number | null;
|
||||||
|
}>
|
||||||
|
> = ({ id, children, title }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const ${config.disclosureName} = useDisclosure(false);
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetDetailed(
|
||||||
|
id == null || ${config.disclosureName}.isOpen === false ? undefined : { id: String(id) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isSubmitting, submit } = useSubmit({ onSuccess: ${config.disclosureName}.close });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<${config.root} open={${config.disclosureName}.isOpen} onOpenChange={${config.disclosureName}.toggle}>
|
||||||
|
<${config.trigger} asChild>{children}</${config.trigger}>
|
||||||
|
<${config.content} ${config.contentProps}>
|
||||||
|
<${config.header}>
|
||||||
|
<${config.title}>{title}</${config.title}>
|
||||||
|
<${config.description} className="sr-only"></${config.description}>
|
||||||
|
</${config.header}>
|
||||||
|
<${config.body}${bodyProps}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="grid grid-cols-2 gap-5">
|
||||||
|
<Skeleton className="h-15" />
|
||||||
|
<Skeleton className="h-15" />
|
||||||
|
<Skeleton className="h-15" />
|
||||||
|
<Skeleton className="h-15" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<MainForm
|
||||||
|
initialValues={data ? prepareInitialValues(data) : initialValues}
|
||||||
|
onSubmit={submit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</${config.body}>
|
||||||
|
<${config.footer}>
|
||||||
|
<Button type="submit" form={FORM_ID} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
||||||
|
{intl.formatMessage({ id: 'STATE.SAVING' })}...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
intl.formatMessage({ id: 'ACTION.SAVE' })
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<${config.close} asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
{intl.formatMessage({ id: 'ACTION.CANCEL' })}
|
||||||
|
</Button>
|
||||||
|
</${config.close}>
|
||||||
|
</${config.footer}>
|
||||||
|
</${config.content}>
|
||||||
|
</${config.root}>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ManageTriggerSourceBuilder();
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { SourceFile } from './shared/SourceFile';
|
||||||
|
|
||||||
|
class MenuSourceBuilder {
|
||||||
|
build() {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import { NotepadText } from 'lucide-react'",
|
||||||
|
"import { IntlShape } from 'react-intl'",
|
||||||
|
"import { MenuItem } from '#app/config/types'",
|
||||||
|
"import { MODULE_NAME_LABEL, MODULE_PERMISSION_VIEW, MODULE_ROOT_PATH } from './meta'"
|
||||||
|
]);
|
||||||
|
|
||||||
|
file.addSection(`
|
||||||
|
export default (intl: IntlShape): MenuItem => ({
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: \`MODULES.\${MODULE_NAME_LABEL.toUpperCase()}.TITLE\`,
|
||||||
|
defaultMessage: MODULE_NAME_LABEL,
|
||||||
|
}),
|
||||||
|
icon: NotepadText,
|
||||||
|
path: MODULE_ROOT_PATH,
|
||||||
|
permissions: [MODULE_PERMISSION_VIEW],
|
||||||
|
})
|
||||||
|
`);
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MenuSourceBuilder();
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { SourceFile } from './shared/SourceFile';
|
||||||
|
|
||||||
|
class ModuleConfigSourceBuilder {
|
||||||
|
build(moduleId: string) {
|
||||||
|
const file = new SourceFile();
|
||||||
|
|
||||||
|
file.addImports([
|
||||||
|
"import { AppModuleConfig } from '#app/services/modules/types';",
|
||||||
|
"import menu from './menu';",
|
||||||
|
"import messages from './messages';",
|
||||||
|
"import routes from './routing';"
|
||||||
|
]);
|
||||||
|
|
||||||
|
file.addSection(`
|
||||||
|
export default {
|
||||||
|
id: '${moduleId}',
|
||||||
|
area: 'business',
|
||||||
|
routeScope: 'protected',
|
||||||
|
order: 100,
|
||||||
|
routes,
|
||||||
|
menu,
|
||||||
|
messages,
|
||||||
|
} satisfies AppModuleConfig;
|
||||||
|
`);
|
||||||
|
|
||||||
|
return file.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ModuleConfigSourceBuilder();
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import { ModuleFormContainer } from '#devtools/entities/ModuleGeneratorTypes';
|
|
||||||
|
|
||||||
class FormDialogTemplateBuilder {
|
|
||||||
build(container: ModuleFormContainer) {
|
|
||||||
if (this.isSheet(container)) {
|
|
||||||
return this.buildSheet(this.getSheetSide(container));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.buildDialog(this.getDialogSize(container));
|
|
||||||
}
|
|
||||||
|
|
||||||
private isSheet(
|
|
||||||
container: ModuleFormContainer
|
|
||||||
): container is Extract<ModuleFormContainer, `sheet-${string}`> {
|
|
||||||
return container.startsWith('sheet-');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSheetSide(
|
|
||||||
container: Extract<ModuleFormContainer, `sheet-${string}`>
|
|
||||||
): 'top' | 'right' | 'bottom' | 'left' {
|
|
||||||
switch (container) {
|
|
||||||
case 'sheet-top':
|
|
||||||
return 'top';
|
|
||||||
case 'sheet-right':
|
|
||||||
return 'right';
|
|
||||||
case 'sheet-bottom':
|
|
||||||
return 'bottom';
|
|
||||||
case 'sheet-left':
|
|
||||||
return 'left';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDialogSize(
|
|
||||||
container: Extract<ModuleFormContainer, `dialog-${string}`>
|
|
||||||
): 'default' | 'md' | 'fullscreen' {
|
|
||||||
switch (container) {
|
|
||||||
case 'dialog-default':
|
|
||||||
return 'default';
|
|
||||||
case 'dialog-md':
|
|
||||||
return 'md';
|
|
||||||
case 'dialog-fullscreen':
|
|
||||||
return 'fullscreen';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildDialog(dialogSize: 'default' | 'md' | 'fullscreen') {
|
|
||||||
const raw = `
|
|
||||||
import React from 'react';
|
|
||||||
import { LoaderCircleIcon } from 'lucide-react';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { Button } from '#shared/ui/Button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogBody,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '#shared/ui/Dialog';
|
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
|
||||||
import { useDisclosure } from '@crux-kit/hooks';
|
|
||||||
import { useGetDetailed } from '../../api/detailed';
|
|
||||||
import { initialValues, FORM_ID, MainForm, prepareInitialValues, useSubmit } from '../forms/MainForm';
|
|
||||||
|
|
||||||
export const ManageTrigger: React.FC<
|
|
||||||
React.PropsWithChildren<{
|
|
||||||
title: string;
|
|
||||||
id: number | null;
|
|
||||||
}>
|
|
||||||
> = ({ id, children, title }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const dialog = useDisclosure(false);
|
|
||||||
|
|
||||||
const { data, isLoading } = useGetDetailed(
|
|
||||||
id == null || dialog.isOpen === false ? undefined : { id: String(id) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isSubmitting, submit } = useSubmit({ onSuccess: dialog.close });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={dialog.isOpen} onOpenChange={dialog.toggle}>
|
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
||||||
<DialogContent variant="${dialogSize}">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only"></DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogBody>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="grid grid-cols-2 gap-5">
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<MainForm
|
|
||||||
initialValues={data ? prepareInitialValues(data) : initialValues}
|
|
||||||
onSubmit={submit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DialogBody>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="submit" form={FORM_ID} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
|
||||||
{intl.formatMessage({ id: 'STATE.SAVING' })}...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
intl.formatMessage({ id: 'ACTION.SAVE' })
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">
|
|
||||||
{intl.formatMessage({ id: 'ACTION.CANCEL' })}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildSheet(side: 'top' | 'right' | 'bottom' | 'left') {
|
|
||||||
const raw = `
|
|
||||||
import React from 'react';
|
|
||||||
import { LoaderCircleIcon } from 'lucide-react';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { Button } from '#shared/ui/Button';
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetBody,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetFooter,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from '#shared/ui/Sheet';
|
|
||||||
import { Skeleton } from '#shared/ui/Skeleton';
|
|
||||||
import { useDisclosure } from '@crux-kit/hooks';
|
|
||||||
import { useGetDetailed } from '../../api/detailed';
|
|
||||||
import { initialValues, FORM_ID, MainForm, prepareInitialValues, useSubmit } from '../forms/MainForm';
|
|
||||||
|
|
||||||
export const ManageTrigger: React.FC<
|
|
||||||
React.PropsWithChildren<{
|
|
||||||
title: string;
|
|
||||||
id: number | null;
|
|
||||||
}>
|
|
||||||
> = ({ id, children, title }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const sheet = useDisclosure(false);
|
|
||||||
|
|
||||||
const { data, isLoading } = useGetDetailed(
|
|
||||||
id == null || sheet.isOpen === false ? undefined : { id: String(id) },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isSubmitting, submit } = useSubmit({ onSuccess: sheet.close });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sheet open={sheet.isOpen} onOpenChange={sheet.toggle}>
|
|
||||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
|
||||||
<SheetContent side="${side}" className="max-h-dvh">
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle>{title}</SheetTitle>
|
|
||||||
<SheetDescription className="sr-only"></SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<SheetBody className="overflow-y-auto">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="grid grid-cols-2 gap-5">
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
<Skeleton className="h-15" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<MainForm
|
|
||||||
initialValues={data ? prepareInitialValues(data) : initialValues}
|
|
||||||
onSubmit={submit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SheetBody>
|
|
||||||
<SheetFooter>
|
|
||||||
<Button type="submit" form={FORM_ID} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<LoaderCircleIcon className="h-4 w-4 animate-spin" />
|
|
||||||
{intl.formatMessage({ id: 'STATE.SAVING' })}...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
intl.formatMessage({ id: 'ACTION.SAVE' })
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SheetClose asChild>
|
|
||||||
<Button variant="outline">
|
|
||||||
{intl.formatMessage({ id: 'ACTION.CANCEL' })}
|
|
||||||
</Button>
|
|
||||||
</SheetClose>
|
|
||||||
</SheetFooter>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new FormDialogTemplateBuilder();
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { ModuleTableConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
|
||||||
|
|
||||||
class ListPageTemplateBuilder {
|
|
||||||
build(_data: ModuleTableConfig, params: { addTable: boolean }) {
|
|
||||||
const imports = [
|
|
||||||
"import React from 'react';",
|
|
||||||
"import { Container } from '#shared/widgets/common/Container';",
|
|
||||||
"import { PermissionPageGate } from '#core/Permission';",
|
|
||||||
"import { MODULE_PERMISSION_VIEW } from '../../config/meta';"
|
|
||||||
];
|
|
||||||
|
|
||||||
let table = '';
|
|
||||||
|
|
||||||
if (params.addTable) {
|
|
||||||
imports.push("import { DataTable } from '../../view/widgets/DataTable';");
|
|
||||||
table = '<DataTable />';
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = `
|
|
||||||
${imports.join('\n')}
|
|
||||||
\n
|
|
||||||
const ListPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<PermissionPageGate requiredPermissions={[MODULE_PERMISSION_VIEW]}>
|
|
||||||
<Container>
|
|
||||||
${table}
|
|
||||||
</Container>
|
|
||||||
</PermissionPageGate>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListPage;
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ListPageTemplateBuilder();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
class MenuTemplateBuilder {
|
|
||||||
build() {
|
|
||||||
const raw = `
|
|
||||||
import { NotepadText } from 'lucide-react'
|
|
||||||
import { IntlShape } from 'react-intl'
|
|
||||||
import { MenuItem } from '#app/config/types'
|
|
||||||
import { MODULE_NAME_LABEL, MODULE_PERMISSION_VIEW, MODULE_ROOT_PATH } from './meta'
|
|
||||||
|
|
||||||
export default (intl: IntlShape): MenuItem => ({
|
|
||||||
title: intl.formatMessage({
|
|
||||||
id: \`MODULES.\${MODULE_NAME_LABEL.toUpperCase()}.TITLE\`,
|
|
||||||
defaultMessage: MODULE_NAME_LABEL,
|
|
||||||
}),
|
|
||||||
icon: NotepadText,
|
|
||||||
path: MODULE_ROOT_PATH,
|
|
||||||
permissions: [MODULE_PERMISSION_VIEW],
|
|
||||||
})
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new MenuTemplateBuilder();
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
class ModuleTemplateBuilder {
|
|
||||||
build(moduleId: string) {
|
|
||||||
const raw = `
|
|
||||||
import { AppModuleConfig } from '#app/services/modules/types';
|
|
||||||
import menu from './menu';
|
|
||||||
import messages from './messages';
|
|
||||||
import routes from './routing';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: '${moduleId}',
|
|
||||||
area: 'business',
|
|
||||||
routeScope: 'protected',
|
|
||||||
order: 100,
|
|
||||||
routes,
|
|
||||||
menu,
|
|
||||||
messages,
|
|
||||||
} satisfies AppModuleConfig;
|
|
||||||
`;
|
|
||||||
|
|
||||||
return raw.replace(/\{\s*'\s*'\s*\}/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ModuleTemplateBuilder();
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
type SourceChunk = string | null | false | undefined;
|
||||||
|
|
||||||
|
const EMPTY_JSX_TEXT_EXPRESSION_REGEX = /\{\s*'\s*'\s*\}/g;
|
||||||
|
|
||||||
|
function normalizeSource(value: string): string {
|
||||||
|
const source = value.replace(/^\n/, '').replace(/\n\s*$/, '');
|
||||||
|
const lines = source.split('\n');
|
||||||
|
const indents = lines
|
||||||
|
.filter((line) => line.trim().length > 0)
|
||||||
|
.map((line) => line.match(/^\s*/)?.[0].length ?? 0);
|
||||||
|
|
||||||
|
const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => line.slice(Math.min(line.match(/^\s*/)?.[0].length ?? 0, minIndent)))
|
||||||
|
.join('\n')
|
||||||
|
.replace(EMPTY_JSX_TEXT_EXPRESSION_REGEX, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function source(value: string): string {
|
||||||
|
return normalizeSource(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function indentSource(value: string, spaces: number): string {
|
||||||
|
const prefix = ' '.repeat(spaces);
|
||||||
|
|
||||||
|
return normalizeSource(value)
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => (line.length > 0 ? `${prefix}${line}` : line))
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SourceFile {
|
||||||
|
private readonly imports = new Set<string>();
|
||||||
|
private readonly sections: string[] = [];
|
||||||
|
|
||||||
|
addImport(value: SourceChunk): this {
|
||||||
|
if (value) {
|
||||||
|
this.imports.add(normalizeSource(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addImports(values: SourceChunk[]): this {
|
||||||
|
values.forEach((value) => this.addImport(value));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSection(value: SourceChunk): this {
|
||||||
|
if (value) {
|
||||||
|
this.sections.push(normalizeSource(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return [Array.from(this.imports).join('\n'), this.sections.join('\n\n')]
|
||||||
|
.filter((section) => section.length > 0)
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-15
@@ -1,10 +1,10 @@
|
|||||||
import { GenerateModulePayload } from '#devtools/data/api/dto';
|
import { GenerateModulePayload } from '#devtools/data/api/dto';
|
||||||
import DataTableTemplateBuilder from '#devtools/data/builders/DataTableTemplateBuilder';
|
import DataTableSourceBuilder from '#devtools/data/builders/DataTableSourceBuilder';
|
||||||
import FormDialogTemplateBuilder from '#devtools/data/builders/formDialogTemplateBuilder';
|
import FormSourceBuilder from '#devtools/data/builders/FormSourceBuilder';
|
||||||
import FormTemplateBuilder from '#devtools/data/builders/FormTemplateBuilder';
|
import ListPageSourceBuilder from '#devtools/data/builders/ListPageSourceBuilder';
|
||||||
import ListPageTemplateBuilder from '#devtools/data/builders/ListPageTemplateBuilder';
|
import ManageTriggerSourceBuilder from '#devtools/data/builders/ManageTriggerSourceBuilder';
|
||||||
import MenuTemplateBuilder from '#devtools/data/builders/MenuTemplateBuilder';
|
import MenuSourceBuilder from '#devtools/data/builders/MenuSourceBuilder';
|
||||||
import ModuleTemplateBuilder from '#devtools/data/builders/ModuleTemplateBuilder';
|
import ModuleConfigSourceBuilder from '#devtools/data/builders/ModuleConfigSourceBuilder';
|
||||||
import { ModuleAsset } from '#devtools/entities/ModuleAsset';
|
import { ModuleAsset } from '#devtools/entities/ModuleAsset';
|
||||||
import { ModuleGeneratorConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
import { ModuleGeneratorConfig } from '#devtools/entities/ModuleGeneratorTypes';
|
||||||
|
|
||||||
@@ -18,14 +18,12 @@ export default function buildGenerateModulePayload(
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
table: values.table,
|
table: values.table,
|
||||||
generate: values.generate,
|
generate: values.generate,
|
||||||
form: shouldGenerateForm ? FormTemplateBuilder.buildComponent(values.form) : null,
|
form: shouldGenerateForm ? FormSourceBuilder.buildComponent(values.form) : null,
|
||||||
formDefinition: shouldGenerateForm ? FormTemplateBuilder.buildDefinition(values.form) : null,
|
formDefinition: shouldGenerateForm ? FormSourceBuilder.buildDefinition(values.form) : null,
|
||||||
dataTable: shouldGenerateDataTable ? DataTableTemplateBuilder.build(values) : null,
|
dataTable: shouldGenerateDataTable ? DataTableSourceBuilder.build(values) : null,
|
||||||
list: ListPageTemplateBuilder.build(values.datatable, {
|
list: ListPageSourceBuilder.build(values.datatable, { addTable: shouldGenerateDataTable }),
|
||||||
addTable: shouldGenerateDataTable
|
formDialog: shouldGenerateForm ? ManageTriggerSourceBuilder.build(values.form.size) : null,
|
||||||
}),
|
menu: shouldGenerateDataTable ? MenuSourceBuilder.build() : null,
|
||||||
formDialog: shouldGenerateForm ? FormDialogTemplateBuilder.build(values.form.size) : null,
|
module: ModuleConfigSourceBuilder.build(values.name)
|
||||||
menu: shouldGenerateDataTable ? MenuTemplateBuilder.build() : null,
|
|
||||||
module: ModuleTemplateBuilder.build(values.name)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ Table and column requests are cached through React Query. Column queries remain
|
|||||||
`buildGenerateModulePayload` delegates source generation to focused builders:
|
`buildGenerateModulePayload` delegates source generation to focused builders:
|
||||||
|
|
||||||
| Builder | Payload field | Generated destination |
|
| Builder | Payload field | Generated destination |
|
||||||
| ------------------------------------- | ---------------- | ----------------------------------- |
|
| ----------------------------------- | ---------------- | ----------------------------------- |
|
||||||
| `FormTemplateBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
|
| `FormSourceBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
|
||||||
| `FormTemplateBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
|
| `FormSourceBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
|
||||||
| `FormDialogTemplateBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
|
| `ManageTriggerSourceBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
|
||||||
| `DataTableTemplateBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
|
| `DataTableSourceBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
|
||||||
| `ListPageTemplateBuilder` | `list` | `pages/ListPage/index.tsx` |
|
| `ListPageSourceBuilder` | `list` | `pages/ListPage/index.tsx` |
|
||||||
| `MenuTemplateBuilder` | `menu` | `config/menu.ts` |
|
| `MenuSourceBuilder` | `menu` | `config/menu.ts` |
|
||||||
| `ModuleTemplateBuilder` | `module` | `config/module.ts` |
|
| `ModuleConfigSourceBuilder` | `module` | `config/module.ts` |
|
||||||
|
|
||||||
If form generation is disabled, `form`, `formDefinition`, and `formDialog` are `null`. If datatable generation is disabled, `dataTable` and `menu` are `null`, while the list-page builder emits a page without a table.
|
If form generation is disabled, `form`, `formDefinition`, and `formDialog` are `null`. If datatable generation is disabled, `dataTable` and `menu` are `null`, while the list-page builder emits a page without a table.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user