V0.38.3 extension config sections (#236)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -458,6 +458,58 @@ returns them as a frozen struct. Results are cached per invocation.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Config Sections (v0.38.3)
|
||||||
|
|
||||||
|
Packages can declare a `config_section` in their manifest to inject a
|
||||||
|
Preact configuration component into the Settings, Admin, or Team Admin
|
||||||
|
surfaces. This enables headless extensions and libraries to own their
|
||||||
|
configuration UX without requiring a full surface.
|
||||||
|
|
||||||
|
**Manifest schema:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config_section": {
|
||||||
|
"label": "My Extension",
|
||||||
|
"icon": "M12 2L2 7...",
|
||||||
|
"component": "js/config.js",
|
||||||
|
"surfaces": ["admin", "settings", "team-admin"],
|
||||||
|
"category": "system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `label` | string | Yes | Nav link text. Falls back to package title if empty. |
|
||||||
|
| `icon` | string | No | SVG path data in compact format (admin CatIcon). |
|
||||||
|
| `component` | string | No | Relative path within the package archive. Default: `js/config.js`. |
|
||||||
|
| `surfaces` | string[] | Yes | Target surfaces: `admin`, `settings`, `team-admin`. |
|
||||||
|
| `category` | string | No | Admin-only: category tab to appear under. Default: `system`. |
|
||||||
|
|
||||||
|
**Component contract:**
|
||||||
|
|
||||||
|
The config section component is a standard ES module exporting a
|
||||||
|
default Preact component. It is lazy-loaded via dynamic `import()` from
|
||||||
|
the existing asset-serving endpoint (`GET /surfaces/:id/:component`).
|
||||||
|
|
||||||
|
The component uses `sw.sdk` to read/write its own package settings:
|
||||||
|
- `sw.api.admin.packages.settings(packageId)` — read
|
||||||
|
- `sw.api.admin.packages.updateSettings(packageId, data)` — write
|
||||||
|
|
||||||
|
Settings are stored in the `package_settings` JSONB column (admin scope)
|
||||||
|
and `package_user_settings` table (user scope).
|
||||||
|
|
||||||
|
**Discovery:** At page load, the backend queries enabled packages for
|
||||||
|
`config_section` entries targeting the current surface and passes them
|
||||||
|
as `__CONFIG_SECTIONS__` to the frontend. The surface merges them into
|
||||||
|
its nav and section module map.
|
||||||
|
|
||||||
|
**No new tables or endpoints.** Config sections are purely manifest-driven,
|
||||||
|
using existing package storage and settings infrastructure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Builtin Seeding
|
### Builtin Seeding
|
||||||
|
|
||||||
On startup, `SeedBuiltinExtensions()` scans `extensions/builtin/`
|
On startup, `SeedBuiltinExtensions()` scans `extensions/builtin/`
|
||||||
|
|||||||
@@ -272,6 +272,99 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── v0.38.3: Config section manifest ──
|
||||||
|
|
||||||
|
await T.test('packages', 'config-section-install', 'install pkg with config_section → manifest stored', async function () {
|
||||||
|
var manifest = makeManifest({
|
||||||
|
tier: 'browser',
|
||||||
|
type: 'extension',
|
||||||
|
config_section: {
|
||||||
|
label: 'SDK Test Config',
|
||||||
|
icon: 'M12 2L2 7',
|
||||||
|
component: 'js/config.js',
|
||||||
|
surfaces: ['admin', 'settings'],
|
||||||
|
category: 'system'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Extension needs at least one of tools/pipes/hooks
|
||||||
|
var file = makePkgFile(manifest, [
|
||||||
|
{ name: 'js/config.js', content: 'export default function() { return "config"; }' }
|
||||||
|
]);
|
||||||
|
var r = await sw.api.admin.packages.install(file);
|
||||||
|
T.assert(r && r.id === manifest.id, 'expected install success');
|
||||||
|
|
||||||
|
// Verify manifest roundtrip
|
||||||
|
var pkg = await sw.api.admin.packages.get(manifest.id);
|
||||||
|
T.assert(pkg && pkg.manifest, 'expected manifest in response');
|
||||||
|
var cs = pkg.manifest.config_section;
|
||||||
|
T.assert(cs && cs.label === 'SDK Test Config', 'expected config_section.label preserved');
|
||||||
|
T.assert(cs.surfaces && cs.surfaces.length === 2, 'expected 2 surfaces');
|
||||||
|
T.assert(cs.component === 'js/config.js', 'expected component preserved');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
T.cleanup.push(async function () {
|
||||||
|
try { await sw.api.admin.packages.del(manifest.id); } catch (_) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await T.test('packages', 'config-section-settings', 'config section settings round-trip', async function () {
|
||||||
|
var manifest = makeManifest({
|
||||||
|
tier: 'browser',
|
||||||
|
type: 'extension',
|
||||||
|
config_section: {
|
||||||
|
label: 'SDK Config RT',
|
||||||
|
component: 'js/config.js',
|
||||||
|
surfaces: ['settings']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var file = makePkgFile(manifest, [
|
||||||
|
{ name: 'js/config.js', content: 'export default function() { return "config"; }' }
|
||||||
|
]);
|
||||||
|
var r = await sw.api.admin.packages.install(file);
|
||||||
|
T.assert(r && r.id, 'install success');
|
||||||
|
|
||||||
|
// Write settings
|
||||||
|
var settings = { api_key_label: 'My Service', timeout: 30 };
|
||||||
|
await sw.api.admin.packages.updateSettings(manifest.id, settings);
|
||||||
|
|
||||||
|
// Read back
|
||||||
|
var stored = await sw.api.admin.packages.settings(manifest.id);
|
||||||
|
T.assert(stored && stored.api_key_label === 'My Service', 'expected settings round-trip');
|
||||||
|
T.assert(stored.timeout === 30, 'expected numeric setting preserved');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
T.cleanup.push(async function () {
|
||||||
|
try { await sw.api.admin.packages.del(manifest.id); } catch (_) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await T.test('packages', 'config-section-list', 'list packages → config_section visible', async function () {
|
||||||
|
var manifest = makeManifest({
|
||||||
|
tier: 'browser',
|
||||||
|
type: 'extension',
|
||||||
|
config_section: {
|
||||||
|
label: 'SDK List Test',
|
||||||
|
component: 'js/config.js',
|
||||||
|
surfaces: ['admin']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var file = makePkgFile(manifest, [
|
||||||
|
{ name: 'js/config.js', content: 'export default function() {}' }
|
||||||
|
]);
|
||||||
|
await sw.api.admin.packages.install(file);
|
||||||
|
|
||||||
|
var list = await sw.api.admin.packages.list();
|
||||||
|
var found = (list || []).find(function (p) { return p.id === manifest.id; });
|
||||||
|
T.assert(found, 'expected package in list');
|
||||||
|
T.assert(found.manifest && found.manifest.config_section, 'expected config_section in list response');
|
||||||
|
T.assert(found.manifest.config_section.label === 'SDK List Test', 'expected label match');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
T.cleanup.push(async function () {
|
||||||
|
try { await sw.api.admin.packages.del(manifest.id); } catch (_) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await T.test('packages', 'consumer-bad-dep', 'consumer with non-existent dependency → 400', async function () {
|
await T.test('packages', 'consumer-bad-dep', 'consumer with non-existent dependency → 400', async function () {
|
||||||
var manifest = makeManifest({ dependencies: { 'nonexistent-lib-xyz': '>=1.0.0' } });
|
var manifest = makeManifest({ dependencies: { 'nonexistent-lib-xyz': '>=1.0.0' } });
|
||||||
var file = makePkgFile(manifest, [
|
var file = makePkgFile(manifest, [
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ type AdminPageData struct {
|
|||||||
Users []UserRow `json:"users,omitempty"`
|
Users []UserRow `json:"users,omitempty"`
|
||||||
Roles []RoleConfig `json:"roles"`
|
Roles []RoleConfig `json:"roles"`
|
||||||
Policies any `json:"policies,omitempty"`
|
Policies any `json:"policies,omitempty"`
|
||||||
|
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatPageData is what the chat surface templates receive.
|
// ChatPageData is what the chat surface templates receive.
|
||||||
@@ -110,14 +111,16 @@ type NotesPageData struct {
|
|||||||
// SettingsPageData is what the settings surface templates receive.
|
// SettingsPageData is what the settings surface templates receive.
|
||||||
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
|
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
|
||||||
type SettingsPageData struct {
|
type SettingsPageData struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
|
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Loader registration ──────────────────────
|
// ── Loader registration ──────────────────────
|
||||||
|
|
||||||
// TeamAdminPageData is what the team-admin surface receives.
|
// TeamAdminPageData is what the team-admin surface receives.
|
||||||
type TeamAdminPageData struct {
|
type TeamAdminPageData struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
|
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectsPageData is what the projects surface receives.
|
// ProjectsPageData is what the projects surface receives.
|
||||||
@@ -246,6 +249,9 @@ func (e *Engine) adminLoader(c *gin.Context, s store.Stores) (any, error) {
|
|||||||
data.Users = e.loadUsers(ctx, s)
|
data.Users = e.loadUsers(ctx, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v0.38.3: Discover extension config sections for admin surface
|
||||||
|
data.ConfigSections = configSectionsForSurface(ctx, s, "admin")
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +415,10 @@ func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
|
|||||||
if section == "" {
|
if section == "" {
|
||||||
section = "members"
|
section = "members"
|
||||||
}
|
}
|
||||||
return &TeamAdminPageData{Section: section}, nil
|
return &TeamAdminPageData{
|
||||||
|
Section: section,
|
||||||
|
ConfigSections: configSectionsForSurface(context.Background(), s, "team-admin"),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
|
func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
|
||||||
@@ -418,10 +427,84 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
|
|||||||
section = "general"
|
section = "general"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SettingsPageData{Section: section}, nil
|
return &SettingsPageData{
|
||||||
|
Section: section,
|
||||||
|
ConfigSections: configSectionsForSurface(context.Background(), s, "settings"),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) {
|
func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) {
|
||||||
projectID := c.Param("id")
|
projectID := c.Param("id")
|
||||||
return &ProjectsPageData{ProjectID: projectID}, nil
|
return &ProjectsPageData{ProjectID: projectID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Config section discovery (v0.38.3) ───────
|
||||||
|
//
|
||||||
|
// Packages declare a config_section in their manifest. This helper scans
|
||||||
|
// enabled packages and returns entries whose surfaces array includes the
|
||||||
|
// target surface. No new tables or endpoints — purely manifest-driven.
|
||||||
|
|
||||||
|
func configSectionsForSurface(ctx context.Context, s store.Stores, surfaceID string) []ConfigSectionEntry {
|
||||||
|
if s.Packages == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pkgs, err := s.Packages.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[pages] config sections: failed to list packages: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sections []ConfigSectionEntry
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
if !pkg.Enabled || pkg.Manifest == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
csRaw, ok := pkg.Manifest["config_section"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cs, ok := csRaw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this section targets the requested surface
|
||||||
|
surfacesRaw, _ := cs["surfaces"].([]any)
|
||||||
|
if len(surfacesRaw) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, v := range surfacesRaw {
|
||||||
|
if s, ok := v.(string); ok && s == surfaceID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
label, _ := cs["label"].(string)
|
||||||
|
if label == "" {
|
||||||
|
label = pkg.Title
|
||||||
|
}
|
||||||
|
icon, _ := cs["icon"].(string)
|
||||||
|
component, _ := cs["component"].(string)
|
||||||
|
if component == "" {
|
||||||
|
component = "js/config.js"
|
||||||
|
}
|
||||||
|
category, _ := cs["category"].(string)
|
||||||
|
if category == "" {
|
||||||
|
category = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
sections = append(sections, ConfigSectionEntry{
|
||||||
|
PackageID: pkg.ID,
|
||||||
|
Label: label,
|
||||||
|
Icon: icon,
|
||||||
|
Component: component,
|
||||||
|
Category: category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ type ExtensionNavItem struct {
|
|||||||
Route string
|
Route string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigSectionEntry describes a package-declared config section for
|
||||||
|
// lazy-loading in Settings, Admin, or Team Admin surfaces.
|
||||||
|
// v0.38.3: manifest-driven discovery — no new tables or endpoints.
|
||||||
|
type ConfigSectionEntry struct {
|
||||||
|
PackageID string `json:"package_id"` // section key & asset path prefix
|
||||||
|
Label string `json:"label"` // nav link text
|
||||||
|
Icon string `json:"icon"` // optional SVG path data (admin CatIcon format)
|
||||||
|
Component string `json:"component"` // relative asset path, e.g. "js/config.js"
|
||||||
|
Category string `json:"category"` // admin only: category tab (default "system")
|
||||||
|
}
|
||||||
|
|
||||||
// extensionNavItems returns enabled extension surfaces for sidebar rendering.
|
// extensionNavItems returns enabled extension surfaces for sidebar rendering.
|
||||||
// Queries the DB at render time so newly installed surfaces appear immediately.
|
// Queries the DB at render time so newly installed surfaces appear immediately.
|
||||||
func (e *Engine) extensionNavItems() []ExtensionNavItem {
|
func (e *Engine) extensionNavItems() []ExtensionNavItem {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
{{/* Inject section name for Preact routing */}}
|
{{/* Inject section name for Preact routing */}}
|
||||||
<script nonce="{{.CSPNonce}}">
|
<script nonce="{{.CSPNonce}}">
|
||||||
window.__SECTION__ = '{{.Section}}';
|
window.__SECTION__ = '{{.Section}}';
|
||||||
|
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{/* Vendor: Preact + htm → globals, then SDK boot, then admin surface */}}
|
{{/* Vendor: Preact + htm → globals, then SDK boot, then admin surface */}}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
{{/* Inject section name for Preact routing */}}
|
{{/* Inject section name for Preact routing */}}
|
||||||
<script nonce="{{.CSPNonce}}">
|
<script nonce="{{.CSPNonce}}">
|
||||||
window.__SECTION__ = '{{.Section}}';
|
window.__SECTION__ = '{{.Section}}';
|
||||||
|
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{/* Bridge section scripts removed in v0.37.7 — all sections are native Preact */}}
|
{{/* Bridge section scripts removed in v0.37.7 — all sections are native Preact */}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
{{define "scripts-team-admin"}}
|
{{define "scripts-team-admin"}}
|
||||||
<script nonce="{{.CSPNonce}}">
|
<script nonce="{{.CSPNonce}}">
|
||||||
window.__SECTION__ = '{{.Section}}';
|
window.__SECTION__ = '{{.Section}}';
|
||||||
|
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="module" nonce="{{.CSPNonce}}">
|
<script type="module" nonce="{{.CSPNonce}}">
|
||||||
|
|||||||
@@ -12350,6 +12350,28 @@ components:
|
|||||||
resolved_ver:
|
resolved_ver:
|
||||||
type: string
|
type: string
|
||||||
description: Actual library version at dependency creation time
|
description: Actual library version at dependency creation time
|
||||||
|
ConfigSection:
|
||||||
|
type: object
|
||||||
|
description: "v0.38.3: Manifest-declared config section for Settings/Admin/Team Admin surfaces"
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
description: Nav link text
|
||||||
|
icon:
|
||||||
|
type: string
|
||||||
|
description: Optional SVG path data (compact format)
|
||||||
|
component:
|
||||||
|
type: string
|
||||||
|
description: Relative asset path within package archive (default js/config.js)
|
||||||
|
surfaces:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum: [admin, settings, team-admin]
|
||||||
|
description: Which surfaces show this section
|
||||||
|
category:
|
||||||
|
type: string
|
||||||
|
description: Admin-only category tab (default system)
|
||||||
ExtConnection:
|
ExtConnection:
|
||||||
type: object
|
type: object
|
||||||
description: "v0.38.1: Extension connection credential (secrets masked in list responses)"
|
description: "v0.38.1: Extension connection credential (secrets masked in list responses)"
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
* AdminSurface — root admin surface
|
* AdminSurface — root admin surface
|
||||||
*
|
*
|
||||||
* Reads globals:
|
* Reads globals:
|
||||||
* __SECTION__ — active section name (string)
|
* __SECTION__ — active section name (string)
|
||||||
* __BASE__ — base path
|
* __BASE__ — base path
|
||||||
|
* __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null)
|
||||||
*
|
*
|
||||||
* Layout: topbar (back + category tabs) + body (sidebar nav + content area).
|
* Layout: topbar (back + category tabs) + body (sidebar nav + content area).
|
||||||
* All 24 sections are native Preact components loaded lazily.
|
* All 24+ sections are native Preact components loaded lazily.
|
||||||
*/
|
*/
|
||||||
const { html } = window;
|
const { html } = window;
|
||||||
const { useState, useEffect, useCallback, useMemo } = hooks;
|
const { useState, useEffect, useCallback, useMemo } = hooks;
|
||||||
@@ -37,6 +38,20 @@ const ADMIN_LABELS = {
|
|||||||
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── v0.38.3: Extension config sections ──────
|
||||||
|
// Packages declare config_section in their manifest targeting "admin".
|
||||||
|
// We merge them into the appropriate category and section module map.
|
||||||
|
const _configSections = window.__CONFIG_SECTIONS__ || [];
|
||||||
|
const _base = window.__BASE__ || '';
|
||||||
|
for (const cs of _configSections) {
|
||||||
|
const pkgId = cs.package_id;
|
||||||
|
const cat = cs.category || 'system';
|
||||||
|
// Add to category (create if new — unlikely but safe)
|
||||||
|
if (!ADMIN_SECTIONS[cat]) ADMIN_SECTIONS[cat] = [];
|
||||||
|
if (!ADMIN_SECTIONS[cat].includes(pkgId)) ADMIN_SECTIONS[cat].push(pkgId);
|
||||||
|
ADMIN_LABELS[pkgId] = cs.label;
|
||||||
|
}
|
||||||
|
|
||||||
const CATEGORY_META = {
|
const CATEGORY_META = {
|
||||||
people: { label: 'People', first: 'users', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|C9 7 4' },
|
people: { label: 'People', first: 'users', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|C9 7 4' },
|
||||||
ai: { label: 'AI', first: 'providers', icon: 'R4 4 16 16 2|L9 9 9.01 9|L15 9 15.01 9|M8 14s1.5 2 4 2 4-2 4-2' },
|
ai: { label: 'AI', first: 'providers', icon: 'R4 4 16 16 2|L9 9 9.01 9|L15 9 15.01 9|M8 14s1.5 2 4 2 4-2 4-2' },
|
||||||
@@ -76,6 +91,13 @@ const sectionModules = {
|
|||||||
stats: () => import(`./stats.js${_v}`),
|
stats: () => import(`./stats.js${_v}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// v0.38.3: Register dynamic section loaders for extension config sections
|
||||||
|
for (const cs of _configSections) {
|
||||||
|
const pkgId = cs.package_id;
|
||||||
|
const component = cs.component || 'js/config.js';
|
||||||
|
sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Derive category from section ────────────
|
// ── Derive category from section ────────────
|
||||||
function catForSection(section) {
|
function catForSection(section) {
|
||||||
for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) {
|
for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
* SettingsSurface — root settings surface
|
* SettingsSurface — root settings surface
|
||||||
*
|
*
|
||||||
* Reads globals:
|
* Reads globals:
|
||||||
* __SECTION__ — active section name (string)
|
* __SECTION__ — active section name (string)
|
||||||
* __BASE__ — base path
|
* __BASE__ — base path
|
||||||
|
* __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null)
|
||||||
*
|
*
|
||||||
* Policy flags (BYOK, personas) come from sw.auth.policies,
|
* Policy flags (BYOK, personas) come from sw.auth.policies,
|
||||||
* populated at SDK boot via GET /api/v1/profile/permissions.
|
* populated at SDK boot via GET /api/v1/profile/permissions.
|
||||||
@@ -75,6 +76,20 @@ const SECTION_TITLES = {
|
|||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── v0.38.3: Extension config sections ──────
|
||||||
|
// Packages declare config_section in their manifest. The backend passes
|
||||||
|
// matching entries via __CONFIG_SECTIONS__. We merge them into the nav
|
||||||
|
// and section module map for lazy loading.
|
||||||
|
const _configSections = window.__CONFIG_SECTIONS__ || [];
|
||||||
|
const EXT_NAV_ITEMS = _configSections.map(cs => ({ key: cs.package_id, label: cs.label }));
|
||||||
|
const _base = window.__BASE__ || '';
|
||||||
|
for (const cs of _configSections) {
|
||||||
|
const pkgId = cs.package_id;
|
||||||
|
const component = cs.component || 'js/config.js';
|
||||||
|
sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`);
|
||||||
|
SECTION_TITLES[pkgId] = cs.label;
|
||||||
|
}
|
||||||
|
|
||||||
function SettingsSurface() {
|
function SettingsSurface() {
|
||||||
const BASE = window.__BASE__ || '';
|
const BASE = window.__BASE__ || '';
|
||||||
const section = window.__SECTION__ || 'general';
|
const section = window.__SECTION__ || 'general';
|
||||||
@@ -195,6 +210,20 @@ function SettingsSurface() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
${EXT_NAV_ITEMS.length > 0 && html`
|
||||||
|
<div>
|
||||||
|
<div class="settings-nav-sep"></div>
|
||||||
|
<div class="settings-nav-label" style="font-size:10px;font-weight:600;color:var(--text-3);padding:8px 12px 4px;text-transform:uppercase;letter-spacing:0.5px;">Extensions</div>
|
||||||
|
${EXT_NAV_ITEMS.map(item => html`
|
||||||
|
<a href="${BASE}/settings/${item.key}"
|
||||||
|
class="settings-nav-link ${section === item.key ? 'active' : ''}"
|
||||||
|
onClick=${onNavClick}>
|
||||||
|
${item.label}
|
||||||
|
</a>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${/* Content */''}
|
${/* Content */''}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
* TeamAdminSurface — root team admin surface
|
* TeamAdminSurface — root team admin surface
|
||||||
*
|
*
|
||||||
* Reads globals:
|
* Reads globals:
|
||||||
* __SECTION__ — active section name (string)
|
* __SECTION__ — active section name (string)
|
||||||
* __BASE__ — base path
|
* __BASE__ — base path
|
||||||
|
* __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null)
|
||||||
*
|
*
|
||||||
* Layout: topbar (back + team name) + sidebar nav + content area.
|
* Layout: topbar (back + team name) + sidebar nav + content area.
|
||||||
* All 10 sections are native Preact components loaded lazily.
|
* All 10+ sections are native Preact components loaded lazily.
|
||||||
*/
|
*/
|
||||||
const { html } = window;
|
const { html } = window;
|
||||||
const { useState, useEffect, useCallback, useMemo } = hooks;
|
const { useState, useEffect, useCallback, useMemo } = hooks;
|
||||||
@@ -45,6 +46,16 @@ const sectionModules = {
|
|||||||
activity: () => import('./activity.js'),
|
activity: () => import('./activity.js'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── v0.38.3: Extension config sections ──────
|
||||||
|
const _configSections = window.__CONFIG_SECTIONS__ || [];
|
||||||
|
const _base = window.__BASE__ || '';
|
||||||
|
for (const cs of _configSections) {
|
||||||
|
const pkgId = cs.package_id;
|
||||||
|
const component = cs.component || 'js/config.js';
|
||||||
|
SECTIONS.push({ key: pkgId, label: cs.label });
|
||||||
|
sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`);
|
||||||
|
}
|
||||||
|
|
||||||
function TeamAdminSurface() {
|
function TeamAdminSurface() {
|
||||||
const BASE = window.__BASE__ || '';
|
const BASE = window.__BASE__ || '';
|
||||||
const section = window.__SECTION__ || 'members';
|
const section = window.__SECTION__ || 'members';
|
||||||
|
|||||||
Reference in New Issue
Block a user