appflare 0.2.7 → 0.2.8
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/cli/templates/dashboard/builders/table-routes/table/get-route.ts +132 -7
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +3 -0
- package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +52 -0
- package/cli/templates/dashboard/index.ts +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { DiscoveredTable } from "../../../types";
|
|
2
|
-
import { buildColumnHeaders, buildRowCells } from "../fragments";
|
|
3
2
|
import { buildPaginationHtml } from "../common/pagination";
|
|
4
3
|
import { buildSearchBarHtml } from "../common/search-bar";
|
|
5
|
-
import { buildActionsCell } from "./actions-cell";
|
|
6
|
-
import { resolvePrimaryKey } from "../helpers";
|
|
7
|
-
import { buildFieldInput, buildSearchConditions } from "../fragments";
|
|
8
|
-
import { shouldIncludeEditField, shouldIncludeCreateField } from "../helpers";
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Builds the GET route handler code string for /admin/table/:tableName
|
|
@@ -14,6 +9,8 @@ import { shouldIncludeEditField, shouldIncludeCreateField } from "../helpers";
|
|
|
14
9
|
export function buildTableGetRoute(
|
|
15
10
|
table: DiscoveredTable,
|
|
16
11
|
defaultSort: string,
|
|
12
|
+
primaryKey: string,
|
|
13
|
+
hasPrimaryKey: boolean,
|
|
17
14
|
columns: string[],
|
|
18
15
|
searchConditions: string,
|
|
19
16
|
headers: string,
|
|
@@ -28,6 +25,133 @@ export function buildTableGetRoute(
|
|
|
28
25
|
`/admin/table/${table.exportName}`,
|
|
29
26
|
"Search term or filter...",
|
|
30
27
|
);
|
|
28
|
+
const headerSelectionCell = hasPrimaryKey
|
|
29
|
+
? `<th class="w-10"><input id="select-all-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" /></th>`
|
|
30
|
+
: `<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>`;
|
|
31
|
+
const rowSelectionCell = hasPrimaryKey
|
|
32
|
+
? `<td><input type="checkbox" class="checkbox checkbox-xs row-select-checkbox" value="\${String((row as any).${primaryKey} ?? '')}" /></td>`
|
|
33
|
+
: `<td><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></td>`;
|
|
34
|
+
const bulkDeleteUi = hasPrimaryKey
|
|
35
|
+
? `
|
|
36
|
+
\t\t\t\t\t\t<div id="bulk-delete-bar-${table.exportName}" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 hidden">
|
|
37
|
+
\t\t\t\t\t\t\t<div class="bg-base-100 border border-base-200 rounded-xl shadow-lg px-3 py-2 flex items-center gap-3">
|
|
38
|
+
\t\t\t\t\t\t\t\t<div class="text-xs text-base-content/70">
|
|
39
|
+
\t\t\t\t\t\t\t\t\t<span id="bulk-selected-count-${table.exportName}" class="font-medium text-base-content">0</span> selected
|
|
40
|
+
\t\t\t\t\t\t\t\t</div>
|
|
41
|
+
\t\t\t\t\t\t\t\t<label class="label cursor-pointer gap-2 py-0 px-1">
|
|
42
|
+
\t\t\t\t\t\t\t\t\t<span class="label-text text-xs">All matching (\${total})</span>
|
|
43
|
+
\t\t\t\t\t\t\t\t\t<input id="bulk-all-matching-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" />
|
|
44
|
+
\t\t\t\t\t\t\t\t</label>
|
|
45
|
+
\t\t\t\t\t\t\t\t<button id="bulk-delete-trigger-${table.exportName}" type="button" class="btn btn-error btn-xs gap-1">
|
|
46
|
+
\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:delete-outline" width="14" height="14"></iconify-icon>
|
|
47
|
+
\t\t\t\t\t\t\t\t\tDelete selected
|
|
48
|
+
\t\t\t\t\t\t\t\t</button>
|
|
49
|
+
\t\t\t\t\t\t\t</div>
|
|
50
|
+
\t\t\t\t\t\t</div>
|
|
51
|
+
|
|
52
|
+
\t\t\t\t\t\t<dialog id="bulk-delete-modal-${table.exportName}" class="modal">
|
|
53
|
+
\t\t\t\t\t\t\t<div class="modal-box max-w-sm p-6 space-y-4">
|
|
54
|
+
\t\t\t\t\t\t\t\t<h3 class="font-semibold text-base">Delete selected rows?</h3>
|
|
55
|
+
\t\t\t\t\t\t\t\t<p class="text-sm text-base-content/70" id="bulk-delete-description-${table.exportName}">This action cannot be undone.</p>
|
|
56
|
+
\t\t\t\t\t\t\t\t<form id="bulk-delete-form-${table.exportName}" hx-post="/admin/table/${table.exportName}/delete-bulk" hx-target="#main-content" hx-swap="outerHTML" class="space-y-3">
|
|
57
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="bulkMode" id="bulk-delete-mode-${table.exportName}" value="selected" />
|
|
58
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="selectedIds" id="bulk-delete-ids-${table.exportName}" value="" />
|
|
59
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
|
|
60
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
|
|
61
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
|
|
62
|
+
\t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
|
|
63
|
+
\t\t\t\t\t\t\t\t\t<div class="flex justify-end gap-2 pt-1">
|
|
64
|
+
\t\t\t\t\t\t\t\t\t\t<button type="button" class="btn btn-ghost btn-sm" onclick="this.closest('dialog')?.close()">Cancel</button>
|
|
65
|
+
\t\t\t\t\t\t\t\t\t\t<button type="submit" class="btn btn-error btn-sm">Delete</button>
|
|
66
|
+
\t\t\t\t\t\t\t\t\t</div>
|
|
67
|
+
\t\t\t\t\t\t\t\t</form>
|
|
68
|
+
\t\t\t\t\t\t\t</div>
|
|
69
|
+
\t\t\t\t\t\t\t<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
70
|
+
\t\t\t\t\t\t</dialog>
|
|
71
|
+
|
|
72
|
+
\t\t\t\t\t\t<script>
|
|
73
|
+
\t\t\t\t\t\t\t(() => {
|
|
74
|
+
\t\t\t\t\t\t\t\tconst container = document.querySelector('#main-content');
|
|
75
|
+
\t\t\t\t\t\t\t\tif (!container) return;
|
|
76
|
+
|
|
77
|
+
\t\t\t\t\t\t\t\tconst rowCheckboxes = Array.from(container.querySelectorAll('.row-select-checkbox')).filter((node) => node instanceof HTMLInputElement);
|
|
78
|
+
\t\t\t\t\t\t\t\tif (rowCheckboxes.length === 0) return;
|
|
79
|
+
|
|
80
|
+
\t\t\t\t\t\t\t\tconst selectAll = container.querySelector('#select-all-${table.exportName}');
|
|
81
|
+
\t\t\t\t\t\t\t\tconst bar = container.querySelector('#bulk-delete-bar-${table.exportName}');
|
|
82
|
+
\t\t\t\t\t\t\t\tconst selectedCount = container.querySelector('#bulk-selected-count-${table.exportName}');
|
|
83
|
+
\t\t\t\t\t\t\t\tconst allMatching = container.querySelector('#bulk-all-matching-${table.exportName}');
|
|
84
|
+
\t\t\t\t\t\t\t\tconst trigger = container.querySelector('#bulk-delete-trigger-${table.exportName}');
|
|
85
|
+
\t\t\t\t\t\t\t\tconst modal = container.querySelector('#bulk-delete-modal-${table.exportName}');
|
|
86
|
+
\t\t\t\t\t\t\t\tconst modeInput = container.querySelector('#bulk-delete-mode-${table.exportName}');
|
|
87
|
+
\t\t\t\t\t\t\t\tconst idsInput = container.querySelector('#bulk-delete-ids-${table.exportName}');
|
|
88
|
+
\t\t\t\t\t\t\t\tconst description = container.querySelector('#bulk-delete-description-${table.exportName}');
|
|
89
|
+
|
|
90
|
+
\t\t\t\t\t\t\t\tif (!(selectAll instanceof HTMLInputElement) ||
|
|
91
|
+
\t\t\t\t\t\t\t\t\t!(bar instanceof HTMLElement) ||
|
|
92
|
+
\t\t\t\t\t\t\t\t\t!(selectedCount instanceof HTMLElement) ||
|
|
93
|
+
\t\t\t\t\t\t\t\t\t!(allMatching instanceof HTMLInputElement) ||
|
|
94
|
+
\t\t\t\t\t\t\t\t\t!(trigger instanceof HTMLButtonElement) ||
|
|
95
|
+
\t\t\t\t\t\t\t\t\t!(modal instanceof HTMLDialogElement) ||
|
|
96
|
+
\t\t\t\t\t\t\t\t\t!(modeInput instanceof HTMLInputElement) ||
|
|
97
|
+
\t\t\t\t\t\t\t\t\t!(idsInput instanceof HTMLInputElement) ||
|
|
98
|
+
\t\t\t\t\t\t\t\t\t!(description instanceof HTMLElement)) {
|
|
99
|
+
\t\t\t\t\t\t\t\t\treturn;
|
|
100
|
+
\t\t\t\t\t\t\t\t}
|
|
101
|
+
|
|
102
|
+
\t\t\t\t\t\t\t\tconst totalRows = Number(\${total});
|
|
103
|
+
\t\t\t\t\t\t\t\tconst getSelectedIds = () => rowCheckboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value).filter((value) => value.length > 0);
|
|
104
|
+
|
|
105
|
+
\t\t\t\t\t\t\t\tconst updateUi = () => {
|
|
106
|
+
\t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
|
|
107
|
+
\t\t\t\t\t\t\t\t\tconst selected = allMatching.checked ? totalRows : ids.length;
|
|
108
|
+
\t\t\t\t\t\t\t\t\tselectedCount.textContent = String(selected);
|
|
109
|
+
\t\t\t\t\t\t\t\t\tbar.classList.toggle('hidden', selected === 0);
|
|
110
|
+
\t\t\t\t\t\t\t\t\tconst checkedCount = ids.length;
|
|
111
|
+
\t\t\t\t\t\t\t\t\tselectAll.checked = checkedCount > 0 && checkedCount === rowCheckboxes.length;
|
|
112
|
+
\t\t\t\t\t\t\t\t\tselectAll.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
|
|
113
|
+
\t\t\t\t\t\t\t\t};
|
|
114
|
+
|
|
115
|
+
\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
|
|
116
|
+
\t\t\t\t\t\t\t\t\tcheckbox.addEventListener('change', () => {
|
|
117
|
+
\t\t\t\t\t\t\t\t\t\tif (allMatching.checked) allMatching.checked = false;
|
|
118
|
+
\t\t\t\t\t\t\t\t\t\tupdateUi();
|
|
119
|
+
\t\t\t\t\t\t\t\t\t});
|
|
120
|
+
\t\t\t\t\t\t\t\t});
|
|
121
|
+
|
|
122
|
+
\t\t\t\t\t\t\t\tselectAll.addEventListener('change', () => {
|
|
123
|
+
\t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
|
|
124
|
+
\t\t\t\t\t\t\t\t\t\tcheckbox.checked = selectAll.checked;
|
|
125
|
+
\t\t\t\t\t\t\t\t\t});
|
|
126
|
+
\t\t\t\t\t\t\t\t\tif (allMatching.checked && !selectAll.checked) allMatching.checked = false;
|
|
127
|
+
\t\t\t\t\t\t\t\t\tupdateUi();
|
|
128
|
+
\t\t\t\t\t\t\t\t});
|
|
129
|
+
|
|
130
|
+
\t\t\t\t\t\t\t\tallMatching.addEventListener('change', () => {
|
|
131
|
+
\t\t\t\t\t\t\t\t\tif (allMatching.checked) {
|
|
132
|
+
\t\t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
|
|
133
|
+
\t\t\t\t\t\t\t\t\t\t\tcheckbox.checked = true;
|
|
134
|
+
\t\t\t\t\t\t\t\t\t\t});
|
|
135
|
+
\t\t\t\t\t\t\t\t\t}
|
|
136
|
+
\t\t\t\t\t\t\t\t\tupdateUi();
|
|
137
|
+
\t\t\t\t\t\t\t\t});
|
|
138
|
+
|
|
139
|
+
\t\t\t\t\t\t\t\ttrigger.addEventListener('click', () => {
|
|
140
|
+
\t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
|
|
141
|
+
\t\t\t\t\t\t\t\t\tif (!allMatching.checked && ids.length === 0) return;
|
|
142
|
+
\t\t\t\t\t\t\t\t\tmodeInput.value = allMatching.checked ? 'all-matching' : 'selected';
|
|
143
|
+
\t\t\t\t\t\t\t\t\tidsInput.value = ids.join(',');
|
|
144
|
+
\t\t\t\t\t\t\t\t\tdescription.textContent = allMatching.checked
|
|
145
|
+
\t\t\t\t\t\t\t\t\t\t? 'This will permanently delete all rows matching the current search context.'
|
|
146
|
+
\t\t\t\t\t\t\t\t\t\t: 'This action cannot be undone.';
|
|
147
|
+
\t\t\t\t\t\t\t\t\tmodal.showModal();
|
|
148
|
+
\t\t\t\t\t\t\t\t});
|
|
149
|
+
|
|
150
|
+
\t\t\t\t\t\t\t\tupdateUi();
|
|
151
|
+
\t\t\t\t\t\t\t})();
|
|
152
|
+
\t\t\t\t\t\t</script>
|
|
153
|
+
`
|
|
154
|
+
: "";
|
|
31
155
|
|
|
32
156
|
return `
|
|
33
157
|
\tadminApp.get('/table/${table.exportName}', async (c) => {
|
|
@@ -69,7 +193,7 @@ export function buildTableGetRoute(
|
|
|
69
193
|
\t\t\t\t\t<table class="table table-sm md:table-md w-full">
|
|
70
194
|
\t\t\t\t\t\t<thead>
|
|
71
195
|
\t\t\t\t\t\t\t<tr class="border-b border-base-200">
|
|
72
|
-
\t\t\t\t\t\t\t\t
|
|
196
|
+
\t\t\t\t\t\t\t\t${headerSelectionCell}
|
|
73
197
|
\t\t\t\t\t\t\t\t${headers}
|
|
74
198
|
\t\t\t\t\t\t\t\t<th class="w-[100px] text-right">
|
|
75
199
|
\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:dots-horizontal" width="16" height="16" class="opacity-30"></iconify-icon>
|
|
@@ -79,7 +203,7 @@ export function buildTableGetRoute(
|
|
|
79
203
|
\t\t\t\t\t\t<tbody>
|
|
80
204
|
\t\t\t\t\t\t\t\${data.map((row, rowIndex) => html\`
|
|
81
205
|
\t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
|
|
82
|
-
\t\t\t\t\t\t\t\t
|
|
206
|
+
\t\t\t\t\t\t\t\t${rowSelectionCell}
|
|
83
207
|
\t\t\t\t\t\t\t\t${rowCells}
|
|
84
208
|
\t\t\t\t\t\t\t\t${actionsCell}
|
|
85
209
|
\t\t\t\t\t\t\t</tr>
|
|
@@ -130,6 +254,7 @@ export function buildTableGetRoute(
|
|
|
130
254
|
\t\t\t\t\t\t\t${searchBarHtml}
|
|
131
255
|
\t\t\t\t\t\t</div>
|
|
132
256
|
\t\t\t\t\t\t\${tableHtml}
|
|
257
|
+
\t\t\t\t\t\t${bulkDeleteUi}
|
|
133
258
|
\t\t\t\t\t</div>
|
|
134
259
|
\t\t\t\t\t<div class="drawer-side z-50">
|
|
135
260
|
\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" aria-label="close sidebar" class="drawer-overlay"></label>
|
|
@@ -56,6 +56,8 @@ export function buildTableRoute(table: DiscoveredTable): string {
|
|
|
56
56
|
buildTableGetRoute(
|
|
57
57
|
table,
|
|
58
58
|
defaultSort,
|
|
59
|
+
primaryKey,
|
|
60
|
+
hasPrimaryKey,
|
|
59
61
|
columns,
|
|
60
62
|
searchConditions,
|
|
61
63
|
headers,
|
|
@@ -70,6 +72,7 @@ export function buildTableRoute(table: DiscoveredTable): string {
|
|
|
70
72
|
primaryKey,
|
|
71
73
|
primaryKeyType,
|
|
72
74
|
hasPrimaryKey,
|
|
75
|
+
searchConditions,
|
|
73
76
|
createAssignments,
|
|
74
77
|
editAssignments,
|
|
75
78
|
)
|
|
@@ -8,6 +8,7 @@ export function buildTablePostRoutes(
|
|
|
8
8
|
primaryKey: string,
|
|
9
9
|
primaryKeyType: string | undefined,
|
|
10
10
|
hasPrimaryKey: boolean,
|
|
11
|
+
searchConditions: string,
|
|
11
12
|
createAssignments: string,
|
|
12
13
|
editAssignments: string,
|
|
13
14
|
): string {
|
|
@@ -20,6 +21,11 @@ export function buildTablePostRoutes(
|
|
|
20
21
|
\t\t`
|
|
21
22
|
: "";
|
|
22
23
|
|
|
24
|
+
const parseBulkIds =
|
|
25
|
+
primaryKeyType === "number"
|
|
26
|
+
? "idValues.map((value) => Number(value)).filter((value) => !Number.isNaN(value))"
|
|
27
|
+
: "idValues";
|
|
28
|
+
|
|
23
29
|
const editRoute = hasPrimaryKey
|
|
24
30
|
? `
|
|
25
31
|
\tadminApp.post('/table/${exportName}/edit', async (c) => {
|
|
@@ -74,6 +80,52 @@ export function buildTablePostRoutes(
|
|
|
74
80
|
\t\t\t.where(eq(tableSchema.${primaryKey}, idValue as any))
|
|
75
81
|
\t\t\t.execute();
|
|
76
82
|
|
|
83
|
+
\t\tconst query = new URLSearchParams({
|
|
84
|
+
\t\t\tpage: getValue(body.page) || '1',
|
|
85
|
+
\t\t\tsort: getValue(body.sort) || '${defaultSort}',
|
|
86
|
+
\t\t\torder: getValue(body.order) || 'desc',
|
|
87
|
+
\t\t\tsearch: getValue(body.search) || '',
|
|
88
|
+
\t\t});
|
|
89
|
+
\t\treturn c.redirect('/admin/table/${exportName}?' + query.toString());
|
|
90
|
+
\t});
|
|
91
|
+
|
|
92
|
+
\tadminApp.post('/table/${exportName}/delete-bulk', async (c) => {
|
|
93
|
+
\t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
|
|
94
|
+
\t\tconst tableSchema = (schema as any).${exportName};
|
|
95
|
+
\t\tif (!tableSchema) return c.text('Table missing', 404);
|
|
96
|
+
|
|
97
|
+
\t\tconst body = await c.req.parseBody();
|
|
98
|
+
\t\tconst getValue = (value: unknown) => (typeof value === 'string' ? value : '');
|
|
99
|
+
\t\tconst mode = getValue(body.bulkMode);
|
|
100
|
+
\t\tconst selectedIdsRaw = getValue(body.selectedIds);
|
|
101
|
+
\t\tconst search = getValue(body.search);
|
|
102
|
+
|
|
103
|
+
\t\tif (mode === 'all-matching') {
|
|
104
|
+
\t\t\tlet deleteQuery = db.delete(tableSchema);
|
|
105
|
+
\t\t\tif (search) {
|
|
106
|
+
\t\t\t\tconst searchConditions = [];
|
|
107
|
+
\t\t\t\t${searchConditions}
|
|
108
|
+
\t\t\t\tif (searchConditions.length > 0) {
|
|
109
|
+
\t\t\t\t\tdeleteQuery = deleteQuery.where(or(...searchConditions)) as any;
|
|
110
|
+
\t\t\t\t}
|
|
111
|
+
\t\t\t}
|
|
112
|
+
\t\t\tawait deleteQuery.execute();
|
|
113
|
+
\t\t} else {
|
|
114
|
+
\t\t\tconst idValues = selectedIdsRaw
|
|
115
|
+
\t\t\t\t.split(',')
|
|
116
|
+
\t\t\t\t.map((value) => value.trim())
|
|
117
|
+
\t\t\t\t.filter((value) => value.length > 0);
|
|
118
|
+
\t\t\tif (idValues.length === 0) return c.text('No rows selected', 400);
|
|
119
|
+
|
|
120
|
+
\t\t\tconst parsedIds: unknown[] = ${parseBulkIds};
|
|
121
|
+
\t\t\tif (parsedIds.length === 0) return c.text('No valid selected rows', 400);
|
|
122
|
+
|
|
123
|
+
\t\t\tawait db
|
|
124
|
+
\t\t\t\t.delete(tableSchema)
|
|
125
|
+
\t\t\t\t.where(inArray(tableSchema.${primaryKey}, parsedIds as any))
|
|
126
|
+
\t\t\t\t.execute();
|
|
127
|
+
\t\t}
|
|
128
|
+
|
|
77
129
|
\t\tconst query = new URLSearchParams({
|
|
78
130
|
\t\t\tpage: getValue(body.page) || '1',
|
|
79
131
|
\t\t\tsort: getValue(body.sort) || '${defaultSort}',
|
|
@@ -31,7 +31,7 @@ export function generateDashboardSource(
|
|
|
31
31
|
return `import { Hono } from "hono";
|
|
32
32
|
import { html, raw } from "hono/html";
|
|
33
33
|
import { drizzle } from "drizzle-orm/d1";
|
|
34
|
-
import { eq, desc, asc, sql, like, or } from "drizzle-orm";
|
|
34
|
+
import { eq, desc, asc, sql, like, or, inArray } from "drizzle-orm";
|
|
35
35
|
import { createAuth } from "./auth.config";
|
|
36
36
|
import * as schema from "${schemaImportPath}";
|
|
37
37
|
import { users } from "./auth.schema";
|