mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
fix: added the checkbox table variants
This commit is contained in:
@@ -20,13 +20,26 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { writable } from 'svelte/store';
|
||||
import Create from './createUser.svelte';
|
||||
import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
Table,
|
||||
Layout,
|
||||
Typography,
|
||||
FloatingActionBar
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { canWriteUsers } from '$lib/stores/roles';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import ViewSelector from '$lib/components/viewSelector.svelte';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -46,11 +59,47 @@
|
||||
}
|
||||
]);
|
||||
|
||||
let selectedUsers: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
|
||||
showCreateUser.set(false);
|
||||
await goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${event.detail.$id}`
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedUsers.map((userId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).users.delete(userId)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.UserDelete, {
|
||||
total: selectedUsers.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedUsers.length} user${selectedUsers.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.USERS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.UserDelete);
|
||||
} finally {
|
||||
selectedUsers = [];
|
||||
showDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -68,7 +117,11 @@
|
||||
</Layout.Stack>
|
||||
|
||||
{#if data.users.total}
|
||||
<Table.Root columns={$columns} let:root>
|
||||
<Table.Root
|
||||
columns={$columns}
|
||||
allowSelection={$canWriteUsers}
|
||||
bind:selectedRows={selectedUsers}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each $columns as { id, title } (id)}
|
||||
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
|
||||
@@ -77,7 +130,8 @@
|
||||
{#each data.users.users as user}
|
||||
<Table.Row.Link
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`}
|
||||
{root}>
|
||||
{root}
|
||||
id={user.$id}>
|
||||
{#each $columns as { id } (id)}
|
||||
<Table.Cell column={id} {root}>
|
||||
{#if id === '$id'}
|
||||
@@ -175,6 +229,33 @@
|
||||
allowCreate={$canWriteUsers}
|
||||
on:click={() => showCreateUser.set(true)} />
|
||||
{/if}
|
||||
|
||||
{#if selectedUsers.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedUsers.length.toString()} />
|
||||
<span>
|
||||
{selectedUsers.length > 1 ? 'users' : 'user'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedUsers = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Create bind:showCreate={$showCreateUser} on:created={userCreated} />
|
||||
|
||||
<Confirm title="Delete users" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedUsers.length}</b>
|
||||
{selectedUsers.length > 1 ? 'users' : 'user'}?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will permanently remove the selected users and all their
|
||||
data.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import { PAGE_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { showCreateUser } from './+page.svelte';
|
||||
|
||||
export const load: PageLoad = async ({ url, route, params }) => {
|
||||
export const load: PageLoad = async ({ url, route, params, depends }) => {
|
||||
depends(Dependencies.USERS);
|
||||
|
||||
const page = getPage(url);
|
||||
const search = getSearch(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
|
||||
@@ -19,17 +19,72 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { writable } from 'svelte/store';
|
||||
import { canWriteTeams } from '$lib/stores/roles';
|
||||
import { Icon, Layout, Table } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Icon,
|
||||
Layout,
|
||||
Table,
|
||||
FloatingActionBar,
|
||||
Badge,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const region = page.params.region;
|
||||
const project = page.params.project;
|
||||
|
||||
let selectedTeams: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
const columns = writable([
|
||||
{ id: 'name', title: 'Name', type: 'string', width: { min: 200, max: 300 } },
|
||||
{ id: 'members', title: 'Members', type: 'string', width: { min: 120, max: 150 } },
|
||||
{ id: 'created', title: 'Created', type: 'string', width: { min: 120, max: 180 } }
|
||||
]);
|
||||
|
||||
const teamCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
|
||||
await goto(`${base}/project-${region}-${project}/auth/teams/team-${event.detail.$id}`);
|
||||
};
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedTeams.map((teamId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.delete(teamId)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.TeamDelete, {
|
||||
total: selectedTeams.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedTeams.length} team${selectedTeams.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.TEAMS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.TeamDelete);
|
||||
} finally {
|
||||
selectedTeams = [];
|
||||
showDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -46,32 +101,55 @@
|
||||
</Layout.Stack>
|
||||
|
||||
{#if data.teams.total}
|
||||
<Table.Root columns={3} let:root>
|
||||
<Table.Root
|
||||
columns={$columns}
|
||||
allowSelection={$canWriteTeams}
|
||||
bind:selectedRows={selectedTeams}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Members</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Created</Table.Header.Cell>
|
||||
{#each $columns as { id, title }}
|
||||
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
{#each data.teams.teams as team}
|
||||
{#each data.teams.teams as team (team.$id)}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${region}-${project}/auth/teams/team-${team.$id}`}>
|
||||
<Table.Cell {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<AvatarInitials size="xs" name={team.name} />
|
||||
<span class="u-trim">{team.name}</span>
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{team.total} members
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<DualTimeView time={team.$createdAt} />
|
||||
</Table.Cell>
|
||||
href={`${base}/project-${region}-${project}/auth/teams/team-${team.$id}`}
|
||||
id={team.$id}>
|
||||
{#each $columns as column}
|
||||
<Table.Cell column={column.id} {root}>
|
||||
{#if column.id === 'name'}
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<AvatarInitials size="xs" name={team.name} />
|
||||
<span class="u-trim">{team.name}</span>
|
||||
</Layout.Stack>
|
||||
{:else if column.id === 'members'}
|
||||
{team.total} members
|
||||
{:else if column.id === 'created'}
|
||||
<DualTimeView time={team.$createdAt} />
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedTeams.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedTeams.length.toString()} />
|
||||
<span>
|
||||
{selectedTeams.length > 1 ? 'teams' : 'team'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedTeams = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Teams"
|
||||
limit={data.limit}
|
||||
@@ -94,3 +172,14 @@
|
||||
</Container>
|
||||
|
||||
<Create bind:showCreate={$showCreateTeam} on:created={teamCreated} />
|
||||
|
||||
<Confirm title="Delete teams" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedTeams.length}</b>
|
||||
{selectedTeams.length > 1 ? 'teams' : 'team'}?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will permanently remove the selected teams and all their
|
||||
memberships.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import { PAGE_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ url, route, params }) => {
|
||||
export const load: PageLoad = async ({ url, route, params, depends }) => {
|
||||
depends(Dependencies.TEAMS);
|
||||
|
||||
const page = getPage(url);
|
||||
const search = getSearch(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
|
||||
+85
-5
@@ -11,15 +11,28 @@
|
||||
import CreateMember from '../createMembership.svelte';
|
||||
import DeleteMembership from '../deleteMembership.svelte';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Table, Layout, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { Click, trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import {
|
||||
Table,
|
||||
Layout,
|
||||
Icon,
|
||||
FloatingActionBar,
|
||||
Badge,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showCreate = false;
|
||||
let showDelete = false;
|
||||
let selectedMembership: Models.Membership;
|
||||
let selectedMemberships: string[] = [];
|
||||
let showBulkDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
const region = page.params.region;
|
||||
const project = page.params.project;
|
||||
@@ -27,6 +40,40 @@
|
||||
async function memberCreated() {
|
||||
invalidate(Dependencies.MEMBERSHIPS);
|
||||
}
|
||||
|
||||
async function handleBulkDelete() {
|
||||
showBulkDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedMemberships.map((membershipId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
|
||||
teamId: page.params.team,
|
||||
membershipId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MembershipUpdate, {
|
||||
total: selectedMemberships.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedMemberships.length} membership${selectedMemberships.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.MEMBERSHIPS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MembershipUpdate);
|
||||
} finally {
|
||||
selectedMemberships = [];
|
||||
showBulkDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -40,6 +87,8 @@
|
||||
{#if data.memberships.total}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedMemberships}
|
||||
columns={[
|
||||
{ id: 'name' },
|
||||
{ id: 'roles' },
|
||||
@@ -52,11 +101,12 @@
|
||||
<Table.Header.Cell column="joined" {root}>Joined</Table.Header.Cell>
|
||||
<Table.Header.Cell column="actions" {root} />
|
||||
</svelte:fragment>
|
||||
{#each data.memberships.memberships as membership}
|
||||
{#each data.memberships.memberships as membership (membership.$id)}
|
||||
{@const username = membership.userName ? membership.userName : '-'}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${region}-${project}/auth/user-${membership.userId}`}>
|
||||
href={`${base}/project-${region}-${project}/auth/user-${membership.userId}`}
|
||||
id={membership.$id}>
|
||||
<Table.Cell column="name" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<AvatarInitials size="xs" name={username} />
|
||||
@@ -85,6 +135,22 @@
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedMemberships.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedMemberships.length.toString()} />
|
||||
<span>
|
||||
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedMemberships = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showBulkDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Memberships"
|
||||
limit={data.limit}
|
||||
@@ -93,7 +159,7 @@
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find ‘{data.search}'</b>
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no members that match your search.</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -118,3 +184,17 @@
|
||||
{selectedMembership}
|
||||
bind:showDelete
|
||||
on:deleted={() => invalidate(Dependencies.MEMBERSHIPS)} />
|
||||
|
||||
<Confirm
|
||||
title="Delete memberships"
|
||||
bind:open={showBulkDelete}
|
||||
onSubmit={handleBulkDelete}
|
||||
disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedMemberships.length}</b>
|
||||
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will remove the selected members from this team.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
+87
-3
@@ -6,23 +6,76 @@
|
||||
import { Container } from '$lib/layout';
|
||||
import DeleteMembership from '../deleteMembership.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Table, Layout, Empty, Card } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Table,
|
||||
Layout,
|
||||
Empty,
|
||||
Card,
|
||||
FloatingActionBar,
|
||||
Badge,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let selectedMembership: Models.Membership;
|
||||
let showDelete = false;
|
||||
let selectedMemberships: string[] = [];
|
||||
let showBulkDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
const region = page.params.region;
|
||||
const project = page.params.project;
|
||||
|
||||
async function handleBulkDelete() {
|
||||
showBulkDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedMemberships.map((membershipId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
|
||||
teamId:
|
||||
data.memberships.memberships.find((m) => m.$id === membershipId)?.teamId || '',
|
||||
membershipId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MembershipUpdate, {
|
||||
total: selectedMemberships.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedMemberships.length} membership${selectedMemberships.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.MEMBERSHIPS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MembershipUpdate);
|
||||
} finally {
|
||||
selectedMemberships = [];
|
||||
showBulkDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
{#if data.memberships.total}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedMemberships}
|
||||
columns={[
|
||||
{ id: 'name' },
|
||||
{ id: 'roles' },
|
||||
@@ -38,7 +91,8 @@
|
||||
{#each data.memberships.memberships as membership}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${region}-${project}/auth/teams/team-${membership.teamId}`}>
|
||||
href={`${base}/project-${region}-${project}/auth/teams/team-${membership.teamId}`}
|
||||
id={membership.$id}>
|
||||
<Table.Cell column="name" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<AvatarInitials size="xs" name={membership.teamName} />
|
||||
@@ -83,6 +137,36 @@
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
|
||||
{#if selectedMemberships.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedMemberships.length.toString()} />
|
||||
<span>
|
||||
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedMemberships = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showBulkDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<DeleteMembership {selectedMembership} bind:showDelete />
|
||||
|
||||
<Confirm
|
||||
title="Delete memberships"
|
||||
bind:open={showBulkDelete}
|
||||
onSubmit={handleBulkDelete}
|
||||
disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedMemberships.length}</b>
|
||||
{selectedMemberships.length > 1 ? 'memberships' : 'membership'}?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will remove the user from the selected teams.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
export const load: PageLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.MEMBERSHIPS);
|
||||
|
||||
const codes = await sdk.forProject(params.region, params.project).locale.listCodes();
|
||||
return {
|
||||
localeCodes: codes.localeCodes,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import { CARD_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ url, route, params }) => {
|
||||
export const load: PageLoad = async ({ url, route, params, depends }) => {
|
||||
depends(Dependencies.BUCKET);
|
||||
|
||||
const page = getPage(url);
|
||||
const search = getSearch(url);
|
||||
const view = getView(url, route, View.Grid);
|
||||
|
||||
+87
-32
@@ -14,7 +14,17 @@
|
||||
import { uploader } from '$lib/stores/uploader';
|
||||
import { sdk } from '$lib/stores/sdk.js';
|
||||
import DeleteFile from './deleteFile.svelte';
|
||||
import { Layout, Table, Icon, Popover, ActionMenu } from '@appwrite.io/pink-svelte';
|
||||
import { bucket } from './store';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import {
|
||||
Layout,
|
||||
Table,
|
||||
Icon,
|
||||
Popover,
|
||||
ActionMenu,
|
||||
FloatingActionBar,
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import DualTimeView from '$lib/components/dualTimeView.svelte';
|
||||
import {
|
||||
@@ -28,6 +38,9 @@
|
||||
|
||||
let showDelete = false;
|
||||
let selectedFile: Models.File = null;
|
||||
let selectedFiles: string[] = [];
|
||||
let showBulkDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
const bucketId = page.params.bucket;
|
||||
|
||||
@@ -52,19 +65,41 @@
|
||||
}
|
||||
|
||||
async function deleteFile(file: Models.File) {
|
||||
selectedFile = file;
|
||||
showDelete = true;
|
||||
}
|
||||
|
||||
async function handleBulkDelete() {
|
||||
showBulkDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedFiles.map((fileId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).storage.deleteFile({
|
||||
bucketId,
|
||||
fileId
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.deleteFile({ bucketId: file.bucketId, fileId: file.$id });
|
||||
await invalidate(Dependencies.FILES);
|
||||
await uploader.removeFile(file);
|
||||
trackEvent(Submit.FileDelete);
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.FileDelete, {
|
||||
total: selectedFiles.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.FILES);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.FileDelete);
|
||||
} finally {
|
||||
selectedFiles = [];
|
||||
showBulkDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +147,8 @@
|
||||
{#if data.files.total}
|
||||
<Table.Root
|
||||
let:root
|
||||
allowSelection
|
||||
bind:selectedRows={selectedFiles}
|
||||
columns={[
|
||||
{ id: 'filename' },
|
||||
{ id: 'type', width: { min: 140 } },
|
||||
@@ -128,7 +165,7 @@
|
||||
</svelte:fragment>
|
||||
{#each data.files.files as file}
|
||||
{#if file.chunksTotal / file.chunksUploaded !== 1}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Row.Base {root} id={file.$id}>
|
||||
<Table.Cell column="filename" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<span class="avatar is-size-small is-color-empty"></span>
|
||||
@@ -158,7 +195,7 @@
|
||||
</Table.Row.Base>
|
||||
{:else}
|
||||
{@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/file-${file.$id}`}
|
||||
<Table.Row.Link {href} {root}>
|
||||
<Table.Row.Link {href} {root} id={file.$id}>
|
||||
<Table.Cell column="filename" {root}>
|
||||
<div class="u-flex u-gap-12 u-cross-center">
|
||||
<Avatar size="xs" src={getPreview(file.$id)} alt={file.name} />
|
||||
@@ -213,37 +250,55 @@
|
||||
offset={data.offset}
|
||||
total={data.files.total} />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no files that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
text>
|
||||
Documentation
|
||||
</Button>
|
||||
<Button
|
||||
secondary
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}`}>
|
||||
Clear Search
|
||||
</Button>
|
||||
</div>
|
||||
<EmptySearch target="files" search={data.search} hidePagination={data.files.total === 0}>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}`}>
|
||||
Clear Search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
href="https://appwrite.io/docs/products/storage"
|
||||
target="file"
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
allowCreate
|
||||
on:click={() =>
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/create`
|
||||
)} />
|
||||
{/if}
|
||||
|
||||
{#if selectedFiles.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedFiles.length.toString()} />
|
||||
<span>
|
||||
{selectedFiles.length > 1 ? 'files' : 'file'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedFiles = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showBulkDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
{#if selectedFile}
|
||||
<DeleteFile file={selectedFile} bind:showDelete on:deleted={fileDeleted} />
|
||||
{/if}
|
||||
<DeleteFile bind:showDelete file={selectedFile} on:deleted={fileDeleted} />
|
||||
|
||||
<Confirm
|
||||
title="Delete files"
|
||||
bind:open={showBulkDelete}
|
||||
onSubmit={handleBulkDelete}
|
||||
disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedFiles.length}</b>
|
||||
{selectedFiles.length > 1 ? 'files' : 'file'} from <b>{$bucket?.name}</b>?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will permanently remove the selected files.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
@@ -5,12 +5,59 @@
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { Table } from '@appwrite.io/pink-svelte';
|
||||
import { Table, FloatingActionBar, Badge, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import Confirm from '$lib/components/confirm.svelte';
|
||||
import { canWriteBuckets } from '$lib/stores/roles';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let selectedBuckets: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
deleting = true;
|
||||
|
||||
const promises = selectedBuckets.map((bucketId) =>
|
||||
sdk.forProject(page.params.region, page.params.project).storage.deleteBucket(bucketId)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.BucketDelete, {
|
||||
total: selectedBuckets.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedBuckets.length} bucket${selectedBuckets.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.BUCKET);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.BucketDelete);
|
||||
} finally {
|
||||
selectedBuckets = [];
|
||||
showDelete = false;
|
||||
deleting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table.Root columns={$columns} let:root>
|
||||
<Table.Root
|
||||
columns={$columns}
|
||||
allowSelection={$canWriteBuckets}
|
||||
bind:selectedRows={selectedBuckets}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each $columns as { id, title }}
|
||||
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
|
||||
@@ -19,7 +66,8 @@
|
||||
{#each data.buckets.buckets as bucket (bucket.$id)}
|
||||
<Table.Row.Link
|
||||
{root}
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucket.$id}`}>
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucket.$id}`}
|
||||
id={bucket.$id}>
|
||||
{#each $columns as column}
|
||||
<Table.Cell column={column.id} {root}>
|
||||
{#if column.id === '$id'}
|
||||
@@ -38,3 +86,30 @@
|
||||
</Table.Row.Link>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
|
||||
{#if selectedBuckets.length > 0}
|
||||
<FloatingActionBar>
|
||||
<svelte:fragment slot="start">
|
||||
<Badge content={selectedBuckets.length.toString()} />
|
||||
<span>
|
||||
{selectedBuckets.length > 1 ? 'buckets' : 'bucket'}
|
||||
selected
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<Button text on:click={() => (selectedBuckets = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</FloatingActionBar>
|
||||
{/if}
|
||||
|
||||
<Confirm title="Delete buckets" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete <b>{selectedBuckets.length}</b>
|
||||
{selectedBuckets.length > 1 ? 'buckets' : 'bucket'}?
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
This action is irreversible and will permanently remove the selected buckets and all their
|
||||
files.
|
||||
</Typography.Text>
|
||||
</Confirm>
|
||||
|
||||
Reference in New Issue
Block a user