Skip to content

Commit 4388237

Browse files
committed
fix: navbar bugs, features page consolidation, and sidebar improvements
- Fix drag-and-drop reorder 403: add CSRF token to sidebar_tree.js fetch - Fix nav category switching lag: show loading spinner + use prefetch cache - Fix entity template creation: add hx-target/hx-swap, HTMX-aware error response - Consolidate features page: merge Plugin Hub + Addon Settings into single page with inline enable/disable toggles for owners - Add quick note "View in Journal" link in toast notification - Add fully hideable sidebar with floating restore button - Add chronicle:notify HX-Trigger listener for server-sent toast notifications - Add html option to Chronicle.notify() for rich toast content https://claude.ai/code/session_01QJLkgjQDu5qohzJKGV4hj9
1 parent cea673d commit 4388237

14 files changed

Lines changed: 285 additions & 82 deletions

File tree

.ai/status.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88
<!-- ====================================================================== -->
99

1010
## Last Updated
11+
2026-03-10 -- **Bug fixes & UX improvements (navbar, features page, entity templates).**
12+
13+
1. **Drag-and-drop CSRF fix**: `sidebar_tree.js` reorderEntity() was using raw `fetch()` without CSRF token, causing 403 on drag-drop reorder. Added `Chronicle.getCsrf()` header.
14+
15+
2. **Nav menu lag fix**: `sidebar_drill.js` category switching caused stale content flash. Now shows loading spinner immediately and uses prefetch cache for instant swaps.
16+
17+
3. **Entity template creation fix**: `entity_types.templ` form lacked `hx-target`/`hx-swap`, causing broken page when HTMX swapped full page into form. Added proper targeting, partial response on error, and `chronicle:notify` HX-Trigger support in notifications.js.
18+
19+
4. **Features page consolidation**: Merged Plugin Hub (read-only) and Addon Settings (management) into a single Features page at `/campaigns/:id/plugins`. Owners see inline enable/disable toggles. Added `AddonID` and `Installed` fields to `PluginHubAddon`. Settings page link now points to `/plugins`. Toggle handler supports `redirect_to=plugins` for redirect after toggle.
20+
21+
5. **Quick notes discoverability**: After creating a quick note, toast now includes a clickable "View in Journal" link. Added `html` option to `Chronicle.notify()`.
22+
23+
6. **Fully hideable navbar**: Added sidebar hide button (double-angle-left icon) next to pin button. When hidden, sidebar fully disappears (0px width). Floating restore button appears at top-left. State persists in `localStorage`.
24+
25+
Branch: `claude/fix-navbar-features-page-8RZuE`.
26+
27+
### Previous Update
1128
2026-03-10 -- **Per-player visibility + Co-DM grants (Phase 2 complete).** Implemented per-player content sharing across all content types:
1229

1330
1. **Maps**: Added `visibility_rules` JSON column to `map_markers` and `map_drawings` (migration 002). Updated repository to filter with `JSON_CONTAINS` for non-owners. Updated `ListMarkers` signature to include `userID` for per-player filtering.

.ai/todo.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ Known broken or missing things, ordered by severity.
3333

3434
### Medium
3535

