fix: deduplicate spaces dropdown, group by visibility type
Personal space (slug=username) forced to private so it doesn't appear as both public and private. Dropdown now groups spaces: Private (red) at top, Permissioned (yellow) middle, Public (green) bottom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d96130f919
commit
7177b2882c
|
|
@ -226,15 +226,27 @@ spaces.get("/", async (c) => {
|
|||
}
|
||||
|
||||
const spacesList = [];
|
||||
const seenSlugs = new Set<string>();
|
||||
const username = claims?.username?.toLowerCase();
|
||||
|
||||
for (const slug of slugs) {
|
||||
await loadCommunity(slug);
|
||||
const data = getDocumentData(slug);
|
||||
if (data?.meta) {
|
||||
const vis = data.meta.visibility || "public";
|
||||
// Deduplicate by slug
|
||||
if (seenSlugs.has(slug)) continue;
|
||||
seenSlugs.add(slug);
|
||||
|
||||
let vis = data.meta.visibility || "public";
|
||||
const isOwner = !!(claims && data.meta.ownerDID === claims.sub);
|
||||
const memberEntry = claims ? data.members?.[claims.sub] : undefined;
|
||||
const isMember = !!memberEntry;
|
||||
|
||||
// Owner's personal space (slug matches username) is always private
|
||||
if (isOwner && username && slug === username) {
|
||||
vis = "private";
|
||||
}
|
||||
|
||||
// Determine accessibility
|
||||
const isPublicSpace = vis === "public";
|
||||
const isPermissioned = vis === "permissioned";
|
||||
|
|
@ -276,8 +288,13 @@ spaces.get("/", async (c) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort: user's own spaces first, then demo, then others alphabetically
|
||||
// Sort: private first (red), then permissioned (yellow), then public (green)
|
||||
// Within each group: user's own spaces first, then alphabetically
|
||||
const visOrder: Record<string, number> = { private: 0, permissioned: 1, public: 2 };
|
||||
spacesList.sort((a, b) => {
|
||||
const va = visOrder[a.visibility || "public"] ?? 2;
|
||||
const vb = visOrder[b.visibility || "public"] ?? 2;
|
||||
if (va !== vb) return va - vb;
|
||||
if (a.role && !b.role) return -1;
|
||||
if (!a.role && b.role) return 1;
|
||||
if (a.slug === "demo") return -1;
|
||||
|
|
|
|||
|
|
@ -131,6 +131,23 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
return s.name;
|
||||
}
|
||||
|
||||
#renderSpaceGroup(spaces: SpaceInfo[], current: string, moduleId: string): string {
|
||||
return spaces
|
||||
.map((s) => {
|
||||
const vis = this.#visibilityInfo(s);
|
||||
const canEdit = s.role === "owner" || s.role === "admin";
|
||||
return `
|
||||
<div class="item-row ${vis.cls} ${s.slug === current ? "active" : ""}">
|
||||
<a class="item" href="${rspaceNavUrl(s.slug, moduleId)}">
|
||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
||||
<span class="item-name">${this.#displayName(s)}</span>
|
||||
<span class="item-vis ${vis.cls}">${vis.label}</span>
|
||||
</a>${canEdit ? `<button class="item-gear" data-edit-slug="${s.slug}" data-edit-name="${s.name.replace(/"/g, """)}" title="Edit Space">⚙</button>` : ""}
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
#demoSpaceHTML(current: string, moduleId: string): string {
|
||||
const isActive = current === "demo";
|
||||
return `
|
||||
|
|
@ -171,10 +188,14 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
// Split: user's own spaces vs permissioned spaces they can discover
|
||||
// Split spaces by visibility and role
|
||||
const mySpaces = this.#spaces.filter((s) => s.role);
|
||||
const discoverSpaces = this.#spaces.filter((s) => s.accessible === false);
|
||||
|
||||
const privateSpaces = mySpaces.filter((s) => s.visibility === "private");
|
||||
const permissionedSpaces = mySpaces.filter((s) => s.visibility === "permissioned");
|
||||
const publicSpaces = mySpaces.filter((s) => s.visibility === "public");
|
||||
|
||||
const hasOwnedSpace = mySpaces.some((s) => s.relationship === "owner");
|
||||
|
||||
let html = "";
|
||||
|
|
@ -194,23 +215,24 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
html += `<div class="divider"></div>`;
|
||||
}
|
||||
|
||||
// ── Your spaces ──
|
||||
if (mySpaces.length > 0) {
|
||||
html += `<div class="section-label">Your spaces</div>`;
|
||||
html += mySpaces
|
||||
.map((s) => {
|
||||
const vis = this.#visibilityInfo(s);
|
||||
const canEdit = s.role === "owner" || s.role === "admin";
|
||||
return `
|
||||
<div class="item-row ${vis.cls} ${s.slug === current ? "active" : ""}">
|
||||
<a class="item" href="${rspaceNavUrl(s.slug, moduleId)}">
|
||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
||||
<span class="item-name">${this.#displayName(s)}</span>
|
||||
<span class="item-vis ${vis.cls}">${vis.label}</span>
|
||||
</a>${canEdit ? `<button class="item-gear" data-edit-slug="${s.slug}" data-edit-name="${s.name.replace(/"/g, """)}" title="Edit Space">⚙</button>` : ""}
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
// ── Private spaces (red) — top ──
|
||||
if (privateSpaces.length > 0) {
|
||||
html += `<div class="section-label section-label--private">Private</div>`;
|
||||
html += this.#renderSpaceGroup(privateSpaces, current, moduleId);
|
||||
}
|
||||
|
||||
// ── Permissioned spaces (yellow) — middle ──
|
||||
if (permissionedSpaces.length > 0) {
|
||||
if (privateSpaces.length > 0) html += `<div class="divider"></div>`;
|
||||
html += `<div class="section-label section-label--permissioned">Permissioned</div>`;
|
||||
html += this.#renderSpaceGroup(permissionedSpaces, current, moduleId);
|
||||
}
|
||||
|
||||
// ── Public spaces (green) — bottom ──
|
||||
if (publicSpaces.length > 0) {
|
||||
if (privateSpaces.length > 0 || permissionedSpaces.length > 0) html += `<div class="divider"></div>`;
|
||||
html += `<div class="section-label section-label--public">Public</div>`;
|
||||
html += this.#renderSpaceGroup(publicSpaces, current, moduleId);
|
||||
}
|
||||
|
||||
// ── Discover (permissioned spaces the user can request access to) ──
|
||||
|
|
@ -1094,6 +1116,9 @@ const STYLES = `
|
|||
padding: 8px 14px 4px; font-size: 0.7rem; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5;
|
||||
}
|
||||
.section-label--private { color: #f87171; opacity: 0.85; }
|
||||
.section-label--permissioned { color: #fbbf24; opacity: 0.85; }
|
||||
.section-label--public { color: #34d399; opacity: 0.85; }
|
||||
|
||||
.item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue