@sonicjs-cms/core 2.5.0 → 2.7.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-AYPF6C4D.cjs} +5 -5
- package/dist/{chunk-YIXSSJWD.cjs.map → chunk-AYPF6C4D.cjs.map} +1 -1
- package/dist/chunk-CLIH2T74.js +403 -0
- package/dist/chunk-CLIH2T74.js.map +1 -0
- package/dist/{chunk-BHNDALCA.js → chunk-DNHJS6RN.js} +6 -4
- package/dist/chunk-DNHJS6RN.js.map +1 -0
- package/dist/{chunk-YYV3XQOQ.cjs → chunk-E2BXLXPW.cjs} +7 -7
- package/dist/{chunk-YYV3XQOQ.cjs.map → chunk-E2BXLXPW.cjs.map} +1 -1
- package/dist/{chunk-AZLU3ROK.cjs → chunk-EHSZ6TAN.cjs} +11 -4
- package/dist/chunk-EHSZ6TAN.cjs.map +1 -0
- package/dist/{chunk-3YUHXWSG.js → chunk-F332TENF.js} +3 -3
- package/dist/{chunk-3YUHXWSG.js.map → chunk-F332TENF.js.map} +1 -1
- package/dist/{chunk-V5LBQN3I.js → chunk-GRN3GHUG.js} +11 -4
- package/dist/chunk-GRN3GHUG.js.map +1 -0
- package/dist/{chunk-UAQL2VWX.cjs → chunk-J7F3NPAP.cjs} +2436 -707
- package/dist/chunk-J7F3NPAP.cjs.map +1 -0
- package/dist/{chunk-VEL7QRYI.js → chunk-L2IDZI7F.js} +9 -2
- package/dist/chunk-L2IDZI7F.js.map +1 -0
- package/dist/{chunk-ILZ3DP4I.cjs → chunk-MPT5PA6U.cjs} +24 -2
- package/dist/chunk-MPT5PA6U.cjs.map +1 -0
- package/dist/{chunk-ZWV3EBZ7.cjs → chunk-MYB5RY7H.cjs} +6 -4
- package/dist/chunk-MYB5RY7H.cjs.map +1 -0
- package/dist/{chunk-OJZ45OJD.js → chunk-UISZ2MBW.js} +2272 -544
- package/dist/chunk-UISZ2MBW.js.map +1 -0
- package/dist/{chunk-AVPUX57O.js → chunk-V3KVSEG6.js} +3 -3
- package/dist/{chunk-AVPUX57O.js.map → chunk-V3KVSEG6.js.map} +1 -1
- package/dist/{chunk-TJTWRO4G.js → chunk-Y3EWJQ4D.js} +4 -4
- package/dist/{chunk-TJTWRO4G.js.map → chunk-Y3EWJQ4D.js.map} +1 -1
- package/dist/{chunk-LWG2MWDA.cjs → chunk-Y72M3MVX.cjs} +4 -4
- package/dist/{chunk-LWG2MWDA.cjs.map → chunk-Y72M3MVX.cjs.map} +1 -1
- 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-I4V3VZWF.cjs → chunk-YRFAQ6MI.cjs} +9 -2
- package/dist/chunk-YRFAQ6MI.cjs.map +1 -0
- package/dist/{collection-config-B6gMPunn.d.cts → collection-config-BF95LgQb.d.cts} +1 -1
- package/dist/{collection-config-B6gMPunn.d.ts → collection-config-BF95LgQb.d.ts} +1 -1
- package/dist/index.cjs +4098 -424
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -8
- package/dist/index.d.ts +503 -8
- package/dist/index.js +4008 -341
- 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-LEMFV2ND.cjs +13 -0
- package/dist/{migrations-NIEUFG44.cjs.map → migrations-LEMFV2ND.cjs.map} +1 -1
- package/dist/migrations-RKQES6XY.js +4 -0
- package/dist/{migrations-TGZKJKV4.js.map → migrations-RKQES6XY.js.map} +1 -1
- package/dist/{plugin-bootstrap-dYhD9fQR.d.ts → plugin-bootstrap-CB-xaBfK.d.ts} +2 -2
- package/dist/{plugin-bootstrap-SHsdjE6X.d.cts → plugin-bootstrap-U-cw9jn3.d.cts} +2 -2
- package/dist/plugins.cjs +11 -11
- package/dist/plugins.js +2 -2
- package/dist/routes.cjs +27 -27
- package/dist/routes.d.cts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +7 -7
- 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 +14 -14
- package/dist/utils.d.cts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/migrations/029_ai_search_plugin.sql +45 -0
- package/package.json +4 -2
- package/dist/chunk-AI2JJIJX.cjs +0 -211
- package/dist/chunk-AI2JJIJX.cjs.map +0 -1
- package/dist/chunk-AZLU3ROK.cjs.map +0 -1
- package/dist/chunk-BHNDALCA.js.map +0 -1
- package/dist/chunk-I4V3VZWF.cjs.map +0 -1
- package/dist/chunk-ILZ3DP4I.cjs.map +0 -1
- package/dist/chunk-OJZ45OJD.js.map +0 -1
- package/dist/chunk-QDBNW7KQ.js +0 -209
- package/dist/chunk-QDBNW7KQ.js.map +0 -1
- package/dist/chunk-SGAG6FD3.js.map +0 -1
- package/dist/chunk-UAQL2VWX.cjs.map +0 -1
- package/dist/chunk-V5LBQN3I.js.map +0 -1
- package/dist/chunk-VEL7QRYI.js.map +0 -1
- package/dist/chunk-ZWV3EBZ7.cjs.map +0 -1
- package/dist/migrations-NIEUFG44.cjs +0 -13
- package/dist/migrations-TGZKJKV4.js +0 -4
|
@@ -1,10 +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 { PluginBuilder } from './chunk-
|
|
7
|
-
import { QueryFilterBuilder,
|
|
2
|
+
import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity } from './chunk-Y3EWJQ4D.js';
|
|
3
|
+
import { PluginService } from './chunk-YFJJU26H.js';
|
|
4
|
+
import { MigrationService } from './chunk-L2IDZI7F.js';
|
|
5
|
+
import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-GRN3GHUG.js';
|
|
6
|
+
import { PluginBuilder } from './chunk-CLIH2T74.js';
|
|
7
|
+
import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml, getBlocksFieldConfig, parseBlocksValue } from './chunk-DNHJS6RN.js';
|
|
8
8
|
import { metricsTracker } from './chunk-FICTAGD4.js';
|
|
9
9
|
import { Hono } from 'hono';
|
|
10
10
|
import { cors } from 'hono/cors';
|
|
@@ -1583,6 +1583,107 @@ adminApiRoutes.get("/collections/:id", async (c) => {
|
|
|
1583
1583
|
return c.json({ error: "Failed to fetch collection" }, 500);
|
|
1584
1584
|
}
|
|
1585
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
|
+
});
|
|
1586
1687
|
adminApiRoutes.post("/collections", async (c) => {
|
|
1587
1688
|
try {
|
|
1588
1689
|
const contentType = c.req.header("Content-Type");
|
|
@@ -1752,7 +1853,7 @@ adminApiRoutes.delete("/collections/:id", async (c) => {
|
|
|
1752
1853
|
});
|
|
1753
1854
|
adminApiRoutes.get("/migrations/status", async (c) => {
|
|
1754
1855
|
try {
|
|
1755
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1856
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
|
|
1756
1857
|
const db = c.env.DB;
|
|
1757
1858
|
const migrationService = new MigrationService2(db);
|
|
1758
1859
|
const status = await migrationService.getMigrationStatus();
|
|
@@ -1777,7 +1878,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
|
|
|
1777
1878
|
error: "Unauthorized. Admin access required."
|
|
1778
1879
|
}, 403);
|
|
1779
1880
|
}
|
|
1780
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1881
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
|
|
1781
1882
|
const db = c.env.DB;
|
|
1782
1883
|
const migrationService = new MigrationService2(db);
|
|
1783
1884
|
const result = await migrationService.runPendingMigrations();
|
|
@@ -1796,7 +1897,7 @@ adminApiRoutes.post("/migrations/run", async (c) => {
|
|
|
1796
1897
|
});
|
|
1797
1898
|
adminApiRoutes.get("/migrations/validate", async (c) => {
|
|
1798
1899
|
try {
|
|
1799
|
-
const { MigrationService: MigrationService2 } = await import('./migrations-
|
|
1900
|
+
const { MigrationService: MigrationService2 } = await import('./migrations-RKQES6XY.js');
|
|
1800
1901
|
const db = c.env.DB;
|
|
1801
1902
|
const migrationService = new MigrationService2(db);
|
|
1802
1903
|
const validation = await migrationService.validateSchema();
|
|
@@ -1823,7 +1924,7 @@ function renderLoginPage(data, demoLoginActive = false) {
|
|
|
1823
1924
|
<meta charset="UTF-8">
|
|
1824
1925
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1825
1926
|
<title>Login - SonicJS AI</title>
|
|
1826
|
-
<link rel="icon" type="image/
|
|
1927
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
1827
1928
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
|
1828
1929
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
1829
1930
|
<script>
|
|
@@ -2000,7 +2101,7 @@ function renderRegisterPage(data) {
|
|
|
2000
2101
|
<meta charset="UTF-8">
|
|
2001
2102
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2002
2103
|
<title>Register - SonicJS AI</title>
|
|
2003
|
-
<link rel="icon" type="image/
|
|
2104
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
2004
2105
|
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
|
|
2005
2106
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
2006
2107
|
<script>
|
|
@@ -2023,40 +2124,18 @@ function renderRegisterPage(data) {
|
|
|
2023
2124
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
|
2024
2125
|
<!-- Logo Section -->
|
|
2025
2126
|
<div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
|
|
2026
|
-
<div class="mx-auto w-
|
|
2027
|
-
<svg class="w-
|
|
2028
|
-
<path
|
|
2029
|
-
<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>
|
|
2030
|
-
<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>
|
|
2031
|
-
<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>
|
|
2032
|
-
<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>
|
|
2033
|
-
<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>
|
|
2034
|
-
<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>
|
|
2035
|
-
<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>
|
|
2036
|
-
<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"/>
|
|
2037
2130
|
</svg>
|
|
2038
2131
|
</div>
|
|
2039
|
-
<
|
|
2040
|
-
|
|
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>
|
|
2041
2134
|
</div>
|
|
2042
2135
|
|
|
2043
2136
|
<!-- Form Container -->
|
|
2044
2137
|
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
2045
2138
|
<div class="bg-zinc-900 shadow-sm ring-1 ring-white/10 rounded-xl px-6 py-8 sm:px-10">
|
|
2046
|
-
<!-- Setup Banner -->
|
|
2047
|
-
${data.isSetup ? `
|
|
2048
|
-
<div class="mb-6 rounded-lg bg-blue-500/10 p-4 ring-1 ring-blue-500/20">
|
|
2049
|
-
<div class="flex items-start gap-x-3">
|
|
2050
|
-
<svg class="h-5 w-5 text-blue-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
2051
|
-
<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"/>
|
|
2052
|
-
</svg>
|
|
2053
|
-
<div class="flex-1">
|
|
2054
|
-
<p class="text-sm font-medium text-blue-300">First-time Setup</p>
|
|
2055
|
-
<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>
|
|
2056
|
-
</div>
|
|
2057
|
-
</div>
|
|
2058
|
-
</div>
|
|
2059
|
-
` : ""}
|
|
2060
2139
|
<!-- Alerts -->
|
|
2061
2140
|
${data.error ? `<div class="mb-6">${renderAlert({ type: "error", message: data.error })}</div>` : ""}
|
|
2062
2141
|
|
|
@@ -2171,7 +2250,6 @@ function renderRegisterPage(data) {
|
|
|
2171
2250
|
</html>
|
|
2172
2251
|
`;
|
|
2173
2252
|
}
|
|
2174
|
-
var adminExistsCache = null;
|
|
2175
2253
|
async function isRegistrationEnabled(db) {
|
|
2176
2254
|
try {
|
|
2177
2255
|
const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ?").bind("core-auth").first();
|
|
@@ -2193,21 +2271,6 @@ async function isFirstUserRegistration(db) {
|
|
|
2193
2271
|
return false;
|
|
2194
2272
|
}
|
|
2195
2273
|
}
|
|
2196
|
-
async function checkAdminUserExists(db) {
|
|
2197
|
-
if (adminExistsCache !== null) {
|
|
2198
|
-
return adminExistsCache;
|
|
2199
|
-
}
|
|
2200
|
-
try {
|
|
2201
|
-
const result = await db.prepare("SELECT id FROM users WHERE role = ?").bind("admin").first();
|
|
2202
|
-
adminExistsCache = !!result;
|
|
2203
|
-
return adminExistsCache;
|
|
2204
|
-
} catch {
|
|
2205
|
-
return false;
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
function setAdminExists() {
|
|
2209
|
-
adminExistsCache = true;
|
|
2210
|
-
}
|
|
2211
2274
|
var baseRegistrationSchema = z.object({
|
|
2212
2275
|
email: z.string().email("Valid email is required"),
|
|
2213
2276
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
@@ -2269,11 +2332,8 @@ authRoutes.get("/register", async (c) => {
|
|
|
2269
2332
|
}
|
|
2270
2333
|
}
|
|
2271
2334
|
const error = c.req.query("error");
|
|
2272
|
-
const isSetup = c.req.query("setup") === "true";
|
|
2273
2335
|
const pageData = {
|
|
2274
|
-
error: error || void 0
|
|
2275
|
-
isSetup: isSetup && isFirstUser
|
|
2276
|
-
// Only show setup message if truly first user
|
|
2336
|
+
error: error || void 0
|
|
2277
2337
|
};
|
|
2278
2338
|
return c.html(renderRegisterPage(pageData));
|
|
2279
2339
|
});
|
|
@@ -2548,9 +2608,6 @@ authRoutes.post("/register/form", async (c) => {
|
|
|
2548
2608
|
now.getTime(),
|
|
2549
2609
|
now.getTime()
|
|
2550
2610
|
).run();
|
|
2551
|
-
if (isFirstUser) {
|
|
2552
|
-
setAdminExists();
|
|
2553
|
-
}
|
|
2554
2611
|
const token = await AuthManager.generateToken(userId, normalizedEmail, role);
|
|
2555
2612
|
setCookie(c, "auth_token", token, {
|
|
2556
2613
|
httpOnly: true,
|
|
@@ -2672,7 +2729,6 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2672
2729
|
if (existingAdmin) {
|
|
2673
2730
|
const passwordHash2 = await AuthManager.hashPassword("sonicjs!");
|
|
2674
2731
|
await db.prepare("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?").bind(passwordHash2, Date.now(), existingAdmin.id).run();
|
|
2675
|
-
setAdminExists();
|
|
2676
2732
|
return c.json({
|
|
2677
2733
|
message: "Admin user already exists (password updated)",
|
|
2678
2734
|
user: {
|
|
@@ -2703,7 +2759,6 @@ authRoutes.post("/seed-admin", async (c) => {
|
|
|
2703
2759
|
now,
|
|
2704
2760
|
now
|
|
2705
2761
|
).run();
|
|
2706
|
-
setAdminExists();
|
|
2707
2762
|
return c.json({
|
|
2708
2763
|
message: "Admin user created successfully",
|
|
2709
2764
|
user: {
|
|
@@ -3413,7 +3468,164 @@ var test_cleanup_default = app;
|
|
|
3413
3468
|
// src/templates/pages/admin-content-form.template.ts
|
|
3414
3469
|
init_admin_layout_catalyst_template();
|
|
3415
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
|
+
|
|
3416
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
|
+
}
|
|
3417
3629
|
function renderDynamicField(field, options = {}) {
|
|
3418
3630
|
const { value = "", errors = [], disabled = false, className = "", pluginStatuses = {}, collectionId = "", contentId = "" } = options;
|
|
3419
3631
|
const opts = field.field_options || {};
|
|
@@ -3837,11 +4049,11 @@ function renderDynamicField(field, options = {}) {
|
|
|
3837
4049
|
`;
|
|
3838
4050
|
break;
|
|
3839
4051
|
case "select":
|
|
3840
|
-
const
|
|
4052
|
+
const selectOptions = opts.options || [];
|
|
3841
4053
|
const multiple = opts.multiple ? "multiple" : "";
|
|
3842
4054
|
const selectedValues = Array.isArray(value) ? value : [value];
|
|
3843
4055
|
fieldHTML = `
|
|
3844
|
-
<select
|
|
4056
|
+
<select
|
|
3845
4057
|
id="${fieldId}"
|
|
3846
4058
|
name="${fieldName}${opts.multiple ? "[]" : ""}"
|
|
3847
4059
|
class="${baseClasses} ${errorClasses}"
|
|
@@ -3850,7 +4062,7 @@ function renderDynamicField(field, options = {}) {
|
|
|
3850
4062
|
${disabled ? "disabled" : ""}
|
|
3851
4063
|
>
|
|
3852
4064
|
${!required && !opts.multiple ? '<option value="">Choose an option...</option>' : ""}
|
|
3853
|
-
${
|
|
4065
|
+
${selectOptions.map((option) => {
|
|
3854
4066
|
const optionValue = typeof option === "string" ? option : option.value;
|
|
3855
4067
|
const optionLabel = typeof option === "string" ? option : option.label;
|
|
3856
4068
|
const selected = selectedValues.includes(optionValue) ? "selected" : "";
|
|
@@ -3869,43 +4081,124 @@ function renderDynamicField(field, options = {}) {
|
|
|
3869
4081
|
` : ""}
|
|
3870
4082
|
`;
|
|
3871
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;
|
|
3872
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
|
+
};
|
|
3873
4136
|
fieldHTML = `
|
|
3874
4137
|
<div class="media-field-container">
|
|
3875
|
-
<input type="hidden" id="${fieldId}" name="${fieldName}" value="${
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
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
|
+
|
|
3879
4164
|
<div class="media-actions mt-2 space-x-2">
|
|
3880
4165
|
<button
|
|
3881
4166
|
type="button"
|
|
3882
|
-
onclick="openMediaSelector('${fieldId}')"
|
|
4167
|
+
onclick="openMediaSelector('${fieldId}', ${isMultiple})"
|
|
3883
4168
|
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all"
|
|
3884
4169
|
${disabled ? "disabled" : ""}
|
|
3885
4170
|
>
|
|
3886
4171
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
3887
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>
|
|
3888
4173
|
</svg>
|
|
3889
|
-
Select Media
|
|
4174
|
+
${isMultiple ? "Select Media (Multiple)" : "Select Media"}
|
|
3890
4175
|
</button>
|
|
3891
|
-
${
|
|
4176
|
+
${(isMultiple ? mediaValues.length > 0 : singleValue) ? `
|
|
3892
4177
|
<button
|
|
3893
4178
|
type="button"
|
|
3894
4179
|
onclick="clearMediaField('${fieldId}')"
|
|
3895
4180
|
class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-xl hover:bg-red-700 transition-all"
|
|
3896
4181
|
${disabled ? "disabled" : ""}
|
|
3897
4182
|
>
|
|
3898
|
-
Remove
|
|
4183
|
+
${isMultiple ? "Clear All" : "Remove"}
|
|
3899
4184
|
</button>
|
|
3900
4185
|
` : ""}
|
|
3901
4186
|
</div>
|
|
3902
4187
|
</div>
|
|
3903
4188
|
`;
|
|
3904
4189
|
break;
|
|
4190
|
+
case "object":
|
|
4191
|
+
return renderStructuredObjectField(field, options);
|
|
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, options, baseClasses, errorClasses);
|
|
4196
|
+
}
|
|
4197
|
+
return renderStructuredArrayField(field, options);
|
|
3905
4198
|
default:
|
|
3906
4199
|
fieldHTML = `
|
|
3907
|
-
<input
|
|
3908
|
-
type="text"
|
|
4200
|
+
<input
|
|
4201
|
+
type="text"
|
|
3909
4202
|
id="${fieldId}"
|
|
3910
4203
|
name="${fieldName}"
|
|
3911
4204
|
value="${escapeHtml2(value)}"
|
|
@@ -3955,76 +4248,824 @@ function renderFieldGroup(title, fields, collapsible = false) {
|
|
|
3955
4248
|
</div>
|
|
3956
4249
|
`;
|
|
3957
4250
|
}
|
|
3958
|
-
function
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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))}">
|
|
3968
4278
|
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
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
|
+
`;
|
|
3994
4307
|
}
|
|
3995
|
-
function
|
|
3996
|
-
const
|
|
3997
|
-
const
|
|
3998
|
-
const
|
|
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("");
|
|
3999
4325
|
return `
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
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))}">
|
|
4009
4353
|
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
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>
|
|
4014
4366
|
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
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("");
|
|
4433
|
+
}
|
|
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
|
+
}
|
|
4473
|
+
}
|
|
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
|
+
}
|
|
4487
|
+
}
|
|
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
|
+
}
|
|
4517
|
+
}
|
|
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
|
+
`;
|
|
4536
|
+
}
|
|
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
|
+
`;
|
|
4548
|
+
}
|
|
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
|
+
}));
|
|
4616
|
+
}
|
|
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
|
+
}
|
|
5009
|
+
|
|
5010
|
+
// src/plugins/available/tinymce-plugin/index.ts
|
|
5011
|
+
var builder = PluginBuilder.create({
|
|
5012
|
+
name: "tinymce-plugin",
|
|
5013
|
+
version: "1.0.0",
|
|
5014
|
+
description: "Powerful WYSIWYG rich text editor for content creation"
|
|
5015
|
+
});
|
|
5016
|
+
builder.metadata({
|
|
5017
|
+
author: {
|
|
5018
|
+
name: "SonicJS Team",
|
|
5019
|
+
email: "team@sonicjs.com"
|
|
5020
|
+
},
|
|
5021
|
+
license: "MIT",
|
|
5022
|
+
compatibility: "^2.0.0"
|
|
5023
|
+
});
|
|
5024
|
+
builder.lifecycle({
|
|
5025
|
+
activate: async () => {
|
|
5026
|
+
console.info("\u2705 TinyMCE plugin activated");
|
|
5027
|
+
},
|
|
5028
|
+
deactivate: async () => {
|
|
5029
|
+
console.info("\u274C TinyMCE plugin deactivated");
|
|
5030
|
+
}
|
|
5031
|
+
});
|
|
5032
|
+
builder.build();
|
|
5033
|
+
function getTinyMCEScript(apiKey = "no-api-key") {
|
|
5034
|
+
return `<script src="https://cdn.tiny.cloud/1/${apiKey}/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>`;
|
|
5035
|
+
}
|
|
5036
|
+
function getTinyMCEInitScript(config) {
|
|
5037
|
+
const skin = config?.skin || "oxide-dark";
|
|
5038
|
+
const contentCss = skin.includes("dark") ? "dark" : "default";
|
|
5039
|
+
const defaultHeight = config?.defaultHeight || 300;
|
|
5040
|
+
return `
|
|
5041
|
+
// Initialize TinyMCE for all richtext fields
|
|
5042
|
+
function initializeTinyMCE() {
|
|
5043
|
+
if (typeof tinymce !== 'undefined') {
|
|
5044
|
+
// Find all textareas that need TinyMCE
|
|
5045
|
+
document.querySelectorAll('.richtext-container textarea').forEach((textarea) => {
|
|
5046
|
+
// Skip if already initialized
|
|
5047
|
+
if (tinymce.get(textarea.id)) {
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
// Get configuration from data attributes
|
|
5052
|
+
const container = textarea.closest('.richtext-container');
|
|
5053
|
+
const height = container?.dataset.height || ${defaultHeight};
|
|
5054
|
+
const toolbar = container?.dataset.toolbar || 'full';
|
|
5055
|
+
|
|
5056
|
+
tinymce.init({
|
|
5057
|
+
selector: '#' + textarea.id,
|
|
5058
|
+
skin: '${skin}',
|
|
5059
|
+
content_css: '${contentCss}',
|
|
5060
|
+
height: parseInt(height),
|
|
5061
|
+
menubar: false,
|
|
5062
|
+
plugins: [
|
|
5063
|
+
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
|
5064
|
+
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
|
5065
|
+
'insertdatetime', 'media', 'table', 'help', 'wordcount'
|
|
5066
|
+
],
|
|
5067
|
+
toolbar: toolbar === 'simple'
|
|
5068
|
+
? 'bold italic underline | bullist numlist | link'
|
|
4028
5069
|
: toolbar === 'minimal'
|
|
4029
5070
|
? 'bold italic | link'
|
|
4030
5071
|
: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
|
|
@@ -4909,73 +5950,373 @@ function renderContentFormPage(data) {
|
|
|
4909
5950
|
if (preview) {
|
|
4910
5951
|
preview.classList.add('hidden');
|
|
4911
5952
|
}
|
|
4912
|
-
}
|
|
5953
|
+
}
|
|
5954
|
+
|
|
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
|
+
}
|
|
4913
6233
|
|
|
4914
|
-
|
|
4915
|
-
closeMediaSelector();
|
|
4916
|
-
}
|
|
6234
|
+
const selectedId = document.getElementById(fieldId)?.value;
|
|
4917
6235
|
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
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
|
+
}
|
|
4922
6243
|
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
return;
|
|
4928
|
-
}
|
|
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;
|
|
4929
6248
|
|
|
4930
|
-
|
|
6249
|
+
button.appendChild(titleEl);
|
|
4931
6250
|
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
if (hiddenInput) {
|
|
4935
|
-
hiddenInput.value = mediaUrl;
|
|
4936
|
-
}
|
|
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';
|
|
4937
6253
|
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
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
|
+
}
|
|
4944
6260
|
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
6261
|
+
if (item.slug) {
|
|
6262
|
+
const slugEl = document.createElement('span');
|
|
6263
|
+
slugEl.textContent = item.slug;
|
|
6264
|
+
metaRow.appendChild(slugEl);
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
if (metaRow.childElementCount > 0) {
|
|
6268
|
+
button.appendChild(metaRow);
|
|
6269
|
+
}
|
|
6270
|
+
|
|
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>';
|
|
4956
6286
|
}
|
|
4957
|
-
}
|
|
6287
|
+
};
|
|
4958
6288
|
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
6289
|
+
loadResults();
|
|
6290
|
+
|
|
6291
|
+
searchInput.addEventListener('input', () => {
|
|
6292
|
+
if (referenceSearchTimeout) {
|
|
6293
|
+
clearTimeout(referenceSearchTimeout);
|
|
6294
|
+
}
|
|
6295
|
+
referenceSearchTimeout = setTimeout(() => {
|
|
6296
|
+
loadResults(searchInput.value.trim());
|
|
6297
|
+
}, 250);
|
|
4963
6298
|
});
|
|
4964
|
-
|
|
4965
|
-
if (selectedItem) {
|
|
4966
|
-
selectedItem.classList.add('ring-2', 'ring-lime-500', 'dark:ring-lime-400');
|
|
4967
|
-
}
|
|
4968
|
-
};
|
|
6299
|
+
}
|
|
4969
6300
|
|
|
4970
|
-
|
|
4971
|
-
document.
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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;
|
|
4975
6306
|
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
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
|
+
});
|
|
4979
6320
|
|
|
4980
6321
|
// Custom select options
|
|
4981
6322
|
function addCustomOption(input, selectId) {
|
|
@@ -5142,7 +6483,7 @@ function renderContentListPage(data) {
|
|
|
5142
6483
|
if (data.search) urlParams.set("search", data.search);
|
|
5143
6484
|
if (data.page && data.page !== 1) urlParams.set("page", data.page.toString());
|
|
5144
6485
|
const currentParams = urlParams.toString();
|
|
5145
|
-
|
|
6486
|
+
data.modelName !== "all" || data.status !== "all" || !!data.search;
|
|
5146
6487
|
const filterBarData = {
|
|
5147
6488
|
filters: [
|
|
5148
6489
|
{
|
|
@@ -5172,6 +6513,11 @@ function renderContentListPage(data) {
|
|
|
5172
6513
|
}
|
|
5173
6514
|
],
|
|
5174
6515
|
actions: [
|
|
6516
|
+
{
|
|
6517
|
+
label: "Advanced Search",
|
|
6518
|
+
className: "btn-primary",
|
|
6519
|
+
onclick: "openAdvancedSearch()"
|
|
6520
|
+
},
|
|
5175
6521
|
{
|
|
5176
6522
|
label: "Refresh",
|
|
5177
6523
|
className: "btn-secondary",
|
|
@@ -5323,12 +6669,57 @@ function renderContentListPage(data) {
|
|
|
5323
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">
|
|
5324
6670
|
<div class="px-6 py-5">
|
|
5325
6671
|
<div class="flex items-center justify-between">
|
|
5326
|
-
<div class="flex items-center space-x-4">
|
|
5327
|
-
<!--
|
|
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 -->
|
|
5328
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">
|
|
5329
6720
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search</label>
|
|
5330
6721
|
<form onsubmit="performContentSearch(event)" class="flex items-center space-x-2">
|
|
5331
|
-
<div class="relative group">
|
|
6722
|
+
<div class="relative group flex-1">
|
|
5332
6723
|
<input
|
|
5333
6724
|
type="text"
|
|
5334
6725
|
name="search"
|
|
@@ -5336,7 +6727,7 @@ function renderContentListPage(data) {
|
|
|
5336
6727
|
value="${data.search || ""}"
|
|
5337
6728
|
oninput="toggleContentClearButton()"
|
|
5338
6729
|
placeholder="Search content..."
|
|
5339
|
-
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"
|
|
5340
6731
|
>
|
|
5341
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">
|
|
5342
6733
|
<svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
@@ -5408,57 +6799,6 @@ function renderContentListPage(data) {
|
|
|
5408
6799
|
}
|
|
5409
6800
|
</script>
|
|
5410
6801
|
</div>
|
|
5411
|
-
|
|
5412
|
-
${filterBarData.filters.map((filter) => {
|
|
5413
|
-
const selectedOption = filter.options.find((opt) => opt.selected);
|
|
5414
|
-
const selectedColor = selectedOption?.color || "cyan";
|
|
5415
|
-
const colorMap = {
|
|
5416
|
-
"cyan": "bg-cyan-400 dark:bg-cyan-400",
|
|
5417
|
-
"lime": "bg-lime-400 dark:bg-lime-400",
|
|
5418
|
-
"pink": "bg-pink-400 dark:bg-pink-400",
|
|
5419
|
-
"purple": "bg-purple-400 dark:bg-purple-400",
|
|
5420
|
-
"amber": "bg-amber-400 dark:bg-amber-400",
|
|
5421
|
-
"zinc": "bg-zinc-400 dark:bg-zinc-400"
|
|
5422
|
-
};
|
|
5423
|
-
return `
|
|
5424
|
-
<div>
|
|
5425
|
-
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white">${filter.label}</label>
|
|
5426
|
-
<div class="mt-2 grid grid-cols-1">
|
|
5427
|
-
<div class="col-start-1 row-start-1 flex items-center gap-3 pl-3 pr-8 pointer-events-none">
|
|
5428
|
-
${filter.name === "status" ? `<span class="inline-block size-2 shrink-0 rounded-full border border-transparent ${colorMap[selectedColor]}"></span>` : ""}
|
|
5429
|
-
</div>
|
|
5430
|
-
<select
|
|
5431
|
-
name="${filter.name}"
|
|
5432
|
-
onchange="updateContentFilters('${filter.name}', this.value)"
|
|
5433
|
-
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"
|
|
5434
|
-
>
|
|
5435
|
-
${filter.options.map((opt) => `
|
|
5436
|
-
<option value="${opt.value}" ${opt.selected ? "selected" : ""}>${opt.label}</option>
|
|
5437
|
-
`).join("")}
|
|
5438
|
-
</select>
|
|
5439
|
-
<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">
|
|
5440
|
-
<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" />
|
|
5441
|
-
</svg>
|
|
5442
|
-
</div>
|
|
5443
|
-
</div>
|
|
5444
|
-
`;
|
|
5445
|
-
}).join("")}
|
|
5446
|
-
|
|
5447
|
-
<!-- Clear Filters Button -->
|
|
5448
|
-
${hasActiveFilters ? `
|
|
5449
|
-
<div>
|
|
5450
|
-
<label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2"> </label>
|
|
5451
|
-
<button
|
|
5452
|
-
onclick="clearAllFilters()"
|
|
5453
|
-
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"
|
|
5454
|
-
>
|
|
5455
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
|
5456
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
|
5457
|
-
</svg>
|
|
5458
|
-
Clear Filters
|
|
5459
|
-
</button>
|
|
5460
|
-
</div>
|
|
5461
|
-
` : ""}
|
|
5462
6802
|
</div>
|
|
5463
6803
|
<div class="flex items-center gap-x-3">
|
|
5464
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>
|
|
@@ -5628,8 +6968,9 @@ function renderContentListPage(data) {
|
|
|
5628
6968
|
});
|
|
5629
6969
|
|
|
5630
6970
|
// Store current bulk action context
|
|
5631
|
-
let
|
|
5632
|
-
|
|
6971
|
+
// Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
|
|
6972
|
+
var currentBulkAction = null;
|
|
6973
|
+
var currentSelectedIds = [];
|
|
5633
6974
|
|
|
5634
6975
|
// Perform bulk action
|
|
5635
6976
|
function performBulkAction(action) {
|
|
@@ -5719,49 +7060,336 @@ function renderContentListPage(data) {
|
|
|
5719
7060
|
} else {
|
|
5720
7061
|
alert('Error: ' + (data.error || 'Unknown error'));
|
|
5721
7062
|
}
|
|
5722
|
-
})
|
|
5723
|
-
.catch(err => {
|
|
5724
|
-
console.error('Bulk action error:', err);
|
|
5725
|
-
alert('Failed to perform bulk action');
|
|
5726
|
-
})
|
|
5727
|
-
.finally(() => {
|
|
5728
|
-
// Clear context
|
|
5729
|
-
currentBulkAction = null;
|
|
5730
|
-
currentSelectedIds = [];
|
|
7063
|
+
})
|
|
7064
|
+
.catch(err => {
|
|
7065
|
+
console.error('Bulk action error:', err);
|
|
7066
|
+
alert('Failed to perform bulk action');
|
|
7067
|
+
})
|
|
7068
|
+
.finally(() => {
|
|
7069
|
+
// Clear context
|
|
7070
|
+
currentBulkAction = null;
|
|
7071
|
+
currentSelectedIds = [];
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
7074
|
+
|
|
7075
|
+
// Helper to get action text for display
|
|
7076
|
+
function getActionText(action) {
|
|
7077
|
+
const actionCount = currentSelectedIds.length;
|
|
7078
|
+
switch(action) {
|
|
7079
|
+
case 'publish':
|
|
7080
|
+
return \`publish \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7081
|
+
case 'draft':
|
|
7082
|
+
return \`move \${actionCount} item\${actionCount > 1 ? 's' : ''} to draft\`;
|
|
7083
|
+
case 'delete':
|
|
7084
|
+
return \`delete \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7085
|
+
default:
|
|
7086
|
+
return \`perform action on \${actionCount} item\${actionCount > 1 ? 's' : ''}\`;
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
|
|
7090
|
+
</script>
|
|
7091
|
+
|
|
7092
|
+
<!-- Confirmation Dialog for Bulk Actions -->
|
|
7093
|
+
${renderConfirmationDialog({
|
|
7094
|
+
id: "bulk-action-confirm",
|
|
7095
|
+
title: "Confirm Bulk Action",
|
|
7096
|
+
message: "Are you sure you want to perform this action? This operation will affect multiple items.",
|
|
7097
|
+
confirmText: "Confirm",
|
|
7098
|
+
cancelText: "Cancel",
|
|
7099
|
+
confirmClass: "bg-blue-500 hover:bg-blue-400",
|
|
7100
|
+
iconColor: "blue",
|
|
7101
|
+
onConfirm: "executeBulkAction()"
|
|
7102
|
+
})}
|
|
7103
|
+
|
|
7104
|
+
<!-- Confirmation Dialog Script -->
|
|
7105
|
+
${getConfirmationDialogScript()}
|
|
7106
|
+
|
|
7107
|
+
<!-- Advanced Search Modal -->
|
|
7108
|
+
<div id="advancedSearchModal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
7109
|
+
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
7110
|
+
<!-- Background overlay -->
|
|
7111
|
+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="closeAdvancedSearch()"></div>
|
|
7112
|
+
|
|
7113
|
+
<!-- Modal panel -->
|
|
7114
|
+
<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">
|
|
7115
|
+
<div class="bg-white dark:bg-zinc-900 px-4 pt-5 pb-4 sm:p-6">
|
|
7116
|
+
<!-- Header -->
|
|
7117
|
+
<div class="flex items-center justify-between mb-4">
|
|
7118
|
+
<h3 class="text-lg font-semibold text-zinc-950 dark:text-white" id="modal-title">
|
|
7119
|
+
\u{1F50D} Advanced Search
|
|
7120
|
+
</h3>
|
|
7121
|
+
<button onclick="closeAdvancedSearch()" class="text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300">
|
|
7122
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
7123
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
7124
|
+
</svg>
|
|
7125
|
+
</button>
|
|
7126
|
+
</div>
|
|
7127
|
+
|
|
7128
|
+
<!-- Search Form -->
|
|
7129
|
+
<form id="advancedSearchForm" class="space-y-4">
|
|
7130
|
+
<!-- Search Input -->
|
|
7131
|
+
<div>
|
|
7132
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Query</label>
|
|
7133
|
+
<div class="relative">
|
|
7134
|
+
<input
|
|
7135
|
+
type="text"
|
|
7136
|
+
id="searchQuery"
|
|
7137
|
+
name="query"
|
|
7138
|
+
placeholder="Enter your search query..."
|
|
7139
|
+
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"
|
|
7140
|
+
autocomplete="off"
|
|
7141
|
+
/>
|
|
7142
|
+
<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>
|
|
7143
|
+
</div>
|
|
7144
|
+
</div>
|
|
7145
|
+
|
|
7146
|
+
<!-- Mode Toggle -->
|
|
7147
|
+
<div>
|
|
7148
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Search Mode</label>
|
|
7149
|
+
<div class="flex gap-4">
|
|
7150
|
+
<label class="flex items-center">
|
|
7151
|
+
<input type="radio" name="mode" value="ai" checked class="mr-2">
|
|
7152
|
+
<span class="text-sm text-zinc-950 dark:text-white">\u{1F916} AI Search (Semantic)</span>
|
|
7153
|
+
</label>
|
|
7154
|
+
<label class="flex items-center">
|
|
7155
|
+
<input type="radio" name="mode" value="keyword" class="mr-2">
|
|
7156
|
+
<span class="text-sm text-zinc-950 dark:text-white">\u{1F524} Keyword Search</span>
|
|
7157
|
+
</label>
|
|
7158
|
+
</div>
|
|
7159
|
+
</div>
|
|
7160
|
+
|
|
7161
|
+
<!-- Filters -->
|
|
7162
|
+
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
|
7163
|
+
<h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-3">Filters</h4>
|
|
7164
|
+
|
|
7165
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
7166
|
+
<!-- Collection Filter -->
|
|
7167
|
+
<div>
|
|
7168
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Collections</label>
|
|
7169
|
+
<select
|
|
7170
|
+
id="filterCollections"
|
|
7171
|
+
name="collections"
|
|
7172
|
+
multiple
|
|
7173
|
+
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"
|
|
7174
|
+
size="4"
|
|
7175
|
+
>
|
|
7176
|
+
<option value="">All Collections</option>
|
|
7177
|
+
${data.models.map(
|
|
7178
|
+
(model) => `
|
|
7179
|
+
<option value="${model.name}">${model.displayName}</option>
|
|
7180
|
+
`
|
|
7181
|
+
).join("")}
|
|
7182
|
+
</select>
|
|
7183
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Hold Ctrl/Cmd to select multiple</p>
|
|
7184
|
+
</div>
|
|
7185
|
+
|
|
7186
|
+
<!-- Status Filter -->
|
|
7187
|
+
<div>
|
|
7188
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Status</label>
|
|
7189
|
+
<select
|
|
7190
|
+
id="filterStatus"
|
|
7191
|
+
name="status"
|
|
7192
|
+
multiple
|
|
7193
|
+
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"
|
|
7194
|
+
size="4"
|
|
7195
|
+
>
|
|
7196
|
+
<option value="published">Published</option>
|
|
7197
|
+
<option value="draft">Draft</option>
|
|
7198
|
+
<option value="review">Under Review</option>
|
|
7199
|
+
<option value="scheduled">Scheduled</option>
|
|
7200
|
+
<option value="archived">Archived</option>
|
|
7201
|
+
</select>
|
|
7202
|
+
</div>
|
|
7203
|
+
</div>
|
|
7204
|
+
</div>
|
|
7205
|
+
|
|
7206
|
+
<!-- Actions -->
|
|
7207
|
+
<div class="flex items-center justify-end gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
|
7208
|
+
<button
|
|
7209
|
+
type="button"
|
|
7210
|
+
onclick="closeAdvancedSearch()"
|
|
7211
|
+
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"
|
|
7212
|
+
>
|
|
7213
|
+
Cancel
|
|
7214
|
+
</button>
|
|
7215
|
+
<button
|
|
7216
|
+
type="submit"
|
|
7217
|
+
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"
|
|
7218
|
+
>
|
|
7219
|
+
Search
|
|
7220
|
+
</button>
|
|
7221
|
+
</div>
|
|
7222
|
+
</form>
|
|
7223
|
+
</div>
|
|
7224
|
+
|
|
7225
|
+
<!-- Results Area -->
|
|
7226
|
+
<div id="searchResults" class="hidden px-4 pb-4 sm:px-6">
|
|
7227
|
+
<div class="border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
|
7228
|
+
<div id="searchResultsContent" class="space-y-3"></div>
|
|
7229
|
+
<div id="searchResultsPagination" class="mt-4 flex items-center justify-between"></div>
|
|
7230
|
+
</div>
|
|
7231
|
+
</div>
|
|
7232
|
+
</div>
|
|
7233
|
+
</div>
|
|
7234
|
+
</div>
|
|
7235
|
+
|
|
7236
|
+
<script>
|
|
7237
|
+
// Open modal
|
|
7238
|
+
function openAdvancedSearch() {
|
|
7239
|
+
document.getElementById('advancedSearchModal').classList.remove('hidden');
|
|
7240
|
+
document.getElementById('searchQuery').focus();
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
// Close modal
|
|
7244
|
+
function closeAdvancedSearch() {
|
|
7245
|
+
document.getElementById('advancedSearchModal').classList.add('hidden');
|
|
7246
|
+
document.getElementById('searchResults').classList.add('hidden');
|
|
7247
|
+
}
|
|
7248
|
+
|
|
7249
|
+
// Autocomplete
|
|
7250
|
+
// Using var instead of let to avoid redeclaration errors when HTMX re-executes script tags
|
|
7251
|
+
var autocompleteTimeout;
|
|
7252
|
+
var searchQueryInput = document.getElementById('searchQuery');
|
|
7253
|
+
if (searchQueryInput) {
|
|
7254
|
+
searchQueryInput.addEventListener('input', (e) => {
|
|
7255
|
+
const query = e.target.value.trim();
|
|
7256
|
+
const suggestionsDiv = document.getElementById('searchSuggestions');
|
|
7257
|
+
|
|
7258
|
+
clearTimeout(autocompleteTimeout);
|
|
7259
|
+
|
|
7260
|
+
if (query.length < 2) {
|
|
7261
|
+
suggestionsDiv.classList.add('hidden');
|
|
7262
|
+
return;
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
autocompleteTimeout = setTimeout(async () => {
|
|
7266
|
+
try {
|
|
7267
|
+
const res = await fetch(\`/api/search/suggest?q=\${encodeURIComponent(query)}\`);
|
|
7268
|
+
const { data } = await res.json();
|
|
7269
|
+
|
|
7270
|
+
if (data && data.length > 0) {
|
|
7271
|
+
suggestionsDiv.innerHTML = data.map(s => \`
|
|
7272
|
+
<div class="px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer" onclick="selectSuggestion('\${s.replace(/'/g, "\\'")}')">\${s}</div>
|
|
7273
|
+
\`).join('');
|
|
7274
|
+
suggestionsDiv.classList.remove('hidden');
|
|
7275
|
+
} else {
|
|
7276
|
+
suggestionsDiv.classList.add('hidden');
|
|
7277
|
+
}
|
|
7278
|
+
} catch (error) {
|
|
7279
|
+
console.error('Autocomplete error:', error);
|
|
7280
|
+
}
|
|
7281
|
+
}, 300);
|
|
7282
|
+
});
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
function selectSuggestion(suggestion) {
|
|
7286
|
+
document.getElementById('searchQuery').value = suggestion;
|
|
7287
|
+
document.getElementById('searchSuggestions').classList.add('hidden');
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// Hide suggestions when clicking outside
|
|
7291
|
+
document.addEventListener('click', (e) => {
|
|
7292
|
+
const suggestionsDiv = document.getElementById('searchSuggestions');
|
|
7293
|
+
if (!e.target.closest('#searchQuery') && !e.target.closest('#searchSuggestions')) {
|
|
7294
|
+
suggestionsDiv.classList.add('hidden');
|
|
7295
|
+
}
|
|
7296
|
+
});
|
|
7297
|
+
|
|
7298
|
+
// Form submission
|
|
7299
|
+
var advancedSearchForm = document.getElementById('advancedSearchForm');
|
|
7300
|
+
if (advancedSearchForm) {
|
|
7301
|
+
advancedSearchForm.addEventListener('submit', async (e) => {
|
|
7302
|
+
e.preventDefault();
|
|
7303
|
+
|
|
7304
|
+
const formData = new FormData(e.target);
|
|
7305
|
+
const query = formData.get('query');
|
|
7306
|
+
const mode = formData.get('mode') || 'ai';
|
|
7307
|
+
|
|
7308
|
+
// Build filters
|
|
7309
|
+
const filters = {};
|
|
7310
|
+
|
|
7311
|
+
const collections = Array.from(formData.getAll('collections')).filter(c => c !== '');
|
|
7312
|
+
if (collections.length > 0) {
|
|
7313
|
+
// Need to convert collection names to IDs - for now, pass names
|
|
7314
|
+
filters.collections = collections;
|
|
7315
|
+
}
|
|
7316
|
+
|
|
7317
|
+
const status = Array.from(formData.getAll('status'));
|
|
7318
|
+
if (status.length > 0) {
|
|
7319
|
+
filters.status = status;
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
const dateStart = formData.get('date_start');
|
|
7323
|
+
const dateEnd = formData.get('date_end');
|
|
7324
|
+
if (dateStart || dateEnd) {
|
|
7325
|
+
filters.dateRange = {
|
|
7326
|
+
start: dateStart ? new Date(dateStart) : null,
|
|
7327
|
+
end: dateEnd ? new Date(dateEnd) : null,
|
|
7328
|
+
field: 'created_at'
|
|
7329
|
+
};
|
|
7330
|
+
}
|
|
7331
|
+
|
|
7332
|
+
// Execute search
|
|
7333
|
+
try {
|
|
7334
|
+
const res = await fetch('/api/search', {
|
|
7335
|
+
method: 'POST',
|
|
7336
|
+
headers: {'Content-Type': 'application/json'},
|
|
7337
|
+
body: JSON.stringify({
|
|
7338
|
+
query,
|
|
7339
|
+
mode,
|
|
7340
|
+
filters,
|
|
7341
|
+
limit: 20
|
|
7342
|
+
})
|
|
7343
|
+
});
|
|
7344
|
+
|
|
7345
|
+
const { data } = await res.json();
|
|
7346
|
+
|
|
7347
|
+
if (data && data.results) {
|
|
7348
|
+
displaySearchResults(data);
|
|
7349
|
+
}
|
|
7350
|
+
} catch (error) {
|
|
7351
|
+
console.error('Search error:', error);
|
|
7352
|
+
alert('Search failed. Please try again.');
|
|
7353
|
+
}
|
|
5731
7354
|
});
|
|
5732
7355
|
}
|
|
5733
7356
|
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
const
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
7357
|
+
function displaySearchResults(searchData) {
|
|
7358
|
+
const resultsDiv = document.getElementById('searchResultsContent');
|
|
7359
|
+
const resultsSection = document.getElementById('searchResults');
|
|
7360
|
+
|
|
7361
|
+
if (searchData.results.length === 0) {
|
|
7362
|
+
resultsDiv.innerHTML = '<p class="text-sm text-zinc-500 dark:text-zinc-400">No results found.</p>';
|
|
7363
|
+
} else {
|
|
7364
|
+
resultsDiv.innerHTML = searchData.results.map(result => \`
|
|
7365
|
+
<div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800">
|
|
7366
|
+
<div class="flex items-start justify-between">
|
|
7367
|
+
<div class="flex-1">
|
|
7368
|
+
<h4 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">
|
|
7369
|
+
<a href="/admin/content/\${result.id}/edit" class="hover:text-indigo-600 dark:hover:text-indigo-400">\${result.title || 'Untitled'}</a>
|
|
7370
|
+
</h4>
|
|
7371
|
+
<p class="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
|
|
7372
|
+
\${result.collection_name} \u2022 \${new Date(result.created_at).toLocaleDateString()}
|
|
7373
|
+
\${result.relevance_score ? \` \u2022 Relevance: \${(result.relevance_score * 100).toFixed(0)}%\` : ''}
|
|
7374
|
+
</p>
|
|
7375
|
+
\${result.snippet ? \`<p class="text-sm text-zinc-600 dark:text-zinc-400">\${result.snippet}</p>\` : ''}
|
|
7376
|
+
</div>
|
|
7377
|
+
<div class="ml-4">
|
|
7378
|
+
<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>
|
|
7379
|
+
</div>
|
|
7380
|
+
</div>
|
|
7381
|
+
</div>
|
|
7382
|
+
\`).join('');
|
|
5746
7383
|
}
|
|
7384
|
+
|
|
7385
|
+
resultsSection.classList.remove('hidden');
|
|
7386
|
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
5747
7387
|
}
|
|
5748
7388
|
|
|
7389
|
+
// Make functions globally available
|
|
7390
|
+
window.openAdvancedSearch = openAdvancedSearch;
|
|
7391
|
+
window.closeAdvancedSearch = closeAdvancedSearch;
|
|
5749
7392
|
</script>
|
|
5750
|
-
|
|
5751
|
-
<!-- Confirmation Dialog for Bulk Actions -->
|
|
5752
|
-
${renderConfirmationDialog({
|
|
5753
|
-
id: "bulk-action-confirm",
|
|
5754
|
-
title: "Confirm Bulk Action",
|
|
5755
|
-
message: "Are you sure you want to perform this action? This operation will affect multiple items.",
|
|
5756
|
-
confirmText: "Confirm",
|
|
5757
|
-
cancelText: "Cancel",
|
|
5758
|
-
confirmClass: "bg-blue-500 hover:bg-blue-400",
|
|
5759
|
-
iconColor: "blue",
|
|
5760
|
-
onConfirm: "executeBulkAction()"
|
|
5761
|
-
})}
|
|
5762
|
-
|
|
5763
|
-
<!-- Confirmation Dialog Script -->
|
|
5764
|
-
${getConfirmationDialogScript()}
|
|
5765
7393
|
`;
|
|
5766
7394
|
const layoutData = {
|
|
5767
7395
|
title: "Content Management",
|
|
@@ -5965,6 +7593,122 @@ async function isPluginActive2(db, pluginId) {
|
|
|
5965
7593
|
|
|
5966
7594
|
// src/routes/admin-content.ts
|
|
5967
7595
|
var adminContentRoutes = new Hono();
|
|
7596
|
+
function parseFieldValue(field, formData, options = {}) {
|
|
7597
|
+
const { skipValidation = false } = options;
|
|
7598
|
+
const value = formData.get(field.field_name);
|
|
7599
|
+
const errors = [];
|
|
7600
|
+
const blocksConfig = getBlocksFieldConfig(field.field_options);
|
|
7601
|
+
if (blocksConfig) {
|
|
7602
|
+
const parsed = parseBlocksValue(value, blocksConfig);
|
|
7603
|
+
if (!skipValidation && field.is_required && parsed.value.length === 0) {
|
|
7604
|
+
parsed.errors.push(`${field.field_label} is required`);
|
|
7605
|
+
}
|
|
7606
|
+
return { value: parsed.value, errors: parsed.errors };
|
|
7607
|
+
}
|
|
7608
|
+
if (!skipValidation && field.is_required && (!value || value.toString().trim() === "")) {
|
|
7609
|
+
return { value: null, errors: [`${field.field_label} is required`] };
|
|
7610
|
+
}
|
|
7611
|
+
switch (field.field_type) {
|
|
7612
|
+
case "number":
|
|
7613
|
+
if (value && isNaN(Number(value))) {
|
|
7614
|
+
if (!skipValidation) {
|
|
7615
|
+
errors.push(`${field.field_label} must be a valid number`);
|
|
7616
|
+
}
|
|
7617
|
+
return { value: null, errors };
|
|
7618
|
+
}
|
|
7619
|
+
return { value: value ? Number(value) : null, errors: [] };
|
|
7620
|
+
case "boolean":
|
|
7621
|
+
const submitted = formData.get(`${field.field_name}_submitted`);
|
|
7622
|
+
return { value: submitted ? value === "true" : false, errors: [] };
|
|
7623
|
+
case "select":
|
|
7624
|
+
if (field.field_options?.multiple) {
|
|
7625
|
+
return { value: formData.getAll(`${field.field_name}[]`), errors: [] };
|
|
7626
|
+
}
|
|
7627
|
+
return { value, errors: [] };
|
|
7628
|
+
case "array": {
|
|
7629
|
+
if (!value || value.toString().trim() === "") {
|
|
7630
|
+
if (!skipValidation && field.is_required) {
|
|
7631
|
+
errors.push(`${field.field_label} is required`);
|
|
7632
|
+
}
|
|
7633
|
+
return { value: [], errors };
|
|
7634
|
+
}
|
|
7635
|
+
try {
|
|
7636
|
+
const parsed = JSON.parse(value.toString());
|
|
7637
|
+
if (!Array.isArray(parsed)) {
|
|
7638
|
+
if (!skipValidation) {
|
|
7639
|
+
errors.push(`${field.field_label} must be a JSON array`);
|
|
7640
|
+
}
|
|
7641
|
+
return { value: [], errors };
|
|
7642
|
+
}
|
|
7643
|
+
if (!skipValidation && field.is_required && parsed.length === 0) {
|
|
7644
|
+
errors.push(`${field.field_label} is required`);
|
|
7645
|
+
}
|
|
7646
|
+
return { value: parsed, errors };
|
|
7647
|
+
} catch {
|
|
7648
|
+
if (!skipValidation) {
|
|
7649
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7650
|
+
}
|
|
7651
|
+
return { value: [], errors };
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
case "object": {
|
|
7655
|
+
if (!value || value.toString().trim() === "") {
|
|
7656
|
+
if (!skipValidation && field.is_required) {
|
|
7657
|
+
errors.push(`${field.field_label} is required`);
|
|
7658
|
+
}
|
|
7659
|
+
return { value: {}, errors };
|
|
7660
|
+
}
|
|
7661
|
+
try {
|
|
7662
|
+
const parsed = JSON.parse(value.toString());
|
|
7663
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7664
|
+
if (!skipValidation) {
|
|
7665
|
+
errors.push(`${field.field_label} must be a JSON object`);
|
|
7666
|
+
}
|
|
7667
|
+
return { value: {}, errors };
|
|
7668
|
+
}
|
|
7669
|
+
if (!skipValidation && field.is_required && Object.keys(parsed).length === 0) {
|
|
7670
|
+
errors.push(`${field.field_label} is required`);
|
|
7671
|
+
}
|
|
7672
|
+
return { value: parsed, errors };
|
|
7673
|
+
} catch {
|
|
7674
|
+
if (!skipValidation) {
|
|
7675
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7676
|
+
}
|
|
7677
|
+
return { value: {}, errors };
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
case "json": {
|
|
7681
|
+
if (!value || value.toString().trim() === "") {
|
|
7682
|
+
if (!skipValidation && field.is_required) {
|
|
7683
|
+
errors.push(`${field.field_label} is required`);
|
|
7684
|
+
}
|
|
7685
|
+
return { value: null, errors };
|
|
7686
|
+
}
|
|
7687
|
+
try {
|
|
7688
|
+
return { value: JSON.parse(value.toString()), errors: [] };
|
|
7689
|
+
} catch {
|
|
7690
|
+
if (!skipValidation) {
|
|
7691
|
+
errors.push(`${field.field_label} must be valid JSON`);
|
|
7692
|
+
}
|
|
7693
|
+
return { value: null, errors };
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
default:
|
|
7697
|
+
return { value, errors: [] };
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
7700
|
+
function extractFieldData(fields, formData, options = {}) {
|
|
7701
|
+
const data = {};
|
|
7702
|
+
const errors = {};
|
|
7703
|
+
for (const field of fields) {
|
|
7704
|
+
const result = parseFieldValue(field, formData, options);
|
|
7705
|
+
data[field.field_name] = result.value;
|
|
7706
|
+
if (result.errors.length > 0) {
|
|
7707
|
+
errors[field.field_name] = result.errors;
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
return { data, errors };
|
|
7711
|
+
}
|
|
5968
7712
|
adminContentRoutes.use("*", requireAuth());
|
|
5969
7713
|
async function getCollectionFields(db, collectionId) {
|
|
5970
7714
|
const cache = getCacheService(CACHE_CONFIGS.collection);
|
|
@@ -6438,109 +8182,7 @@ adminContentRoutes.post("/", async (c) => {
|
|
|
6438
8182
|
`);
|
|
6439
8183
|
}
|
|
6440
8184
|
const fields = await getCollectionFields(db, collectionId);
|
|
6441
|
-
const data =
|
|
6442
|
-
const errors = {};
|
|
6443
|
-
for (const field of fields) {
|
|
6444
|
-
const value = formData.get(field.field_name);
|
|
6445
|
-
const blocksConfig = getBlocksFieldConfig(field.field_options);
|
|
6446
|
-
if (blocksConfig) {
|
|
6447
|
-
const parsed = parseBlocksValue(value, blocksConfig);
|
|
6448
|
-
if (field.is_required && parsed.value.length === 0) {
|
|
6449
|
-
parsed.errors.push(`${field.field_label} is required`);
|
|
6450
|
-
}
|
|
6451
|
-
if (parsed.errors.length > 0) {
|
|
6452
|
-
errors[field.field_name] = parsed.errors;
|
|
6453
|
-
}
|
|
6454
|
-
data[field.field_name] = parsed.value;
|
|
6455
|
-
continue;
|
|
6456
|
-
}
|
|
6457
|
-
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
6458
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6459
|
-
continue;
|
|
6460
|
-
}
|
|
6461
|
-
switch (field.field_type) {
|
|
6462
|
-
case "number":
|
|
6463
|
-
if (value && isNaN(Number(value))) {
|
|
6464
|
-
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
6465
|
-
} else {
|
|
6466
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6467
|
-
}
|
|
6468
|
-
break;
|
|
6469
|
-
case "boolean":
|
|
6470
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
6471
|
-
break;
|
|
6472
|
-
case "select":
|
|
6473
|
-
if (field.field_options?.multiple) {
|
|
6474
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6475
|
-
} else {
|
|
6476
|
-
data[field.field_name] = value;
|
|
6477
|
-
}
|
|
6478
|
-
break;
|
|
6479
|
-
case "array": {
|
|
6480
|
-
if (!value || value.toString().trim() === "") {
|
|
6481
|
-
data[field.field_name] = [];
|
|
6482
|
-
if (field.is_required) {
|
|
6483
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6484
|
-
}
|
|
6485
|
-
break;
|
|
6486
|
-
}
|
|
6487
|
-
try {
|
|
6488
|
-
const parsed = JSON.parse(value.toString());
|
|
6489
|
-
if (!Array.isArray(parsed)) {
|
|
6490
|
-
errors[field.field_name] = [`${field.field_label} must be a JSON array`];
|
|
6491
|
-
} else {
|
|
6492
|
-
if (field.is_required && parsed.length === 0) {
|
|
6493
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6494
|
-
}
|
|
6495
|
-
data[field.field_name] = parsed;
|
|
6496
|
-
}
|
|
6497
|
-
} catch {
|
|
6498
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6499
|
-
}
|
|
6500
|
-
break;
|
|
6501
|
-
}
|
|
6502
|
-
case "object": {
|
|
6503
|
-
if (!value || value.toString().trim() === "") {
|
|
6504
|
-
data[field.field_name] = {};
|
|
6505
|
-
if (field.is_required) {
|
|
6506
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6507
|
-
}
|
|
6508
|
-
break;
|
|
6509
|
-
}
|
|
6510
|
-
try {
|
|
6511
|
-
const parsed = JSON.parse(value.toString());
|
|
6512
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6513
|
-
errors[field.field_name] = [`${field.field_label} must be a JSON object`];
|
|
6514
|
-
} else {
|
|
6515
|
-
if (field.is_required && Object.keys(parsed).length === 0) {
|
|
6516
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6517
|
-
}
|
|
6518
|
-
data[field.field_name] = parsed;
|
|
6519
|
-
}
|
|
6520
|
-
} catch {
|
|
6521
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6522
|
-
}
|
|
6523
|
-
break;
|
|
6524
|
-
}
|
|
6525
|
-
case "json": {
|
|
6526
|
-
if (!value || value.toString().trim() === "") {
|
|
6527
|
-
data[field.field_name] = null;
|
|
6528
|
-
if (field.is_required) {
|
|
6529
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6530
|
-
}
|
|
6531
|
-
break;
|
|
6532
|
-
}
|
|
6533
|
-
try {
|
|
6534
|
-
data[field.field_name] = JSON.parse(value.toString());
|
|
6535
|
-
} catch {
|
|
6536
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6537
|
-
}
|
|
6538
|
-
break;
|
|
6539
|
-
}
|
|
6540
|
-
default:
|
|
6541
|
-
data[field.field_name] = value;
|
|
6542
|
-
}
|
|
6543
|
-
}
|
|
8185
|
+
const { data, errors } = extractFieldData(fields, formData);
|
|
6544
8186
|
if (Object.keys(errors).length > 0) {
|
|
6545
8187
|
const formDataWithErrors = {
|
|
6546
8188
|
collection,
|
|
@@ -6657,109 +8299,7 @@ adminContentRoutes.put("/:id", async (c) => {
|
|
|
6657
8299
|
`);
|
|
6658
8300
|
}
|
|
6659
8301
|
const fields = await getCollectionFields(db, existingContent.collection_id);
|
|
6660
|
-
const data =
|
|
6661
|
-
const errors = {};
|
|
6662
|
-
for (const field of fields) {
|
|
6663
|
-
const value = formData.get(field.field_name);
|
|
6664
|
-
const blocksConfig = getBlocksFieldConfig(field.field_options);
|
|
6665
|
-
if (blocksConfig) {
|
|
6666
|
-
const parsed = parseBlocksValue(value, blocksConfig);
|
|
6667
|
-
if (field.is_required && parsed.value.length === 0) {
|
|
6668
|
-
parsed.errors.push(`${field.field_label} is required`);
|
|
6669
|
-
}
|
|
6670
|
-
if (parsed.errors.length > 0) {
|
|
6671
|
-
errors[field.field_name] = parsed.errors;
|
|
6672
|
-
}
|
|
6673
|
-
data[field.field_name] = parsed.value;
|
|
6674
|
-
continue;
|
|
6675
|
-
}
|
|
6676
|
-
if (field.is_required && (!value || value.toString().trim() === "")) {
|
|
6677
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6678
|
-
continue;
|
|
6679
|
-
}
|
|
6680
|
-
switch (field.field_type) {
|
|
6681
|
-
case "number":
|
|
6682
|
-
if (value && isNaN(Number(value))) {
|
|
6683
|
-
errors[field.field_name] = [`${field.field_label} must be a valid number`];
|
|
6684
|
-
} else {
|
|
6685
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6686
|
-
}
|
|
6687
|
-
break;
|
|
6688
|
-
case "boolean":
|
|
6689
|
-
data[field.field_name] = formData.get(`${field.field_name}_submitted`) ? value === "true" : false;
|
|
6690
|
-
break;
|
|
6691
|
-
case "select":
|
|
6692
|
-
if (field.field_options?.multiple) {
|
|
6693
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6694
|
-
} else {
|
|
6695
|
-
data[field.field_name] = value;
|
|
6696
|
-
}
|
|
6697
|
-
break;
|
|
6698
|
-
case "array": {
|
|
6699
|
-
if (!value || value.toString().trim() === "") {
|
|
6700
|
-
data[field.field_name] = [];
|
|
6701
|
-
if (field.is_required) {
|
|
6702
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6703
|
-
}
|
|
6704
|
-
break;
|
|
6705
|
-
}
|
|
6706
|
-
try {
|
|
6707
|
-
const parsed = JSON.parse(value.toString());
|
|
6708
|
-
if (!Array.isArray(parsed)) {
|
|
6709
|
-
errors[field.field_name] = [`${field.field_label} must be a JSON array`];
|
|
6710
|
-
} else {
|
|
6711
|
-
if (field.is_required && parsed.length === 0) {
|
|
6712
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6713
|
-
}
|
|
6714
|
-
data[field.field_name] = parsed;
|
|
6715
|
-
}
|
|
6716
|
-
} catch {
|
|
6717
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6718
|
-
}
|
|
6719
|
-
break;
|
|
6720
|
-
}
|
|
6721
|
-
case "object": {
|
|
6722
|
-
if (!value || value.toString().trim() === "") {
|
|
6723
|
-
data[field.field_name] = {};
|
|
6724
|
-
if (field.is_required) {
|
|
6725
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6726
|
-
}
|
|
6727
|
-
break;
|
|
6728
|
-
}
|
|
6729
|
-
try {
|
|
6730
|
-
const parsed = JSON.parse(value.toString());
|
|
6731
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6732
|
-
errors[field.field_name] = [`${field.field_label} must be a JSON object`];
|
|
6733
|
-
} else {
|
|
6734
|
-
if (field.is_required && Object.keys(parsed).length === 0) {
|
|
6735
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6736
|
-
}
|
|
6737
|
-
data[field.field_name] = parsed;
|
|
6738
|
-
}
|
|
6739
|
-
} catch {
|
|
6740
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6741
|
-
}
|
|
6742
|
-
break;
|
|
6743
|
-
}
|
|
6744
|
-
case "json": {
|
|
6745
|
-
if (!value || value.toString().trim() === "") {
|
|
6746
|
-
data[field.field_name] = null;
|
|
6747
|
-
if (field.is_required) {
|
|
6748
|
-
errors[field.field_name] = [`${field.field_label} is required`];
|
|
6749
|
-
}
|
|
6750
|
-
break;
|
|
6751
|
-
}
|
|
6752
|
-
try {
|
|
6753
|
-
data[field.field_name] = JSON.parse(value.toString());
|
|
6754
|
-
} catch {
|
|
6755
|
-
errors[field.field_name] = [`${field.field_label} must be valid JSON`];
|
|
6756
|
-
}
|
|
6757
|
-
break;
|
|
6758
|
-
}
|
|
6759
|
-
default:
|
|
6760
|
-
data[field.field_name] = value;
|
|
6761
|
-
}
|
|
6762
|
-
}
|
|
8302
|
+
const { data, errors } = extractFieldData(fields, formData);
|
|
6763
8303
|
if (Object.keys(errors).length > 0) {
|
|
6764
8304
|
const formDataWithErrors = {
|
|
6765
8305
|
id,
|
|
@@ -6872,33 +8412,7 @@ adminContentRoutes.post("/preview", async (c) => {
|
|
|
6872
8412
|
return c.html("<p>Collection not found</p>");
|
|
6873
8413
|
}
|
|
6874
8414
|
const fields = await getCollectionFields(db, collectionId);
|
|
6875
|
-
const data = {};
|
|
6876
|
-
for (const field of fields) {
|
|
6877
|
-
const value = formData.get(field.field_name);
|
|
6878
|
-
const blocksConfig = getBlocksFieldConfig(field.field_options);
|
|
6879
|
-
if (blocksConfig) {
|
|
6880
|
-
const parsed = parseBlocksValue(value, blocksConfig);
|
|
6881
|
-
data[field.field_name] = parsed.value;
|
|
6882
|
-
continue;
|
|
6883
|
-
}
|
|
6884
|
-
switch (field.field_type) {
|
|
6885
|
-
case "number":
|
|
6886
|
-
data[field.field_name] = value ? Number(value) : null;
|
|
6887
|
-
break;
|
|
6888
|
-
case "boolean":
|
|
6889
|
-
data[field.field_name] = value === "true";
|
|
6890
|
-
break;
|
|
6891
|
-
case "select":
|
|
6892
|
-
if (field.field_options?.multiple) {
|
|
6893
|
-
data[field.field_name] = formData.getAll(`${field.field_name}[]`);
|
|
6894
|
-
} else {
|
|
6895
|
-
data[field.field_name] = value;
|
|
6896
|
-
}
|
|
6897
|
-
break;
|
|
6898
|
-
default:
|
|
6899
|
-
data[field.field_name] = value;
|
|
6900
|
-
}
|
|
6901
|
-
}
|
|
8415
|
+
const { data } = extractFieldData(fields, formData, { skipValidation: true });
|
|
6902
8416
|
const previewHTML = `
|
|
6903
8417
|
<!DOCTYPE html>
|
|
6904
8418
|
<html lang="en">
|
|
@@ -8245,14 +9759,87 @@ function renderUserEditPage(data) {
|
|
|
8245
9759
|
</div>
|
|
8246
9760
|
</div>
|
|
8247
9761
|
</div>
|
|
9762
|
+
</div>
|
|
9763
|
+
|
|
9764
|
+
<!-- Profile Information -->
|
|
9765
|
+
<div class="mb-8">
|
|
9766
|
+
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Profile Information</h3>
|
|
9767
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4">Extended profile data for this user</p>
|
|
9768
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
9769
|
+
<div>
|
|
9770
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Display Name</label>
|
|
9771
|
+
<input
|
|
9772
|
+
type="text"
|
|
9773
|
+
name="profile_display_name"
|
|
9774
|
+
value="${escapeHtml(data.userToEdit.profile?.displayName || "")}"
|
|
9775
|
+
placeholder="Public display name"
|
|
9776
|
+
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"
|
|
9777
|
+
/>
|
|
9778
|
+
</div>
|
|
9779
|
+
|
|
9780
|
+
<div>
|
|
9781
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Company</label>
|
|
9782
|
+
<input
|
|
9783
|
+
type="text"
|
|
9784
|
+
name="profile_company"
|
|
9785
|
+
value="${escapeHtml(data.userToEdit.profile?.company || "")}"
|
|
9786
|
+
placeholder="Company or organization"
|
|
9787
|
+
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"
|
|
9788
|
+
/>
|
|
9789
|
+
</div>
|
|
9790
|
+
|
|
9791
|
+
<div>
|
|
9792
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Job Title</label>
|
|
9793
|
+
<input
|
|
9794
|
+
type="text"
|
|
9795
|
+
name="profile_job_title"
|
|
9796
|
+
value="${escapeHtml(data.userToEdit.profile?.jobTitle || "")}"
|
|
9797
|
+
placeholder="Job title or role"
|
|
9798
|
+
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"
|
|
9799
|
+
/>
|
|
9800
|
+
</div>
|
|
9801
|
+
|
|
9802
|
+
<div>
|
|
9803
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Website</label>
|
|
9804
|
+
<input
|
|
9805
|
+
type="url"
|
|
9806
|
+
name="profile_website"
|
|
9807
|
+
value="${escapeHtml(data.userToEdit.profile?.website || "")}"
|
|
9808
|
+
placeholder="https://example.com"
|
|
9809
|
+
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"
|
|
9810
|
+
/>
|
|
9811
|
+
</div>
|
|
9812
|
+
|
|
9813
|
+
<div>
|
|
9814
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Location</label>
|
|
9815
|
+
<input
|
|
9816
|
+
type="text"
|
|
9817
|
+
name="profile_location"
|
|
9818
|
+
value="${escapeHtml(data.userToEdit.profile?.location || "")}"
|
|
9819
|
+
placeholder="City, Country"
|
|
9820
|
+
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"
|
|
9821
|
+
/>
|
|
9822
|
+
</div>
|
|
9823
|
+
|
|
9824
|
+
<div>
|
|
9825
|
+
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Date of Birth</label>
|
|
9826
|
+
<input
|
|
9827
|
+
type="date"
|
|
9828
|
+
name="profile_date_of_birth"
|
|
9829
|
+
value="${data.userToEdit.profile?.dateOfBirth ? new Date(data.userToEdit.profile.dateOfBirth).toISOString().split("T")[0] : ""}"
|
|
9830
|
+
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"
|
|
9831
|
+
/>
|
|
9832
|
+
</div>
|
|
9833
|
+
</div>
|
|
8248
9834
|
|
|
8249
9835
|
<div class="mt-6">
|
|
8250
9836
|
<label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Bio</label>
|
|
8251
9837
|
<textarea
|
|
8252
|
-
name="
|
|
9838
|
+
name="profile_bio"
|
|
8253
9839
|
rows="3"
|
|
9840
|
+
placeholder="Short bio or description"
|
|
8254
9841
|
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"
|
|
8255
|
-
>${escapeHtml(data.userToEdit.bio || "")}</textarea>
|
|
9842
|
+
>${escapeHtml(data.userToEdit.profile?.bio || "")}</textarea>
|
|
8256
9843
|
</div>
|
|
8257
9844
|
</div>
|
|
8258
9845
|
|
|
@@ -9786,7 +11373,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9786
11373
|
const userId = c.req.param("id");
|
|
9787
11374
|
try {
|
|
9788
11375
|
const userStmt = db.prepare(`
|
|
9789
|
-
SELECT id, email, username, first_name, last_name, phone,
|
|
11376
|
+
SELECT id, email, username, first_name, last_name, phone, avatar_url,
|
|
9790
11377
|
role, is_active, email_verified, two_factor_enabled, created_at, last_login_at
|
|
9791
11378
|
FROM users
|
|
9792
11379
|
WHERE id = ?
|
|
@@ -9799,6 +11386,21 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9799
11386
|
dismissible: true
|
|
9800
11387
|
}), 404);
|
|
9801
11388
|
}
|
|
11389
|
+
const profileStmt = db.prepare(`
|
|
11390
|
+
SELECT display_name, bio, company, job_title, website, location, date_of_birth
|
|
11391
|
+
FROM user_profiles
|
|
11392
|
+
WHERE user_id = ?
|
|
11393
|
+
`);
|
|
11394
|
+
const profileData = await profileStmt.bind(userId).first();
|
|
11395
|
+
const profile = profileData ? {
|
|
11396
|
+
displayName: profileData.display_name,
|
|
11397
|
+
bio: profileData.bio,
|
|
11398
|
+
company: profileData.company,
|
|
11399
|
+
jobTitle: profileData.job_title,
|
|
11400
|
+
website: profileData.website,
|
|
11401
|
+
location: profileData.location,
|
|
11402
|
+
dateOfBirth: profileData.date_of_birth
|
|
11403
|
+
} : void 0;
|
|
9802
11404
|
const editData = {
|
|
9803
11405
|
id: userToEdit.id,
|
|
9804
11406
|
email: userToEdit.email,
|
|
@@ -9806,14 +11408,14 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9806
11408
|
firstName: userToEdit.first_name || "",
|
|
9807
11409
|
lastName: userToEdit.last_name || "",
|
|
9808
11410
|
phone: userToEdit.phone,
|
|
9809
|
-
bio: userToEdit.bio,
|
|
9810
11411
|
avatarUrl: userToEdit.avatar_url,
|
|
9811
11412
|
role: userToEdit.role,
|
|
9812
11413
|
isActive: Boolean(userToEdit.is_active),
|
|
9813
11414
|
emailVerified: Boolean(userToEdit.email_verified),
|
|
9814
11415
|
twoFactorEnabled: Boolean(userToEdit.two_factor_enabled),
|
|
9815
11416
|
createdAt: userToEdit.created_at,
|
|
9816
|
-
lastLoginAt: userToEdit.last_login_at
|
|
11417
|
+
lastLoginAt: userToEdit.last_login_at,
|
|
11418
|
+
profile
|
|
9817
11419
|
};
|
|
9818
11420
|
const pageData = {
|
|
9819
11421
|
userToEdit: editData,
|
|
@@ -9829,7 +11431,7 @@ userRoutes.get("/users/:id/edit", async (c) => {
|
|
|
9829
11431
|
console.error("User edit page error:", error);
|
|
9830
11432
|
return c.html(renderAlert2({
|
|
9831
11433
|
type: "error",
|
|
9832
|
-
message: "Failed to load user
|
|
11434
|
+
message: "Failed to load user. Please try again.",
|
|
9833
11435
|
dismissible: true
|
|
9834
11436
|
}), 500);
|
|
9835
11437
|
}
|
|
@@ -9845,10 +11447,17 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9845
11447
|
const username = sanitizeInput(formData.get("username")?.toString());
|
|
9846
11448
|
const email = formData.get("email")?.toString()?.trim().toLowerCase() || "";
|
|
9847
11449
|
const phone = sanitizeInput(formData.get("phone")?.toString()) || null;
|
|
9848
|
-
const bio = sanitizeInput(formData.get("bio")?.toString()) || null;
|
|
9849
11450
|
const role = formData.get("role")?.toString() || "viewer";
|
|
9850
11451
|
const isActive = formData.get("is_active") === "1";
|
|
9851
11452
|
const emailVerified = formData.get("email_verified") === "1";
|
|
11453
|
+
const profileDisplayName = sanitizeInput(formData.get("profile_display_name")?.toString()) || null;
|
|
11454
|
+
const profileBio = sanitizeInput(formData.get("profile_bio")?.toString()) || null;
|
|
11455
|
+
const profileCompany = sanitizeInput(formData.get("profile_company")?.toString()) || null;
|
|
11456
|
+
const profileJobTitle = sanitizeInput(formData.get("profile_job_title")?.toString()) || null;
|
|
11457
|
+
const profileWebsite = formData.get("profile_website")?.toString()?.trim() || null;
|
|
11458
|
+
const profileLocation = sanitizeInput(formData.get("profile_location")?.toString()) || null;
|
|
11459
|
+
const profileDateOfBirthStr = formData.get("profile_date_of_birth")?.toString()?.trim() || null;
|
|
11460
|
+
const profileDateOfBirth = profileDateOfBirthStr ? new Date(profileDateOfBirthStr).getTime() : null;
|
|
9852
11461
|
if (!firstName || !lastName || !username || !email) {
|
|
9853
11462
|
return c.html(renderAlert2({
|
|
9854
11463
|
type: "error",
|
|
@@ -9864,6 +11473,17 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9864
11473
|
dismissible: true
|
|
9865
11474
|
}));
|
|
9866
11475
|
}
|
|
11476
|
+
if (profileWebsite) {
|
|
11477
|
+
try {
|
|
11478
|
+
new URL(profileWebsite);
|
|
11479
|
+
} catch {
|
|
11480
|
+
return c.html(renderAlert2({
|
|
11481
|
+
type: "error",
|
|
11482
|
+
message: "Please enter a valid website URL.",
|
|
11483
|
+
dismissible: true
|
|
11484
|
+
}));
|
|
11485
|
+
}
|
|
11486
|
+
}
|
|
9867
11487
|
const checkStmt = db.prepare(`
|
|
9868
11488
|
SELECT id FROM users
|
|
9869
11489
|
WHERE (username = ? OR email = ?) AND id != ?
|
|
@@ -9872,14 +11492,14 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9872
11492
|
if (existingUser) {
|
|
9873
11493
|
return c.html(renderAlert2({
|
|
9874
11494
|
type: "error",
|
|
9875
|
-
message: "Username or email is already taken by another user
|
|
11495
|
+
message: "Username or email is already taken by another user.",
|
|
9876
11496
|
dismissible: true
|
|
9877
11497
|
}));
|
|
9878
11498
|
}
|
|
9879
11499
|
const updateStmt = db.prepare(`
|
|
9880
11500
|
UPDATE users SET
|
|
9881
11501
|
first_name = ?, last_name = ?, username = ?, email = ?,
|
|
9882
|
-
phone = ?,
|
|
11502
|
+
phone = ?, role = ?, is_active = ?, email_verified = ?,
|
|
9883
11503
|
updated_at = ?
|
|
9884
11504
|
WHERE id = ?
|
|
9885
11505
|
`);
|
|
@@ -9889,20 +11509,63 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9889
11509
|
username,
|
|
9890
11510
|
email,
|
|
9891
11511
|
phone,
|
|
9892
|
-
bio,
|
|
9893
11512
|
role,
|
|
9894
11513
|
isActive ? 1 : 0,
|
|
9895
11514
|
emailVerified ? 1 : 0,
|
|
9896
11515
|
Date.now(),
|
|
9897
11516
|
userId
|
|
9898
11517
|
).run();
|
|
11518
|
+
const hasProfileData = profileDisplayName || profileBio || profileCompany || profileJobTitle || profileWebsite || profileLocation || profileDateOfBirth;
|
|
11519
|
+
if (hasProfileData) {
|
|
11520
|
+
const now = Date.now();
|
|
11521
|
+
const profileCheckStmt = db.prepare(`SELECT id FROM user_profiles WHERE user_id = ?`);
|
|
11522
|
+
const existingProfile = await profileCheckStmt.bind(userId).first();
|
|
11523
|
+
if (existingProfile) {
|
|
11524
|
+
const updateProfileStmt = db.prepare(`
|
|
11525
|
+
UPDATE user_profiles SET
|
|
11526
|
+
display_name = ?, bio = ?, company = ?, job_title = ?,
|
|
11527
|
+
website = ?, location = ?, date_of_birth = ?, updated_at = ?
|
|
11528
|
+
WHERE user_id = ?
|
|
11529
|
+
`);
|
|
11530
|
+
await updateProfileStmt.bind(
|
|
11531
|
+
profileDisplayName,
|
|
11532
|
+
profileBio,
|
|
11533
|
+
profileCompany,
|
|
11534
|
+
profileJobTitle,
|
|
11535
|
+
profileWebsite,
|
|
11536
|
+
profileLocation,
|
|
11537
|
+
profileDateOfBirth,
|
|
11538
|
+
now,
|
|
11539
|
+
userId
|
|
11540
|
+
).run();
|
|
11541
|
+
} else {
|
|
11542
|
+
const profileId = `profile_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
11543
|
+
const insertProfileStmt = db.prepare(`
|
|
11544
|
+
INSERT INTO user_profiles (id, user_id, display_name, bio, company, job_title, website, location, date_of_birth, created_at, updated_at)
|
|
11545
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11546
|
+
`);
|
|
11547
|
+
await insertProfileStmt.bind(
|
|
11548
|
+
profileId,
|
|
11549
|
+
userId,
|
|
11550
|
+
profileDisplayName,
|
|
11551
|
+
profileBio,
|
|
11552
|
+
profileCompany,
|
|
11553
|
+
profileJobTitle,
|
|
11554
|
+
profileWebsite,
|
|
11555
|
+
profileLocation,
|
|
11556
|
+
profileDateOfBirth,
|
|
11557
|
+
now,
|
|
11558
|
+
now
|
|
11559
|
+
).run();
|
|
11560
|
+
}
|
|
11561
|
+
}
|
|
9899
11562
|
await logActivity(
|
|
9900
11563
|
db,
|
|
9901
11564
|
user.userId,
|
|
9902
|
-
"user
|
|
11565
|
+
"user.update",
|
|
9903
11566
|
"users",
|
|
9904
11567
|
userId,
|
|
9905
|
-
{ fields: ["first_name", "last_name", "username", "email", "phone", "
|
|
11568
|
+
{ fields: ["first_name", "last_name", "username", "email", "phone", "role", "is_active", "email_verified", "profile"] },
|
|
9906
11569
|
c.req.header("x-forwarded-for") || c.req.header("cf-connecting-ip"),
|
|
9907
11570
|
c.req.header("user-agent")
|
|
9908
11571
|
);
|
|
@@ -9915,7 +11578,7 @@ userRoutes.put("/users/:id", async (c) => {
|
|
|
9915
11578
|
console.error("User update error:", error);
|
|
9916
11579
|
return c.html(renderAlert2({
|
|
9917
11580
|
type: "error",
|
|
9918
|
-
message: "Failed to update user
|
|
11581
|
+
message: "Failed to update user. Please try again.",
|
|
9919
11582
|
dismissible: true
|
|
9920
11583
|
}));
|
|
9921
11584
|
}
|
|
@@ -14106,6 +15769,19 @@ var AVAILABLE_PLUGINS = [
|
|
|
14106
15769
|
permissions: [],
|
|
14107
15770
|
dependencies: [],
|
|
14108
15771
|
is_core: true
|
|
15772
|
+
},
|
|
15773
|
+
{
|
|
15774
|
+
id: "ai-search",
|
|
15775
|
+
name: "ai-search-plugin",
|
|
15776
|
+
display_name: "AI Search",
|
|
15777
|
+
description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
|
|
15778
|
+
version: "1.0.0",
|
|
15779
|
+
author: "SonicJS Team",
|
|
15780
|
+
category: "search",
|
|
15781
|
+
icon: "\u{1F50D}",
|
|
15782
|
+
permissions: [],
|
|
15783
|
+
dependencies: [],
|
|
15784
|
+
is_core: true
|
|
14109
15785
|
}
|
|
14110
15786
|
];
|
|
14111
15787
|
adminPluginRoutes.get("/", async (c) => {
|
|
@@ -14184,6 +15860,9 @@ adminPluginRoutes.get("/:id", async (c) => {
|
|
|
14184
15860
|
const user = c.get("user");
|
|
14185
15861
|
const db = c.env.DB;
|
|
14186
15862
|
const pluginId = c.req.param("id");
|
|
15863
|
+
if (pluginId === "ai-search") {
|
|
15864
|
+
return c.text("", 404);
|
|
15865
|
+
}
|
|
14187
15866
|
if (user?.role !== "admin") {
|
|
14188
15867
|
return c.redirect("/admin/plugins");
|
|
14189
15868
|
}
|
|
@@ -14476,6 +16155,33 @@ adminPluginRoutes.post("/install", async (c) => {
|
|
|
14476
16155
|
});
|
|
14477
16156
|
return c.json({ success: true, plugin: easyMdxPlugin2 });
|
|
14478
16157
|
}
|
|
16158
|
+
if (body.name === "ai-search-plugin" || body.name === "ai-search") {
|
|
16159
|
+
const defaultSettings = {
|
|
16160
|
+
enabled: true,
|
|
16161
|
+
ai_mode_enabled: true,
|
|
16162
|
+
selected_collections: [],
|
|
16163
|
+
dismissed_collections: [],
|
|
16164
|
+
autocomplete_enabled: true,
|
|
16165
|
+
cache_duration: 1,
|
|
16166
|
+
results_limit: 20,
|
|
16167
|
+
index_media: false
|
|
16168
|
+
};
|
|
16169
|
+
const aiSearchPlugin = await pluginService.installPlugin({
|
|
16170
|
+
id: "ai-search",
|
|
16171
|
+
name: "ai-search-plugin",
|
|
16172
|
+
display_name: "AI Search",
|
|
16173
|
+
description: "Advanced search with Cloudflare AI Search. Full-text search, semantic search, and advanced filtering across all content collections.",
|
|
16174
|
+
version: "1.0.0",
|
|
16175
|
+
author: "SonicJS Team",
|
|
16176
|
+
category: "search",
|
|
16177
|
+
icon: "\u{1F50D}",
|
|
16178
|
+
permissions: [],
|
|
16179
|
+
dependencies: [],
|
|
16180
|
+
is_core: true,
|
|
16181
|
+
settings: defaultSettings
|
|
16182
|
+
});
|
|
16183
|
+
return c.json({ success: true, plugin: aiSearchPlugin });
|
|
16184
|
+
}
|
|
14479
16185
|
if (body.name === "turnstile-plugin") {
|
|
14480
16186
|
const turnstilePlugin = await pluginService.installPlugin({
|
|
14481
16187
|
id: "turnstile",
|
|
@@ -18376,7 +20082,8 @@ function getFieldTypeBadge(fieldType) {
|
|
|
18376
20082
|
"boolean": "Boolean",
|
|
18377
20083
|
"date": "Date",
|
|
18378
20084
|
"select": "Select",
|
|
18379
|
-
"media": "Media"
|
|
20085
|
+
"media": "Media",
|
|
20086
|
+
"reference": "Reference"
|
|
18380
20087
|
};
|
|
18381
20088
|
const typeColors = {
|
|
18382
20089
|
"text": "bg-blue-500/10 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300 ring-blue-500/20 dark:ring-blue-400/20",
|
|
@@ -18387,7 +20094,8 @@ function getFieldTypeBadge(fieldType) {
|
|
|
18387
20094
|
"boolean": "bg-amber-500/10 dark:bg-amber-400/10 text-amber-700 dark:text-amber-300 ring-amber-500/20 dark:ring-amber-400/20",
|
|
18388
20095
|
"date": "bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-cyan-500/20 dark:ring-cyan-400/20",
|
|
18389
20096
|
"select": "bg-indigo-500/10 dark:bg-indigo-400/10 text-indigo-700 dark:text-indigo-300 ring-indigo-500/20 dark:ring-indigo-400/20",
|
|
18390
|
-
"media": "bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-rose-500/20 dark:ring-rose-400/20"
|
|
20097
|
+
"media": "bg-rose-500/10 dark:bg-rose-400/10 text-rose-700 dark:text-rose-300 ring-rose-500/20 dark:ring-rose-400/20",
|
|
20098
|
+
"reference": "bg-teal-500/10 dark:bg-teal-400/10 text-teal-700 dark:text-teal-300 ring-teal-500/20 dark:ring-teal-400/20"
|
|
18391
20099
|
};
|
|
18392
20100
|
const label = typeLabels[fieldType] || fieldType;
|
|
18393
20101
|
const color = typeColors[fieldType] || "bg-zinc-500/10 dark:bg-zinc-400/10 text-zinc-700 dark:text-zinc-300 ring-zinc-500/20 dark:ring-zinc-400/20";
|
|
@@ -18867,6 +20575,7 @@ function renderCollectionFormPage(data) {
|
|
|
18867
20575
|
<option value="date">Date</option>
|
|
18868
20576
|
<option value="select">Select</option>
|
|
18869
20577
|
<option value="media">Media</option>
|
|
20578
|
+
<option value="reference">Reference</option>
|
|
18870
20579
|
</select>
|
|
18871
20580
|
<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-blue-600 dark:text-blue-400 sm:size-4">
|
|
18872
20581
|
<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" />
|
|
@@ -19184,11 +20893,14 @@ function renderCollectionFormPage(data) {
|
|
|
19184
20893
|
}
|
|
19185
20894
|
|
|
19186
20895
|
// Show/hide options container based on field type
|
|
19187
|
-
|
|
20896
|
+
// Use the dropdown's actual value (not field.field_type) to ensure consistency
|
|
20897
|
+
const fieldType = fieldTypeSelect?.value || field.field_type;
|
|
19188
20898
|
const optionsContainer = document.getElementById('field-options-container');
|
|
19189
20899
|
const helpText = document.getElementById('field-type-help');
|
|
19190
20900
|
|
|
19191
|
-
|
|
20901
|
+
console.log('[Edit Field] Showing options for field type:', fieldType, '(original:', field.field_type, ')');
|
|
20902
|
+
|
|
20903
|
+
if (['select', 'media', 'richtext', 'reference'].includes(fieldType)) {
|
|
19192
20904
|
optionsContainer.classList.remove('hidden');
|
|
19193
20905
|
|
|
19194
20906
|
// Set help text based on type
|
|
@@ -19202,6 +20914,9 @@ function renderCollectionFormPage(data) {
|
|
|
19202
20914
|
case 'richtext':
|
|
19203
20915
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
19204
20916
|
break;
|
|
20917
|
+
case 'reference':
|
|
20918
|
+
helpText.textContent = 'Link to content from other collections';
|
|
20919
|
+
break;
|
|
19205
20920
|
}
|
|
19206
20921
|
} else {
|
|
19207
20922
|
optionsContainer.classList.add('hidden');
|
|
@@ -19336,7 +21051,7 @@ function renderCollectionFormPage(data) {
|
|
|
19336
21051
|
const fieldNameInput = document.getElementById('modal-field-name');
|
|
19337
21052
|
|
|
19338
21053
|
// Show/hide options based on field type
|
|
19339
|
-
if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
|
|
21054
|
+
if (['select', 'media', 'richtext', 'guid', 'reference'].includes(this.value)) {
|
|
19340
21055
|
optionsContainer.classList.remove('hidden');
|
|
19341
21056
|
|
|
19342
21057
|
// Set default options and help text based on type
|
|
@@ -19353,6 +21068,10 @@ function renderCollectionFormPage(data) {
|
|
|
19353
21068
|
fieldOptions.value = '{"toolbar": "full", "height": 400}';
|
|
19354
21069
|
helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
|
|
19355
21070
|
break;
|
|
21071
|
+
case 'reference':
|
|
21072
|
+
fieldOptions.value = '{"collection": ["pages", "posts"]}';
|
|
21073
|
+
helpText.textContent = 'Link to content from other collections';
|
|
21074
|
+
break;
|
|
19356
21075
|
}
|
|
19357
21076
|
} else {
|
|
19358
21077
|
optionsContainer.classList.add('hidden');
|
|
@@ -19921,6 +21640,8 @@ adminCollectionsRoutes.post("/:id/fields", async (c) => {
|
|
|
19921
21640
|
fieldConfig.type = "quill";
|
|
19922
21641
|
} else if (fieldType === "mdxeditor") {
|
|
19923
21642
|
fieldConfig.type = "mdxeditor";
|
|
21643
|
+
} else if (fieldType === "reference") {
|
|
21644
|
+
fieldConfig.type = "reference";
|
|
19924
21645
|
}
|
|
19925
21646
|
schema.properties[fieldName] = fieldConfig;
|
|
19926
21647
|
if (isRequired && !schema.required.includes(fieldName)) {
|
|
@@ -20009,8 +21730,15 @@ adminCollectionsRoutes.put("/:collectionId/fields/:fieldId", async (c) => {
|
|
|
20009
21730
|
schema.required = [];
|
|
20010
21731
|
}
|
|
20011
21732
|
if (schema.properties[fieldName]) {
|
|
21733
|
+
let parsedFieldOptions = {};
|
|
21734
|
+
try {
|
|
21735
|
+
parsedFieldOptions = JSON.parse(fieldOptions);
|
|
21736
|
+
} catch (e) {
|
|
21737
|
+
console.error("[Field Update] Error parsing field options:", e);
|
|
21738
|
+
}
|
|
20012
21739
|
const updatedFieldConfig = {
|
|
20013
21740
|
...schema.properties[fieldName],
|
|
21741
|
+
...parsedFieldOptions,
|
|
20014
21742
|
type: fieldType,
|
|
20015
21743
|
title: fieldLabel,
|
|
20016
21744
|
searchable: isSearchable
|
|
@@ -22034,6 +23762,6 @@ var ROUTES_INFO = {
|
|
|
22034
23762
|
reference: "https://github.com/sonicjs/sonicjs"
|
|
22035
23763
|
};
|
|
22036
23764
|
|
|
22037
|
-
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,
|
|
22038
|
-
//# sourceMappingURL=chunk-
|
|
22039
|
-
//# sourceMappingURL=chunk-
|
|
23765
|
+
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, getConfirmationDialogScript2 as getConfirmationDialogScript, renderConfirmationDialog2 as renderConfirmationDialog, router, test_cleanup_default, userRoutes };
|
|
23766
|
+
//# sourceMappingURL=chunk-UISZ2MBW.js.map
|
|
23767
|
+
//# sourceMappingURL=chunk-UISZ2MBW.js.map
|