@sonicjs-cms/core 3.0.0-beta.20 → 3.0.0-beta.21

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.
Files changed (51) hide show
  1. package/dist/{chunk-RDH6PUOP.js → chunk-4LVVMVQI.js} +2 -2
  2. package/dist/{chunk-RDH6PUOP.js.map → chunk-4LVVMVQI.js.map} +1 -1
  3. package/dist/{chunk-WCA4VENG.js → chunk-4UYJPHSV.js} +5 -5
  4. package/dist/{chunk-WCA4VENG.js.map → chunk-4UYJPHSV.js.map} +1 -1
  5. package/dist/{chunk-DSXEVMOM.js → chunk-5U75XP74.js} +2 -2
  6. package/dist/{chunk-DSXEVMOM.js.map → chunk-5U75XP74.js.map} +1 -1
  7. package/dist/{chunk-DTGCXWWM.cjs → chunk-5UXU3LP4.cjs} +10 -10
  8. package/dist/{chunk-DTGCXWWM.cjs.map → chunk-5UXU3LP4.cjs.map} +1 -1
  9. package/dist/{chunk-GMZ5TGYP.js → chunk-7OTOXWEB.js} +3 -3
  10. package/dist/{chunk-GMZ5TGYP.js.map → chunk-7OTOXWEB.js.map} +1 -1
  11. package/dist/{chunk-3E7IROND.cjs → chunk-7RUW244J.cjs} +217 -208
  12. package/dist/chunk-7RUW244J.cjs.map +1 -0
  13. package/dist/{chunk-5RSB5SEK.cjs → chunk-A4KUJ3LD.cjs} +4 -4
  14. package/dist/{chunk-5RSB5SEK.cjs.map → chunk-A4KUJ3LD.cjs.map} +1 -1
  15. package/dist/{chunk-AQJEMVAY.cjs → chunk-AOUVUF32.cjs} +3 -3
  16. package/dist/{chunk-AQJEMVAY.cjs.map → chunk-AOUVUF32.cjs.map} +1 -1
  17. package/dist/{chunk-4SHNHNRP.cjs → chunk-FCLKX2OS.cjs} +15 -15
  18. package/dist/{chunk-4SHNHNRP.cjs.map → chunk-FCLKX2OS.cjs.map} +1 -1
  19. package/dist/{chunk-MYQ7KZUQ.js → chunk-GICS5AIO.js} +4 -4
  20. package/dist/{chunk-MYQ7KZUQ.js.map → chunk-GICS5AIO.js.map} +1 -1
  21. package/dist/{chunk-W53J3BJ2.js → chunk-JEX3UPZL.js} +3 -3
  22. package/dist/{chunk-W53J3BJ2.js.map → chunk-JEX3UPZL.js.map} +1 -1
  23. package/dist/{chunk-5ARIHAGN.cjs → chunk-KVBHB7QP.cjs} +2 -2
  24. package/dist/{chunk-5ARIHAGN.cjs.map → chunk-KVBHB7QP.cjs.map} +1 -1
  25. package/dist/{chunk-TQG5FNNR.cjs → chunk-OJC4G5JE.cjs} +2 -2
  26. package/dist/{chunk-TQG5FNNR.cjs.map → chunk-OJC4G5JE.cjs.map} +1 -1
  27. package/dist/{chunk-5UCMLBRQ.js → chunk-ZP77LTN4.js} +24 -15
  28. package/dist/chunk-ZP77LTN4.js.map +1 -0
  29. package/dist/index.cjs +450 -321
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +204 -75
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.cjs +33 -33
  34. package/dist/middleware.js +4 -4
  35. package/dist/migrations-HMX5SPN2.js +4 -0
  36. package/dist/{migrations-HZJ6U6SX.js.map → migrations-HMX5SPN2.js.map} +1 -1
  37. package/dist/migrations-V73ARYV5.cjs +13 -0
  38. package/dist/{migrations-VQAFXMU7.cjs.map → migrations-V73ARYV5.cjs.map} +1 -1
  39. package/dist/plugins.cjs +40 -40
  40. package/dist/plugins.js +3 -3
  41. package/dist/routes.cjs +26 -26
  42. package/dist/routes.js +6 -6
  43. package/dist/services.cjs +18 -18
  44. package/dist/services.js +2 -2
  45. package/dist/utils.cjs +3 -3
  46. package/dist/utils.js +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-3E7IROND.cjs.map +0 -1
  49. package/dist/chunk-5UCMLBRQ.js.map +0 -1
  50. package/dist/migrations-HZJ6U6SX.js +0 -4
  51. package/dist/migrations-VQAFXMU7.cjs +0 -13
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createAuth } from './chunk-SOZUG52O.js';
2
- import { getEmailService, hasEmailService, setEmailService, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-5UCMLBRQ.js';
3
- export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-5UCMLBRQ.js';
2
+ import { getEmailService, hasEmailService, setEmailService, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-ZP77LTN4.js';
3
+ export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-ZP77LTN4.js';
4
4
  import './chunk-VVYVFCML.js';
5
5
  import './chunk-RMRJGMDE.js';
6
6
  import { SettingsService, setAppInstance } from './chunk-2XAQTPAE.js';
@@ -8,30 +8,30 @@ export { Logger, getLogger, initLogger } from './chunk-2XAQTPAE.js';
8
8
  import { getAllCacheStats, CACHE_CONFIGS, getCacheService, clearAllCaches, parseCacheKey } from './chunk-K25XHMM3.js';
9
9
  import { schema_exports } from './chunk-AI663NBO.js';
10
10
  export { apiTokens, content, contentVersions, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-AI663NBO.js';
11
- import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-WCA4VENG.js';
12
- export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-WCA4VENG.js';
11
+ import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-4UYJPHSV.js';
12
+ export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-4UYJPHSV.js';
13
13
  import { RbacService, DocumentsService } from './chunk-3TMTYJAJ.js';
14
14
  export { DocumentsService, RbacService } from './chunk-3TMTYJAJ.js';
15
15
  import { nanoid } from './chunk-UWUP2VMT.js';
16
- import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-RDH6PUOP.js';
17
- export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-RDH6PUOP.js';
18
- export { MigrationService } from './chunk-DSXEVMOM.js';
16
+ import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-4LVVMVQI.js';
17
+ export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-4LVVMVQI.js';
18
+ export { MigrationService } from './chunk-5U75XP74.js';
19
19
  export { renderFilterBar } from './chunk-JQISFW6U.js';
20
20
  import { renderAdminLayout } from './chunk-OBA2RYZN.js';
21
21
  export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-OBA2RYZN.js';
22
22
  import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-5V62WT6M.js';
23
23
  export { renderAdminLayoutCatalyst } from './chunk-5V62WT6M.js';
24
- import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes, setPluginMenu, resolvePluginMenuItems } from './chunk-MYQ7KZUQ.js';
25
- export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick, getPluginMenu, registerPlugins, resetPluginMenu, resolvePluginMenuItems, setPluginMenu, topoSort } from './chunk-MYQ7KZUQ.js';
26
- import { definePlugin, setPluginDefinitions } from './chunk-GMZ5TGYP.js';
27
- export { applySchemaDefaults, definePlugin, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields } from './chunk-GMZ5TGYP.js';
24
+ import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes, setPluginMenu, resolvePluginMenuItems } from './chunk-GICS5AIO.js';
25
+ export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick, getPluginMenu, registerPlugins, resetPluginMenu, resolvePluginMenuItems, setPluginMenu, topoSort } from './chunk-GICS5AIO.js';
26
+ import { definePlugin, setPluginDefinitions } from './chunk-7OTOXWEB.js';
27
+ export { applySchemaDefaults, definePlugin, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields } from './chunk-7OTOXWEB.js';
28
28
  import { setHookSystem } from './chunk-RNZFGN4R.js';
