feat(web): add public package browsing and organization redirect management

This commit is contained in:
2026-03-21 10:54:10 +00:00
parent 392060bf23
commit 6d6ed61e70
10 changed files with 244 additions and 16 deletions

View File

@@ -6,3 +6,4 @@ export * from './sg-view-packages.js';
export * from './sg-view-tokens.js';
export * from './sg-view-settings.js';
export * from './sg-view-admin.js';
export * from './sg-view-public-packages.js';

View File

@@ -39,6 +39,9 @@ export class SgAppShell extends DeesElement {
@state()
accessor localAuthEnabled: boolean = true;
@state()
accessor showLoginForm: boolean = false;
private viewTabs = [
{
name: 'Dashboard',
@@ -117,7 +120,24 @@ export class SgAppShell extends DeesElement {
];
public render(): TemplateResult {
if (!this.loginState.isLoggedIn) {
// Authenticated: full appdash
if (this.loginState.isLoggedIn) {
return html`
<div class="maincontainer">
<dees-simple-appdash
name="Stack.Gallery"
.viewTabs=${this.resolvedViewTabs}
.selectedView=${this.resolvedViewTabs.find(
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView,
) || this.resolvedViewTabs[0]}
>
</dees-simple-appdash>
</div>
`;
}
// Login form requested
if (this.showLoginForm) {
return html`
<div class="maincontainer">
<sg-login-view
@@ -133,16 +153,14 @@ export class SgAppShell extends DeesElement {
`;
}
// Public browsing mode: package search + detail
return html`
<div class="maincontainer">
<dees-simple-appdash
name="Stack.Gallery"
.viewTabs=${this.resolvedViewTabs}
.selectedView=${this.resolvedViewTabs.find(
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView,
) || this.resolvedViewTabs[0]}
<sg-public-layout
@sign-in=${() => { this.showLoginForm = true; }}
>
</dees-simple-appdash>
<sg-view-public-packages></sg-view-public-packages>
</sg-public-layout>
</div>
`;
}

View File

@@ -19,6 +19,7 @@ export class SgViewOrganizations extends DeesElement {
currentOrg: null,
repositories: [],
members: [],
redirects: [],
};
@state()
@@ -72,6 +73,10 @@ export class SgViewOrganizations extends DeesElement {
appstate.fetchMembersAction,
{ organizationId: orgId },
);
await appstate.organizationsStatePart.dispatchAction(
appstate.fetchRedirectsAction,
{ organizationId: orgId },
);
}
public render(): TemplateResult {
@@ -81,11 +86,13 @@ export class SgViewOrganizations extends DeesElement {
.organization="${this.organizationsState.currentOrg}"
.repositories="${this.organizationsState.repositories}"
.members="${this.organizationsState.members}"
.redirects="${this.organizationsState.redirects}"
@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)}"
@delete-redirect="${(e: CustomEvent) => this.handleDeleteRedirect(e.detail.redirectId)}"
></sg-organization-detail-view>
`;
}
@@ -116,11 +123,21 @@ export class SgViewOrganizations extends DeesElement {
currentOrg: null,
repositories: [],
members: [],
redirects: [],
});
}
private async handleDeleteRedirect(redirectId: string) {
if (!this.detailOrgId) return;
await appstate.organizationsStatePart.dispatchAction(
appstate.deleteRedirectAction,
{ redirectId, organizationId: this.detailOrgId },
);
}
private async handleEditOrg(data: {
organizationId: string;
name?: string;
displayName?: string;
description?: string;
website?: string;

View File

@@ -0,0 +1,132 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-public-packages')
export class SgViewPublicPackages extends DeesElement {
@state()
accessor packagesState: appstate.IPackagesState = {
packages: [],
currentPackage: null,
versions: [],
total: 0,
query: '',
protocolFilter: '',
};
@state()
accessor detailPackageId: string | null = null;
@state()
accessor loading: boolean = false;
constructor() {
super();
const pkgSub = appstate.packagesStatePart
.select((s) => s)
.subscribe((s) => {
this.packagesState = s;
this.loading = false;
});
this.rxSubscriptions.push(pkgSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
this.loading = true;
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ offset: 0 },
);
this.loading = false;
}
public render(): TemplateResult {
if (this.detailPackageId && this.packagesState.currentPackage) {
return html`
<sg-package-detail-view
.package="${this.packagesState.currentPackage}"
.versions="${this.packagesState.versions}"
@back="${() => this.goBack()}"
></sg-package-detail-view>
`;
}
return html`
<sg-public-search-view
.packages="${this.packagesState.packages}"
.total="${this.packagesState.total}"
.query="${this.packagesState.query}"
.activeProtocol="${this.packagesState.protocolFilter}"
.protocols="${['npm', 'oci', 'maven', 'cargo', 'pypi', 'composer', 'rubygems']}"
.loading="${this.loading}"
@search="${(e: CustomEvent) => this.search(e.detail.query)}"
@filter="${(e: CustomEvent) => this.filter(e.detail.protocol)}"
@select="${(e: CustomEvent) => this.selectPackage(e.detail.packageId)}"
@page="${(e: CustomEvent) => this.paginate(e.detail.offset)}"
></sg-public-search-view>
`;
}
private async selectPackage(packageId: string) {
this.detailPackageId = packageId;
await appstate.packagesStatePart.dispatchAction(
appstate.fetchPackageAction,
{ packageId },
);
await appstate.packagesStatePart.dispatchAction(
appstate.fetchPackageVersionsAction,
{ packageId },
);
}
private goBack() {
this.detailPackageId = null;
appstate.packagesStatePart.setState({
...appstate.packagesStatePart.getState(),
currentPackage: null,
versions: [],
});
}
private async search(query: string) {
this.loading = true;
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ query, protocol: this.packagesState.protocolFilter, offset: 0 },
);
}
private async filter(protocol: string) {
this.loading = true;
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ query: this.packagesState.query, protocol, offset: 0 },
);
}
private async paginate(offset: number) {
this.loading = true;
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{
query: this.packagesState.query,
protocol: this.packagesState.protocolFilter,
offset,
},
);
}
}

View File

@@ -3,10 +3,8 @@ import { css } from '@design.estate/dees-element';
export const viewHostCss = css`
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: auto;
padding: 24px;
box-sizing: border-box;
margin: auto;
max-width: 1280px;
padding: 16px 16px;
}
`;