From 087b8c0bb3652d0bf355147a74b78e88526d044f Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 20 Mar 2026 16:48:04 +0000 Subject: [PATCH] feat(web-organizations): add organization detail editing and isolate detail view state from global navigation --- changelog.md | 8 ++++ ts/00_commitinfo_data.ts | 2 +- ts/opsserver/handlers/admin.handler.ts | 8 ++-- ts/registry.ts | 4 +- ts_web/00_commitinfo_data.ts | 2 +- ts_web/appstate.ts | 24 ++++++++++++ ts_web/elements/sg-app-shell.ts | 6 +-- ts_web/elements/sg-view-organizations.ts | 48 ++++++++++++++++++------ ts_web/elements/sg-view-packages.ts | 20 +++++----- 9 files changed, 90 insertions(+), 32 deletions(-) diff --git a/changelog.md b/changelog.md index a0bc306..8181c85 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-20 - 1.6.0 - feat(web-organizations) +add organization detail editing and isolate detail view state from global navigation + +- adds an update organization action to persist organization detail edits from the detail view +- updates organization and package views to track selected detail entities locally instead of mutating global ui state +- preserves resolved app shell tabs for role-based filtering after async tab loading +- includes type-cast fixes for admin auth provider responses and bundled file Response bodies + ## 2026-03-20 - 1.5.1 - fix(web-app) update dashboard navigation to use the router directly and refresh admin tabs on login changes diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 844b385..4643bfe 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@stack.gallery/registry', - version: '1.5.1', + version: '1.6.0', description: 'Enterprise-grade multi-protocol package registry' } diff --git a/ts/opsserver/handlers/admin.handler.ts b/ts/opsserver/handlers/admin.handler.ts index c3cc962..ca66e56 100644 --- a/ts/opsserver/handlers/admin.handler.ts +++ b/ts/opsserver/handlers/admin.handler.ts @@ -26,7 +26,7 @@ export class AdminHandler { try { const providers = await AuthProvider.getAllProviders(); return { - providers: providers.map((p) => p.toAdminInfo()), + providers: providers.map((p) => p.toAdminInfo() as unknown as interfaces.data.IAuthProvider), }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; @@ -124,7 +124,7 @@ export class AdminHandler { }, }); - return { provider: provider.toAdminInfo() }; + return { provider: provider.toAdminInfo() as unknown as interfaces.data.IAuthProvider }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to create provider'); @@ -146,7 +146,7 @@ export class AdminHandler { throw new plugins.typedrequest.TypedResponseError('Provider not found'); } - return { provider: provider.toAdminInfo() }; + return { provider: provider.toAdminInfo() as unknown as interfaces.data.IAuthProvider }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to get provider'); @@ -235,7 +235,7 @@ export class AdminHandler { metadata: { providerName: provider.name }, }); - return { provider: provider.toAdminInfo() }; + return { provider: provider.toAdminInfo() as unknown as interfaces.data.IAuthProvider }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) throw error; throw new plugins.typedrequest.TypedResponseError('Failed to update provider'); diff --git a/ts/registry.ts b/ts/registry.ts index c6d1055..d9ce847 100644 --- a/ts/registry.ts +++ b/ts/registry.ts @@ -325,7 +325,7 @@ export class StackGalleryRegistry { // Get bundled file const file = bundledFileMap.get(filePath); if (file) { - return new Response(file.data, { + return new Response(file.data as unknown as BodyInit, { status: 200, headers: { 'Content-Type': file.contentType }, }); @@ -334,7 +334,7 @@ export class StackGalleryRegistry { // SPA fallback: serve index.html for unknown paths const indexFile = bundledFileMap.get('/index.html'); if (indexFile) { - return new Response(indexFile.data, { + return new Response(indexFile.data as unknown as BodyInit, { status: 200, headers: { 'Content-Type': 'text/html' }, }); diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 844b385..4643bfe 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@stack.gallery/registry', - version: '1.5.1', + version: '1.6.0', description: 'Enterprise-grade multi-protocol package registry' } diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 0b482fe..2f3845d 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -276,6 +276,30 @@ export const createOrganizationAction = organizationsStatePart.createAction<{ } }); +export const updateOrganizationAction = organizationsStatePart.createAction<{ + organizationId: string; + displayName?: string; + description?: string; + website?: string; + isPublic?: boolean; +}>(async (statePartArg, dataArg) => { + const context = getActionContext(); + if (!context.identity) return statePartArg.getState(); + try { + const typedRequest = createTypedRequest( + 'updateOrganization', + ); + const response = await typedRequest.fire({ + identity: context.identity, + ...dataArg, + }); + // Update the current org in state + return { ...statePartArg.getState(), currentOrg: response.organization }; + } catch { + return statePartArg.getState(); + } +}); + export const deleteOrganizationAction = organizationsStatePart.createAction<{ organizationId: string; }>(async (statePartArg, dataArg) => { diff --git a/ts_web/elements/sg-app-shell.ts b/ts_web/elements/sg-app-shell.ts index 6c5c0b4..cea8de8 100644 --- a/ts_web/elements/sg-app-shell.ts +++ b/ts_web/elements/sg-app-shell.ts @@ -152,7 +152,7 @@ export class SgAppShell extends DeesElement { this.fetchAuthProviders(); // Resolve async view tab imports - const allTabs = await Promise.all( + this.allResolvedViewTabs = await Promise.all( this.viewTabs.map(async (tab) => ({ name: tab.name, iconName: tab.iconName, @@ -162,8 +162,8 @@ export class SgAppShell extends DeesElement { // Filter admin tab based on user role this.resolvedViewTabs = this.loginState.identity?.isSystemAdmin - ? allTabs - : allTabs.filter((t) => t.name !== 'Admin'); + ? this.allResolvedViewTabs + : this.allResolvedViewTabs.filter((t) => t.name !== 'Admin'); this.requestUpdate(); await this.updateComplete; diff --git a/ts_web/elements/sg-view-organizations.ts b/ts_web/elements/sg-view-organizations.ts index e1d7ad1..51219b2 100644 --- a/ts_web/elements/sg-view-organizations.ts +++ b/ts_web/elements/sg-view-organizations.ts @@ -1,5 +1,6 @@ import * as appstate from '../appstate.js'; import * as shared from './shared/index.js'; +import { appRouter } from '../router.js'; import { css, cssManager, @@ -23,6 +24,9 @@ export class SgViewOrganizations extends DeesElement { @state() accessor uiState: appstate.IUiState = { activeView: 'organizations' }; + @state() + accessor detailOrgId: string | null = null; + constructor() { super(); const orgSub = appstate.organizationsStatePart @@ -48,9 +52,10 @@ export class SgViewOrganizations extends DeesElement { async connectedCallback() { super.connectedCallback(); await appstate.organizationsStatePart.dispatchAction(appstate.fetchOrganizationsAction, null); - // If there's an entity ID, load the detail + // If there's an entity ID from the URL, copy it to internal state if (this.uiState.activeEntityId) { - await this.loadOrgDetail(this.uiState.activeEntityId); + this.detailOrgId = this.uiState.activeEntityId; + await this.loadOrgDetail(this.detailOrgId); } } @@ -70,7 +75,7 @@ export class SgViewOrganizations extends DeesElement { } public render(): TemplateResult { - if (this.uiState.activeEntityId && this.organizationsState.currentOrg) { + if (this.detailOrgId && this.organizationsState.currentOrg) { return html` `; } @@ -93,10 +100,7 @@ export class SgViewOrganizations extends DeesElement { } private selectOrg(orgId: string) { - appstate.uiStatePart.setState({ - ...appstate.uiStatePart.getState(), - activeEntityId: orgId, - }); + this.detailOrgId = orgId; this.loadOrgDetail(orgId); } @@ -106,10 +110,7 @@ export class SgViewOrganizations extends DeesElement { } private goBack() { - appstate.uiStatePart.setState({ - ...appstate.uiStatePart.getState(), - activeEntityId: undefined, - }); + this.detailOrgId = null; appstate.organizationsStatePart.setState({ ...appstate.organizationsStatePart.getState(), currentOrg: null, @@ -118,6 +119,31 @@ export class SgViewOrganizations extends DeesElement { }); } + private async handleEditOrg(data: { + organizationId: string; + displayName?: string; + description?: string; + website?: string; + isPublic?: boolean; + }) { + await appstate.organizationsStatePart.dispatchAction( + appstate.updateOrganizationAction, + data, + ); + // Re-load detail to reflect changes + if (this.detailOrgId) { + await this.loadOrgDetail(this.detailOrgId); + } + } + + private async handleDeleteOrg(organizationId: string) { + await appstate.organizationsStatePart.dispatchAction( + appstate.deleteOrganizationAction, + { organizationId }, + ); + this.goBack(); + } + private async createOrg(data: { name: string; displayName?: string; description?: string }) { await appstate.organizationsStatePart.dispatchAction( appstate.createOrganizationAction, diff --git a/ts_web/elements/sg-view-packages.ts b/ts_web/elements/sg-view-packages.ts index e7e3f16..46b04e7 100644 --- a/ts_web/elements/sg-view-packages.ts +++ b/ts_web/elements/sg-view-packages.ts @@ -1,5 +1,6 @@ import * as appstate from '../appstate.js'; import * as shared from './shared/index.js'; +import { appRouter } from '../router.js'; import { css, cssManager, @@ -25,6 +26,9 @@ export class SgViewPackages extends DeesElement { @state() accessor uiState: appstate.IUiState = { activeView: 'packages' }; + @state() + accessor detailPackageId: string | null = null; + constructor() { super(); const pkgSub = appstate.packagesStatePart @@ -49,8 +53,10 @@ export class SgViewPackages extends DeesElement { async connectedCallback() { super.connectedCallback(); + // If there's an entity ID from the URL, copy it to internal state if (this.uiState.activeEntityId) { - await this.loadPackageDetail(this.uiState.activeEntityId); + this.detailPackageId = this.uiState.activeEntityId; + await this.loadPackageDetail(this.detailPackageId); } else { await appstate.packagesStatePart.dispatchAction( appstate.searchPackagesAction, @@ -71,7 +77,7 @@ export class SgViewPackages extends DeesElement { } public render(): TemplateResult { - if (this.uiState.activeEntityId && this.packagesState.currentPackage) { + if (this.detailPackageId && this.packagesState.currentPackage) { return html`