29
29
  export { getHookSystem, getTypedHooks, hasHookSystem, resetHookSystem, setHookSystem } from './chunk-RNZFGN4R.js';
30
30
  export { QueryFilterBuilder, TemplateRenderer, buildQuery, renderTemplate, templateRenderer } from './chunk-QZGABF2M.js';
31
31
  export { metricsTracker } from './chunk-FICTAGD4.js';
32
32
  import './chunk-X7ZAEI5S.js';
33
- import { getCoreVersion, package_default } from './chunk-W53J3BJ2.js';
34
- export { SONICJS_VERSION, getCoreVersion } from './chunk-W53J3BJ2.js';
33
+ import { getCoreVersion, package_default } from './chunk-JEX3UPZL.js';
34
+ export { SONICJS_VERSION, getCoreVersion } from './chunk-JEX3UPZL.js';
35
35
  import { escapeHtml, sanitizeInput } from './chunk-TQABQWOP.js';
36
36
  export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
37
37
  export { HOOKS } from './chunk-QXOZI5Q2.js';
@@ -193,9 +193,9 @@ var DatabaseToolsService = class {
193
193
  }
194
194
  }
195
195
  /**
196
- * Get table data with optional pagination and sorting
196
+ * Get table data with optional pagination, sorting, and search
197
197
  */
