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.
@@ -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<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>
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<td><input type="checkbox" class="checkbox checkbox-xs" /></td>
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },