From 3e60372f5e6414cf6f83298ad30eaee300f281ad Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 24 Nov 2025 13:00:21 +0530 Subject: [PATCH 1/2] wip --- src/lib/components/permissions/project.svelte | 133 ++++++++++++++++++ src/lib/components/roles/roles.svelte | 8 +- src/lib/stores/billing.ts | 2 + .../createMember.svelte | 114 ++++++++++++++- .../project-[region]-[project]/+layout.ts | 2 + 5 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 src/lib/components/permissions/project.svelte diff --git a/src/lib/components/permissions/project.svelte b/src/lib/components/permissions/project.svelte new file mode 100644 index 0000000000..8533bd8d1a --- /dev/null +++ b/src/lib/components/permissions/project.svelte @@ -0,0 +1,133 @@ + + + + Grant access to any project in the organization. + + + {#if results?.projects?.length} + + {#each results.projects as project (project.$id)} + toggleSelection(project)}> + +
+ p.$id === project.$id)} /> +
+
+ + + + + {project.name} + + {project.$id} + + + + +
+ {/each} +
+ + +

Total results: {results?.total}

+ +
+ {:else if search} + + + + {:else if isLoading} + +
+ +
+ {:else} + + + + Need a hand? Learn more in our + documentation. + + + + {/if} + + + + +
diff --git a/src/lib/components/roles/roles.svelte b/src/lib/components/roles/roles.svelte index 79d1b1d838..ece8a9d5a0 100644 --- a/src/lib/components/roles/roles.svelte +++ b/src/lib/components/roles/roles.svelte @@ -1,11 +1,17 @@

Roles

-

Owner, Developer, Editor, Analyst and Billing.

+

+ {isProjectSpecific + ? 'Owner, Developer, Editor and Analyst' + : 'Owner, Developer, Editor, Analyst and Billing.'} +

r.value !== 'billing'); + export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; diff --git a/src/routes/(console)/organization-[organization]/createMember.svelte b/src/routes/(console)/organization-[organization]/createMember.svelte index 63a55ff911..8ff4a495db 100644 --- a/src/routes/(console)/organization-[organization]/createMember.svelte +++ b/src/routes/(console)/organization-[organization]/createMember.svelte @@ -2,7 +2,8 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Modal } from '$lib/components'; - import { InputText, InputEmail, Button } from '$lib/elements/forms'; + import ChooseProject from '$lib/components/permissions/project.svelte'; + import { InputText, InputEmail, Button, InputSwitch } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { createEventDispatcher } from 'svelte'; @@ -11,12 +12,13 @@ import { Dependencies } from '$lib/constants'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; import { isCloud, isSelfHosted } from '$lib/system'; - import { roles } from '$lib/stores/billing'; + import { projectSpecificRoles, roles } from '$lib/stores/billing'; import InputSelect from '$lib/elements/forms/inputSelect.svelte'; import Roles from '$lib/components/roles/roles.svelte'; - import { Icon, Popover } from '@appwrite.io/pink-svelte'; - import { IconInfo } from '@appwrite.io/pink-icons-svelte'; + import { Icon, Popover, Table } from '@appwrite.io/pink-svelte'; + import { IconInfo, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; import { Layout } from '@appwrite.io/pink-svelte'; + import type { Models } from '@appwrite.io/console'; export let showCreate = false; @@ -27,11 +29,38 @@ error: string, role: string = isSelfHosted ? 'owner' : 'developer'; + let showChooseProjects = false; + let isInvitingToSpecificProjects = false; + let selectedProjects: Models.Project[] = []; + let selectedProjectsWithRole: { project: Models.Project; role: string }[] = []; + + $: selectedProjectsWithRole = [ + ...selectedProjectsWithRole.filter((pr) => + selectedProjects.some((p) => p.$id === pr.project.$id) + ), + ...selectedProjects + .filter((p) => !selectedProjectsWithRole.some((pr) => pr.project.$id === p.$id)) + .map((p) => ({ project: p, role })) + ]; + + function removeProject(projectId: string) { + selectedProjects = selectedProjects.filter((p) => p.$id !== projectId); + } + async function create() { try { + const roles = isInvitingToSpecificProjects + ? [ + 'member', + ...selectedProjectsWithRole.map( + (pr) => `project:${pr.project.$id}/${pr.role}` + ) + ] + : [role]; + const team = await sdk.forConsole.teams.createMembership({ teamId: $organization.$id, - roles: [role], + roles, email, url: `${page.url.origin}${base}/invite`, name: name || undefined @@ -71,7 +100,68 @@ autofocus bind:value={email} /> - {#if isCloud} + + + + If enabled, you will be able to select specific projects to invite the member to. + Otherwise, the member will be invited to all projects in the organization. + + + {#if isInvitingToSpecificProjects} + {#if selectedProjects.length > 0} + + + Project + + + Role + + + + + + + + + {#each selectedProjectsWithRole as selected} + + {selected.project.name} + + + + + + + + {/each} + + {/if} +

+ +
+ {:else} @@ -83,8 +173,18 @@ {/if} + - + + + diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index 2c5c14ab86..81b69276b1 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -23,6 +23,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { (org) => org.$id === project.teamId ); + console.log('organization', organization); + const includedInBasePlans = plansInfo.has(organization.billingPlan); const [org, regionalConsoleVariables, rolesResult, organizationPlan] = await Promise.all([ From 2479fa6065c015118a7bf922d765c95fe5a13466 Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Mon, 24 Nov 2025 19:47:57 +0530 Subject: [PATCH 2/2] works --- src/lib/stores/billing.ts | 2 +- .../createMember.svelte | 168 +++++++-------- .../members/+page.svelte | 6 +- .../members/edit.svelte | 201 +++++++++++++----- .../members}/project.svelte | 107 ++++++---- .../project-[region]-[project]/+layout.ts | 2 - 6 files changed, 306 insertions(+), 180 deletions(-) rename src/{lib/components/permissions => routes/(console)/organization-[organization]/members}/project.svelte (54%) diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 5cea34fa35..f20ef98dda 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -67,7 +67,7 @@ export const roles = [ } ]; -export const projectSpecificRoles = roles.filter((r) => r.value !== 'billing'); +export const projectRoles = roles.filter((r) => r.value !== 'billing'); export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; diff --git a/src/routes/(console)/organization-[organization]/createMember.svelte b/src/routes/(console)/organization-[organization]/createMember.svelte index 8ff4a495db..460cf842be 100644 --- a/src/routes/(console)/organization-[organization]/createMember.svelte +++ b/src/routes/(console)/organization-[organization]/createMember.svelte @@ -2,23 +2,23 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Modal } from '$lib/components'; - import ChooseProject from '$lib/components/permissions/project.svelte'; import { InputText, InputEmail, Button, InputSwitch } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { createEventDispatcher } from 'svelte'; import { organization } from '$lib/stores/organization'; import { invalidate } from '$app/navigation'; - import { Dependencies } from '$lib/constants'; + import { BillingPlan, Dependencies } from '$lib/constants'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; import { isCloud, isSelfHosted } from '$lib/system'; - import { projectSpecificRoles, roles } from '$lib/stores/billing'; + import { projectRoles, roles } from '$lib/stores/billing'; import InputSelect from '$lib/elements/forms/inputSelect.svelte'; import Roles from '$lib/components/roles/roles.svelte'; import { Icon, Popover, Table } from '@appwrite.io/pink-svelte'; import { IconInfo, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; import { Layout } from '@appwrite.io/pink-svelte'; import type { Models } from '@appwrite.io/console'; + import ChooseProject from './members/project.svelte'; export let showCreate = false; @@ -31,20 +31,12 @@ let showChooseProjects = false; let isInvitingToSpecificProjects = false; - let selectedProjects: Models.Project[] = []; let selectedProjectsWithRole: { project: Models.Project; role: string }[] = []; - $: selectedProjectsWithRole = [ - ...selectedProjectsWithRole.filter((pr) => - selectedProjects.some((p) => p.$id === pr.project.$id) - ), - ...selectedProjects - .filter((p) => !selectedProjectsWithRole.some((pr) => pr.project.$id === p.$id)) - .map((p) => ({ project: p, role })) - ]; - function removeProject(projectId: string) { - selectedProjects = selectedProjects.filter((p) => p.$id !== projectId); + selectedProjectsWithRole = selectedProjectsWithRole.filter( + (p) => p.project.$id !== projectId + ); } async function create() { @@ -100,91 +92,93 @@ autofocus bind:value={email} /> - - - - If enabled, you will be able to select specific projects to invite the member to. - Otherwise, the member will be invited to all projects in the organization. - - - {#if isInvitingToSpecificProjects} - {#if selectedProjects.length > 0} - - - Project - - - Role - - - - - - - + {#if isCloud} + {#if $organization?.billingPlan === BillingPlan.SCALE} + + + If enabled, you will be able to select specific projects to invite the member to. + Otherwise, the member will be invited to all projects in the organization. - {#each selectedProjectsWithRole as selected} - - {selected.project.name} - - - - - - - - {/each} - + + {/if} + {#if isInvitingToSpecificProjects} + {#if selectedProjectsWithRole.length > 0} + + + Project + + + Role + + + + + + + + + {#each selectedProjectsWithRole as selected} + + {selected.project.name} + + + + + + + + {/each} + + {/if} +
+ +
+ {:else} + + + + + + + + {/if} -
- -
- {:else} - - - - - - - - {/if} - diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte index 05c3500802..55bf6527a9 100644 --- a/src/routes/(console)/organization-[organization]/members/+page.svelte +++ b/src/routes/(console)/organization-[organization]/members/+page.svelte @@ -143,7 +143,11 @@ - {member.roles.map((role) => getRoleLabel(role)).join(', ')} + {#if member.roles.some((role) => role.startsWith('project:'))} + {"Custom"} + {:else} + {member.roles.map((role) => getRoleLabel(role)).join(', ')} + {/if} import { Modal } from '$lib/components'; - import { Button } from '$lib/elements/forms'; + import { Button, InputSwitch } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { createEventDispatcher } from 'svelte'; import { organization } from '$lib/stores/organization'; import { invalidate } from '$app/navigation'; - import { Dependencies } from '$lib/constants'; + import { BillingPlan, Dependencies } from '$lib/constants'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; import InputSelect from '$lib/elements/forms/inputSelect.svelte'; - import type { Models } from '@appwrite.io/console'; + import { Query, type Models } from '@appwrite.io/console'; import Roles from '$lib/components/roles/roles.svelte'; - import { IconInfo } from '@appwrite.io/pink-icons-svelte'; - import { Icon, Layout, Popover } from '@appwrite.io/pink-svelte'; + import { IconInfo, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; + import { Icon, Layout, Popover, Table } from '@appwrite.io/pink-svelte'; + import { projectRoles, roles } from '$lib/stores/billing'; + import ChooseProject from './project.svelte'; + import { isSelfHosted } from '$lib/system'; - export let showEdit = false; - export let selectedMember: Models.Membership; + let { + showEdit = $bindable(), + selectedMember + }: { showEdit: boolean; selectedMember: Models.Membership } = $props(); const dispatch = createEventDispatcher(); + const defaultTeamRole = isSelfHosted ? 'owner' : 'developer'; - let error: string; - let role = selectedMember?.roles?.[0]; + let error: string = $state(null); + let showChooseProjects = $state(false); - const roles = [ - { - label: 'Owner', - value: 'owner' - }, - { - label: 'Developer', - value: 'developer' - }, - { - label: 'Editor', - value: 'editor' - }, - { - label: 'Analyst', - value: 'analyst' - }, - { - label: 'Billing', - value: 'billing' - } - ]; + let isProjectSpecificRoles = $derived( + selectedMember?.roles?.some((r) => r.startsWith('project:')) + ); + let teamRole = $derived(isProjectSpecificRoles ? defaultTeamRole : selectedMember?.roles?.[0]); + let isRestrictingToSpecificProjects = $derived(isProjectSpecificRoles); + let selectedProjectsWithRole: { project: Models.Project; role: string }[] = $state([]); + + function removeProject(projectId: string) { + selectedProjectsWithRole = selectedProjectsWithRole.filter( + (p) => p.project.$id !== projectId + ); + } async function submit() { try { + const roles = isRestrictingToSpecificProjects + ? [ + 'member', + ...selectedProjectsWithRole.map( + (pr) => `project:${pr.project.$id}/${pr.role}` + ) + ] + : [teamRole]; + const membership = await sdk.forConsole.teams.updateMembership({ teamId: $organization.$id, membershipId: selectedMember.$id, - roles: [role] + roles: roles }); await invalidate(Dependencies.ACCOUNT); await invalidate(Dependencies.ORGANIZATION); @@ -69,30 +74,126 @@ } } - $: if (!showEdit) { - error = null; - role = null; - } + $effect(() => { + if (!showEdit) { + error = null; + } + }); - $: if (showEdit && !role) { - role = selectedMember.roles?.[0]; - } + $effect(() => { + if (!showEdit || !isProjectSpecificRoles) { + return; + } + + async function loadProjects() { + const projectIdToRole = selectedMember?.roles + ?.filter((r) => r.startsWith('project:')) + .reduce((acc, r) => { + const parts = r.split(':')[1]; + const [projectId, role] = parts.split('/'); + acc[projectId] = role; + return acc; + }, {}); + const projectIdsQuery = Object.keys(projectIdToRole).map((id) => + Query.equal('$id', id) + ); + const projectsList = await sdk.forConsole.projects.list({ + queries: [Query.equal('teamId', $organization.$id), Query.or(projectIdsQuery)] + }); + selectedProjectsWithRole = projectsList.projects.map((p) => ({ + project: p, + role: projectIdToRole[p.$id] + })); + } + + loadProjects(); + }); - - - - - - - - + {#if $organization?.billingPlan === BillingPlan.SCALE} + + + If enabled, you will be able to allow access specific projects. + Otherwise, the member will have access to all projects in the organization. + + + {/if} + {#if isRestrictingToSpecificProjects} + {#if selectedProjectsWithRole.length > 0} + + + Project + + + Role + + + + + + + + + {#each selectedProjectsWithRole as selected} + + {selected.project.name} + + + + + + + + {/each} + + {/if} +
+ +
+ {:else} + + + + + + + + + {/if} - +
+ + diff --git a/src/lib/components/permissions/project.svelte b/src/routes/(console)/organization-[organization]/members/project.svelte similarity index 54% rename from src/lib/components/permissions/project.svelte rename to src/routes/(console)/organization-[organization]/members/project.svelte index 8533bd8d1a..d58cbfc003 100644 --- a/src/lib/components/permissions/project.svelte +++ b/src/routes/(console)/organization-[organization]/members/project.svelte @@ -1,19 +1,41 @@ @@ -73,7 +100,9 @@ p.$id === project.$id)} /> + checked={selectedProjectsWithRole.some( + (p) => p.project.$id === project.$id + )} />
@@ -95,36 +124,36 @@ -

Total results: {results?.total}

- +

Total results: {results?.total}

+
{:else if search} - - - + + + {:else if isLoading} - -
- -
+ +
+ +
{:else} - - - - Need a hand? Learn more in our - documentation. - - - + + + + Need a hand? Learn more in our + documentation. + + + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index 81b69276b1..2c5c14ab86 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -23,8 +23,6 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { (org) => org.$id === project.teamId ); - console.log('organization', organization); - const includedInBasePlans = plansInfo.has(organization.billingPlan); const [org, regionalConsoleVariables, rolesResult, organizationPlan] = await Promise.all([