Link a member's empty role list to the space's roles (#653)

This commit is contained in:
Coracle-Bot 2026-09-25 17:17:14 +00:00 committed by hodlbod
parent a7cb5fe4d6
commit 5c0617fbf1
5 changed files with 40 additions and 9 deletions

View file

@ -1451,6 +1451,8 @@ Acceptance:
- Searching the directory for the role's name filters to members holding it.
- Deleting a role warns that members keep their space membership, and confirming
removes it from the list; bob sees no "Manage Roles" option.
- A member's "Edit roles" in a space with no roles opens the roles list rather
than ending there.
### US-094 — Invite people to a space

View file

@ -290,6 +290,18 @@ test("US-093 create roles and assign them", async ({seed, as}) => {
await expect(memberCard(admin, "Bob Barnacle")).toBeVisible()
await expect(memberCard(admin, "Bob Barnacle").locator(".role-badge")).toHaveCount(0)
// A space with no roles left offers the roles list from the member's own dialog
await memberCard(admin, "Bob Barnacle").getByRole("button").last().click()
await menuItem(admin, "Edit roles").click()
const bobsRoles = dialog(admin, "Edit Member")
await expect(bobsRoles.getByText("This space has no roles yet.")).toBeVisible()
await bobsRoles.getByRole("button", {name: "Manage Roles"}).click()
await expect(emptyState).toBeVisible()
const bob = await as(users.bob, spacePath(url) + "/directory")
await expect(memberCard(bob, "Carol Cutter")).toBeVisible()

View file

@ -3,6 +3,7 @@
import {sortBy} from "@welshman/lib"
import {RelayRoles} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
@ -13,8 +14,11 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RoleItem from "@app/components/RoleItem.svelte"
import SpaceRoles from "@app/components/SpaceRoles.svelte"
import {app, profiles, relayManagement} from "@app/core"
import {deriveUserCanManageRoles} from "@app/management"
import {deriveSpaceMemberRoles} from "@app/roles"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
type Props = {
@ -30,9 +34,12 @@
const profileDisplay = $profiles.display(pubkey, [url]).$
const memberRoles = deriveSpaceMemberRoles(url)
const initial = new Set(get(memberRoles).get(pubkey) ?? [])
const canManageRoles = deriveUserCanManageRoles(url)
const back = () => history.back()
const manageRoles = () => pushModal(SpaceRoles, {url})
const toggle = (id: string) => {
const next = new Set(selected)
@ -114,8 +121,15 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="button button-primary" onclick={submit} disabled={loading}>
<Spinner {loading}>Save changes</Spinner>
</Button>
{#if roles.length > 0}
<Button class="button button-primary" onclick={submit} disabled={loading}>
<Spinner {loading}>Save changes</Spinner>
</Button>
{:else if $canManageRoles}
<Button class="button button-primary" onclick={manageRoles}>
<Icon icon={UsersGroup} />
Manage Roles
</Button>
{/if}
</ModalFooter>
</Modal>

View file

@ -150,6 +150,11 @@ export const deriveUserIsSpaceOwner = (url: string) =>
([$user, $relay]) => $user.pubkey === $relay?.pubkey,
)
export const deriveUserCanManageRoles = (url: string) =>
derived(deriveSpaceSupportedMethods(url), $methods =>
["createrole", "editrole", "deleterole"].some(method => $methods.includes(method)),
)
export const deriveUserCanCreateRoom = (url: string) =>
derived(
[

View file

@ -18,7 +18,7 @@
import SpaceAdmins from "@app/components/SpaceAdmins.svelte"
import SpaceRoles from "@app/components/SpaceRoles.svelte"
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
import {deriveSpaceSupportedMethods} from "@app/management"
import {deriveSpaceSupportedMethods, deriveUserCanManageRoles} from "@app/management"
import {deriveSpaceMemberRoles} from "@app/roles"
import {relayMemberLists, relayRoles} from "@app/core"
import {deriveDisplaysByPubkey} from "@app/social"
@ -33,9 +33,7 @@
const members = $relayMemberLists.forUrl(url)
const memberRoles = deriveSpaceMemberRoles(url)
const supportedMethods = deriveSpaceSupportedMethods(url)
const canManageRoles = $derived(
["createrole", "editrole", "deleterole"].some(method => $supportedMethods.includes(method)),
)
const canManageRoles = deriveUserCanManageRoles(url)
const canListBans = $derived($supportedMethods.includes("listbannedpubkeys"))
const canListAdmins = $derived($supportedMethods.includes("listmethodassignees"))
@ -116,7 +114,7 @@
<Icon icon={AddCircle} />
Invite people
</Button>
{#if canManageRoles || canListBans || canListAdmins}
{#if $canManageRoles || canListBans || canListAdmins}
<div class="relative">
<Button
class="button button-neutral button-sm button-square"
@ -129,7 +127,7 @@
<ul
transition:fly
class="menu bg-surface absolute right-0 z-popover mt-2 w-48 gap-1 rounded-2xl p-2">
{#if canManageRoles}
{#if $canManageRoles}
<li>
<Button onclick={manageRoles}>
<Icon icon={UsersGroup} />