feat(web-organizations): add organization detail editing and isolate detail view state from global navigation
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-03-20 - 1.5.1 - fix(web-app)
|
||||||
update dashboard navigation to use the router directly and refresh admin tabs on login changes
|
update dashboard navigation to use the router directly and refresh admin tabs on login changes
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@stack.gallery/registry',
|
name: '@stack.gallery/registry',
|
||||||
version: '1.5.1',
|
version: '1.6.0',
|
||||||
description: 'Enterprise-grade multi-protocol package registry'
|
description: 'Enterprise-grade multi-protocol package registry'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class AdminHandler {
|
|||||||
try {
|
try {
|
||||||
const providers = await AuthProvider.getAllProviders();
|
const providers = await AuthProvider.getAllProviders();
|
||||||
return {
|
return {
|
||||||
providers: providers.map((p) => p.toAdminInfo()),
|
providers: providers.map((p) => p.toAdminInfo() as unknown as interfaces.data.IAuthProvider),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw 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) {
|
} catch (error) {
|
||||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||||
throw new plugins.typedrequest.TypedResponseError('Failed to create provider');
|
throw new plugins.typedrequest.TypedResponseError('Failed to create provider');
|
||||||
@@ -146,7 +146,7 @@ export class AdminHandler {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Provider not found');
|
throw new plugins.typedrequest.TypedResponseError('Provider not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { provider: provider.toAdminInfo() };
|
return { provider: provider.toAdminInfo() as unknown as interfaces.data.IAuthProvider };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||||
throw new plugins.typedrequest.TypedResponseError('Failed to get provider');
|
throw new plugins.typedrequest.TypedResponseError('Failed to get provider');
|
||||||
@@ -235,7 +235,7 @@ export class AdminHandler {
|
|||||||
metadata: { providerName: provider.name },
|
metadata: { providerName: provider.name },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { provider: provider.toAdminInfo() };
|
return { provider: provider.toAdminInfo() as unknown as interfaces.data.IAuthProvider };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||||
throw new plugins.typedrequest.TypedResponseError('Failed to update provider');
|
throw new plugins.typedrequest.TypedResponseError('Failed to update provider');
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ export class StackGalleryRegistry {
|
|||||||
// Get bundled file
|
// Get bundled file
|
||||||
const file = bundledFileMap.get(filePath);
|
const file = bundledFileMap.get(filePath);
|
||||||
if (file) {
|
if (file) {
|
||||||
return new Response(file.data, {
|
return new Response(file.data as unknown as BodyInit, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': file.contentType },
|
headers: { 'Content-Type': file.contentType },
|
||||||
});
|
});
|
||||||
@@ -334,7 +334,7 @@ export class StackGalleryRegistry {
|
|||||||
// SPA fallback: serve index.html for unknown paths
|
// SPA fallback: serve index.html for unknown paths
|
||||||
const indexFile = bundledFileMap.get('/index.html');
|
const indexFile = bundledFileMap.get('/index.html');
|
||||||
if (indexFile) {
|
if (indexFile) {
|
||||||
return new Response(indexFile.data, {
|
return new Response(indexFile.data as unknown as BodyInit, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'text/html' },
|
headers: { 'Content-Type': 'text/html' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@stack.gallery/registry',
|
name: '@stack.gallery/registry',
|
||||||
version: '1.5.1',
|
version: '1.6.0',
|
||||||
description: 'Enterprise-grade multi-protocol package registry'
|
description: 'Enterprise-grade multi-protocol package registry'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<interfaces.requests.IReq_UpdateOrganization>(
|
||||||
|
'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<{
|
export const deleteOrganizationAction = organizationsStatePart.createAction<{
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export class SgAppShell extends DeesElement {
|
|||||||
this.fetchAuthProviders();
|
this.fetchAuthProviders();
|
||||||
|
|
||||||
// Resolve async view tab imports
|
// Resolve async view tab imports
|
||||||
const allTabs = await Promise.all(
|
this.allResolvedViewTabs = await Promise.all(
|
||||||
this.viewTabs.map(async (tab) => ({
|
this.viewTabs.map(async (tab) => ({
|
||||||
name: tab.name,
|
name: tab.name,
|
||||||
iconName: tab.iconName,
|
iconName: tab.iconName,
|
||||||
@@ -162,8 +162,8 @@ export class SgAppShell extends DeesElement {
|
|||||||
|
|
||||||
// Filter admin tab based on user role
|
// Filter admin tab based on user role
|
||||||
this.resolvedViewTabs = this.loginState.identity?.isSystemAdmin
|
this.resolvedViewTabs = this.loginState.identity?.isSystemAdmin
|
||||||
? allTabs
|
? this.allResolvedViewTabs
|
||||||
: allTabs.filter((t) => t.name !== 'Admin');
|
: this.allResolvedViewTabs.filter((t) => t.name !== 'Admin');
|
||||||
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
|
import { appRouter } from '../router.js';
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
@@ -23,6 +24,9 @@ export class SgViewOrganizations extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor uiState: appstate.IUiState = { activeView: 'organizations' };
|
accessor uiState: appstate.IUiState = { activeView: 'organizations' };
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor detailOrgId: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const orgSub = appstate.organizationsStatePart
|
const orgSub = appstate.organizationsStatePart
|
||||||
@@ -48,9 +52,10 @@ export class SgViewOrganizations extends DeesElement {
|
|||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
await appstate.organizationsStatePart.dispatchAction(appstate.fetchOrganizationsAction, null);
|
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) {
|
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 {
|
public render(): TemplateResult {
|
||||||
if (this.uiState.activeEntityId && this.organizationsState.currentOrg) {
|
if (this.detailOrgId && this.organizationsState.currentOrg) {
|
||||||
return html`
|
return html`
|
||||||
<sg-organization-detail-view
|
<sg-organization-detail-view
|
||||||
.organization="${this.organizationsState.currentOrg}"
|
.organization="${this.organizationsState.currentOrg}"
|
||||||
@@ -79,6 +84,8 @@ export class SgViewOrganizations extends DeesElement {
|
|||||||
@back="${() => this.goBack()}"
|
@back="${() => this.goBack()}"
|
||||||
@select-repo="${(e: CustomEvent) => this.selectRepo(e.detail.repositoryId)}"
|
@select-repo="${(e: CustomEvent) => this.selectRepo(e.detail.repositoryId)}"
|
||||||
@create-repo="${() => {/* TODO: create repo modal */}}"
|
@create-repo="${() => {/* TODO: create repo modal */}}"
|
||||||
|
@edit="${(e: CustomEvent) => this.handleEditOrg(e.detail)}"
|
||||||
|
@delete="${(e: CustomEvent) => this.handleDeleteOrg(e.detail.organizationId)}"
|
||||||
></sg-organization-detail-view>
|
></sg-organization-detail-view>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -93,10 +100,7 @@ export class SgViewOrganizations extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectOrg(orgId: string) {
|
private selectOrg(orgId: string) {
|
||||||
appstate.uiStatePart.setState({
|
this.detailOrgId = orgId;
|
||||||
...appstate.uiStatePart.getState(),
|
|
||||||
activeEntityId: orgId,
|
|
||||||
});
|
|
||||||
this.loadOrgDetail(orgId);
|
this.loadOrgDetail(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,10 +110,7 @@ export class SgViewOrganizations extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private goBack() {
|
private goBack() {
|
||||||
appstate.uiStatePart.setState({
|
this.detailOrgId = null;
|
||||||
...appstate.uiStatePart.getState(),
|
|
||||||
activeEntityId: undefined,
|
|
||||||
});
|
|
||||||
appstate.organizationsStatePart.setState({
|
appstate.organizationsStatePart.setState({
|
||||||
...appstate.organizationsStatePart.getState(),
|
...appstate.organizationsStatePart.getState(),
|
||||||
currentOrg: null,
|
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 }) {
|
private async createOrg(data: { name: string; displayName?: string; description?: string }) {
|
||||||
await appstate.organizationsStatePart.dispatchAction(
|
await appstate.organizationsStatePart.dispatchAction(
|
||||||
appstate.createOrganizationAction,
|
appstate.createOrganizationAction,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
|
import { appRouter } from '../router.js';
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
@@ -25,6 +26,9 @@ export class SgViewPackages extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor uiState: appstate.IUiState = { activeView: 'packages' };
|
accessor uiState: appstate.IUiState = { activeView: 'packages' };
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor detailPackageId: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const pkgSub = appstate.packagesStatePart
|
const pkgSub = appstate.packagesStatePart
|
||||||
@@ -49,8 +53,10 @@ export class SgViewPackages extends DeesElement {
|
|||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
// If there's an entity ID from the URL, copy it to internal state
|
||||||
if (this.uiState.activeEntityId) {
|
if (this.uiState.activeEntityId) {
|
||||||
await this.loadPackageDetail(this.uiState.activeEntityId);
|
this.detailPackageId = this.uiState.activeEntityId;
|
||||||
|
await this.loadPackageDetail(this.detailPackageId);
|
||||||
} else {
|
} else {
|
||||||
await appstate.packagesStatePart.dispatchAction(
|
await appstate.packagesStatePart.dispatchAction(
|
||||||
appstate.searchPackagesAction,
|
appstate.searchPackagesAction,
|
||||||
@@ -71,7 +77,7 @@ export class SgViewPackages extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
if (this.uiState.activeEntityId && this.packagesState.currentPackage) {
|
if (this.detailPackageId && this.packagesState.currentPackage) {
|
||||||
return html`
|
return html`
|
||||||
<sg-package-detail-view
|
<sg-package-detail-view
|
||||||
.package="${this.packagesState.currentPackage}"
|
.package="${this.packagesState.currentPackage}"
|
||||||
@@ -98,18 +104,12 @@ export class SgViewPackages extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectPackage(packageId: string) {
|
private selectPackage(packageId: string) {
|
||||||
appstate.uiStatePart.setState({
|
this.detailPackageId = packageId;
|
||||||
...appstate.uiStatePart.getState(),
|
|
||||||
activeEntityId: packageId,
|
|
||||||
});
|
|
||||||
this.loadPackageDetail(packageId);
|
this.loadPackageDetail(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private goBack() {
|
private goBack() {
|
||||||
appstate.uiStatePart.setState({
|
this.detailPackageId = null;
|
||||||
...appstate.uiStatePart.getState(),
|
|
||||||
activeEntityId: undefined,
|
|
||||||
});
|
|
||||||
appstate.packagesStatePart.setState({
|
appstate.packagesStatePart.setState({
|
||||||
...appstate.packagesStatePart.getState(),
|
...appstate.packagesStatePart.getState(),
|
||||||
currentPackage: null,
|
currentPackage: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user