@sonicjs-cms/core 3.0.0-beta.22 → 3.0.0-beta.24
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/adapters.cjs +512 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +342 -0
- package/dist/adapters.d.ts +342 -0
- package/dist/adapters.js +501 -0
- package/dist/adapters.js.map +1 -0
- package/dist/admin-documents-form.template-55V2ZGWT.js +6 -0
- package/dist/{admin-documents-form.template-DDSH6ROU.js.map → admin-documents-form.template-55V2ZGWT.js.map} +1 -1
- package/dist/admin-documents-form.template-YNAVMJCV.cjs +19 -0
- package/dist/{admin-layout-catalyst.template-YQ4EMF2J.js.map → admin-documents-form.template-YNAVMJCV.cjs.map} +1 -1
- package/dist/admin-layout-catalyst.template-3XZXDGF7.cjs +21 -0
- package/dist/{admin-layout-catalyst.template-KDHKVLXR.cjs.map → admin-layout-catalyst.template-3XZXDGF7.cjs.map} +1 -1
- package/dist/admin-layout-catalyst.template-6UQGLCKI.js +7 -0
- package/dist/{admin-documents-form.template-LSZKGA5J.cjs.map → admin-layout-catalyst.template-6UQGLCKI.js.map} +1 -1
- package/dist/{chunk-FTXGZ3BF.cjs → chunk-37V3RET3.cjs} +21 -37
- package/dist/chunk-37V3RET3.cjs.map +1 -0
- package/dist/{chunk-3CT3SUR4.cjs → chunk-A2SN3RVL.cjs} +3 -3
- package/dist/chunk-A2SN3RVL.cjs.map +1 -0
- package/dist/{chunk-CRGUD4KC.cjs → chunk-BBEKHLTW.cjs} +9 -9
- package/dist/{chunk-CRGUD4KC.cjs.map → chunk-BBEKHLTW.cjs.map} +1 -1
- package/dist/{chunk-RMRJGMDE.js → chunk-BHND5PEV.js} +3 -3
- package/dist/{chunk-RMRJGMDE.js.map → chunk-BHND5PEV.js.map} +1 -1
- package/dist/{chunk-OBA2RYZN.js → chunk-BJROXF4N.js} +3 -3
- package/dist/{chunk-OBA2RYZN.js.map → chunk-BJROXF4N.js.map} +1 -1
- package/dist/{chunk-EUJAEB6W.cjs → chunk-BP52BFIV.cjs} +53 -4
- package/dist/chunk-BP52BFIV.cjs.map +1 -0
- package/dist/{chunk-PXNTCCPE.cjs → chunk-E66UVONV.cjs} +8 -8
- package/dist/{chunk-PXNTCCPE.cjs.map → chunk-E66UVONV.cjs.map} +1 -1
- package/dist/{chunk-CHWT5KBK.js → chunk-FHPEM77Y.js} +9 -25
- package/dist/chunk-FHPEM77Y.js.map +1 -0
- package/dist/{chunk-JFLZHOQP.cjs → chunk-I767LVN7.cjs} +531 -419
- package/dist/chunk-I767LVN7.cjs.map +1 -0
- package/dist/{chunk-N32OWET6.cjs → chunk-JMJM7DMM.cjs} +5 -5
- package/dist/{chunk-N32OWET6.cjs.map → chunk-JMJM7DMM.cjs.map} +1 -1
- package/dist/{chunk-QWSR4THH.cjs → chunk-KKNEYIZJ.cjs} +12 -5
- package/dist/chunk-KKNEYIZJ.cjs.map +1 -0
- package/dist/{chunk-73O4GPJ6.js → chunk-LNI3FYSI.js} +12 -5
- package/dist/chunk-LNI3FYSI.js.map +1 -0
- package/dist/{chunk-PYZYU7EK.cjs → chunk-QCJHCSOK.cjs} +36 -2
- package/dist/chunk-QCJHCSOK.cjs.map +1 -0
- package/dist/{chunk-MGHHPNJN.cjs → chunk-QJUJNTIU.cjs} +11 -59
- package/dist/chunk-QJUJNTIU.cjs.map +1 -0
- package/dist/{chunk-DOMXI5PM.js → chunk-RJ2O5IIE.js} +36 -2
- package/dist/chunk-RJ2O5IIE.js.map +1 -0
- package/dist/{chunk-5V62WT6M.js → chunk-THFZSDIU.js} +4 -2
- package/dist/chunk-THFZSDIU.js.map +1 -0
- package/dist/{chunk-2FCKKOA5.js → chunk-UIHIXTIS.js} +3 -3
- package/dist/chunk-UIHIXTIS.js.map +1 -0
- package/dist/{chunk-JQISFW6U.js → chunk-URIBTX27.js} +3 -3
- package/dist/{chunk-JQISFW6U.js.map → chunk-URIBTX27.js.map} +1 -1
- package/dist/{chunk-K623Q6WD.cjs → chunk-UYJQDDEE.cjs} +4 -2
- package/dist/chunk-UYJQDDEE.cjs.map +1 -0
- package/dist/{chunk-6KNTZ67K.js → chunk-VC5XVPLM.js} +234 -126
- package/dist/chunk-VC5XVPLM.js.map +1 -0
- package/dist/{chunk-I2DM27HP.js → chunk-VGEZNFWH.js} +49 -4
- package/dist/chunk-VGEZNFWH.js.map +1 -0
- package/dist/{chunk-GB2Q2DNK.js → chunk-VYQVDL2M.js} +5 -49
- package/dist/chunk-VYQVDL2M.js.map +1 -0
- package/dist/{define-plugin-DsKiGu1q.d.cts → define-plugin-DMcvEy_C.d.cts} +33 -0
- package/dist/{define-plugin-BYIey6sp.d.ts → define-plugin-D_6D9vVZ.d.ts} +33 -0
- package/dist/index.cjs +2587 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +2281 -22
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +34 -34
- package/dist/middleware.js +5 -5
- package/dist/migrations-FGXZPKDC.cjs +13 -0
- package/dist/{migrations-UIMA6ZL6.cjs.map → migrations-FGXZPKDC.cjs.map} +1 -1
- package/dist/migrations-Z6D27NAW.js +4 -0
- package/dist/{migrations-HLK2QF4T.js.map → migrations-Z6D27NAW.js.map} +1 -1
- package/dist/plugins.cjs +40 -40
- package/dist/plugins.d.cts +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +3 -3
- package/dist/routes.cjs +29 -29
- package/dist/routes.js +9 -9
- package/dist/services.cjs +18 -18
- package/dist/services.js +2 -2
- package/dist/templates.cjs +17 -17
- package/dist/templates.js +3 -3
- package/dist/utils.cjs +3 -3
- package/dist/utils.js +1 -1
- package/migrations/0001_core.sql +7 -1
- package/package.json +10 -3
- package/dist/admin-documents-form.template-DDSH6ROU.js +0 -6
- package/dist/admin-documents-form.template-LSZKGA5J.cjs +0 -19
- package/dist/admin-layout-catalyst.template-KDHKVLXR.cjs +0 -21
- package/dist/admin-layout-catalyst.template-YQ4EMF2J.js +0 -7
- package/dist/chunk-2FCKKOA5.js.map +0 -1
- package/dist/chunk-3CT3SUR4.cjs.map +0 -1
- package/dist/chunk-5V62WT6M.js.map +0 -1
- package/dist/chunk-6KNTZ67K.js.map +0 -1
- package/dist/chunk-73O4GPJ6.js.map +0 -1
- package/dist/chunk-CHWT5KBK.js.map +0 -1
- package/dist/chunk-DOMXI5PM.js.map +0 -1
- package/dist/chunk-EUJAEB6W.cjs.map +0 -1
- package/dist/chunk-FTXGZ3BF.cjs.map +0 -1
- package/dist/chunk-GB2Q2DNK.js.map +0 -1
- package/dist/chunk-I2DM27HP.js.map +0 -1
- package/dist/chunk-JFLZHOQP.cjs.map +0 -1
- package/dist/chunk-K623Q6WD.cjs.map +0 -1
- package/dist/chunk-MGHHPNJN.cjs.map +0 -1
- package/dist/chunk-PYZYU7EK.cjs.map +0 -1
- package/dist/chunk-QWSR4THH.cjs.map +0 -1
- package/dist/migrations-HLK2QF4T.js +0 -4
- package/dist/migrations-UIMA6ZL6.cjs +0 -13
package/dist/index.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
import { createAuth } from './chunk-SOZUG52O.js';
|
|
2
|
-
import { getEmailService, hasEmailService, setEmailService, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-
|
|
3
|
-
export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-
|
|
2
|
+
import { getEmailService, hasEmailService, setEmailService, reconcileMenuFromPlugins, invalidateVariablesCache, resolveVariablesInObject, resolveDocScope, denyIfNotAllowed, DocumentRepository, dispatchHookEvent, getCustomData, createDocumentSchema, renderTable, getVariablesCached, setVariablesCache, TenantService, isValidMemberRole, renderConfirmationDialog, getConfirmationDialogScript, VALID_MEMBER_ROLES, userProfilesPlugin, lexicalEditorPlugin, api_media_default, api_system_default, apiDocumentsRoutes, api_default, adminDocumentsRoutes, admin_api_default, adminCollectionsRoutes, adminSettingsRoutes, router2, admin_content_default, adminMediaRoutes, router, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default, getDocumentRequestContext, updateDocumentSchema, effectiveTenantForType } from './chunk-VC5XVPLM.js';
|
|
3
|
+
export { DocumentRepository, ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-VC5XVPLM.js';
|
|
4
4
|
import './chunk-VVYVFCML.js';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-BHND5PEV.js';
|
|
6
6
|
import { SettingsService, setAppInstance } from './chunk-2XAQTPAE.js';
|
|
7
7
|
export { Logger, getLogger, initLogger } from './chunk-2XAQTPAE.js';
|
|
8
8
|
import { getAllCacheStats, CACHE_CONFIGS, getCacheService, clearAllCaches, parseCacheKey } from './chunk-K25XHMM3.js';
|
|
9
9
|
import { schema_exports } from './chunk-AI663NBO.js';
|
|
10
10
|
export { apiTokens, content, contentVersions, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-AI663NBO.js';
|
|
11
|
-
import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-
|
|
12
|
-
export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-
|
|
11
|
+
import { requireAuth, DocumentTypeRegistry, requireRbac, requireRole, getJwtExpirySecondsFromDb, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-FHPEM77Y.js';
|
|
12
|
+
export { AuthManager, PermissionManager, bootstrapDocumentTypes, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-FHPEM77Y.js';
|
|
13
13
|
import { RbacService, DocumentsService } from './chunk-3TMTYJAJ.js';
|
|
14
14
|
export { DocumentsService, RbacService } from './chunk-3TMTYJAJ.js';
|
|
15
15
|
import { nanoid } from './chunk-UWUP2VMT.js';
|
|
16
|
-
import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-
|
|
17
|
-
export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-
|
|
18
|
-
export { MigrationService } from './chunk-
|
|
19
|
-
export { renderFilterBar } from './chunk-
|
|
20
|
-
import { renderAdminLayout } from './chunk-
|
|
21
|
-
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-
|
|
22
|
-
import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-
|
|
23
|
-
export { renderAdminLayoutCatalyst } from './chunk-
|
|
24
|
-
import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes
|
|
25
|
-
export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick,
|
|
26
|
-
import { definePlugin, setPluginDefinitions } from './chunk-
|
|
27
|
-
export { applySchemaDefaults, definePlugin, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields } from './chunk-
|
|
16
|
+
import { PluginService, PLUGIN_REGISTRY, invalidateTenantCache, getCollectionRegistry, TENANT_COOKIE, MULTI_TENANT_PLUGIN_ID, collectionRecordToRow, tenantMiddleware } from './chunk-RJ2O5IIE.js';
|
|
17
|
+
export { PluginBootstrapService, PluginService as PluginServiceClass, getAvailableCollectionNames, loadCollectionConfig, loadCollectionConfigs, registerCollections, validateCollectionConfig } from './chunk-RJ2O5IIE.js';
|
|
18
|
+
export { MigrationService } from './chunk-UIHIXTIS.js';
|
|
19
|
+
export { renderFilterBar } from './chunk-URIBTX27.js';
|
|
20
|
+
import { renderAdminLayout } from './chunk-BJROXF4N.js';
|
|
21
|
+
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-BJROXF4N.js';
|
|
22
|
+
import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
|
|
23
|
+
export { renderAdminLayoutCatalyst } from './chunk-THFZSDIU.js';
|
|
24
|
+
import { TurnstileService, HookSystemImpl, createPluginWirer, registerPluginRoutes } from './chunk-VYQVDL2M.js';
|
|
25
|
+
export { HookSystemImpl, HookUtils, PluginDependencyCycleError, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, RegisterPluginsError, ScopedHookSystem as ScopedHookSystemClass, collectCronSchedules, collectCrons, createScheduledHandler, dispatchCronTick, registerPlugins, topoSort } from './chunk-VYQVDL2M.js';
|
|
26
|
+
import { definePlugin, setPluginMenu, setPluginDefinitions, resolvePluginMenuItems } from './chunk-VGEZNFWH.js';
|
|
27
|
+
export { applySchemaDefaults, definePlugin, getPluginMenu, isDefinedPlugin, parseConfigSchema, parseFormDataToSettings, renderSchemaFields, resetPluginMenu, resolvePluginMenuItems, setPluginMenu } from './chunk-VGEZNFWH.js';
|
|
28
28
|
import { setHookSystem } from './chunk-RNZFGN4R.js';
|
|
29
29
|
export { getHookSystem, getTypedHooks, hasHookSystem, resetHookSystem, setHookSystem } from './chunk-RNZFGN4R.js';
|
|
30
30
|
export { QueryFilterBuilder, TemplateRenderer, buildQuery, renderTemplate, templateRenderer } from './chunk-QZGABF2M.js';
|
|
31
31
|
export { metricsTracker } from './chunk-FICTAGD4.js';
|
|
32
32
|
import './chunk-X7ZAEI5S.js';
|
|
33
|
-
import { getCoreVersion, package_default } from './chunk-
|
|
34
|
-
export { SONICJS_VERSION, getCoreVersion } from './chunk-
|
|
33
|
+
import { getCoreVersion, package_default } from './chunk-LNI3FYSI.js';
|
|
34
|
+
export { SONICJS_VERSION, getCoreVersion } from './chunk-LNI3FYSI.js';
|
|
35
35
|
import { escapeHtml, sanitizeInput } from './chunk-TQABQWOP.js';
|
|
36
36
|
export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
|
|
37
37
|
export { HOOKS } from './chunk-QXOZI5Q2.js';
|
|
@@ -8112,7 +8112,7 @@ adminRoutes5.get("/settings", async (c) => {
|
|
|
8112
8112
|
const db = c.env.DB;
|
|
8113
8113
|
const user = c.get("user");
|
|
8114
8114
|
const settings = await getSettings4(db);
|
|
8115
|
-
const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-
|
|
8115
|
+
const { renderAdminLayoutCatalyst: renderAdminLayoutCatalyst2 } = await import('./admin-layout-catalyst.template-6UQGLCKI.js');
|
|
8116
8116
|
const content2 = `
|
|
8117
8117
|
<div>
|
|
8118
8118
|
<div class="mb-6">
|
|
@@ -11777,6 +11777,1436 @@ function pluginMenuMiddleware() {
|
|
|
11777
11777
|
}
|
|
11778
11778
|
};
|
|
11779
11779
|
}
|
|
11780
|
+
|
|
11781
|
+
// src/plugins/core-plugins/menu-plugin/services/menu-repository.ts
|
|
11782
|
+
async function listMenuItems(db, opts) {
|
|
11783
|
+
try {
|
|
11784
|
+
let sql = `
|
|
11785
|
+
SELECT id, slug, sort_order, data
|
|
11786
|
+
FROM documents
|
|
11787
|
+
WHERE type_id = 'menu_item'
|
|
11788
|
+
AND tenant_id = 'default'
|
|
11789
|
+
AND is_current_draft = 1
|
|
11790
|
+
AND deleted_at IS NULL
|
|
11791
|
+
`;
|
|
11792
|
+
const binds = [];
|
|
11793
|
+
if (opts?.source) ;
|
|
11794
|
+
sql += ` ORDER BY sort_order ASC`;
|
|
11795
|
+
const stmt = db.prepare(sql);
|
|
11796
|
+
const result = await (binds.length ? stmt.bind(...binds) : stmt).all();
|
|
11797
|
+
return (result.results ?? []).map((row) => {
|
|
11798
|
+
const data = JSON.parse(row.data);
|
|
11799
|
+
return {
|
|
11800
|
+
...data,
|
|
11801
|
+
id: row.id,
|
|
11802
|
+
slug: row.slug,
|
|
11803
|
+
sortOrder: row.sort_order,
|
|
11804
|
+
tenantId: "default"
|
|
11805
|
+
};
|
|
11806
|
+
});
|
|
11807
|
+
} catch {
|
|
11808
|
+
return [];
|
|
11809
|
+
}
|
|
11810
|
+
}
|
|
11811
|
+
async function fetchPluginStatuses(db, pluginIds) {
|
|
11812
|
+
if (pluginIds.length === 0) return {};
|
|
11813
|
+
try {
|
|
11814
|
+
const rows = await db.prepare(
|
|
11815
|
+
`SELECT slug, json_extract(data, '$.status') AS status
|
|
11816
|
+
FROM documents
|
|
11817
|
+
WHERE type_id = 'plugin' AND tenant_id = 'default'
|
|
11818
|
+
AND is_current_draft = 1 AND deleted_at IS NULL`
|
|
11819
|
+
).all();
|
|
11820
|
+
const statusMap = /* @__PURE__ */ new Map();
|
|
11821
|
+
for (const row of rows.results ?? []) {
|
|
11822
|
+
statusMap.set(row.slug, row.status ?? "inactive");
|
|
11823
|
+
}
|
|
11824
|
+
const result = {};
|
|
11825
|
+
for (const id of pluginIds) {
|
|
11826
|
+
const s = statusMap.get(id);
|
|
11827
|
+
result[id] = s === "active" ? "active" : "inactive";
|
|
11828
|
+
}
|
|
11829
|
+
return result;
|
|
11830
|
+
} catch {
|
|
11831
|
+
return {};
|
|
11832
|
+
}
|
|
11833
|
+
}
|
|
11834
|
+
function buildSidebarTree(items) {
|
|
11835
|
+
const visible = items.filter((i) => i.visible);
|
|
11836
|
+
visible.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
11837
|
+
const byId = /* @__PURE__ */ new Map();
|
|
11838
|
+
const roots = [];
|
|
11839
|
+
for (const item of visible) {
|
|
11840
|
+
byId.set(item.id, {
|
|
11841
|
+
id: item.id,
|
|
11842
|
+
label: item.label,
|
|
11843
|
+
url: item.url,
|
|
11844
|
+
icon: item.icon,
|
|
11845
|
+
target: item.target,
|
|
11846
|
+
isExternal: item.isExternal,
|
|
11847
|
+
visible: item.visible,
|
|
11848
|
+
source: item.source,
|
|
11849
|
+
sortOrder: item.sortOrder,
|
|
11850
|
+
children: []
|
|
11851
|
+
});
|
|
11852
|
+
}
|
|
11853
|
+
for (const item of visible) {
|
|
11854
|
+
const node = byId.get(item.id);
|
|
11855
|
+
if (item.parent && byId.has(item.parent)) {
|
|
11856
|
+
byId.get(item.parent).children.push(node);
|
|
11857
|
+
} else {
|
|
11858
|
+
roots.push(node);
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
for (const node of byId.values()) {
|
|
11862
|
+
node.children.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
11863
|
+
}
|
|
11864
|
+
return roots;
|
|
11865
|
+
}
|
|
11866
|
+
async function upsertSystemItem(db, slug, data, sortOrder) {
|
|
11867
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11868
|
+
const existing = await db.prepare(
|
|
11869
|
+
`SELECT id FROM documents WHERE slug = ? AND type_id = 'menu_item' AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL`
|
|
11870
|
+
).bind(slug).first();
|
|
11871
|
+
if (existing) {
|
|
11872
|
+
return;
|
|
11873
|
+
}
|
|
11874
|
+
const id = nanoid();
|
|
11875
|
+
await db.prepare(
|
|
11876
|
+
`INSERT INTO documents (
|
|
11877
|
+
id, root_id, type_id, type_version, version_of_id, version_number,
|
|
11878
|
+
is_current_draft, is_published, status, parent_root_id,
|
|
11879
|
+
slug, path, title, zone, sort_order, visible,
|
|
11880
|
+
published_at, scheduled_at, expires_at, deleted_at,
|
|
11881
|
+
tenant_id, locale, translation_group_id,
|
|
11882
|
+
data, metadata, owner_id, created_by, updated_by, created_at, updated_at
|
|
11883
|
+
) VALUES (
|
|
11884
|
+
?, ?, ?, ?, ?, ?,
|
|
11885
|
+
?, ?, ?, ?,
|
|
11886
|
+
?, ?, ?, ?, ?, ?,
|
|
11887
|
+
?, ?, ?, ?,
|
|
11888
|
+
?, ?, ?,
|
|
11889
|
+
?, ?, ?, ?, ?, ?, ?
|
|
11890
|
+
)`
|
|
11891
|
+
).bind(
|
|
11892
|
+
id,
|
|
11893
|
+
// id
|
|
11894
|
+
id,
|
|
11895
|
+
// root_id
|
|
11896
|
+
"menu_item",
|
|
11897
|
+
// type_id
|
|
11898
|
+
1,
|
|
11899
|
+
// type_version
|
|
11900
|
+
null,
|
|
11901
|
+
// version_of_id
|
|
11902
|
+
1,
|
|
11903
|
+
// version_number
|
|
11904
|
+
1,
|
|
11905
|
+
// is_current_draft
|
|
11906
|
+
1,
|
|
11907
|
+
// is_published
|
|
11908
|
+
"published",
|
|
11909
|
+
// status
|
|
11910
|
+
"",
|
|
11911
|
+
// parent_root_id
|
|
11912
|
+
slug,
|
|
11913
|
+
// slug
|
|
11914
|
+
null,
|
|
11915
|
+
// path
|
|
11916
|
+
data.label,
|
|
11917
|
+
// title
|
|
11918
|
+
null,
|
|
11919
|
+
// zone
|
|
11920
|
+
sortOrder,
|
|
11921
|
+
// sort_order
|
|
11922
|
+
data.visible ? 1 : 0,
|
|
11923
|
+
// visible
|
|
11924
|
+
now,
|
|
11925
|
+
// published_at
|
|
11926
|
+
null,
|
|
11927
|
+
// scheduled_at
|
|
11928
|
+
null,
|
|
11929
|
+
// expires_at
|
|
11930
|
+
null,
|
|
11931
|
+
// deleted_at
|
|
11932
|
+
"default",
|
|
11933
|
+
// tenant_id
|
|
11934
|
+
"default",
|
|
11935
|
+
// locale
|
|
11936
|
+
"",
|
|
11937
|
+
// translation_group_id
|
|
11938
|
+
JSON.stringify(data),
|
|
11939
|
+
// data
|
|
11940
|
+
"{}",
|
|
11941
|
+
// metadata
|
|
11942
|
+
null,
|
|
11943
|
+
// owner_id
|
|
11944
|
+
"system",
|
|
11945
|
+
// created_by
|
|
11946
|
+
"system",
|
|
11947
|
+
// updated_by
|
|
11948
|
+
now,
|
|
11949
|
+
// created_at
|
|
11950
|
+
now
|
|
11951
|
+
// updated_at
|
|
11952
|
+
).run();
|
|
11953
|
+
}
|
|
11954
|
+
async function reorderItems(db, items) {
|
|
11955
|
+
if (items.length === 0) return;
|
|
11956
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11957
|
+
const stmts = items.map(
|
|
11958
|
+
(item) => db.prepare(
|
|
11959
|
+
`UPDATE documents SET
|
|
11960
|
+
sort_order = ?,
|
|
11961
|
+
data = json_set(data, '$.parent', ?),
|
|
11962
|
+
updated_at = ?
|
|
11963
|
+
WHERE id = ? AND tenant_id = 'default'`
|
|
11964
|
+
).bind(item.sortOrder, item.parent ?? null, now, item.id)
|
|
11965
|
+
);
|
|
11966
|
+
await db.batch(stmts);
|
|
11967
|
+
}
|
|
11968
|
+
async function toggleVisibility(db, id, visible) {
|
|
11969
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11970
|
+
const visInt = visible ? 1 : 0;
|
|
11971
|
+
await db.prepare(
|
|
11972
|
+
`UPDATE documents SET
|
|
11973
|
+
data = json_set(data, '$.visible', ?),
|
|
11974
|
+
visible = ?,
|
|
11975
|
+
updated_at = ?
|
|
11976
|
+
WHERE id = ? AND tenant_id = 'default' AND type_id = 'menu_item'`
|
|
11977
|
+
).bind(visInt, visInt, now, id).run();
|
|
11978
|
+
}
|
|
11979
|
+
async function updateItem(db, id, changes, lockedFields) {
|
|
11980
|
+
const lockedKeys = new Set(lockedFields.filter((f) => f !== "parent"));
|
|
11981
|
+
for (const key of Object.keys(changes)) {
|
|
11982
|
+
if (lockedKeys.has(key)) {
|
|
11983
|
+
return false;
|
|
11984
|
+
}
|
|
11985
|
+
}
|
|
11986
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
11987
|
+
const jsonSetParts = [];
|
|
11988
|
+
const binds = [];
|
|
11989
|
+
const dataFields = ["label", "icon", "target", "permissions", "url", "parent", "visible"];
|
|
11990
|
+
for (const field of dataFields) {
|
|
11991
|
+
if (field in changes) {
|
|
11992
|
+
jsonSetParts.push(`'$.${field}'`, "?");
|
|
11993
|
+
binds.push(
|
|
11994
|
+
field === "permissions" ? JSON.stringify(changes[field]) : changes[field] ?? null
|
|
11995
|
+
);
|
|
11996
|
+
}
|
|
11997
|
+
}
|
|
11998
|
+
const setParts = [];
|
|
11999
|
+
if (jsonSetParts.length > 0) {
|
|
12000
|
+
setParts.push(`data = json_set(data, ${jsonSetParts.join(", ")})`);
|
|
12001
|
+
}
|
|
12002
|
+
if ("sortOrder" in changes && changes.sortOrder !== void 0) {
|
|
12003
|
+
setParts.push(`sort_order = ?`);
|
|
12004
|
+
binds.push(changes.sortOrder);
|
|
12005
|
+
}
|
|
12006
|
+
if ("visible" in changes && changes.visible !== void 0) {
|
|
12007
|
+
setParts.push(`visible = ?`);
|
|
12008
|
+
binds.push(changes.visible ? 1 : 0);
|
|
12009
|
+
}
|
|
12010
|
+
if (setParts.length === 0) {
|
|
12011
|
+
return true;
|
|
12012
|
+
}
|
|
12013
|
+
setParts.push(`updated_at = ?`);
|
|
12014
|
+
binds.push(now);
|
|
12015
|
+
binds.push(id);
|
|
12016
|
+
const sql = `UPDATE documents SET ${setParts.join(", ")} WHERE id = ? AND tenant_id = 'default' AND type_id = 'menu_item'`;
|
|
12017
|
+
await db.prepare(sql).bind(...binds).run();
|
|
12018
|
+
return true;
|
|
12019
|
+
}
|
|
12020
|
+
async function deleteItem(db, id) {
|
|
12021
|
+
try {
|
|
12022
|
+
const row = await db.prepare(
|
|
12023
|
+
`SELECT data FROM documents WHERE id = ? AND tenant_id = 'default' AND is_current_draft = 1 AND deleted_at IS NULL`
|
|
12024
|
+
).bind(id).first();
|
|
12025
|
+
if (!row) {
|
|
12026
|
+
return { ok: false, reason: "Item not found" };
|
|
12027
|
+
}
|
|
12028
|
+
const data = JSON.parse(row.data);
|
|
12029
|
+
if (data.source === "system" || data.source === "plugin") {
|
|
12030
|
+
return { ok: false, reason: "Cannot delete system or plugin items" };
|
|
12031
|
+
}
|
|
12032
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
12033
|
+
await db.prepare(
|
|
12034
|
+
`UPDATE documents SET deleted_at = ? WHERE id = ? AND tenant_id = 'default'`
|
|
12035
|
+
).bind(now, id).run();
|
|
12036
|
+
return { ok: true };
|
|
12037
|
+
} catch (err) {
|
|
12038
|
+
return { ok: false, reason: err instanceof Error ? err.message : "Unknown error" };
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
|
|
12042
|
+
// src/services/menu-icons.ts
|
|
12043
|
+
var MENU_ICON_MAP = {
|
|
12044
|
+
"puzzle-piece": `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M14.5 2A1.5 1.5 0 0013 3.5V4H8.5A1.5 1.5 0 007 5.5V10h-.5a1.5 1.5 0 100 3H7v4.5A1.5 1.5 0 008.5 19H13v-.5a1.5 1.5 0 113 0V19h.5a1.5 1.5 0 001.5-1.5V13h-.5a1.5 1.5 0 110-3h.5V5.5A1.5 1.5 0 0017 4h-.5v-.5A1.5 1.5 0 0014.5 2z"/></svg>`,
|
|
12045
|
+
envelope: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/></svg>`,
|
|
12046
|
+
cog: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/></svg>`,
|
|
12047
|
+
chart: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/></svg>`,
|
|
12048
|
+
sparkles: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>`,
|
|
12049
|
+
bolt: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/></svg>`,
|
|
12050
|
+
document: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm3 1h6v2H7V5zm0 4h6v2H7V9zm0 4h4v2H7v-2z" clip-rule="evenodd"/></svg>`,
|
|
12051
|
+
lock: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>`,
|
|
12052
|
+
photo: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>`,
|
|
12053
|
+
"magnifying-glass": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>`,
|
|
12054
|
+
"chart-bar": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg>`,
|
|
12055
|
+
image: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/></svg>`,
|
|
12056
|
+
palette: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z"/></svg>`,
|
|
12057
|
+
"hand-raised": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.05 4.575a1.575 1.575 0 1 0-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 0 1 3.15 0v1.5m-3.15 0 .075 5.925m3.075-5.925v2.925m0-2.925a1.575 1.575 0 0 1 3.15 0V9.9m-3.15-2.4v5.325M16.5 9.9a1.575 1.575 0 0 1 3.15 0V15a6.15 6.15 0 0 1-6.15 6.15H12A6.15 6.15 0 0 1 5.85 15V9.525"/></svg>`,
|
|
12058
|
+
key: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"/></svg>`,
|
|
12059
|
+
"arrow-right": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>`,
|
|
12060
|
+
"shield-check": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>`,
|
|
12061
|
+
"credit-card": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>`,
|
|
12062
|
+
"document-text": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5.586a1 1 0 0 1 .707.293l5.414 5.414a1 1 0 0 1 .293.707V19a2 2 0 0 1-2 2z"/></svg>`,
|
|
12063
|
+
"pencil-square": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>`,
|
|
12064
|
+
server: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd"/></svg>`,
|
|
12065
|
+
"building-office": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/></svg>`,
|
|
12066
|
+
home: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>`,
|
|
12067
|
+
users: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>`,
|
|
12068
|
+
collection: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"/></svg>`,
|
|
12069
|
+
"bars-3": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>`,
|
|
12070
|
+
"cog-6-tooth": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>`,
|
|
12071
|
+
star: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>`,
|
|
12072
|
+
tag: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>`,
|
|
12073
|
+
link: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>`,
|
|
12074
|
+
"external-link": `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>`,
|
|
12075
|
+
eye: `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>`,
|
|
12076
|
+
"eye-slash": `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd"/><path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z"/></svg>`
|
|
12077
|
+
};
|
|
12078
|
+
var FALLBACK = `<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path d="M14.5 2A1.5 1.5 0 0013 3.5V4H8.5A1.5 1.5 0 007 5.5V10h-.5a1.5 1.5 0 100 3H7v4.5A1.5 1.5 0 008.5 19H13v-.5a1.5 1.5 0 113 0V19h.5a1.5 1.5 0 001.5-1.5V13h-.5a1.5 1.5 0 110-3h.5V5.5A1.5 1.5 0 0017 4h-.5v-.5A1.5 1.5 0 0014.5 2z"/></svg>`;
|
|
12079
|
+
function resolveIcon2(nameOrSvg) {
|
|
12080
|
+
if (!nameOrSvg) return FALLBACK;
|
|
12081
|
+
if (nameOrSvg.startsWith("<svg")) return nameOrSvg;
|
|
12082
|
+
return MENU_ICON_MAP[nameOrSvg] ?? FALLBACK;
|
|
12083
|
+
}
|
|
12084
|
+
|
|
12085
|
+
// src/middleware/menu.ts
|
|
12086
|
+
var EXTERNAL_ICON = `<svg class="h-3 w-3 ml-1 shrink-0 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>`;
|
|
12087
|
+
var CHEVRON = `<svg data-menu-chevron class="h-4 w-4 rotate-180 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>`;
|
|
12088
|
+
function renderTopLevelItem(item, currentPath) {
|
|
12089
|
+
const isActive = currentPath === item.url || item.url !== "/admin" && currentPath.startsWith(item.url);
|
|
12090
|
+
const icon = resolveIcon2(item.icon);
|
|
12091
|
+
const label = escapeHtml(item.label);
|
|
12092
|
+
const targetAttr = item.target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
12093
|
+
const externalAffordance = item.isExternal ? EXTERNAL_ICON : "";
|
|
12094
|
+
if (item.children.length > 0) {
|
|
12095
|
+
const accordionId = `acc-${item.id.replace(/[^a-z0-9]/gi, "-")}`;
|
|
12096
|
+
const isChildActive = item.children.some(
|
|
12097
|
+
(c) => currentPath === c.url || currentPath.startsWith(c.url)
|
|
12098
|
+
);
|
|
12099
|
+
const accordionActive = isActive || isChildActive;
|
|
12100
|
+
return `
|
|
12101
|
+
<div data-menu-accordion class="relative">
|
|
12102
|
+
${accordionActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
|
|
12103
|
+
<div class="flex w-full items-center">
|
|
12104
|
+
<a href="${item.url}"${targetAttr}
|
|
12105
|
+
class="flex flex-1 items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm/5 font-medium ${accordionActive ? "text-zinc-950 dark:text-white" : "text-zinc-950 hover:bg-zinc-950/5 dark:text-white dark:hover:bg-white/5"}"
|
|
12106
|
+
${accordionActive ? 'data-current="true"' : ""}
|
|
12107
|
+
>
|
|
12108
|
+
<span class="shrink-0 ${accordionActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
|
|
12109
|
+
<span class="truncate">${label}</span>${externalAffordance}
|
|
12110
|
+
</a>
|
|
12111
|
+
<button
|
|
12112
|
+
onclick="toggleMenuAccordion('${accordionId}')"
|
|
12113
|
+
class="flex items-center justify-center rounded-lg p-2 text-zinc-500 hover:bg-zinc-950/5 dark:text-zinc-400 dark:hover:bg-white/5 flex-shrink-0"
|
|
12114
|
+
aria-label="Toggle ${label} submenu"
|
|
12115
|
+
>
|
|
12116
|
+
${CHEVRON}
|
|
12117
|
+
</button>
|
|
12118
|
+
</div>
|
|
12119
|
+
<div id="${accordionId}" class="pl-6 mt-0.5 flex flex-col gap-0.5">
|
|
12120
|
+
${item.children.map((child) => renderChildItem(child, currentPath)).join("")}
|
|
12121
|
+
</div>
|
|
12122
|
+
</div>`;
|
|
12123
|
+
}
|
|
12124
|
+
return `
|
|
12125
|
+
<span class="relative">
|
|
12126
|
+
${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
|
|
12127
|
+
<a href="${item.url}"${targetAttr}
|
|
12128
|
+
class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm/5 font-medium ${isActive ? "text-zinc-950 dark:text-white" : "text-zinc-950 hover:bg-zinc-950/5 dark:text-white dark:hover:bg-white/5"}"
|
|
12129
|
+
${isActive ? 'data-current="true"' : ""}
|
|
12130
|
+
>
|
|
12131
|
+
<span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
|
|
12132
|
+
<span class="truncate">${label}</span>${externalAffordance}
|
|
12133
|
+
</a>
|
|
12134
|
+
</span>`;
|
|
12135
|
+
}
|
|
12136
|
+
function renderChildItem(item, currentPath) {
|
|
12137
|
+
const isActive = currentPath === item.url || item.url !== "/admin" && currentPath.startsWith(item.url);
|
|
12138
|
+
const icon = resolveIcon2(item.icon);
|
|
12139
|
+
const label = escapeHtml(item.label);
|
|
12140
|
+
const targetAttr = item.target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
12141
|
+
const externalAffordance = item.isExternal ? EXTERNAL_ICON : "";
|
|
12142
|
+
return `
|
|
12143
|
+
<span class="relative">
|
|
12144
|
+
${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
|
|
12145
|
+
<a href="${item.url}"${targetAttr}
|
|
12146
|
+
class="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left text-sm/5 font-medium ${isActive ? "text-zinc-950 dark:text-white" : "text-zinc-600 hover:bg-zinc-950/5 dark:text-zinc-400 dark:hover:bg-white/5"}"
|
|
12147
|
+
${isActive ? 'data-current="true"' : ""}
|
|
12148
|
+
>
|
|
12149
|
+
<span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">${icon}</span>
|
|
12150
|
+
<span class="truncate">${label}</span>${externalAffordance}
|
|
12151
|
+
</a>
|
|
12152
|
+
</span>`;
|
|
12153
|
+
}
|
|
12154
|
+
var ACCORDION_SCRIPT = `
|
|
12155
|
+
<script>
|
|
12156
|
+
function toggleMenuAccordion(id) {
|
|
12157
|
+
var el = document.getElementById(id)
|
|
12158
|
+
if (!el) return
|
|
12159
|
+
var chevrons = el.parentElement ? el.parentElement.querySelectorAll('[data-menu-chevron]') : []
|
|
12160
|
+
if (el.style.display === 'none' || el.style.display === '') {
|
|
12161
|
+
el.style.display = 'flex'
|
|
12162
|
+
el.style.flexDirection = 'column'
|
|
12163
|
+
chevrons.forEach(function(c) { c.style.transform = 'rotate(180deg)' })
|
|
12164
|
+
} else {
|
|
12165
|
+
el.style.display = 'none'
|
|
12166
|
+
chevrons.forEach(function(c) { c.style.transform = 'rotate(0deg)' })
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
// Open active accordions on load
|
|
12170
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
12171
|
+
document.querySelectorAll('[data-menu-accordion]').forEach(function(acc) {
|
|
12172
|
+
if (acc.querySelector('[data-current]')) {
|
|
12173
|
+
var sub = acc.querySelector('[id^="acc-"]')
|
|
12174
|
+
if (sub) { sub.style.display = 'flex'; sub.style.flexDirection = 'column' }
|
|
12175
|
+
}
|
|
12176
|
+
})
|
|
12177
|
+
})
|
|
12178
|
+
</script>`;
|
|
12179
|
+
async function isMenuPluginActive(db) {
|
|
12180
|
+
try {
|
|
12181
|
+
const row = await db.prepare(
|
|
12182
|
+
`SELECT json_extract(data, '$.status') AS status
|
|
12183
|
+
FROM documents
|
|
12184
|
+
WHERE slug = 'menu' AND type_id = 'plugin' AND tenant_id = 'default'
|
|
12185
|
+
AND is_current_draft = 1 AND deleted_at IS NULL
|
|
12186
|
+
LIMIT 1`
|
|
12187
|
+
).first();
|
|
12188
|
+
if (!row) return false;
|
|
12189
|
+
return row.status === "active";
|
|
12190
|
+
} catch {
|
|
12191
|
+
return false;
|
|
12192
|
+
}
|
|
12193
|
+
}
|
|
12194
|
+
function menuMiddleware() {
|
|
12195
|
+
return async (c, next) => {
|
|
12196
|
+
const path = new URL(c.req.url).pathname;
|
|
12197
|
+
if (!path.startsWith("/admin")) {
|
|
12198
|
+
return next();
|
|
12199
|
+
}
|
|
12200
|
+
await next();
|
|
12201
|
+
if (!c.res.headers.get("content-type")?.includes("text/html")) return;
|
|
12202
|
+
if (!await isMenuPluginActive(c.env.DB)) return;
|
|
12203
|
+
let items = [];
|
|
12204
|
+
try {
|
|
12205
|
+
items = await listMenuItems(c.env.DB);
|
|
12206
|
+
} catch {
|
|
12207
|
+
return;
|
|
12208
|
+
}
|
|
12209
|
+
if (items.length === 0) return;
|
|
12210
|
+
const tree = buildSidebarTree(items);
|
|
12211
|
+
const renderedItems = tree.map((item) => renderTopLevelItem(item, path)).join("");
|
|
12212
|
+
const navItemsHtml = renderedItems + ACCORDION_SCRIPT;
|
|
12213
|
+
const status = c.res.status;
|
|
12214
|
+
const headers = new Headers(c.res.headers);
|
|
12215
|
+
const html4 = await c.res.text();
|
|
12216
|
+
const markerRegex = /<!-- ADMIN_SIDEBAR_NAV_ITEMS -->[\s\S]*?<!-- \/ADMIN_SIDEBAR_NAV_ITEMS -->/g;
|
|
12217
|
+
if (markerRegex.test(html4)) {
|
|
12218
|
+
const newHtml = html4.replace(
|
|
12219
|
+
/<!-- ADMIN_SIDEBAR_NAV_ITEMS -->[\s\S]*?<!-- \/ADMIN_SIDEBAR_NAV_ITEMS -->/g,
|
|
12220
|
+
navItemsHtml
|
|
12221
|
+
);
|
|
12222
|
+
c.res = new Response(newHtml, { status, headers });
|
|
12223
|
+
} else {
|
|
12224
|
+
c.res = new Response(html4, { status, headers });
|
|
12225
|
+
}
|
|
12226
|
+
};
|
|
12227
|
+
}
|
|
12228
|
+
|
|
12229
|
+
// src/plugins/core-plugins/menu-plugin/services/menu-defaults.ts
|
|
12230
|
+
var SYSTEM_MENU_ITEMS = [
|
|
12231
|
+
{
|
|
12232
|
+
id: "menu:system:content",
|
|
12233
|
+
label: "Content",
|
|
12234
|
+
url: "/admin/content",
|
|
12235
|
+
icon: "document",
|
|
12236
|
+
target: "_self",
|
|
12237
|
+
isExternal: false,
|
|
12238
|
+
visible: true,
|
|
12239
|
+
parent: null,
|
|
12240
|
+
source: "system",
|
|
12241
|
+
pluginId: null,
|
|
12242
|
+
permissions: [],
|
|
12243
|
+
lockedFields: ["url"],
|
|
12244
|
+
sortOrder: 10
|
|
12245
|
+
},
|
|
12246
|
+
{
|
|
12247
|
+
id: "menu:system:collections",
|
|
12248
|
+
label: "Collections",
|
|
12249
|
+
url: "/admin/collections",
|
|
12250
|
+
icon: "collection",
|
|
12251
|
+
target: "_self",
|
|
12252
|
+
isExternal: false,
|
|
12253
|
+
visible: true,
|
|
12254
|
+
parent: null,
|
|
12255
|
+
source: "system",
|
|
12256
|
+
pluginId: null,
|
|
12257
|
+
permissions: [],
|
|
12258
|
+
lockedFields: ["url"],
|
|
12259
|
+
sortOrder: 20
|
|
12260
|
+
},
|
|
12261
|
+
{
|
|
12262
|
+
id: "menu:system:users",
|
|
12263
|
+
label: "Users",
|
|
12264
|
+
url: "/admin/users",
|
|
12265
|
+
icon: "users",
|
|
12266
|
+
target: "_self",
|
|
12267
|
+
isExternal: false,
|
|
12268
|
+
visible: true,
|
|
12269
|
+
parent: null,
|
|
12270
|
+
source: "system",
|
|
12271
|
+
pluginId: null,
|
|
12272
|
+
permissions: [],
|
|
12273
|
+
lockedFields: ["url"],
|
|
12274
|
+
sortOrder: 30
|
|
12275
|
+
},
|
|
12276
|
+
{
|
|
12277
|
+
id: "menu:system:settings",
|
|
12278
|
+
label: "Settings",
|
|
12279
|
+
url: "/admin/settings",
|
|
12280
|
+
icon: "cog",
|
|
12281
|
+
target: "_self",
|
|
12282
|
+
isExternal: false,
|
|
12283
|
+
visible: true,
|
|
12284
|
+
parent: null,
|
|
12285
|
+
source: "system",
|
|
12286
|
+
pluginId: null,
|
|
12287
|
+
permissions: [],
|
|
12288
|
+
lockedFields: ["url"],
|
|
12289
|
+
sortOrder: 40
|
|
12290
|
+
},
|
|
12291
|
+
{
|
|
12292
|
+
id: "menu:system:plugins",
|
|
12293
|
+
label: "Plugins",
|
|
12294
|
+
url: "/admin/plugins",
|
|
12295
|
+
icon: "collection",
|
|
12296
|
+
target: "_self",
|
|
12297
|
+
isExternal: false,
|
|
12298
|
+
visible: true,
|
|
12299
|
+
parent: null,
|
|
12300
|
+
source: "system",
|
|
12301
|
+
pluginId: null,
|
|
12302
|
+
permissions: [],
|
|
12303
|
+
lockedFields: ["url"],
|
|
12304
|
+
sortOrder: 50
|
|
12305
|
+
},
|
|
12306
|
+
{
|
|
12307
|
+
id: "menu:system:menu",
|
|
12308
|
+
label: "Menu",
|
|
12309
|
+
url: "/admin/menu",
|
|
12310
|
+
icon: "bars-3",
|
|
12311
|
+
target: "_self",
|
|
12312
|
+
isExternal: false,
|
|
12313
|
+
visible: true,
|
|
12314
|
+
parent: null,
|
|
12315
|
+
source: "system",
|
|
12316
|
+
pluginId: null,
|
|
12317
|
+
permissions: [],
|
|
12318
|
+
lockedFields: ["url"],
|
|
12319
|
+
sortOrder: 60
|
|
12320
|
+
}
|
|
12321
|
+
];
|
|
12322
|
+
|
|
12323
|
+
// src/plugins/core-plugins/menu-plugin/templates/admin-menu-list.template.ts
|
|
12324
|
+
init_admin_layout_catalyst_template();
|
|
12325
|
+
function pluginStatusBadge(status) {
|
|
12326
|
+
if (status === "inactive") {
|
|
12327
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 ml-1">disabled</span>`;
|
|
12328
|
+
}
|
|
12329
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/20 dark:ring-green-500/20 ml-1">enabled</span>`;
|
|
12330
|
+
}
|
|
12331
|
+
function sourceBadge(source) {
|
|
12332
|
+
switch (source) {
|
|
12333
|
+
case "system":
|
|
12334
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-700/50 text-zinc-600 dark:text-zinc-300 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50">system</span>`;
|
|
12335
|
+
case "plugin":
|
|
12336
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-400/20">plugin</span>`;
|
|
12337
|
+
default:
|
|
12338
|
+
return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-300 ring-1 ring-inset ring-green-700/10 dark:ring-green-400/20">user</span>`;
|
|
12339
|
+
}
|
|
12340
|
+
}
|
|
12341
|
+
function iconPreview(icon) {
|
|
12342
|
+
if (!icon) return '<span class="text-zinc-400 dark:text-zinc-500">\u2014</span>';
|
|
12343
|
+
if (icon.trim().startsWith("<")) {
|
|
12344
|
+
return `<span class="w-5 h-5 inline-flex items-center justify-center">${icon}</span>`;
|
|
12345
|
+
}
|
|
12346
|
+
return `<span class="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[80px]">${escapeHtml(icon)}</span>`;
|
|
12347
|
+
}
|
|
12348
|
+
function renderMenuListPage(data) {
|
|
12349
|
+
const messageBanner = data.message ? `<div class="mb-6 rounded-lg bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-200 dark:border-cyan-700 px-4 py-3 text-sm text-cyan-800 dark:text-cyan-200">
|
|
12350
|
+
${escapeHtml(data.message)}
|
|
12351
|
+
</div>` : "";
|
|
12352
|
+
const pluginStatuses = data.pluginStatuses ?? {};
|
|
12353
|
+
const rows = data.items.map((item, index) => {
|
|
12354
|
+
const isFirst = index === 0;
|
|
12355
|
+
const isLast = index === data.items.length - 1;
|
|
12356
|
+
const moveUpForm = !isFirst ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-up" class="inline">
|
|
12357
|
+
<button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move up">
|
|
12358
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12359
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
|
|
12360
|
+
</svg>
|
|
12361
|
+
</button>
|
|
12362
|
+
</form>` : `<span class="w-6 inline-block"></span>`;
|
|
12363
|
+
const moveDownForm = !isLast ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-down" class="inline">
|
|
12364
|
+
<button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move down">
|
|
12365
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12366
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
12367
|
+
</svg>
|
|
12368
|
+
</button>
|
|
12369
|
+
</form>` : `<span class="w-6 inline-block"></span>`;
|
|
12370
|
+
const deleteButton = item.source === "user" ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/delete" onsubmit="return confirm('Delete this item?')" class="inline">
|
|
12371
|
+
<button type="submit" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 hover:bg-red-100 dark:hover:bg-red-500/20 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 transition-colors">
|
|
12372
|
+
Delete
|
|
12373
|
+
</button>
|
|
12374
|
+
</form>` : "";
|
|
12375
|
+
return `
|
|
12376
|
+
<tr class="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30 transition-colors cursor-pointer" onclick="if(!event.target.closest('a,button,input,form')){window.location='/admin/menu/${escapeHtml(item.id)}'}">
|
|
12377
|
+
<td class="px-4 py-3 w-16">
|
|
12378
|
+
<div class="flex items-center gap-0.5">
|
|
12379
|
+
${moveUpForm}
|
|
12380
|
+
${moveDownForm}
|
|
12381
|
+
</div>
|
|
12382
|
+
</td>
|
|
12383
|
+
<td class="px-4 py-3 w-16">
|
|
12384
|
+
${iconPreview(item.icon)}
|
|
12385
|
+
</td>
|
|
12386
|
+
<td class="px-4 py-3">
|
|
12387
|
+
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">${escapeHtml(item.label)}</span>
|
|
12388
|
+
${item.parent ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">child</span>` : ""}
|
|
12389
|
+
</td>
|
|
12390
|
+
<td class="px-4 py-3 max-w-[200px]">
|
|
12391
|
+
<span class="text-sm text-zinc-500 dark:text-zinc-400 truncate block">${escapeHtml(item.url)}</span>
|
|
12392
|
+
</td>
|
|
12393
|
+
<td class="px-4 py-3">
|
|
12394
|
+
${sourceBadge(item.source)}
|
|
12395
|
+
${item.source === "plugin" && item.pluginId ? pluginStatusBadge(pluginStatuses[item.pluginId] ?? "active") : ""}
|
|
12396
|
+
</td>
|
|
12397
|
+
<td class="px-4 py-3">
|
|
12398
|
+
<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/visibility" class="inline">
|
|
12399
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
12400
|
+
<input
|
|
12401
|
+
type="checkbox"
|
|
12402
|
+
name="visible"
|
|
12403
|
+
value="true"
|
|
12404
|
+
${item.visible ? "checked" : ""}
|
|
12405
|
+
onchange="this.form.requestSubmit()"
|
|
12406
|
+
class="sr-only peer"
|
|
12407
|
+
>
|
|
12408
|
+
<div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
|
|
12409
|
+
</label>
|
|
12410
|
+
</form>
|
|
12411
|
+
</td>
|
|
12412
|
+
<td class="px-4 py-3">
|
|
12413
|
+
<div class="flex items-center gap-2">
|
|
12414
|
+
<a href="/admin/menu/${escapeHtml(item.id)}" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50 transition-colors">
|
|
12415
|
+
Edit
|
|
12416
|
+
</a>
|
|
12417
|
+
${deleteButton}
|
|
12418
|
+
</div>
|
|
12419
|
+
</td>
|
|
12420
|
+
</tr>`;
|
|
12421
|
+
}).join("\n");
|
|
12422
|
+
const emptyState = data.items.length === 0 ? `<tr>
|
|
12423
|
+
<td colspan="7" class="px-4 py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">
|
|
12424
|
+
No menu items yet. <a href="/admin/menu/new" class="text-cyan-600 dark:text-cyan-400 underline hover:no-underline">Add a link</a> to get started.
|
|
12425
|
+
</td>
|
|
12426
|
+
</tr>` : "";
|
|
12427
|
+
const pageContent = `
|
|
12428
|
+
<div>
|
|
12429
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
|
|
12430
|
+
<div>
|
|
12431
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Menu Manager</h1>
|
|
12432
|
+
<p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage navigation links and their order</p>
|
|
12433
|
+
</div>
|
|
12434
|
+
<div class="mt-4 sm:mt-0">
|
|
12435
|
+
<a href="/admin/menu/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
12436
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
12437
|
+
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/>
|
|
12438
|
+
</svg>
|
|
12439
|
+
Add Link
|
|
12440
|
+
</a>
|
|
12441
|
+
</div>
|
|
12442
|
+
</div>
|
|
12443
|
+
|
|
12444
|
+
${messageBanner}
|
|
12445
|
+
|
|
12446
|
+
<div class="rounded-xl overflow-hidden bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
12447
|
+
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
|
12448
|
+
<thead>
|
|
12449
|
+
<tr class="bg-zinc-50 dark:bg-zinc-800/50">
|
|
12450
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Order</th>
|
|
12451
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Icon</th>
|
|
12452
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Label</th>
|
|
12453
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">URL</th>
|
|
12454
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Source</th>
|
|
12455
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Visible</th>
|
|
12456
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
|
|
12457
|
+
</tr>
|
|
12458
|
+
</thead>
|
|
12459
|
+
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
|
12460
|
+
${rows}
|
|
12461
|
+
${emptyState}
|
|
12462
|
+
</tbody>
|
|
12463
|
+
</table>
|
|
12464
|
+
</div>
|
|
12465
|
+
</div>
|
|
12466
|
+
`;
|
|
12467
|
+
const layoutData = {
|
|
12468
|
+
title: "Menu Manager",
|
|
12469
|
+
pageTitle: "Menu Manager",
|
|
12470
|
+
currentPath: data.currentPath ?? "/admin/menu",
|
|
12471
|
+
user: data.user,
|
|
12472
|
+
version: data.version,
|
|
12473
|
+
dynamicMenuItems: data.dynamicMenuItems,
|
|
12474
|
+
content: pageContent
|
|
12475
|
+
};
|
|
12476
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
12477
|
+
}
|
|
12478
|
+
function renderMenuSettingsContent(items, pluginStatuses = {}, message) {
|
|
12479
|
+
const messageBanner = "";
|
|
12480
|
+
const rows = items.map((item, index) => {
|
|
12481
|
+
const isFirst = index === 0;
|
|
12482
|
+
const isLast = index === items.length - 1;
|
|
12483
|
+
const moveUpForm = !isFirst ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-up" class="inline">
|
|
12484
|
+
<button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move up">
|
|
12485
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/></svg>
|
|
12486
|
+
</button>
|
|
12487
|
+
</form>` : `<span class="w-6 inline-block"></span>`;
|
|
12488
|
+
const moveDownForm = !isLast ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/move-down" class="inline">
|
|
12489
|
+
<button type="submit" class="p-1 rounded hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors" title="Move down">
|
|
12490
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
12491
|
+
</button>
|
|
12492
|
+
</form>` : `<span class="w-6 inline-block"></span>`;
|
|
12493
|
+
const deleteButton = item.source === "user" ? `<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/delete" onsubmit="return confirm('Delete this item?')" class="inline">
|
|
12494
|
+
<button type="submit" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-500/10 hover:bg-red-100 dark:hover:bg-red-500/20 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/20 transition-colors">Delete</button>
|
|
12495
|
+
</form>` : "";
|
|
12496
|
+
return `
|
|
12497
|
+
<tr class="border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30 transition-colors cursor-pointer" onclick="if(!event.target.closest('a,button,input,form')){window.location='/admin/menu/${escapeHtml(item.id)}'}">
|
|
12498
|
+
<td class="px-4 py-3 w-16">
|
|
12499
|
+
<div class="flex items-center gap-0.5">${moveUpForm}${moveDownForm}</div>
|
|
12500
|
+
</td>
|
|
12501
|
+
<td class="px-4 py-3 w-16">${iconPreview(item.icon)}</td>
|
|
12502
|
+
<td class="px-4 py-3">
|
|
12503
|
+
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">${escapeHtml(item.label)}</span>
|
|
12504
|
+
${item.parent ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">child</span>` : ""}
|
|
12505
|
+
</td>
|
|
12506
|
+
<td class="px-4 py-3 max-w-[200px]">
|
|
12507
|
+
<span class="text-sm text-zinc-500 dark:text-zinc-400 truncate block">${escapeHtml(item.url)}</span>
|
|
12508
|
+
</td>
|
|
12509
|
+
<td class="px-4 py-3">${sourceBadge(item.source)}${item.source === "plugin" && item.pluginId ? pluginStatusBadge(pluginStatuses[item.pluginId] ?? "active") : ""}</td>
|
|
12510
|
+
<td class="px-4 py-3">
|
|
12511
|
+
<form method="POST" action="/admin/menu/${escapeHtml(item.id)}/visibility" class="inline">
|
|
12512
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
12513
|
+
<input type="checkbox" name="visible" value="true" ${item.visible ? "checked" : ""} onchange="this.form.requestSubmit()" class="sr-only peer">
|
|
12514
|
+
<div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
|
|
12515
|
+
</label>
|
|
12516
|
+
</form>
|
|
12517
|
+
</td>
|
|
12518
|
+
<td class="px-4 py-3">
|
|
12519
|
+
<div class="flex items-center gap-2">
|
|
12520
|
+
<a href="/admin/menu/${escapeHtml(item.id)}" class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded-md text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 ring-1 ring-inset ring-zinc-300/50 dark:ring-zinc-600/50 transition-colors">Edit</a>
|
|
12521
|
+
${deleteButton}
|
|
12522
|
+
</div>
|
|
12523
|
+
</td>
|
|
12524
|
+
</tr>`;
|
|
12525
|
+
}).join("\n");
|
|
12526
|
+
const emptyState = items.length === 0 ? `<tr><td colspan="7" class="px-4 py-12 text-center text-sm text-zinc-400 dark:text-zinc-500">No menu items yet. <a href="/admin/menu/new" class="text-cyan-600 dark:text-cyan-400 underline hover:no-underline">Add a link</a> to get started.</td></tr>` : "";
|
|
12527
|
+
return `
|
|
12528
|
+
<script>
|
|
12529
|
+
(function() {
|
|
12530
|
+
function getCsrfToken() {
|
|
12531
|
+
var c = document.cookie.split('; ').find(function(r) { return r.startsWith('csrf_token='); });
|
|
12532
|
+
return c ? c.substring(c.indexOf('=') + 1) : '';
|
|
12533
|
+
}
|
|
12534
|
+
document.addEventListener('submit', function(e) {
|
|
12535
|
+
var form = e.target;
|
|
12536
|
+
if (!form || form.tagName !== 'FORM') return;
|
|
12537
|
+
if ((form.method || 'GET').toUpperCase() === 'GET') return;
|
|
12538
|
+
if (!form.querySelector('input[name="_csrf"]')) {
|
|
12539
|
+
var input = document.createElement('input');
|
|
12540
|
+
input.type = 'hidden';
|
|
12541
|
+
input.name = '_csrf';
|
|
12542
|
+
input.value = getCsrfToken();
|
|
12543
|
+
form.appendChild(input);
|
|
12544
|
+
}
|
|
12545
|
+
});
|
|
12546
|
+
})();
|
|
12547
|
+
</script>
|
|
12548
|
+
<div>
|
|
12549
|
+
<div class="flex items-center justify-between mb-4">
|
|
12550
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400">Manage navigation links and their order in the sidebar.</p>
|
|
12551
|
+
<a href="/admin/menu/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
|
|
12552
|
+
<svg class="-ml-0.5 mr-1.5 h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/></svg>
|
|
12553
|
+
Add Link
|
|
12554
|
+
</a>
|
|
12555
|
+
</div>
|
|
12556
|
+
${messageBanner}
|
|
12557
|
+
<div class="rounded-xl overflow-hidden bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
12558
|
+
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
|
|
12559
|
+
<thead>
|
|
12560
|
+
<tr class="bg-zinc-50 dark:bg-zinc-800/50">
|
|
12561
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Order</th>
|
|
12562
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider w-16">Icon</th>
|
|
12563
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Label</th>
|
|
12564
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">URL</th>
|
|
12565
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Source</th>
|
|
12566
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Visible</th>
|
|
12567
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Actions</th>
|
|
12568
|
+
</tr>
|
|
12569
|
+
</thead>
|
|
12570
|
+
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
|
12571
|
+
${rows}
|
|
12572
|
+
${emptyState}
|
|
12573
|
+
</tbody>
|
|
12574
|
+
</table>
|
|
12575
|
+
</div>
|
|
12576
|
+
</div>
|
|
12577
|
+
`;
|
|
12578
|
+
}
|
|
12579
|
+
|
|
12580
|
+
// src/plugins/core-plugins/menu-plugin/templates/admin-menu-form.template.ts
|
|
12581
|
+
init_admin_layout_catalyst_template();
|
|
12582
|
+
|
|
12583
|
+
// src/plugins/core-plugins/menu-plugin/templates/icon-picker.template.ts
|
|
12584
|
+
var ICONS = [
|
|
12585
|
+
{
|
|
12586
|
+
name: "puzzle-piece",
|
|
12587
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"/>'
|
|
12588
|
+
},
|
|
12589
|
+
{
|
|
12590
|
+
name: "document",
|
|
12591
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>'
|
|
12592
|
+
},
|
|
12593
|
+
{
|
|
12594
|
+
name: "collection",
|
|
12595
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>'
|
|
12596
|
+
},
|
|
12597
|
+
{
|
|
12598
|
+
name: "users",
|
|
12599
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>'
|
|
12600
|
+
},
|
|
12601
|
+
{
|
|
12602
|
+
name: "cog",
|
|
12603
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>'
|
|
12604
|
+
},
|
|
12605
|
+
{
|
|
12606
|
+
name: "envelope",
|
|
12607
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>'
|
|
12608
|
+
},
|
|
12609
|
+
{
|
|
12610
|
+
name: "chart",
|
|
12611
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>'
|
|
12612
|
+
},
|
|
12613
|
+
{
|
|
12614
|
+
name: "bolt",
|
|
12615
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>'
|
|
12616
|
+
},
|
|
12617
|
+
{
|
|
12618
|
+
name: "sparkles",
|
|
12619
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/>'
|
|
12620
|
+
},
|
|
12621
|
+
{
|
|
12622
|
+
name: "photo",
|
|
12623
|
+
svg: '<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"/>'
|
|
12624
|
+
},
|
|
12625
|
+
{
|
|
12626
|
+
name: "lock",
|
|
12627
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>'
|
|
12628
|
+
},
|
|
12629
|
+
{
|
|
12630
|
+
name: "magnifying-glass",
|
|
12631
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>'
|
|
12632
|
+
},
|
|
12633
|
+
{
|
|
12634
|
+
name: "chart-bar",
|
|
12635
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>'
|
|
12636
|
+
},
|
|
12637
|
+
{
|
|
12638
|
+
name: "image",
|
|
12639
|
+
svg: '<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"/>'
|
|
12640
|
+
},
|
|
12641
|
+
{
|
|
12642
|
+
name: "key",
|
|
12643
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>'
|
|
12644
|
+
},
|
|
12645
|
+
{
|
|
12646
|
+
name: "shield-check",
|
|
12647
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>'
|
|
12648
|
+
},
|
|
12649
|
+
{
|
|
12650
|
+
name: "credit-card",
|
|
12651
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>'
|
|
12652
|
+
},
|
|
12653
|
+
{
|
|
12654
|
+
name: "document-text",
|
|
12655
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>'
|
|
12656
|
+
},
|
|
12657
|
+
{
|
|
12658
|
+
name: "pencil-square",
|
|
12659
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>'
|
|
12660
|
+
},
|
|
12661
|
+
{
|
|
12662
|
+
name: "server",
|
|
12663
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>'
|
|
12664
|
+
},
|
|
12665
|
+
{
|
|
12666
|
+
name: "building-office",
|
|
12667
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>'
|
|
12668
|
+
},
|
|
12669
|
+
{
|
|
12670
|
+
name: "home",
|
|
12671
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>'
|
|
12672
|
+
},
|
|
12673
|
+
{
|
|
12674
|
+
name: "star",
|
|
12675
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>'
|
|
12676
|
+
},
|
|
12677
|
+
{
|
|
12678
|
+
name: "tag",
|
|
12679
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>'
|
|
12680
|
+
},
|
|
12681
|
+
{
|
|
12682
|
+
name: "link",
|
|
12683
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>'
|
|
12684
|
+
},
|
|
12685
|
+
{
|
|
12686
|
+
name: "external-link",
|
|
12687
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>'
|
|
12688
|
+
},
|
|
12689
|
+
{
|
|
12690
|
+
name: "eye",
|
|
12691
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>'
|
|
12692
|
+
},
|
|
12693
|
+
{
|
|
12694
|
+
name: "bars-3",
|
|
12695
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>'
|
|
12696
|
+
},
|
|
12697
|
+
{
|
|
12698
|
+
name: "arrow-right",
|
|
12699
|
+
svg: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>'
|
|
12700
|
+
}
|
|
12701
|
+
];
|
|
12702
|
+
function renderIconPicker(selectedIcon) {
|
|
12703
|
+
const safeSelected = escapeHtml(selectedIcon);
|
|
12704
|
+
const iconButtons = ICONS.map(({ name, svg }) => {
|
|
12705
|
+
const isSelected = name === selectedIcon;
|
|
12706
|
+
const selectedClasses = isSelected ? "ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900/30" : "ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800";
|
|
12707
|
+
return `
|
|
12708
|
+
<button
|
|
12709
|
+
type="button"
|
|
12710
|
+
data-icon-name="${name}"
|
|
12711
|
+
onclick="selectIcon('${name}')"
|
|
12712
|
+
class="icon-btn flex flex-col items-center gap-1 p-2 rounded-lg transition-all duration-150 ${selectedClasses}"
|
|
12713
|
+
title="${name}"
|
|
12714
|
+
>
|
|
12715
|
+
<svg class="w-6 h-6 text-zinc-700 dark:text-zinc-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12716
|
+
${svg}
|
|
12717
|
+
</svg>
|
|
12718
|
+
<span class="text-[10px] text-zinc-500 dark:text-zinc-400 truncate max-w-[56px] text-center leading-tight">${name}</span>
|
|
12719
|
+
</button>`;
|
|
12720
|
+
}).join("\n");
|
|
12721
|
+
return `
|
|
12722
|
+
<div class="space-y-4">
|
|
12723
|
+
<input type="hidden" name="icon" id="icon-input" value="${safeSelected}">
|
|
12724
|
+
|
|
12725
|
+
<div>
|
|
12726
|
+
<p class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Choose an icon</p>
|
|
12727
|
+
<div class="grid grid-cols-6 sm:grid-cols-8 md:grid-cols-10 gap-2 max-h-64 overflow-y-auto p-1 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900">
|
|
12728
|
+
${iconButtons}
|
|
12729
|
+
</div>
|
|
12730
|
+
</div>
|
|
12731
|
+
|
|
12732
|
+
<div>
|
|
12733
|
+
<label class="text-sm font-medium text-zinc-700 dark:text-zinc-300 block mb-1">Custom SVG (overrides selection above)</label>
|
|
12734
|
+
<textarea
|
|
12735
|
+
id="custom-svg-input"
|
|
12736
|
+
rows="3"
|
|
12737
|
+
placeholder='<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>'
|
|
12738
|
+
oninput="handleCustomSvg(this.value)"
|
|
12739
|
+
class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 font-mono"
|
|
12740
|
+
></textarea>
|
|
12741
|
+
<p class="mt-1 text-xs text-zinc-400 dark:text-zinc-500">Paste raw SVG markup to use a custom icon.</p>
|
|
12742
|
+
</div>
|
|
12743
|
+
</div>
|
|
12744
|
+
|
|
12745
|
+
<script>
|
|
12746
|
+
(function () {
|
|
12747
|
+
function selectIcon(name) {
|
|
12748
|
+
document.getElementById('icon-input').value = name;
|
|
12749
|
+
document.getElementById('custom-svg-input').value = '';
|
|
12750
|
+
document.querySelectorAll('.icon-btn').forEach(function (btn) {
|
|
12751
|
+
var isSelected = btn.getAttribute('data-icon-name') === name;
|
|
12752
|
+
btn.className = btn.className
|
|
12753
|
+
.replace(/ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900\\/30/g, '')
|
|
12754
|
+
.replace(/ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800/g, '')
|
|
12755
|
+
.trim();
|
|
12756
|
+
if (isSelected) {
|
|
12757
|
+
btn.className += ' ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900/30';
|
|
12758
|
+
} else {
|
|
12759
|
+
btn.className += ' ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800';
|
|
12760
|
+
}
|
|
12761
|
+
});
|
|
12762
|
+
}
|
|
12763
|
+
|
|
12764
|
+
function handleCustomSvg(value) {
|
|
12765
|
+
var trimmed = value.trim();
|
|
12766
|
+
if (trimmed) {
|
|
12767
|
+
document.getElementById('icon-input').value = trimmed;
|
|
12768
|
+
document.querySelectorAll('.icon-btn').forEach(function (btn) {
|
|
12769
|
+
btn.className = btn.className
|
|
12770
|
+
.replace(/ring-2 ring-cyan-500 bg-cyan-50 dark:bg-cyan-900\\/30/g, '')
|
|
12771
|
+
.replace(/ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800/g, '')
|
|
12772
|
+
.trim();
|
|
12773
|
+
btn.className += ' ring-1 ring-zinc-200 dark:ring-zinc-700 hover:ring-cyan-300 dark:hover:ring-cyan-600 hover:bg-zinc-50 dark:hover:bg-zinc-800';
|
|
12774
|
+
});
|
|
12775
|
+
}
|
|
12776
|
+
}
|
|
12777
|
+
|
|
12778
|
+
window.selectIcon = selectIcon;
|
|
12779
|
+
window.handleCustomSvg = handleCustomSvg;
|
|
12780
|
+
})();
|
|
12781
|
+
</script>
|
|
12782
|
+
`;
|
|
12783
|
+
}
|
|
12784
|
+
|
|
12785
|
+
// src/plugins/core-plugins/menu-plugin/templates/admin-menu-form.template.ts
|
|
12786
|
+
function renderMenuFormPage(data) {
|
|
12787
|
+
const { item, topLevelItems } = data;
|
|
12788
|
+
const isEdit = item !== null;
|
|
12789
|
+
const errorBanner = data.error ? `<div class="mb-6 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-4 py-3 text-sm text-red-800 dark:text-red-300">
|
|
12790
|
+
${escapeHtml(data.error)}
|
|
12791
|
+
</div>` : "";
|
|
12792
|
+
const parentOptions = topLevelItems.filter((t) => !isEdit || t.id !== item?.id).sort((a, b) => a.label.localeCompare(b.label)).map((t) => {
|
|
12793
|
+
const selected = item?.parent === t.id ? "selected" : "";
|
|
12794
|
+
return `<option value="${escapeHtml(t.id)}" ${selected}>${escapeHtml(t.label)}</option>`;
|
|
12795
|
+
}).join("\n");
|
|
12796
|
+
const targetSelfSelected = !item || item.target === "_self" ? "selected" : "";
|
|
12797
|
+
const targetBlankSelected = item?.target === "_blank" ? "selected" : "";
|
|
12798
|
+
const visibleChecked = item ? item.visible ? "checked" : "" : "checked";
|
|
12799
|
+
const formAttrs = isEdit ? `method="POST" action="/admin/menu/${escapeHtml(item.id)}/update"` : `method="POST" action="/admin/menu"`;
|
|
12800
|
+
const submitLabel = isEdit ? "Save Changes" : "Add Item";
|
|
12801
|
+
const pageTitle = isEdit ? `Edit: ${escapeHtml(item.label)}` : "Add Menu Item";
|
|
12802
|
+
const pageContent = `
|
|
12803
|
+
<div class="max-w-2xl">
|
|
12804
|
+
<div class="mb-6">
|
|
12805
|
+
<a href="/admin/menu" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
|
|
12806
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
12807
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
12808
|
+
</svg>
|
|
12809
|
+
Back to Menu Manager
|
|
12810
|
+
</a>
|
|
12811
|
+
<h1 class="mt-3 text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">${pageTitle}</h1>
|
|
12812
|
+
</div>
|
|
12813
|
+
|
|
12814
|
+
${errorBanner}
|
|
12815
|
+
|
|
12816
|
+
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
|
|
12817
|
+
<form ${formAttrs} class="space-y-6">
|
|
12818
|
+
|
|
12819
|
+
<div>
|
|
12820
|
+
<label for="menu-label" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
|
12821
|
+
Label <span class="text-red-500">*</span>
|
|
12822
|
+
</label>
|
|
12823
|
+
<input
|
|
12824
|
+
id="menu-label"
|
|
12825
|
+
type="text"
|
|
12826
|
+
name="label"
|
|
12827
|
+
required
|
|
12828
|
+
value="${isEdit ? escapeHtml(item.label) : ""}"
|
|
12829
|
+
placeholder="e.g. Documentation"
|
|
12830
|
+
class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
|
|
12831
|
+
>
|
|
12832
|
+
</div>
|
|
12833
|
+
|
|
12834
|
+
<div>
|
|
12835
|
+
<label for="menu-url" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">URL</label>
|
|
12836
|
+
<input
|
|
12837
|
+
id="menu-url"
|
|
12838
|
+
type="text"
|
|
12839
|
+
name="url"
|
|
12840
|
+
value="${isEdit ? escapeHtml(item.url) : ""}"
|
|
12841
|
+
placeholder="/path/to/page or https://example.com"
|
|
12842
|
+
class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
|
|
12843
|
+
>
|
|
12844
|
+
</div>
|
|
12845
|
+
|
|
12846
|
+
<div class="grid grid-cols-2 gap-4">
|
|
12847
|
+
<div>
|
|
12848
|
+
<label for="menu-target" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">Open in</label>
|
|
12849
|
+
<select
|
|
12850
|
+
id="menu-target"
|
|
12851
|
+
name="target"
|
|
12852
|
+
class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
|
|
12853
|
+
>
|
|
12854
|
+
<option value="_self" ${targetSelfSelected}>Same tab</option>
|
|
12855
|
+
<option value="_blank" ${targetBlankSelected}>New tab</option>
|
|
12856
|
+
</select>
|
|
12857
|
+
</div>
|
|
12858
|
+
|
|
12859
|
+
<div>
|
|
12860
|
+
<label for="menu-parent" class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">Parent</label>
|
|
12861
|
+
<select
|
|
12862
|
+
id="menu-parent"
|
|
12863
|
+
name="parent"
|
|
12864
|
+
class="w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-cyan-500 transition-shadow"
|
|
12865
|
+
>
|
|
12866
|
+
<option value="">Top level (none)</option>
|
|
12867
|
+
${parentOptions}
|
|
12868
|
+
</select>
|
|
12869
|
+
</div>
|
|
12870
|
+
</div>
|
|
12871
|
+
|
|
12872
|
+
<div class="flex items-center gap-3">
|
|
12873
|
+
<label class="relative inline-flex items-center cursor-pointer">
|
|
12874
|
+
<input
|
|
12875
|
+
id="menu-visible"
|
|
12876
|
+
type="checkbox"
|
|
12877
|
+
name="visible"
|
|
12878
|
+
value="1"
|
|
12879
|
+
${visibleChecked}
|
|
12880
|
+
class="sr-only peer"
|
|
12881
|
+
>
|
|
12882
|
+
<div class="w-9 h-5 bg-zinc-200 dark:bg-zinc-700 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-cyan-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-cyan-600"></div>
|
|
12883
|
+
</label>
|
|
12884
|
+
<label for="menu-visible" class="text-sm font-medium text-zinc-700 dark:text-zinc-300 cursor-pointer">Visible in navigation</label>
|
|
12885
|
+
</div>
|
|
12886
|
+
|
|
12887
|
+
<div>
|
|
12888
|
+
<p class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Icon</p>
|
|
12889
|
+
${renderIconPicker(item?.icon ?? "link")}
|
|
12890
|
+
</div>
|
|
12891
|
+
|
|
12892
|
+
<div class="flex items-center justify-end gap-3 pt-2 border-t border-zinc-100 dark:border-zinc-800">
|
|
12893
|
+
<a href="/admin/menu" class="inline-flex items-center px-3.5 py-2 text-sm font-medium rounded-lg text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors">
|
|
12894
|
+
Cancel
|
|
12895
|
+
</a>
|
|
12896
|
+
<button
|
|
12897
|
+
type="submit"
|
|
12898
|
+
class="inline-flex items-center px-3.5 py-2 text-sm font-semibold rounded-lg bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
|
|
12899
|
+
>
|
|
12900
|
+
${submitLabel}
|
|
12901
|
+
</button>
|
|
12902
|
+
</div>
|
|
12903
|
+
|
|
12904
|
+
</form>
|
|
12905
|
+
</div>
|
|
12906
|
+
</div>
|
|
12907
|
+
`;
|
|
12908
|
+
const layoutData = {
|
|
12909
|
+
title: isEdit ? "Edit Menu Item" : "Add Menu Item",
|
|
12910
|
+
pageTitle: isEdit ? "Edit Menu Item" : "Add Menu Item",
|
|
12911
|
+
currentPath: data.currentPath ?? "/admin/menu",
|
|
12912
|
+
user: data.user,
|
|
12913
|
+
version: data.version,
|
|
12914
|
+
dynamicMenuItems: data.dynamicMenuItems,
|
|
12915
|
+
content: pageContent
|
|
12916
|
+
};
|
|
12917
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
12918
|
+
}
|
|
12919
|
+
|
|
12920
|
+
// src/plugins/core-plugins/menu-plugin/routes/admin-menu.ts
|
|
12921
|
+
function sanitizeUrl(url) {
|
|
12922
|
+
const trimmed = url.trim();
|
|
12923
|
+
if (/^javascript:/i.test(trimmed) || /^data:/i.test(trimmed)) return "";
|
|
12924
|
+
return trimmed;
|
|
12925
|
+
}
|
|
12926
|
+
var adminMenuRoutes = new Hono();
|
|
12927
|
+
adminMenuRoutes.use("*", requireAuth());
|
|
12928
|
+
adminMenuRoutes.use("*", requireRole(["admin"]));
|
|
12929
|
+
adminMenuRoutes.get("/", async (c) => {
|
|
12930
|
+
const db = c.env.DB;
|
|
12931
|
+
const user = c.get("user");
|
|
12932
|
+
const items = await listMenuItems(db);
|
|
12933
|
+
buildSidebarTree(items);
|
|
12934
|
+
const pluginIds = [...new Set(items.filter((i) => i.pluginId).map((i) => i.pluginId))];
|
|
12935
|
+
const pluginStatuses = await fetchPluginStatuses(db, pluginIds);
|
|
12936
|
+
return c.html(renderMenuListPage({
|
|
12937
|
+
items,
|
|
12938
|
+
pluginStatuses,
|
|
12939
|
+
user,
|
|
12940
|
+
currentPath: "/admin/menu",
|
|
12941
|
+
version: c.get("appVersion"),
|
|
12942
|
+
dynamicMenuItems: c.get("pluginMenuItems"),
|
|
12943
|
+
message: c.req.query("message")
|
|
12944
|
+
}));
|
|
12945
|
+
});
|
|
12946
|
+
adminMenuRoutes.get("/new", async (c) => {
|
|
12947
|
+
const db = c.env.DB;
|
|
12948
|
+
const topLevelItems = await listMenuItems(db);
|
|
12949
|
+
return c.html(renderMenuFormPage({
|
|
12950
|
+
item: null,
|
|
12951
|
+
topLevelItems: topLevelItems.filter((i) => !i.parent),
|
|
12952
|
+
user: c.get("user"),
|
|
12953
|
+
currentPath: "/admin/menu",
|
|
12954
|
+
version: c.get("appVersion"),
|
|
12955
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
12956
|
+
}));
|
|
12957
|
+
});
|
|
12958
|
+
adminMenuRoutes.get("/:id", async (c) => {
|
|
12959
|
+
const db = c.env.DB;
|
|
12960
|
+
const id = c.req.param("id");
|
|
12961
|
+
const items = await listMenuItems(db);
|
|
12962
|
+
const item = items.find((i) => i.id === id);
|
|
12963
|
+
if (!item) return c.notFound();
|
|
12964
|
+
const topLevelItems = items.filter((i) => !i.parent && i.id !== id);
|
|
12965
|
+
return c.html(renderMenuFormPage({
|
|
12966
|
+
item,
|
|
12967
|
+
topLevelItems,
|
|
12968
|
+
user: c.get("user"),
|
|
12969
|
+
currentPath: "/admin/menu",
|
|
12970
|
+
version: c.get("appVersion"),
|
|
12971
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
12972
|
+
}));
|
|
12973
|
+
});
|
|
12974
|
+
adminMenuRoutes.post("/", async (c) => {
|
|
12975
|
+
const db = c.env.DB;
|
|
12976
|
+
const form = await c.req.formData();
|
|
12977
|
+
const label = String(form.get("label") ?? "").trim();
|
|
12978
|
+
const url = String(form.get("url") ?? "").trim();
|
|
12979
|
+
const icon = String(form.get("icon") ?? "link").trim();
|
|
12980
|
+
const target = form.get("target") === "_blank" ? "_blank" : "_self";
|
|
12981
|
+
const parent = String(form.get("parent") ?? "").trim() || null;
|
|
12982
|
+
const visible = form.has("visible");
|
|
12983
|
+
if (!label) {
|
|
12984
|
+
return c.html(renderMenuFormPage({
|
|
12985
|
+
item: null,
|
|
12986
|
+
topLevelItems: [],
|
|
12987
|
+
user: c.get("user"),
|
|
12988
|
+
currentPath: "/admin/menu",
|
|
12989
|
+
version: c.get("appVersion"),
|
|
12990
|
+
dynamicMenuItems: c.get("pluginMenuItems"),
|
|
12991
|
+
error: "Label is required"
|
|
12992
|
+
}), 400);
|
|
12993
|
+
}
|
|
12994
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
12995
|
+
const id = nanoid();
|
|
12996
|
+
const slug = `menu:user:${id}`;
|
|
12997
|
+
const safeUrl = sanitizeUrl(url);
|
|
12998
|
+
const isExternal = /^https?:\/\//.test(safeUrl);
|
|
12999
|
+
const data = JSON.stringify({
|
|
13000
|
+
label,
|
|
13001
|
+
url: safeUrl,
|
|
13002
|
+
icon,
|
|
13003
|
+
target,
|
|
13004
|
+
isExternal,
|
|
13005
|
+
visible,
|
|
13006
|
+
parent,
|
|
13007
|
+
source: "user",
|
|
13008
|
+
pluginId: null,
|
|
13009
|
+
permissions: [],
|
|
13010
|
+
lockedFields: []
|
|
13011
|
+
});
|
|
13012
|
+
const items = await listMenuItems(db);
|
|
13013
|
+
const maxSort = items.reduce((m, i) => Math.max(m, i.sortOrder), 0);
|
|
13014
|
+
const sortOrder = maxSort + 10;
|
|
13015
|
+
await db.prepare(
|
|
13016
|
+
`INSERT INTO documents (id, root_id, type_id, type_version, version_of_id, version_number,
|
|
13017
|
+
is_current_draft, is_published, status, parent_root_id, slug, path, title, zone,
|
|
13018
|
+
sort_order, visible, published_at, scheduled_at, expires_at, deleted_at,
|
|
13019
|
+
tenant_id, locale, translation_group_id, data, metadata,
|
|
13020
|
+
owner_id, created_by, updated_by, created_at, updated_at)
|
|
13021
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
|
13022
|
+
).bind(
|
|
13023
|
+
id,
|
|
13024
|
+
id,
|
|
13025
|
+
"menu_item",
|
|
13026
|
+
1,
|
|
13027
|
+
null,
|
|
13028
|
+
1,
|
|
13029
|
+
1,
|
|
13030
|
+
1,
|
|
13031
|
+
"published",
|
|
13032
|
+
"",
|
|
13033
|
+
slug,
|
|
13034
|
+
null,
|
|
13035
|
+
label,
|
|
13036
|
+
null,
|
|
13037
|
+
sortOrder,
|
|
13038
|
+
visible ? 1 : 0,
|
|
13039
|
+
now,
|
|
13040
|
+
null,
|
|
13041
|
+
null,
|
|
13042
|
+
null,
|
|
13043
|
+
"default",
|
|
13044
|
+
"default",
|
|
13045
|
+
"",
|
|
13046
|
+
data,
|
|
13047
|
+
"{}",
|
|
13048
|
+
null,
|
|
13049
|
+
c.get("user")?.userId ?? "system",
|
|
13050
|
+
c.get("user")?.userId ?? "system",
|
|
13051
|
+
now,
|
|
13052
|
+
now
|
|
13053
|
+
).run();
|
|
13054
|
+
return c.redirect("/admin/menu?message=Item+created");
|
|
13055
|
+
});
|
|
13056
|
+
adminMenuRoutes.put("/reorder", async (c) => {
|
|
13057
|
+
const db = c.env.DB;
|
|
13058
|
+
const body = await c.req.json();
|
|
13059
|
+
if (!Array.isArray(body)) return c.json({ error: "Invalid body" }, 400);
|
|
13060
|
+
await reorderItems(db, body);
|
|
13061
|
+
return c.json({ ok: true });
|
|
13062
|
+
});
|
|
13063
|
+
adminMenuRoutes.post("/:id/visibility", async (c) => {
|
|
13064
|
+
const db = c.env.DB;
|
|
13065
|
+
const id = c.req.param("id");
|
|
13066
|
+
const form = await c.req.formData();
|
|
13067
|
+
const visible = form.get("visible") === "true";
|
|
13068
|
+
await toggleVisibility(db, id, visible);
|
|
13069
|
+
return c.redirect("/admin/menu?message=Visibility+updated");
|
|
13070
|
+
});
|
|
13071
|
+
async function handleMenuItemUpdate(c) {
|
|
13072
|
+
const db = c.env.DB;
|
|
13073
|
+
const id = c.req.param("id");
|
|
13074
|
+
const form = await c.req.formData();
|
|
13075
|
+
const items = await listMenuItems(db);
|
|
13076
|
+
const item = items.find((i) => i.id === id);
|
|
13077
|
+
if (!item) return c.json({ error: "Not found" }, 404);
|
|
13078
|
+
const locked = new Set(item.lockedFields);
|
|
13079
|
+
const changes = {};
|
|
13080
|
+
if (form.has("label") && !locked.has("label")) changes.label = String(form.get("label")).trim();
|
|
13081
|
+
if (form.has("icon") && !locked.has("icon")) changes.icon = String(form.get("icon")).trim();
|
|
13082
|
+
if (form.has("target") && !locked.has("target")) changes.target = form.get("target") === "_blank" ? "_blank" : "_self";
|
|
13083
|
+
if (form.has("url") && !locked.has("url")) changes.url = sanitizeUrl(String(form.get("url")));
|
|
13084
|
+
if (form.has("parent")) changes.parent = String(form.get("parent")).trim() || null;
|
|
13085
|
+
changes.visible = form.has("visible");
|
|
13086
|
+
const ok = await updateItem(db, id, changes, item.lockedFields);
|
|
13087
|
+
if (!ok) return c.json({ error: "Cannot modify locked fields" }, 403);
|
|
13088
|
+
return c.redirect("/admin/menu?message=Item+updated");
|
|
13089
|
+
}
|
|
13090
|
+
adminMenuRoutes.put("/:id", handleMenuItemUpdate);
|
|
13091
|
+
adminMenuRoutes.post("/:id/update", handleMenuItemUpdate);
|
|
13092
|
+
adminMenuRoutes.delete("/:id", async (c) => {
|
|
13093
|
+
const db = c.env.DB;
|
|
13094
|
+
const id = c.req.param("id");
|
|
13095
|
+
const result = await deleteItem(db, id);
|
|
13096
|
+
if (!result.ok) return c.json({ error: result.reason }, 403);
|
|
13097
|
+
return c.redirect("/admin/menu?message=Item+deleted");
|
|
13098
|
+
});
|
|
13099
|
+
adminMenuRoutes.post("/:id/delete", async (c) => {
|
|
13100
|
+
const db = c.env.DB;
|
|
13101
|
+
const id = c.req.param("id");
|
|
13102
|
+
const result = await deleteItem(db, id);
|
|
13103
|
+
if (!result.ok) return c.json({ error: result.reason }, 403);
|
|
13104
|
+
return c.redirect("/admin/menu?message=Item+deleted");
|
|
13105
|
+
});
|
|
13106
|
+
adminMenuRoutes.post("/:id/move-up", async (c) => {
|
|
13107
|
+
const db = c.env.DB;
|
|
13108
|
+
const id = c.req.param("id");
|
|
13109
|
+
const allItems = await listMenuItems(db);
|
|
13110
|
+
const cur = allItems.find((i) => i.id === id);
|
|
13111
|
+
if (!cur) return c.redirect("/admin/menu");
|
|
13112
|
+
const siblings = allItems.filter((i) => i.parent === cur.parent);
|
|
13113
|
+
const idx = siblings.findIndex((i) => i.id === id);
|
|
13114
|
+
const prev = siblings[idx - 1];
|
|
13115
|
+
if (idx > 0 && prev) {
|
|
13116
|
+
await reorderItems(db, [
|
|
13117
|
+
{ id: cur.id, sortOrder: prev.sortOrder, parent: cur.parent },
|
|
13118
|
+
{ id: prev.id, sortOrder: cur.sortOrder, parent: prev.parent }
|
|
13119
|
+
]);
|
|
13120
|
+
return c.redirect("/admin/menu?message=Reordered");
|
|
13121
|
+
}
|
|
13122
|
+
return c.redirect("/admin/menu");
|
|
13123
|
+
});
|
|
13124
|
+
adminMenuRoutes.post("/:id/move-down", async (c) => {
|
|
13125
|
+
const db = c.env.DB;
|
|
13126
|
+
const id = c.req.param("id");
|
|
13127
|
+
const allItems = await listMenuItems(db);
|
|
13128
|
+
const cur = allItems.find((i) => i.id === id);
|
|
13129
|
+
if (!cur) return c.redirect("/admin/menu");
|
|
13130
|
+
const siblings = allItems.filter((i) => i.parent === cur.parent);
|
|
13131
|
+
const idx = siblings.findIndex((i) => i.id === id);
|
|
13132
|
+
const next = siblings[idx + 1];
|
|
13133
|
+
if (idx !== -1 && idx < siblings.length - 1 && next) {
|
|
13134
|
+
await reorderItems(db, [
|
|
13135
|
+
{ id: cur.id, sortOrder: next.sortOrder, parent: cur.parent },
|
|
13136
|
+
{ id: next.id, sortOrder: cur.sortOrder, parent: next.parent }
|
|
13137
|
+
]);
|
|
13138
|
+
return c.redirect("/admin/menu?message=Reordered");
|
|
13139
|
+
}
|
|
13140
|
+
return c.redirect("/admin/menu");
|
|
13141
|
+
});
|
|
13142
|
+
var menuPlugin = definePlugin({
|
|
13143
|
+
id: "menu",
|
|
13144
|
+
version: "1.0.0",
|
|
13145
|
+
name: "Menu Manager",
|
|
13146
|
+
description: "Admin sidebar navigation manager.",
|
|
13147
|
+
sonicjsVersionRange: "^3.0.0",
|
|
13148
|
+
author: { name: "SonicJS Team", email: "team@sonicjs.com" },
|
|
13149
|
+
register(app2) {
|
|
13150
|
+
app2.route("/admin/menu", adminMenuRoutes);
|
|
13151
|
+
},
|
|
13152
|
+
async onBoot(ctx) {
|
|
13153
|
+
const env = ctx.env ?? {};
|
|
13154
|
+
const db = env.DB;
|
|
13155
|
+
if (!db) return;
|
|
13156
|
+
try {
|
|
13157
|
+
const typeRegistry = new DocumentTypeRegistry(db);
|
|
13158
|
+
await typeRegistry.register({
|
|
13159
|
+
id: "menu_item",
|
|
13160
|
+
name: "menu_item",
|
|
13161
|
+
displayName: "Menu Item",
|
|
13162
|
+
description: "Admin sidebar navigation item",
|
|
13163
|
+
schema: z.object({}),
|
|
13164
|
+
pluginId: "menu",
|
|
13165
|
+
source: "plugin",
|
|
13166
|
+
queryableFields: [
|
|
13167
|
+
{ name: "parent", path: "$.parent", kind: "scalar", type: "text", column: "q_menu_parent" },
|
|
13168
|
+
{ name: "visible", path: "$.visible", kind: "scalar", type: "boolean", column: "q_menu_visible" },
|
|
13169
|
+
{ name: "source", path: "$.source", kind: "scalar", type: "text", column: "q_menu_source" },
|
|
13170
|
+
{ name: "pluginId", path: "$.pluginId", kind: "scalar", type: "text", column: "q_menu_plugin_id" }
|
|
13171
|
+
],
|
|
13172
|
+
settings: {
|
|
13173
|
+
baseGrants: { admin: ["read", "create", "update", "delete", "manage"] },
|
|
13174
|
+
internal: true
|
|
13175
|
+
}
|
|
13176
|
+
});
|
|
13177
|
+
for (const item of SYSTEM_MENU_ITEMS) {
|
|
13178
|
+
await upsertSystemItem(db, item.id, {
|
|
13179
|
+
label: item.label,
|
|
13180
|
+
url: item.url,
|
|
13181
|
+
icon: item.icon,
|
|
13182
|
+
target: item.target,
|
|
13183
|
+
isExternal: item.isExternal,
|
|
13184
|
+
visible: item.visible,
|
|
13185
|
+
parent: item.parent,
|
|
13186
|
+
source: item.source,
|
|
13187
|
+
pluginId: item.pluginId,
|
|
13188
|
+
permissions: [...item.permissions],
|
|
13189
|
+
lockedFields: [...item.lockedFields]
|
|
13190
|
+
}, item.sortOrder);
|
|
13191
|
+
}
|
|
13192
|
+
await reconcileMenuFromPlugins(db);
|
|
13193
|
+
} catch {
|
|
13194
|
+
}
|
|
13195
|
+
},
|
|
13196
|
+
settingsTabContent: {
|
|
13197
|
+
async loadData(db) {
|
|
13198
|
+
const items = await listMenuItems(db);
|
|
13199
|
+
const pluginIds = [...new Set(items.filter((i) => i.pluginId).map((i) => i.pluginId))];
|
|
13200
|
+
const pluginStatuses = await fetchPluginStatuses(db, pluginIds);
|
|
13201
|
+
return { items, pluginStatuses };
|
|
13202
|
+
},
|
|
13203
|
+
render({ data }) {
|
|
13204
|
+
const items = data?.items ?? [];
|
|
13205
|
+
const pluginStatuses = data?.pluginStatuses ?? {};
|
|
13206
|
+
return renderMenuSettingsContent(items, pluginStatuses);
|
|
13207
|
+
}
|
|
13208
|
+
}
|
|
13209
|
+
});
|
|
11780
13210
|
init_admin_layout_catalyst_template();
|
|
11781
13211
|
var adminRoutes6 = new Hono();
|
|
11782
13212
|
adminRoutes6.use("*", requireAuth());
|
|
@@ -16762,7 +18192,8 @@ function createSonicJSApp(config = {}) {
|
|
|
16762
18192
|
helloWorldPlugin,
|
|
16763
18193
|
multiTenantPlugin,
|
|
16764
18194
|
lexicalEditorPlugin,
|
|
16765
|
-
versioningPlugin
|
|
18195
|
+
versioningPlugin,
|
|
18196
|
+
menuPlugin
|
|
16766
18197
|
];
|
|
16767
18198
|
const corePluginsAfterCatchAll = [emailPluginV3, magicLinkPlugin, emailReconciliationPlugin];
|
|
16768
18199
|
const wirePlugins = createPluginWirer(
|
|
@@ -16900,6 +18331,7 @@ function createSonicJSApp(config = {}) {
|
|
|
16900
18331
|
app2.use("/admin/*", requireAuth());
|
|
16901
18332
|
app2.use("/admin/*", requireRbac("portal", "access"));
|
|
16902
18333
|
app2.use("/admin/*", pluginMenuMiddleware());
|
|
18334
|
+
app2.use("/admin/*", menuMiddleware());
|
|
16903
18335
|
const NAV_LANDING = [
|
|
16904
18336
|
{ path: "/admin/content", perm: "content:read" },
|
|
16905
18337
|
{ path: "/admin/media", perm: "media:read" },
|
|
@@ -20619,10 +22051,837 @@ var site_settings_collection_default = {
|
|
|
20619
22051
|
function createDb(d1) {
|
|
20620
22052
|
return drizzle(d1, { schema: schema_exports });
|
|
20621
22053
|
}
|
|
22054
|
+
var mcpTypeConfigSchema = z.object({
|
|
22055
|
+
read: z.boolean().default(true),
|
|
22056
|
+
write: z.boolean().default(true)
|
|
22057
|
+
});
|
|
22058
|
+
var mcpConfigSchema = z.object({
|
|
22059
|
+
/** Collections exposed over MCP. Omitted → every active collection. */
|
|
22060
|
+
expose: z.array(z.string()).optional(),
|
|
22061
|
+
/** Per-collection read/write overrides, keyed by collection name. */
|
|
22062
|
+
types: z.record(z.string(), mcpTypeConfigSchema).default({}),
|
|
22063
|
+
/** Field names stripped from every tool response + write schema. */
|
|
22064
|
+
redactFields: z.array(z.string()).default([]),
|
|
22065
|
+
/** Max documents returned by list_* tools and document resources. */
|
|
22066
|
+
listLimit: z.number().int().positive().max(200).default(50)
|
|
22067
|
+
});
|
|
22068
|
+
function resolveMcpConfig(raw, collections) {
|
|
22069
|
+
const cfg = mcpConfigSchema.parse(raw ?? {});
|
|
22070
|
+
const exposeSet = cfg.expose ? new Set(cfg.expose) : null;
|
|
22071
|
+
const types = collections.filter((c) => exposeSet ? exposeSet.has(c.name) : true).map((c) => {
|
|
22072
|
+
const override = cfg.types[c.name];
|
|
22073
|
+
return {
|
|
22074
|
+
typeId: c.name,
|
|
22075
|
+
displayName: c.displayName ?? c.name,
|
|
22076
|
+
read: override?.read ?? true,
|
|
22077
|
+
write: override?.write ?? true
|
|
22078
|
+
};
|
|
22079
|
+
});
|
|
22080
|
+
return { types, redactFields: cfg.redactFields, listLimit: cfg.listLimit };
|
|
22081
|
+
}
|
|
22082
|
+
|
|
22083
|
+
// src/plugins/core-plugins/mcp-plugin/schema/field-to-jsonschema.ts
|
|
22084
|
+
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
22085
|
+
"id",
|
|
22086
|
+
"root_id",
|
|
22087
|
+
"rootId",
|
|
22088
|
+
"created_at",
|
|
22089
|
+
"createdAt",
|
|
22090
|
+
"updated_at",
|
|
22091
|
+
"updatedAt",
|
|
22092
|
+
"version_number",
|
|
22093
|
+
"versionNumber"
|
|
22094
|
+
]);
|
|
22095
|
+
function fieldToJsonSchema(field) {
|
|
22096
|
+
const base = {};
|
|
22097
|
+
if (field.title) base.title = field.title;
|
|
22098
|
+
if (field.description) base.description = field.description;
|
|
22099
|
+
switch (field.type) {
|
|
22100
|
+
case "number":
|
|
22101
|
+
return {
|
|
22102
|
+
...base,
|
|
22103
|
+
type: "number",
|
|
22104
|
+
...field.min != null ? { minimum: field.min } : {},
|
|
22105
|
+
...field.max != null ? { maximum: field.max } : {}
|
|
22106
|
+
};
|
|
22107
|
+
case "boolean":
|
|
22108
|
+
case "checkbox":
|
|
22109
|
+
return { ...base, type: "boolean" };
|
|
22110
|
+
case "date":
|
|
22111
|
+
case "datetime":
|
|
22112
|
+
return { ...base, type: "string", format: "date-time" };
|
|
22113
|
+
case "select":
|
|
22114
|
+
case "radio":
|
|
22115
|
+
return { ...base, type: "string", ...field.enum ? { enum: field.enum } : {} };
|
|
22116
|
+
case "multiselect":
|
|
22117
|
+
return {
|
|
22118
|
+
...base,
|
|
22119
|
+
type: "array",
|
|
22120
|
+
items: { type: "string", ...field.enum ? { enum: field.enum } : {} }
|
|
22121
|
+
};
|
|
22122
|
+
case "array":
|
|
22123
|
+
return {
|
|
22124
|
+
...base,
|
|
22125
|
+
type: "array",
|
|
22126
|
+
items: field.items ? fieldToJsonSchema(field.items) : {}
|
|
22127
|
+
};
|
|
22128
|
+
case "object":
|
|
22129
|
+
return {
|
|
22130
|
+
...base,
|
|
22131
|
+
type: "object",
|
|
22132
|
+
...field.properties ? { properties: mapProperties(field.properties) } : {}
|
|
22133
|
+
};
|
|
22134
|
+
case "json":
|
|
22135
|
+
return { ...base };
|
|
22136
|
+
// string-like: string, slug, email, url, richtext, markdown, textarea, editors,
|
|
22137
|
+
// reference, media, color, file, user
|
|
22138
|
+
default: {
|
|
22139
|
+
const node = { ...base, type: "string" };
|
|
22140
|
+
if (field.minLength != null) node.minLength = field.minLength;
|
|
22141
|
+
if (field.maxLength != null) node.maxLength = field.maxLength;
|
|
22142
|
+
if (field.pattern) node.pattern = field.pattern;
|
|
22143
|
+
if (field.format) node.format = field.format;
|
|
22144
|
+
return node;
|
|
22145
|
+
}
|
|
22146
|
+
}
|
|
22147
|
+
}
|
|
22148
|
+
function mapProperties(props) {
|
|
22149
|
+
const out = {};
|
|
22150
|
+
for (const [name, field] of Object.entries(props)) {
|
|
22151
|
+
out[name] = fieldToJsonSchema(field);
|
|
22152
|
+
}
|
|
22153
|
+
return out;
|
|
22154
|
+
}
|
|
22155
|
+
function collectionToJsonSchema(schema, opts = {}) {
|
|
22156
|
+
const properties = {};
|
|
22157
|
+
const required = [];
|
|
22158
|
+
const redact2 = new Set(opts.redactFields ?? []);
|
|
22159
|
+
const schemaRequired = new Set(schema.required ?? []);
|
|
22160
|
+
for (const [name, field] of Object.entries(schema.properties ?? {})) {
|
|
22161
|
+
if (redact2.has(name)) continue;
|
|
22162
|
+
if (opts.forWrite && SYSTEM_FIELDS.has(name)) continue;
|
|
22163
|
+
properties[name] = fieldToJsonSchema(field);
|
|
22164
|
+
if (field.required || schemaRequired.has(name)) required.push(name);
|
|
22165
|
+
}
|
|
22166
|
+
return { type: "object", properties, ...required.length ? { required } : {} };
|
|
22167
|
+
}
|
|
22168
|
+
|
|
22169
|
+
// src/plugins/core-plugins/mcp-plugin/tools/registry.ts
|
|
22170
|
+
var PHASE_FLAGS = { includeWrite: true, includeSearch: false };
|
|
22171
|
+
var EMPTY_OBJECT_SCHEMA = { type: "object", properties: {} };
|
|
22172
|
+
function buildToolRegistry(cfg, collections, opts = {}) {
|
|
22173
|
+
const tools = [];
|
|
22174
|
+
tools.push({
|
|
22175
|
+
name: "list_collections",
|
|
22176
|
+
description: "List every content collection exposed over MCP, with its read/write access.",
|
|
22177
|
+
inputSchema: EMPTY_OBJECT_SCHEMA,
|
|
22178
|
+
op: "list_collections"
|
|
22179
|
+
});
|
|
22180
|
+
if (opts.includeSearch) {
|
|
22181
|
+
tools.push({
|
|
22182
|
+
name: "search_content",
|
|
22183
|
+
description: "Search content across exposed collections by title or slug.",
|
|
22184
|
+
inputSchema: {
|
|
22185
|
+
type: "object",
|
|
22186
|
+
properties: { query: { type: "string", description: "Text matched against document title and slug." } },
|
|
22187
|
+
required: ["query"]
|
|
22188
|
+
},
|
|
22189
|
+
op: "search_content"
|
|
22190
|
+
});
|
|
22191
|
+
}
|
|
22192
|
+
for (const t of cfg.types) {
|
|
22193
|
+
const col = collections.get(t.typeId);
|
|
22194
|
+
const writeSchema = col ? collectionToJsonSchema(col.schema, { forWrite: true, redactFields: cfg.redactFields }) : EMPTY_OBJECT_SCHEMA;
|
|
22195
|
+
if (t.read) {
|
|
22196
|
+
tools.push({
|
|
22197
|
+
name: `list_${t.typeId}`,
|
|
22198
|
+
description: `List ${t.displayName} documents.`,
|
|
22199
|
+
inputSchema: {
|
|
22200
|
+
type: "object",
|
|
22201
|
+
properties: {
|
|
22202
|
+
status: {
|
|
22203
|
+
type: "string",
|
|
22204
|
+
enum: ["published", "draft"],
|
|
22205
|
+
description: "Lifecycle to list. Defaults to published."
|
|
22206
|
+
},
|
|
22207
|
+
limit: { type: "number", description: `Max documents to return (capped at ${cfg.listLimit}).` }
|
|
22208
|
+
}
|
|
22209
|
+
},
|
|
22210
|
+
op: "list",
|
|
22211
|
+
typeId: t.typeId
|
|
22212
|
+
});
|
|
22213
|
+
tools.push({
|
|
22214
|
+
name: `get_${t.typeId}`,
|
|
22215
|
+
description: `Get one ${t.displayName} document by id or slug.`,
|
|
22216
|
+
inputSchema: {
|
|
22217
|
+
type: "object",
|
|
22218
|
+
properties: {
|
|
22219
|
+
id: { type: "string", description: "Document id or root id." },
|
|
22220
|
+
slug: { type: "string", description: "Document slug (alternative to id)." }
|
|
22221
|
+
}
|
|
22222
|
+
},
|
|
22223
|
+
op: "get",
|
|
22224
|
+
typeId: t.typeId
|
|
22225
|
+
});
|
|
22226
|
+
}
|
|
22227
|
+
if (opts.includeWrite && t.write) {
|
|
22228
|
+
tools.push({
|
|
22229
|
+
name: `create_${t.typeId}`,
|
|
22230
|
+
description: `Create a ${t.displayName} document.`,
|
|
22231
|
+
inputSchema: {
|
|
22232
|
+
type: "object",
|
|
22233
|
+
properties: {
|
|
22234
|
+
title: { type: "string" },
|
|
22235
|
+
slug: { type: "string" },
|
|
22236
|
+
data: writeSchema,
|
|
22237
|
+
publish: { type: "boolean", description: "Publish immediately. Defaults to false (draft)." }
|
|
22238
|
+
}
|
|
22239
|
+
},
|
|
22240
|
+
op: "create",
|
|
22241
|
+
typeId: t.typeId
|
|
22242
|
+
});
|
|
22243
|
+
tools.push({
|
|
22244
|
+
name: `update_${t.typeId}`,
|
|
22245
|
+
description: `Update a ${t.displayName} document (saves a new draft).`,
|
|
22246
|
+
inputSchema: {
|
|
22247
|
+
type: "object",
|
|
22248
|
+
properties: {
|
|
22249
|
+
id: { type: "string", description: "Root id of the document to update." },
|
|
22250
|
+
title: { type: "string" },
|
|
22251
|
+
slug: { type: "string" },
|
|
22252
|
+
data: writeSchema
|
|
22253
|
+
},
|
|
22254
|
+
required: ["id"]
|
|
22255
|
+
},
|
|
22256
|
+
op: "update",
|
|
22257
|
+
typeId: t.typeId
|
|
22258
|
+
});
|
|
22259
|
+
tools.push({
|
|
22260
|
+
name: `publish_${t.typeId}`,
|
|
22261
|
+
description: `Publish a ${t.displayName} document.`,
|
|
22262
|
+
inputSchema: {
|
|
22263
|
+
type: "object",
|
|
22264
|
+
properties: { id: { type: "string", description: "Root id of the document to publish." } },
|
|
22265
|
+
required: ["id"]
|
|
22266
|
+
},
|
|
22267
|
+
op: "publish",
|
|
22268
|
+
typeId: t.typeId
|
|
22269
|
+
});
|
|
22270
|
+
tools.push({
|
|
22271
|
+
name: `delete_${t.typeId}`,
|
|
22272
|
+
description: `Delete a ${t.displayName} document.`,
|
|
22273
|
+
inputSchema: {
|
|
22274
|
+
type: "object",
|
|
22275
|
+
properties: { id: { type: "string", description: "Root id of the document to delete." } },
|
|
22276
|
+
required: ["id"]
|
|
22277
|
+
},
|
|
22278
|
+
op: "delete",
|
|
22279
|
+
typeId: t.typeId
|
|
22280
|
+
});
|
|
22281
|
+
}
|
|
22282
|
+
}
|
|
22283
|
+
return tools;
|
|
22284
|
+
}
|
|
22285
|
+
|
|
22286
|
+
// src/plugins/core-plugins/mcp-plugin/jsonrpc.ts
|
|
22287
|
+
var JSON_RPC = {
|
|
22288
|
+
PARSE_ERROR: -32700,
|
|
22289
|
+
INVALID_REQUEST: -32600,
|
|
22290
|
+
METHOD_NOT_FOUND: -32601,
|
|
22291
|
+
INVALID_PARAMS: -32602,
|
|
22292
|
+
INTERNAL_ERROR: -32603,
|
|
22293
|
+
/** Custom: no valid API key on the request. */
|
|
22294
|
+
UNAUTHORIZED: -32001
|
|
22295
|
+
};
|
|
22296
|
+
var McpToolError = class extends Error {
|
|
22297
|
+
constructor(message) {
|
|
22298
|
+
super(message);
|
|
22299
|
+
this.name = "McpToolError";
|
|
22300
|
+
}
|
|
22301
|
+
};
|
|
22302
|
+
function rpcResult(id, result) {
|
|
22303
|
+
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
22304
|
+
}
|
|
22305
|
+
function rpcError(id, code, message, data) {
|
|
22306
|
+
return { jsonrpc: "2.0", id: id ?? null, error: { code, message, ...{} } };
|
|
22307
|
+
}
|
|
22308
|
+
function parseJsonRpc(body) {
|
|
22309
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
22310
|
+
return { error: { code: JSON_RPC.INVALID_REQUEST, message: "Request must be a JSON object" } };
|
|
22311
|
+
}
|
|
22312
|
+
const b = body;
|
|
22313
|
+
if (b.jsonrpc !== "2.0") {
|
|
22314
|
+
return { error: { code: JSON_RPC.INVALID_REQUEST, message: 'jsonrpc must be "2.0"' } };
|
|
22315
|
+
}
|
|
22316
|
+
if (typeof b.method !== "string" || b.method.length === 0) {
|
|
22317
|
+
return { error: { code: JSON_RPC.INVALID_REQUEST, message: "method must be a non-empty string" } };
|
|
22318
|
+
}
|
|
22319
|
+
const id = b.id === void 0 ? null : b.id;
|
|
22320
|
+
const params = b.params && typeof b.params === "object" && !Array.isArray(b.params) ? b.params : {};
|
|
22321
|
+
return { jsonrpc: "2.0", id, method: b.method, params };
|
|
22322
|
+
}
|
|
22323
|
+
|
|
22324
|
+
// src/plugins/core-plugins/mcp-plugin/tools/documents.ts
|
|
22325
|
+
function redact(data, fields) {
|
|
22326
|
+
if (!fields.length) return data;
|
|
22327
|
+
const out = { ...data };
|
|
22328
|
+
for (const f of fields) delete out[f];
|
|
22329
|
+
return out;
|
|
22330
|
+
}
|
|
22331
|
+
function shapeDocument(doc, redactFields) {
|
|
22332
|
+
return {
|
|
22333
|
+
id: doc.id,
|
|
22334
|
+
rootId: doc.rootId,
|
|
22335
|
+
typeId: doc.typeId,
|
|
22336
|
+
title: doc.title,
|
|
22337
|
+
slug: doc.slug,
|
|
22338
|
+
status: doc.status,
|
|
22339
|
+
isPublished: doc.isPublished,
|
|
22340
|
+
publishedAt: doc.publishedAt,
|
|
22341
|
+
updatedAt: doc.updatedAt,
|
|
22342
|
+
data: redact(doc.data, redactFields)
|
|
22343
|
+
};
|
|
22344
|
+
}
|
|
22345
|
+
async function resolveType(ctx, typeId) {
|
|
22346
|
+
const docType = await new DocumentTypeRegistry(ctx.db).findById(typeId);
|
|
22347
|
+
if (!docType || !docType.isActive) throw new McpToolError(`Unknown document type: ${typeId}`);
|
|
22348
|
+
const effTenant = effectiveTenantForType(ctx.tenantId, docType.settings);
|
|
22349
|
+
const repo = new DocumentRepository(ctx.db, effTenant);
|
|
22350
|
+
return { docType, repo };
|
|
22351
|
+
}
|
|
22352
|
+
async function execList(ctx, typeId, args) {
|
|
22353
|
+
const { docType, repo } = await resolveType(ctx, typeId);
|
|
22354
|
+
const status = args.status === "draft" ? "draft" : "published";
|
|
22355
|
+
const limit = Math.min(Math.max(1, args.limit ?? ctx.listLimit), ctx.listLimit);
|
|
22356
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
22357
|
+
const docs = await repo.list({
|
|
22358
|
+
typeId,
|
|
22359
|
+
status,
|
|
22360
|
+
timeWindow: status === "published",
|
|
22361
|
+
now,
|
|
22362
|
+
limit
|
|
22363
|
+
});
|
|
22364
|
+
const allowed = await Promise.all(
|
|
22365
|
+
docs.map((d) => repo.isAllowed(ctx.principalSet, d.rootId, "read", docType.settings))
|
|
22366
|
+
);
|
|
22367
|
+
return docs.filter((_, i) => allowed[i]).map((d) => shapeDocument(d, ctx.redactFields));
|
|
22368
|
+
}
|
|
22369
|
+
async function execGet(ctx, typeId, args) {
|
|
22370
|
+
const { docType, repo } = await resolveType(ctx, typeId);
|
|
22371
|
+
let doc = null;
|
|
22372
|
+
if (args.id) {
|
|
22373
|
+
doc = await repo.getById(args.id) ?? await repo.getPublished(args.id) ?? await repo.getCurrentDraft(args.id);
|
|
22374
|
+
} else if (args.slug) {
|
|
22375
|
+
doc = await repo.getBySlug(typeId, args.slug);
|
|
22376
|
+
} else {
|
|
22377
|
+
throw new McpToolError('get requires an "id" or "slug" argument');
|
|
22378
|
+
}
|
|
22379
|
+
if (!doc || doc.typeId !== typeId) throw new McpToolError("Document not found");
|
|
22380
|
+
const ok = await repo.isAllowed(ctx.principalSet, doc.rootId, "read", docType.settings);
|
|
22381
|
+
if (!ok) throw new McpToolError("Document not found");
|
|
22382
|
+
return shapeDocument(doc, ctx.redactFields);
|
|
22383
|
+
}
|
|
22384
|
+
|
|
22385
|
+
// src/plugins/core-plugins/mcp-plugin/tools/mutations.ts
|
|
22386
|
+
async function resolveWriteScope(ctx, typeId) {
|
|
22387
|
+
const docType = await new DocumentTypeRegistry(ctx.db).findById(typeId);
|
|
22388
|
+
if (!docType || !docType.isActive) throw new McpToolError(`Unknown document type: ${typeId}`);
|
|
22389
|
+
const effTenant = effectiveTenantForType(ctx.tenantId, docType.settings);
|
|
22390
|
+
const repo = new DocumentRepository(ctx.db, effTenant);
|
|
22391
|
+
const svc = new DocumentsService(ctx.db, {
|
|
22392
|
+
queryableFields: docType.queryableFields ?? [],
|
|
22393
|
+
typeSchemaVersion: docType.schemaVersion,
|
|
22394
|
+
maxVersionsPerRoot: docType.settings?.maxVersionsPerRoot,
|
|
22395
|
+
tenantId: effTenant,
|
|
22396
|
+
versioning: docType.settings?.versioning ?? false
|
|
22397
|
+
});
|
|
22398
|
+
return { docType, effTenant, repo, svc };
|
|
22399
|
+
}
|
|
22400
|
+
async function assertAllowed(scope, ctx, rootId, permission) {
|
|
22401
|
+
const ok = await scope.repo.isAllowed(ctx.principalSet, rootId, permission, scope.docType.settings ?? {});
|
|
22402
|
+
if (!ok) throw new McpToolError(`Permission denied: cannot ${permission} ${scope.docType.id}`);
|
|
22403
|
+
}
|
|
22404
|
+
function applyUserFieldDefaults(data, typeId, userId) {
|
|
22405
|
+
const col = getCollectionRegistry().getByName(typeId);
|
|
22406
|
+
if (!col?.schema?.properties) return data;
|
|
22407
|
+
const props = col.schema.properties;
|
|
22408
|
+
const patched = { ...data };
|
|
22409
|
+
for (const [name, field] of Object.entries(props)) {
|
|
22410
|
+
if (field.type === "user" && !patched[name]) patched[name] = userId;
|
|
22411
|
+
}
|
|
22412
|
+
return patched;
|
|
22413
|
+
}
|
|
22414
|
+
async function execCreate(ctx, typeId, args) {
|
|
22415
|
+
const scope = await resolveWriteScope(ctx, typeId);
|
|
22416
|
+
await assertAllowed(scope, ctx, "", "create");
|
|
22417
|
+
const data = redact(applyUserFieldDefaults(args.data ?? {}, typeId, ctx.userId), ctx.redactFields);
|
|
22418
|
+
const input = createDocumentSchema.parse({
|
|
22419
|
+
typeId,
|
|
22420
|
+
tenantId: scope.effTenant,
|
|
22421
|
+
title: args.title ?? null,
|
|
22422
|
+
slug: args.slug ?? null,
|
|
22423
|
+
data,
|
|
22424
|
+
ownerId: ctx.userId,
|
|
22425
|
+
publishOnCreate: Boolean(args.publish)
|
|
22426
|
+
});
|
|
22427
|
+
const doc = await scope.svc.create(input, ctx.userId);
|
|
22428
|
+
return shapeDocument(doc, ctx.redactFields);
|
|
22429
|
+
}
|
|
22430
|
+
async function execUpdate(ctx, typeId, args) {
|
|
22431
|
+
if (!args.id) throw new McpToolError('update requires an "id" (root id)');
|
|
22432
|
+
const scope = await resolveWriteScope(ctx, typeId);
|
|
22433
|
+
const current = await scope.repo.getCurrentDraft(args.id);
|
|
22434
|
+
if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
|
|
22435
|
+
await assertAllowed(scope, ctx, args.id, "update");
|
|
22436
|
+
const patch = updateDocumentSchema.parse({
|
|
22437
|
+
...args.title !== void 0 ? { title: args.title } : {},
|
|
22438
|
+
...args.slug !== void 0 ? { slug: args.slug } : {},
|
|
22439
|
+
...args.data !== void 0 ? { data: redact(args.data, ctx.redactFields) } : {}
|
|
22440
|
+
});
|
|
22441
|
+
const doc = await scope.svc.saveDraft(args.id, patch, ctx.userId);
|
|
22442
|
+
return shapeDocument(doc, ctx.redactFields);
|
|
22443
|
+
}
|
|
22444
|
+
async function execPublish(ctx, typeId, args) {
|
|
22445
|
+
if (!args.id) throw new McpToolError('publish requires an "id" (root id)');
|
|
22446
|
+
const scope = await resolveWriteScope(ctx, typeId);
|
|
22447
|
+
const current = await scope.repo.getCurrentDraft(args.id);
|
|
22448
|
+
if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
|
|
22449
|
+
await assertAllowed(scope, ctx, args.id, "publish");
|
|
22450
|
+
const doc = await scope.svc.publish(current.id, ctx.userId);
|
|
22451
|
+
return shapeDocument(doc, ctx.redactFields);
|
|
22452
|
+
}
|
|
22453
|
+
async function execDelete(ctx, typeId, args) {
|
|
22454
|
+
if (!args.id) throw new McpToolError('delete requires an "id" (root id)');
|
|
22455
|
+
const scope = await resolveWriteScope(ctx, typeId);
|
|
22456
|
+
const current = await scope.repo.getCurrentDraft(args.id) ?? await scope.repo.getPublished(args.id);
|
|
22457
|
+
if (!current || current.typeId !== typeId) throw new McpToolError("Document not found");
|
|
22458
|
+
await assertAllowed(scope, ctx, args.id, "delete");
|
|
22459
|
+
if (scope.docType.settings?.pii) {
|
|
22460
|
+
await scope.svc.erase(args.id, scope.effTenant);
|
|
22461
|
+
return { rootId: args.id, deleted: true, erased: true };
|
|
22462
|
+
}
|
|
22463
|
+
const ids = /* @__PURE__ */ new Set();
|
|
22464
|
+
ids.add(current.id);
|
|
22465
|
+
if (current.isCurrentDraft) {
|
|
22466
|
+
const published = await scope.repo.getPublished(args.id);
|
|
22467
|
+
if (published) ids.add(published.id);
|
|
22468
|
+
} else {
|
|
22469
|
+
const draft = await scope.repo.getCurrentDraft(args.id);
|
|
22470
|
+
if (draft) ids.add(draft.id);
|
|
22471
|
+
}
|
|
22472
|
+
for (const id of ids) await scope.svc.softDelete(id);
|
|
22473
|
+
return { rootId: args.id, deleted: true, erased: false };
|
|
22474
|
+
}
|
|
22475
|
+
|
|
22476
|
+
// src/plugins/core-plugins/mcp-plugin/tools/static.ts
|
|
22477
|
+
function execListCollections(types) {
|
|
22478
|
+
return types.map((t) => ({ typeId: t.typeId, displayName: t.displayName, read: t.read, write: t.write }));
|
|
22479
|
+
}
|
|
22480
|
+
|
|
22481
|
+
// src/plugins/core-plugins/mcp-plugin/resources/schemas.ts
|
|
22482
|
+
function buildResourceList(cfg) {
|
|
22483
|
+
const out = [
|
|
22484
|
+
{ uri: "sonicjs://collections", name: "All collections", mimeType: "application/json" }
|
|
22485
|
+
];
|
|
22486
|
+
for (const t of cfg.types) {
|
|
22487
|
+
out.push({
|
|
22488
|
+
uri: `sonicjs://collections/${t.typeId}/schema`,
|
|
22489
|
+
name: `${t.displayName} schema`,
|
|
22490
|
+
mimeType: "application/json"
|
|
22491
|
+
});
|
|
22492
|
+
}
|
|
22493
|
+
return out;
|
|
22494
|
+
}
|
|
22495
|
+
var SCHEMA_URI = /^sonicjs:\/\/collections\/([^/]+)\/schema$/;
|
|
22496
|
+
function readResource(uri, cfg, collections) {
|
|
22497
|
+
if (uri === "sonicjs://collections") {
|
|
22498
|
+
const list = cfg.types.map((t) => ({
|
|
22499
|
+
typeId: t.typeId,
|
|
22500
|
+
displayName: t.displayName,
|
|
22501
|
+
read: t.read,
|
|
22502
|
+
write: t.write
|
|
22503
|
+
}));
|
|
22504
|
+
return { uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) };
|
|
22505
|
+
}
|
|
22506
|
+
const m = uri.match(SCHEMA_URI);
|
|
22507
|
+
if (m) {
|
|
22508
|
+
const typeId = m[1];
|
|
22509
|
+
const exposed = cfg.types.find((t) => t.typeId === typeId);
|
|
22510
|
+
const col = collections.get(typeId);
|
|
22511
|
+
if (!exposed || !col) throw new McpToolError(`Unknown collection: ${typeId}`);
|
|
22512
|
+
const schema = collectionToJsonSchema(col.schema, { redactFields: cfg.redactFields });
|
|
22513
|
+
return { uri, mimeType: "application/json", text: JSON.stringify(schema, null, 2) };
|
|
22514
|
+
}
|
|
22515
|
+
throw new McpToolError(`Unknown resource URI: ${uri}`);
|
|
22516
|
+
}
|
|
22517
|
+
|
|
22518
|
+
// src/plugins/core-plugins/mcp-plugin/routes/mcp.ts
|
|
22519
|
+
var SERVER_INFO = { name: "sonicjs-mcp", version: "1.0.0" };
|
|
22520
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
22521
|
+
function createMcpRoutes(options = {}) {
|
|
22522
|
+
const app2 = new Hono();
|
|
22523
|
+
app2.post("/", async (c) => {
|
|
22524
|
+
let body;
|
|
22525
|
+
try {
|
|
22526
|
+
body = await c.req.json();
|
|
22527
|
+
} catch {
|
|
22528
|
+
return c.json(rpcError(null, JSON_RPC.PARSE_ERROR, "Invalid JSON"));
|
|
22529
|
+
}
|
|
22530
|
+
const parsed = parseJsonRpc(body);
|
|
22531
|
+
if ("error" in parsed) {
|
|
22532
|
+
const id2 = body && typeof body === "object" ? body.id ?? null : null;
|
|
22533
|
+
return c.json(rpcError(id2, parsed.error.code, parsed.error.message));
|
|
22534
|
+
}
|
|
22535
|
+
const { id, method, params } = parsed;
|
|
22536
|
+
const user = c.get("user");
|
|
22537
|
+
if (!user) {
|
|
22538
|
+
return c.json(
|
|
22539
|
+
rpcError(id, JSON_RPC.UNAUTHORIZED, "Unauthorized: provide a valid API key via Authorization: Bearer sk_...")
|
|
22540
|
+
);
|
|
22541
|
+
}
|
|
22542
|
+
const { tenantId, principalSet, userId } = getDocumentRequestContext(c);
|
|
22543
|
+
const registry = getCollectionRegistry();
|
|
22544
|
+
const active = registry.listActive();
|
|
22545
|
+
const collections = new Map(active.map((col) => [col.name, col]));
|
|
22546
|
+
const cfg = resolveMcpConfig(options, active);
|
|
22547
|
+
const tools = buildToolRegistry(cfg, collections, PHASE_FLAGS);
|
|
22548
|
+
try {
|
|
22549
|
+
switch (method) {
|
|
22550
|
+
case "initialize":
|
|
22551
|
+
return c.json(
|
|
22552
|
+
rpcResult(id, {
|
|
22553
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
22554
|
+
serverInfo: SERVER_INFO,
|
|
22555
|
+
capabilities: { tools: {}, resources: {} }
|
|
22556
|
+
})
|
|
22557
|
+
);
|
|
22558
|
+
case "tools/list":
|
|
22559
|
+
return c.json(
|
|
22560
|
+
rpcResult(id, {
|
|
22561
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
|
|
22562
|
+
})
|
|
22563
|
+
);
|
|
22564
|
+
case "tools/call": {
|
|
22565
|
+
const name = typeof params.name === "string" ? params.name : "";
|
|
22566
|
+
const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
|
|
22567
|
+
const tool = tools.find((t) => t.name === name);
|
|
22568
|
+
if (!tool) return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Unknown tool: ${name}`));
|
|
22569
|
+
const readCtx = {
|
|
22570
|
+
db: c.env.DB,
|
|
22571
|
+
tenantId,
|
|
22572
|
+
principalSet,
|
|
22573
|
+
listLimit: cfg.listLimit,
|
|
22574
|
+
redactFields: cfg.redactFields
|
|
22575
|
+
};
|
|
22576
|
+
const writeCtx = {
|
|
22577
|
+
db: c.env.DB,
|
|
22578
|
+
tenantId,
|
|
22579
|
+
principalSet,
|
|
22580
|
+
userId: userId ?? "",
|
|
22581
|
+
redactFields: cfg.redactFields
|
|
22582
|
+
};
|
|
22583
|
+
try {
|
|
22584
|
+
let result;
|
|
22585
|
+
switch (tool.op) {
|
|
22586
|
+
case "list_collections":
|
|
22587
|
+
result = execListCollections(cfg.types);
|
|
22588
|
+
break;
|
|
22589
|
+
case "list":
|
|
22590
|
+
result = await execList(readCtx, tool.typeId, args);
|
|
22591
|
+
break;
|
|
22592
|
+
case "get":
|
|
22593
|
+
result = await execGet(readCtx, tool.typeId, args);
|
|
22594
|
+
break;
|
|
22595
|
+
case "create":
|
|
22596
|
+
result = await execCreate(writeCtx, tool.typeId, args);
|
|
22597
|
+
break;
|
|
22598
|
+
case "update":
|
|
22599
|
+
result = await execUpdate(writeCtx, tool.typeId, args);
|
|
22600
|
+
break;
|
|
22601
|
+
case "publish":
|
|
22602
|
+
result = await execPublish(writeCtx, tool.typeId, args);
|
|
22603
|
+
break;
|
|
22604
|
+
case "delete":
|
|
22605
|
+
result = await execDelete(writeCtx, tool.typeId, args);
|
|
22606
|
+
break;
|
|
22607
|
+
default:
|
|
22608
|
+
return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Tool not available: ${name}`));
|
|
22609
|
+
}
|
|
22610
|
+
return c.json(rpcResult(id, { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }));
|
|
22611
|
+
} catch (e) {
|
|
22612
|
+
if (e instanceof McpToolError) {
|
|
22613
|
+
return c.json(rpcResult(id, { content: [{ type: "text", text: e.message }], isError: true }));
|
|
22614
|
+
}
|
|
22615
|
+
throw e;
|
|
22616
|
+
}
|
|
22617
|
+
}
|
|
22618
|
+
case "resources/list":
|
|
22619
|
+
return c.json(rpcResult(id, { resources: buildResourceList(cfg) }));
|
|
22620
|
+
case "resources/read": {
|
|
22621
|
+
const uri = typeof params.uri === "string" ? params.uri : "";
|
|
22622
|
+
if (!uri) return c.json(rpcError(id, JSON_RPC.INVALID_PARAMS, 'resources/read requires a "uri" param'));
|
|
22623
|
+
try {
|
|
22624
|
+
const contents = readResource(uri, cfg, collections);
|
|
22625
|
+
return c.json(rpcResult(id, { contents: [contents] }));
|
|
22626
|
+
} catch (e) {
|
|
22627
|
+
if (e instanceof McpToolError) return c.json(rpcError(id, JSON_RPC.INVALID_PARAMS, e.message));
|
|
22628
|
+
throw e;
|
|
22629
|
+
}
|
|
22630
|
+
}
|
|
22631
|
+
default:
|
|
22632
|
+
return c.json(rpcError(id, JSON_RPC.METHOD_NOT_FOUND, `Method not found: ${method}`));
|
|
22633
|
+
}
|
|
22634
|
+
} catch (e) {
|
|
22635
|
+
console.error("[mcp] handler error:", e);
|
|
22636
|
+
return c.json(rpcError(id, JSON_RPC.INTERNAL_ERROR, "Internal error"));
|
|
22637
|
+
}
|
|
22638
|
+
});
|
|
22639
|
+
return app2;
|
|
22640
|
+
}
|
|
22641
|
+
|
|
22642
|
+
// src/plugins/core-plugins/mcp-plugin/admin/templates.ts
|
|
22643
|
+
init_admin_layout_catalyst_template();
|
|
22644
|
+
function accessBadge(ok) {
|
|
22645
|
+
return ok ? `<span class="inline-flex items-center rounded-full bg-emerald-50 dark:bg-emerald-900/20 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:text-emerald-400 ring-1 ring-emerald-600/20 dark:ring-emerald-400/20">Yes</span>` : `<span class="inline-flex items-center rounded-full bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 text-xs font-medium text-zinc-500 dark:text-zinc-400 ring-1 ring-zinc-200 dark:ring-zinc-700">No</span>`;
|
|
22646
|
+
}
|
|
22647
|
+
function toolNamesForType(typeId, tools) {
|
|
22648
|
+
const names = tools.filter((t) => t.typeId === typeId).map((t) => `<code class="text-xs font-mono text-zinc-500 dark:text-zinc-400">${escapeHtml(t.name)}</code>`);
|
|
22649
|
+
return names.length ? names.join('<span class="text-zinc-300 dark:text-zinc-600 mx-1">,</span>') : "\u2014";
|
|
22650
|
+
}
|
|
22651
|
+
function buildClaudeJson(endpointUrl) {
|
|
22652
|
+
return JSON.stringify(
|
|
22653
|
+
{
|
|
22654
|
+
mcpServers: {
|
|
22655
|
+
sonicjs: {
|
|
22656
|
+
type: "http",
|
|
22657
|
+
url: endpointUrl,
|
|
22658
|
+
headers: { Authorization: "Bearer sk_YOUR_API_KEY" }
|
|
22659
|
+
}
|
|
22660
|
+
}
|
|
22661
|
+
},
|
|
22662
|
+
null,
|
|
22663
|
+
2
|
|
22664
|
+
);
|
|
22665
|
+
}
|
|
22666
|
+
function buildCursorJson(endpointUrl) {
|
|
22667
|
+
return JSON.stringify(
|
|
22668
|
+
{
|
|
22669
|
+
mcpServers: {
|
|
22670
|
+
sonicjs: {
|
|
22671
|
+
url: endpointUrl,
|
|
22672
|
+
headers: { Authorization: "Bearer sk_YOUR_API_KEY" }
|
|
22673
|
+
}
|
|
22674
|
+
}
|
|
22675
|
+
},
|
|
22676
|
+
null,
|
|
22677
|
+
2
|
|
22678
|
+
);
|
|
22679
|
+
}
|
|
22680
|
+
function renderMcpDashboardPage(data) {
|
|
22681
|
+
const { endpointUrl, types, tools, user, version, dynamicMenuItems } = data;
|
|
22682
|
+
const safeEndpoint = escapeHtml(endpointUrl);
|
|
22683
|
+
const typeCount = types.length;
|
|
22684
|
+
const toolCount = tools.length;
|
|
22685
|
+
const typeRows = types.length ? types.map(
|
|
22686
|
+
(t) => `
|
|
22687
|
+
<tr class="border-t border-zinc-950/5 dark:border-white/10">
|
|
22688
|
+
<td class="py-3 pr-4 text-sm font-mono text-zinc-950 dark:text-white">${escapeHtml(t.typeId)}</td>
|
|
22689
|
+
<td class="py-3 pr-4 text-sm text-zinc-500 dark:text-zinc-400">${escapeHtml(t.displayName)}</td>
|
|
22690
|
+
<td class="py-3 pr-4">${accessBadge(t.read)}</td>
|
|
22691
|
+
<td class="py-3 pr-4">${accessBadge(t.write)}</td>
|
|
22692
|
+
<td class="py-3 text-sm">${toolNamesForType(t.typeId, tools)}</td>
|
|
22693
|
+
</tr>`
|
|
22694
|
+
).join("") : `<tr><td colspan="5" class="py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">No collections exposed. Register collections via <code class="font-mono">registerCollections()</code> in your app entry point.</td></tr>`;
|
|
22695
|
+
const claudeJson = escapeHtml(buildClaudeJson(endpointUrl));
|
|
22696
|
+
const cursorJson = escapeHtml(buildCursorJson(endpointUrl));
|
|
22697
|
+
const content2 = `
|
|
22698
|
+
<div class="space-y-8">
|
|
22699
|
+
|
|
22700
|
+
<!-- Header -->
|
|
22701
|
+
<div class="sm:flex sm:items-center sm:justify-between">
|
|
22702
|
+
<div class="sm:flex-auto">
|
|
22703
|
+
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">MCP Server</h1>
|
|
22704
|
+
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
|
|
22705
|
+
Model Context Protocol endpoint for AI agents (Claude Code, Cursor, VS Code). Requests are authenticated with API keys.
|
|
22706
|
+
</p>
|
|
22707
|
+
</div>
|
|
22708
|
+
<div class="mt-4 sm:mt-0 sm:ml-16">
|
|
22709
|
+
<a href="/admin/plugins/api-keys"
|
|
22710
|
+
class="inline-flex items-center justify-center rounded-lg bg-cyan-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-cyan-500 transition-colors shadow-sm">
|
|
22711
|
+
Mint API Key
|
|
22712
|
+
</a>
|
|
22713
|
+
</div>
|
|
22714
|
+
</div>
|
|
22715
|
+
|
|
22716
|
+
<!-- Status -->
|
|
22717
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
|
|
22718
|
+
<h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Endpoint</h2>
|
|
22719
|
+
<div class="flex items-center gap-3">
|
|
22720
|
+
<code id="endpoint-url" class="flex-1 rounded-lg bg-zinc-50 dark:bg-zinc-800 px-3 py-2 text-sm font-mono text-zinc-950 dark:text-white ring-1 ring-zinc-950/5 dark:ring-white/5 break-all">${safeEndpoint}</code>
|
|
22721
|
+
<button type="button" onclick="copyEndpoint()"
|
|
22722
|
+
class="shrink-0 rounded-lg bg-zinc-100 dark:bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 ring-1 ring-zinc-950/5 dark:ring-white/10 transition-colors">
|
|
22723
|
+
Copy
|
|
22724
|
+
</button>
|
|
22725
|
+
</div>
|
|
22726
|
+
<div class="mt-4 flex flex-wrap gap-4 text-sm text-zinc-500 dark:text-zinc-400">
|
|
22727
|
+
<span>
|
|
22728
|
+
<span class="font-semibold text-zinc-950 dark:text-white">${typeCount}</span>
|
|
22729
|
+
collection${typeCount === 1 ? "" : "s"} exposed
|
|
22730
|
+
</span>
|
|
22731
|
+
<span class="text-zinc-300 dark:text-zinc-600">·</span>
|
|
22732
|
+
<span>
|
|
22733
|
+
<span class="font-semibold text-zinc-950 dark:text-white">${toolCount}</span>
|
|
22734
|
+
tool${toolCount === 1 ? "" : "s"} available
|
|
22735
|
+
</span>
|
|
22736
|
+
<span class="text-zinc-300 dark:text-zinc-600">·</span>
|
|
22737
|
+
<span>Auth via <a href="/admin/plugins/api-keys" class="text-cyan-600 dark:text-cyan-400 hover:underline">API Keys</a></span>
|
|
22738
|
+
</div>
|
|
22739
|
+
</div>
|
|
22740
|
+
|
|
22741
|
+
<!-- Exposed collections table -->
|
|
22742
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-x-auto">
|
|
22743
|
+
<h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Exposed collections</h2>
|
|
22744
|
+
<table class="w-full text-left">
|
|
22745
|
+
<thead>
|
|
22746
|
+
<tr class="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
|
22747
|
+
<th class="pb-2 pr-4">ID</th>
|
|
22748
|
+
<th class="pb-2 pr-4">Display name</th>
|
|
22749
|
+
<th class="pb-2 pr-4">Read</th>
|
|
22750
|
+
<th class="pb-2 pr-4">Write</th>
|
|
22751
|
+
<th class="pb-2">Tools</th>
|
|
22752
|
+
</tr>
|
|
22753
|
+
</thead>
|
|
22754
|
+
<tbody>${typeRows}</tbody>
|
|
22755
|
+
</table>
|
|
22756
|
+
</div>
|
|
22757
|
+
|
|
22758
|
+
<!-- Integration guide -->
|
|
22759
|
+
<div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm space-y-6">
|
|
22760
|
+
<div>
|
|
22761
|
+
<h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-1">Integration guide</h2>
|
|
22762
|
+
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
|
22763
|
+
<a href="/admin/plugins/api-keys" class="text-cyan-600 dark:text-cyan-400 hover:underline">Mint an API key</a>,
|
|
22764
|
+
then paste one of the configs below into your MCP client.
|
|
22765
|
+
Replace <code class="font-mono text-xs">sk_YOUR_API_KEY</code> with the key.
|
|
22766
|
+
</p>
|
|
22767
|
+
</div>
|
|
22768
|
+
|
|
22769
|
+
<!-- Claude Code -->
|
|
22770
|
+
<div>
|
|
22771
|
+
<div class="flex items-center justify-between mb-2">
|
|
22772
|
+
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
|
22773
|
+
Claude Code — <code class="font-mono text-xs">~/.claude/mcp.json</code>
|
|
22774
|
+
</h3>
|
|
22775
|
+
<button type="button" onclick="copyConfig('claude-config')"
|
|
22776
|
+
class="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
|
|
22777
|
+
Copy
|
|
22778
|
+
</button>
|
|
22779
|
+
</div>
|
|
22780
|
+
<pre id="claude-config" class="rounded-lg bg-zinc-950 dark:bg-zinc-800 px-4 py-3 text-sm font-mono text-zinc-100 overflow-x-auto ring-1 ring-zinc-700 dark:ring-zinc-600 whitespace-pre">${claudeJson}</pre>
|
|
22781
|
+
</div>
|
|
22782
|
+
|
|
22783
|
+
<!-- Cursor -->
|
|
22784
|
+
<div>
|
|
22785
|
+
<div class="flex items-center justify-between mb-2">
|
|
22786
|
+
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
|
22787
|
+
Cursor — <code class="font-mono text-xs">~/.cursor/mcp.json</code>
|
|
22788
|
+
</h3>
|
|
22789
|
+
<button type="button" onclick="copyConfig('cursor-config')"
|
|
22790
|
+
class="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors">
|
|
22791
|
+
Copy
|
|
22792
|
+
</button>
|
|
22793
|
+
</div>
|
|
22794
|
+
<pre id="cursor-config" class="rounded-lg bg-zinc-950 dark:bg-zinc-800 px-4 py-3 text-sm font-mono text-zinc-100 overflow-x-auto ring-1 ring-zinc-700 dark:ring-zinc-600 whitespace-pre">${cursorJson}</pre>
|
|
22795
|
+
</div>
|
|
22796
|
+
|
|
22797
|
+
<p class="text-xs text-zinc-400 dark:text-zinc-500">
|
|
22798
|
+
MCP v1 = HTTP POST only (no SSE stream). Clients that require stdio transport can use
|
|
22799
|
+
<code class="font-mono">npx mcp-remote ${safeEndpoint}</code> as a local bridge.
|
|
22800
|
+
</p>
|
|
22801
|
+
</div>
|
|
22802
|
+
|
|
22803
|
+
</div>
|
|
22804
|
+
|
|
22805
|
+
<script>
|
|
22806
|
+
function copyEndpoint() {
|
|
22807
|
+
navigator.clipboard.writeText(document.getElementById('endpoint-url').textContent.trim()).then(() => {
|
|
22808
|
+
window.showNotification && window.showNotification('Endpoint URL copied', 'success')
|
|
22809
|
+
})
|
|
22810
|
+
}
|
|
22811
|
+
function copyConfig(id) {
|
|
22812
|
+
navigator.clipboard.writeText(document.getElementById(id).textContent).then(() => {
|
|
22813
|
+
window.showNotification && window.showNotification('Config copied', 'success')
|
|
22814
|
+
})
|
|
22815
|
+
}
|
|
22816
|
+
</script>`;
|
|
22817
|
+
const layoutData = {
|
|
22818
|
+
title: "MCP Server",
|
|
22819
|
+
pageTitle: "MCP Server",
|
|
22820
|
+
currentPath: "/admin/mcp",
|
|
22821
|
+
user,
|
|
22822
|
+
content: content2,
|
|
22823
|
+
version,
|
|
22824
|
+
dynamicMenuItems
|
|
22825
|
+
};
|
|
22826
|
+
return renderAdminLayoutCatalyst(layoutData);
|
|
22827
|
+
}
|
|
22828
|
+
|
|
22829
|
+
// src/plugins/core-plugins/mcp-plugin/admin/routes.ts
|
|
22830
|
+
function createMcpAdminRoutes(options = {}) {
|
|
22831
|
+
const routes2 = new Hono();
|
|
22832
|
+
routes2.use("*", requireAuth());
|
|
22833
|
+
routes2.use("*", async (c, next) => {
|
|
22834
|
+
const user = c.get("user");
|
|
22835
|
+
if (user?.role !== "admin") return c.json({ error: "Access denied" }, 403);
|
|
22836
|
+
return next();
|
|
22837
|
+
});
|
|
22838
|
+
routes2.get("/", async (c) => {
|
|
22839
|
+
const url = new URL(c.req.url);
|
|
22840
|
+
const endpointUrl = `${url.protocol}//${url.host}/mcp`;
|
|
22841
|
+
const registry = getCollectionRegistry();
|
|
22842
|
+
const active = registry.listActive();
|
|
22843
|
+
const collections = new Map(active.map((col) => [col.name, col]));
|
|
22844
|
+
const cfg = resolveMcpConfig(options, active);
|
|
22845
|
+
const tools = buildToolRegistry(cfg, collections, PHASE_FLAGS);
|
|
22846
|
+
const user = c.get("user");
|
|
22847
|
+
return c.html(
|
|
22848
|
+
renderMcpDashboardPage({
|
|
22849
|
+
endpointUrl,
|
|
22850
|
+
types: cfg.types,
|
|
22851
|
+
tools,
|
|
22852
|
+
user: { name: user.email, email: user.email, role: user.role },
|
|
22853
|
+
version: c.get("appVersion"),
|
|
22854
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
22855
|
+
})
|
|
22856
|
+
);
|
|
22857
|
+
});
|
|
22858
|
+
return routes2;
|
|
22859
|
+
}
|
|
22860
|
+
|
|
22861
|
+
// src/plugins/core-plugins/mcp-plugin/index.ts
|
|
22862
|
+
var MCP_ICON = `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`;
|
|
22863
|
+
function mcpPlugin(options = {}) {
|
|
22864
|
+
return definePlugin({
|
|
22865
|
+
id: "mcp",
|
|
22866
|
+
version: "1.0.0",
|
|
22867
|
+
name: "MCP Server",
|
|
22868
|
+
description: "Exposes SonicJS content as Model Context Protocol tools for AI agents (Claude Code, Cursor, \u2026).",
|
|
22869
|
+
sonicjsVersionRange: "^3.0.0",
|
|
22870
|
+
author: { name: "SonicJS Team" },
|
|
22871
|
+
register(app2) {
|
|
22872
|
+
app2.route("/mcp", createMcpRoutes(options));
|
|
22873
|
+
app2.route("/admin/mcp", createMcpAdminRoutes(options));
|
|
22874
|
+
},
|
|
22875
|
+
menu: [{ label: "MCP Server", path: "/admin/mcp", icon: MCP_ICON, order: 87 }]
|
|
22876
|
+
});
|
|
22877
|
+
}
|
|
22878
|
+
function createMcpPlugin(options) {
|
|
22879
|
+
return mcpPlugin(options);
|
|
22880
|
+
}
|
|
20622
22881
|
|
|
20623
22882
|
// src/index.ts
|
|
20624
22883
|
var VERSION = package_default.version;
|
|
20625
22884
|
|
|
20626
|
-
export { BUILT_IN_PROVIDERS, CloudflareEmailProvider2 as CloudflareEmailProvider, OAuthService, VERSION, createDb, createHelloWorldPlugin, createOAuthProvidersPlugin, createRedirectPlugin, createSonicJSApp, emailReconciliationPlugin, generateTriggersComment, helloWorldPlugin, oauthProvidersPlugin, parseCronTriggers, redirectPlugin, setupCoreMiddleware, setupCoreRoutes, site_settings_collection_default as siteSettingsCollection, updateWranglerTriggers };
|
|
22885
|
+
export { BUILT_IN_PROVIDERS, CloudflareEmailProvider2 as CloudflareEmailProvider, OAuthService, VERSION, createDb, createHelloWorldPlugin, createMcpPlugin, createOAuthProvidersPlugin, createRedirectPlugin, createSonicJSApp, emailReconciliationPlugin, generateTriggersComment, helloWorldPlugin, mcpPlugin, oauthProvidersPlugin, parseCronTriggers, redirectPlugin, setupCoreMiddleware, setupCoreRoutes, site_settings_collection_default as siteSettingsCollection, updateWranglerTriggers };
|
|
20627
22886
|
//# sourceMappingURL=index.js.map
|
|
20628
22887
|
//# sourceMappingURL=index.js.map
|