List a space's admins and edit a member's permissions
This commit is contained in:
parent
10004658ee
commit
90133dd465
7 changed files with 483 additions and 1 deletions
|
|
@ -1521,6 +1521,20 @@ Acceptance:
|
|||
- She still sees "Action Items", which is the queue `allowpubkey` resolves.
|
||||
- admin, who owns the relay and so holds every method, sees all three.
|
||||
|
||||
### US-130 — Share out admin permissions
|
||||
|
||||
As admin, I want to hand a member individual management permissions and see who
|
||||
holds what, so that moderation is shared without handing anyone the whole relay.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- "Admins" in the directory's menu lists the space's owner and everyone holding
|
||||
assigned methods, each with a badge per permission they hold.
|
||||
- Checking permissions under a member's "Edit permissions" puts them in that
|
||||
list, and unchecking those permissions takes them back out.
|
||||
- A member given "List banned members" finds "Banned Members" in the directory
|
||||
menu they had no menu in before, and still no "Admins".
|
||||
|
||||
### US-098 — Browse and create hosted spaces
|
||||
|
||||
As a space owner, I want to see the spaces I host and spin up new ones, so that
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ const openSpaceMenu = (page: Page) => page.getByRole("button", {name: /space\.te
|
|||
const memberCard = (page: Page, name: string) =>
|
||||
page.locator(".card-interactive").filter({hasText: name})
|
||||
|
||||
// Exact, because several permission names are a substring of another — "Ban members" of "Unban
|
||||
// members".
|
||||
const permission = (scope: Locator, name: string) =>
|
||||
scope.getByRole("checkbox", {name, exact: true})
|
||||
|
||||
const openEventMenu = (card: Locator) => menuButton(card).click()
|
||||
|
||||
const articleCard = (page: Page, title: string) =>
|
||||
|
|
@ -715,6 +720,75 @@ test("US-097 work through the action-items queue", async ({seed, as}) => {
|
|||
await expect(bobsSpace.getByRole("button", {name: /^Action Items/})).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("US-130 share out admin permissions", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.join(user.admin, "general")
|
||||
space.join(user.bob, "general")
|
||||
space.profile(user.bob, {name: "Bob Barnacle"})
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
const admin = await as(users.admin, spacePath(url) + "/directory")
|
||||
|
||||
await expect(memberCard(admin, "Bob Barnacle")).toBeVisible()
|
||||
|
||||
await admin.getByRole("button", {name: "More options"}).click()
|
||||
await menuItem(admin, "Admins").click()
|
||||
|
||||
const admins = dialog(admin, "Admins")
|
||||
|
||||
// The owner holds every method implicitly, so the relay leaves them out of listmethodassignees
|
||||
// and the client names them from the space's nip 11 pubkey instead.
|
||||
await expect(admins.locator(".badge").filter({hasText: "Owner"})).toBeVisible()
|
||||
await expect(admins.getByText("Nobody else has been given management permissions.")).toBeVisible()
|
||||
|
||||
await admins.getByRole("button", {name: "Go back"}).click()
|
||||
|
||||
await memberCard(admin, "Bob Barnacle").getByRole("button").last().click()
|
||||
await menuItem(admin, "Edit permissions").click()
|
||||
|
||||
const permissions = dialog(admin, "Edit Permissions")
|
||||
|
||||
await permission(permissions, "Ban members").check()
|
||||
await permission(permissions, "List banned members").check()
|
||||
await permissions.getByRole("button", {name: "Save changes"}).click()
|
||||
|
||||
await expect(admin.getByRole("alert")).toContainText("Permissions updated!")
|
||||
|
||||
await admin.getByRole("button", {name: "More options"}).click()
|
||||
await menuItem(admin, "Admins").click()
|
||||
|
||||
await expect(
|
||||
admins.locator(".card").filter({hasText: "Bob Barnacle"}).locator(".badge"),
|
||||
).toHaveText(["Ban members", "List banned members"])
|
||||
|
||||
await admins.getByRole("button", {name: "Go back"}).click()
|
||||
|
||||
// bob holds two methods now, so he gets the one control they cover and nothing else
|
||||
const bob = await as(users.bob, spacePath(url) + "/directory")
|
||||
|
||||
await bob.getByRole("button", {name: "More options"}).click()
|
||||
|
||||
await expect(menuItem(bob, "Banned Members")).toBeVisible()
|
||||
await expect(menuItem(bob, "Admins")).toHaveCount(0)
|
||||
|
||||
await memberCard(admin, "Bob Barnacle").getByRole("button").last().click()
|
||||
await menuItem(admin, "Edit permissions").click()
|
||||
await permission(permissions, "Ban members").uncheck()
|
||||
await permission(permissions, "List banned members").uncheck()
|
||||
await permissions.getByRole("button", {name: "Save changes"}).click()
|
||||
|
||||
await expect(admin.getByRole("alert")).toContainText("Permissions updated!")
|
||||
|
||||
await admin.getByRole("button", {name: "More options"}).click()
|
||||
await menuItem(admin, "Admins").click()
|
||||
|
||||
await expect(admins.getByText("Nobody else has been given management permissions.")).toBeVisible()
|
||||
})
|
||||
|
||||
test("US-098 browse and create hosted spaces", async ({seed, as}) => {
|
||||
await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
|
|
|
|||
112
src/app/components/SpaceAdmins.svelte
Normal file
112
src/app/components/SpaceAdmins.svelte
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {Relays} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import Badge from "@lib/components/Badge.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import SpaceMemberMethods from "@app/components/SpaceMemberMethods.svelte"
|
||||
import {fromApp} from "@app/core"
|
||||
import {
|
||||
deriveSpaceMethodAssignees,
|
||||
deriveSpaceSupportedMethods,
|
||||
displayManagementMethod,
|
||||
loadSpaceMethodAssignees,
|
||||
} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
|
||||
const relay = fromApp($app => $app.use(Relays).one(url))
|
||||
const assignees = deriveSpaceMethodAssignees(url)
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canEdit = $derived(
|
||||
["assignmethod", "unassignmethod"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const editMethods = (pubkey: string) => pushModal(SpaceMemberMethods, {url, pubkey})
|
||||
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
const error = await loadSpaceMethodAssignees(url)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Admins</ModalTitle>
|
||||
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if $relay?.pubkey}
|
||||
<div class="card flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile pubkey={$relay.pubkey} {url} />
|
||||
</div>
|
||||
<Badge>Owner</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<Spinner loading>Loading admins...</Spinner>
|
||||
{:else if $assignees.length === 0}
|
||||
<div class="card bg-surface p-4 text-sm opacity-70">
|
||||
Nobody else has been given management permissions.
|
||||
</div>
|
||||
{:else}
|
||||
{#each $assignees as { pubkey, methods } (pubkey)}
|
||||
<div class="card flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canEdit}
|
||||
<Button
|
||||
class="button button-ghost button-sm button-square"
|
||||
aria-label="Edit permissions"
|
||||
onclick={() => editMethods(pubkey)}>
|
||||
<Icon size={4} icon={Pen} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each methods as method (method)}
|
||||
<Badge class="badge-sm font-normal">{displayManagementMethod(method)}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="button button-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import SpaceMemberMethods from "@app/components/SpaceMemberMethods.svelte"
|
||||
import SpaceMemberRoles from "@app/components/SpaceMemberRoles.svelte"
|
||||
import {profiles, relayManagement} from "@app/core"
|
||||
import {deriveSpaceSupportedMethods} from "@app/management"
|
||||
|
|
@ -26,11 +28,17 @@
|
|||
const canBan = $derived($supportedMethods.includes("banpubkey"))
|
||||
const canAssign = $derived($supportedMethods.includes("assignrole"))
|
||||
const canUnassign = $derived($supportedMethods.includes("unassignrole"))
|
||||
const canEditMethods = $derived(
|
||||
$supportedMethods.includes("listmethodassignees") &&
|
||||
["assignmethod", "unassignmethod"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const editRoles = () => pushModal(SpaceMemberRoles, {url, pubkey})
|
||||
|
||||
const editMethods = () => pushModal(SpaceMemberMethods, {url, pubkey})
|
||||
|
||||
const removeMember = () =>
|
||||
pushModal(Confirm, {
|
||||
title: "Remove Member",
|
||||
|
|
@ -79,6 +87,14 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canEditMethods}
|
||||
<li>
|
||||
<Button onclick={editMethods}>
|
||||
<Icon icon={ShieldUser} />
|
||||
Edit permissions
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canUnallow}
|
||||
<li>
|
||||
<Button onclick={removeMember}>
|
||||
|
|
|
|||
163
src/app/components/SpaceMemberMethods.svelte
Normal file
163
src/app/components/SpaceMemberMethods.svelte
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {get} from "svelte/store"
|
||||
import {spec} from "@welshman/lib"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {profiles, relayManagement} from "@app/core"
|
||||
import {
|
||||
MANAGEMENT_METHOD_GROUPS,
|
||||
deriveSpaceMethodAssignees,
|
||||
deriveSpaceSupportedMethods,
|
||||
loadSpaceMethodAssignees,
|
||||
} from "@app/management"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
const {url, pubkey}: Props = $props()
|
||||
|
||||
const profileDisplay = $profiles.display(pubkey, [url]).$
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canAssign = $derived($supportedMethods.includes("assignmethod"))
|
||||
const canUnassign = $derived($supportedMethods.includes("unassignmethod"))
|
||||
|
||||
// A relay can grant methods this client knows nothing about, and a list that left them out
|
||||
// would revoke them on the next save.
|
||||
const groups = $derived.by(() => {
|
||||
const known = MANAGEMENT_METHOD_GROUPS.flatMap(group => group.methods.map(({method}) => method))
|
||||
const extra = [...assigned].filter(method => !known.includes(method))
|
||||
|
||||
if (extra.length > 0) {
|
||||
return [
|
||||
...MANAGEMENT_METHOD_GROUPS,
|
||||
{label: "Other", methods: extra.map(method => ({method, label: method}))},
|
||||
]
|
||||
}
|
||||
|
||||
return MANAGEMENT_METHOD_GROUPS
|
||||
})
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const toggle = (method: string) => {
|
||||
const next = new Set(selected)
|
||||
|
||||
if (next.has(method)) {
|
||||
next.delete(method)
|
||||
} else {
|
||||
next.add(method)
|
||||
}
|
||||
|
||||
selected = next
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
saving = true
|
||||
|
||||
try {
|
||||
const management = $relayManagement.forUrl(url)
|
||||
|
||||
for (const method of selected) {
|
||||
if (!assigned.has(method)) {
|
||||
const {error} = await management.assignMethod(pubkey, method)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const method of assigned) {
|
||||
if (!selected.has(method)) {
|
||||
const {error} = await management.unassignMethod(pubkey, method)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await loadSpaceMethodAssignees(url)
|
||||
|
||||
pushToast({message: "Permissions updated!"})
|
||||
back()
|
||||
} finally {
|
||||
saving = false
|
||||
}
|
||||
}
|
||||
|
||||
let assigned = $state(new Set<string>())
|
||||
let selected = $state(new Set<string>())
|
||||
let loading = $state(true)
|
||||
let saving = $state(false)
|
||||
|
||||
onMount(async () => {
|
||||
const error = await loadSpaceMethodAssignees(url)
|
||||
|
||||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
assigned = new Set(get(deriveSpaceMethodAssignees(url)).find(spec({pubkey}))?.methods ?? [])
|
||||
selected = new Set(assigned)
|
||||
loading = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Edit Permissions</ModalTitle>
|
||||
<ModalSubtitle>
|
||||
Choose what <span class="text-primary">@{$profileDisplay}</span> can administer
|
||||
</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
{#if loading}
|
||||
<Spinner loading>Loading permissions...</Spinner>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each groups as group (group.label)}
|
||||
<div class="flex flex-col gap-2">
|
||||
<strong class="text-sm opacity-70">{group.label}</strong>
|
||||
{#each group.methods as { method, label } (method)}
|
||||
<label class="card card-sm flex cursor-pointer justify-between gap-3">
|
||||
{label}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
checked={selected.has(method)}
|
||||
disabled={selected.has(method) ? !canUnassign : !canAssign}
|
||||
onchange={() => toggle(method)} />
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="button button-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
{#if canAssign || canUnassign}
|
||||
<Button class="button button-primary" onclick={submit} disabled={saving}>
|
||||
<Spinner loading={saving}>Save changes</Spinner>
|
||||
</Button>
|
||||
{/if}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
@ -22,6 +22,93 @@ export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
|||
return store
|
||||
}
|
||||
|
||||
// NIP-86 has no way to ask a relay which methods it can hand out — `supportedmethods` answers for
|
||||
// the pubkey that signed the request — so the catalog of grantable methods is the client's own.
|
||||
export const MANAGEMENT_METHOD_GROUPS = [
|
||||
{
|
||||
label: "Members",
|
||||
methods: [
|
||||
{method: "allowpubkey", label: "Add members"},
|
||||
{method: "unallowpubkey", label: "Remove members"},
|
||||
{method: "listallowedpubkeys", label: "List members"},
|
||||
{method: "banpubkey", label: "Ban members"},
|
||||
{method: "unbanpubkey", label: "Unban members"},
|
||||
{method: "listbannedpubkeys", label: "List banned members"},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
methods: [
|
||||
{method: "banevent", label: "Delete content"},
|
||||
{method: "allowevent", label: "Dismiss reports"},
|
||||
{method: "listbannedevents", label: "List deleted content"},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Roles",
|
||||
methods: [
|
||||
{method: "createrole", label: "Create roles"},
|
||||
{method: "editrole", label: "Edit roles"},
|
||||
{method: "deleterole", label: "Delete roles"},
|
||||
{method: "assignrole", label: "Assign roles"},
|
||||
{method: "unassignrole", label: "Unassign roles"},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Invites",
|
||||
methods: [
|
||||
{method: "createclaim", label: "Create invites"},
|
||||
{method: "deleteclaim", label: "Delete invites"},
|
||||
{method: "listclaims", label: "List invites"},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Space",
|
||||
methods: [
|
||||
{method: "changerelayname", label: "Change the name"},
|
||||
{method: "changerelaydescription", label: "Change the description"},
|
||||
{method: "changerelayicon", label: "Change the icon"},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Admins",
|
||||
methods: [
|
||||
{method: "assignmethod", label: "Grant permissions"},
|
||||
{method: "unassignmethod", label: "Revoke permissions"},
|
||||
{method: "listmethodassignees", label: "List admins"},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const methodLabels = new Map(
|
||||
MANAGEMENT_METHOD_GROUPS.flatMap(group =>
|
||||
group.methods.map(({method, label}): [string, string] => [method, label]),
|
||||
),
|
||||
)
|
||||
|
||||
export const displayManagementMethod = (method: string) => methodLabels.get(method) ?? method
|
||||
|
||||
export type MethodAssigneeItem = {
|
||||
pubkey: string
|
||||
methods: string[]
|
||||
}
|
||||
|
||||
// One store per space rather than per user: the relay answers the same assignments to anyone
|
||||
// allowed to ask, and the admin list has to agree with the editor that changes it.
|
||||
export const deriveSpaceMethodAssignees = simpleCache(([url]: [url: string]) =>
|
||||
writable<MethodAssigneeItem[]>([]),
|
||||
)
|
||||
|
||||
export const loadSpaceMethodAssignees = async (url: string) => {
|
||||
const {result, error} = await relayManagement.get().forUrl(url).listMethodAssignees()
|
||||
|
||||
if (result) {
|
||||
deriveSpaceMethodAssignees(url).set(result)
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
const deriveSupportedMethodsForPubkey = simpleCache(([, url]: [pubkey: string, url: string]) => {
|
||||
let checkedAt = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMember from "@app/components/SpaceMember.svelte"
|
||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||
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"
|
||||
|
|
@ -35,6 +37,7 @@
|
|||
["createrole", "editrole", "deleterole"].some(method => $supportedMethods.includes(method)),
|
||||
)
|
||||
const canListBans = $derived($supportedMethods.includes("listbannedpubkeys"))
|
||||
const canListAdmins = $derived($supportedMethods.includes("listmethodassignees"))
|
||||
|
||||
// Each member with their resolved roles (sorted by order).
|
||||
const memberList = derived([members, memberRoles, roles], ([$members, $memberRoles, $roles]) => {
|
||||
|
|
@ -61,6 +64,11 @@
|
|||
pushModal(SpaceRoles, {url})
|
||||
}
|
||||
|
||||
const spaceAdmins = () => {
|
||||
menuOpen = false
|
||||
pushModal(SpaceAdmins, {url})
|
||||
}
|
||||
|
||||
const bannedMembers = () => {
|
||||
menuOpen = false
|
||||
pushModal(SpaceMembersBanned, {url})
|
||||
|
|
@ -110,7 +118,7 @@
|
|||
<Icon icon={AddCircle} />
|
||||
Invite people
|
||||
</Button>
|
||||
{#if canManageRoles || canListBans}
|
||||
{#if canManageRoles || canListBans || canListAdmins}
|
||||
<div class="relative">
|
||||
<Button
|
||||
class="button button-neutral button-sm button-square"
|
||||
|
|
@ -131,6 +139,14 @@
|
|||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canListAdmins}
|
||||
<li>
|
||||
<Button onclick={spaceAdmins}>
|
||||
<Icon icon={ShieldUser} />
|
||||
Admins
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canListBans}
|
||||
<li>
|
||||
<Button onclick={bannedMembers}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue