Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/lib/components/roles/roles.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<script>
import Base from './base.svelte';

const { isProjectSpecific = false } = $props();
</script>

<Base>
<div class="u-flex-vertical u-gap-8">
<p class="u-bold">Roles</p>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
<p>
{isProjectSpecific
? 'Owner, Developer, Editor and Analyst'
: 'Owner, Developer, Editor, Analyst and Billing.'}
</p>
<p>
<a
class="link"
Expand Down
2 changes: 2 additions & 0 deletions src/lib/stores/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const roles = [
}
];

export const projectRoles = roles.filter((r) => r.value !== 'billing');

export const teamStatusReadonly = 'readonly';
export const billingLimitOutstandingInvoice = 'outstanding_invoice';

Expand Down
128 changes: 111 additions & 17 deletions src/routes/(console)/organization-[organization]/createMember.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -72,19 +93,92 @@
bind:value={email} />
<InputText id="member-name" label="Name" placeholder="Enter name" bind:value={name} />
{#if isCloud}
<InputSelect required id="role" label="Role" options={roles} bind:value={role}>
<Layout.Stack direction="row" gap="none" alignItems="center" slot="info">
<Popover let:toggle>
<Button extraCompact size="s" on:click={toggle}>
<Icon size="s" icon={IconInfo} />
</Button>
<svelte:component this={Roles} slot="tooltip" />
</Popover>
</Layout.Stack>
</InputSelect>
{#if $organization?.billingPlan === BillingPlan.SCALE}
<InputSwitch
id="inviting-to-specific-projects"
label="Invite to specific projects"
bind:value={isInvitingToSpecificProjects}>
<svelte:fragment slot="description">
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.
</svelte:fragment>
</InputSwitch>
{/if}
{#if isInvitingToSpecificProjects}
{#if selectedProjectsWithRole.length > 0}
<Table.Root
columns={[{ id: 'project' }, { id: 'role' }, { id: 'action', width: 40 }]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="project" {root}>Project</Table.Header.Cell>
<Table.Header.Cell column="role" {root}>
<Layout.Stack direction="row" gap="xxs" alignItems="center">
Role
<Popover let:toggle>
<Button extraCompact size="s" on:click={toggle}>
<Icon size="s" icon={IconInfo} />
</Button>
<svelte:component
this={Roles}
isProjectSpecific={true}
slot="tooltip" />
</Popover>
</Layout.Stack>
</Table.Header.Cell>
<Table.Header.Cell column="action" {root} />
</svelte:fragment>
{#each selectedProjectsWithRole as selected}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>{selected.project.name}</Table.Cell>
<Table.Cell column="role" {root}>
<InputSelect
required
id="role"
options={projectRoles}
bind:value={selected.role} />
</Table.Cell>
<Table.Cell column="action" {root}>
<Button
compact
icon
on:click={() => removeProject(selected.project.$id)}>
<Icon icon={IconX} size="s" />
</Button>
</Table.Cell>
</Table.Row.Base>
{/each}
Comment on lines +130 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate id="role" attributes in table rows.

Each InputSelect in the loop uses id="role", creating duplicate IDs in the DOM. This violates HTML standards and can cause accessibility issues with label association.

Consider using unique IDs per project.

-                            <InputSelect
-                                required
-                                id="role"
-                                options={projectRoles}
-                                bind:value={selected.role} />
+                            <InputSelect
+                                required
+                                id={`role-${selected.project.$id}`}
+                                options={projectRoles}
+                                bind:value={selected.role} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{#each selectedProjectsWithRole as selected}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>{selected.project.name}</Table.Cell>
<Table.Cell column="role" {root}>
<InputSelect
required
id="role"
options={projectRoles}
bind:value={selected.role} />
</Table.Cell>
<Table.Cell column="action" {root}>
<Button
compact
icon
on:click={() => removeProject(selected.project.$id)}>
<Icon icon={IconX} size="s" />
</Button>
</Table.Cell>
</Table.Row.Base>
{/each}
{#each selectedProjectsWithRole as selected}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>{selected.project.name}</Table.Cell>
<Table.Cell column="role" {root}>
<InputSelect
required
id={`role-${selected.project.$id}`}
options={projectRoles}
bind:value={selected.role} />
</Table.Cell>
<Table.Cell column="action" {root}>
<Button
compact
icon
on:click={() => removeProject(selected.project.$id)}>
<Icon icon={IconX} size="s" />
</Button>
</Table.Cell>
</Table.Row.Base>
{/each}
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/createMember.svelte around
lines 130 to 149 the InputSelect inside the each loop uses a static id="role"
for every row, producing duplicate DOM IDs; change the id to a unique value per
iteration (for example by appending the project id or the loop index such as
id={`role-${selected.project.$id}`} or id={`role-${i}`}) and update any
corresponding labels' for attribute to match the new id so each input has a
unique, accessible identifier.

</Table.Root>
{/if}
<div>
<Button compact on:click={() => (showChooseProjects = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Choose projects
</Button>
</div>
{:else}
<InputSelect required id="role" label="Role" options={roles} bind:value={role}>
<Layout.Stack direction="row" gap="none" alignItems="center" slot="info">
<Popover let:toggle>
<Button extraCompact size="s" on:click={toggle}>
<Icon size="s" icon={IconInfo} />
</Button>
<svelte:component this={Roles} slot="tooltip" />
</Popover>
</Layout.Stack>
</InputSelect>
{/if}
{/if}
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit submissionLoader>Send invite</Button>
<Button
submit
submissionLoader
disabled={isInvitingToSpecificProjects && selectedProjectsWithRole.length === 0}
>Send invite</Button>
</svelte:fragment>
</Modal>

<ChooseProject
orgId={$organization.$id}
bind:projectsWithRole={selectedProjectsWithRole}
bind:show={showChooseProjects} />
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@
</Typography.Text>
</Table.Cell>
<Table.Cell column="roles" {root}>
{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}
</Table.Cell>
<Table.Cell column="mfa" {root}>
<Badge
Expand Down
Loading
Loading