4 Commits

Author SHA1 Message Date
c60a0ed536 v1.6.0
Some checks failed
Release / build-and-release (push) Failing after 23s
2026-03-20 16:48:04 +00:00
087b8c0bb3 feat(web-organizations): add organization detail editing and isolate detail view state from global navigation 2026-03-20 16:48:04 +00:00
ffe7ffbde9 v1.5.1
Some checks failed
Release / build-and-release (push) Failing after 26s
2026-03-20 16:44:44 +00:00
b9a3d79b5f fix(web-app): update dashboard navigation to use the router directly and refresh admin tabs on login changes 2026-03-20 16:44:44 +00:00
12 changed files with 109 additions and 48 deletions

View File

@@ -1,5 +1,20 @@
# 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
- removes the global router workaround in the dashboard and imports appRouter directly
- re-filters resolved view tabs when login state changes so the Admin tab matches system admin access
- adds dashboard navigation support for the organizations view
## 2026-03-20 - 1.5.0 - feat(opsserver,web)
replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend

View File

@@ -1,6 +1,6 @@
{
"name": "@stack.gallery/registry",
"version": "1.5.0",
"version": "1.6.0",
"exports": "./mod.ts",
"nodeModulesDir": "auto",
"tasks": {

View File

@@ -1,6 +1,6 @@
{
"name": "@stack.gallery/registry",
"version": "1.5.0",
"version": "1.6.0",
"private": true,
"description": "Enterprise-grade multi-protocol package registry",
"type": "module",

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@stack.gallery/registry',
version: '1.5.0',
version: '1.6.0',
description: 'Enterprise-grade multi-protocol package registry'
}

View File

@@ -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');

View File

@@ -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' },
});

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@stack.gallery/registry',
version: '1.5.0',
version: '1.6.0',
description: 'Enterprise-grade multi-protocol package registry'
}

View File

@@ -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<{
organizationId: string;
}>(async (statePartArg, dataArg) => {

View File

@@ -72,19 +72,23 @@ export class SgAppShell extends DeesElement {
},
];
private allResolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
constructor() {
super();
document.title = 'Stack.Gallery Registry';
// Make appRouter globally accessible for view elements
(globalThis as any).__sgAppRouter = appRouter;
const loginSubscription = appstate.loginStatePart
.select((s) => s)
.subscribe((loginState) => {
this.loginState = loginState;
// Re-filter tabs when login state changes
if (loginState.isLoggedIn && this.allResolvedViewTabs.length > 0) {
this.resolvedViewTabs = loginState.identity?.isSystemAdmin
? this.allResolvedViewTabs
: this.allResolvedViewTabs.filter((t) => t.name !== 'Admin');
}
});
this.rxSubscriptions.push(loginSubscription);
@@ -148,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,
@@ -158,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;

View File

@@ -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,
@@ -77,24 +78,15 @@ export class SgViewDashboard extends DeesElement {
private handleNavigate(e: CustomEvent) {
const { type, id } = e.detail;
if (type === 'org' && id) {
const { appRouter } = await_import_router();
appRouter.navigateToEntity('organizations', id);
} else if (type === 'package' && id) {
const { appRouter } = await_import_router();
appRouter.navigateToEntity('packages', id);
} else if (type === 'packages') {
const { appRouter } = await_import_router();
appRouter.navigateToView('packages');
} else if (type === 'tokens') {
const { appRouter } = await_import_router();
appRouter.navigateToView('tokens');
} else if (type === 'organizations') {
appRouter.navigateToView('organizations');
}
}
}
// Lazy import to avoid circular dependency
function await_import_router() {
// Dynamic import not needed here since router is a separate module
// We use a workaround by importing at the module level
return { appRouter: (globalThis as any).__sgAppRouter };
}

View File

@@ -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`
<sg-organization-detail-view
.organization="${this.organizationsState.currentOrg}"
@@ -79,6 +84,8 @@ export class SgViewOrganizations extends DeesElement {
@back="${() => this.goBack()}"
@select-repo="${(e: CustomEvent) => this.selectRepo(e.detail.repositoryId)}"
@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>
`;
}
@@ -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,

View File

@@ -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`
<sg-package-detail-view
.package="${this.packagesState.currentPackage}"
@@ -98,18 +104,12 @@ export class SgViewPackages extends DeesElement {
}
private selectPackage(packageId: string) {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: packageId,
});
this.detailPackageId = packageId;
this.loadPackageDetail(packageId);
}
private goBack() {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: undefined,
});
this.detailPackageId = null;
appstate.packagesStatePart.setState({
...appstate.packagesStatePart.getState(),
currentPackage: null,