From 9baccd3407650a8fa76b366d73076ef40979cdd1 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 20 Aug 2026 22:31:56 +0530 Subject: [PATCH 1/4] fix(permissions): render member roles instead of marking them unresolvable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parsePermission only recognised user and team, so a member role fell through to the invalid branch: it rendered as a raw truncated string in the styling reserved for a deleted entity, and its hover card showed the missing-entity avatar — presenting a perfectly valid grant as broken. It also called onNotFound. Nothing passes that callback today, but d36135f5b wired it to auto-remove stale roles before row.svelte moved to runes turned the event into a prop and left the consumer behind. If that wiring is restored, member roles would be silently deleted from any resource a user merely opened. A membership can only be read through its team or its user, and the role string carries neither, so the ID stands in for a name. That is still an improvement on the raw role: the badge now reads Member and the hover card offers the membership ID to copy. --- src/lib/components/permissions/custom.svelte | 4 +- src/lib/components/permissions/row.svelte | 56 +++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/lib/components/permissions/custom.svelte b/src/lib/components/permissions/custom.svelte index 64c08d9fc6..832db9157e 100644 --- a/src/lib/components/permissions/custom.svelte +++ b/src/lib/components/permissions/custom.svelte @@ -34,8 +34,8 @@ required id="custom-permission" label="Role" - placeholder="user:[USER_ID] or team:[TEAM_ID]/[ROLE]" - helper="A permission should be formatted as: user:[USER_ID] or team:[TEAM_ID]/[ROLE]¸" + placeholder="user:[USER_ID], team:[TEAM_ID]/[ROLE] or member:[MEMBERSHIP_ID]" + helper="A permission should be formatted as: user:[USER_ID], team:[TEAM_ID]/[ROLE] or member:[MEMBERSHIP_ID]" bind:value /> diff --git a/src/lib/components/permissions/row.svelte b/src/lib/components/permissions/row.svelte index aa44cb73ad..c9e8c36d47 100644 --- a/src/lib/components/permissions/row.svelte +++ b/src/lib/components/permissions/row.svelte @@ -16,7 +16,7 @@ Typography } from '@appwrite.io/pink-svelte'; import Avatar from '../avatar.svelte'; - import { IconAnonymous, IconMinusSm } from '@appwrite.io/pink-icons-svelte'; + import { IconAnonymous, IconMinusSm, IconUsers } from '@appwrite.io/pink-icons-svelte'; import { page } from '$app/state'; import { menuOpen } from '$lib/components/menu/store'; import { base } from '$app/paths'; @@ -39,8 +39,10 @@ let { role, placement = 'bottom-start', children, onNotFound }: Props = $props(); + const parsedRole = $derived(parsePermission(role)); + type ParsedPermission = { - type: 'user' | 'team' | 'other'; + type: 'user' | 'team' | 'member' | 'other'; id: string; roleName?: string; isValid: boolean; @@ -58,9 +60,9 @@ return { type: 'other', id: permission, isValid: false }; } - if (type === 'user' || type === 'team') { + if (type === 'user' || type === 'team' || type === 'member') { return { - type: type as 'user' | 'team', + type: type as 'user' | 'team' | 'member', id, roleName, isValid: true @@ -78,6 +80,12 @@ return { notFound: true, roleName: parsed.roleName, customName: parsed.id }; } + // A membership can only be read through its team or its user, neither of which the role + // carries, so the ID is shown as-is rather than reported as a role that no longer exists. + if (parsed.type === 'member') { + return { roleName: parsed.roleName, customName: parsed.id }; + } + if (parsed.type === 'user') { try { return await sdk @@ -177,7 +185,7 @@ {role} {:then data} {formatName( - data.name ?? data?.email ?? data?.phone ?? '-', + data.name ?? data?.email ?? data?.phone ?? data?.customName ?? '-', $isSmallViewport ? 16 : 20 )} {/await} @@ -185,7 +193,11 @@ + content={parsedRole.type === 'member' + ? 'Member' + : parsedRole.type === 'user' + ? 'User' + : 'Team'} /> {/if} @@ -205,7 +217,37 @@ {:then data} - {#if data.notFound} + {#if parsedRole.type === 'member'} + + + + + + + + + + Team membership + + + + + + + {:else if data.notFound} Date: Thu, 20 Aug 2026 22:32:02 +0530 Subject: [PATCH 2/4] feat(permissions): add a membership picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The member role has been in the API and the docs since 1.0, but neither console ever offered it — the only way to set one was to know the syntax and type it into the custom permission box. It keys on a membership ID, which nothing in the console surfaces, so the picker resolves it in two steps: find the user, then choose which of their team memberships to grant. It has to run in that direction — teams.listMemberships indexes only the membership ID and the user ID for search, so picking a team first would leave the member step with a search box that matches nothing. users.list indexes name, email and phone, so the first step behaves like the existing user picker. --- src/lib/components/permissions/actions.svelte | 10 + src/lib/components/permissions/member.svelte | 284 ++++++++++++++++++ .../components/permissions/permissions.svelte | 3 + src/lib/components/permissions/roles.svelte | 3 + 4 files changed, 300 insertions(+) create mode 100644 src/lib/components/permissions/member.svelte diff --git a/src/lib/components/permissions/actions.svelte b/src/lib/components/permissions/actions.svelte index 132ee5b405..d40e6255e8 100644 --- a/src/lib/components/permissions/actions.svelte +++ b/src/lib/components/permissions/actions.svelte @@ -2,6 +2,7 @@ import { createEventDispatcher } from 'svelte'; import Label from './label.svelte'; import Custom from './custom.svelte'; + import Member from './member.svelte'; import Team from './team.svelte'; import User from './user.svelte'; import type { Permission } from './permissions.svelte'; @@ -10,6 +11,7 @@ export let showUser: boolean; export let showTeam: boolean; + export let showMember: boolean; export let showLabel: boolean; export let showCustom: boolean; export let groups: Writable>; @@ -55,6 +57,11 @@ showTeam = true; hide(e); }}>Select teams + { + showMember = true; + hide(e); + }}>Select memberships { showLabel = true; @@ -82,6 +89,9 @@ }} {groups} /> {/if} +{#if showMember} + +{/if} {#if showLabel} + {:else if loadError} + + + + + {loadError} + + + + + {:else if search}