feat(spaces): add visibility/description to settings panel, merge invite sources
- Space Settings section in dropdown with visibility (public/permissioned/private) and description fields, matching the full edit space modal - GET /api/spaces/:slug now includes description field - listSpaceInvites merges both space_invites and identity_invites tables so email invites appear in Pending Invites - revokeSpaceInvite falls through to identity_invites table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
858457c056
commit
59d2cc9933
|
|
@ -631,6 +631,7 @@ spaces.get("/:slug", async (c) => {
|
|||
slug: data.meta.slug,
|
||||
name: data.meta.name,
|
||||
visibility: data.meta.visibility,
|
||||
description: data.meta.description || "",
|
||||
createdAt: data.meta.createdAt,
|
||||
ownerDID: data.meta.ownerDID,
|
||||
memberCount: Object.keys(data.members || {}).length,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
private _moduleSettingsValues: Record<string, string | boolean> = {};
|
||||
private _moduleSaveError = "";
|
||||
private _moduleSaving = false;
|
||||
private _visibility: "public" | "permissioned" | "private" = "public";
|
||||
private _description = "";
|
||||
private _spaceName = "";
|
||||
private _spaceSaving = false;
|
||||
private _spaceSaveMsg = "";
|
||||
|
||||
static define() {
|
||||
if (!customElements.get("rstack-space-settings")) {
|
||||
|
|
@ -210,6 +215,21 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
this._myRole = this._isOwner ? "admin" : (myMember?.role || "viewer");
|
||||
}
|
||||
|
||||
// Load space metadata (visibility, description)
|
||||
if (this._isAdmin && token) {
|
||||
try {
|
||||
const res = await fetch(`/api/spaces/${encodeURIComponent(this._space)}`, {
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
});
|
||||
if (res.ok) {
|
||||
const info = await res.json();
|
||||
this._visibility = info.visibility || "public";
|
||||
this._description = info.description || "";
|
||||
this._spaceName = info.name || this._space;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Load invites (admin only)
|
||||
if (this._isAdmin && token) {
|
||||
try {
|
||||
|
|
@ -408,6 +428,22 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
<div class="panel-content">
|
||||
${moduleSettingsHTML}
|
||||
|
||||
${this._isAdmin ? `
|
||||
<section class="section">
|
||||
<h3>Space Settings</h3>
|
||||
<label class="field-label">Visibility</label>
|
||||
<select class="input" id="space-visibility">
|
||||
<option value="public" ${this._visibility === "public" ? "selected" : ""}>Public — anyone can read</option>
|
||||
<option value="permissioned" ${this._visibility === "permissioned" ? "selected" : ""}>Permissioned — sign in to access</option>
|
||||
<option value="private" ${this._visibility === "private" ? "selected" : ""}>Private — invite only</option>
|
||||
</select>
|
||||
<label class="field-label" style="margin-top:8px">Description</label>
|
||||
<textarea class="input" id="space-description" rows="2" placeholder="Optional description…" style="resize:vertical">${this._esc(this._description)}</textarea>
|
||||
<button class="add-btn" id="space-save" style="margin-top:8px" ${this._spaceSaving ? "disabled" : ""}>${this._spaceSaving ? "Saving…" : "Save"}</button>
|
||||
${this._spaceSaveMsg ? `<span class="space-save-msg">${this._esc(this._spaceSaveMsg)}</span>` : ""}
|
||||
</section>
|
||||
` : ""}
|
||||
|
||||
<section class="section">
|
||||
<h3>Members <span class="count">${this._members.length}</span></h3>
|
||||
<div class="members-list">${membersHTML}</div>
|
||||
|
|
@ -560,6 +596,46 @@ export class RStackSpaceSettings extends HTMLElement {
|
|||
});
|
||||
|
||||
sr.getElementById("mod-cfg-save")?.addEventListener("click", () => this._saveModuleSettings());
|
||||
|
||||
// Space settings save
|
||||
sr.getElementById("space-save")?.addEventListener("click", () => this._saveSpaceSettings());
|
||||
}
|
||||
|
||||
private async _saveSpaceSettings() {
|
||||
const sr = this.shadowRoot!;
|
||||
const vis = (sr.getElementById("space-visibility") as HTMLSelectElement)?.value;
|
||||
const desc = (sr.getElementById("space-description") as HTMLTextAreaElement)?.value;
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
this._spaceSaving = true;
|
||||
this._spaceSaveMsg = "";
|
||||
this._render();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/spaces/${encodeURIComponent(this._space)}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ visibility: vis, description: desc }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this._visibility = data.visibility || vis;
|
||||
this._description = data.description ?? desc;
|
||||
this._spaceSaveMsg = "Saved";
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({})) as { error?: string };
|
||||
this._spaceSaveMsg = (err as any).error || "Failed to save";
|
||||
}
|
||||
} catch {
|
||||
this._spaceSaveMsg = "Network error";
|
||||
}
|
||||
this._spaceSaving = false;
|
||||
this._render();
|
||||
setTimeout(() => { this._spaceSaveMsg = ""; this._render(); }, 3000);
|
||||
}
|
||||
|
||||
private async _lookupUser(username: string) {
|
||||
|
|
@ -952,6 +1028,19 @@ const PANEL_CSS = `
|
|||
.input:focus { outline: none; border-color: #14b8a6; }
|
||||
.input::placeholder { color: var(--rs-text-muted); }
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--rs-text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.space-save-msg {
|
||||
font-size: 0.78rem;
|
||||
color: #14b8a6;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.add-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
|
|
|||
Loading…
Reference in New Issue