mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '8.x' into 'feat-documentsdb'.
This commit is contained in:
@@ -10,8 +10,10 @@
|
||||
Layout,
|
||||
Popover,
|
||||
Selector,
|
||||
Typography
|
||||
Typography,
|
||||
Icon
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
let {
|
||||
@@ -21,7 +23,8 @@
|
||||
allowNoColumns = false,
|
||||
showAnyway = false,
|
||||
children,
|
||||
onPreferencesUpdated = null
|
||||
onPreferencesUpdated = null,
|
||||
onCustomOptionClick = null
|
||||
}: {
|
||||
columns: Writable<Column[]>;
|
||||
isCustomTable?: boolean;
|
||||
@@ -30,6 +33,7 @@
|
||||
showAnyway?: boolean;
|
||||
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
|
||||
onPreferencesUpdated?: () => void;
|
||||
onCustomOptionClick?: () => void;
|
||||
} = $props();
|
||||
|
||||
let search = $state('');
|
||||
@@ -115,7 +119,7 @@
|
||||
cols.map((col) =>
|
||||
col.exclude
|
||||
? col
|
||||
: filteredColumns.some((fc) => fc.id === col.id)
|
||||
: filteredColumns.some((fc) => fc.id === col.id && !col.disable)
|
||||
? { ...col, hide: false }
|
||||
: col
|
||||
)
|
||||
@@ -126,7 +130,9 @@
|
||||
function deselectAll() {
|
||||
columns.update((cols) => {
|
||||
const realColumns = cols.filter((col) => !col.exclude && !col.isAction);
|
||||
const filtered = filteredColumns.filter((col) => !col.exclude && !col.isAction);
|
||||
const filtered = filteredColumns.filter(
|
||||
(col) => !col.exclude && !col.isAction && !col.disable
|
||||
);
|
||||
|
||||
if (filtered.length === 0) return cols;
|
||||
|
||||
@@ -187,7 +193,7 @@
|
||||
{@const placement = isNewStyle ? 'bottom-start' : 'bottom-end'}
|
||||
<Popover let:toggle {placement} padding="none">
|
||||
{@render children(toggle, selectedColumnsNumber)}
|
||||
<svelte:fragment slot="tooltip">
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<div bind:this={containerRef} class="actions-menu-wrapper" style:max-height={maxHeight}>
|
||||
<ActionMenu.Root>
|
||||
{#if isNewStyle && showActions}
|
||||
@@ -231,7 +237,8 @@
|
||||
on:click={() => toggleColumn(column)}
|
||||
disabled={allowNoColumns
|
||||
? false
|
||||
: visibleRealColumns.length <= 1 && !column.hide}>
|
||||
: (visibleRealColumns.length <= 1 && !column.hide) ||
|
||||
column.disable}>
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
@@ -243,6 +250,26 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if onCustomOptionClick && isCustomTable}
|
||||
<Divider />
|
||||
<Layout.Stack gap="s" direction="row" style="padding-block-start: 0.175rem">
|
||||
<Button
|
||||
text
|
||||
size="s"
|
||||
fullWidth
|
||||
on:click={() => {
|
||||
toggle();
|
||||
onCustomOptionClick();
|
||||
}}>
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
|
||||
Custom
|
||||
</Layout.Stack>
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
allowNoColumns?: boolean;
|
||||
showAnyway?: boolean;
|
||||
disableButton?: boolean;
|
||||
onCustomOptionClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -34,7 +35,8 @@
|
||||
hideColumns = false,
|
||||
allowNoColumns = false,
|
||||
showAnyway = false,
|
||||
disableButton = false
|
||||
disableButton = false,
|
||||
onCustomOptionClick = null
|
||||
}: Props = $props();
|
||||
|
||||
let showCountBadge = $state(false);
|
||||
@@ -65,6 +67,7 @@
|
||||
{showAnyway}
|
||||
{isCustomTable}
|
||||
{allowNoColumns}
|
||||
{onCustomOptionClick}
|
||||
onPreferencesUpdated={updateBadgeState}>
|
||||
{#snippet children(toggle, selectedColumnsNumber)}
|
||||
<Button.Button
|
||||
|
||||
@@ -47,6 +47,7 @@ export type Column = PinkColumn & {
|
||||
array?: boolean;
|
||||
format?: string;
|
||||
exclude?: boolean;
|
||||
disable?: boolean;
|
||||
elements?: string[] | { value: string | number; label: string }[];
|
||||
encrypt?: boolean;
|
||||
icon?: ComponentType;
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { InputTags } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { Input, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
let {
|
||||
collectionId,
|
||||
databaseType,
|
||||
inModal = false,
|
||||
onSuccess = null,
|
||||
onFailure = null
|
||||
}: {
|
||||
collectionId: string;
|
||||
databaseType: string;
|
||||
inModal?: boolean;
|
||||
onSuccess?: () => Promise<void> | void;
|
||||
onFailure?: (error: Error) => Promise<void> | void;
|
||||
} = $props();
|
||||
|
||||
let names = $state<string[]>(getDisplayNames());
|
||||
|
||||
const isDisabled = $derived(
|
||||
!symmetricDifference(names, getDisplayNames()).length || names.length > 5
|
||||
);
|
||||
|
||||
function getDisplayNames() {
|
||||
const displayNames = preferences.getDisplayNames(collectionId, databaseType) ?? [];
|
||||
return displayNames.filter((name) => !name.startsWith('$'));
|
||||
}
|
||||
|
||||
export function hasChanged() {
|
||||
return isDisabled;
|
||||
}
|
||||
|
||||
export async function updateDisplayNames() {
|
||||
try {
|
||||
const regularArray = [...names];
|
||||
|
||||
await preferences.setDisplayNames(
|
||||
$organization.$id,
|
||||
collectionId,
|
||||
regularArray,
|
||||
databaseType
|
||||
);
|
||||
|
||||
await onSuccess?.();
|
||||
|
||||
// reset with new values!
|
||||
names = getDisplayNames();
|
||||
} catch (error) {
|
||||
await onFailure?.(error);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
names = getDisplayNames();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
<Layout.Stack gap="s">
|
||||
{#key names.length}
|
||||
<InputTags
|
||||
bind:tags={names}
|
||||
id="custom-columns-{collectionId}"
|
||||
placeholder="Enter document keys"
|
||||
label={inModal ? null : 'Fields to display'} />
|
||||
{/key}
|
||||
|
||||
<Input.Helper state="default">
|
||||
ID, createdAt, and updatedAt are always included and cannot be modified
|
||||
</Input.Helper>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
+98
-30
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { hasPageQueries, queries } from '$lib/components/filters';
|
||||
import ViewSelector from '$lib/components/viewSelector.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import { page } from '$app/state';
|
||||
@@ -22,15 +23,22 @@
|
||||
import { EmptySheet, EmptySheetCards } from '$database/(entity)';
|
||||
import {
|
||||
isCollectionsCsvImportInProgress,
|
||||
noSqlDocument
|
||||
noSqlDocument,
|
||||
collectionColumns
|
||||
} from '$database/collection-[collection]/store';
|
||||
import { canWriteRows } from '$lib/stores/roles';
|
||||
import SpreadSheet from '$database/collection-[collection]/spreadsheet.svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import CustomColumnsEditor from '$database/collection-[collection]/(components)/customColumnsEditor.svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
|
||||
const { data }: PageProps = $props();
|
||||
|
||||
let showImportCSV = $state(false);
|
||||
let showCustomColumnsModal = $state(false);
|
||||
|
||||
let columnsError: string = $state(null);
|
||||
let customColumnEditor: CustomColumnsEditor | null = $state(null);
|
||||
|
||||
function buildInitDoc() {
|
||||
const now = new Date().toISOString();
|
||||
@@ -76,38 +84,67 @@
|
||||
<Container expanded expandHeightButton style="background: var(--bgcolor-neutral-primary)">
|
||||
<Layout.Stack direction="column" gap="xl">
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<Button
|
||||
secondary
|
||||
event={Click.DatabaseImportCsv}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
event="create_document"
|
||||
on:click={() => {
|
||||
if (!$noSqlDocument.isNew) {
|
||||
noSqlDocument.create(buildInitDoc());
|
||||
}
|
||||
}}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create document
|
||||
</Button>
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Tooltip>
|
||||
<div>
|
||||
<ViewSelector
|
||||
onlyIcon
|
||||
ui="new"
|
||||
hideView
|
||||
showAnyway
|
||||
isCustomTable
|
||||
view={data.view}
|
||||
columns={collectionColumns}
|
||||
onCustomOptionClick={() => (showCustomColumnsModal = true)} />
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="tooltip">Columns</svelte:fragment>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
style="padding-right: 40px;">
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
on:click={() => {
|
||||
$expandTabs = !$expandTabs;
|
||||
preferences.setKey('entityHeaderExpanded', $expandTabs);
|
||||
}}>
|
||||
<Icon icon={!$expandTabs ? IconChevronDown : IconChevronUp} size="s" />
|
||||
event={Click.DatabaseImportCsv}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
event="create_document"
|
||||
on:click={() => {
|
||||
if (!$noSqlDocument.isNew) {
|
||||
noSqlDocument.create(buildInitDoc());
|
||||
}
|
||||
}}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create document
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
on:click={() => {
|
||||
$expandTabs = !$expandTabs;
|
||||
preferences.setKey('entityHeaderExpanded', $expandTabs);
|
||||
}}>
|
||||
<Icon
|
||||
icon={!$expandTabs ? IconChevronDown : IconChevronUp}
|
||||
size="s" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
{#if $isSmallViewport}
|
||||
@@ -189,6 +226,37 @@
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
title="Custom columns"
|
||||
bind:error={columnsError}
|
||||
bind:show={showCustomColumnsModal}
|
||||
onSubmit={async () => {
|
||||
await customColumnEditor?.updateDisplayNames();
|
||||
}}>
|
||||
<svelte:fragment slot="description">
|
||||
Add up to 5 document fields to display as columns in the table view for easy identification.
|
||||
</svelte:fragment>
|
||||
|
||||
<CustomColumnsEditor
|
||||
inModal
|
||||
bind:this={customColumnEditor}
|
||||
databaseType={data.database.type}
|
||||
collectionId={page.params.collection}
|
||||
onSuccess={() => {
|
||||
columnsError = null;
|
||||
showCustomColumnsModal = false;
|
||||
}}
|
||||
onFailure={(error) => {
|
||||
columnsError = error.message;
|
||||
}} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button size="s" secondary on:click={() => (showCustomColumnsModal = false)}>Cancel</Button>
|
||||
|
||||
<Button size="s" submit disabled={customColumnEditor?.hasChanged()}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
:global(.small-button-dimensions) {
|
||||
width: 32px !important;
|
||||
|
||||
+27
-61
@@ -3,84 +3,50 @@
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputTags } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { page } from '$app/state';
|
||||
import { Input, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { getTerminologies } from '$database/(entity)';
|
||||
import CustomColumnsEditor from '../(components)/customColumnsEditor.svelte';
|
||||
|
||||
const collectionId = page.params.collection;
|
||||
const { terminology } = getTerminologies();
|
||||
|
||||
let names = $state<string[]>(
|
||||
(preferences.getDisplayNames(collectionId, terminology.type) ?? []).filter(
|
||||
(name) => !name.startsWith('$')
|
||||
)
|
||||
);
|
||||
|
||||
async function updateDisplayName() {
|
||||
try {
|
||||
// $state makes proxy,
|
||||
// structuredClone doesn't work
|
||||
const regularArray = [...names];
|
||||
|
||||
await preferences.setDisplayNames(
|
||||
$organization.$id,
|
||||
collectionId,
|
||||
regularArray,
|
||||
terminology.type
|
||||
);
|
||||
|
||||
await invalidate(Dependencies.TEAM);
|
||||
addNotification({
|
||||
message: 'Display names have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.TableUpdateDisplayNames);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.TableUpdateDisplayNames);
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = $derived(
|
||||
!symmetricDifference(
|
||||
names,
|
||||
(preferences.getDisplayNames(collectionId, terminology.type) ?? []).filter(
|
||||
(name) => !name.startsWith('$')
|
||||
)
|
||||
).length || names.length > 5
|
||||
);
|
||||
let customColumnsEditor: CustomColumnsEditor | null = $state(null);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateDisplayName}>
|
||||
<Form
|
||||
onSubmit={async () => {
|
||||
await customColumnsEditor?.updateDisplayNames();
|
||||
}}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Display name</svelte:fragment>
|
||||
Add up to 5 document fields to display as columns in the collection view.
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<Layout.Stack gap="s">
|
||||
{#key names.length}
|
||||
<InputTags
|
||||
id="display-names"
|
||||
label="Document keys"
|
||||
placeholder="Enter document keys"
|
||||
bind:tags={names} />
|
||||
{/key}
|
||||
<Input.Helper state="default">
|
||||
ID, createdAt, and updatedAt are always included and cannot be modified
|
||||
</Input.Helper>
|
||||
</Layout.Stack>
|
||||
<CustomColumnsEditor
|
||||
{collectionId}
|
||||
databaseType={terminology.type}
|
||||
bind:this={customColumnsEditor}
|
||||
onSuccess={async () => {
|
||||
await invalidate(Dependencies.TEAM);
|
||||
addNotification({
|
||||
message: 'Display names have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.CollectionUpdateDisplayNames);
|
||||
}}
|
||||
onFailure={(error) => {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.CollectionUpdateDisplayNames);
|
||||
}} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={isDisabled} submit>Update</Button>
|
||||
<Button disabled={customColumnsEditor?.hasChanged()} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
+7
-4
@@ -171,7 +171,7 @@
|
||||
type: 'dynamic',
|
||||
icon: IconCode /* fuzzy search based Icon later */,
|
||||
isEditable: false,
|
||||
hide: false
|
||||
hide: !!selectedColumnsToHide?.includes(key)
|
||||
}));
|
||||
|
||||
const staticColumns: Column[] = [
|
||||
@@ -185,7 +185,8 @@
|
||||
icon: IconFingerPrint,
|
||||
isEditable: false,
|
||||
isPrimary: false,
|
||||
hide: !!selectedColumnsToHide?.includes('$id')
|
||||
hide: false,
|
||||
disable: true
|
||||
},
|
||||
...customColumns,
|
||||
{
|
||||
@@ -197,7 +198,8 @@
|
||||
type: 'datetime',
|
||||
icon: IconCalendar,
|
||||
isEditable: false,
|
||||
hide: !!selectedColumnsToHide?.includes('$createdAt')
|
||||
hide: false,
|
||||
disable: true
|
||||
},
|
||||
{
|
||||
id: '$updatedAt',
|
||||
@@ -208,7 +210,8 @@
|
||||
type: 'datetime',
|
||||
icon: IconCalendar,
|
||||
isEditable: false,
|
||||
hide: !!selectedColumnsToHide?.includes('$updatedAt')
|
||||
hide: false,
|
||||
disable: true
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
Reference in New Issue
Block a user