36+
- [x] **Drag-and-drop sidebar reorder fails with 403**`sidebar_tree.js:reorderEntity()` used raw `fetch()` without CSRF token. Fixed by adding `Chronicle.getCsrf()` to headers.
37+
- [x] **Nav menu flashes stale content on category switch**`sidebar_drill.js` kept old content visible during HTMX fetch. Fixed with immediate loading spinner and prefetch cache usage.
38+
- [x] **Entity template creation form broken for HTMX** — Form lacked `hx-target`/`hx-swap`, and error handler returned full page for HTMX requests. Added proper targeting, partial response, and `chronicle:notify` HX-Trigger support.
39+
- [x] **Features page appears duplicated** — Plugin Hub (read-only) and Addon Settings (management) were separate pages. Consolidated into single `/plugins` page with inline enable/disable toggles for owners.
40+
- [x] **Quick notes not discoverable after creation** — Toast only said "Note created" with no link. Added "View in Journal" clickable link in toast. Added `html` option to `Chronicle.notify()`.
41+
3642
- [x] **Unified permission model (Phase 1)** — Created `internal/permissions/role.go` with shared role constants and `CanSeeDmOnly`/`CanSetDmOnly` helpers. Replaced ~30 magic `role >= 3` checks across calendar, timeline, maps, entities, syncapi, app/routes. Fixed dm_only inconsistencies: tags now Owner-only (was Scribe+). Added Owner-only guards on dm_only creation in calendar events, timeline events, map markers. JS `permissions.js` uses named `ROLE_OWNER` constant. Permission matrix documented in `.ai/conventions.md`.
3743
- [x] **Per-player visibility + Co-DM grants (Phase 2)** — Maps: `visibility_rules` JSON on markers/drawings with `JSON_CONTAINS` filtering. Notes: `shared_with` JSON with Private/Everyone/Specific Players UI + member picker popover. Co-DM: `DmGrantIDs` in CampaignSettings, `IsDmGranted` in CampaignContext, `VisibilityRole()` method, DM grants API + settings UI. DM-granted users see dm_only content but cannot create it.
3844
- [x] **Tags not hideable from players** — Implemented `dm_only` column (migration 000038), role-based filtering in repo/service/handler, eye-slash badge + DM checkbox in tag_picker.js.
@@ -201,6 +207,8 @@ _Quick capture, backlinks, enhanced graph, editor power-ups. See `.ai/obsidian-n
201207

202208
### Phase W: Polish, Ecosystem & Delight
203209

210+
- [ ] **Sprint W-0: Nav Menu Reorg Mode** — Small icon button near Dashboard in sidebar. Click to enter reorg mode for current level (categories or entities). Category level: drag to reorder category icons. Entity level: drag to reorder, create folders/submenus. Click again to exit reorg mode. Must work on desktop, tablet, and mobile. Button is context-aware: on base nav, reorders categories; drilled into a category, reorders entities.
211+
- [ ] **Sprint W-0.5: Owner Visual Customization** — Change "Chronicle" brand name per-campaign with optional image/logo. Top bar color/gradient/animation/background image (responsive). Visual customization editor with faux site outline (editable boxes for colors/backgrounds). Appearance-only, not layout editing. Future: per-addon "feature in use" indicators showing which entities/pages use each feature, which widgets are available, click associations to navigate. Disabled feature banner with "offline" sticker instead of complete removal.
204212
- [ ] **Sprint W-1: Command Palette & Saved Filters** — Ctrl+Shift+P action palette with fuzzy search. Saved entity list filter presets as sidebar links in `saved_filters` table.
205213
- [ ] **Sprint W-2: Map Drawing Tools, Regions & Measurement** — Leaflet.Draw integration (freehand, polygons, circles, rectangles, text). Uses existing `map_drawings` table. Per-drawing visibility, color/opacity. Also: map regions (polygon fills/strokes/labels), measurement/distance tool, map embed layout block for entity pages.
206214
- [ ] **Sprint W-2.5: Nested / Linked Maps** — Click marker to open sub-map. `linked_map_id` on markers. Breadcrumb navigation between map levels. Competitive gap vs World Anvil/LegendKeeper.

internal/app/routes.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,13 @@ func (a *addonListerAdapter) ListForPluginHub(ctx context.Context, campaignID st
153153
result := make([]campaigns.PluginHubAddon, len(addonList))
154154
for i, ca := range addonList {
155155
result[i] = campaigns.PluginHubAddon{
156-
Slug: ca.AddonSlug,
157-
Name: ca.AddonName,
158-
Icon: ca.AddonIcon,
159-
Category: string(ca.AddonCategory),
160-
Enabled: ca.Enabled,
156+
AddonID: ca.AddonID,
157+
Slug: ca.AddonSlug,
158+
Name: ca.AddonName,
159+
Icon: ca.AddonIcon,
160+
Category: string(ca.AddonCategory),
161+
Enabled: ca.Enabled,
162+
Installed: ca.Installed,
161163
}
162164
}
163165
return result, nil

internal/plugins/addons/handler.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,18 @@ func (h *Handler) ToggleCampaignAddon(c echo.Context) error {
189189
}
190190
}
191191

192-
// Return updated addon list for HTMX swap.
192+
// If the toggle came from the Plugin Hub page, redirect back there
193+
// so it re-renders with updated state.
194+
if c.FormValue("redirect_to") == "plugins" {
195+
redirectURL := "/campaigns/" + cc.Campaign.ID + "/plugins"
196+
if middleware.IsHTMX(c) {
197+
c.Response().Header().Set("HX-Redirect", redirectURL)
198+
return c.NoContent(http.StatusNoContent)
199+
}
200+
return c.Redirect(http.StatusSeeOther, redirectURL)
201+
}
202+
203+
// Return updated addon list for HTMX swap (addons settings page).
193204
if middleware.IsHTMX(c) {
194205
addons, err := h.service.ListForCampaign(ctx, cc.Campaign.ID)
195206
if err != nil {

internal/plugins/campaigns/handler.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ type AuditLogger interface {
8181

8282
// PluginHubAddon is a minimal addon representation for the plugin hub page.
8383
type PluginHubAddon struct {
84-
Slug string
85-
Name string
86-
Icon string
87-
Category string
88-
Enabled bool
84+
AddonID int
85+
Slug string
86+
Name string
87+
Icon string
88+
Category string
89+
Enabled bool
90+
Installed bool // Whether backing code exists (for showing Coming Soon vs toggle).
8991
}
9092

9193
// AddonLister lists addons for the plugin hub page. Avoids importing the addons
@@ -553,7 +555,8 @@ func (h *Handler) PluginHub(c echo.Context) error {
553555
}
554556

555557
isOwner := cc.MemberRole >= RoleOwner
556-
return middleware.Render(c, http.StatusOK, PluginHubPage(cc, addons, isOwner))
558+
csrfToken := middleware.GetCSRFToken(c)
559+
return middleware.Render(c, http.StatusOK, PluginHubPage(cc, addons, isOwner, csrfToken))
557560
}
558561

559562
// --- Customization Hub ---

internal/plugins/campaigns/settings.templ

Lines changed: 139 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package campaigns
66
import (
77
"encoding/json"
88
"fmt"
9+
"strconv"
910
"github.com/keyxmakerx/chronicle/internal/templates/layouts"
1011
)
1112

@@ -142,7 +143,7 @@ templ CampaignSettingsPage(cc *CampaignContext, transfer *OwnershipTransfer, ent
142143
<div class="flex items-center justify-between mb-2">
143144
<h2 class="text-lg font-semibold text-fg">Features</h2>
144145
<a
145-
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/addons/settings", cc.Campaign.ID)) }
146+
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/plugins", cc.Campaign.ID)) }
146147
class="text-sm link"
147148
>
148149
<i class="fa-solid fa-plug mr-1"></i> Manage Features
@@ -392,87 +393,167 @@ func pluginDescription(slug string) string {
392393
}
393394
}
394395

395-
// PluginHubPage renders the features hub showing all enabled features with quick access links.
396-
templ PluginHubPage(cc *CampaignContext, addons []PluginHubAddon, isOwner bool) {
396+
// PluginHubPage renders the unified features page where owners can toggle
397+
// features on/off and all members can see enabled features with quick access links.
398+
templ PluginHubPage(cc *CampaignContext, addons []PluginHubAddon, isOwner bool, csrfToken string) {
397399
@layouts.App(cc.Campaign.Name + " - Features") {
398400
<div class="max-w-3xl mx-auto">
399401
<div class="flex items-center justify-between mb-6">
400402
<div>
401403
<h1 class="text-2xl font-bold text-fg">Features</h1>
402-
<p class="text-sm text-fg-muted mt-1">Quick access to all campaign features.</p>
404+
if isOwner {
405+
<p class="text-sm text-fg-muted mt-1">Enable or disable features for this campaign.</p>
406+
} else {
407+
<p class="text-sm text-fg-muted mt-1">Quick access to all campaign features.</p>
408+
}
403409
</div>
404-
if isOwner {
405-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/addons/settings", cc.Campaign.ID)) } class="btn-secondary text-sm">
406-
<i class="fa-solid fa-gear mr-1.5"></i> Manage
407-
</a>
408-
}
410+
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/edit", cc.Campaign.ID)) } class="btn-secondary text-sm">
411+
Back to Settings
412+
</a>
409413
</div>
410414

411-
<!-- Enabled features -->
412-
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
413-
for _, addon := range addons {
414-
if addon.Enabled {
415-
<a
416-
href={ templ.SafeURL(pluginURL(cc.Campaign.ID, addon.Slug)) }
417-
class="card p-4 flex items-start gap-3 hover:ring-2 hover:ring-accent/50 transition-all group"
418-
>
419-
<div class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0 group-hover:bg-accent/20 transition-colors">
420-
if addon.Icon != "" {
421-
<i class={ "fa-solid " + addon.Icon, "text-accent text-lg" }></i>
422-
} else {
423-
<i class="fa-solid fa-puzzle-piece text-accent text-lg"></i>
424-
}
425-
</div>
426-
<div class="flex-1 min-w-0">
427-
<div class="font-semibold text-fg group-hover:text-accent transition-colors">{ addon.Name }</div>
428-
<p class="text-xs text-fg-muted mt-0.5">{ pluginDescription(addon.Slug) }</p>
429-
</div>
430-
<i class="fa-solid fa-arrow-right text-fg-muted text-xs mt-1 opacity-0 group-hover:opacity-100 transition-opacity"></i>
431-
</a>
432-
}
433-
}
415+
<div id="plugin-hub-list">
416+
@PluginHubListContent(cc, addons, isOwner, csrfToken)
434417
</div>
435418

436-
<!-- Disabled features (show for owners) -->
419+
// Info panel for owners.
437420
if isOwner {
438-
@disabledPlugins(cc.Campaign.ID, addons)
421+
<div class="card p-6 mt-6">
422+
<div class="flex items-start gap-4">
423+
<span class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
424+
<i class="fa-solid fa-circle-info text-accent"></i>
425+
</span>
426+
<div>
427+
<h3 class="text-sm font-semibold text-fg mb-1">About Features</h3>
428+
<p class="text-sm text-fg-secondary leading-relaxed">
429+
Features add optional capabilities to your campaign like calendars, maps, and timelines.
430+
Toggle them on or off at any time — disabling does not delete your data.
431+
</p>
432+
</div>
433+
</div>
434+
</div>
439435
}
440436
</div>
441437
}
442438
}
443439

444-
// disabledPlugins shows features that are not enabled, with a prompt to enable them.
445-
templ disabledPlugins(campaignID string, addons []PluginHubAddon) {
440+
// PluginHubListContent renders the addon list (for HTMX partial swaps after toggling).
441+
templ PluginHubListContent(cc *CampaignContext, addons []PluginHubAddon, isOwner bool, csrfToken string) {
442+
// Enabled features with quick access links.
443+
if hasEnabled(addons) {
444+
<div class="mb-8">
445+
<h2 class="section-header mb-3">
446+
<i class="fa-solid fa-check-circle mr-2 text-emerald-500"></i>Enabled
447+
</h2>
448+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
449+
for _, addon := range addons {
450+
if addon.Enabled {
451+
@pluginHubCard(cc, addon, isOwner, csrfToken)
452+
}
453+
}
454+
</div>
455+
</div>
456+
}
457+
458+
// Disabled/available features.
446459
if hasDisabled(addons) {
447-
<div class="mt-4">
448-
<h2 class="text-sm font-semibold text-fg-muted uppercase tracking-wider mb-3">Available Features</h2>
449-
<div class="space-y-2">
460+
<div class="mb-6">
461+
<h2 class="section-header mb-3">
462+
<i class="fa-solid fa-circle-plus mr-2 text-fg-muted"></i>Available
463+
</h2>
464+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
450465
for _, addon := range addons {
451466
if !addon.Enabled {
452-
<div class="card p-3 flex items-center gap-3 opacity-60">
453-
<div class="w-8 h-8 rounded-lg bg-surface-alt flex items-center justify-center shrink-0">
454-
if addon.Icon != "" {
455-
<i class={ "fa-solid " + addon.Icon, "text-fg-muted" }></i>
456-
} else {
457-
<i class="fa-solid fa-puzzle-piece text-fg-muted"></i>
458-
}
459-
</div>
460-
<div class="flex-1 min-w-0">
461-
<span class="text-sm text-fg">{ addon.Name }</span>
462-
<span class="text-xs text-fg-muted ml-2">Disabled</span>
463-
</div>
464-
<a
465-
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/addons/settings", campaignID)) }
466-
class="text-xs text-accent hover:underline"
467-
>
468-
Enable
469-
</a>
470-
</div>
467+
@pluginHubCard(cc, addon, isOwner, csrfToken)
471468
}
472469
}
473470
</div>
474471
</div>
475472
}
473+
474+
if len(addons) == 0 {
475+
<div class="card p-8 text-center">
476+
<p class="text-fg-muted">No features available yet.</p>
477+
</div>
478+
}
479+
}
480+
481+
// pluginHubCard renders a single feature card with optional toggle for owners.
482+
templ pluginHubCard(cc *CampaignContext, addon PluginHubAddon, isOwner bool, csrfToken string) {
483+
<div class={ "card p-4 flex items-start gap-3 transition-all",
484+
templ.KV("hover:ring-2 hover:ring-accent/50", addon.Enabled),
485+
templ.KV("opacity-60", !addon.Enabled && !isOwner) }>
486+
<div class={ "w-10 h-10 rounded-lg flex items-center justify-center shrink-0",
487+
templ.KV("bg-accent/10", addon.Enabled),
488+
templ.KV("bg-surface-alt", !addon.Enabled) }>
489+
if addon.Icon != "" {
490+
<i class={ "fa-solid " + addon.Icon, "text-lg",
491+
templ.KV("text-accent", addon.Enabled),
492+
templ.KV("text-fg-muted", !addon.Enabled) }></i>
493+
} else {
494+
<i class={ "fa-solid fa-puzzle-piece text-lg",
495+
templ.KV("text-accent", addon.Enabled),
496+
templ.KV("text-fg-muted", !addon.Enabled) }></i>
497+
}
498+
</div>
499+
<div class="flex-1 min-w-0">
500+
if addon.Enabled {
501+
<a
502+
href={ templ.SafeURL(pluginURL(cc.Campaign.ID, addon.Slug)) }
503+
class="font-semibold text-fg hover:text-accent transition-colors"
504+
>
505+
{ addon.Name }
506+
<i class="fa-solid fa-arrow-right text-fg-muted text-xs ml-1"></i>
507+
</a>
508+
} else {
509+
<div class="font-semibold text-fg">{ addon.Name }</div>
510+
}
511+
<p class="text-xs text-fg-muted mt-0.5">{ pluginDescription(addon.Slug) }</p>
512+
</div>
513+
// Toggle button for owners; "Coming Soon" badge for uninstalled addons.
514+
if isOwner {
515+
if addon.Installed {
516+
<form
517+
method="POST"
518+
hx-put={ fmt.Sprintf("/campaigns/%s/addons/%s/toggle", cc.Campaign.ID, strconv.Itoa(addon.AddonID)) }
519+
hx-target="#plugin-hub-list"
520+
hx-swap="innerHTML"
521+
class="shrink-0"
522+
>
523+
<input type="hidden" name="csrf_token" value={ csrfToken }/>
524+
<input type="hidden" name="redirect_to" value="plugins"/>
525+
if addon.Enabled {
526+
<input type="hidden" name="action" value="disable"/>
527+
<button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 hover:bg-emerald-200 dark:hover:bg-emerald-900/50 transition-colors" title="Click to disable">
528+
<i class="fa-solid fa-check text-[10px]"></i>
529+
Enabled
530+
</button>
531+
} else {
532+
<input type="hidden" name="action" value="enable"/>
533+
<button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-full bg-surface-alt text-fg-muted border border-edge hover:bg-accent/10 hover:text-accent transition-colors" title="Click to enable">
534+
<i class="fa-solid fa-plus text-[10px]"></i>
535+
Enable
536+
</button>
537+
}
538+
</form>
539+
} else {
540+
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-full bg-surface-alt text-fg-muted border border-edge cursor-default shrink-0">
541+
<i class="fa-solid fa-clock text-[10px]"></i>
542+
Coming Soon
543+
</span>
544+
}
545+
}
546+
</div>
547+
}
548+
549+
// hasEnabled checks if any addon in the list is enabled.
550+
func hasEnabled(addons []PluginHubAddon) bool {
551+
for _, a := range addons {
552+
if a.Enabled {
553+
return true
554+
}
555+
}
556+
return false
476557
}
477558

478559
// hasDisabled checks if any addon in the list is disabled.

internal/plugins/entities/entity_types.templ

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ templ EntityTypeListContent(cc *campaigns.CampaignContext, entityTypes []EntityT
6060
method="POST"
6161
action={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/entity-types", cc.Campaign.ID)) }
6262
hx-post={ fmt.Sprintf("/campaigns/%s/entity-types", cc.Campaign.ID) }
63+
hx-target="#entity-type-list"
64+
hx-swap="innerHTML"
6365
class="space-y-4"
6466
>
6567
<input type="hidden" name="csrf_token" value={ csrfToken }/>

internal/plugins/entities/handler.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1515,12 +1515,22 @@ func (h *Handler) CreateEntityType(c echo.Context) error {
15151515
et, err := h.service.CreateEntityType(c.Request().Context(), cc.Campaign.ID, input)
15161516
if err != nil {
15171517
entityTypes, _ := h.service.GetEntityTypes(c.Request().Context(), cc.Campaign.ID)
1518-
counts, _ := h.service.CountByType(c.Request().Context(), cc.Campaign.ID, int(cc.MemberRole), auth.GetUserID(c))
1518+
role := cc.VisibilityRole()
1519+
counts, _ := h.service.CountByType(c.Request().Context(), cc.Campaign.ID, role, auth.GetUserID(c))
15191520
csrfToken := middleware.GetCSRFToken(c)
15201521
errMsg := "failed to create entity type"
15211522
if appErr, ok := err.(*apperror.AppError); ok {
15221523
errMsg = appErr.Message
15231524
}
1525+
// Return partial for HTMX requests so the swap target (#entity-type-list) gets correct content.
1526+
// Use HX-Trigger to show a toast notification with the error message.
1527+
if middleware.IsHTMX(c) {
1528+
c.Response().Header().Set("HX-Retarget", "#entity-type-list")
1529+
c.Response().Header().Set("HX-Reswap", "innerHTML")
1530+
c.Response().Header().Set("HX-Trigger", `{"chronicle:notify":{"message":"`+errMsg+`","type":"error"}}`)
1531+
return middleware.Render(c, http.StatusOK,
1532+
EntityTypeListContent(cc, entityTypes, counts, csrfToken))
1533+
}
15241534
return middleware.Render(c, http.StatusOK,
15251535
EntityTypesManagePage(cc, entityTypes, counts, csrfToken, errMsg))
15261536
}

0 commit comments

Comments
 (0)