@sonicjs-cms/core 2.4.0 → 2.6.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/dist/{app-Db0AfT5F.d.cts → app-DV27cjPy.d.cts} +1 -1
- package/dist/{app-Db0AfT5F.d.ts → app-DV27cjPy.d.ts} +1 -1
- package/dist/{chunk-YIXSSJWD.cjs → chunk-63K7XXRX.cjs} +5 -5
- package/dist/{chunk-YIXSSJWD.cjs.map → chunk-63K7XXRX.cjs.map} +1 -1
- package/dist/{chunk-VNCYCH3H.js → chunk-7DL5SPPX.js} +59 -5
- package/dist/chunk-7DL5SPPX.js.map +1 -0
- package/dist/{chunk-AZLU3ROK.cjs → chunk-BZC4FYW7.cjs} +4 -4
- package/dist/chunk-BZC4FYW7.cjs.map +1 -0
- package/dist/chunk-CLIH2T74.js +403 -0
- package/dist/chunk-CLIH2T74.js.map +1 -0
- package/dist/{chunk-D2NLCPO2.js → chunk-EVZOVYLO.js} +53 -2
- package/dist/chunk-EVZOVYLO.js.map +1 -0
- package/dist/{chunk-DXM575E2.js → chunk-EYWR6UA2.js} +6 -6
- package/dist/chunk-EYWR6UA2.js.map +1 -0
- package/dist/{chunk-CPXAVWCU.js → chunk-F332TENF.js} +278 -3
- package/dist/chunk-F332TENF.js.map +1 -0
- package/dist/{chunk-FT6NBHNX.js → chunk-F6GZURXJ.js} +2536 -600
- package/dist/chunk-F6GZURXJ.js.map +1 -0
- package/dist/{chunk-2MI3LZFH.cjs → chunk-IIRVZSP2.cjs} +53 -2
- package/dist/chunk-IIRVZSP2.cjs.map +1 -0
- package/dist/{chunk-V5LBQN3I.js → chunk-KA2PDJNB.js} +4 -4
- package/dist/chunk-KA2PDJNB.js.map +1 -0
- package/dist/{chunk-AVPUX57O.js → chunk-KAOWRIFD.js} +3 -3
- package/dist/{chunk-AVPUX57O.js.map → chunk-KAOWRIFD.js.map} +1 -1
- package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
- package/dist/chunk-MPT5PA6U.cjs.map +1 -0
- package/dist/{chunk-A4SVOGG6.cjs → chunk-N7TDLOUE.cjs} +2696 -762
- package/dist/chunk-N7TDLOUE.cjs.map +1 -0
- package/dist/{chunk-7I5INVNR.cjs → chunk-T3YIKW2A.cjs} +9 -9
- package/dist/chunk-T3YIKW2A.cjs.map +1 -0
- package/dist/{chunk-DTLB6UIH.cjs → chunk-Y72M3MVX.cjs} +280 -2
- package/dist/chunk-Y72M3MVX.cjs.map +1 -0
- package/dist/{chunk-SGAG6FD3.js → chunk-YFJJU26H.js} +24 -2
- package/dist/chunk-YFJJU26H.js.map +1 -0
- package/dist/chunk-YHW27CBV.cjs +406 -0
- package/dist/chunk-YHW27CBV.cjs.map +1 -0
- package/dist/{chunk-FYEDK7K7.cjs → chunk-YMTTGHEK.cjs} +61 -4
- package/dist/chunk-YMTTGHEK.cjs.map +1 -0
- package/dist/{collection-config-FLlGtsh9.d.cts → collection-config-BF95LgQb.d.cts} +10 -2
- package/dist/{collection-config-FLlGtsh9.d.ts → collection-config-BF95LgQb.d.ts} +10 -2
- package/dist/index.cjs +2001 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +504 -9
- package/dist/index.d.ts +504 -9
- package/dist/index.js +1893 -41
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +24 -24
- package/dist/middleware.d.cts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +3 -3
- package/dist/migrations-QNYAWQLB.cjs +13 -0
- package/dist/{migrations-32QAYLWJ.cjs.map → migrations-QNYAWQLB.cjs.map} +1 -1
- package/dist/migrations-R6NQBKQV.js +4 -0
- package/dist/{migrations-57ZHBQ4X.js.map → migrations-R6NQBKQV.js.map} +1 -1
- package/dist/{plugin-bootstrap-CDh0JHtW.d.ts → plugin-bootstrap-CB-xaBfK.d.ts} +2 -2
- package/dist/{plugin-bootstrap-C0E3jdz-.d.cts → plugin-bootstrap-U-cw9jn3.d.cts} +2 -2
- package/dist/plugin-manager-Baa6xXqB.d.ts +328 -0
- package/dist/plugin-manager-vBal9Zip.d.cts +328 -0
- package/dist/plugins.cjs +20 -7
- package/dist/plugins.d.cts +53 -310
- package/dist/plugins.d.ts +53 -310
- package/dist/plugins.js +2 -1
- package/dist/routes.cjs +27 -26
- package/dist/routes.d.cts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +7 -6
- package/dist/services.cjs +16 -16
- package/dist/services.d.cts +2 -2
- package/dist/services.d.ts +2 -2
- package/dist/services.js +2 -2
- package/dist/templates.cjs +17 -17
- package/dist/templates.js +2 -2
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utils.cjs +23 -11
- package/dist/utils.d.cts +38 -1
- package/dist/utils.d.ts +38 -1
- package/dist/utils.js +1 -1
- package/migrations/027_fix_slug_field_type.sql +18 -0
- package/migrations/028_fix_slug_field_type_in_schemas.sql +30 -0
- package/migrations/029_ai_search_plugin.sql +45 -0
- package/package.json +5 -2
- package/dist/chunk-2MI3LZFH.cjs.map +0 -1
- package/dist/chunk-7I5INVNR.cjs.map +0 -1
- package/dist/chunk-A4SVOGG6.cjs.map +0 -1
- package/dist/chunk-AZLU3ROK.cjs.map +0 -1
- package/dist/chunk-CPXAVWCU.js.map +0 -1
- package/dist/chunk-D2NLCPO2.js.map +0 -1
- package/dist/chunk-DTLB6UIH.cjs.map +0 -1
- package/dist/chunk-DXM575E2.js.map +0 -1
- package/dist/chunk-FT6NBHNX.js.map +0 -1
- package/dist/chunk-FYEDK7K7.cjs.map +0 -1
- package/dist/chunk-ILZ3DP4I.cjs.map +0 -1
- package/dist/chunk-SGAG6FD3.js.map +0 -1
- package/dist/chunk-V5LBQN3I.js.map +0 -1
- package/dist/chunk-VNCYCH3H.js.map +0 -1
- package/dist/migrations-32QAYLWJ.cjs +0 -13
- package/dist/migrations-57ZHBQ4X.js +0 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getCacheService, CACHE_CONFIGS, getLogger, SettingsService } from './chunk-3YNNVSMC.js';
|
|
2
|
-
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-
|
|
3
|
-
import { PluginService } from './chunk-
|
|
4
|
-
import { MigrationService } from './chunk-
|
|
5
|
-
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-
|
|
6
|
-
import {
|
|
2
|
+
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-EYWR6UA2.js';
|
|
3
|
+
import { PluginService } from './chunk-YFJJU26H.js';
|
|
4
|
+
import { MigrationService } from './chunk-EVZOVYLO.js';
|
|
5
|
+
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-KA2PDJNB.js';
|
|
6
|
+
import { PluginBuilder } from './chunk-CLIH2T74.js';
|
|
7
|
+
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml, getBlocksFieldConfig, parseBlocksValue } from './chunk-7DL5SPPX.js';
|
|
7
8
|
import { metricsTracker } from './chunk-FICTAGD4.js';
|
|
8
9
|
import { Hono } from 'hono';
|
|
9
10
|
import { cors } from 'hono/cors';
|
|
@@ -14,6 +15,37 @@ import { html, raw } from 'hono/html';
|
|
|
14
15
|
// src/schemas/index.ts
|
|
15
16
|
var schemaDefinitions = [];
|
|
16
17
|
var apiContentCrudRoutes = new Hono();
|
|
18
|
+
apiContentCrudRoutes.get("/check-slug", async (c) => {
|
|
19
|
+
try {
|
|
20
|
+
const db = c.env.DB;
|
|
21
|
+
const collectionId = c.req.query("collectionId");
|
|
22
|
+
const slug = c.req.query("slug");
|
|
23
|
+
const excludeId = c.req.query("excludeId");
|
|
24
|
+
if (!collectionId || !slug) {
|
|
25
|
+
return c.json({ error: "collectionId and slug are required" }, 400);
|
|
26
|
+
}
|
|
27
|
+
let query = "SELECT id FROM content WHERE collection_id = ? AND slug = ?";
|
|
28
|
+
const params = [collectionId, slug];
|
|
29
|
+
if (excludeId) {
|
|
30
|
+
query += " AND id != ?";
|
|
31
|
+
params.push(excludeId);
|
|
32
|
+
}
|
|
33
|
+
const existing = await db.prepare(query).bind(...params).first();
|
|
34
|
+
if (existing) {
|
|
35
|
+
return c.json({
|
|
36
|
+
available: false,
|
|
37
|
+
message: "This URL slug is already in use in this collection"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return c.json({ available: true });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("Error checking slug:", error);
|
|
43
|
+
return c.json({
|
|
44
|
+
error: "Failed to check slug availability",
|
|
45
|
+
details: error instanceof Error ? error.message : String(error)
|
|
46
|
+
}, 500);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
17
49
|
apiContentCrudRoutes.get("/:id", async (c) => {
|
|
18
50
|
try {
|
|
19
51
|
const id = c.req.param("id");
|
|
@@ -1551,6 +1583,107 @@ adminApiRoutes.get("/collections/:id", async (c) => {
|
|
|
1551
1583
|
return c.json({ error: "Failed to fetch collection" }, 500);
|
|
1552
1584
|
}
|
|
1553
1585
|
});
|
|
1586
|
+
adminApiRoutes.get("/references", async (c) => {
|
|
1587
|
+
try {
|
|
1588
|
+
const db = c.env.DB;
|
|
1589
|
+
const url = new URL(c.req.url);
|
|
1590
|
+
const collectionParams = url.searchParams.getAll("collection").flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
1591
|
+
const search = c.req.query("search") || "";
|
|
1592
|
+
const id = c.req.query("id") || "";
|
|
1593
|
+
const limit = Math.min(Number.parseInt(c.req.query("limit") || "20", 10) || 20, 100);
|
|
1594
|
+
if (collectionParams.length === 0) {
|
|
1595
|
+
return c.json({ error: "Collection is required" }, 400);
|
|
1596
|
+
}
|
|
1597
|
+
const placeholders = collectionParams.map(() => "?").join(", ");
|
|
1598
|
+
const collectionStmt = db.prepare(`
|
|
1599
|
+
SELECT id, name, display_name
|
|
1600
|
+
FROM collections
|
|
1601
|
+
WHERE id IN (${placeholders}) OR name IN (${placeholders})
|
|
1602
|
+
`);
|
|
1603
|
+
const collectionResults = await collectionStmt.bind(...collectionParams, ...collectionParams).all();
|
|
1604
|
+
const collections = collectionResults.results || [];
|
|
1605
|
+
if (collections.length === 0) {
|
|
1606
|
+
return c.json({ error: "Collection not found" }, 404);
|
|
1607
|
+
}
|
|
1608
|
+
const collectionById = Object.fromEntries(
|
|
1609
|
+
collections.map((entry) => [
|
|
1610
|
+
entry.id,
|
|
1611
|
+
{
|
|
1612
|
+
id: entry.id,
|
|
1613
|
+
name: entry.name,
|
|
1614
|
+
display_name: entry.display_name
|
|
1615
|
+
}
|
|
1616
|
+
])
|
|
1617
|
+
);
|
|
1618
|
+
const collectionIds = collections.map((entry) => entry.id);
|
|
1619
|
+
if (id) {
|
|
1620
|
+
const idPlaceholders = collectionIds.map(() => "?").join(", ");
|
|
1621
|
+
const itemStmt = db.prepare(`
|
|
1622
|
+
SELECT id, title, slug, collection_id
|
|
1623
|
+
FROM content
|
|
1624
|
+
WHERE id = ? AND collection_id IN (${idPlaceholders})
|
|
1625
|
+
LIMIT 1
|
|
1626
|
+
`);
|
|
1627
|
+
const item = await itemStmt.bind(id, ...collectionIds).first();
|
|
1628
|
+
if (!item) {
|
|
1629
|
+
return c.json({ error: "Reference not found" }, 404);
|
|
1630
|
+
}
|
|
1631
|
+
return c.json({
|
|
1632
|
+
data: {
|
|
1633
|
+
id: item.id,
|
|
1634
|
+
title: item.title,
|
|
1635
|
+
slug: item.slug,
|
|
1636
|
+
collection: collectionById[item.collection_id]
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
let stmt;
|
|
1641
|
+
let results;
|
|
1642
|
+
const listPlaceholders = collectionIds.map(() => "?").join(", ");
|
|
1643
|
+
const statusFilterValues = ["published"];
|
|
1644
|
+
const statusClause = ` AND status IN (${statusFilterValues.map(() => "?").join(", ")})`;
|
|
1645
|
+
if (search) {
|
|
1646
|
+
const searchParam = `%${search}%`;
|
|
1647
|
+
stmt = db.prepare(`
|
|
1648
|
+
SELECT id, title, slug, status, updated_at, collection_id
|
|
1649
|
+
FROM content
|
|
1650
|
+
WHERE collection_id IN (${listPlaceholders})
|
|
1651
|
+
AND (title LIKE ? OR slug LIKE ?)
|
|
1652
|
+
${statusClause}
|
|
1653
|
+
ORDER BY updated_at DESC
|
|
1654
|
+
LIMIT ?
|
|
1655
|
+
`);
|
|
1656
|
+
const queryResults = await stmt.bind(...collectionIds, searchParam, searchParam, ...statusFilterValues, limit).all();
|
|
1657
|
+
results = queryResults.results;
|
|
1658
|
+
} else {
|
|
1659
|
+
stmt = db.prepare(`
|
|
1660
|
+
SELECT id, title, slug, status, updated_at, collection_id
|
|
1661
|
+
FROM content
|
|
1662
|
+
WHERE collection_id IN (${listPlaceholders})
|
|
1663
|
+
${statusClause}
|
|
1664
|
+
ORDER BY updated_at DESC
|
|
1665
|
+
LIMIT ?
|
|
1666
|
+
`);
|
|
1667
|
+
const queryResults = await stmt.bind(...collectionIds, ...statusFilterValues, limit).all();
|
|
1668
|
+
results = queryResults.results;
|
|
1669
|
+
}
|
|
1670
|
+
const items = (results || []).map((row) => ({
|
|
1671
|
+
id: row.id,
|
|
1672
|
+
title: row.title,
|
|
1673
|
+
slug: row.slug,
|
|
1674
|
+
status: row.status,
|
|
1675
|
+
updated_at: row.updated_at ? Number(row.updated_at) : null,
|
|
1676
|
+
collection: collectionById[row.collection_id]
|
|
1677
|
+
}));
|
|
1678
|
+
return c.json({
|
|
1679
|
+
data: items,
|
|
1680
|
+
count: items.length
|
|
1681
|
+
});
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
console.error("Error fetching reference options:", error);
|
|
1684
|
+
return c.json({ error: "Failed to fetch references" }, 500);
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1554
1687
|
adminApiRoutes.post("/collections", async (c) => {
|
|
1555
1688
|
try {
|
|
1556
1689
|
const contentType = c.req.header("Content-Type");
|
|
@@ -1720,7 +1853,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
|
|
|
1720
1853
|
});
|
|
1721
1854
|
adminApiRoutes.get("/migrations/status", async (c) => {
|
|
1722
1855
|
try {
|
|
1723
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1856
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-R6NQBKQV.js');
|
|
1724
1857
|
const db = c.env.DB;
|
|
1725
1858
|
const migrationService = new MigrationService2(db);
|
|
1726
1859
|
const status = await migrationService.getMigrationStatus();
|
|
@@ -1745,7 +1878,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
|
|
|
1745
1878
|
error: "Unauthorized. Admin access required."
|
|
1746
1879
|
}, 403);
|
|
1747
1880
|
}
|
|
1748
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1881
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-R6NQBKQV.js');
|
|
1749
1882
|
const db = c.env.DB;
|
|
1750
1883
|
const migrationService = new MigrationService2(db);
|
|
1751
1884
|
const result = await migrationService.runPendingMigrations();
|
|
@@ -1764,7 +1897,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
|
|
|
1764
1897
|
});
|
|
1765
1898
|
adminApiRoutes.get("/migrations/validate", async (c) => {
|
|
1766
1899
|
try {
|
|
1767
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1900
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-R6NQBKQV.js');
|
|
1768
1901
|
const db = c.env.DB;
|
|
1769
1902
|
const migrationService = new MigrationService2(db);
|
|
1770
1903
|
const validation = await migrationService.validateSchema();
|
|
@@ -1791,7 +1924,7 @@ function renderLoginPage(data, demoLoginActive = false) {
|
|
|
1791
1924
|
<meta charset="UTF-8">
|
|
1792
1925
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1793
1926
|
<title>Login - SonicJS AI</title>
|
|
1794
|
-
<link rel="icon" type="image/
|
|
1927
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
1795
1928
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
|
1796
1929
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
1797
1930
|
<script>
|
|
@@ -1968,7 +2101,7 @@ function renderRegisterPage(data) {
|
|
|
1968
2101
|
<meta charset="UTF-8">
|
|
1969
2102
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1970
2103
|
<title>Register - SonicJS AI</title>
|
|
1971
|
-
<link rel="icon" type="image/
|
|
2104
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
1972
2105
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
|
1973
2106
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
1974
2107
|
<script>
|
|
@@ -1991,40 +2124,18 @@ function renderRegisterPage(data) {
|
|
|
1991
2124
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
|
1992
2125
|
<!-- Logo Section -->
|
|
1993
2126
|
<div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
|
|
1994
|
-
<div class="mx-auto w-
|
|
1995
|
-
<svg class="w-
|
|
1996
|
-
<path
|
|
1997
|
-
<path fill="#F1F2F2" d="M974.78,1398.211c-5.016,6.574-10.034,13.146-15.048,19.721c-1.828,2.398-3.657,4.796-5.487,7.194 c1.994,1.719,3.958,3.51,5.873,5.424c18.724,18.731,28.089,41.216,28.089,67.459c0,26.251-9.366,48.658-28.089,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-9.848,0-19.156-1.308-27.923-3.923l-4.185,3.354 c-8.587,6.885-17.154,13.796-25.725,20.702c17.52,8.967,36.86,13.487,58.054,13.487c35.533,0,65.91-12.608,91.124-37.821 c25.214-25.215,37.821-55.584,37.821-91.125c0-35.534-12.607-65.911-37.821-91.126 C981.004,1403.663,977.926,1400.854,974.78,1398.211z"></path>
|
|
1998
|
-
<path fill="#F1F2F2" d="M1364.644,1439.619c-4.72,0-8.702,1.624-11.943,4.865c-3.249,3.249-4.866,7.23-4.866,11.944v138.014 l-167.651-211.003c-0.297-0.586-0.74-1.03-1.327-1.326c-4.721-4.714-10.249-7.742-16.588-9.069 c-6.346-1.326-12.608-0.732-18.801,1.77c-6.192,2.509-11.059,6.49-14.598,11.944c-3.539,5.46-5.308,11.577-5.308,18.357v208.348 c0,4.721,1.618,8.703,4.866,11.944c3.241,3.241,7.222,4.865,11.943,4.865c2.945,0,5.751-0.738,8.405-2.211 c2.654-1.472,4.713-3.463,6.193-5.971c1.473-2.503,2.212-5.378,2.212-8.627v-205.251l166.325,209.675 c2.06,2.654,4.423,4.865,7.078,6.635c5.308,3.829,11.349,5.75,18.137,5.75c5.308,0,10.464-1.182,15.482-3.538 c3.539-1.769,6.56-4.127,9.069-7.078c2.502-2.945,4.491-6.338,5.971-10.175c1.473-3.829,2.212-7.664,2.212-11.501v-141.552 c0-4.714-1.624-8.695-4.865-11.944C1373.339,1441.243,1369.359,1439.619,1364.644,1439.619z"></path>
|
|
1999
|
-
<path fill="#F1F2F2" d="M1508.406,1432.983c-2.654-1.472-5.46-2.212-8.404-2.212c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.395-4.865,7.3-4.865,11.723v163.228c0,4.721,1.616,8.702,4.865,11.943c3.241,3.249,7.223,4.866,11.944,4.866 c2.944,0,5.751-0.732,8.404-2.212c2.655-1.472,4.714-3.539,6.193-6.194c1.473-2.654,2.213-5.453,2.213-8.404V1447.58 c0-2.945-0.74-5.75-2.213-8.405C1513.12,1436.522,1511.06,1434.462,1508.406,1432.983z"></path>
|
|
2000
|
-
<path fill="#F1F2F2" d="M1499.78,1367.957c-4.575,0-8.481,1.625-11.722,4.866c-3.249,3.249-4.865,7.23-4.865,11.943 c0,2.951,0.732,5.75,2.212,8.405c1.472,2.654,3.463,4.721,5.971,6.193c2.503,1.479,5.378,2.212,8.627,2.212 c4.423,0,8.328-1.618,11.721-4.865c3.387-3.243,5.088-7.224,5.088-11.944c0-4.713-1.701-8.694-5.088-11.943 C1508.33,1369.582,1504.349,1367.957,1499.78,1367.957z"></path>
|
|
2001
|
-
<path fill="#F1F2F2" d="M1859.627,1369.727H1747.27c-35.388,0-65.69,12.607-90.904,37.821 c-25.213,25.215-37.82,55.591-37.82,91.125c0,35.54,12.607,65.911,37.82,91.125c25.215,25.215,55.516,37.821,90.904,37.821h56.178 c4.714,0,8.695-1.618,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943c0-4.714-1.624-8.695-4.865-11.943 c-3.249-3.243-7.23-4.866-11.944-4.866h-56.178c-26.251,0-48.659-9.359-67.237-28.09c-18.579-18.723-27.868-41.207-27.868-67.459 c0-26.243,9.29-48.659,27.868-67.237c18.579-18.579,40.987-27.868,67.237-27.868h112.357c4.714,0,8.696-1.693,11.944-5.087 c3.241-3.387,4.865-7.368,4.865-11.943c0-4.569-1.624-8.475-4.865-11.723C1868.322,1371.351,1864.341,1369.727,1859.627,1369.727z "></path>
|
|
2002
|
-
<path fill="#06b6d4" d="M2219.256,1371.054h-112.357c-4.423,0-8.336,1.624-11.723,4.865c-3.393,3.249-5.087,7.23-5.087,11.944 c0,4.721,1.694,8.702,5.087,11.943c3.387,3.249,7.3,4.866,11.723,4.866h95.547v95.105c0,26.251-9.365,48.659-28.088,67.237 c-18.731,18.579-41.215,27.868-67.459,27.868c-26.251,0-48.659-9.289-67.237-27.868c-18.579-18.579-27.868-40.987-27.868-67.237 c0-4.713-1.701-8.771-5.088-12.165c-3.393-3.387-7.374-5.087-11.943-5.087c-4.575,0-8.481,1.7-11.722,5.087 c-3.249,3.393-4.865,7.451-4.865,12.165c0,35.388,12.607,65.69,37.82,90.904c25.215,25.213,55.584,37.82,91.126,37.82 c35.532,0,65.91-12.607,91.125-37.82c25.214-25.215,37.82-55.516,37.82-90.904v-111.915c0-4.714-1.624-8.695-4.865-11.944 C2227.951,1372.678,2223.971,1371.054,2219.256,1371.054z"></path>
|
|
2003
|
-
<path fill="#06b6d4" d="M2574.24,1502.875c-14.306-14.156-31.483-21.234-51.533-21.234H2410.35 c-10.617,0-19.762-3.829-27.426-11.501c-7.672-7.664-11.501-16.954-11.501-27.868c0-10.907,3.829-20.196,11.501-27.868 c7.664-7.664,16.809-11.501,27.426-11.501h112.357c4.714,0,8.695-1.617,11.944-4.866c3.241-3.241,4.865-7.222,4.865-11.943 c0-4.714-1.624-8.695-4.865-11.944c-3.249-3.241-7.23-4.865-11.944-4.865H2410.35c-20.058,0-37.158,7.154-51.313,21.454 c-14.156,14.308-21.232,31.483-21.232,51.534c0,20.058,7.077,37.234,21.232,51.534c14.156,14.308,31.255,21.454,51.313,21.454 h112.357c7.078,0,13.637,1.77,19.684,5.308c6.042,3.539,10.838,8.336,14.377,14.377c3.538,6.047,5.307,12.607,5.307,19.685 c0,10.616-3.835,19.76-11.501,27.425c-7.672,7.673-16.961,11.502-27.868,11.502h-168.094c-4.721,0-8.703,1.7-11.944,5.087 c-3.249,3.393-4.865,7.374-4.865,11.943c0,4.576,1.616,8.481,4.865,11.723c3.241,3.249,7.223,4.866,11.944,4.866h168.094 c20.051,0,37.227-7.078,51.533-21.234c14.302-14.155,21.454-31.331,21.454-51.534 C2595.695,1534.213,2588.542,1517.03,2574.24,1502.875z"></path>
|
|
2004
|
-
<path fill="#06b6d4" d="M854.024,1585.195l20.001-16.028c16.616-13.507,33.04-27.265,50.086-40.251 c1.13-0.861,2.9-1.686,2.003-3.516c-0.843-1.716-2.481-2.302-4.484-2.123c-8.514,0.765-17.016-0.538-25.537-0.353 c-1.124,0.024-2.768,0.221-3.163-1.25c-0.371-1.369,1.088-2.063,1.919-2.894c6.26-6.242,12.574-12.43,18.816-18.691 c9.303-9.327,18.565-18.714,27.851-28.066c1.848-1.859,3.701-3.713,5.549-5.572c2.655-2.661,5.309-5.315,7.958-7.982 c0.574-0.579,1.259-1.141,1.246-1.94c-0.004-0.257-0.078-0.538-0.254-0.853c-0.556-0.981-1.441-1.1-2.469-0.957 c-0.658,0.096-1.315,0.185-1.973,0.275c-3.844,0.538-7.689,1.076-11.533,1.608c-3.641,0.505-7.281,1.02-10.922,1.529 c-4.162,0.582-8.324,1.158-12.486,1.748c-1.142,0.161-2.409,1.662-3.354,0.508c-0.419-0.508-0.431-1.028-0.251-1.531 c0.269-0.741,0.957-1.441,1.387-2.021c3.414-4.58,6.882-9.124,10.356-13.662c1.74-2.272,3.48-4.544,5.214-6.822 c4.682-6.141,9.369-12.281,14.051-18.422c0.09-0.119,0.181-0.237,0.271-0.355c6.848-8.98,13.7-17.958,20.553-26.936 c0.488-0.64,0.977-1.28,1.465-1.92c2.159-2.828,4.315-5.658,6.476-8.486c4.197-5.501,8.454-10.954,12.67-16.442 c0.263-0.347,0.538-0.718,0.717-1.106c0.269-0.586,0.299-1.196-0.335-1.776c-0.825-0.753-1.8-0.15-2.595,0.419 c-0.67,0.472-1.333,0.957-1.955,1.489c-2.206,1.889-4.401,3.797-6.595,5.698c-3.958,3.438-7.922,6.876-11.976,10.194 c-2.443,2.003-4.865,4.028-7.301,6.038c-18.689-10.581-39.53-15.906-62.549-15.906c-35.54,0-65.911,12.607-91.125,37.82 c-25.214,25.215-37.821,55.592-37.821,91.126c0,35.54,12.607,65.91,37.821,91.125c4.146,4.146,8.445,7.916,12.87,11.381 c-9.015,11.14-18.036,22.277-27.034,33.429c-1.208,1.489-3.755,3.151-2.745,4.891c0.078,0.144,0.173,0.281,0.305,0.425 c1.321,1.429,3.492-1.303,4.933-2.457c6.673-5.333,13.333-10.685,19.982-16.042c3.707-2.984,7.417-5.965,11.124-8.952 c1.474-1.188,2.951-2.373,4.425-3.561c6.41-5.164,12.816-10.333,19.238-15.481L854.024,1585.195z M797.552,1498.009 c0-26.243,9.29-48.728,27.868-67.459c18.579-18.723,40.987-28.089,67.238-28.089c12.273,0,23.712,2.075,34.34,6.171 c-3.37,2.905-6.734,5.816-10.069,8.762c-6.075,5.351-12.365,10.469-18.667,15.564c-4.179,3.378-8.371,6.744-12.514,10.164 c-7.54,6.23-15.037,12.52-22.529,18.804c-7.091,5.955-14.182,11.904-21.19,17.949c-1.136,0.974-3.055,1.907-2.135,3.94 c0.831,1.836,2.774,1.417,4.341,1.578l12.145-0.599l14.151-0.698c1.031-0.102,2.192-0.257,2.89,0.632 c0.034,0.044,0.073,0.078,0.106,0.127c1.017,1.561-0.67,2.105-1.387,2.942c-6.308,7.318-12.616,14.637-18.978,21.907 c-8.161,9.339-16.353,18.649-24.544,27.958c-2.146,2.433-4.275,4.879-6.422,7.312c-1.034,1.172-2.129,2.272-1.238,3.922 c0.933,1.728,2.685,1.752,4.323,1.602c4.134-0.367,8.263-0.489,12.396-0.492c0.242,0,0.485-0.005,0.728-0.004 c2.711,0.009,5.422,0.068,8.134,0.145c2.582,0.074,5.166,0.165,7.752,0.249c0.275,1.62-0.879,2.356-1.62,3.259 c-1.333,1.626-2.667,3.247-4,4.867c-4.315,5.252-8.62,10.514-12.928,15.772c-3.562-2.725-7.007-5.733-10.324-9.051 C806.842,1546.667,797.552,1524.26,797.552,1498.009z"></path>
|
|
2127
|
+
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-white">
|
|
2128
|
+
<svg class="h-7 w-7 text-zinc-950" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2129
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
2005
2130
|
</svg>
|
|
2006
2131
|
</div>
|
|
2007
|
-
<
|
|
2008
|
-
|
|
2132
|
+
<h1 class="mt-6 text-3xl font-semibold tracking-tight text-white">SonicJS AI</h1>
|
|
2133
|
+
<p class="mt-2 text-sm text-zinc-400">Create your account and get started</p>
|
|
2009
2134
|
</div>
|
|
2010
2135
|
|
|
2011
2136
|
<!-- Form Container -->
|
|
2012
2137
|
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
2013
2138
|
<div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
|
|
2014
|
-
<!-- Setup Banner -->
|
|
2015
|
-
${data.isSetup ? `
|
|
2016
|
-
<div class="mb-6 rounded-lg bg-blue-500/10 p-4 ring-1 ring-blue-500/20">
|
|
2017
|
-
<div class="flex items-start gap-x-3">
|
|
2018
|
-
<svg class="h-5 w-5 text-blue-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2019
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
2020
|
-
</svg>
|
|
2021
|
-
<div class="flex-1">
|
|
2022
|
-
<p class="text-sm font-medium text-blue-300">First-time Setup</p>
|
|
2023
|
-
<p class="text-sm text-blue-400/80 mt-1">This account will be the administrator with full access to manage your SonicJS installation.</p>
|
|
2024
|
-
</div>
|
|
2025
|
-
</div>
|
|
2026
|
-
</div>
|
|
2027
|
-
` : ""}
|
|
2028
2139
|
<!-- Alerts -->
|
|
2029
2140
|
${data.error ? `<div class="mb-6">${renderAlert({ type: "error", message: data.error })}</div>` : ""}
|
|
2030
2141
|
|
|
@@ -2139,7 +2250,6 @@ function renderRegisterPage(data) {
|
|
|
2139
2250
|
</html>
|
|
2140
2251
|
`;
|
|
2141
2252
|
}
|
|
2142
|
-
var adminExistsCache = null;
|
|
2143
2253
|
async function isRegistrationEnabled(db) {
|
|
2144
2254
|
try {
|
|
2145
2255
|
const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
|
|
@@ -2161,21 +2271,6 @@ async function isFirstUserRegistration(db) {
|
|
|
2161
2271
|
return false;
|
|
2162
2272
|
}
|
|
2163
2273
|
}
|
|
2164
|
-
async function checkAdminUserExists(db) {
|
|
2165
|
-
if (adminExistsCache !== null) {
|
|
2166
|
-
return adminExistsCache;
|
|
2167
|
-
}
|
|
2168
|
-
try {
|
|
2169
|
-
const result = await db.prepare("SELECT id FROM users WHERE role = ?").bind("admin").first();
|
|
2170
|
-
adminExistsCache = !!result;
|
|
2171
|
-
return adminExistsCache;
|
|
2172
|
-
} catch {
|
|
2173
|
-
return false;
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
function setAdminExists() {
|
|
2177
|
-
adminExistsCache = true;
|
|
2178
|
-
}
|
|
2179
2274
|
var baseRegistrationSchema = z.object({
|
|
2180
2275
|
email: z.string().email("Valid email is required"),
|
|
2181
2276
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
@@ -2237,11 +2332,8 @@ authRoutes.get("/register", async (c) => {
|
|
|
2237
2332
|
}
|
|
2238
2333
|
}
|
|
2239
2334
|
const error = c.req.query("error");
|
|
2240
|
-
const isSetup = c.req.query("setup") === "true";
|
|
2241
2335
|
const pageData = {
|
|
2242
|
-
error: error || void 0
|
|
2243
|
-
isSetup: isSetup && isFirstUser
|
|
2244
|
-
// Only show setup message if truly first user
|
|
2336
|
+
error: error || void 0
|
|
2245
2337
|
};
|
|
2246
2338
|
return c.html(renderRegisterPage(pageData));
|
|
2247
2339
|
});
|
|
@@ -2516,9 +2608,6 @@ authRoutes.post("/register/form", async (c) => {
|
|
|
2516
2608
|
now.getTime(),
|
|
2517
2609
|
now.getTime()
|
|
2518
2610
|
).run();
|
|
2519
|
-
if (isFirstUser) {
|
|
2520
|
-
setAdminExists();
|
|
2521
|
-
}
|
|
2522
2611
|
const token = await AuthManager.generateToken(userId, normalizedEmail, role);
|
|
2523
2612
|
setCookie(c, "auth_token", token, {
|
|
2524
2613
|
httpOnly: true,
|
|
@@ -2640,7 +2729,6 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2640
2729
|
if (existingAdmin) {
|
|
2641
2730
|
const passwordHash2 = await AuthManager.hashPassword("sonicjs!");
|
|
2642
2731
|
await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
|
|
2643
|
-
setAdminExists();
|
|
2644
2732
|
return c.json({
|
|
2645
2733
|
message: "Admin user already exists (password updated)",
|
|
2646
2734
|
user: {
|
|
@@ -2671,7 +2759,6 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2671
2759
|
now,
|
|
2672
2760
|
now
|
|
2673
2761
|
).run();
|
|
2674
|
-
setAdminExists();
|
|
2675
2762
|
return c.json({
|
|
2676
2763
|
message: "Admin user created successfully",
|
|
2677
2764
|
user: {
|
|
@@ -3381,9 +3468,166 @@ var test_cleanup_default = app;
|
|
|
3381
3468
|
// src/templates/pages/admin-content-form.template.ts
|
|
3382
3469
|
init_admin_layout_catalyst_template();
|
|
3383
3470
|
|
|
3471
|
+
// src/templates/components/drag-sortable.template.ts
|
|
3472
|
+
function getDragSortableScript() {
|
|
3473
|
+
return `
|
|
3474
|
+
<script>
|
|
3475
|
+
if (!window.__sonicDragSortableInit) {
|
|
3476
|
+
window.__sonicDragSortableInit = true;
|
|
3477
|
+
|
|
3478
|
+
window.initializeDragSortable = function(container, options) {
|
|
3479
|
+
if (!container || container.dataset.dragSortableInit === 'true') {
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
container.dataset.dragSortableInit = 'true';
|
|
3484
|
+
const itemSelector = options && options.itemSelector ? options.itemSelector : '.sortable-item';
|
|
3485
|
+
const handleSelector = options && options.handleSelector ? options.handleSelector : '[data-action="drag-handle"]';
|
|
3486
|
+
const onUpdate = options && typeof options.onUpdate === 'function' ? options.onUpdate : function() {};
|
|
3487
|
+
let activeDragItem = null;
|
|
3488
|
+
|
|
3489
|
+
const getDragAfterElement = function(list, y) {
|
|
3490
|
+
const items = Array.from(list.querySelectorAll(itemSelector + ':not(.is-dragging)'));
|
|
3491
|
+
let closest = { offset: Number.NEGATIVE_INFINITY, element: null };
|
|
3492
|
+
items.forEach(function(item) {
|
|
3493
|
+
const box = item.getBoundingClientRect();
|
|
3494
|
+
const offset = y - box.top - box.height / 2;
|
|
3495
|
+
if (offset < 0 && offset > closest.offset) {
|
|
3496
|
+
closest = { offset: offset, element: item };
|
|
3497
|
+
}
|
|
3498
|
+
});
|
|
3499
|
+
return closest.element;
|
|
3500
|
+
};
|
|
3501
|
+
|
|
3502
|
+
const activateDragItem = function(event) {
|
|
3503
|
+
const target = event.target;
|
|
3504
|
+
if (!(target instanceof Element)) return;
|
|
3505
|
+
const handle = target.closest(handleSelector);
|
|
3506
|
+
if (!handle) return;
|
|
3507
|
+
const item = handle.closest(itemSelector);
|
|
3508
|
+
if (!item) return;
|
|
3509
|
+
activeDragItem = item;
|
|
3510
|
+
};
|
|
3511
|
+
|
|
3512
|
+
const clearActiveDragItem = function() {
|
|
3513
|
+
activeDragItem = null;
|
|
3514
|
+
};
|
|
3515
|
+
|
|
3516
|
+
container.addEventListener('pointerdown', activateDragItem);
|
|
3517
|
+
container.addEventListener('mousedown', activateDragItem);
|
|
3518
|
+
container.addEventListener('pointerup', clearActiveDragItem);
|
|
3519
|
+
container.addEventListener('mouseup', clearActiveDragItem);
|
|
3520
|
+
|
|
3521
|
+
container.addEventListener('dragstart', function(event) {
|
|
3522
|
+
const target = event.target;
|
|
3523
|
+
if (!(target instanceof Element)) return;
|
|
3524
|
+
const item = target.closest(itemSelector);
|
|
3525
|
+
if (!item || item !== activeDragItem) {
|
|
3526
|
+
event.preventDefault();
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
item.classList.add('is-dragging');
|
|
3530
|
+
if (event.dataTransfer) {
|
|
3531
|
+
event.dataTransfer.setData('text/plain', '');
|
|
3532
|
+
}
|
|
3533
|
+
});
|
|
3534
|
+
|
|
3535
|
+
container.addEventListener('dragend', function(event) {
|
|
3536
|
+
const target = event.target;
|
|
3537
|
+
if (target instanceof Element) {
|
|
3538
|
+
const item = target.closest(itemSelector);
|
|
3539
|
+
if (item) {
|
|
3540
|
+
item.classList.remove('is-dragging');
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
activeDragItem = null;
|
|
3544
|
+
onUpdate();
|
|
3545
|
+
});
|
|
3546
|
+
|
|
3547
|
+
container.addEventListener('dragover', function(event) {
|
|
3548
|
+
event.preventDefault();
|
|
3549
|
+
const dragging = container.querySelector(itemSelector + '.is-dragging');
|
|
3550
|
+
if (!dragging) return;
|
|
3551
|
+
const afterElement = getDragAfterElement(container, event.clientY);
|
|
3552
|
+
if (afterElement === null) {
|
|
3553
|
+
container.appendChild(dragging);
|
|
3554
|
+
} else {
|
|
3555
|
+
container.insertBefore(dragging, afterElement);
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
container.addEventListener('drop', function() {
|
|
3560
|
+
onUpdate();
|
|
3561
|
+
});
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
</script>
|
|
3565
|
+
`;
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3384
3568
|
// src/templates/components/dynamic-field.template.ts
|
|
3569
|
+
function getReadFieldValueScript() {
|
|
3570
|
+
return `
|
|
3571
|
+
<script>
|
|
3572
|
+
if (!window.__sonicReadFieldValueInit) {
|
|
3573
|
+
window.__sonicReadFieldValueInit = true;
|
|
3574
|
+
|
|
3575
|
+
window.sonicReadFieldValue = function(fieldWrapper) {
|
|
3576
|
+
const fieldType = fieldWrapper.dataset.fieldType;
|
|
3577
|
+
const select = fieldWrapper.querySelector('select');
|
|
3578
|
+
const textarea = fieldWrapper.querySelector('textarea');
|
|
3579
|
+
const inputs = Array.from(fieldWrapper.querySelectorAll('input'));
|
|
3580
|
+
const checkbox = inputs.find((input) => input.type === 'checkbox');
|
|
3581
|
+
const nonHiddenInput = inputs.find((input) => input.type !== 'hidden' && input.type !== 'checkbox');
|
|
3582
|
+
const hiddenInput = inputs.find((input) => input.type === 'hidden');
|
|
3583
|
+
|
|
3584
|
+
if (fieldType === 'object' || fieldType === 'array') {
|
|
3585
|
+
if (!hiddenInput) {
|
|
3586
|
+
return fieldType === 'array' ? [] : {};
|
|
3587
|
+
}
|
|
3588
|
+
const rawValue = hiddenInput.value || '';
|
|
3589
|
+
if (!rawValue.trim()) {
|
|
3590
|
+
return fieldType === 'array' ? [] : {};
|
|
3591
|
+
}
|
|
3592
|
+
try {
|
|
3593
|
+
return JSON.parse(rawValue);
|
|
3594
|
+
} catch {
|
|
3595
|
+
return fieldType === 'array' ? [] : {};
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
if (fieldType === 'boolean' && checkbox) {
|
|
3600
|
+
return checkbox.checked;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
if (select) {
|
|
3604
|
+
if (select.multiple) {
|
|
3605
|
+
return Array.from(select.selectedOptions).map((option) => option.value);
|
|
3606
|
+
}
|
|
3607
|
+
return select.value;
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
if (fieldType === 'quill' || fieldType === 'media') {
|
|
3611
|
+
return hiddenInput ? hiddenInput.value : '';
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
const textSource = textarea || nonHiddenInput || hiddenInput;
|
|
3615
|
+
if (!textSource) {
|
|
3616
|
+
return '';
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
if (fieldType === 'number') {
|
|
3620
|
+
return textSource.value === '' ? null : Number(textSource.value);
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
return textSource.value;
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
</script>
|
|
3627
|
+
`;
|
|
3628
|
+
}
|
|
3385
3629
|
function renderDynamicField(field, options = {}) {
|
|
3386
|
-
const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {} } = options;
|
|
3630
|
+
const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {}, collectionId = "", contentId = "" } = options;
|
|
3387
3631
|
const opts = field.field_options || {};
|
|
3388
3632
|
const required = field.is_required ? "required" : "";
|
|
3389
3633
|
const baseClasses = `w-full rounded-lg px-3 py-2 text-sm text-zinc-950 dark:text-white bg-white dark:bg-zinc-800 shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow ${className}`;
|
|
@@ -3636,67 +3880,171 @@ function renderDynamicField(field, options = {}) {
|
|
|
3636
3880
|
`;
|
|
3637
3881
|
break;
|
|
3638
3882
|
case "slug":
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3883
|
+
const slugPattern = opts.pattern || "^[a-z0-9-]+$";
|
|
3884
|
+
const collectionIdValue = collectionId || opts.collectionId || "";
|
|
3885
|
+
const contentIdValue = contentId || opts.contentId || "";
|
|
3886
|
+
const isEditMode = !!value;
|
|
3642
3887
|
fieldHTML = `
|
|
3643
|
-
<
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3888
|
+
<div class="slug-field-container">
|
|
3889
|
+
<input
|
|
3890
|
+
type="text"
|
|
3891
|
+
id="${fieldId}"
|
|
3892
|
+
name="${fieldName}"
|
|
3893
|
+
value="${escapeHtml2(value)}"
|
|
3894
|
+
placeholder="${opts.placeholder || "url-friendly-slug"}"
|
|
3895
|
+
maxlength="${opts.maxLength || 100}"
|
|
3896
|
+
data-pattern="${slugPattern}"
|
|
3897
|
+
data-collection-id="${collectionIdValue}"
|
|
3898
|
+
data-content-id="${contentIdValue}"
|
|
3899
|
+
data-is-edit-mode="${isEditMode}"
|
|
3900
|
+
class="${baseClasses} ${errorClasses}"
|
|
3901
|
+
${required}
|
|
3902
|
+
${disabled ? "disabled" : ""}
|
|
3903
|
+
>
|
|
3904
|
+
<div id="${fieldId}-status" class="slug-status mt-1 text-sm min-h-[20px]"></div>
|
|
3905
|
+
<button
|
|
3906
|
+
type="button"
|
|
3907
|
+
class="regenerate-slug-btn mt-2 text-sm text-cyan-600 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-300 flex items-center gap-1 transition-colors"
|
|
3908
|
+
onclick="window.regenerateSlugFromTitle_${fieldId.replace(/-/g, "_")}()"
|
|
3909
|
+
>
|
|
3910
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
3911
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
3912
|
+
</svg>
|
|
3913
|
+
Regenerate from title
|
|
3914
|
+
</button>
|
|
3915
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Use lowercase letters, numbers, and hyphens only</p>
|
|
3916
|
+
</div>
|
|
3917
|
+
|
|
3656
3918
|
<script>
|
|
3657
3919
|
(function() {
|
|
3658
|
-
const
|
|
3920
|
+
const slugField = document.getElementById('${fieldId}');
|
|
3921
|
+
const statusDiv = document.getElementById('${fieldId}-status');
|
|
3922
|
+
const isEditMode = slugField.dataset.isEditMode === 'true';
|
|
3659
3923
|
const pattern = new RegExp('${slugPattern}');
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
})();
|
|
3673
|
-
|
|
3674
|
-
function generateSlugFromTitle(slugFieldId) {
|
|
3675
|
-
const titleField = document.querySelector('input[name="title"]');
|
|
3676
|
-
const slugField = document.getElementById(slugFieldId);
|
|
3677
|
-
if (titleField && slugField) {
|
|
3678
|
-
const slug = titleField.value
|
|
3924
|
+
const collectionId = slugField.dataset.collectionId;
|
|
3925
|
+
const contentId = slugField.dataset.contentId;
|
|
3926
|
+
|
|
3927
|
+
let checkTimeout;
|
|
3928
|
+
let lastCheckedSlug = '';
|
|
3929
|
+
let manuallyEdited = false;
|
|
3930
|
+
|
|
3931
|
+
// Shared slug generation function
|
|
3932
|
+
function generateSlug(text) {
|
|
3933
|
+
if (!text) return '';
|
|
3934
|
+
|
|
3935
|
+
return text
|
|
3679
3936
|
.toLowerCase()
|
|
3937
|
+
.normalize('NFD')
|
|
3938
|
+
.replace(/[\\u0300-\\u036f]/g, '')
|
|
3680
3939
|
.replace(/[^a-z0-9\\s_-]/g, '')
|
|
3681
3940
|
.replace(/\\s+/g, '-')
|
|
3682
3941
|
.replace(/[-_]+/g, '-')
|
|
3683
|
-
.replace(/^[-_]
|
|
3684
|
-
|
|
3942
|
+
.replace(/^[-_]+|[-_]+$/g, '')
|
|
3943
|
+
.substring(0, 100);
|
|
3685
3944
|
}
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3945
|
+
|
|
3946
|
+
// Check if slug is available
|
|
3947
|
+
async function checkSlugAvailability(slug) {
|
|
3948
|
+
if (!slug || !collectionId) return;
|
|
3949
|
+
|
|
3950
|
+
// Don't check if it's the same as last time
|
|
3951
|
+
if (slug === lastCheckedSlug) return;
|
|
3952
|
+
lastCheckedSlug = slug;
|
|
3953
|
+
|
|
3954
|
+
try {
|
|
3955
|
+
// Show checking status
|
|
3956
|
+
statusDiv.innerHTML = '<span class="text-gray-400">\u23F3 Checking availability...</span>';
|
|
3957
|
+
|
|
3958
|
+
// Build URL
|
|
3959
|
+
let url = \`/api/content/check-slug?collectionId=\${encodeURIComponent(collectionId)}&slug=\${encodeURIComponent(slug)}\`;
|
|
3960
|
+
if (contentId) {
|
|
3961
|
+
url += \`&excludeId=\${encodeURIComponent(contentId)}\`;
|
|
3696
3962
|
}
|
|
3697
|
-
|
|
3963
|
+
|
|
3964
|
+
const response = await fetch(url);
|
|
3965
|
+
const data = await response.json();
|
|
3966
|
+
|
|
3967
|
+
if (data.available) {
|
|
3968
|
+
statusDiv.innerHTML = '<span class="text-green-500 dark:text-green-400">\u2713 Available</span>';
|
|
3969
|
+
slugField.setCustomValidity('');
|
|
3970
|
+
} else {
|
|
3971
|
+
statusDiv.innerHTML = \`<span class="text-red-500 dark:text-red-400">\u2717 \${data.message || 'Already in use'}</span>\`;
|
|
3972
|
+
slugField.setCustomValidity(data.message || 'This slug is already in use');
|
|
3973
|
+
}
|
|
3974
|
+
} catch (error) {
|
|
3975
|
+
console.error('Error checking slug:', error);
|
|
3976
|
+
statusDiv.innerHTML = '<span class="text-yellow-500 dark:text-yellow-400">\u26A0 Could not verify</span>';
|
|
3977
|
+
}
|
|
3698
3978
|
}
|
|
3699
|
-
|
|
3979
|
+
|
|
3980
|
+
// Format validation and duplicate checking
|
|
3981
|
+
slugField.addEventListener('input', function() {
|
|
3982
|
+
const value = this.value;
|
|
3983
|
+
|
|
3984
|
+
// Mark as manually edited if user types directly
|
|
3985
|
+
if (document.activeElement === this) {
|
|
3986
|
+
manuallyEdited = true;
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
// Clear status if empty
|
|
3990
|
+
if (!value) {
|
|
3991
|
+
statusDiv.innerHTML = '';
|
|
3992
|
+
this.setCustomValidity('');
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// Pattern validation
|
|
3997
|
+
if (!pattern.test(value)) {
|
|
3998
|
+
this.setCustomValidity('Please use only lowercase letters, numbers, and hyphens.');
|
|
3999
|
+
statusDiv.innerHTML = '<span class="text-red-500 dark:text-red-400">\u2717 Invalid format</span>';
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
// Debounce the availability check
|
|
4004
|
+
clearTimeout(checkTimeout);
|
|
4005
|
+
checkTimeout = setTimeout(() => {
|
|
4006
|
+
checkSlugAvailability(value);
|
|
4007
|
+
}, 500); // Wait 500ms after user stops typing
|
|
4008
|
+
});
|
|
4009
|
+
|
|
4010
|
+
// Initial check if field has value
|
|
4011
|
+
if (slugField.value) {
|
|
4012
|
+
checkSlugAvailability(slugField.value);
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
// Auto-generate only in create mode
|
|
4016
|
+
// Wait for all fields to be rendered before attaching listeners
|
|
4017
|
+
if (!isEditMode) {
|
|
4018
|
+
// Use setTimeout to ensure all fields in the form are rendered
|
|
4019
|
+
setTimeout(() => {
|
|
4020
|
+
const titleField = document.querySelector('input[name="title"]');
|
|
4021
|
+
if (titleField) {
|
|
4022
|
+
titleField.addEventListener('input', function() {
|
|
4023
|
+
if (!manuallyEdited) {
|
|
4024
|
+
const slug = generateSlug(this.value);
|
|
4025
|
+
slugField.value = slug;
|
|
4026
|
+
|
|
4027
|
+
// Trigger validation and duplicate check
|
|
4028
|
+
slugField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
}, 0);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
// Global function for regenerate button
|
|
4036
|
+
window.regenerateSlugFromTitle_${fieldId.replace(/-/g, "_")} = function() {
|
|
4037
|
+
const titleField = document.querySelector('input[name="title"]');
|
|
4038
|
+
if (titleField && slugField) {
|
|
4039
|
+
const slug = generateSlug(titleField.value);
|
|
4040
|
+
slugField.value = slug;
|
|
4041
|
+
manuallyEdited = false;
|
|
4042
|
+
|
|
4043
|
+
// Trigger validation and duplicate check
|
|
4044
|
+
slugField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4045
|
+
}
|
|
4046
|
+
};
|
|
4047
|
+
})();
|
|
3700
4048
|
</script>
|
|
3701
4049
|
`;
|
|
3702
4050
|
break;
|
|
@@ -3733,43 +4081,124 @@ function renderDynamicField(field, options = {}) {
|
|
|
3733
4081
|
` : ""}
|
|
3734
4082
|
`;
|
|
3735
4083
|
break;
|
|
4084
|
+
case "reference":
|
|
4085
|
+
let referenceCollections = [];
|
|
4086
|
+
if (Array.isArray(opts.collection)) {
|
|
4087
|
+
referenceCollections = opts.collection.filter(Boolean);
|
|
4088
|
+
} else if (typeof opts.collection === "string" && opts.collection) {
|
|
4089
|
+
referenceCollections = [opts.collection];
|
|
4090
|
+
}
|
|
4091
|
+
const referenceCollectionsAttr = referenceCollections.join(",");
|
|
4092
|
+
const hasReferenceCollection = referenceCollections.length > 0;
|
|
4093
|
+
const hasReferenceValue = Boolean(value);
|
|
4094
|
+
fieldHTML = `
|
|
4095
|
+
<div class="reference-field-container space-y-3" data-reference-field data-field-name="${escapeHtml2(fieldName)}" data-reference-collection="${escapeHtml2(referenceCollections[0] || "")}" data-reference-collections="${escapeHtml2(referenceCollectionsAttr)}">
|
|
4096
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(value)}">
|
|
4097
|
+
<div class="rounded-lg border border-zinc-200 bg-white/60 px-3 py-2 text-sm text-zinc-600 dark:border-white/10 dark:bg-white/5 dark:text-zinc-300" data-reference-display>
|
|
4098
|
+
${hasReferenceCollection ? hasReferenceValue ? "Loading selection..." : "No reference selected." : "Reference collection not configured."}
|
|
4099
|
+
</div>
|
|
4100
|
+
<div class="flex flex-wrap gap-2">
|
|
4101
|
+
<button
|
|
4102
|
+
type="button"
|
|
4103
|
+
onclick="openReferenceSelector('${fieldId}')"
|
|
4104
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
|
|
4105
|
+
${hasReferenceCollection ? "" : "disabled"}
|
|
4106
|
+
>
|
|
4107
|
+
Select reference
|
|
4108
|
+
</button>
|
|
4109
|
+
<button
|
|
4110
|
+
type="button"
|
|
4111
|
+
onclick="clearReferenceField('${fieldId}')"
|
|
4112
|
+
class="inline-flex items-center justify-center rounded-lg border border-zinc-200 px-3 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/10"
|
|
4113
|
+
data-reference-clear
|
|
4114
|
+
${hasReferenceValue ? "" : "disabled"}
|
|
4115
|
+
>
|
|
4116
|
+
Remove
|
|
4117
|
+
</button>
|
|
4118
|
+
</div>
|
|
4119
|
+
</div>
|
|
4120
|
+
`;
|
|
4121
|
+
break;
|
|
3736
4122
|
case "media":
|
|
4123
|
+
const isMultiple = opts.multiple === true;
|
|
4124
|
+
const mediaValues = isMultiple && value ? Array.isArray(value) ? value : String(value).split(",").filter(Boolean) : [];
|
|
4125
|
+
const singleValue = !isMultiple ? value : "";
|
|
4126
|
+
const isVideoUrl = (url) => {
|
|
4127
|
+
const videoExtensions = [".mp4", ".webm", ".ogg", ".mov", ".avi"];
|
|
4128
|
+
return videoExtensions.some((ext) => url.toLowerCase().endsWith(ext));
|
|
4129
|
+
};
|
|
4130
|
+
const renderMediaPreview = (url, alt, classes) => {
|
|
4131
|
+
if (isVideoUrl(url)) {
|
|
4132
|
+
return `<video src="${url}" class="${classes}" muted></video>`;
|
|
4133
|
+
}
|
|
4134
|
+
return `<img src="${url}" alt="${alt}" class="${classes}">`;
|
|
4135
|
+
};
|
|
3737
4136
|
fieldHTML = `
|
|
3738
4137
|
<div class="media-field-container">
|
|
3739
|
-
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
4138
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${isMultiple ? mediaValues.join(",") : singleValue}" data-multiple="${isMultiple}">
|
|
4139
|
+
|
|
4140
|
+
${isMultiple ? `
|
|
4141
|
+
<div class="media-preview-grid grid grid-cols-4 gap-2 mb-2 ${mediaValues.length === 0 ? "hidden" : ""}" id="${fieldId}-preview">
|
|
4142
|
+
${mediaValues.map((url, idx) => `
|
|
4143
|
+
<div class="relative media-preview-item" data-url="${url}">
|
|
4144
|
+
${renderMediaPreview(url, `Media ${idx + 1}`, "w-full h-24 object-cover rounded-lg border border-white/20")}
|
|
4145
|
+
<button
|
|
4146
|
+
type="button"
|
|
4147
|
+
onclick="removeMediaFromMultiple('${fieldId}', '${url}')"
|
|
4148
|
+
class="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
|
|
4149
|
+
${disabled ? "disabled" : ""}
|
|
4150
|
+
>
|
|
4151
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
4152
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
4153
|
+
</svg>
|
|
4154
|
+
</button>
|
|
4155
|
+
</div>
|
|
4156
|
+
`).join("")}
|
|
4157
|
+
</div>
|
|
4158
|
+
` : `
|
|
4159
|
+
<div class="media-preview ${singleValue ? "" : "hidden"}" id="${fieldId}-preview">
|
|
4160
|
+
${singleValue ? renderMediaPreview(singleValue, "Selected media", "w-32 h-32 object-cover rounded-lg border border-white/20") : ""}
|
|
4161
|
+
</div>
|
|
4162
|
+
`}
|
|
4163
|
+
|
|
3743
4164
|
<div class="media-actions mt-2 space-x-2">
|
|
3744
4165
|
<button
|
|
3745
4166
|
type="button"
|
|
3746
|
-
onclick="openMediaSelector('${fieldId}')"
|
|
4167
|
+
onclick="openMediaSelector('${fieldId}', ${isMultiple})"
|
|
3747
4168
|
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
|
|
3748
4169
|
${disabled ? "disabled" : ""}
|
|
3749
4170
|
>
|
|
3750
4171
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
3751
4172
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
3752
4173
|
</svg>
|
|
3753
|
-
Select Media
|
|
4174
|
+
${isMultiple ? "Select Media (Multiple)" : "Select Media"}
|
|
3754
4175
|
</button>
|
|
3755
|
-
${
|
|
4176
|
+
${(isMultiple ? mediaValues.length > 0 : singleValue) ? `
|
|
3756
4177
|
<button
|
|
3757
4178
|
type="button"
|
|
3758
4179
|
onclick="clearMediaField('${fieldId}')"
|
|
3759
4180
|
class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
|
|
3760
4181
|
${disabled ? "disabled" : ""}
|
|
3761
4182
|
>
|
|
3762
|
-
Remove
|
|
4183
|
+
${isMultiple ? "Clear All" : "Remove"}
|
|
3763
4184
|
</button>
|
|
3764
4185
|
` : ""}
|
|
3765
4186
|
</div>
|
|
3766
4187
|
</div>
|
|
3767
4188
|
`;
|
|
3768
4189
|
break;
|
|
4190
|
+
case "object":
|
|
4191
|
+
return renderStructuredObjectField(field, options2);
|
|
4192
|
+
case "array":
|
|
4193
|
+
const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
|
|
4194
|
+
if (itemsConfig.blocks && typeof itemsConfig.blocks === "object") {
|
|
4195
|
+
return renderBlocksField(field, options2, baseClasses, errorClasses);
|
|
4196
|
+
}
|
|
4197
|
+
return renderStructuredArrayField(field, options2);
|
|
3769
4198
|
default:
|
|
3770
4199
|
fieldHTML = `
|
|
3771
|
-
<input
|
|
3772
|
-
type="text"
|
|
4200
|
+
<input
|
|
4201
|
+
type="text"
|
|
3773
4202
|
id="${fieldId}"
|
|
3774
4203
|
name="${fieldName}"
|
|
3775
4204
|
value="${escapeHtml2(value)}"
|
|
@@ -3819,217 +4248,764 @@ function renderFieldGroup(title, fields, collapsible = false) {
|
|
|
3819
4248
|
</div>
|
|
3820
4249
|
`;
|
|
3821
4250
|
}
|
|
3822
|
-
function
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
}
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
4251
|
+
function renderBlocksField(field, options, baseClasses, errorClasses) {
|
|
4252
|
+
const { value = [], pluginStatuses = {} } = options;
|
|
4253
|
+
const opts = field.field_options || {};
|
|
4254
|
+
const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
|
|
4255
|
+
const blocks = normalizeBlockDefinitions(itemsConfig.blocks);
|
|
4256
|
+
const discriminator = typeof itemsConfig.discriminator === "string" && itemsConfig.discriminator ? itemsConfig.discriminator : "blockType";
|
|
4257
|
+
const blockValues = normalizeBlocksValue(value, discriminator);
|
|
4258
|
+
const fieldId = `field-${field.field_name}`;
|
|
4259
|
+
const fieldName = field.field_name;
|
|
4260
|
+
const emptyState = blockValues.length === 0 ? `
|
|
4261
|
+
<div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-blocks-empty>
|
|
4262
|
+
No blocks yet. Add your first block to get started.
|
|
4263
|
+
</div>
|
|
4264
|
+
` : "";
|
|
4265
|
+
const blockOptions = blocks.map((block) => `<option value="${escapeHtml2(block.name)}">${escapeHtml2(block.label)}</option>`).join("");
|
|
4266
|
+
const blockItems = blockValues.map(
|
|
4267
|
+
(blockValue, index) => renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses)
|
|
4268
|
+
).join("");
|
|
4269
|
+
const templates = blocks.map((block) => renderBlockTemplate(field, block, discriminator, pluginStatuses)).join("");
|
|
4270
|
+
return `
|
|
4271
|
+
<div
|
|
4272
|
+
class="blocks-field space-y-4"
|
|
4273
|
+
data-blocks='${escapeHtml2(JSON.stringify(blocks))}'
|
|
4274
|
+
data-blocks-discriminator="${escapeHtml2(discriminator)}"
|
|
4275
|
+
data-field-name="${escapeHtml2(fieldName)}"
|
|
4276
|
+
>
|
|
4277
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(blockValues))}">
|
|
4278
|
+
|
|
4279
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
4280
|
+
<div class="flex-1">
|
|
4281
|
+
<select
|
|
4282
|
+
class="${baseClasses} ${errorClasses}"
|
|
4283
|
+
data-role="block-type-select"
|
|
4284
|
+
>
|
|
4285
|
+
<option value="">Choose a block...</option>
|
|
4286
|
+
${blockOptions}
|
|
4287
|
+
</select>
|
|
4288
|
+
</div>
|
|
4289
|
+
<button
|
|
4290
|
+
type="button"
|
|
4291
|
+
data-action="add-block"
|
|
4292
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
|
|
4293
|
+
>
|
|
4294
|
+
Add Block
|
|
4295
|
+
</button>
|
|
4296
|
+
</div>
|
|
4297
|
+
|
|
4298
|
+
<div class="space-y-4" data-blocks-list>
|
|
4299
|
+
${blockItems || emptyState}
|
|
4300
|
+
</div>
|
|
4301
|
+
|
|
4302
|
+
${templates}
|
|
4303
|
+
</div>
|
|
4304
|
+
${getDragSortableScript()}
|
|
4305
|
+
${getBlocksFieldScript()}
|
|
4306
|
+
`;
|
|
4307
|
+
}
|
|
4308
|
+
function renderStructuredObjectField(field, options, baseClasses, errorClasses) {
|
|
4309
|
+
const { value = {}, pluginStatuses = {} } = options;
|
|
4310
|
+
const opts = field.field_options || {};
|
|
4311
|
+
const properties = opts.properties && typeof opts.properties === "object" ? opts.properties : {};
|
|
4312
|
+
const fieldId = `field-${field.field_name}`;
|
|
4313
|
+
const fieldName = field.field_name;
|
|
4314
|
+
const objectValue = normalizeStructuredObjectValue(value);
|
|
4315
|
+
const subfields = Object.entries(properties).map(
|
|
4316
|
+
([propertyName, propertyConfig]) => renderStructuredSubfield(
|
|
4317
|
+
field,
|
|
4318
|
+
propertyName,
|
|
4319
|
+
propertyConfig,
|
|
4320
|
+
objectValue,
|
|
4321
|
+
pluginStatuses,
|
|
4322
|
+
field.field_name
|
|
4323
|
+
)
|
|
4324
|
+
).join("");
|
|
4325
|
+
return `
|
|
4326
|
+
<div class="space-y-4" data-structured-object data-field-name="${escapeHtml2(fieldName)}">
|
|
4327
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(objectValue))}">
|
|
4328
|
+
<div class="space-y-4" data-structured-object-fields>
|
|
4329
|
+
${subfields}
|
|
4330
|
+
</div>
|
|
4331
|
+
</div>
|
|
4332
|
+
${getStructuredFieldScript()}
|
|
4333
|
+
`;
|
|
4334
|
+
}
|
|
4335
|
+
function renderStructuredArrayField(field, options, baseClasses, errorClasses) {
|
|
4336
|
+
const { value = [], pluginStatuses = {} } = options;
|
|
4337
|
+
const opts = field.field_options || {};
|
|
4338
|
+
const itemsConfig = opts.items && typeof opts.items === "object" ? opts.items : {};
|
|
4339
|
+
const fieldId = `field-${field.field_name}`;
|
|
4340
|
+
const fieldName = field.field_name;
|
|
4341
|
+
const arrayValue = normalizeStructuredArrayValue(value);
|
|
4342
|
+
const items = arrayValue.map(
|
|
4343
|
+
(itemValue, index) => renderStructuredArrayItem(field, itemsConfig, String(index), itemValue, pluginStatuses)
|
|
4344
|
+
).join("");
|
|
4345
|
+
const emptyState = arrayValue.length === 0 ? `
|
|
4346
|
+
<div class="rounded-lg border border-dashed border-zinc-200 dark:border-white/10 px-4 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400" data-structured-empty>
|
|
4347
|
+
No items yet. Add the first item to get started.
|
|
4348
|
+
</div>
|
|
4349
|
+
` : "";
|
|
4350
|
+
return `
|
|
4351
|
+
<div class="space-y-4" data-structured-array data-field-name="${escapeHtml2(fieldName)}">
|
|
4352
|
+
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${escapeHtml2(JSON.stringify(arrayValue))}">
|
|
4353
|
+
|
|
4354
|
+
<div class="flex items-center justify-between gap-3">
|
|
4355
|
+
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
4356
|
+
${escapeHtml2(opts.itemLabel || "Items")}
|
|
4357
|
+
</div>
|
|
4358
|
+
<button
|
|
4359
|
+
type="button"
|
|
4360
|
+
data-action="add-item"
|
|
4361
|
+
class="inline-flex items-center justify-center rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white hover:bg-zinc-800 dark:bg-white/10 dark:hover:bg-white/20"
|
|
4362
|
+
>
|
|
4363
|
+
Add item
|
|
4364
|
+
</button>
|
|
4365
|
+
</div>
|
|
4366
|
+
|
|
4367
|
+
<div class="space-y-4" data-structured-array-list>
|
|
4368
|
+
${items || emptyState}
|
|
4369
|
+
</div>
|
|
4370
|
+
|
|
4371
|
+
<template data-structured-array-template>
|
|
4372
|
+
${renderStructuredArrayItem(field, itemsConfig, "__INDEX__", {}, pluginStatuses)}
|
|
4373
|
+
</template>
|
|
4374
|
+
</div>
|
|
4375
|
+
${getDragSortableScript()}
|
|
4376
|
+
${getStructuredFieldScript()}
|
|
4377
|
+
`;
|
|
4378
|
+
}
|
|
4379
|
+
function renderStructuredArrayItem(field, itemConfig, index, itemValue, pluginStatuses) {
|
|
4380
|
+
const itemFields = renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses);
|
|
4381
|
+
return `
|
|
4382
|
+
<div class="structured-array-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-array-index="${escapeHtml2(index)}" draggable="true">
|
|
4383
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
4384
|
+
<div class="flex items-center gap-3">
|
|
4385
|
+
<div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
|
|
4386
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
4387
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
|
|
4388
|
+
</svg>
|
|
4389
|
+
</div>
|
|
4390
|
+
<div class="text-sm font-semibold text-zinc-900 dark:text-white">
|
|
4391
|
+
Item <span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-array-order-label></span>
|
|
4392
|
+
</div>
|
|
4393
|
+
</div>
|
|
4394
|
+
<div class="flex flex-wrap gap-2 text-xs">
|
|
4395
|
+
<button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item up" title="Move up">
|
|
4396
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
|
|
4397
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
|
|
4398
|
+
</svg>
|
|
4399
|
+
</button>
|
|
4400
|
+
<button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move item down" title="Move down">
|
|
4401
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
|
|
4402
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
|
|
4403
|
+
</svg>
|
|
4404
|
+
</button>
|
|
4405
|
+
<button type="button" data-action="remove-item" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
|
|
4406
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
4407
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 0 00-7.5 0"/>
|
|
4408
|
+
</svg>
|
|
4409
|
+
Delete item
|
|
4410
|
+
</button>
|
|
4411
|
+
</div>
|
|
4412
|
+
</div>
|
|
4413
|
+
<div class="mt-4 space-y-4" data-array-item-fields>
|
|
4414
|
+
${itemFields}
|
|
4415
|
+
</div>
|
|
4416
|
+
</div>
|
|
4417
|
+
`;
|
|
4418
|
+
}
|
|
4419
|
+
function renderStructuredItemFields(field, itemConfig, index, itemValue, pluginStatuses) {
|
|
4420
|
+
const itemType = itemConfig?.type || "string";
|
|
4421
|
+
if (itemType === "object" && itemConfig?.properties && typeof itemConfig.properties === "object") {
|
|
4422
|
+
const fieldPrefix = `array-${field.field_name}-${index}`;
|
|
4423
|
+
return Object.entries(itemConfig.properties).map(
|
|
4424
|
+
([propertyName, propertyConfig]) => renderStructuredSubfield(
|
|
4425
|
+
field,
|
|
4426
|
+
propertyName,
|
|
4427
|
+
propertyConfig,
|
|
4428
|
+
itemValue || {},
|
|
4429
|
+
pluginStatuses,
|
|
4430
|
+
fieldPrefix
|
|
4431
|
+
)
|
|
4432
|
+
).join("");
|
|
3984
4433
|
}
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
};
|
|
3994
|
-
|
|
3995
|
-
|
|
4434
|
+
const normalizedField = normalizeBlockField(itemConfig, "Item");
|
|
4435
|
+
const fieldValue = itemValue ?? normalizedField.defaultValue ?? "";
|
|
4436
|
+
const fieldDefinition = {
|
|
4437
|
+
id: `array-${field.field_name}-${index}-value`,
|
|
4438
|
+
field_name: `array-${field.field_name}-${index}-value`,
|
|
4439
|
+
field_type: normalizedField.type,
|
|
4440
|
+
field_label: normalizedField.label,
|
|
4441
|
+
field_options: normalizedField.options,
|
|
4442
|
+
is_required: normalizedField.required};
|
|
4443
|
+
return `
|
|
4444
|
+
<div class="structured-subfield" data-structured-field="__value" data-field-type="${escapeHtml2(normalizedField.type)}">
|
|
4445
|
+
${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
|
|
4446
|
+
</div>
|
|
4447
|
+
`;
|
|
4448
|
+
}
|
|
4449
|
+
function renderStructuredSubfield(field, propertyName, propertyConfig, objectValue, pluginStatuses, fieldPrefix) {
|
|
4450
|
+
const normalizedField = normalizeBlockField(propertyConfig, propertyName);
|
|
4451
|
+
const fieldValue = objectValue?.[propertyName] ?? normalizedField.defaultValue ?? "";
|
|
4452
|
+
const fieldDefinition = {
|
|
4453
|
+
field_name: `${fieldPrefix}__${propertyName}`,
|
|
4454
|
+
field_type: normalizedField.type,
|
|
4455
|
+
field_label: normalizedField.label,
|
|
4456
|
+
field_options: normalizedField.options,
|
|
4457
|
+
is_required: normalizedField.required};
|
|
4458
|
+
return `
|
|
4459
|
+
<div class="structured-subfield" data-structured-field="${escapeHtml2(propertyName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
|
|
4460
|
+
${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
|
|
4461
|
+
</div>
|
|
4462
|
+
`;
|
|
4463
|
+
}
|
|
4464
|
+
function normalizeStructuredObjectValue(value) {
|
|
4465
|
+
if (!value) return {};
|
|
4466
|
+
if (typeof value === "string") {
|
|
4467
|
+
try {
|
|
4468
|
+
const parsed = JSON.parse(value);
|
|
4469
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4470
|
+
} catch {
|
|
4471
|
+
return {};
|
|
4472
|
+
}
|
|
3996
4473
|
}
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4474
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
4475
|
+
return {};
|
|
4476
|
+
}
|
|
4477
|
+
function normalizeStructuredArrayValue(value) {
|
|
4478
|
+
if (!value) return [];
|
|
4479
|
+
if (Array.isArray(value)) return value;
|
|
4480
|
+
if (typeof value === "string") {
|
|
4481
|
+
try {
|
|
4482
|
+
const parsed = JSON.parse(value);
|
|
4483
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
4484
|
+
} catch {
|
|
4485
|
+
return [];
|
|
4486
|
+
}
|
|
4003
4487
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4488
|
+
return [];
|
|
4489
|
+
}
|
|
4490
|
+
function normalizeBlockDefinitions(rawBlocks) {
|
|
4491
|
+
if (!rawBlocks || typeof rawBlocks !== "object") return [];
|
|
4492
|
+
return Object.entries(rawBlocks).filter(([name, block]) => typeof name === "string" && block && typeof block === "object").map(([name, block]) => ({
|
|
4493
|
+
name,
|
|
4494
|
+
label: block.label || name,
|
|
4495
|
+
description: block.description,
|
|
4496
|
+
properties: block.properties && typeof block.properties === "object" ? block.properties : {}
|
|
4497
|
+
}));
|
|
4498
|
+
}
|
|
4499
|
+
function normalizeBlocksValue(value, discriminator) {
|
|
4500
|
+
const normalizeItem = (item) => {
|
|
4501
|
+
if (!item || typeof item !== "object") return null;
|
|
4502
|
+
if (item[discriminator]) return item;
|
|
4503
|
+
if (item.blockType && item.data && typeof item.data === "object") {
|
|
4504
|
+
return { [discriminator]: item.blockType, ...item.data };
|
|
4505
|
+
}
|
|
4506
|
+
return item;
|
|
4507
|
+
};
|
|
4508
|
+
const fromArray = (items) => items.map(normalizeItem).filter((item) => item && typeof item === "object");
|
|
4509
|
+
if (Array.isArray(value)) return fromArray(value);
|
|
4510
|
+
if (typeof value === "string" && value.trim()) {
|
|
4511
|
+
try {
|
|
4512
|
+
const parsed = JSON.parse(value);
|
|
4513
|
+
return Array.isArray(parsed) ? fromArray(parsed) : [];
|
|
4514
|
+
} catch {
|
|
4515
|
+
return [];
|
|
4516
|
+
}
|
|
4015
4517
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4518
|
+
return [];
|
|
4519
|
+
}
|
|
4520
|
+
function renderBlockTemplate(field, block, discriminator, pluginStatuses) {
|
|
4521
|
+
return `
|
|
4522
|
+
<template data-block-template="${escapeHtml2(block.name)}">
|
|
4523
|
+
${renderBlockCard(field, block, discriminator, "__INDEX__", {}, pluginStatuses)}
|
|
4524
|
+
</template>
|
|
4525
|
+
`;
|
|
4526
|
+
}
|
|
4527
|
+
function renderBlockItem(field, blockValue, blocks, discriminator, index, pluginStatuses) {
|
|
4528
|
+
const blockType = blockValue?.[discriminator] || blockValue?.blockType;
|
|
4529
|
+
const blockDefinition = blocks.find((block) => block.name === blockType);
|
|
4530
|
+
if (!blockDefinition) {
|
|
4531
|
+
return `
|
|
4532
|
+
<div class="rounded-lg border border-amber-200 bg-amber-50/50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200" data-block-raw="${escapeHtml2(JSON.stringify(blockValue || {}))}">
|
|
4533
|
+
Unknown block type: <strong>${escapeHtml2(String(blockType || "unknown"))}</strong>. This block will be preserved as-is.
|
|
4534
|
+
</div>
|
|
4535
|
+
`;
|
|
4022
4536
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4537
|
+
const data = blockValue && typeof blockValue === "object" ? Object.fromEntries(Object.entries(blockValue).filter(([key]) => key !== discriminator)) : {};
|
|
4538
|
+
return renderBlockCard(field, blockDefinition, discriminator, String(index), data, pluginStatuses);
|
|
4539
|
+
}
|
|
4540
|
+
function renderBlockCard(field, block, discriminator, index, data, pluginStatuses) {
|
|
4541
|
+
const blockFields = Object.entries(block.properties).map(([fieldName, fieldConfig]) => {
|
|
4542
|
+
if (fieldConfig?.type === "array" && fieldConfig?.items?.blocks) {
|
|
4543
|
+
return `
|
|
4544
|
+
<div class="rounded-lg border border-dashed border-amber-200 bg-amber-50/50 px-4 py-3 text-xs text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">
|
|
4545
|
+
Nested blocks are not supported yet for "${escapeHtml2(fieldName)}".
|
|
4546
|
+
</div>
|
|
4547
|
+
`;
|
|
4029
4548
|
}
|
|
4030
|
-
|
|
4549
|
+
const normalizedField = normalizeBlockField(fieldConfig, fieldName);
|
|
4550
|
+
const fieldValue = data?.[fieldName] ?? normalizedField.defaultValue ?? "";
|
|
4551
|
+
const fieldDefinition = {
|
|
4552
|
+
id: `block-${field.field_name}-${index}-${fieldName}`,
|
|
4553
|
+
field_name: `block-${field.field_name}-${index}-${fieldName}`,
|
|
4554
|
+
field_type: normalizedField.type,
|
|
4555
|
+
field_label: normalizedField.label,
|
|
4556
|
+
field_options: normalizedField.options,
|
|
4557
|
+
is_required: normalizedField.required};
|
|
4558
|
+
return `
|
|
4559
|
+
<div class="blocks-subfield" data-block-field="${escapeHtml2(fieldName)}" data-field-type="${escapeHtml2(normalizedField.type)}">
|
|
4560
|
+
${renderDynamicField(fieldDefinition, { value: fieldValue, pluginStatuses })}
|
|
4561
|
+
</div>
|
|
4562
|
+
`;
|
|
4563
|
+
}).join("");
|
|
4564
|
+
return `
|
|
4565
|
+
<div class="blocks-item rounded-lg border border-zinc-200 dark:border-white/10 bg-white/60 dark:bg-white/5 p-4 shadow-sm" data-block-type="${escapeHtml2(block.name)}" data-block-discriminator="${escapeHtml2(discriminator)}" draggable="true">
|
|
4566
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
4567
|
+
<div class="flex items-start gap-3">
|
|
4568
|
+
<div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400" data-action="drag-handle" title="Drag to reorder">
|
|
4569
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
4570
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
|
|
4571
|
+
</svg>
|
|
4572
|
+
</div>
|
|
4573
|
+
<div>
|
|
4574
|
+
<div class="text-sm font-semibold text-zinc-900 dark:text-white">
|
|
4575
|
+
${escapeHtml2(block.label)}
|
|
4576
|
+
<span class="ml-2 text-xs font-normal text-zinc-500 dark:text-zinc-400" data-block-order-label></span>
|
|
4577
|
+
</div>
|
|
4578
|
+
${block.description ? `<p class="text-xs text-zinc-500 dark:text-zinc-400">${escapeHtml2(block.description)}</p>` : ""}
|
|
4579
|
+
</div>
|
|
4580
|
+
</div>
|
|
4581
|
+
<div class="flex flex-wrap gap-2 text-xs">
|
|
4582
|
+
<button type="button" data-action="move-up" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block up" title="Move up">
|
|
4583
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
|
|
4584
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6l-4 4m4-4l4 4m-4-4v12"/>
|
|
4585
|
+
</svg>
|
|
4586
|
+
</button>
|
|
4587
|
+
<button type="button" data-action="move-down" class="inline-flex items-center justify-center rounded-md border border-zinc-200 px-2 py-1 text-zinc-600 hover:bg-zinc-100 dark:border-white/10 dark:text-zinc-300 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent dark:disabled:hover:bg-transparent" aria-label="Move block down" title="Move down">
|
|
4588
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="4">
|
|
4589
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18l4-4m-4 4l-4-4m4 4V6"/>
|
|
4590
|
+
</svg>
|
|
4591
|
+
</button>
|
|
4592
|
+
<button type="button" data-action="remove-block" class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-xs font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors">
|
|
4593
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
|
4594
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
|
|
4595
|
+
</svg>
|
|
4596
|
+
Delete block
|
|
4597
|
+
</button>
|
|
4598
|
+
</div>
|
|
4599
|
+
</div>
|
|
4600
|
+
<div class="mt-4 space-y-4">
|
|
4601
|
+
${blockFields}
|
|
4602
|
+
</div>
|
|
4603
|
+
</div>
|
|
4604
|
+
`;
|
|
4605
|
+
}
|
|
4606
|
+
function normalizeBlockField(fieldConfig, fieldName) {
|
|
4607
|
+
const type = fieldConfig?.type || "text";
|
|
4608
|
+
const label = fieldConfig?.title || fieldName;
|
|
4609
|
+
const required = fieldConfig?.required === true;
|
|
4610
|
+
const options = { ...fieldConfig };
|
|
4611
|
+
if (type === "select" && Array.isArray(fieldConfig?.enum)) {
|
|
4612
|
+
options.options = fieldConfig.enum.map((value, index) => ({
|
|
4613
|
+
value,
|
|
4614
|
+
label: fieldConfig.enumLabels?.[index] || value
|
|
4615
|
+
}));
|
|
4031
4616
|
}
|
|
4032
|
-
|
|
4617
|
+
return {
|
|
4618
|
+
type,
|
|
4619
|
+
label,
|
|
4620
|
+
required,
|
|
4621
|
+
defaultValue: fieldConfig?.default,
|
|
4622
|
+
options
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
function getStructuredFieldScript() {
|
|
4626
|
+
return `
|
|
4627
|
+
${getReadFieldValueScript()}
|
|
4628
|
+
<script>
|
|
4629
|
+
if (!window.__sonicStructuredFieldInit) {
|
|
4630
|
+
window.__sonicStructuredFieldInit = true;
|
|
4631
|
+
|
|
4632
|
+
function initializeStructuredFields() {
|
|
4633
|
+
const readFieldValue = window.sonicReadFieldValue;
|
|
4634
|
+
|
|
4635
|
+
const readStructuredValue = (container) => {
|
|
4636
|
+
const fields = Array.from(container.querySelectorAll('.structured-subfield'));
|
|
4637
|
+
if (fields.length === 1 && fields[0].dataset.structuredField === '__value') {
|
|
4638
|
+
return readFieldValue(fields[0]);
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
return fields.reduce((acc, fieldWrapper) => {
|
|
4642
|
+
const fieldName = fieldWrapper.dataset.structuredField;
|
|
4643
|
+
if (!fieldName || fieldName === '__value') return acc;
|
|
4644
|
+
acc[fieldName] = readFieldValue(fieldWrapper);
|
|
4645
|
+
return acc;
|
|
4646
|
+
}, {});
|
|
4647
|
+
};
|
|
4648
|
+
|
|
4649
|
+
document.querySelectorAll('[data-structured-object]').forEach((container) => {
|
|
4650
|
+
if (container.dataset.structuredInitialized === 'true') {
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
container.dataset.structuredInitialized = 'true';
|
|
4654
|
+
const hiddenInput = container.querySelector('input[type="hidden"]');
|
|
4655
|
+
|
|
4656
|
+
const updateHiddenInput = () => {
|
|
4657
|
+
if (!hiddenInput) return;
|
|
4658
|
+
const value = readStructuredValue(container);
|
|
4659
|
+
hiddenInput.value = JSON.stringify(value);
|
|
4660
|
+
};
|
|
4661
|
+
|
|
4662
|
+
container.addEventListener('input', updateHiddenInput);
|
|
4663
|
+
container.addEventListener('change', updateHiddenInput);
|
|
4664
|
+
updateHiddenInput();
|
|
4665
|
+
});
|
|
4666
|
+
|
|
4667
|
+
document.querySelectorAll('[data-structured-array]').forEach((container) => {
|
|
4668
|
+
if (container.dataset.structuredInitialized === 'true') {
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
container.dataset.structuredInitialized = 'true';
|
|
4672
|
+
const list = container.querySelector('[data-structured-array-list]');
|
|
4673
|
+
const hiddenInput = container.querySelector('input[type="hidden"]');
|
|
4674
|
+
const template = container.querySelector('template[data-structured-array-template]');
|
|
4675
|
+
|
|
4676
|
+
const updateOrderLabels = () => {
|
|
4677
|
+
const items = Array.from(container.querySelectorAll('.structured-array-item'));
|
|
4678
|
+
items.forEach((item, index) => {
|
|
4679
|
+
const label = item.querySelector('[data-array-order-label]');
|
|
4680
|
+
if (label) {
|
|
4681
|
+
label.textContent = '#'+ (index + 1);
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4684
|
+
const moveUpButton = item.querySelector('[data-action="move-up"]');
|
|
4685
|
+
if (moveUpButton instanceof HTMLButtonElement) {
|
|
4686
|
+
moveUpButton.disabled = index === 0;
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
const moveDownButton = item.querySelector('[data-action="move-down"]');
|
|
4690
|
+
if (moveDownButton instanceof HTMLButtonElement) {
|
|
4691
|
+
moveDownButton.disabled = index === items.length - 1;
|
|
4692
|
+
}
|
|
4693
|
+
});
|
|
4694
|
+
};
|
|
4695
|
+
|
|
4696
|
+
const updateHiddenInput = () => {
|
|
4697
|
+
if (!hiddenInput || !list) return;
|
|
4698
|
+
const items = Array.from(list.querySelectorAll('.structured-array-item'));
|
|
4699
|
+
const values = items.map((item) => readStructuredValue(item));
|
|
4700
|
+
hiddenInput.value = JSON.stringify(values);
|
|
4701
|
+
|
|
4702
|
+
const emptyState = list.querySelector('[data-structured-empty]');
|
|
4703
|
+
if (emptyState) {
|
|
4704
|
+
emptyState.style.display = values.length === 0 ? 'block' : 'none';
|
|
4705
|
+
}
|
|
4706
|
+
updateOrderLabels();
|
|
4707
|
+
};
|
|
4708
|
+
|
|
4709
|
+
if (typeof window.initializeDragSortable === 'function' && list) {
|
|
4710
|
+
window.initializeDragSortable(list, {
|
|
4711
|
+
itemSelector: '.structured-array-item',
|
|
4712
|
+
handleSelector: '[data-action="drag-handle"]',
|
|
4713
|
+
onUpdate: updateHiddenInput
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
container.addEventListener('click', (event) => {
|
|
4718
|
+
const target = event.target;
|
|
4719
|
+
if (!(target instanceof Element)) return;
|
|
4720
|
+
const actionButton = target.closest('[data-action]');
|
|
4721
|
+
if (!actionButton || actionButton.hasAttribute('disabled')) return;
|
|
4722
|
+
|
|
4723
|
+
const action = actionButton.getAttribute('data-action');
|
|
4724
|
+
|
|
4725
|
+
if (action === 'add-item') {
|
|
4726
|
+
if (!list || !template) return;
|
|
4727
|
+
const nextIndex = list.querySelectorAll('.structured-array-item').length;
|
|
4728
|
+
const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
|
|
4729
|
+
list.insertAdjacentHTML('beforeend', html);
|
|
4730
|
+
if (typeof initializeTinyMCE === 'function') {
|
|
4731
|
+
initializeTinyMCE();
|
|
4732
|
+
}
|
|
4733
|
+
if (typeof window.initializeQuillEditors === 'function') {
|
|
4734
|
+
window.initializeQuillEditors();
|
|
4735
|
+
}
|
|
4736
|
+
if (typeof initializeMDXEditor === 'function') {
|
|
4737
|
+
initializeMDXEditor();
|
|
4738
|
+
}
|
|
4739
|
+
updateHiddenInput();
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
const item = actionButton.closest('.structured-array-item');
|
|
4744
|
+
if (!item || !list) return;
|
|
4745
|
+
|
|
4746
|
+
if (action === 'remove-item') {
|
|
4747
|
+
item.remove();
|
|
4748
|
+
updateHiddenInput();
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
if (action === 'move-up') {
|
|
4753
|
+
const previous = item.previousElementSibling;
|
|
4754
|
+
if (previous) {
|
|
4755
|
+
list.insertBefore(item, previous);
|
|
4756
|
+
updateHiddenInput();
|
|
4757
|
+
}
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
if (action === 'move-down') {
|
|
4762
|
+
const next = item.nextElementSibling;
|
|
4763
|
+
if (next) {
|
|
4764
|
+
list.insertBefore(next, item);
|
|
4765
|
+
updateHiddenInput();
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
|
|
4770
|
+
container.addEventListener('input', (event) => {
|
|
4771
|
+
const target = event.target;
|
|
4772
|
+
if (!(target instanceof Element)) return;
|
|
4773
|
+
if (target.closest('[data-structured-array-list]')) {
|
|
4774
|
+
updateHiddenInput();
|
|
4775
|
+
}
|
|
4776
|
+
});
|
|
4777
|
+
|
|
4778
|
+
container.addEventListener('change', (event) => {
|
|
4779
|
+
const target = event.target;
|
|
4780
|
+
if (!(target instanceof Element)) return;
|
|
4781
|
+
if (target.closest('[data-structured-array-list]')) {
|
|
4782
|
+
updateHiddenInput();
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4785
|
+
|
|
4786
|
+
updateHiddenInput();
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4790
|
+
window.initializeStructuredFields = initializeStructuredFields;
|
|
4791
|
+
|
|
4792
|
+
if (document.readyState === 'loading') {
|
|
4793
|
+
document.addEventListener('DOMContentLoaded', initializeStructuredFields);
|
|
4794
|
+
} else {
|
|
4795
|
+
initializeStructuredFields();
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
document.addEventListener('htmx:afterSwap', function() {
|
|
4799
|
+
setTimeout(initializeStructuredFields, 50);
|
|
4800
|
+
});
|
|
4801
|
+
} else if (typeof window.initializeStructuredFields === 'function') {
|
|
4802
|
+
window.initializeStructuredFields();
|
|
4803
|
+
}
|
|
4804
|
+
</script>
|
|
4805
|
+
`;
|
|
4806
|
+
}
|
|
4807
|
+
function getBlocksFieldScript() {
|
|
4808
|
+
return `
|
|
4809
|
+
${getReadFieldValueScript()}
|
|
4810
|
+
<script>
|
|
4811
|
+
if (!window.__sonicBlocksFieldInit) {
|
|
4812
|
+
window.__sonicBlocksFieldInit = true;
|
|
4813
|
+
|
|
4814
|
+
function initializeBlocksFields() {
|
|
4815
|
+
document.querySelectorAll('.blocks-field').forEach((container) => {
|
|
4816
|
+
if (container.dataset.blocksInitialized === 'true') {
|
|
4817
|
+
return;
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
container.dataset.blocksInitialized = 'true';
|
|
4821
|
+
const list = container.querySelector('[data-blocks-list]');
|
|
4822
|
+
const hiddenInput = container.querySelector('input[type="hidden"]');
|
|
4823
|
+
const typeSelect = container.querySelector('[data-role="block-type-select"]');
|
|
4824
|
+
const discriminator = container.dataset.blocksDiscriminator || 'blockType';
|
|
4825
|
+
|
|
4826
|
+
const updateOrderLabels = () => {
|
|
4827
|
+
const items = Array.from(container.querySelectorAll('.blocks-item'));
|
|
4828
|
+
items.forEach((item, index) => {
|
|
4829
|
+
const label = item.querySelector('[data-block-order-label]');
|
|
4830
|
+
if (label) {
|
|
4831
|
+
label.textContent = '#'+ (index + 1);
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4834
|
+
const moveUpButton = item.querySelector('[data-action="move-up"]');
|
|
4835
|
+
if (moveUpButton instanceof HTMLButtonElement) {
|
|
4836
|
+
moveUpButton.disabled = index === 0;
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
const moveDownButton = item.querySelector('[data-action="move-down"]');
|
|
4840
|
+
if (moveDownButton instanceof HTMLButtonElement) {
|
|
4841
|
+
moveDownButton.disabled = index === items.length - 1;
|
|
4842
|
+
}
|
|
4843
|
+
});
|
|
4844
|
+
};
|
|
4845
|
+
|
|
4846
|
+
const readFieldValue = window.sonicReadFieldValue;
|
|
4847
|
+
|
|
4848
|
+
const readBlockItem = (item) => {
|
|
4849
|
+
if (item.dataset.blockRaw) {
|
|
4850
|
+
try {
|
|
4851
|
+
return JSON.parse(item.dataset.blockRaw);
|
|
4852
|
+
} catch (error) {
|
|
4853
|
+
return {};
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
const blockType = item.dataset.blockType;
|
|
4858
|
+
const data = {};
|
|
4859
|
+
|
|
4860
|
+
item.querySelectorAll('.blocks-subfield').forEach((fieldWrapper) => {
|
|
4861
|
+
const fieldName = fieldWrapper.dataset.blockField;
|
|
4862
|
+
if (!fieldName) {
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
data[fieldName] = readFieldValue(fieldWrapper);
|
|
4866
|
+
});
|
|
4867
|
+
|
|
4868
|
+
return { [discriminator]: blockType, ...data };
|
|
4869
|
+
};
|
|
4870
|
+
|
|
4871
|
+
const updateHiddenInput = () => {
|
|
4872
|
+
if (!hiddenInput || !list) return;
|
|
4873
|
+
const items = Array.from(list.querySelectorAll('.blocks-item, [data-block-raw]'));
|
|
4874
|
+
const blocksData = items.map((item) => readBlockItem(item));
|
|
4875
|
+
hiddenInput.value = JSON.stringify(blocksData);
|
|
4876
|
+
|
|
4877
|
+
const emptyState = list.querySelector('[data-blocks-empty]');
|
|
4878
|
+
if (emptyState) {
|
|
4879
|
+
emptyState.style.display = blocksData.length === 0 ? 'block' : 'none';
|
|
4880
|
+
}
|
|
4881
|
+
updateOrderLabels();
|
|
4882
|
+
};
|
|
4883
|
+
|
|
4884
|
+
const initializeEditors = () => {
|
|
4885
|
+
if (typeof initializeTinyMCE === 'function') {
|
|
4886
|
+
initializeTinyMCE();
|
|
4887
|
+
}
|
|
4888
|
+
if (typeof window.initializeQuillEditors === 'function') {
|
|
4889
|
+
window.initializeQuillEditors();
|
|
4890
|
+
}
|
|
4891
|
+
if (typeof initializeMDXEditor === 'function') {
|
|
4892
|
+
initializeMDXEditor();
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
|
|
4896
|
+
if (typeof window.initializeDragSortable === 'function' && list) {
|
|
4897
|
+
window.initializeDragSortable(list, {
|
|
4898
|
+
itemSelector: '.blocks-item',
|
|
4899
|
+
handleSelector: '[data-action="drag-handle"]',
|
|
4900
|
+
onUpdate: updateHiddenInput
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
container.addEventListener('click', (event) => {
|
|
4905
|
+
const target = event.target;
|
|
4906
|
+
if (!(target instanceof Element)) return;
|
|
4907
|
+
const actionButton = target.closest('[data-action]');
|
|
4908
|
+
if (!actionButton) return;
|
|
4909
|
+
|
|
4910
|
+
if (actionButton.hasAttribute('disabled')) {
|
|
4911
|
+
return;
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
const action = actionButton.getAttribute('data-action');
|
|
4915
|
+
if (action === 'add-block') {
|
|
4916
|
+
const blockType = typeSelect ? typeSelect.value : '';
|
|
4917
|
+
if (!blockType || !list) return;
|
|
4918
|
+
const template = container.querySelector('template[data-block-template="' + blockType + '"]');
|
|
4919
|
+
if (!template) return;
|
|
4920
|
+
|
|
4921
|
+
const nextIndex = list.querySelectorAll('.blocks-item').length;
|
|
4922
|
+
const html = template.innerHTML.replace(/__INDEX__/g, String(nextIndex));
|
|
4923
|
+
list.insertAdjacentHTML('beforeend', html);
|
|
4924
|
+
if (typeSelect) {
|
|
4925
|
+
typeSelect.value = '';
|
|
4926
|
+
}
|
|
4927
|
+
initializeEditors();
|
|
4928
|
+
if (typeof window.initializeStructuredFields === 'function') {
|
|
4929
|
+
window.initializeStructuredFields();
|
|
4930
|
+
}
|
|
4931
|
+
updateHiddenInput();
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
const item = actionButton.closest('.blocks-item');
|
|
4936
|
+
if (!item || !list) return;
|
|
4937
|
+
|
|
4938
|
+
if (action === 'remove-block') {
|
|
4939
|
+
item.remove();
|
|
4940
|
+
updateHiddenInput();
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
if (action === 'move-up') {
|
|
4945
|
+
const previous = item.previousElementSibling;
|
|
4946
|
+
if (previous) {
|
|
4947
|
+
list.insertBefore(item, previous);
|
|
4948
|
+
updateHiddenInput();
|
|
4949
|
+
}
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
if (action === 'move-down') {
|
|
4954
|
+
const next = item.nextElementSibling;
|
|
4955
|
+
if (next) {
|
|
4956
|
+
list.insertBefore(next, item);
|
|
4957
|
+
updateHiddenInput();
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
});
|
|
4961
|
+
|
|
4962
|
+
container.addEventListener('input', (event) => {
|
|
4963
|
+
const target = event.target;
|
|
4964
|
+
if (!(target instanceof Element)) return;
|
|
4965
|
+
if (target.closest('[data-blocks-list]')) {
|
|
4966
|
+
updateHiddenInput();
|
|
4967
|
+
}
|
|
4968
|
+
});
|
|
4969
|
+
|
|
4970
|
+
container.addEventListener('change', (event) => {
|
|
4971
|
+
const target = event.target;
|
|
4972
|
+
if (!(target instanceof Element)) return;
|
|
4973
|
+
if (target.closest('[data-blocks-list]')) {
|
|
4974
|
+
updateHiddenInput();
|
|
4975
|
+
}
|
|
4976
|
+
});
|
|
4977
|
+
|
|
4978
|
+
updateHiddenInput();
|
|
4979
|
+
});
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
window.initializeBlocksFields = initializeBlocksFields;
|
|
4983
|
+
|
|
4984
|
+
if (document.readyState === 'loading') {
|
|
4985
|
+
document.addEventListener('DOMContentLoaded', initializeBlocksFields);
|
|
4986
|
+
} else {
|
|
4987
|
+
initializeBlocksFields();
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
document.addEventListener('htmx:afterSwap', function() {
|
|
4991
|
+
setTimeout(initializeBlocksFields, 50);
|
|
4992
|
+
});
|
|
4993
|
+
} else if (typeof window.initializeBlocksFields === 'function') {
|
|
4994
|
+
window.initializeBlocksFields();
|
|
4995
|
+
}
|
|
4996
|
+
</script>
|
|
4997
|
+
`;
|
|
4998
|
+
}
|
|
4999
|
+
function escapeHtml2(text) {
|
|
5000
|
+
if (typeof text !== "string") return String(text || "");
|
|
5001
|
+
return text.replace(/[&<>"']/g, (char) => ({
|
|
5002
|
+
"&": "&",
|
|
5003
|
+
"<": "<",
|
|
5004
|
+
">": ">",
|
|
5005
|
+
'"': """,
|
|
5006
|
+
"'": "'"
|
|
5007
|
+
})[char] || char);
|
|
5008
|
+
}
|
|
4033
5009
|
|
|
4034
5010
|
// src/plugins/available/tinymce-plugin/index.ts
|
|
4035
5011
|
var builder = PluginBuilder.create({
|
|
@@ -4573,17 +5549,24 @@ function renderContentFormPage(data) {
|
|
|
4573
5549
|
const coreFieldsHTML = coreFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4574
5550
|
value: getFieldValue(field.field_name),
|
|
4575
5551
|
errors: data.validationErrors?.[field.field_name] || [],
|
|
4576
|
-
pluginStatuses
|
|
5552
|
+
pluginStatuses,
|
|
5553
|
+
collectionId: data.collection.id,
|
|
5554
|
+
contentId: data.id
|
|
5555
|
+
// Pass content ID when editing
|
|
4577
5556
|
}));
|
|
4578
5557
|
const contentFieldsHTML = contentFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4579
5558
|
value: getFieldValue(field.field_name),
|
|
4580
5559
|
errors: data.validationErrors?.[field.field_name] || [],
|
|
4581
|
-
pluginStatuses
|
|
5560
|
+
pluginStatuses,
|
|
5561
|
+
collectionId: data.collection.id,
|
|
5562
|
+
contentId: data.id
|
|
4582
5563
|
}));
|
|
4583
5564
|
const metaFieldsHTML = metaFields.sort((a, b) => a.field_order - b.field_order).map((field) => renderDynamicField(field, {
|
|
4584
5565
|
value: getFieldValue(field.field_name),
|
|
4585
5566
|
errors: data.validationErrors?.[field.field_name] || [],
|
|
4586
|
-
pluginStatuses
|
|
5567
|
+
pluginStatuses,
|
|
5568
|
+
collectionId: data.collection.id,
|
|
5569
|
+
contentId: data.id
|
|
4587
5570
|
}));
|
|
4588
5571
|
const pageContent = `
|
|
4589
5572
|
<div class="space-y-6">
|
|
@@ -4969,71 +5952,371 @@ function renderContentFormPage(data) {
|
|
|
4969
5952
|
}
|
|
4970
5953
|
}
|
|
4971
5954
|
|
|
4972
|
-
// Close modal
|
|
4973
|
-
closeMediaSelector();
|
|
4974
|
-
}
|
|
5955
|
+
// Close modal
|
|
5956
|
+
closeMediaSelector();
|
|
5957
|
+
}
|
|
5958
|
+
|
|
5959
|
+
function clearMediaField(fieldId) {
|
|
5960
|
+
const hiddenInput = document.getElementById(fieldId);
|
|
5961
|
+
const preview = document.getElementById(fieldId + '-preview');
|
|
5962
|
+
|
|
5963
|
+
if (hiddenInput) {
|
|
5964
|
+
hiddenInput.value = '';
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
if (preview) {
|
|
5968
|
+
// Clear all children if it's a grid, or hide it
|
|
5969
|
+
if (preview.classList.contains('media-preview-grid')) {
|
|
5970
|
+
preview.innerHTML = '';
|
|
5971
|
+
}
|
|
5972
|
+
preview.classList.add('hidden');
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
// Global function to remove a single media from multiple selection
|
|
5977
|
+
window.removeMediaFromMultiple = function(fieldId, urlToRemove) {
|
|
5978
|
+
const hiddenInput = document.getElementById(fieldId);
|
|
5979
|
+
if (!hiddenInput) return;
|
|
5980
|
+
|
|
5981
|
+
const values = hiddenInput.value.split(',').filter(url => url !== urlToRemove);
|
|
5982
|
+
hiddenInput.value = values.join(',');
|
|
5983
|
+
|
|
5984
|
+
// Remove preview item
|
|
5985
|
+
const previewItem = document.querySelector(\`[data-url="\${urlToRemove}"]\`);
|
|
5986
|
+
if (previewItem) {
|
|
5987
|
+
previewItem.remove();
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5990
|
+
// Hide preview grid if empty
|
|
5991
|
+
if (values.length === 0) {
|
|
5992
|
+
const preview = document.getElementById(fieldId + '-preview');
|
|
5993
|
+
if (preview) {
|
|
5994
|
+
preview.classList.add('hidden');
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
};
|
|
5998
|
+
|
|
5999
|
+
// Global function called by media selector buttons
|
|
6000
|
+
window.selectMediaFile = function(mediaId, mediaUrl, filename) {
|
|
6001
|
+
if (!currentMediaFieldId) {
|
|
6002
|
+
console.error('No field ID set for media selection');
|
|
6003
|
+
return;
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
const fieldId = currentMediaFieldId;
|
|
6007
|
+
|
|
6008
|
+
// Set the hidden input value to the media URL (not ID)
|
|
6009
|
+
const hiddenInput = document.getElementById(fieldId);
|
|
6010
|
+
if (hiddenInput) {
|
|
6011
|
+
hiddenInput.value = mediaUrl;
|
|
6012
|
+
}
|
|
6013
|
+
|
|
6014
|
+
// Update the preview
|
|
6015
|
+
const preview = document.getElementById(fieldId + '-preview');
|
|
6016
|
+
if (preview) {
|
|
6017
|
+
preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
|
|
6018
|
+
preview.classList.remove('hidden');
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
// Show the remove button by finding the media actions container and updating it
|
|
6022
|
+
const mediaField = hiddenInput?.closest('.media-field-container');
|
|
6023
|
+
if (mediaField) {
|
|
6024
|
+
const actionsDiv = mediaField.querySelector('.media-actions');
|
|
6025
|
+
if (actionsDiv && !actionsDiv.querySelector('button:has-text("Remove")')) {
|
|
6026
|
+
const removeBtn = document.createElement('button');
|
|
6027
|
+
removeBtn.type = 'button';
|
|
6028
|
+
removeBtn.onclick = () => clearMediaField(fieldId);
|
|
6029
|
+
removeBtn.className = 'inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all';
|
|
6030
|
+
removeBtn.textContent = 'Remove';
|
|
6031
|
+
actionsDiv.appendChild(removeBtn);
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
|
|
6035
|
+
// DON'T close the modal - let user click OK button
|
|
6036
|
+
// Visual feedback: highlight the selected item
|
|
6037
|
+
document.querySelectorAll('#media-selector-grid [data-media-id]').forEach(el => {
|
|
6038
|
+
el.classList.remove('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
|
|
6039
|
+
});
|
|
6040
|
+
const selectedItem = document.querySelector(\`#media-selector-grid [data-media-id="\${mediaId}"]\`);
|
|
6041
|
+
if (selectedItem) {
|
|
6042
|
+
selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
|
|
6043
|
+
}
|
|
6044
|
+
};
|
|
6045
|
+
|
|
6046
|
+
function setMediaField(fieldId, mediaUrl) {
|
|
6047
|
+
document.getElementById(fieldId).value = mediaUrl;
|
|
6048
|
+
const preview = document.getElementById(fieldId + '-preview');
|
|
6049
|
+
preview.innerHTML = \`<img src="\${mediaUrl}" alt="Selected media" class="w-32 h-32 object-cover rounded-lg ring-1 ring-zinc-950/10 dark:ring-white/10">\`;
|
|
6050
|
+
preview.classList.remove('hidden');
|
|
6051
|
+
|
|
6052
|
+
// Close modal
|
|
6053
|
+
document.querySelector('.fixed.inset-0')?.remove();
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
// Reference field functions
|
|
6057
|
+
let currentReferenceFieldId = null;
|
|
6058
|
+
let referenceSearchTimeout = null;
|
|
6059
|
+
|
|
6060
|
+
function getReferenceContainer(fieldId) {
|
|
6061
|
+
const input = document.getElementById(fieldId);
|
|
6062
|
+
return input ? input.closest('[data-reference-field]') : null;
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
function getReferenceCollections(container) {
|
|
6066
|
+
if (!container) return [];
|
|
6067
|
+
const rawCollections = container.dataset.referenceCollections || '';
|
|
6068
|
+
const collections = rawCollections
|
|
6069
|
+
.split(',')
|
|
6070
|
+
.map((value) => value.trim())
|
|
6071
|
+
.filter(Boolean);
|
|
6072
|
+
if (collections.length > 0) {
|
|
6073
|
+
return collections;
|
|
6074
|
+
}
|
|
6075
|
+
const singleCollection = container.dataset.referenceCollection;
|
|
6076
|
+
return singleCollection ? [singleCollection] : [];
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
async function fetchReferenceItems(collections, search = '', limit = 20) {
|
|
6080
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
6081
|
+
collections.forEach((collection) => params.append('collection', collection));
|
|
6082
|
+
if (search) {
|
|
6083
|
+
params.set('search', search);
|
|
6084
|
+
}
|
|
6085
|
+
const response = await fetch('/admin/api/references?' + params.toString());
|
|
6086
|
+
if (!response.ok) {
|
|
6087
|
+
throw new Error('Failed to load references');
|
|
6088
|
+
}
|
|
6089
|
+
const data = await response.json();
|
|
6090
|
+
return data?.data || [];
|
|
6091
|
+
}
|
|
6092
|
+
|
|
6093
|
+
async function fetchReferenceById(collections, id) {
|
|
6094
|
+
if (!id) return null;
|
|
6095
|
+
const params = new URLSearchParams({ id });
|
|
6096
|
+
collections.forEach((collection) => params.append('collection', collection));
|
|
6097
|
+
const response = await fetch('/admin/api/references?' + params.toString());
|
|
6098
|
+
if (!response.ok) {
|
|
6099
|
+
return null;
|
|
6100
|
+
}
|
|
6101
|
+
const data = await response.json();
|
|
6102
|
+
return data?.data || null;
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
function renderReferenceDisplay(container, item, fallbackMessage = 'No reference selected.') {
|
|
6106
|
+
const display = container.querySelector('[data-reference-display]');
|
|
6107
|
+
const removeButton = container.querySelector('[data-reference-clear]');
|
|
6108
|
+
if (!display) return;
|
|
6109
|
+
|
|
6110
|
+
display.innerHTML = '';
|
|
6111
|
+
|
|
6112
|
+
if (!item) {
|
|
6113
|
+
display.textContent = fallbackMessage;
|
|
6114
|
+
if (removeButton) {
|
|
6115
|
+
removeButton.disabled = true;
|
|
6116
|
+
}
|
|
6117
|
+
return;
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
const title = item.title || item.slug || item.id || 'Untitled';
|
|
6121
|
+
const titleEl = document.createElement('div');
|
|
6122
|
+
titleEl.className = 'font-medium text-zinc-900 dark:text-white';
|
|
6123
|
+
titleEl.textContent = title;
|
|
6124
|
+
|
|
6125
|
+
display.appendChild(titleEl);
|
|
6126
|
+
|
|
6127
|
+
const metaRow = document.createElement('div');
|
|
6128
|
+
metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
|
|
6129
|
+
|
|
6130
|
+
if (item.collection?.display_name || item.collection?.name) {
|
|
6131
|
+
const collectionLabel = document.createElement('span');
|
|
6132
|
+
collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
|
|
6133
|
+
collectionLabel.textContent = item.collection.display_name || item.collection.name;
|
|
6134
|
+
metaRow.appendChild(collectionLabel);
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
if (item.slug) {
|
|
6138
|
+
const slugEl = document.createElement('span');
|
|
6139
|
+
slugEl.textContent = item.slug;
|
|
6140
|
+
metaRow.appendChild(slugEl);
|
|
6141
|
+
}
|
|
6142
|
+
|
|
6143
|
+
if (metaRow.childElementCount > 0) {
|
|
6144
|
+
display.appendChild(metaRow);
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
if (removeButton) {
|
|
6148
|
+
removeButton.disabled = false;
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
function updateReferenceField(fieldId, item) {
|
|
6153
|
+
const input = document.getElementById(fieldId);
|
|
6154
|
+
const container = getReferenceContainer(fieldId);
|
|
6155
|
+
if (!input || !container) return;
|
|
6156
|
+
|
|
6157
|
+
input.value = item?.id || '';
|
|
6158
|
+
renderReferenceDisplay(container, item, 'No reference selected.');
|
|
6159
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
6160
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
function clearReferenceField(fieldId) {
|
|
6164
|
+
updateReferenceField(fieldId, null);
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
function closeReferenceSelector() {
|
|
6168
|
+
const modal = document.getElementById('reference-selector-modal');
|
|
6169
|
+
if (modal) {
|
|
6170
|
+
modal.remove();
|
|
6171
|
+
}
|
|
6172
|
+
currentReferenceFieldId = null;
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6175
|
+
function openReferenceSelector(fieldId) {
|
|
6176
|
+
const container = getReferenceContainer(fieldId);
|
|
6177
|
+
const collections = getReferenceCollections(container);
|
|
6178
|
+
if (!container || collections.length === 0) {
|
|
6179
|
+
console.error('Reference collection is missing for field', fieldId);
|
|
6180
|
+
return;
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
currentReferenceFieldId = fieldId;
|
|
6184
|
+
|
|
6185
|
+
const modal = document.createElement('div');
|
|
6186
|
+
modal.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50';
|
|
6187
|
+
modal.id = 'reference-selector-modal';
|
|
6188
|
+
modal.innerHTML = \`
|
|
6189
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
6190
|
+
<div class="flex items-center justify-between gap-3">
|
|
6191
|
+
<h3 class="text-lg font-semibold text-zinc-950 dark:text-white">Select Reference</h3>
|
|
6192
|
+
<button
|
|
6193
|
+
type="button"
|
|
6194
|
+
onclick="closeReferenceSelector()"
|
|
6195
|
+
class="rounded-md text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
|
|
6196
|
+
aria-label="Close"
|
|
6197
|
+
>
|
|
6198
|
+
\u2715
|
|
6199
|
+
</button>
|
|
6200
|
+
</div>
|
|
6201
|
+
<div class="mt-4">
|
|
6202
|
+
<input
|
|
6203
|
+
type="search"
|
|
6204
|
+
id="reference-search-input"
|
|
6205
|
+
placeholder="Search by title or slug..."
|
|
6206
|
+
class="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 dark:border-white/10 dark:bg-zinc-900 dark:text-white"
|
|
6207
|
+
>
|
|
6208
|
+
</div>
|
|
6209
|
+
<div id="reference-results" class="mt-4 space-y-2"></div>
|
|
6210
|
+
<div class="mt-4 flex justify-end">
|
|
6211
|
+
<button
|
|
6212
|
+
type="button"
|
|
6213
|
+
onclick="closeReferenceSelector()"
|
|
6214
|
+
class="rounded-lg bg-zinc-950 dark:bg-white px-4 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors"
|
|
6215
|
+
>
|
|
6216
|
+
Close
|
|
6217
|
+
</button>
|
|
6218
|
+
</div>
|
|
6219
|
+
</div>
|
|
6220
|
+
\`;
|
|
6221
|
+
|
|
6222
|
+
document.body.appendChild(modal);
|
|
6223
|
+
|
|
6224
|
+
const resultsContainer = modal.querySelector('#reference-results');
|
|
6225
|
+
const searchInput = modal.querySelector('#reference-search-input');
|
|
6226
|
+
|
|
6227
|
+
const renderResults = (items) => {
|
|
6228
|
+
resultsContainer.innerHTML = '';
|
|
6229
|
+
if (!items || items.length === 0) {
|
|
6230
|
+
resultsContainer.innerHTML = '<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-white/10 dark:text-zinc-400">No items found.</div>';
|
|
6231
|
+
return;
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6234
|
+
const selectedId = document.getElementById(fieldId)?.value;
|
|
6235
|
+
|
|
6236
|
+
items.forEach((item) => {
|
|
6237
|
+
const button = document.createElement('button');
|
|
6238
|
+
button.type = 'button';
|
|
6239
|
+
button.className = 'w-full text-left rounded-lg border border-zinc-200 px-4 py-3 text-sm text-zinc-700 hover:bg-zinc-50 dark:border-white/10 dark:text-zinc-200 dark:hover:bg-white/5';
|
|
6240
|
+
if (item.id === selectedId) {
|
|
6241
|
+
button.classList.add('ring-2', 'ring-cyan-500', 'dark:ring-cyan-400');
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
const title = item.title || item.slug || item.id || 'Untitled';
|
|
6245
|
+
const titleEl = document.createElement('div');
|
|
6246
|
+
titleEl.className = 'font-medium text-zinc-900 dark:text-white';
|
|
6247
|
+
titleEl.textContent = title;
|
|
4975
6248
|
|
|
4976
|
-
|
|
4977
|
-
document.getElementById(fieldId).value = '';
|
|
4978
|
-
document.getElementById(fieldId + '-preview').classList.add('hidden');
|
|
4979
|
-
}
|
|
6249
|
+
button.appendChild(titleEl);
|
|
4980
6250
|
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
if (!currentMediaFieldId) {
|
|
4984
|
-
console.error('No field ID set for media selection');
|
|
4985
|
-
return;
|
|
4986
|
-
}
|
|
6251
|
+
const metaRow = document.createElement('div');
|
|
6252
|
+
metaRow.className = 'mt-1 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400';
|
|
4987
6253
|
|
|
4988
|
-
|
|
6254
|
+
if (item.collection?.display_name || item.collection?.name) {
|
|
6255
|
+
const collectionLabel = document.createElement('span');
|
|
6256
|
+
collectionLabel.className = 'inline-flex items-center rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-600 dark:bg-white/10 dark:text-zinc-200';
|
|
6257
|
+
collectionLabel.textContent = item.collection.display_name || item.collection.name;
|
|
6258
|
+
metaRow.appendChild(collectionLabel);
|
|
6259
|
+
}
|
|
4989
6260
|
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
6261
|
+
if (item.slug) {
|
|
6262
|
+
const slugEl = document.createElement('span');
|
|
6263
|
+
slugEl.textContent = item.slug;
|
|
6264
|
+
metaRow.appendChild(slugEl);
|
|
6265
|
+
}
|
|
4995
6266
|
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
preview.innerHTML = \`<img src="\${mediaUrl}" alt="\${filename}" class="w-32 h-32 object-cover rounded-lg border border-white/20">\`;
|
|
5000
|
-
preview.classList.remove('hidden');
|
|
5001
|
-
}
|
|
6267
|
+
if (metaRow.childElementCount > 0) {
|
|
6268
|
+
button.appendChild(metaRow);
|
|
6269
|
+
}
|
|
5002
6270
|
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
6271
|
+
button.addEventListener('click', () => {
|
|
6272
|
+
updateReferenceField(fieldId, item);
|
|
6273
|
+
closeReferenceSelector();
|
|
6274
|
+
});
|
|
6275
|
+
|
|
6276
|
+
resultsContainer.appendChild(button);
|
|
6277
|
+
});
|
|
6278
|
+
};
|
|
6279
|
+
|
|
6280
|
+
const loadResults = async (searchValue = '') => {
|
|
6281
|
+
try {
|
|
6282
|
+
const items = await fetchReferenceItems(collections, searchValue);
|
|
6283
|
+
renderResults(items);
|
|
6284
|
+
} catch (error) {
|
|
6285
|
+
resultsContainer.innerHTML = '<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200">Failed to load references.</div>';
|
|
5014
6286
|
}
|
|
5015
|
-
}
|
|
6287
|
+
};
|
|
5016
6288
|
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
6289
|
+
loadResults();
|
|
6290
|
+
|
|
6291
|
+
searchInput.addEventListener('input', () => {
|
|
6292
|
+
if (referenceSearchTimeout) {
|
|
6293
|
+
clearTimeout(referenceSearchTimeout);
|
|
6294
|
+
}
|
|
6295
|
+
referenceSearchTimeout = setTimeout(() => {
|
|
6296
|
+
loadResults(searchInput.value.trim());
|
|
6297
|
+
}, 250);
|
|
5021
6298
|
});
|
|
5022
|
-
|
|
5023
|
-
if (selectedItem) {
|
|
5024
|
-
selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
|
|
5025
|
-
}
|
|
5026
|
-
};
|
|
6299
|
+
}
|
|
5027
6300
|
|
|
5028
|
-
|
|
5029
|
-
document.
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
6301
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
6302
|
+
document.querySelectorAll('[data-reference-field]').forEach(async (container) => {
|
|
6303
|
+
const input = container.querySelector('input[type="hidden"]');
|
|
6304
|
+
const collections = getReferenceCollections(container);
|
|
6305
|
+
if (!input || collections.length === 0) return;
|
|
5033
6306
|
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
6307
|
+
if (!input.value) {
|
|
6308
|
+
renderReferenceDisplay(container, null, 'No reference selected.');
|
|
6309
|
+
return;
|
|
6310
|
+
}
|
|
6311
|
+
|
|
6312
|
+
const item = await fetchReferenceById(collections, input.value);
|
|
6313
|
+
if (item) {
|
|
6314
|
+
renderReferenceDisplay(container, item);
|
|
6315
|
+
} else {
|
|
6316
|
+
renderReferenceDisplay(container, null, 'Reference not found.');
|
|
6317
|
+
}
|
|
6318
|
+
});
|
|
6319
|
+
});
|
|
5037
6320
|
|
|
5038
6321
|
// Custom select options
|
|
5039
6322
|
function addCustomOption(input, selectId) {
|
|
@@ -5200,7 +6483,7 @@ function renderContentListPage(data) {
|
|
|
5200
6483
|
if (data.search) urlParams.set("search", data.search);
|
|
5201
6484
|
if (data.page && data.page !== 1) urlParams.set("page", data.page.toString());
|
|
5202
6485
|
const currentParams = urlParams.toString();
|
|
5203
|
-
|
|
6486
|
+
data.modelName !== "all" || data.status !== "all" || !!data.search;
|
|
5204
6487
|
const filterBarData = {
|
|
5205
6488
|
filters: [
|
|
5206
6489
|
{
|
|
@@ -5230,6 +6513,11 @@ function renderContentListPage(data) {
|
|
|
5230
6513
|
}
|
|
5231
6514
|
],
|
|
5232
6515
|
actions: [
|
|
6516
|
+
{
|
|
6517
|
+
label: "Advanced Search",
|
|
6518
|
+
className: "btn-primary",
|
|
6519
|
+
onclick: "openAdvancedSearch()"
|
|
6520
|
+
},
|
|
5233
6521
|
{
|
|
5234
6522
|
label: "Refresh",
|
|
5235
6523
|
className: "btn-secondary",
|
|
@@ -5381,12 +6669,57 @@ function renderContentListPage(data) {
|
|
|
5381
6669
|
<div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 rounded-xl">
|
|
5382
6670
|
<div class="px-6 py-5">
|
|
5383
6671
|
<div class="flex items-center justify-between">
|
|
5384
|
-
<div class="flex items-center space-x-4">
|
|
5385
|
-
<!--
|
|
6672
|
+
<div class="flex items-center space-x-4 flex-1">
|
|
6673
|
+
<!-- Model Filter -->
|
|
6674
|
+
<div>
|
|
6675
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Model</label>
|
|
6676
|
+
<div class="grid grid-cols-1">
|
|
6677
|
+
<select
|
|
6678
|
+
name="model"
|
|
6679
|
+
onchange="updateContentFilters('model', this.value)"
|
|
6680
|
+
class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 min-w-40"
|
|
6681
|
+
>
|
|
6682
|
+
<option value="all" ${data.modelName === "all" ? "selected" : ""}>All Models</option>
|
|
6683
|
+
${data.models.map((model) => `
|
|
6684
|
+
<option value="${model.name}" ${data.modelName === model.name ? "selected" : ""}>
|
|
6685
|
+
${model.displayName}
|
|
6686
|
+
</option>
|
|
6687
|
+
`).join("")}
|
|
6688
|
+
</select>
|
|
6689
|
+
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
6690
|
+
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
|
|
6691
|
+
</svg>
|
|
6692
|
+
</div>
|
|
6693
|
+
</div>
|
|
6694
|
+
|
|
6695
|
+
<!-- Status Filter -->
|
|
5386
6696
|
<div>
|
|
6697
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
|
|
6698
|
+
<div class="grid grid-cols-1">
|
|
6699
|
+
<select
|
|
6700
|
+
name="status"
|
|
6701
|
+
onchange="updateContentFilters('status', this.value)"
|
|
6702
|
+
class="col-start-1 row-start-1 w-full appearance-none rounded-lg bg-white/5 dark:bg-white/5 py-2 pl-3 pr-8 text-sm text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 min-w-40"
|
|
6703
|
+
>
|
|
6704
|
+
<option value="all" ${data.status === "all" ? "selected" : ""}>All Status</option>
|
|
6705
|
+
<option value="draft" ${data.status === "draft" ? "selected" : ""}>Draft</option>
|
|
6706
|
+
<option value="review" ${data.status === "review" ? "selected" : ""}>Under Review</option>
|
|
6707
|
+
<option value="scheduled" ${data.status === "scheduled" ? "selected" : ""}>Scheduled</option>
|
|
6708
|
+
<option value="published" ${data.status === "published" ? "selected" : ""}>Published</option>
|
|
6709
|
+
<option value="archived" ${data.status === "archived" ? "selected" : ""}>Archived</option>
|
|
6710
|
+
<option value="deleted" ${data.status === "deleted" ? "selected" : ""}>Deleted</option>
|
|
6711
|
+
</select>
|
|
6712
|
+
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
6713
|
+
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
|
|
6714
|
+
</svg>
|
|
6715
|
+
</div>
|
|
6716
|
+
</div>
|
|
6717
|
+
|
|
6718
|
+
<!-- Search Input -->
|
|
6719
|
+
<div class="flex-1 max-w-md">
|
|
5387
6720
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
|
|
5388
6721
|
<form onsubmit="performContentSearch(event)" class="flex items-center space-x-2">
|
|
5389
|
-
<div class="relative group">
|
|
6722
|
+
<div class="relative group flex-1">
|
|
5390
6723
|
<input
|
|
5391
6724
|
type="text"
|
|
5392
6725
|
name="search"
|
|
@@ -5394,7 +6727,7 @@ function renderContentListPage(data) {
|
|
|
5394
6727
|
value="${data.search || ""}"
|
|
5395
6728
|
oninput="toggleContentClearButton()"
|
|
5396
6729
|
placeholder="Search content..."
|
|
5397
|
-
class="rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm
|
|
6730
|
+
class="w-full rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
|
|
5398
6731
|
>
|
|
5399
6732
|
<div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
|
|
5400
6733
|
<svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
@@ -5466,57 +6799,6 @@ function renderContentListPage(data) {
|
|
|
5466
6799
|
}
|
|
5467
6800
|
</script>
|
|
5468
6801
|
</div>
|
|
5469
|
-
|
|
5470
|
-
${filterBarData.filters.map((filter) => {
|
|
5471
|
-
const selectedOption = filter.options.find((opt) => opt.selected);
|
|
5472
|
-
const selectedColor = selectedOption?.color || "cyan";
|
|
5473
|
-
const colorMap = {
|
|
5474
|
-
"cyan": "bg-cyan-400 dark:bg-cyan-400",
|
|
5475
|
-
"lime": "bg-lime-400 dark:bg-lime-400",
|
|
5476
|
-
"pink": "bg-pink-400 dark:bg-pink-400",
|
|
5477
|
-
"purple": "bg-purple-400 dark:bg-purple-400",
|
|
5478
|
-
"amber": "bg-amber-400 dark:bg-amber-400",
|
|
5479
|
-
"zinc": "bg-zinc-400 dark:bg-zinc-400"
|
|
5480
|
-
};
|
|
5481
|
-
return `
|
|
5482
|
-
<div>
|
|
5483
|
-
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">${filter.label}</label>
|
|
5484
|
-
<div class="mt-2 grid grid-cols-1">
|
|
5485
|
-
<div class="col-start-1 row-start-1 flex items-center gap-3 pl-3 pr-8 pointer-events-none">
|
|
5486
|
-
${filter.name === "status" ? `<span class="inline-block size-2 shrink-0 rounded-full border border-transparent ${colorMap[selectedColor]}"></span>` : ""}
|
|
5487
|
-
</div>
|
|
5488
|
-
<select
|
|
5489
|
-
name="${filter.name}"
|
|
5490
|
-
onchange="updateContentFilters('${filter.name}', this.value)"
|
|
5491
|
-
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 ${filter.name === "status" ? "pl-8" : "pl-3"} pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-cyan-500/30 dark:outline-cyan-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-cyan-500 dark:focus-visible:outline-cyan-400 sm:text-sm/6 min-w-48"
|
|
5492
|
-
>
|
|
5493
|
-
${filter.options.map((opt) => `
|
|
5494
|
-
<option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
|
|
5495
|
-
`).join("")}
|
|
5496
|
-
</select>
|
|
5497
|
-
<svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cyan-600 dark:text-cyan-400 sm:size-4">
|
|
5498
|
-
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
|
|
5499
|
-
</svg>
|
|
5500
|
-
</div>
|
|
5501
|
-
</div>
|
|
5502
|
-
`;
|
|
5503
|
-
}).join("")}
|
|
5504
|
-
|
|
5505
|
-
<!-- Clear Filters Button -->
|
|
5506
|
-
${hasActiveFilters ? `
|
|
5507
|
-
<div>
|
|
5508
|
-
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2"> </label>
|
|
5509
|
-
<button
|
|
5510
|
-
onclick="clearAllFilters()"
|
|
5511
|
-
class="inline-flex items-center gap-x-1.5 px-3 py-2 bg-pink-50 dark:bg-pink-500/10 text-pink-700 dark:text-pink-300 text-sm font-medium rounded-md ring-1 ring-inset ring-pink-600/20 dark:ring-pink-500/20 hover:bg-pink-100 dark:hover:bg-pink-500/20 transition-colors"
|
|
5512
|
-
>
|
|
5513
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
5514
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
5515
|
-
</svg>
|
|
5516
|
-
Clear Filters
|
|
5517
|
-
</button>
|
|
5518
|
-
</div>
|
|
5519
|
-
` : ""}
|
|
5520
6802
|
</div>
|
|
5521
6803
|
<div class="flex items-center gap-x-3">
|
|
5522
6804
|
<span class="text-sm/6 font-medium text-zinc-700 dark:text-zinc-300 px-3 py-1.5 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-sm">${data.totalItems} ${data.totalItems === 1 ? "item" : "items"}</span>
|
|
@@ -5760,66 +7042,352 @@ function renderContentListPage(data) {
|
|
|
5760
7042
|
const menu = document.getElementById('bulk-actions-menu');
|
|
5761
7043
|
menu.classList.add('hidden');
|
|
5762
7044
|
|
|
5763
|
-
fetch('/admin/content/bulk-action', {
|
|
5764
|
-
method: 'POST',
|
|
5765
|
-
headers: {
|
|
5766
|
-
'Content-Type': 'application/json'
|
|
5767
|
-
},
|
|
5768
|
-
body: JSON.stringify({
|
|
5769
|
-
action: currentBulkAction,
|
|
5770
|
-
ids: currentSelectedIds
|
|
5771
|
-
})
|
|
5772
|
-
})
|
|
5773
|
-
.then(res => res.json())
|
|
5774
|
-
.then(data => {
|
|
5775
|
-
if (data.success) {
|
|
5776
|
-
location.reload();
|
|
5777
|
-
} else {
|
|
5778
|
-
alert('Error: ' + (data.error || 'Unknown error'));
|
|
7045
|
+
fetch('/admin/content/bulk-action', {
|
|
7046
|
+
method: 'POST',
|
|
7047
|
+
headers: {
|
|
7048
|
+
'Content-Type': 'application/json'
|
|
7049
|
+
},
|
|
7050
|
+
body: JSON.stringify({
|
|
7051
|
+
action: currentBulkAction,
|
|
7052
|
+
ids: currentSelectedIds
|
|
7053
|
+
})
|
|
7054
|
+
})
|
|
7055
|
+
.then(res => res.json())
|
|
7056
|
+
.then(data => {
|
|
7057
|
+
if (data.success) {
|
|
7058
|
+
location.reload();
|
|
7059
|
+
} else {
|
|
7060
|
+
alert('Error: ' + (data.error || 'Unknown error'));
|
|
7061
|
+
}
|
|
7062
|
+
})
|
|
7063
|
+
.catch(err => {
|
|
7064
|
+
console.error('Bulk action error:', err);
|
|
7065
|
+
alert('Failed to perform bulk action');
|
|
7066
|
+
})
|
|
7067
|
+
.finally(() => {
|
|
7068
|
+
// Clear context
|
|
7069
|
+
currentBulkAction = null;
|
|
7070
|
+
currentSelectedIds = [];
|
|
7071
|
+
});
|
|
7072
|
+
}
|
|
7073
|
+
|
|
7074
|
+
// Helper to get action text for display
|
|
7075
|
+
function getActionText(action) {
|
|
7076
|
+
const actionCount = currentSelectedIds.length;
|
|
7077
|
+
switch(action) {
|
|
7078
|
+
case 'publish':
|
|
7079
|
+
return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7080
|
+
case 'draft':
|
|
7081
|
+
return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
|
|
7082
|
+
case 'delete':
|
|
7083
|
+
return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7084
|
+
default:
|
|
7085
|
+
return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
|
|
7089
|
+
</script>
|
|
7090
|
+
|
|
7091
|
+
<!-- Confirmation Dialog for Bulk Actions -->
|
|
7092
|
+
${renderConfirmationDialog({
|
|
7093
|
+
id: "bulk-action-confirm",
|
|
7094
|
+
title: "Confirm Bulk Action",
|
|
7095
|
+
message: "Are you sure you want to perform this action? This operation will affect multiple items.",
|
|
7096
|
+
confirmText: "Confirm",
|
|
7097
|
+
cancelText: "Cancel",
|
|
7098
|
+
confirmClass: "bg-blue-500 hover:bg-blue-400",
|
|
7099
|
+
iconColor: "blue",
|
|
7100
|
+
onConfirm: "executeBulkAction()"
|
|
7101
|
+
})}
|
|
7102
|
+
|
|
7103
|
+
<!-- Confirmation Dialog Script -->
|
|
7104
|
+
${getConfirmationDialogScript()}
|
|
7105
|
+
|
|
7106
|
+
<!-- Advanced Search Modal -->
|
|
7107
|
+
<div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
7108
|
+
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
7109
|
+
<!-- Background overlay -->
|
|
7110
|
+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
|
|
7111
|
+
|
|
7112
|
+
<!-- Modal panel -->
|
|
7113
|
+
<div class="inline-block align-bottom bg-white dark:bg-zinc-900 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
|
|
7114
|
+
<div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
|
|
7115
|
+
<!-- Header -->
|
|
7116
|
+
<div class="flex items-center justify-between mb-4">
|
|
7117
|
+
<h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
|
|
7118
|
+
\u{1F50D} Advanced Search
|
|
7119
|
+
</h3>
|
|
7120
|
+
<button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
|
|
7121
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
7122
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
7123
|
+
</svg>
|
|
7124
|
+
</button>
|
|
7125
|
+
</div>
|
|
7126
|
+
|
|
7127
|
+
<!-- Search Form -->
|
|
7128
|
+
<form id="advancedSearchForm" class="space-y-4">
|
|
7129
|
+
<!-- Search Input -->
|
|
7130
|
+
<div>
|
|
7131
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
|
|
7132
|
+
<div class="relative">
|
|
7133
|
+
<input
|
|
7134
|
+
type="text"
|
|
7135
|
+
id="searchQuery"
|
|
7136
|
+
name="query"
|
|
7137
|
+
placeholder="Enter your search query..."
|
|
7138
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-4 py-3 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-indigo-500"
|
|
7139
|
+
autocomplete="off"
|
|
7140
|
+
/>
|
|
7141
|
+
<div id="searchSuggestions" class="hidden absolute z-10 w-full mt-1 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 max-h-60 overflow-y-auto"></div>
|
|
7142
|
+
</div>
|
|
7143
|
+
</div>
|
|
7144
|
+
|
|
7145
|
+
<!-- Mode Toggle -->
|
|
7146
|
+
<div>
|
|
7147
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
|
|
7148
|
+
<div class="flex gap-4">
|
|
7149
|
+
<label class="flex items-center">
|
|
7150
|
+
<input type="radio" name="mode" value="ai" checked class="mr-2">
|
|
7151
|
+
<span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
|
|
7152
|
+
</label>
|
|
7153
|
+
<label class="flex items-center">
|
|
7154
|
+
<input type="radio" name="mode" value="keyword" class="mr-2">
|
|
7155
|
+
<span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
|
|
7156
|
+
</label>
|
|
7157
|
+
</div>
|
|
7158
|
+
</div>
|
|
7159
|
+
|
|
7160
|
+
<!-- Filters -->
|
|
7161
|
+
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
|
7162
|
+
<h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
|
|
7163
|
+
|
|
7164
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
7165
|
+
<!-- Collection Filter -->
|
|
7166
|
+
<div>
|
|
7167
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
|
|
7168
|
+
<select
|
|
7169
|
+
id="filterCollections"
|
|
7170
|
+
name="collections"
|
|
7171
|
+
multiple
|
|
7172
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
|
|
7173
|
+
size="4"
|
|
7174
|
+
>
|
|
7175
|
+
<option value="">All Collections</option>
|
|
7176
|
+
${data.models.map(
|
|
7177
|
+
(model) => `
|
|
7178
|
+
<option value="${model.name}">${model.displayName}</option>
|
|
7179
|
+
`
|
|
7180
|
+
).join("")}
|
|
7181
|
+
</select>
|
|
7182
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
|
|
7183
|
+
</div>
|
|
7184
|
+
|
|
7185
|
+
<!-- Status Filter -->
|
|
7186
|
+
<div>
|
|
7187
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
|
|
7188
|
+
<select
|
|
7189
|
+
id="filterStatus"
|
|
7190
|
+
name="status"
|
|
7191
|
+
multiple
|
|
7192
|
+
class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10"
|
|
7193
|
+
size="4"
|
|
7194
|
+
>
|
|
7195
|
+
<option value="published">Published</option>
|
|
7196
|
+
<option value="draft">Draft</option>
|
|
7197
|
+
<option value="review">Under Review</option>
|
|
7198
|
+
<option value="scheduled">Scheduled</option>
|
|
7199
|
+
<option value="archived">Archived</option>
|
|
7200
|
+
</select>
|
|
7201
|
+
</div>
|
|
7202
|
+
</div>
|
|
7203
|
+
</div>
|
|
7204
|
+
|
|
7205
|
+
<!-- Actions -->
|
|
7206
|
+
<div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
|
7207
|
+
<button
|
|
7208
|
+
type="button"
|
|
7209
|
+
onclick="closeAdvancedSearch()"
|
|
7210
|
+
class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-4 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700"
|
|
7211
|
+
>
|
|
7212
|
+
Cancel
|
|
7213
|
+
</button>
|
|
7214
|
+
<button
|
|
7215
|
+
type="submit"
|
|
7216
|
+
class="inline-flex items-center justify-center rounded-lg bg-indigo-600 text-white px-6 py-2.5 text-sm font-semibold hover:bg-indigo-500 shadow-sm"
|
|
7217
|
+
>
|
|
7218
|
+
Search
|
|
7219
|
+
</button>
|
|
7220
|
+
</div>
|
|
7221
|
+
</form>
|
|
7222
|
+
</div>
|
|
7223
|
+
|
|
7224
|
+
<!-- Results Area -->
|
|
7225
|
+
<div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
|
|
7226
|
+
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
|
7227
|
+
<div id="searchResultsContent" class="space-y-3"></div>
|
|
7228
|
+
<div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
|
|
7229
|
+
</div>
|
|
7230
|
+
</div>
|
|
7231
|
+
</div>
|
|
7232
|
+
</div>
|
|
7233
|
+
</div>
|
|
7234
|
+
|
|
7235
|
+
<script>
|
|
7236
|
+
// Open modal
|
|
7237
|
+
function openAdvancedSearch() {
|
|
7238
|
+
document.getElementById('advancedSearchModal').classList.remove('hidden');
|
|
7239
|
+
document.getElementById('searchQuery').focus();
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
// Close modal
|
|
7243
|
+
function closeAdvancedSearch() {
|
|
7244
|
+
document.getElementById('advancedSearchModal').classList.add('hidden');
|
|
7245
|
+
document.getElementById('searchResults').classList.add('hidden');
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
// Autocomplete
|
|
7249
|
+
let autocompleteTimeout;
|
|
7250
|
+
const searchQueryInput = document.getElementById('searchQuery');
|
|
7251
|
+
if (searchQueryInput) {
|
|
7252
|
+
searchQueryInput.addEventListener('input', (e) => {
|
|
7253
|
+
const query = e.target.value.trim();
|
|
7254
|
+
const suggestionsDiv = document.getElementById('searchSuggestions');
|
|
7255
|
+
|
|
7256
|
+
clearTimeout(autocompleteTimeout);
|
|
7257
|
+
|
|
7258
|
+
if (query.length < 2) {
|
|
7259
|
+
suggestionsDiv.classList.add('hidden');
|
|
7260
|
+
return;
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7263
|
+
autocompleteTimeout = setTimeout(async () => {
|
|
7264
|
+
try {
|
|
7265
|
+
const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
|
|
7266
|
+
const { data } = await res.json();
|
|
7267
|
+
|
|
7268
|
+
if (data && data.length > 0) {
|
|
7269
|
+
suggestionsDiv.innerHTML = data.map(s => \`
|
|
7270
|
+
<div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
|
|
7271
|
+
\`).join('');
|
|
7272
|
+
suggestionsDiv.classList.remove('hidden');
|
|
7273
|
+
} else {
|
|
7274
|
+
suggestionsDiv.classList.add('hidden');
|
|
7275
|
+
}
|
|
7276
|
+
} catch (error) {
|
|
7277
|
+
console.error('Autocomplete error:', error);
|
|
7278
|
+
}
|
|
7279
|
+
}, 300);
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
|
|
7283
|
+
function selectSuggestion(suggestion) {
|
|
7284
|
+
document.getElementById('searchQuery').value = suggestion;
|
|
7285
|
+
document.getElementById('searchSuggestions').classList.add('hidden');
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
// Hide suggestions when clicking outside
|
|
7289
|
+
document.addEventListener('click', (e) => {
|
|
7290
|
+
const suggestionsDiv = document.getElementById('searchSuggestions');
|
|
7291
|
+
if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
|
|
7292
|
+
suggestionsDiv.classList.add('hidden');
|
|
7293
|
+
}
|
|
7294
|
+
});
|
|
7295
|
+
|
|
7296
|
+
// Form submission
|
|
7297
|
+
const advancedSearchForm = document.getElementById('advancedSearchForm');
|
|
7298
|
+
if (advancedSearchForm) {
|
|
7299
|
+
advancedSearchForm.addEventListener('submit', async (e) => {
|
|
7300
|
+
e.preventDefault();
|
|
7301
|
+
|
|
7302
|
+
const formData = new FormData(e.target);
|
|
7303
|
+
const query = formData.get('query');
|
|
7304
|
+
const mode = formData.get('mode') || 'ai';
|
|
7305
|
+
|
|
7306
|
+
// Build filters
|
|
7307
|
+
const filters = {};
|
|
7308
|
+
|
|
7309
|
+
const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
|
|
7310
|
+
if (collections.length > 0) {
|
|
7311
|
+
// Need to convert collection names to IDs - for now, pass names
|
|
7312
|
+
filters.collections = collections;
|
|
7313
|
+
}
|
|
7314
|
+
|
|
7315
|
+
const status = Array.from(formData.getAll('status'));
|
|
7316
|
+
if (status.length > 0) {
|
|
7317
|
+
filters.status = status;
|
|
7318
|
+
}
|
|
7319
|
+
|
|
7320
|
+
const dateStart = formData.get('date_start');
|
|
7321
|
+
const dateEnd = formData.get('date_end');
|
|
7322
|
+
if (dateStart || dateEnd) {
|
|
7323
|
+
filters.dateRange = {
|
|
7324
|
+
start: dateStart ? new Date(dateStart) : null,
|
|
7325
|
+
end: dateEnd ? new Date(dateEnd) : null,
|
|
7326
|
+
field: 'created_at'
|
|
7327
|
+
};
|
|
7328
|
+
}
|
|
7329
|
+
|
|
7330
|
+
// Execute search
|
|
7331
|
+
try {
|
|
7332
|
+
const res = await fetch('/api/search', {
|
|
7333
|
+
method: 'POST',
|
|
7334
|
+
headers: {'Content-Type': 'application/json'},
|
|
7335
|
+
body: JSON.stringify({
|
|
7336
|
+
query,
|
|
7337
|
+
mode,
|
|
7338
|
+
filters,
|
|
7339
|
+
limit: 20
|
|
7340
|
+
})
|
|
7341
|
+
});
|
|
7342
|
+
|
|
7343
|
+
const { data } = await res.json();
|
|
7344
|
+
|
|
7345
|
+
if (data && data.results) {
|
|
7346
|
+
displaySearchResults(data);
|
|
7347
|
+
}
|
|
7348
|
+
} catch (error) {
|
|
7349
|
+
console.error('Search error:', error);
|
|
7350
|
+
alert('Search failed. Please try again.');
|
|
5779
7351
|
}
|
|
5780
|
-
})
|
|
5781
|
-
.catch(err => {
|
|
5782
|
-
console.error('Bulk action error:', err);
|
|
5783
|
-
alert('Failed to perform bulk action');
|
|
5784
|
-
})
|
|
5785
|
-
.finally(() => {
|
|
5786
|
-
// Clear context
|
|
5787
|
-
currentBulkAction = null;
|
|
5788
|
-
currentSelectedIds = [];
|
|
5789
7352
|
});
|
|
5790
7353
|
}
|
|
5791
7354
|
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
const
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
7355
|
+
function displaySearchResults(searchData) {
|
|
7356
|
+
const resultsDiv = document.getElementById('searchResultsContent');
|
|
7357
|
+
const resultsSection = document.getElementById('searchResults');
|
|
7358
|
+
|
|
7359
|
+
if (searchData.results.length === 0) {
|
|
7360
|
+
resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
|
|
7361
|
+
} else {
|
|
7362
|
+
resultsDiv.innerHTML = searchData.results.map(result => \`
|
|
7363
|
+
<div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
|
7364
|
+
<div class="flex items-start justify-between">
|
|
7365
|
+
<div class="flex-1">
|
|
7366
|
+
<h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
|
|
7367
|
+
<a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
|
|
7368
|
+
</h4>
|
|
7369
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
|
|
7370
|
+
\${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
|
|
7371
|
+
\${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
|
|
7372
|
+
</p>
|
|
7373
|
+
\${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
|
|
7374
|
+
</div>
|
|
7375
|
+
<div class="ml-4">
|
|
7376
|
+
<span class="px-2 py-1 text-xs rounded-full \${result.status === 'published' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'}">\${result.status}</span>
|
|
7377
|
+
</div>
|
|
7378
|
+
</div>
|
|
7379
|
+
</div>
|
|
7380
|
+
\`).join('');
|
|
5804
7381
|
}
|
|
7382
|
+
|
|
7383
|
+
resultsSection.classList.remove('hidden');
|
|
7384
|
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
5805
7385
|
}
|
|
5806
7386
|
|
|
7387
|
+
// Make functions globally available
|
|
7388
|
+
window.openAdvancedSearch = openAdvancedSearch;
|
|
7389
|
+
window.closeAdvancedSearch = closeAdvancedSearch;
|
|
5807
7390
|
</script>
|
|
5808
|
-
|
|
5809
|
-
<!-- Confirmation Dialog for Bulk Actions -->
|
|
5810
|
-
${renderConfirmationDialog({
|
|
5811
|
-
id: "bulk-action-confirm",
|
|
5812
|
-
title: "Confirm Bulk Action",
|
|
5813
|
-
message: "Are you sure you want to perform this action? This operation will affect multiple items.",
|
|
5814
|
-
confirmText: "Confirm",
|
|
5815
|
-
cancelText: "Cancel",
|
|
5816
|
-
confirmClass: "bg-blue-500 hover:bg-blue-400",
|
|
5817
|
-
iconColor: "blue",
|
|
5818
|
-
onConfirm: "executeBulkAction()"
|
|
5819
|
-
})}
|
|
5820
|
-
|
|
5821
|
-
<!-- Confirmation Dialog Script -->
|
|
5822
|
-
${getConfirmationDialogScript()}
|
|
5823
7391
|
`;
|
|
5824
7392
|
const layoutData = {
|
|
5825
7393
|
title: "Content Management",
|
|
@@ -6023,6 +7591,122 @@ async function isPluginActive2(db, pluginId) {
|
|
|
6023
7591
|
|
|
6024
7592
|
// src/routes/admin-content.ts
|
|
6025
7593
|
var adminContentRoutes = new Hono();
|
|
7594
|
+
function parseFieldValue(field, formData, options = {}) {
|
|
7595
|
+
const { skipValidation = false } = options;
|
|
7596
|
+
const value = formData.get(field.field_name);
|
|
7597
|
+
const errors = [];
|
|
7598
|
+
const blocksConfig = getBlocksFieldConfig(field.field_options);
|
|
7599
|
+
if (blocksConfig) {
|
|
7600
|
+
const parsed = parseBlocksValue(value, blocksConfig);
|
|
7601
|
+
if (!skipValidation && field.is_required && parsed.value.length === 0) {
|
|
7602
|
+
parsed.errors.push(`${field.field_label} is required`);
|
|
7603
|
+
}
|
|
7604
|
+
return { value: parsed.value, errors: parsed.errors };
|
|
7605
|
+
}
|
|
7606
|
+
if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
|
|
7607
|
+
return { value: null, errors: [`${field.field_label} is required`] };
|
|
7608
|
+
}
|
|
7609
|
+
switch (field.field_type) {
|
|
7610
|
+
case "number":
|
|
7611
|
+
if (value && isNaN(Number(value))) {
|
|
7612
|
+
if (!skipValidation) {
|
|
7613
|
+
errors.push(`${field.field_label} must be a valid number`);
|
|
7614
|
+
}
|
|
7615
|
+
return { value: null, errors };
|
|
7616
|
+
}
|
|
7617
|
+
return { value: value ? Number(value) : null, errors: [] };
|
|
7618
|
+
case "boolean":
|
|
7619
|
+
const submitted = formData.get(`${field.field_name}_submitted`);
|
|
7620
|
+
return { value: submitted ? value === "true" : false, errors: [] };
|
|
7621
|
+
case "select":
|
|
7622
|
+
if (field.field_options?.multiple) {
|
|
7623
|
+
return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
|
|
7624
|
+
}
|
|
7625
|
+
return { value, errors: [] };
|
|
7626
|
+
case "array": {
|
|
7627
|
+
if (!value || value.toString().trim() === "") {
|
|
7628
|
+
if (!skipValidation && field.is_required) {
|
|
7629
|
+
errors.push(`${field.field_label} is required`);
|
|
7630
|
+
}
|
|
7631
|
+
return { value: [], errors };
|
|
7632
|
+
}
|
|
7633
|
+
try {
|
|
7634
|
+
const parsed = JSON.parse(value.toString());
|
|
7635
|
+
if (!Array.isArray(parsed)) {
|
|
7636
|
+
if (!skipValidation) {
|
|
7637
|
+
errors.push(`${field.field_label} must be a JSON array`);
|
|
7638
|
+
}
|
|
7639
|
+
return { value: [], errors };
|
|
7640
|
+
}
|
|
7641
|
+
if (!skipValidation && field.is_required && parsed.length === 0) {
|
|
7642
|
+
errors.push(`${field.field_label} is required`);
|
|
7643
|
+
}
|
|
7644
|
+
return { value: parsed, errors };
|
|
7645
|
+
} catch {
|
|
7646
|
+
if (!skipValidation) {
|
|
7647
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7648
|
+
}
|
|
7649
|
+
return { value: [], errors };
|
|
7650
|
+
}
|
|
7651
|
+
}
|
|
7652
|
+
case "object": {
|
|
7653
|
+
if (!value || value.toString().trim() === "") {
|
|
7654
|
+
if (!skipValidation && field.is_required) {
|
|
7655
|
+
errors.push(`${field.field_label} is required`);
|
|
7656
|
+
}
|
|
7657
|
+
return { value: {}, errors };
|
|
7658
|
+
}
|
|
7659
|
+
try {
|
|
7660
|
+
const parsed = JSON.parse(value.toString());
|
|
7661
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7662
|
+
if (!skipValidation) {
|
|
7663
|
+
errors.push(`${field.field_label} must be a JSON object`);
|
|
7664
|
+
}
|
|
7665
|
+
return { value: {}, errors };
|
|
7666
|
+
}
|
|
7667
|
+
if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
|
|
7668
|
+
errors.push(`${field.field_label} is required`);
|
|
7669
|
+
}
|
|
7670
|
+
return { value: parsed, errors };
|
|
7671
|
+
} catch {
|
|
7672
|
+
if (!skipValidation) {
|
|
7673
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7674
|
+
}
|
|
7675
|
+
return { value: {}, errors };
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
case "json": {
|
|
7679
|
+
if (!value || value.toString().trim() === "") {
|
|
7680
|
+
if (!skipValidation && field.is_required) {
|
|
7681
|
+
errors.push(`${field.field_label} is required`);
|
|
7682
|
+
}
|
|
7683
|
+
return { value: null, errors };
|
|
7684
|
+
}
|
|
7685
|
+
try {
|
|
7686
|
+
return { value: JSON.parse(value.toString()), errors: [] };
|
|
7687
|
+
} catch {
|
|
7688
|
+
if (!skipValidation) {
|
|
7689
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7690
|
+
}
|
|
7691
|
+
return { value: null, errors };
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
default:
|
|
7695
|
+
return { value, errors: [] };
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
function extractFieldData(fields, formData, options = {}) {
|
|
7699
|
+
const data = {};
|
|
7700
|
+
const errors = {};
|
|
7701
|
+
for (const field of fields) {
|
|
7702
|
+
const result = parseFieldValue(field, formData, options);
|
|
7703
|
+
data[field.field_name] = result.value;
|
|
7704
|
+
if (result.errors.length > 0) {
|
|
7705
|
+
errors[field.field_name] = result.errors;
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
return { data, errors };
|
|
7709
|
+
}
|
|
6026
7710
|
adminContentRoutes.use("*", requireAuth());
|
|
6027
7711
|
async function getCollectionFields(db, collectionId) {
|
|
6028
7712
|
const cache = getCacheService(CACHE_CONFIGS.collection);
|
|
@@ -6496,36 +8180,7 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
6496
8180
|
`);
|
|
6497
8181
|
}
|
|
6498
8182
|
const fields = await getCollectionFields(db, collectionId);
|
|
6499
|
-
const data =
|
|
6500
|
-
const errors = {};
|
|
6501
|
-
for (const field of fields) {
|
|
6502
|
-
const value = formData.get(field.field_name);
|
|
6503
|
-
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
6504
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6505
|
-
continue;
|
|
6506
|
-
}
|
|
6507
|
-
switch (field.field_type) {
|
|
6508
|
-
case "number":
|
|
6509
|
-
if (value && isNaN(Number(value))) {
|
|
6510
|
-
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
6511
|
-
} else {
|
|
6512
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6513
|
-
}
|
|
6514
|
-
break;
|
|
6515
|
-
case "boolean":
|
|
6516
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
6517
|
-
break;
|
|
6518
|
-
case "select":
|
|
6519
|
-
if (field.field_options?.multiple) {
|
|
6520
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6521
|
-
} else {
|
|
6522
|
-
data[field.field_name] = value;
|
|
6523
|
-
}
|
|
6524
|
-
break;
|
|
6525
|
-
default:
|
|
6526
|
-
data[field.field_name] = value;
|
|
6527
|
-
}
|
|
6528
|
-
}
|
|
8183
|
+
const { data, errors } = extractFieldData(fields, formData);
|
|
6529
8184
|
if (Object.keys(errors).length > 0) {
|
|
6530
8185
|
const formDataWithErrors = {
|
|
6531
8186
|
collection,
|
|
@@ -6642,36 +8297,7 @@ adminContentRoutes.put("/:id", async (c) => {
|
|
|
6642
8297
|
`);
|
|
6643
8298
|
}
|
|
6644
8299
|
const fields = await getCollectionFields(db, existingContent.collection_id);
|
|
6645
|
-
const data =
|
|
6646
|
-
const errors = {};
|
|
6647
|
-
for (const field of fields) {
|
|
6648
|
-
const value = formData.get(field.field_name);
|
|
6649
|
-
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
6650
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6651
|
-
continue;
|
|
6652
|
-
}
|
|
6653
|
-
switch (field.field_type) {
|
|
6654
|
-
case "number":
|
|
6655
|
-
if (value && isNaN(Number(value))) {
|
|
6656
|
-
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
6657
|
-
} else {
|
|
6658
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6659
|
-
}
|
|
6660
|
-
break;
|
|
6661
|
-
case "boolean":
|
|
6662
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
6663
|
-
break;
|
|
6664
|
-
case "select":
|
|
6665
|
-
if (field.field_options?.multiple) {
|
|
6666
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6667
|
-
} else {
|
|
6668
|
-
data[field.field_name] = value;
|
|
6669
|
-
}
|
|
6670
|
-
break;
|
|
6671
|
-
default:
|
|
6672
|
-
data[field.field_name] = value;
|
|
6673
|
-
}
|
|
6674
|
-
}
|
|
8300
|
+
const { data, errors } = extractFieldData(fields, formData);
|
|
6675
8301
|
if (Object.keys(errors).length > 0) {
|
|
6676
8302
|
const formDataWithErrors = {
|
|
6677
8303
|
id,
|
|
@@ -6784,27 +8410,7 @@ adminContentRoutes.post("/preview", async (c) => {
|
|
|
6784
8410
|
return c.html("<p>Collection not found</p>");
|
|
6785
8411
|
}
|
|
6786
8412
|
const fields = await getCollectionFields(db, collectionId);
|
|
6787
|
-
const data = {};
|
|
6788
|
-
for (const field of fields) {
|
|
6789
|
-
const value = formData.get(field.field_name);
|
|
6790
|
-
switch (field.field_type) {
|
|
6791
|
-
case "number":
|
|
6792
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6793
|
-
break;
|
|
6794
|
-
case "boolean":
|
|
6795
|
-
data[field.field_name] = value === "true";
|
|
6796
|
-
break;
|
|
6797
|
-
case "select":
|
|
6798
|
-
if (field.field_options?.multiple) {
|
|
6799
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6800
|
-
} else {
|
|
6801
|
-
data[field.field_name] = value;
|
|
6802
|
-
}
|
|
6803
|
-
break;
|
|
6804
|
-
default:
|
|
6805
|
-
data[field.field_name] = value;
|
|
6806
|
-
}
|
|
6807
|
-
}
|
|
8413
|
+
const { data } = extractFieldData(fields, formData, { skipValidation: true });
|
|
6808
8414
|
const previewHTML = `
|
|
6809
8415
|
<!DOCTYPE html>
|
|
6810
8416
|
<html lang="en">
|
|
@@ -8151,14 +9757,87 @@ function renderUserEditPage(data) {
|
|
|
8151
9757
|
</div>
|
|
8152
9758
|
</div>
|
|
8153
9759
|
</div>
|
|
9760
|
+
</div>
|
|
9761
|
+
|
|
9762
|
+
<!-- Profile Information -->
|
|
9763
|
+
<div class="mb-8">
|
|
9764
|
+
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
|
|
9765
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
|
|
9766
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
9767
|
+
<div>
|
|
9768
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
|
|
9769
|
+
<input
|
|
9770
|
+
type="text"
|
|
9771
|
+
name="profile_display_name"
|
|
9772
|
+
value="${escapeHtml(data.userToEdit.profile?.displayName || "")}"
|
|
9773
|
+
placeholder="Public display name"
|
|
9774
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9775
|
+
/>
|
|
9776
|
+
</div>
|
|
9777
|
+
|
|
9778
|
+
<div>
|
|
9779
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
|
|
9780
|
+
<input
|
|
9781
|
+
type="text"
|
|
9782
|
+
name="profile_company"
|
|
9783
|
+
value="${escapeHtml(data.userToEdit.profile?.company || "")}"
|
|
9784
|
+
placeholder="Company or organization"
|
|
9785
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9786
|
+
/>
|
|
9787
|
+
</div>
|
|
9788
|
+
|
|
9789
|
+
<div>
|
|
9790
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
|
|
9791
|
+
<input
|
|
9792
|
+
type="text"
|
|
9793
|
+
name="profile_job_title"
|
|
9794
|
+
value="${escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
|
|
9795
|
+
placeholder="Job title or role"
|
|
9796
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9797
|
+
/>
|
|
9798
|
+
</div>
|
|
9799
|
+
|
|
9800
|
+
<div>
|
|
9801
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
|
|
9802
|
+
<input
|
|
9803
|
+
type="url"
|
|
9804
|
+
name="profile_website"
|
|
9805
|
+
value="${escapeHtml(data.userToEdit.profile?.website || "")}"
|
|
9806
|
+
placeholder="https://example.com"
|
|
9807
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9808
|
+
/>
|
|
9809
|
+
</div>
|
|
9810
|
+
|
|
9811
|
+
<div>
|
|
9812
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
|
|
9813
|
+
<input
|
|
9814
|
+
type="text"
|
|
9815
|
+
name="profile_location"
|
|
9816
|
+
value="${escapeHtml(data.userToEdit.profile?.location || "")}"
|
|
9817
|
+
placeholder="City, Country"
|
|
9818
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9819
|
+
/>
|
|
9820
|
+
</div>
|
|
9821
|
+
|
|
9822
|
+
<div>
|
|
9823
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
|
|
9824
|
+
<input
|
|
9825
|
+
type="date"
|
|
9826
|
+
name="profile_date_of_birth"
|
|
9827
|
+
value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
|
|
9828
|
+
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
9829
|
+
/>
|
|
9830
|
+
</div>
|
|
9831
|
+
</div>
|
|
8154
9832
|
|
|
8155
9833
|
<div class="mt-6">
|
|
8156
9834
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
|
|
8157
9835
|
<textarea
|
|
8158
|
-
name="
|
|
9836
|
+
name="profile_bio"
|
|
8159
9837
|
rows="3"
|
|
9838
|
+
placeholder="Short bio or description"
|
|
8160
9839
|
class="w-full rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-950 dark:text-white shadow-sm ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 dark:focus:ring-white transition-shadow"
|
|
8161
|
-
>${escapeHtml(data.userToEdit.bio || "")}</textarea>
|
|
9840
|
+
>${escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
|
|
8162
9841
|
</div>
|
|
8163
9842
|
</div>
|
|
8164
9843
|
|
|
@@ -9692,7 +11371,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9692
11371
|
const userId = c.req.param("id");
|
|
9693
11372
|
try {
|
|
9694
11373
|
const userStmt = db.prepare(`
|
|
9695
|
-
SELECT id, email, username, first_name, last_name, phone,
|
|
11374
|
+
SELECT id, email, username, first_name, last_name, phone, avatar_url,
|
|
9696
11375
|
role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
|
|
9697
11376
|
FROM users
|
|
9698
11377
|
WHERE id = ?
|
|
@@ -9705,6 +11384,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9705
11384
|
dismissible: true
|
|
9706
11385
|
}), 404);
|
|
9707
11386
|
}
|
|
11387
|
+
const profileStmt = db.prepare(`
|
|
11388
|
+
SELECT display_name, bio, company, job_title, website, location, date_of_birth
|
|
11389
|
+
FROM user_profiles
|
|
11390
|
+
WHERE user_id = ?
|
|
11391
|
+
`);
|
|
11392
|
+
const profileData = await profileStmt.bind(userId).first();
|
|
11393
|
+
const profile = profileData ? {
|
|
11394
|
+
displayName: profileData.display_name,
|
|
11395
|
+
bio: profileData.bio,
|
|
11396
|
+
company: profileData.company,
|
|
11397
|
+
jobTitle: profileData.job_title,
|
|
11398
|
+
website: profileData.website,
|
|
11399
|
+
location: profileData.location,
|
|
11400
|
+
dateOfBirth: profileData.date_of_birth
|
|
11401
|
+
} : void 0;
|
|
9708
11402
|
const editData = {
|
|
9709
11403
|
id: userToEdit.id,
|
|
9710
11404
|
email: userToEdit.email,
|
|
@@ -9712,14 +11406,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9712
11406
|
firstName: userToEdit.first_name || "",
|
|
9713
11407
|
lastName: userToEdit.last_name || "",
|
|
9714
11408
|
phone: userToEdit.phone,
|
|
9715
|
-
bio: userToEdit.bio,
|
|
9716
11409
|
avatarUrl: userToEdit.avatar_url,
|
|
9717
11410
|
role: userToEdit.role,
|
|
9718
11411
|
isActive: Boolean(userToEdit.is_active),
|
|
9719
11412
|
emailVerified: Boolean(userToEdit.email_verified),
|
|
9720
11413
|
twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
|
|
9721
11414
|
createdAt: userToEdit.created_at,
|
|
9722
|
-
lastLoginAt: userToEdit.last_login_at
|
|
11415
|
+
lastLoginAt: userToEdit.last_login_at,
|
|
11416
|
+
profile
|
|
9723
11417
|
};
|
|
9724
11418
|
const pageData = {
|
|
9725
11419
|
userToEdit: editData,
|
|
@@ -9735,7 +11429,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9735
11429
|
console.error("User edit page error:", error);
|
|
9736
11430
|
return c.html(renderAlert2({
|
|
9737
11431
|
type: "error",
|
|
9738
|
-
message: "Failed to load user
|
|
11432
|
+
message: "Failed to load user. Please try again.",
|
|
9739
11433
|
dismissible: true
|
|
9740
11434
|
}), 500);
|
|
9741
11435
|
}
|
|
@@ -9751,10 +11445,17 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9751
11445
|
const username = sanitizeInput(formData.get("username")?.toString());
|
|
9752
11446
|
const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
|
|
9753
11447
|
const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
|
|
9754
|
-
const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
|
|
9755
11448
|
const role = formData.get("role")?.toString() || "viewer";
|
|
9756
11449
|
const isActive = formData.get("is_active") === "1";
|
|
9757
11450
|
const emailVerified = formData.get("email_verified") === "1";
|
|
11451
|
+
const profileDisplayName = sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
|
|
11452
|
+
const profileBio = sanitizeInput(formData.get("profile_bio")?.toString()) || null;
|
|
11453
|
+
const profileCompany = sanitizeInput(formData.get("profile_company")?.toString()) || null;
|
|
11454
|
+
const profileJobTitle = sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
|
|
11455
|
+
const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
|
|
11456
|
+
const profileLocation = sanitizeInput(formData.get("profile_location")?.toString()) || null;
|
|
11457
|
+
const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
|
|
11458
|
+
const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
|
|
9758
11459
|
if (!firstName || !lastName || !username || !email) {
|
|
9759
11460
|
return c.html(renderAlert2({
|
|
9760
11461
|
type: "error",
|
|
@@ -9770,6 +11471,17 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9770
11471
|
dismissible: true
|
|
9771
11472
|
}));
|
|
9772
11473
|
}
|
|
11474
|
+
if (profileWebsite) {
|
|
11475
|
+
try {
|
|
11476
|
+
new URL(profileWebsite);
|
|
11477
|
+
} catch {
|
|
11478
|
+
return c.html(renderAlert2({
|
|
11479
|
+
type: "error",
|
|
11480
|
+
message: "Please enter a valid website URL.",
|
|
11481
|
+
dismissible: true
|
|
11482
|
+
}));
|
|
11483
|
+
}
|
|
11484
|
+
}
|
|
9773
11485
|
const checkStmt = db.prepare(`
|
|
9774
11486
|
SELECT id FROM users
|
|
9775
11487
|
WHERE (username = ? OR email = ?) AND id != ?
|
|
@@ -9778,14 +11490,14 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9778
11490
|
if (existingUser) {
|
|
9779
11491
|
return c.html(renderAlert2({
|
|
9780
11492
|
type: "error",
|
|
9781
|
-
message: "Username or email is already taken by another user
|
|
11493
|
+
message: "Username or email is already taken by another user.",
|
|
9782
11494
|
dismissible: true
|
|
9783
11495
|
}));
|
|
9784
11496
|
}
|
|
9785
11497
|
const updateStmt = db.prepare(`
|
|
9786
11498
|
UPDATE users SET
|
|
9787
11499
|
first_name = ?, last_name = ?, username = ?, email = ?,
|
|
9788
|
-
phone = ?,
|
|
11500
|
+
phone = ?, role = ?, is_active = ?, email_verified = ?,
|
|
9789
11501
|
updated_at = ?
|
|
9790
11502
|
WHERE id = ?
|
|
9791
11503
|
`);
|
|
@@ -9795,20 +11507,63 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9795
11507
|
username,
|
|
9796
11508
|
email,
|
|
9797
11509
|
phone,
|
|
9798
|
-
bio,
|
|
9799
11510
|
role,
|
|
9800
11511
|
isActive ? 1 : 0,
|
|
9801
11512
|
emailVerified ? 1 : 0,
|
|
9802
11513
|
Date.now(),
|
|
9803
11514
|
userId
|
|
9804
11515
|
).run();
|
|
11516
|
+
const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
|
|
11517
|
+
if (hasProfileData) {
|
|
11518
|
+
const now = Date.now();
|
|
11519
|
+
const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
|
|
11520
|
+
const existingProfile = await profileCheckStmt.bind(userId).first();
|
|
11521
|
+
if (existingProfile) {
|
|
11522
|
+
const updateProfileStmt = db.prepare(`
|
|
11523
|
+
UPDATE user_profiles SET
|
|
11524
|
+
display_name = ?, bio = ?, company = ?, job_title = ?,
|
|
11525
|
+
website = ?, location = ?, date_of_birth = ?, updated_at = ?
|
|
11526
|
+
WHERE user_id = ?
|
|
11527
|
+
`);
|
|
11528
|
+
await updateProfileStmt.bind(
|
|
11529
|
+
profileDisplayName,
|
|
11530
|
+
profileBio,
|
|
11531
|
+
profileCompany,
|
|
11532
|
+
profileJobTitle,
|
|
11533
|
+
profileWebsite,
|
|
11534
|
+
profileLocation,
|
|
11535
|
+
profileDateOfBirth,
|
|
11536
|
+
now,
|
|
11537
|
+
userId
|
|
11538
|
+
).run();
|
|
11539
|
+
} else {
|
|
11540
|
+
const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
11541
|
+
const insertProfileStmt = db.prepare(`
|
|
11542
|
+
INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
|
|
11543
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11544
|
+
`);
|
|
11545
|
+
await insertProfileStmt.bind(
|
|
11546
|
+
profileId,
|
|
11547
|
+
userId,
|
|
11548
|
+
profileDisplayName,
|
|
11549
|
+
profileBio,
|
|
11550
|
+
profileCompany,
|
|
11551
|
+
profileJobTitle,
|
|
11552
|
+
profileWebsite,
|
|
11553
|
+
profileLocation,
|
|
11554
|
+
profileDateOfBirth,
|
|
11555
|
+
now,
|
|
11556
|
+
now
|
|
11557
|
+
).run();
|
|
11558
|
+
}
|
|
11559
|
+
}
|
|
9805
11560
|
await logActivity(
|
|
9806
11561
|
db,
|
|
9807
11562
|
user.userId,
|
|
9808
|
-
"user
|
|
11563
|
+
"user.update",
|
|
9809
11564
|
"users",
|
|
9810
11565
|
userId,
|
|
9811
|
-
{ fields: ["first_name", "last_name", "username", "email", "phone", "
|
|
11566
|
+
{ fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
|
|
9812
11567
|
c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
|
|
9813
11568
|
c.req.header("user-agent")
|
|
9814
11569
|
);
|
|
@@ -9821,7 +11576,7 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9821
11576
|
console.error("User update error:", error);
|
|
9822
11577
|
return c.html(renderAlert2({
|
|
9823
11578
|
type: "error",
|
|
9824
|
-
message: "Failed to update user
|
|
11579
|
+
message: "Failed to update user. Please try again.",
|
|
9825
11580
|
dismissible: true
|
|
9826
11581
|
}));
|
|
9827
11582
|
}
|
|
@@ -13315,6 +15070,9 @@ function renderAuthSettingsForm(settings) {
|
|
|
13315
15070
|
}
|
|
13316
15071
|
|
|
13317
15072
|
// src/templates/pages/admin-plugin-settings.template.ts
|
|
15073
|
+
function escapeHtmlAttr(value) {
|
|
15074
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
15075
|
+
}
|
|
13318
15076
|
function renderPluginSettingsPage(data) {
|
|
13319
15077
|
const { plugin, activity = [], user } = data;
|
|
13320
15078
|
const pageContent = `
|
|
@@ -13592,6 +15350,7 @@ function renderSettingsTab(plugin) {
|
|
|
13592
15350
|
const settings = plugin.settings || {};
|
|
13593
15351
|
const isSeedDataPlugin = plugin.id === "seed-data" || plugin.name === "seed-data";
|
|
13594
15352
|
const isAuthPlugin = plugin.id === "core-auth" || plugin.name === "core-auth";
|
|
15353
|
+
const isTurnstilePlugin = plugin.id === "turnstile" || plugin.name === "turnstile";
|
|
13595
15354
|
return `
|
|
13596
15355
|
${isSeedDataPlugin ? `
|
|
13597
15356
|
<div class="backdrop-blur-md bg-black/20 rounded-xl border border-white/10 shadow-xl p-6 mb-6">
|
|
@@ -13618,12 +15377,15 @@ function renderSettingsTab(plugin) {
|
|
|
13618
15377
|
${isAuthPlugin ? `
|
|
13619
15378
|
<h2 class="text-xl font-semibold text-white mb-4">Authentication Settings</h2>
|
|
13620
15379
|
<p class="text-gray-400 mb-6">Configure user registration fields and validation rules.</p>
|
|
15380
|
+
` : isTurnstilePlugin ? `
|
|
15381
|
+
<h2 class="text-xl font-semibold text-white mb-4">Cloudflare Turnstile Settings</h2>
|
|
15382
|
+
<p class="text-gray-400 mb-6">Configure CAPTCHA-free bot protection for your forms.</p>
|
|
13621
15383
|
` : `
|
|
13622
15384
|
<h2 class="text-xl font-semibold text-white mb-4">Plugin Settings</h2>
|
|
13623
15385
|
`}
|
|
13624
15386
|
|
|
13625
15387
|
<form id="settings-form" class="space-y-6">
|
|
13626
|
-
${isAuthPlugin && Object.keys(settings).length > 0 ? renderAuthSettingsForm(settings) : Object.keys(settings).length > 0 ? renderSettingsFields(settings) : renderNoSettings(plugin)}
|
|
15388
|
+
${isAuthPlugin && Object.keys(settings).length > 0 ? renderAuthSettingsForm(settings) : isTurnstilePlugin && Object.keys(settings).length > 0 ? renderTurnstileSettingsForm(settings) : Object.keys(settings).length > 0 ? renderSettingsFields(settings) : renderNoSettings(plugin)}
|
|
13627
15389
|
|
|
13628
15390
|
${Object.keys(settings).length > 0 ? `
|
|
13629
15391
|
<div class="flex items-center justify-end pt-6 border-t border-white/10">
|
|
@@ -13687,6 +15449,80 @@ function renderSettingsFields(settings) {
|
|
|
13687
15449
|
}
|
|
13688
15450
|
}).join("");
|
|
13689
15451
|
}
|
|
15452
|
+
function renderTurnstileSettingsForm(settings) {
|
|
15453
|
+
const inputClass = "backdrop-blur-sm bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-white placeholder-gray-300 focus:border-blue-400 focus:outline-none transition-colors w-full";
|
|
15454
|
+
const selectClass = "backdrop-blur-sm bg-zinc-800 border border-white/20 rounded-lg px-3 py-2 text-white focus:border-blue-400 focus:outline-none transition-colors w-full [&>option]:bg-zinc-800 [&>option]:text-white";
|
|
15455
|
+
return `
|
|
15456
|
+
<!-- Enable Toggle -->
|
|
15457
|
+
<div class="flex items-center justify-between">
|
|
15458
|
+
<div>
|
|
15459
|
+
<label for="setting_enabled" class="text-sm font-medium text-gray-300">Enable Turnstile</label>
|
|
15460
|
+
<p class="text-xs text-gray-400">Enable or disable Turnstile verification globally</p>
|
|
15461
|
+
</div>
|
|
15462
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
15463
|
+
<input type="checkbox" name="setting_enabled" id="setting_enabled" ${settings.enabled ? "checked" : ""} class="sr-only peer">
|
|
15464
|
+
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
15465
|
+
</label>
|
|
15466
|
+
</div>
|
|
15467
|
+
|
|
15468
|
+
<!-- Site Key -->
|
|
15469
|
+
<div>
|
|
15470
|
+
<label for="setting_siteKey" class="block text-sm font-medium text-gray-300 mb-2">Site Key</label>
|
|
15471
|
+
<input type="text" name="setting_siteKey" id="setting_siteKey" value="${escapeHtmlAttr(settings.siteKey || "")}" placeholder="0x4AAAAAAAA..." class="${inputClass}">
|
|
15472
|
+
<p class="text-xs text-gray-400 mt-1">Your Cloudflare Turnstile site key (public)</p>
|
|
15473
|
+
</div>
|
|
15474
|
+
|
|
15475
|
+
<!-- Secret Key -->
|
|
15476
|
+
<div>
|
|
15477
|
+
<label for="setting_secretKey" class="block text-sm font-medium text-gray-300 mb-2">Secret Key</label>
|
|
15478
|
+
<input type="password" name="setting_secretKey" id="setting_secretKey" value="${escapeHtmlAttr(settings.secretKey || "")}" placeholder="0x4AAAAAAAA..." class="${inputClass}">
|
|
15479
|
+
<p class="text-xs text-gray-400 mt-1">Your Cloudflare Turnstile secret key (private)</p>
|
|
15480
|
+
</div>
|
|
15481
|
+
|
|
15482
|
+
<!-- Theme -->
|
|
15483
|
+
<div>
|
|
15484
|
+
<label for="setting_theme" class="block text-sm font-medium text-gray-300 mb-2">Widget Theme</label>
|
|
15485
|
+
<select name="setting_theme" id="setting_theme" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
|
|
15486
|
+
<option value="auto" ${settings.theme === "auto" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Auto (matches page theme)</option>
|
|
15487
|
+
<option value="light" ${settings.theme === "light" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Light</option>
|
|
15488
|
+
<option value="dark" ${settings.theme === "dark" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Dark</option>
|
|
15489
|
+
</select>
|
|
15490
|
+
<p class="text-xs text-gray-400 mt-1">Visual appearance of the Turnstile widget</p>
|
|
15491
|
+
</div>
|
|
15492
|
+
|
|
15493
|
+
<!-- Size -->
|
|
15494
|
+
<div>
|
|
15495
|
+
<label for="setting_size" class="block text-sm font-medium text-gray-300 mb-2">Widget Size</label>
|
|
15496
|
+
<select name="setting_size" id="setting_size" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
|
|
15497
|
+
<option value="normal" ${settings.size === "normal" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Normal (300x65px)</option>
|
|
15498
|
+
<option value="compact" ${settings.size === "compact" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Compact (130x120px)</option>
|
|
15499
|
+
</select>
|
|
15500
|
+
<p class="text-xs text-gray-400 mt-1">Size of the Turnstile challenge widget</p>
|
|
15501
|
+
</div>
|
|
15502
|
+
|
|
15503
|
+
<!-- Widget Mode -->
|
|
15504
|
+
<div>
|
|
15505
|
+
<label for="setting_mode" class="block text-sm font-medium text-gray-300 mb-2">Widget Mode</label>
|
|
15506
|
+
<select name="setting_mode" id="setting_mode" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
|
|
15507
|
+
<option value="managed" ${!settings.mode || settings.mode === "managed" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Managed (Recommended) - Adaptive challenge</option>
|
|
15508
|
+
<option value="non-interactive" ${settings.mode === "non-interactive" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Non-Interactive - Always visible, minimal friction</option>
|
|
15509
|
+
<option value="invisible" ${settings.mode === "invisible" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Invisible - No visible widget</option>
|
|
15510
|
+
</select>
|
|
15511
|
+
<p class="text-xs text-gray-400 mt-1"><strong>Managed:</strong> Shows challenge only when needed. <strong>Non-Interactive:</strong> Always shows but doesn't require interaction. <strong>Invisible:</strong> Runs in background without UI.</p>
|
|
15512
|
+
</div>
|
|
15513
|
+
|
|
15514
|
+
<!-- Appearance (Pre-clearance) -->
|
|
15515
|
+
<div>
|
|
15516
|
+
<label for="setting_appearance" class="block text-sm font-medium text-gray-300 mb-2">Pre-clearance / Appearance</label>
|
|
15517
|
+
<select name="setting_appearance" id="setting_appearance" class="${selectClass}" style="color: white; background-color: rgb(39, 39, 42);">
|
|
15518
|
+
<option value="always" ${!settings.appearance || settings.appearance === "always" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Always - Pre-clearance enabled (verifies immediately)</option>
|
|
15519
|
+
<option value="execute" ${settings.appearance === "execute" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Execute - Challenge on form submit</option>
|
|
15520
|
+
<option value="interaction-only" ${settings.appearance === "interaction-only" ? "selected" : ""} style="background-color: rgb(39, 39, 42); color: white;">Interaction Only - Only after user interaction</option>
|
|
15521
|
+
</select>
|
|
15522
|
+
<p class="text-xs text-gray-400 mt-1">Controls when Turnstile verification occurs. <strong>Always:</strong> Verifies immediately (pre-clearance). <strong>Execute:</strong> Verifies on form submit. <strong>Interaction Only:</strong> Only after user interaction.</p>
|
|
15523
|
+
</div>
|
|
15524
|
+
`;
|
|
15525
|
+
}
|
|
13690
15526
|
function renderNoSettings(plugin) {
|
|
13691
15527
|
if (plugin.id === "seed-data" || plugin.name === "seed-data") {
|
|
13692
15528
|
return `
|
|
@@ -13918,6 +15754,32 @@ var AVAILABLE_PLUGINS = [
|
|
|
13918
15754
|
permissions: [],
|
|
13919
15755
|
dependencies: [],
|
|
13920
15756
|
is_core: false
|
|
15757
|
+
},
|
|
15758
|
+
{
|
|
15759
|
+
id: "turnstile",
|
|
15760
|
+
name: "turnstile-plugin",
|
|
15761
|
+
display_name: "Cloudflare Turnstile",
|
|
15762
|
+
description: "CAPTCHA-free bot protection for forms using Cloudflare Turnstile. Provides seamless spam prevention with configurable modes, themes, and pre-clearance options.",
|
|
15763
|
+
version: "1.0.0",
|
|
15764
|
+
author: "SonicJS Team",
|
|
15765
|
+
category: "security",
|
|
15766
|
+
icon: "\u{1F6E1}\uFE0F",
|
|
15767
|
+
permissions: [],
|
|
15768
|
+
dependencies: [],
|
|
15769
|
+
is_core: true
|
|
15770
|
+
},
|
|
15771
|
+
{
|
|
15772
|
+
id: "ai-search",
|
|
15773
|
+
name: "ai-search-plugin",
|
|
15774
|
+
display_name: "AI Search",
|
|
15775
|
+
description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
|
|
15776
|
+
version: "1.0.0",
|
|
15777
|
+
author: "SonicJS Team",
|
|
15778
|
+
category: "search",
|
|
15779
|
+
icon: "\u{1F50D}",
|
|
15780
|
+
permissions: [],
|
|
15781
|
+
dependencies: [],
|
|
15782
|
+
is_core: true
|
|
13921
15783
|
}
|
|
13922
15784
|
];
|
|
13923
15785
|
adminPluginRoutes.get("/", async (c) => {
|
|
@@ -13996,6 +15858,9 @@ adminPluginRoutes.get("/:id", async (c) => {
|
|
|
13996
15858
|
const user = c.get("user");
|
|
13997
15859
|
const db = c.env.DB;
|
|
13998
15860
|
const pluginId = c.req.param("id");
|
|
15861
|
+
if (pluginId === "ai-search") {
|
|
15862
|
+
return c.text("", 404);
|
|
15863
|
+
}
|
|
13999
15864
|
if (user?.role !== "admin") {
|
|
14000
15865
|
return c.redirect("/admin/plugins");
|
|
14001
15866
|
}
|
|
@@ -14288,6 +16153,60 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
14288
16153
|
});
|
|
14289
16154
|
return c.json({ success: true, plugin: easyMdxPlugin2 });
|
|
14290
16155
|
}
|
|
16156
|
+
if (body.name === "ai-search-plugin" || body.name === "ai-search") {
|
|
16157
|
+
const defaultSettings = {
|
|
16158
|
+
enabled: true,
|
|
16159
|
+
ai_mode_enabled: true,
|
|
16160
|
+
selected_collections: [],
|
|
16161
|
+
dismissed_collections: [],
|
|
16162
|
+
autocomplete_enabled: true,
|
|
16163
|
+
cache_duration: 1,
|
|
16164
|
+
results_limit: 20,
|
|
16165
|
+
index_media: false
|
|
16166
|
+
};
|
|
16167
|
+
const aiSearchPlugin = await pluginService.installPlugin({
|
|
16168
|
+
id: "ai-search",
|
|
16169
|
+
name: "ai-search-plugin",
|
|
16170
|
+
display_name: "AI Search",
|
|
16171
|
+
description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
|
|
16172
|
+
version: "1.0.0",
|
|
16173
|
+
author: "SonicJS Team",
|
|
16174
|
+
category: "search",
|
|
16175
|
+
icon: "\u{1F50D}",
|
|
16176
|
+
permissions: [],
|
|
16177
|
+
dependencies: [],
|
|
16178
|
+
is_core: true,
|
|
16179
|
+
settings: defaultSettings
|
|
16180
|
+
});
|
|
16181
|
+
return c.json({ success: true, plugin: aiSearchPlugin });
|
|
16182
|
+
}
|
|
16183
|
+
if (body.name === "turnstile-plugin") {
|
|
16184
|
+
const turnstilePlugin = await pluginService.installPlugin({
|
|
16185
|
+
id: "turnstile",
|
|
16186
|
+
name: "turnstile-plugin",
|
|
16187
|
+
display_name: "Cloudflare Turnstile",
|
|
16188
|
+
description: "CAPTCHA-free bot protection for forms using Cloudflare Turnstile. Provides seamless spam prevention with configurable modes, themes, and pre-clearance options.",
|
|
16189
|
+
version: "1.0.0",
|
|
16190
|
+
author: "SonicJS Team",
|
|
16191
|
+
category: "security",
|
|
16192
|
+
icon: "\u{1F6E1}\uFE0F",
|
|
16193
|
+
permissions: [],
|
|
16194
|
+
dependencies: [],
|
|
16195
|
+
is_core: true,
|
|
16196
|
+
settings: {
|
|
16197
|
+
siteKey: "",
|
|
16198
|
+
secretKey: "",
|
|
16199
|
+
theme: "auto",
|
|
16200
|
+
size: "normal",
|
|
16201
|
+
mode: "managed",
|
|
16202
|
+
appearance: "always",
|
|
16203
|
+
preClearanceEnabled: false,
|
|
16204
|
+
preClearanceLevel: "managed",
|
|
16205
|
+
enabled: false
|
|
16206
|
+
}
|
|
16207
|
+
});
|
|
16208
|
+
return c.json({ success: true, plugin: turnstilePlugin });
|
|
16209
|
+
}
|
|
14291
16210
|
return c.json({ error: "Plugin not found in registry" }, 404);
|
|
14292
16211
|
} catch (error) {
|
|
14293
16212
|
console.error("Error installing plugin:", error);
|
|
@@ -19461,16 +21380,30 @@ adminCollectionsRoutes.get("/:id", async (c) => {
|
|
|
19461
21380
|
const schema = typeof collection.schema === "string" ? JSON.parse(collection.schema) : collection.schema;
|
|
19462
21381
|
if (schema && schema.properties) {
|
|
19463
21382
|
let fieldOrder = 0;
|
|
19464
|
-
fields = Object.entries(schema.properties).map(([fieldName, fieldConfig]) =>
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
21383
|
+
fields = Object.entries(schema.properties).map(([fieldName, fieldConfig]) => {
|
|
21384
|
+
let fieldType = fieldConfig.type || "string";
|
|
21385
|
+
if (fieldConfig.enum) {
|
|
21386
|
+
fieldType = "select";
|
|
21387
|
+
} else if (fieldConfig.format === "richtext") {
|
|
21388
|
+
fieldType = "richtext";
|
|
21389
|
+
} else if (fieldConfig.format === "media") {
|
|
21390
|
+
fieldType = "media";
|
|
21391
|
+
} else if (fieldConfig.format === "date-time") {
|
|
21392
|
+
fieldType = "date";
|
|
21393
|
+
} else if (fieldConfig.type === "slug" || fieldConfig.format === "slug") {
|
|
21394
|
+
fieldType = "slug";
|
|
21395
|
+
}
|
|
21396
|
+
return {
|
|
21397
|
+
id: `schema-${fieldName}`,
|
|
21398
|
+
field_name: fieldName,
|
|
21399
|
+
field_type: fieldType,
|
|
21400
|
+
field_label: fieldConfig.title || fieldName,
|
|
21401
|
+
field_options: fieldConfig,
|
|
21402
|
+
field_order: fieldOrder++,
|
|
21403
|
+
is_required: fieldConfig.required === true || schema.required && schema.required.includes(fieldName),
|
|
21404
|
+
is_searchable: fieldConfig.searchable === true || false
|
|
21405
|
+
};
|
|
21406
|
+
});
|
|
19474
21407
|
}
|
|
19475
21408
|
} catch (e) {
|
|
19476
21409
|
console.error("Error parsing collection schema:", e);
|
|
@@ -19685,6 +21618,9 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
|
|
|
19685
21618
|
fieldConfig.enum = parsedOptions.options || [];
|
|
19686
21619
|
} else if (fieldType === "media") {
|
|
19687
21620
|
fieldConfig.format = "media";
|
|
21621
|
+
} else if (fieldType === "slug") {
|
|
21622
|
+
fieldConfig.type = "slug";
|
|
21623
|
+
fieldConfig.format = "slug";
|
|
19688
21624
|
} else if (fieldType === "quill") {
|
|
19689
21625
|
fieldConfig.type = "quill";
|
|
19690
21626
|
} else if (fieldType === "mdxeditor") {
|
|
@@ -21802,6 +23738,6 @@ var ROUTES_INFO = {
|
|
|
21802
23738
|
reference: "https://github.com/sonicjs/sonicjs"
|
|
21803
23739
|
};
|
|
21804
23740
|
|
|
21805
|
-
export {
|
|
21806
|
-
//# sourceMappingURL=chunk-
|
|
21807
|
-
//# sourceMappingURL=chunk-
|
|
23741
|
+
export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, test_cleanup_default, userRoutes };
|
|
23742
|
+
//# sourceMappingURL=chunk-F6GZURXJ.js.map
|
|
23743
|
+
//# sourceMappingURL=chunk-F6GZURXJ.js.map
|