hiuhu-table-generator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gen-datatable.js +2 -0
- package/package.json +21 -0
- package/src/commands/gen-datatable.js +222 -0
- package/src/templates/DataFeature.tsx.tpl +25 -0
- package/src/templates/columnsFeature.tsx.tpl +29 -0
- package/src/templates/feature-datatable.slice.ts.tpl +63 -0
- package/src/templates/feature.service.ts.tpl +9 -0
- package/src/templates/slice-index.ts.tpl +3 -0
- package/src/templates/useDataTableFeature.ts.tpl +72 -0
- package/src/templates/useGetFeature.ts.tpl +9 -0
- package/src/utils/formatter.js +22 -0
- package/src/utils/injectAfter.js +17 -0
- package/src/utils/loadGeneratorConfig.js +97 -0
- package/src/utils/validateDatatable.js +71 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hiuhu-table-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI generator for table feature in Next.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gen-datatable": "./bin/gen-datatable.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"hiuhu",
|
|
15
|
+
"generator",
|
|
16
|
+
"table",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"author": "HiuHu",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
import { injectAfter } from "../utils/injectAfter.js";
|
|
6
|
+
import { loadGeneratorConfig } from "../utils/loadGeneratorConfig.js";
|
|
7
|
+
import {
|
|
8
|
+
toCamelCase,
|
|
9
|
+
toPascalCase,
|
|
10
|
+
toSnakeCase,
|
|
11
|
+
removeExtension,
|
|
12
|
+
} from "../utils/formatter.js";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
/* =======================
|
|
18
|
+
LOAD & VALIDATE CONFIG
|
|
19
|
+
======================= */
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = loadGeneratorConfig();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(err.message);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
sharedTypes,
|
|
30
|
+
sharedTypesAlias,
|
|
31
|
+
features,
|
|
32
|
+
featuresAlias,
|
|
33
|
+
redux,
|
|
34
|
+
reduxAlias,
|
|
35
|
+
reduxStore,
|
|
36
|
+
reduxHook,
|
|
37
|
+
} = config.paths;
|
|
38
|
+
const {
|
|
39
|
+
components: { table, columnHeader },
|
|
40
|
+
types: { state, redux: typesRedux },
|
|
41
|
+
} = config.datatable;
|
|
42
|
+
|
|
43
|
+
const datatableImports = {
|
|
44
|
+
table: buildImport(table),
|
|
45
|
+
columnHeader: buildImport(columnHeader),
|
|
46
|
+
state: buildImport(state),
|
|
47
|
+
redux: buildImport(typesRedux),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const REDUX_STORE_FILE_PATH = path.resolve(process.cwd(), redux, reduxStore);
|
|
51
|
+
const REDUX_HOOK_FILE_ALIAS = `${reduxAlias}/${reduxHook}`;
|
|
52
|
+
|
|
53
|
+
const rawFeature = process.argv[2];
|
|
54
|
+
const rawTableKey = process.argv[3];
|
|
55
|
+
const tableKeyPascal = toPascalCase(rawTableKey);
|
|
56
|
+
const tableKeySnake = toSnakeCase(rawTableKey);
|
|
57
|
+
const feature = toSnakeCase(rawFeature);
|
|
58
|
+
const Feature = toPascalCase(feature + tableKeyPascal);
|
|
59
|
+
const featureVar = toCamelCase(feature + tableKeyPascal);
|
|
60
|
+
const endpoint = process.argv[4];
|
|
61
|
+
const typeName = process.argv[5];
|
|
62
|
+
|
|
63
|
+
if (!feature || !tableKeySnake || !endpoint || !typeName) {
|
|
64
|
+
console.error(
|
|
65
|
+
"Usage: gen:datatable <feature> <tableKey> <endpoint> <typeName>",
|
|
66
|
+
);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const TYPE_BASE_PATH = path.resolve(process.cwd(), sharedTypes);
|
|
71
|
+
|
|
72
|
+
const possibleTypePaths = [
|
|
73
|
+
path.join(TYPE_BASE_PATH, `${typeName}.ts`),
|
|
74
|
+
path.join(TYPE_BASE_PATH, `${typeName.toLowerCase()}.ts`),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const typeExists = possibleTypePaths.some(fs.existsSync);
|
|
78
|
+
|
|
79
|
+
if (!typeExists) {
|
|
80
|
+
console.error(`❌ Type "${typeName}" not found in ${sharedTypes}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const replace = (tpl) =>
|
|
85
|
+
tpl
|
|
86
|
+
.replace(/{{datatableTableImport}}/g, datatableImports.table)
|
|
87
|
+
.replace(/{{datatableTable}}/g, table.component)
|
|
88
|
+
.replace(/{{datatableColumnHeaderImport}}/g, datatableImports.columnHeader)
|
|
89
|
+
.replace(/{{datatableColumnHeader}}/g, columnHeader.component)
|
|
90
|
+
.replace(/{{datatableStateImport}}/g, datatableImports.state)
|
|
91
|
+
.replace(/{{datatableState}}/g, state.name)
|
|
92
|
+
.replace(/{{datatableReduxImport}}/g, datatableImports.redux)
|
|
93
|
+
.replace(/{{datatableRedux}}/g, typesRedux.name)
|
|
94
|
+
.replace(/{{feature}}/g, feature)
|
|
95
|
+
.replace(/{{Feature}}/g, Feature)
|
|
96
|
+
.replace(/{{tablekey}}/g, tableKeySnake)
|
|
97
|
+
.replace(/{{featureVar}}/g, featureVar)
|
|
98
|
+
.replace(/{{Type}}/g, typeName)
|
|
99
|
+
.replace(/{{sharedTypesAlias}}/g, sharedTypesAlias)
|
|
100
|
+
.replace(/{{reduxHookFileAlias}}/g, removeExtension(REDUX_HOOK_FILE_ALIAS))
|
|
101
|
+
.replace(/{{endpoint}}/g, endpoint);
|
|
102
|
+
|
|
103
|
+
const basePath = `${features}/${feature}`;
|
|
104
|
+
|
|
105
|
+
const files = {
|
|
106
|
+
"components/DataFeature.tsx": "DataFeature.tsx.tpl",
|
|
107
|
+
"components/columnsFeature.tsx": "columnsFeature.tsx.tpl",
|
|
108
|
+
"hooks/table/useDataTableFeature.ts": "useDataTableFeature.ts.tpl",
|
|
109
|
+
"hooks/query/useGetFeature.ts": "useGetFeature.ts.tpl",
|
|
110
|
+
"services/feature.service.ts": "feature.service.ts.tpl",
|
|
111
|
+
"slices/feature-__tablekey__-datatable.slice.ts":
|
|
112
|
+
"feature-datatable.slice.ts.tpl",
|
|
113
|
+
"slices/index.ts": "slice-index.ts.tpl",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const reducerKey = `${featureVar}Datatable`;
|
|
117
|
+
const STORE_PATH = path.resolve(REDUX_STORE_FILE_PATH);
|
|
118
|
+
if (!fs.existsSync(STORE_PATH)) {
|
|
119
|
+
console.error("❌ Redux store file not found");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
injectAfter({
|
|
123
|
+
filePath: STORE_PATH,
|
|
124
|
+
marker: "// AUTO-GENERATED IMPORTS (DO NOT REMOVE)",
|
|
125
|
+
content: `import { ${featureVar}DatatableReducer } from "${featuresAlias}/${feature}/slices";`,
|
|
126
|
+
skipIfExists: `import { ${featureVar}DatatableReducer } from "${featuresAlias}/${feature}/slices";`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
injectAfter({
|
|
130
|
+
filePath: STORE_PATH,
|
|
131
|
+
marker: "// AUTO-GENERATED REDUCERS (DO NOT REMOVE)",
|
|
132
|
+
content: ` ${reducerKey}: ${featureVar}DatatableReducer,`,
|
|
133
|
+
skipIfExists: ` ${reducerKey}: ${featureVar}DatatableReducer,`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
injectAfter({
|
|
137
|
+
filePath: STORE_PATH,
|
|
138
|
+
marker: "// AUTO-GENERATED PERSIST (DO NOT REMOVE)",
|
|
139
|
+
content: ` "${reducerKey}",`,
|
|
140
|
+
skipIfExists: ` "${reducerKey}",`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Object.entries(files).forEach(([target, template]) => {
|
|
144
|
+
const filePath = path.join(
|
|
145
|
+
basePath,
|
|
146
|
+
target
|
|
147
|
+
.replace("Feature", Feature)
|
|
148
|
+
.replace("feature", feature)
|
|
149
|
+
.replace("__tablekey__", tableKeySnake),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
153
|
+
|
|
154
|
+
const tpl = fs.readFileSync(
|
|
155
|
+
path.join(__dirname, "templates", template),
|
|
156
|
+
"utf-8",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/* ======================
|
|
160
|
+
SERVICE FILE
|
|
161
|
+
====================== */
|
|
162
|
+
if (target === "services/feature.service.ts") {
|
|
163
|
+
if (!fs.existsSync(filePath)) {
|
|
164
|
+
fs.writeFileSync(filePath, replace(tpl));
|
|
165
|
+
} else {
|
|
166
|
+
injectAfter({
|
|
167
|
+
filePath,
|
|
168
|
+
marker: "// AUTO-GENERATED SERVICE FUNCTIONS (DO NOT REMOVE)",
|
|
169
|
+
content: replace(
|
|
170
|
+
`export const get{{Feature}} = ({ signal }: QueryFunctionContext) =>
|
|
171
|
+
apiGet<{{Type}}[]>("{{endpoint}}", { signal });`,
|
|
172
|
+
),
|
|
173
|
+
skipIfExists: `get${Feature}`,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
injectAfter({
|
|
177
|
+
filePath,
|
|
178
|
+
marker: "// AUTO-GENERATED SERVICE IMPORT (DO NOT REMOVE)",
|
|
179
|
+
content: replace(
|
|
180
|
+
`import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";`,
|
|
181
|
+
),
|
|
182
|
+
skipIfExists: `import { ${typeName} } from "${sharedTypesAlias}/${typeName}`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ======================
|
|
189
|
+
SLICE INDEX
|
|
190
|
+
====================== */
|
|
191
|
+
if (target === "slices/index.ts") {
|
|
192
|
+
if (!fs.existsSync(filePath)) {
|
|
193
|
+
fs.writeFileSync(filePath, replace(tpl));
|
|
194
|
+
} else {
|
|
195
|
+
injectAfter({
|
|
196
|
+
filePath,
|
|
197
|
+
marker: "// AUTO-GENERATED SLICE EXPORTS (DO NOT REMOVE)",
|
|
198
|
+
content:
|
|
199
|
+
replace(`export { default as {{featureVar}}DatatableReducer } from "./{{feature}}-{{tablekey}}-datatable.slice";
|
|
200
|
+
export * from "./{{feature}}-{{tablekey}}-datatable.slice";`),
|
|
201
|
+
skipIfExists: `export { default as ${featureVar}DatatableReducer } from "./${feature}-${tableKeySnake}-datatable.slice";
|
|
202
|
+
export * from "./${feature}-${tableKeySnake}-datatable.slice";`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ======================
|
|
209
|
+
DEFAULT (STRICT)
|
|
210
|
+
====================== */
|
|
211
|
+
if (fs.existsSync(filePath)) {
|
|
212
|
+
console.error(`❌ File already exists: ${filePath}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fs.writeFileSync(filePath, replace(tpl));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
function buildImport({ component, name, importPath }) {
|
|
220
|
+
const symbol = component || name;
|
|
221
|
+
return `import { ${symbol} } from "${importPath}";`;
|
|
222
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
{{datatableTableImport}}
|
|
4
|
+
import { create{{Feature}}Columns } from "./columns{{Feature}}";
|
|
5
|
+
import { useGet{{Feature}} } from "../hooks/query/useGet{{Feature}}";
|
|
6
|
+
import { useDataTable{{Feature}} } from "../hooks/table/useDataTable{{Feature}}";
|
|
7
|
+
|
|
8
|
+
const Data{{Feature}} = () => {
|
|
9
|
+
const { data, isLoading } = useGet{{Feature}}();
|
|
10
|
+
|
|
11
|
+
const tabel = useDataTable{{Feature}}();
|
|
12
|
+
|
|
13
|
+
const columns = useMemo(() => create{{Feature}}Columns(), []);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<{{datatableTable}}
|
|
17
|
+
{...tabel}
|
|
18
|
+
columns={columns}
|
|
19
|
+
data={data}
|
|
20
|
+
isLoading={isLoading}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default Data{{Feature}};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
{{datatableColumnHeaderImport}}
|
|
4
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
5
|
+
import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";
|
|
6
|
+
|
|
7
|
+
export const create{{Feature}}Columns = (): ColumnDef<{{Type}}>[] => [
|
|
8
|
+
{
|
|
9
|
+
accessorKey: "rowNumber",
|
|
10
|
+
id: "nomor_urut",
|
|
11
|
+
header: ({ column }) => <{{datatableColumnHeader}} column={column} title="#" />,
|
|
12
|
+
cell: ({ row, table }) => {
|
|
13
|
+
const filteredRows = table.getRowModel().rows;
|
|
14
|
+
const rowIndex = filteredRows.findIndex((r) => r.id === row.id);
|
|
15
|
+
const { pageIndex, pageSize } = table.getState().pagination;
|
|
16
|
+
const rowNumber = pageIndex * pageSize + rowIndex + 1;
|
|
17
|
+
return <div>{rowNumber}</div>;
|
|
18
|
+
},
|
|
19
|
+
size: 50,
|
|
20
|
+
enableHiding: true,
|
|
21
|
+
enableColumnFilter: false,
|
|
22
|
+
enableGlobalFilter: false,
|
|
23
|
+
enablePinning: false,
|
|
24
|
+
enableSorting: false,
|
|
25
|
+
enableGrouping: false,
|
|
26
|
+
enableMultiSort: false,
|
|
27
|
+
enableResizing: false,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{{datatableStateImport}}
|
|
2
|
+
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
3
|
+
import {
|
|
4
|
+
ColumnFiltersState,
|
|
5
|
+
SortingState,
|
|
6
|
+
RowSelectionState,
|
|
7
|
+
VisibilityState,
|
|
8
|
+
PaginationState,
|
|
9
|
+
ColumnPinningState,
|
|
10
|
+
} from "@tanstack/react-table";
|
|
11
|
+
|
|
12
|
+
const initialState: {{datatableState}} = {
|
|
13
|
+
pagination: { pageIndex: 0, pageSize: 10 },
|
|
14
|
+
sorting: [],
|
|
15
|
+
columnFilters: [],
|
|
16
|
+
globalFilter: "",
|
|
17
|
+
columnVisibility: {},
|
|
18
|
+
rowSelection: {},
|
|
19
|
+
columnPinning: {
|
|
20
|
+
left: ["select", "nomor_urut"],
|
|
21
|
+
right: ["actions"],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const {{featureVar}}DataTableSlice = createSlice({
|
|
26
|
+
name: "{{featureVar}}Datatable",
|
|
27
|
+
initialState,
|
|
28
|
+
reducers: {
|
|
29
|
+
setPagination{{Feature}}(state, action: PayloadAction<PaginationState>) {
|
|
30
|
+
state.pagination = action.payload;
|
|
31
|
+
},
|
|
32
|
+
setSorting{{Feature}}(state, action: PayloadAction<SortingState>) {
|
|
33
|
+
state.sorting = action.payload;
|
|
34
|
+
},
|
|
35
|
+
setColumnFilters{{Feature}}(state, action: PayloadAction<ColumnFiltersState>) {
|
|
36
|
+
state.columnFilters = action.payload;
|
|
37
|
+
},
|
|
38
|
+
setGlobalFilter{{Feature}}(state, action: PayloadAction<string>) {
|
|
39
|
+
state.globalFilter = action.payload;
|
|
40
|
+
},
|
|
41
|
+
setColumnVisibility{{Feature}}(state, action: PayloadAction<VisibilityState>) {
|
|
42
|
+
state.columnVisibility = action.payload;
|
|
43
|
+
},
|
|
44
|
+
setRowSelection{{Feature}}(state, action: PayloadAction<RowSelectionState>) {
|
|
45
|
+
state.rowSelection = action.payload;
|
|
46
|
+
},
|
|
47
|
+
setColumnPinning{{Feature}}(state, action: PayloadAction<ColumnPinningState>) {
|
|
48
|
+
state.columnPinning = action.payload;
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const {
|
|
54
|
+
setPagination{{Feature}},
|
|
55
|
+
setSorting{{Feature}},
|
|
56
|
+
setColumnFilters{{Feature}},
|
|
57
|
+
setGlobalFilter{{Feature}},
|
|
58
|
+
setColumnVisibility{{Feature}},
|
|
59
|
+
setRowSelection{{Feature}},
|
|
60
|
+
setColumnPinning{{Feature}},
|
|
61
|
+
} = {{featureVar}}DataTableSlice.actions;
|
|
62
|
+
|
|
63
|
+
export default {{featureVar}}DataTableSlice.reducer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { apiGet } from "@/frontend/lib/apiClient";
|
|
2
|
+
import { QueryFunctionContext } from "@tanstack/react-query";
|
|
3
|
+
// AUTO-GENERATED SERVICE IMPORT (DO NOT REMOVE)
|
|
4
|
+
import { {{Type}} } from "{{sharedTypesAlias}}/{{Type}}";
|
|
5
|
+
|
|
6
|
+
export const get{{Feature}} = ({ signal }: QueryFunctionContext) =>
|
|
7
|
+
apiGet<{{Type}}[]>("{{endpoint}}", { signal });
|
|
8
|
+
|
|
9
|
+
// AUTO-GENERATED SERVICE FUNCTIONS (DO NOT REMOVE)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ColumnFiltersState,
|
|
3
|
+
functionalUpdate,
|
|
4
|
+
SortingState,
|
|
5
|
+
Updater,
|
|
6
|
+
RowSelectionState,
|
|
7
|
+
VisibilityState,
|
|
8
|
+
PaginationState,
|
|
9
|
+
ColumnPinningState,
|
|
10
|
+
} from "@tanstack/react-table";
|
|
11
|
+
import {
|
|
12
|
+
setColumnFilters{{Feature}},
|
|
13
|
+
setColumnPinning{{Feature}},
|
|
14
|
+
setColumnVisibility{{Feature}},
|
|
15
|
+
setGlobalFilter{{Feature}},
|
|
16
|
+
setPagination{{Feature}},
|
|
17
|
+
setRowSelection{{Feature}},
|
|
18
|
+
setSorting{{Feature}},
|
|
19
|
+
} from "../../slices";
|
|
20
|
+
{{datatableReduxImport}}
|
|
21
|
+
import { useAppDispatch, useAppSelector } from "{{reduxHookFileAlias}}";
|
|
22
|
+
|
|
23
|
+
export function useDataTable{{Feature}}(): {{datatableRedux}} {
|
|
24
|
+
const dispatch = useAppDispatch();
|
|
25
|
+
const {
|
|
26
|
+
pagination,
|
|
27
|
+
sorting,
|
|
28
|
+
columnFilters,
|
|
29
|
+
globalFilter,
|
|
30
|
+
columnVisibility,
|
|
31
|
+
rowSelection,
|
|
32
|
+
columnPinning,
|
|
33
|
+
} = useAppSelector((state) => state.{{featureVar}}Datatable);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
pagination,
|
|
37
|
+
sorting,
|
|
38
|
+
columnFilters,
|
|
39
|
+
globalFilter,
|
|
40
|
+
columnVisibility,
|
|
41
|
+
rowSelection,
|
|
42
|
+
columnPinning,
|
|
43
|
+
|
|
44
|
+
onPaginationChange: (updater: Updater<PaginationState>) => {
|
|
45
|
+
const nextPagination = functionalUpdate(updater, pagination);
|
|
46
|
+
dispatch(setPagination{{Feature}}(nextPagination));
|
|
47
|
+
},
|
|
48
|
+
onSortingChange: (updater: Updater<SortingState>) => {
|
|
49
|
+
const nextSorting = functionalUpdate(updater, sorting);
|
|
50
|
+
dispatch(setSorting{{Feature}}(nextSorting));
|
|
51
|
+
},
|
|
52
|
+
onColumnFiltersChange: (updater: Updater<ColumnFiltersState>) => {
|
|
53
|
+
const nextFilters = functionalUpdate(updater, columnFilters);
|
|
54
|
+
dispatch(setColumnFilters{{Feature}}(nextFilters));
|
|
55
|
+
},
|
|
56
|
+
onGlobalFilterChange: (value: string) => {
|
|
57
|
+
dispatch(setGlobalFilter{{Feature}}(value));
|
|
58
|
+
},
|
|
59
|
+
onColumnVisibilityChange: (updater: Updater<VisibilityState>) => {
|
|
60
|
+
const nextVisibility = functionalUpdate(updater, columnVisibility);
|
|
61
|
+
dispatch(setColumnVisibility{{Feature}}(nextVisibility));
|
|
62
|
+
},
|
|
63
|
+
onRowSelectionChange: (updater: Updater<RowSelectionState>) => {
|
|
64
|
+
const nextSelection = functionalUpdate(updater, rowSelection);
|
|
65
|
+
dispatch(setRowSelection{{Feature}}(nextSelection));
|
|
66
|
+
},
|
|
67
|
+
onColumnPinningChange: (updater: Updater<ColumnPinningState>) => {
|
|
68
|
+
const nextPinning = functionalUpdate(updater, columnPinning);
|
|
69
|
+
dispatch(setColumnPinning{{Feature}}(nextPinning));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function toSnakeCase(str) {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
4
|
+
.replace(/[\s-]+/g, "_")
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function toPascalCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/[_\-\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
11
|
+
.replace(/^(.)/, (m) => m.toUpperCase());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function toCamelCase(str) {
|
|
15
|
+
const pascal = toPascalCase(str);
|
|
16
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function removeExtension(filename) {
|
|
20
|
+
const lastDot = filename.lastIndexOf(".");
|
|
21
|
+
return lastDot === -1 ? filename : filename.substring(0, lastDot);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
export function injectAfter({ filePath, marker, content, skipIfExists }) {
|
|
4
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
5
|
+
|
|
6
|
+
if (skipIfExists && source.includes(skipIfExists)) {
|
|
7
|
+
return; // aman, idempotent
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!source.includes(marker)) {
|
|
11
|
+
throw new Error(`Marker not found in ${filePath}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const updated = source.replace(marker, `${marker}\n${content}`);
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync(filePath, updated);
|
|
17
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { validateDatatable } from "./validateDatatable.js";
|
|
4
|
+
|
|
5
|
+
function assert(condition, message) {
|
|
6
|
+
if (!condition) {
|
|
7
|
+
throw new Error(message);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function hasExtension(filename) {
|
|
12
|
+
return path.extname(filename).length > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadGeneratorConfig() {
|
|
16
|
+
const CONFIG_PATH = path.resolve(process.cwd(), "generator.config.json");
|
|
17
|
+
|
|
18
|
+
assert(fs.existsSync(CONFIG_PATH), "❌ generator.config.json not found");
|
|
19
|
+
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
21
|
+
const paths = config.paths;
|
|
22
|
+
|
|
23
|
+
assert(paths, "❌ paths is missing in generator.config.json");
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
sharedTypes,
|
|
27
|
+
sharedTypesAlias,
|
|
28
|
+
features,
|
|
29
|
+
featuresAlias,
|
|
30
|
+
redux,
|
|
31
|
+
reduxAlias,
|
|
32
|
+
reduxStore,
|
|
33
|
+
reduxHook,
|
|
34
|
+
} = paths;
|
|
35
|
+
|
|
36
|
+
// 1️⃣ Required paths (HARUS ADA SEMUA)
|
|
37
|
+
const required = {
|
|
38
|
+
sharedTypes,
|
|
39
|
+
sharedTypesAlias,
|
|
40
|
+
features,
|
|
41
|
+
featuresAlias,
|
|
42
|
+
redux,
|
|
43
|
+
reduxAlias,
|
|
44
|
+
reduxStore,
|
|
45
|
+
reduxHook,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of Object.entries(required)) {
|
|
49
|
+
assert(
|
|
50
|
+
typeof value === "string" && value.trim().length > 0,
|
|
51
|
+
`❌ paths.${key} must be defined`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2️⃣ File name HARUS ada extension
|
|
56
|
+
assert(
|
|
57
|
+
hasExtension(reduxStore),
|
|
58
|
+
"❌ paths.reduxStore must include file extension (e.g. store.ts)"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
assert(
|
|
62
|
+
hasExtension(reduxHook),
|
|
63
|
+
"❌ paths.reduxHook must include file extension (e.g. hooks.ts)"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const datatable = config.datatable;
|
|
67
|
+
|
|
68
|
+
validateDatatable(datatable);
|
|
69
|
+
|
|
70
|
+
// 3️⃣ File HARUS exist
|
|
71
|
+
const reduxStorePath = path.resolve(process.cwd(), redux, reduxStore);
|
|
72
|
+
const reduxHookPath = path.resolve(process.cwd(), redux, reduxHook);
|
|
73
|
+
|
|
74
|
+
assert(
|
|
75
|
+
fs.existsSync(reduxStorePath),
|
|
76
|
+
`❌ Redux store file not found: ${reduxStorePath}`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
assert(
|
|
80
|
+
fs.existsSync(reduxHookPath),
|
|
81
|
+
`❌ Redux hook file not found: ${reduxHookPath}`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
paths: {
|
|
86
|
+
sharedTypes,
|
|
87
|
+
sharedTypesAlias,
|
|
88
|
+
features,
|
|
89
|
+
featuresAlias,
|
|
90
|
+
redux,
|
|
91
|
+
reduxAlias,
|
|
92
|
+
reduxStore,
|
|
93
|
+
reduxHook,
|
|
94
|
+
},
|
|
95
|
+
datatable,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
function validateEntry(entry, label, requiredKeys) {
|
|
5
|
+
if (!entry) {
|
|
6
|
+
throw new Error(`❌ datatable.${label} is required`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (const key of requiredKeys) {
|
|
10
|
+
if (!entry[key]) {
|
|
11
|
+
throw new Error(`❌ datatable.${label}.${key} is required`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const absPath = path.resolve(process.cwd(), entry.filePath);
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(absPath)) {
|
|
18
|
+
throw new Error(`❌ File not found: ${entry.filePath}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
22
|
+
|
|
23
|
+
const exportName = entry.component || entry.name;
|
|
24
|
+
if (exportName && !content.includes(exportName)) {
|
|
25
|
+
throw new Error(`❌ "${exportName}" is not exported in ${entry.filePath}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateDatatable(datatable) {
|
|
30
|
+
if (!datatable) {
|
|
31
|
+
throw new Error("❌ datatable config is required");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* =======================
|
|
35
|
+
COMPONENTS
|
|
36
|
+
======================= */
|
|
37
|
+
if (!datatable.components) {
|
|
38
|
+
throw new Error("❌ datatable.components is required");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
validateEntry(datatable.components.table, "components.table", [
|
|
42
|
+
"component",
|
|
43
|
+
"importPath",
|
|
44
|
+
"filePath",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
validateEntry(datatable.components.columnHeader, "components.columnHeader", [
|
|
48
|
+
"component",
|
|
49
|
+
"importPath",
|
|
50
|
+
"filePath",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/* =======================
|
|
54
|
+
TYPES
|
|
55
|
+
======================= */
|
|
56
|
+
if (!datatable.types) {
|
|
57
|
+
throw new Error("❌ datatable.types is required");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
validateEntry(datatable.types.state, "types.state", [
|
|
61
|
+
"name",
|
|
62
|
+
"importPath",
|
|
63
|
+
"filePath",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
validateEntry(datatable.types.redux, "types.redux", [
|
|
67
|
+
"name",
|
|
68
|
+
"importPath",
|
|
69
|
+
"filePath",
|
|
70
|
+
]);
|
|
71
|
+
}
|