198
- async getTableData(tableName, limit = 100, offset = 0, sortColumn, sortDirection = "asc") {
198
+ async getTableData(tableName, limit = 100, offset = 0, sortColumn, sortDirection = "asc", search) {
199
199
  try {
200
200
  const tables = await this.getTables();
201
201
  if (!tables.includes(tableName)) {
@@ -206,14 +206,26 @@ var DatabaseToolsService = class {
206
206
  if (sortColumn && !columns.includes(sortColumn)) {
207
207
  sortColumn = void 0;
208
208
  }
209
- const countResult = await this.db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).first();
209
+ const searchTerm = search?.trim();
210
+ let whereClause = "";
211
+ let bindings = [];
212
+ if (searchTerm) {
213
+ const conditions = columns.map(() => `CAST(? AS TEXT) IS NOT NULL AND CAST(? AS TEXT) LIKE ?`).join(" OR ");
214
+ const colConditions = columns.map((col) => `CAST("${col}" AS TEXT) LIKE ?`).join(" OR ");
215
+ whereClause = ` WHERE (${colConditions})`;
216
+ bindings = columns.map(() => `%${searchTerm}%`);
217
+ }
218
+ const countQuery = `SELECT COUNT(*) as count FROM "${tableName}"${whereClause}`;
219
+ const countStmt = this.db.prepare(countQuery);
220
+ const countResult = bindings.length > 0 ? await countStmt.bind(...bindings).first() : await countStmt.first();
210
221
  const totalRows = countResult?.count || 0;
211
- let query = `SELECT * FROM ${tableName}`;
222
+ let query = `SELECT * FROM "${tableName}"${whereClause}`;
212
223
  if (sortColumn) {
213
- query += ` ORDER BY ${sortColumn} ${sortDirection.toUpperCase()}`;
224
+ query += ` ORDER BY "${sortColumn}" ${sortDirection.toUpperCase()}`;
214
225
  }
215
226
  query += ` LIMIT ${limit} OFFSET ${offset}`;
216
- const dataResult = await this.db.prepare(query).all();
227
+ const dataStmt = this.db.prepare(query);
228
+ const dataResult = bindings.length > 0 ? await dataStmt.bind(...bindings).all() : await dataStmt.all();
217
229
  return {
218
230
  tableName,
219
231
  columns,
@@ -317,6 +329,43 @@ function renderDatabaseTablePage(data) {
317
329
  </div>
318
330
  </div>
319
331
 
332
+ <!-- Search bar -->
333
+ <div class="flex items-center gap-3">
334
+ <div class="relative flex-1 max-w-xl">
335
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
336
+ <svg class="h-4 w-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
337
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0"/>
338
+ </svg>
339
+ </div>
340
+ <input
341
+ id="searchInput"
342
+ type="text"
343
+ placeholder="Search all columns including JSON data..."
344
+ value="${escapeHtml2(data.search || "")}"
345
+ class="block w-full rounded-lg border-0 py-2.5 pl-10 pr-10 text-sm text-zinc-900 dark:text-white bg-white dark:bg-zinc-800 ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600"
346
+ onkeydown="if(event.key==='Enter') doSearch()"
347
+ />
348
+ ${data.search ? `
349
+ <button
350
+ onclick="clearSearch()"
351
+ class="absolute inset-y-0 right-0 flex items-center pr-3 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200"
352
+ title="Clear search"
353
+ >
354
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
355
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
356
+ </svg>
357
+ </button>
358
+ ` : ""}
359
+ </div>
360
+ <button
361
+ onclick="doSearch()"
362
+ class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-indigo-500 transition-colors shadow-sm"
363
+ >
364
+ Search
365
+ </button>
366
+ ${data.search ? `<span class="text-sm text-zinc-500 dark:text-zinc-400">Filtered: <strong class="text-zinc-900 dark:text-white">${escapeHtml2(data.search)}</strong></span>` : ""}
367
+ </div>
368
+
320
369
  <!-- Table Card -->
321
370
  <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
322
371
  <!-- Table -->
@@ -351,7 +400,7 @@ function renderDatabaseTablePage(data) {
351
400
  <tr class="${idx % 2 === 0 ? "bg-white dark:bg-zinc-900" : "bg-zinc-50 dark:bg-zinc-900/50"}">
352
401
  ${data.columns.map((col) => `
353
402
  <td class="px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300 whitespace-nowrap max-w-xs overflow-hidden text-ellipsis" title="${escapeHtml2(String(row[col] ?? ""))}">
354
- ${formatCellValue(row[col])}
403
+ ${formatCellValue(row[col], col)}
355
404
  </td>
356
405
  `).join("")}
357
406
  </tr>
@@ -427,79 +476,143 @@ function renderDatabaseTablePage(data) {
427
476
  </div>
428
477
  </div>
429
478
 
479
+ <!-- JSON Viewer Modal -->
480
+ <div id="jsonModal" class="fixed inset-0 z-50 hidden" role="dialog" aria-modal="true">
481
+ <div class="fixed inset-0 bg-black/60 backdrop-blur-sm" onclick="closeJsonModal()"></div>
482
+ <div class="fixed inset-0 flex items-center justify-center p-4 pointer-events-none">
483
+ <div class="pointer-events-auto relative w-full max-w-4xl max-h-[85vh] flex flex-col rounded-xl bg-white dark:bg-zinc-900 shadow-2xl ring-1 ring-zinc-950/10 dark:ring-white/10">
484
+ <div class="flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-700">
485
+ <div>
486
+ <h3 id="jsonModalTitle" class="text-base font-semibold text-zinc-900 dark:text-white">Field Value</h3>
487
+ <p id="jsonModalSubtitle" class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5"></p>
488
+ </div>
489
+ <div class="flex items-center gap-2">
490
+ <button onclick="copyJsonValue()" class="inline-flex items-center gap-1.5 rounded-lg bg-zinc-100 dark:bg-zinc-800 px-3 py-1.5 text-xs font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors">
491
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
492
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
493
+ </svg>
494
+ Copy
495
+ </button>
496
+ <button onclick="closeJsonModal()" class="rounded-lg p-1.5 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">
497
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
498
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
499
+ </svg>
500
+ </button>
501
+ </div>
502
+ </div>
503
+ <div class="flex-1 overflow-auto p-6">
504
+ <pre id="jsonModalContent" class="text-sm font-mono text-zinc-800 dark:text-zinc-200 whitespace-pre-wrap break-words leading-relaxed"></pre>
505
+ </div>
506
+ </div>
507
+ </div>
508
+ </div>
509
+
430
510
  <script>
431
511
  const currentTableName = '${data.tableName}';
432
512
  let currentPage = ${data.currentPage};
433
513
  let currentPageSize = ${data.pageSize};
434
514
  let currentSort = '${data.sortColumn || ""}';
435
515
  let currentSortDir = '${data.sortDirection || "asc"}';
516
+ let currentSearch = ${JSON.stringify(data.search || "")};
517
+
518
+ function buildParams(overrides = {}) {
519
+ const params = new URLSearchParams();
520
+ const p = { page: currentPage, pageSize: currentPageSize, sort: currentSort, dir: currentSortDir, search: currentSearch, ...overrides };
521
+ if (p.page) params.set('page', p.page);
522
+ if (p.pageSize) params.set('pageSize', p.pageSize);
523
+ if (p.sort) { params.set('sort', p.sort); params.set('dir', p.dir || 'asc'); }
524
+ if (p.search) params.set('search', p.search);
525
+ return params;
526
+ }
436
527
 
437
528
  function goToPage(page) {
438
529
  if (page < 1 || page > ${totalPages}) return;
439
- const params = new URLSearchParams();
440
- params.set('page', page);
441
- params.set('pageSize', currentPageSize);
442
- if (currentSort) {
443
- params.set('sort', currentSort);
444
- params.set('dir', currentSortDir);
445
- }
446
- window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
530
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${buildParams({ page })}\`;
447
531
  }
448
532
 
449
533
  function sortTable(column) {
450
- let newDir = 'asc';
451
- if (currentSort === column && currentSortDir === 'asc') {
452
- newDir = 'desc';
453
- }
454
-
455
- const params = new URLSearchParams();
456
- params.set('page', '1');
457
- params.set('pageSize', currentPageSize);
458
- params.set('sort', column);
459
- params.set('dir', newDir);
460
- window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
534
+ const newDir = (currentSort === column && currentSortDir === 'asc') ? 'desc' : 'asc';
535
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${buildParams({ page: 1, sort: column, dir: newDir })}\`;
461
536
  }
462
537
 
463
538
  function changePageSize(newSize) {
464
- const params = new URLSearchParams();
465
- params.set('page', '1');
466
- params.set('pageSize', newSize);
467
- if (currentSort) {
468
- params.set('sort', currentSort);
469
- params.set('dir', currentSortDir);
470
- }
471
- window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
539
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${buildParams({ page: 1, pageSize: newSize })}\`;
472
540
  }
473
541
 
474
542
  function refreshTableData() {
475
543
  window.location.reload();
476
544
  }
477
545
 
478
- function formatCellValue(value) {
479
- if (value === null || value === undefined) {
480
- return '<span class="text-zinc-400 dark:text-zinc-500 italic">null</span>';
481
- }
482
- if (typeof value === 'boolean') {
483
- return \`<span class="px-2 py-0.5 rounded text-xs font-medium \${value ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-400'}">\${value}</span>\`;
484
- }
485
- if (typeof value === 'object') {
486
- return '<span class="text-xs font-mono text-zinc-600 dark:text-zinc-400">' + JSON.stringify(value).substring(0, 50) + (JSON.stringify(value).length > 50 ? '...' : '') + '</span>';
487
- }
488
- const str = String(value);
489
- if (str.length > 100) {
490
- return escapeHtml(str.substring(0, 100)) + '...';
546
+ function doSearch() {
547
+ const val = document.getElementById('searchInput').value.trim();
548
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${buildParams({ page: 1, search: val })}\`;
549
+ }
550
+
551
+ function clearSearch() {
552
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${buildParams({ page: 1, search: '' })}\`;
553
+ }
554
+
555
+ // JSON viewer modal
556
+ let _jsonModalValue = '';
557
+
558
+ function openJsonModal(rawValue, colName) {
559
+ // rawValue comes from dataset \u2014 browser already decoded HTML entities, so it's the original string
560
+ _jsonModalValue = rawValue;
561
+ document.getElementById('jsonModalTitle').textContent = colName;
562
+ const el = document.getElementById('jsonModalContent');
563
+ const subtitle = document.getElementById('jsonModalSubtitle');
564
+ try {
565
+ const parsed = JSON.parse(rawValue);
566
+ const pretty = JSON.stringify(parsed, null, 2);
567
+ subtitle.textContent = 'JSON \u2022 ' + rawValue.length + ' chars';
568
+ el.innerHTML = syntaxHighlight(pretty);
569
+ } catch {
570
+ el.textContent = rawValue;
571
+ subtitle.textContent = rawValue.length + ' chars';
491
572
  }
492
- return escapeHtml(str);
573
+ document.getElementById('jsonModal').classList.remove('hidden');
574
+ document.body.style.overflow = 'hidden';
493
575
  }
494
576
 
577
+ function closeJsonModal() {
578
+ document.getElementById('jsonModal').classList.add('hidden');
579
+ document.body.style.overflow = '';
580
+ }
581
+
582
+ function copyJsonValue() {
583
+ navigator.clipboard.writeText(_jsonModalValue).then(() => {
584
+ const btn = event.currentTarget;
585
+ const orig = btn.innerHTML;
586
+ btn.textContent = 'Copied!';
587
+ setTimeout(() => { btn.innerHTML = orig; }, 1500);
588
+ });
589
+ }
590
+
591
+ function syntaxHighlight(json) {
592
+ json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
593
+ return json.replace(/("(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function(match) {
594
+ let cls = 'text-purple-600 dark:text-purple-400'; // number
595
+ if (/^"/.test(match)) {
596
+ if (/:$/.test(match)) {
597
+ cls = 'text-blue-600 dark:text-blue-400 font-medium'; // key
598
+ } else {
599
+ cls = 'text-green-700 dark:text-green-400'; // string value
600
+ }
601
+ } else if (/true|false/.test(match)) {
602
+ cls = 'text-orange-600 dark:text-orange-400';
603
+ } else if (/null/.test(match)) {
604
+ cls = 'text-zinc-400 italic';
605
+ }
606
+ return \`<span class="\${cls}">\${match}</span>\`;
607
+ });
608
+ }
609
+
610
+ document.addEventListener('keydown', (e) => {
611
+ if (e.key === 'Escape') closeJsonModal();
612
+ });
613
+
495
614
  function escapeHtml(text) {
496
- const map = {
497
- '&': '&amp;',
498
- '<': '&lt;',
499
- '>': '&gt;',
500
- '"': '&quot;',
501
- "'": '&#039;'
502
- };
615
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
503
616
  return String(text).replace(/[&<>"']/g, m => map[m]);
504
617
  }
505
618
  </script>
@@ -566,7 +679,16 @@ function escapeHtml2(text) {
566
679
  };
567
680
  return String(text).replace(/[&<>"']/g, (m) => map[m] || m);
568
681
  }
569
- function formatCellValue(value) {
682
+ function isJsonString(str2) {
683
+ const s = str2.trim();
684
+ return (s.startsWith("{") || s.startsWith("[")) && (s.endsWith("}") || s.endsWith("]"));
685
+ }
686
+ function jsonButton(raw, colName) {
687
+ const preview = raw.length > 60 ? escapeHtml2(raw.substring(0, 60)) + "\u2026" : escapeHtml2(raw);
688
+ return `<button data-json-val="${escapeHtml2(raw)}" data-col-name="${escapeHtml2(colName)}" onclick="openJsonModal(this.dataset.jsonVal, this.dataset.colName)" class="text-left text-xs font-mono text-indigo-600 dark:text-indigo-400 hover:underline">${preview}</button>`;
689
+ }
690
+ function formatCellValue(value, colName) {
691
+ const col = colName || "value";
570
692
  if (value === null || value === void 0) {
571
693
  return '<span class="text-zinc-400 dark:text-zinc-500 italic">null</span>';
572
694
  }
@@ -574,11 +696,15 @@ function formatCellValue(value) {
574
696
  return `<span class="px-2 py-0.5 rounded text-xs font-medium ${value ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" : "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-400"}">${value}</span>`;
575
697
  }
576
698
  if (typeof value === "object") {
577
- return '<span class="text-xs font-mono text-zinc-600 dark:text-zinc-400">' + JSON.stringify(value).substring(0, 50) + (JSON.stringify(value).length > 50 ? "..." : "") + "</span>";
699
+ return jsonButton(JSON.stringify(value), col);
578
700
  }
579
701
  const str2 = String(value);
580
- if (str2.length > 100) {
581
- return escapeHtml2(str2.substring(0, 100)) + "...";
702
+ if (isJsonString(str2)) {
703
+ return jsonButton(str2, col);
704
+ }
705
+ if (str2.length > 120) {
706
+ const preview = escapeHtml2(str2.substring(0, 120)) + "\u2026";
707
+ return `<button data-json-val="${escapeHtml2(str2)}" data-col-name="${escapeHtml2(col)}" onclick="openJsonModal(this.dataset.jsonVal, this.dataset.colName)" class="text-left text-sm text-zinc-700 dark:text-zinc-300 hover:underline">${preview}</button>`;
582
708
  }
583
709
  return escapeHtml2(str2);
584
710
  }
@@ -713,9 +839,10 @@ function createDatabaseToolsAdminRoutes() {
713
839
  const offset = parseInt(c.req.query("offset") || "0");
714
840
  const sortColumn = c.req.query("sort");
715
841
  const sortDirection = c.req.query("dir") || "asc";
842
+ const search = c.req.query("search");
716
843
  const db = c.env.DB;
717
844
  const service = new DatabaseToolsService(db);
718
- const tableData = await service.getTableData(tableName, limit, offset, sortColumn, sortDirection);
845
+ const tableData = await service.getTableData(tableName, limit, offset, sortColumn, sortDirection, search);
719
846
  return c.json({
720
847
  success: true,
721
848
  data: tableData
@@ -739,10 +866,11 @@ function createDatabaseToolsAdminRoutes() {
739
866
  const pageSize = parseInt(c.req.query("pageSize") || "20");
740
867
  const sortColumn = c.req.query("sort");
741
868
  const sortDirection = c.req.query("dir") || "asc";
869
+ const search = c.req.query("search");
742
870
  const offset = (page - 1) * pageSize;
743
871
  const db = c.env.DB;
744
872
  const service = new DatabaseToolsService(db);
745
- const tableData = await service.getTableData(tableName, pageSize, offset, sortColumn, sortDirection);
873
+ const tableData = await service.getTableData(tableName, pageSize, offset, sortColumn, sortDirection, search);
746
874
  const pageData = {
747
875
  user: {
748
876
  name: user.email.split("@")[0] || "Unknown",
@@ -756,7 +884,8 @@ function createDatabaseToolsAdminRoutes() {
756
884
  currentPage: page,
757
885
  pageSize,
758
886
  sortColumn,
759
- sortDirection
887
+ sortDirection,
888
+ search
760
889
  };
761
890
  return c.html(renderDatabaseTablePage(pageData));
762
891
  } catch (error) {