Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c589476590 | |||
| 03529bc140 | |||
| ffade4d5ca | |||
| 9c4636906a | |||
| f44b03b47d | |||
| 6d6ed61e70 | |||
| 392060bf23 | |||
| 8cb5e4fa96 |
@@ -33,9 +33,6 @@ jobs:
|
||||
- name: Install root dependencies
|
||||
run: pnpm install --ignore-scripts
|
||||
|
||||
- name: Install UI dependencies
|
||||
run: cd ui && pnpm install
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
@@ -56,11 +53,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Angular UI
|
||||
run: cd ui && pnpm run build
|
||||
|
||||
- name: Bundle UI into TypeScript
|
||||
run: deno run --allow-all scripts/bundle-ui.ts
|
||||
- name: Build UI
|
||||
run: npx tsbundle
|
||||
|
||||
- name: Compile binaries for all platforms
|
||||
run: mkdir -p dist/binaries && npx tsdeno compile
|
||||
|
||||
27
changelog.md
27
changelog.md
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-21 - 1.8.2 - fix(deps)
|
||||
replace local catalog dependency with published version and simplify npm fixture auth config
|
||||
|
||||
- switch @stack.gallery/catalog from a local file reference to the published ^1.0.1 release
|
||||
- update the npm test fixture .npmrc to use only an auth token entry
|
||||
|
||||
## 2026-03-21 - 1.8.1 - fix(release,test)
|
||||
streamline release UI bundling and add npm fixture registry configuration
|
||||
|
||||
- Update the release workflow to build the UI with tsbundle directly instead of installing UI-specific dependencies and running a separate bundling script
|
||||
- Add an .npmrc fixture for the demo npm package to configure the scoped registry and authentication token for local registry tests
|
||||
|
||||
## 2026-03-21 - 1.8.0 - feat(web)
|
||||
add public package browsing and organization redirect management
|
||||
|
||||
- introduces a public packages view and root route behavior for unauthenticated users
|
||||
- updates the app shell to support public browsing mode with an optional sign-in flow
|
||||
- adds organization redirect state, fetching, and deletion in the organization detail view
|
||||
|
||||
## 2026-03-20 - 1.7.0 - feat(organization)
|
||||
add organization rename redirects and redirect management endpoints
|
||||
|
||||
- add OrgRedirect model and resolve organizations by historical names
|
||||
- support renaming organizations while preserving the previous handle as a redirect alias
|
||||
- add typed requests to list and delete organization redirects with admin permission checks
|
||||
- allow organization update actions to send name changes
|
||||
|
||||
## 2026-03-20 - 1.6.0 - feat(web-organizations)
|
||||
add organization detail editing and isolate detail view state from global navigation
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@stack.gallery/registry",
|
||||
"version": "1.6.0",
|
||||
"version": "1.8.2",
|
||||
"exports": "./mod.ts",
|
||||
"nodeModulesDir": "auto",
|
||||
"tasks": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@stack.gallery/registry",
|
||||
"version": "1.6.0",
|
||||
"version": "1.8.2",
|
||||
"private": true,
|
||||
"description": "Enterprise-grade multi-protocol package registry",
|
||||
"type": "module",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@design.estate/dees-catalog": "^3.43.0",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@push.rocks/smartguard": "^3.1.0",
|
||||
"@stack.gallery/catalog": "file:../catalog"
|
||||
"@stack.gallery/catalog": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -27,8 +27,8 @@ importers:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
'@stack.gallery/catalog':
|
||||
specifier: file:../catalog
|
||||
version: file:../catalog(@tiptap/pm@2.27.2)
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(@tiptap/pm@2.27.2)
|
||||
devDependencies:
|
||||
'@git.zone/tsbundle':
|
||||
specifier: ^2.8.3
|
||||
@@ -845,8 +845,8 @@ packages:
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
'@stack.gallery/catalog@file:../catalog':
|
||||
resolution: {directory: ../catalog, type: directory}
|
||||
'@stack.gallery/catalog@1.0.1':
|
||||
resolution: {integrity: sha512-9wlSACeahEVWKTqcLKQq/kbjOz7p0v5l5NQ0UbhFMYcpFtcGx2mZMHGdSUNrHUHQh7zUDiU5qW6E8GlLJBP43A==}
|
||||
|
||||
'@tempfix/idb@8.0.3':
|
||||
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
|
||||
@@ -3483,7 +3483,7 @@ snapshots:
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@stack.gallery/catalog@file:../catalog(@tiptap/pm@2.27.2)':
|
||||
'@stack.gallery/catalog@1.0.1(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.49.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.1
|
||||
|
||||
1
test/fixtures/npm/@stack-test/demo-package/.npmrc
vendored
Normal file
1
test/fixtures/npm/@stack-test/demo-package/.npmrc
vendored
Normal file
@@ -0,0 +1 @@
|
||||
//localhost:3000/-/npm/npm-test/:_authToken=srg_e0b7410154fdbcbbe077cfead50538438f3b11da136b801099d0c66bb410db22
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@stack.gallery/registry',
|
||||
version: '1.6.0',
|
||||
version: '1.8.2',
|
||||
description: 'Enterprise-grade multi-protocol package registry'
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export { ApiToken } from './apitoken.ts';
|
||||
export { Session } from './session.ts';
|
||||
export { AuditLog } from './auditlog.ts';
|
||||
|
||||
// Organization redirects
|
||||
export { OrgRedirect } from './org.redirect.ts';
|
||||
|
||||
// External authentication models
|
||||
export { AuthProvider } from './auth.provider.ts';
|
||||
export { ExternalIdentity } from './external.identity.ts';
|
||||
|
||||
59
ts/models/org.redirect.ts
Normal file
59
ts/models/org.redirect.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* OrgRedirect model - stores old org handles as redirect aliases
|
||||
* When an org is renamed, the old name becomes a redirect pointing to the org.
|
||||
* Redirects can be explicitly deleted by org admins.
|
||||
*/
|
||||
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { db } from './db.ts';
|
||||
|
||||
@plugins.smartdata.Collection(() => db)
|
||||
export class OrgRedirect extends plugins.smartdata.SmartDataDbDoc<OrgRedirect, OrgRedirect> {
|
||||
@plugins.smartdata.unI()
|
||||
public id: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index({ unique: true })
|
||||
public oldName: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
@plugins.smartdata.index()
|
||||
public organizationId: string = '';
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
/**
|
||||
* Create a redirect from an old org name to the current org
|
||||
*/
|
||||
public static async create(oldName: string, organizationId: string): Promise<OrgRedirect> {
|
||||
const redirect = new OrgRedirect();
|
||||
redirect.id = `redirect:${oldName}`;
|
||||
redirect.oldName = oldName;
|
||||
redirect.organizationId = organizationId;
|
||||
redirect.createdAt = new Date();
|
||||
await redirect.save();
|
||||
return redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a redirect by the old name
|
||||
*/
|
||||
public static async findByName(name: string): Promise<OrgRedirect | null> {
|
||||
return await OrgRedirect.getInstance({ oldName: name } as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all redirects for an organization
|
||||
*/
|
||||
public static async getByOrgId(organizationId: string): Promise<OrgRedirect[]> {
|
||||
return await OrgRedirect.getInstances({ organizationId } as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a redirect by ID
|
||||
*/
|
||||
public static async findById(id: string): Promise<OrgRedirect | null> {
|
||||
return await OrgRedirect.getInstance({ id } as any);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.ts';
|
||||
import * as interfaces from '../../../ts_interfaces/index.ts';
|
||||
import type { OpsServer } from '../classes.opsserver.ts';
|
||||
import { requireValidIdentity } from '../helpers/guards.ts';
|
||||
import { Organization, OrganizationMember, User } from '../../models/index.ts';
|
||||
import { Organization, OrganizationMember, OrgRedirect, User } from '../../models/index.ts';
|
||||
import { PermissionService } from '../../services/permission.service.ts';
|
||||
import { AuditService } from '../../services/audit.service.ts';
|
||||
|
||||
@@ -19,9 +19,18 @@ export class OrganizationHandler {
|
||||
* Helper to resolve organization by ID or name
|
||||
*/
|
||||
private async resolveOrganization(idOrName: string): Promise<Organization | null> {
|
||||
return idOrName.startsWith('Organization:')
|
||||
? await Organization.findById(idOrName)
|
||||
: await Organization.findByName(idOrName);
|
||||
if (idOrName.startsWith('Organization:')) {
|
||||
return await Organization.findById(idOrName);
|
||||
}
|
||||
// Try direct name lookup first
|
||||
const org = await Organization.findByName(idOrName);
|
||||
if (org) return org;
|
||||
// Check redirects
|
||||
const redirect = await OrgRedirect.findByName(idOrName);
|
||||
if (redirect) {
|
||||
return await Organization.findById(redirect.organizationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
@@ -232,6 +241,36 @@ export class OrganizationHandler {
|
||||
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||||
}
|
||||
|
||||
// Handle rename
|
||||
if (dataArg.name && dataArg.name !== org.name) {
|
||||
const newName = dataArg.name;
|
||||
// Validate name format
|
||||
if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(newName)) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Name must be lowercase alphanumeric with optional hyphens and dots',
|
||||
);
|
||||
}
|
||||
// Check new name not taken by another org
|
||||
const existingOrg = await Organization.findByName(newName);
|
||||
if (existingOrg && existingOrg.id !== org.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Organization name already taken');
|
||||
}
|
||||
// Check new name not taken by a redirect pointing elsewhere
|
||||
const existingRedirect = await OrgRedirect.findByName(newName);
|
||||
if (existingRedirect && existingRedirect.organizationId !== org.id) {
|
||||
throw new plugins.typedrequest.TypedResponseError(
|
||||
'Name is reserved as a redirect for another organization',
|
||||
);
|
||||
}
|
||||
// If new name is one of our own redirects, delete that redirect
|
||||
if (existingRedirect && existingRedirect.organizationId === org.id) {
|
||||
await existingRedirect.delete();
|
||||
}
|
||||
// Create redirect from old name
|
||||
await OrgRedirect.create(org.name, org.id);
|
||||
org.name = newName;
|
||||
}
|
||||
|
||||
if (dataArg.displayName !== undefined) org.displayName = dataArg.displayName;
|
||||
if (dataArg.description !== undefined) org.description = dataArg.description;
|
||||
if (dataArg.avatarUrl !== undefined) org.avatarUrl = dataArg.avatarUrl;
|
||||
@@ -544,5 +583,69 @@ export class OrganizationHandler {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Get Org Redirects
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetOrgRedirects>(
|
||||
'getOrgRedirects',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const org = await this.resolveOrganization(dataArg.organizationId);
|
||||
if (!org) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Organization not found');
|
||||
}
|
||||
|
||||
const redirects = await OrgRedirect.getByOrgId(org.id);
|
||||
return {
|
||||
redirects: redirects.map((r) => ({
|
||||
id: r.id,
|
||||
oldName: r.oldName,
|
||||
organizationId: r.organizationId,
|
||||
createdAt: r.createdAt instanceof Date
|
||||
? r.createdAt.toISOString()
|
||||
: String(r.createdAt),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to get redirects');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Delete Org Redirect
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteOrgRedirect>(
|
||||
'deleteOrgRedirect',
|
||||
async (dataArg) => {
|
||||
await requireValidIdentity(this.opsServerRef.authHandler, dataArg);
|
||||
|
||||
try {
|
||||
const redirect = await OrgRedirect.findById(dataArg.redirectId);
|
||||
if (!redirect) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Redirect not found');
|
||||
}
|
||||
|
||||
// Check permission on the org
|
||||
const canManage = await this.permissionService.canManageOrganization(
|
||||
dataArg.identity.userId,
|
||||
redirect.organizationId,
|
||||
);
|
||||
if (!canManage && !dataArg.identity.isSystemAdmin) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||||
}
|
||||
|
||||
await redirect.delete();
|
||||
return { message: 'Redirect deleted successfully' };
|
||||
} catch (error) {
|
||||
if (error instanceof plugins.typedrequest.TypedResponseError) throw error;
|
||||
throw new plugins.typedrequest.TypedResponseError('Failed to delete redirect');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ export interface IOrganizationMember {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface IOrgRedirect {
|
||||
id: string;
|
||||
oldName: string;
|
||||
organizationId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Re-export types used by settings
|
||||
import type { TRepositoryVisibility } from './repository.ts';
|
||||
import type { TRegistryProtocol } from './package.ts';
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface IReq_UpdateOrganization extends
|
||||
request: {
|
||||
identity: data.IIdentity;
|
||||
organizationId: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
avatarUrl?: string;
|
||||
@@ -159,3 +160,37 @@ export interface IReq_RemoveOrganizationMember extends
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Organization Redirect Requests
|
||||
// ============================================================================
|
||||
|
||||
export interface IReq_GetOrgRedirects extends
|
||||
plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetOrgRedirects
|
||||
> {
|
||||
method: 'getOrgRedirects';
|
||||
request: {
|
||||
identity: data.IIdentity;
|
||||
organizationId: string;
|
||||
};
|
||||
response: {
|
||||
redirects: data.IOrgRedirect[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DeleteOrgRedirect extends
|
||||
plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DeleteOrgRedirect
|
||||
> {
|
||||
method: 'deleteOrgRedirect';
|
||||
request: {
|
||||
identity: data.IIdentity;
|
||||
redirectId: string;
|
||||
};
|
||||
response: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@stack.gallery/registry',
|
||||
version: '1.6.0',
|
||||
version: '1.8.2',
|
||||
description: 'Enterprise-grade multi-protocol package registry'
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface IOrganizationsState {
|
||||
currentOrg: interfaces.data.IOrganizationDetail | null;
|
||||
repositories: interfaces.data.IRepository[];
|
||||
members: interfaces.data.IOrganizationMember[];
|
||||
redirects: interfaces.data.IOrgRedirect[];
|
||||
}
|
||||
|
||||
export interface IPackagesState {
|
||||
@@ -70,6 +71,7 @@ export const organizationsStatePart = await appState.getStatePart<IOrganizations
|
||||
currentOrg: null,
|
||||
repositories: [],
|
||||
members: [],
|
||||
redirects: [],
|
||||
},
|
||||
'soft',
|
||||
);
|
||||
@@ -278,6 +280,7 @@ export const createOrganizationAction = organizationsStatePart.createAction<{
|
||||
|
||||
export const updateOrganizationAction = organizationsStatePart.createAction<{
|
||||
organizationId: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
website?: string;
|
||||
@@ -365,6 +368,53 @@ export const fetchMembersAction = organizationsStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchRedirectsAction = organizationsStatePart.createAction<{
|
||||
organizationId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
if (!context.identity) return statePartArg.getState();
|
||||
try {
|
||||
const typedRequest = createTypedRequest<interfaces.requests.IReq_GetOrgRedirects>(
|
||||
'getOrgRedirects',
|
||||
);
|
||||
const response = await typedRequest.fire({
|
||||
identity: context.identity,
|
||||
organizationId: dataArg.organizationId,
|
||||
});
|
||||
return { ...statePartArg.getState(), redirects: response.redirects };
|
||||
} catch {
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteRedirectAction = organizationsStatePart.createAction<{
|
||||
redirectId: string;
|
||||
organizationId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
if (!context.identity) return statePartArg.getState();
|
||||
try {
|
||||
const typedRequest = createTypedRequest<interfaces.requests.IReq_DeleteOrgRedirect>(
|
||||
'deleteOrgRedirect',
|
||||
);
|
||||
await typedRequest.fire({
|
||||
identity: context.identity,
|
||||
redirectId: dataArg.redirectId,
|
||||
});
|
||||
// Re-fetch redirects
|
||||
const listReq = createTypedRequest<interfaces.requests.IReq_GetOrgRedirects>(
|
||||
'getOrgRedirects',
|
||||
);
|
||||
const listResp = await listReq.fire({
|
||||
identity: context.identity,
|
||||
organizationId: dataArg.organizationId,
|
||||
});
|
||||
return { ...statePartArg.getState(), redirects: listResp.redirects };
|
||||
} catch {
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Package Actions
|
||||
// ============================================================================
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -79,14 +79,14 @@ export class SgViewDashboard extends DeesElement {
|
||||
const { type, id } = e.detail;
|
||||
if (type === 'org' && id) {
|
||||
appRouter.navigateToEntity('organizations', id);
|
||||
} else if (type === 'org') {
|
||||
appRouter.navigateToView('organizations');
|
||||
} else if (type === 'package' && id) {
|
||||
appRouter.navigateToEntity('packages', id);
|
||||
} else if (type === 'packages') {
|
||||
appRouter.navigateToView('packages');
|
||||
} else if (type === 'tokens') {
|
||||
appRouter.navigateToView('tokens');
|
||||
} else if (type === 'organizations') {
|
||||
appRouter.navigateToView('organizations');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
132
ts_web/elements/sg-view-public-packages.ts
Normal file
132
ts_web/elements/sg-view-public-packages.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -68,8 +68,14 @@ class AppRouter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is logged in to decide default route
|
||||
const isLoggedIn = appstate.loginStatePart.getState().isLoggedIn;
|
||||
|
||||
if (!path || path === '/') {
|
||||
this.router.pushUrl('/dashboard');
|
||||
if (isLoggedIn) {
|
||||
this.router.pushUrl('/dashboard');
|
||||
}
|
||||
// If not logged in, stay on / for public browsing
|
||||
} else {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
|
||||
Reference in New Issue
Block a user