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 spacesList = [];
|
||||||
|
const seenSlugs = new Set<string>();
|
||||||
|
const username = claims?.username?.toLowerCase();
|
||||||
|
|
||||||
for (const slug of slugs) {
|
for (const slug of slugs) {
|
||||||
await loadCommunity(slug);
|
await loadCommunity(slug);
|
||||||
const data = getDocumentData(slug);
|
const data = getDocumentData(slug);
|
||||||
if (data?.meta) {
|
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 isOwner = !!(claims && data.meta.ownerDID === claims.sub);
|
||||||
const memberEntry = claims ? data.members?.[claims.sub] : undefined;
|
const memberEntry = claims ? data.members?.[claims.sub] : undefined;
|
||||||
const isMember = !!memberEntry;
|
const isMember = !!memberEntry;
|
||||||
|
|
||||||
|
// Owner's personal space (slug matches username) is always private
|
||||||
|
if (isOwner && username && slug === username) {
|
||||||
|
vis = "private";
|
||||||
|
}
|
||||||
|
|
||||||
// Determine accessibility
|
// Determine accessibility
|
||||||
const isPublicSpace = vis === "public";
|
const isPublicSpace = vis === "public";
|
||||||
const isPermissioned = vis === "permissioned";
|
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) => {
|
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.role && b.role) return 1;
|
if (!a.role && b.role) return 1;
|
||||||
if (a.slug === "demo") return -1;
|
if (a.slug === "demo") return -1;
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,23 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
||||||
return s.name;
|
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 {
|
#demoSpaceHTML(current: string, moduleId: string): string {
|
||||||
const isActive = current === "demo";
|
const isActive = current === "demo";
|
||||||
return `
|
return `
|
||||||
|
|
@ -171,10 +188,14 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
||||||
return;
|
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 mySpaces = this.#spaces.filter((s) => s.role);
|
||||||
const discoverSpaces = this.#spaces.filter((s) => s.accessible === false);
|
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");
|
const hasOwnedSpace = mySpaces.some((s) => s.relationship === "owner");
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
@ -194,23 +215,24 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
||||||
html += `<div class="divider"></div>`;
|
html += `<div class="divider"></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Your spaces ──
|
// ── Private spaces (red) — top ──
|
||||||
if (mySpaces.length > 0) {
|
if (privateSpaces.length > 0) {
|
||||||
html += `<div class="section-label">Your spaces</div>`;
|
html += `<div class="section-label section-label--private">Private</div>`;
|
||||||
html += mySpaces
|
html += this.#renderSpaceGroup(privateSpaces, current, moduleId);
|
||||||
.map((s) => {
|
}
|
||||||
const vis = this.#visibilityInfo(s);
|
|
||||||
const canEdit = s.role === "owner" || s.role === "admin";
|
// ── Permissioned spaces (yellow) — middle ──
|
||||||
return `
|
if (permissionedSpaces.length > 0) {
|
||||||
<div class="item-row ${vis.cls} ${s.slug === current ? "active" : ""}">
|
if (privateSpaces.length > 0) html += `<div class="divider"></div>`;
|
||||||
<a class="item" href="${rspaceNavUrl(s.slug, moduleId)}">
|
html += `<div class="section-label section-label--permissioned">Permissioned</div>`;
|
||||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
html += this.#renderSpaceGroup(permissionedSpaces, current, moduleId);
|
||||||
<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>` : ""}
|
// ── Public spaces (green) — bottom ──
|
||||||
</div>`;
|
if (publicSpaces.length > 0) {
|
||||||
})
|
if (privateSpaces.length > 0 || permissionedSpaces.length > 0) html += `<div class="divider"></div>`;
|
||||||
.join("");
|
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) ──
|
// ── 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;
|
padding: 8px 14px 4px; font-size: 0.7rem; font-weight: 600;
|
||||||
text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5;
|
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 {
|
.item {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue