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..460cf842be 100644
--- a/src/routes/(console)/organization-[organization]/createMember.svelte
+++ b/src/routes/(console)/organization-[organization]/createMember.svelte
@@ -2,21 +2,23 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { Modal } from '$lib/components';
- import { InputText, InputEmail, Button } from '$lib/elements/forms';
+ 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 { 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 } 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';
+ import ChooseProject from './members/project.svelte';
export let showCreate = false;
@@ -27,11 +29,30 @@
error: string,
role: string = isSelfHosted ? 'owner' : 'developer';
+ let showChooseProjects = false;
+ let isInvitingToSpecificProjects = false;
+ let selectedProjectsWithRole: { project: Models.Project; role: string }[] = [];
+
+ function removeProject(projectId: string) {
+ selectedProjectsWithRole = selectedProjectsWithRole.filter(
+ (p) => p.project.$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
@@ -72,19 +93,92 @@
bind:value={email} />
{#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.
+
+
+ {/if}
+ {#if isInvitingToSpecificProjects}
+ {#if selectedProjectsWithRole.length > 0}
+
+
+ Project
+
+
+ Role
+
+
+
+
+
+
+
+
+ {#each selectedProjectsWithRole as selected}
+
+ {selected.project.name}
+
+
+
+
+
+
+
+ {/each}
+
+ {/if}
+
+
+
+ {:else}
+
+
+
+
+
+
+
+
+ {/if}
{/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/routes/(console)/organization-[organization]/members/project.svelte b/src/routes/(console)/organization-[organization]/members/project.svelte
new file mode 100644
index 0000000000..d58cbfc003
--- /dev/null
+++ b/src/routes/(console)/organization-[organization]/members/project.svelte
@@ -0,0 +1,162 @@
+
+
+
+ Grant access to any project in the organization.
+
+
+ {#if results?.projects?.length}
+
+ {#each results.projects as project (project.$id)}
+ toggleSelection(project)}>
+
+
+ p.project.$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}
+
+
+
+
+