mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
348 lines
12 KiB
Svelte
348 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { Button } from '$lib/elements/forms';
|
|
import { Modal } from '$lib/components';
|
|
import { Pill } from '$lib/elements';
|
|
import { createEventDispatcher } from 'svelte';
|
|
import { at, empty } from '$lib/helpers/array';
|
|
import { singular } from '$lib/helpers/string';
|
|
import {
|
|
eventServices as services,
|
|
type EventAction,
|
|
type EventResource,
|
|
type EventService
|
|
} from '$lib/constants';
|
|
import { Tooltip, Input, Tag, Layout, Typography } from '@appwrite.io/pink-svelte';
|
|
import { trackEvent } from '$lib/actions/analytics';
|
|
import { IconCheck, IconDuplicate, IconPencil, IconX } from '@appwrite.io/pink-icons-svelte';
|
|
|
|
// Props
|
|
export let show = false;
|
|
export let initialValue = '';
|
|
|
|
// Constants
|
|
const serviceNames = services.map((s) => s.name);
|
|
const resourceNames = services.flatMap((s) => s.resources.map((r) => r.name));
|
|
const actionNames = [
|
|
...services.flatMap((s) => s.actions.map((a) => a.name)),
|
|
...services.flatMap((s) => s.resources.flatMap((r) => r.actions.map((a) => a.name)))
|
|
];
|
|
const attributeNames = [
|
|
...services.flatMap((s) => s.actions.flatMap((a) => a.attributes ?? [])),
|
|
...services.flatMap((s) =>
|
|
s.resources.flatMap((r) => r.actions.flatMap((a) => a.attributes ?? []))
|
|
)
|
|
];
|
|
|
|
// Helpers
|
|
const isService = (s: string) => serviceNames.includes(s);
|
|
const isResource = (s: string) => resourceNames.includes(s);
|
|
const isAction = (s: string) => actionNames.includes(s);
|
|
const isAttribute = (s: string) => attributeNames.includes(s);
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
function create() {
|
|
show = false;
|
|
trackEvent('events_add', {
|
|
value: inputValue
|
|
});
|
|
dispatch('created', inputValue);
|
|
}
|
|
|
|
function select(field: 'service', value: EventService): void;
|
|
function select(field: 'resource', value: EventResource): void;
|
|
function select(field: 'action', value: EventAction): void;
|
|
function select(field: 'attribute', value: string): void;
|
|
function select(
|
|
field: string,
|
|
value: string | EventService | EventResource | EventAction
|
|
): void {
|
|
if (typeof value === 'string') {
|
|
selected[field as 'attribute'] = selected[field] === value ? null : value;
|
|
} else {
|
|
selected[field] = selected[field]?.name === value.name ? null : value;
|
|
}
|
|
|
|
customInput = null;
|
|
|
|
switch (field) {
|
|
case 'service': {
|
|
selected.resource = null;
|
|
selected.action = null;
|
|
selected.attribute = null;
|
|
break;
|
|
}
|
|
case 'resource': {
|
|
const availableActions = selected.resource
|
|
? selected.resource?.actions
|
|
: selected.service?.actions;
|
|
|
|
// We need to check if the selected action name is still
|
|
// on the available list, and if so, change the action to the
|
|
// appropriate one, as it may contain different attributes
|
|
const availableAction = availableActions.find(
|
|
(a) => a.name === selected.action?.name
|
|
);
|
|
|
|
if (!availableAction) {
|
|
selected.action = null;
|
|
selected.attribute = null;
|
|
} else {
|
|
selected.action = availableAction;
|
|
|
|
if (!selected?.action?.attributes?.includes(selected.attribute)) {
|
|
selected.attribute = null;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'action': {
|
|
selected.attribute = null;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetSelected() {
|
|
Object.keys(selected).forEach((key) => (selected[key] = null));
|
|
}
|
|
|
|
function inferSelectedFromCustomInput() {
|
|
resetSelected();
|
|
for (const field of customInput.split('.')) {
|
|
if (isService(field)) {
|
|
selected.service = services.find((s) => s.name === field);
|
|
} else if (isResource(field)) {
|
|
const resource = selected.service?.resources.find((r) => r.name === field);
|
|
selected.resource = resource;
|
|
} else if (isAction(field)) {
|
|
const action =
|
|
selected.resource?.actions.find((a) => a.name === field) ||
|
|
selected.service?.actions.find((a) => a.name === field);
|
|
selected.action = action;
|
|
} else if (isAttribute(field)) {
|
|
const attribute = selected.action?.attributes?.find((a) => a === field);
|
|
selected.attribute = attribute;
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleShowInput(e: Event) {
|
|
e.preventDefault();
|
|
|
|
showInput = !showInput;
|
|
if (showInput) return;
|
|
|
|
helper = null;
|
|
inferSelectedFromCustomInput();
|
|
}
|
|
|
|
function getHelperStr(fields: string[], index: number) {
|
|
const currField = at(fields, index);
|
|
const prevField = at(fields, index - 1);
|
|
const secondToLastField = at(fields, index - 2);
|
|
|
|
if (index === 0 || isService(currField)) return 'service';
|
|
if (isAttribute(currField) || isAction(prevField)) return 'attribute';
|
|
if (isAction(currField)) return 'action';
|
|
if (isResource(currField)) return 'resource';
|
|
if (isService(prevField) || isResource(prevField) || index === 1) {
|
|
return `ID of ${singular(prevField)}`;
|
|
}
|
|
// TODO: Find better name for 'resource or action'
|
|
if (isService(secondToLastField) || index === 2) return 'resource or action';
|
|
if (isResource(secondToLastField)) return 'action';
|
|
|
|
return '';
|
|
}
|
|
|
|
function getCustomInputHelperStr(input: string, selectionStart?: number): string {
|
|
const fields = input.split('.');
|
|
let fieldIndex = -1;
|
|
|
|
if (selectionStart > -1) {
|
|
let currentIndex = 0;
|
|
let arrayIndex = 0;
|
|
|
|
for (const item of fields) {
|
|
currentIndex += item.length + 1;
|
|
if (currentIndex > selectionStart) {
|
|
fieldIndex = arrayIndex;
|
|
break;
|
|
}
|
|
arrayIndex++;
|
|
}
|
|
}
|
|
|
|
return getHelperStr(fields, fieldIndex);
|
|
}
|
|
|
|
// State & Reactive Declarations
|
|
let selected = {
|
|
service: null as EventService | null,
|
|
resource: null as EventResource | null,
|
|
action: null as EventAction | null,
|
|
attribute: null as string | null
|
|
};
|
|
let helper: string = null;
|
|
let customInput: string = null;
|
|
let customInputCursor = -1;
|
|
let showInput = false;
|
|
|
|
$: if (show && initialValue) {
|
|
customInput = initialValue;
|
|
inferSelectedFromCustomInput();
|
|
}
|
|
|
|
$: available = {
|
|
services: services,
|
|
resources: selected.service?.resources || [],
|
|
actions: (selected.resource ? selected.resource?.actions : selected.service?.actions) || [],
|
|
attributes: selected.action?.attributes || []
|
|
};
|
|
|
|
$: eventString = (function createEventString(): Array<{ value: string; description: string }> {
|
|
let fields: string[] = [];
|
|
|
|
// Avoid calculating the event string if the user is typing, as it is not shown
|
|
if (showInput) return [];
|
|
if (customInput) {
|
|
fields = customInput.split('.');
|
|
} else {
|
|
if (selected.service) {
|
|
fields.push(selected.service.name, '*');
|
|
}
|
|
if (selected.resource?.name === 'documents') {
|
|
fields.push('collections', '*');
|
|
}
|
|
if (selected.resource) {
|
|
fields.push(selected.resource.name, '*');
|
|
}
|
|
if (selected.action) {
|
|
fields.push(selected.action.name);
|
|
}
|
|
if (selected.attribute) {
|
|
fields.push(selected.attribute);
|
|
}
|
|
}
|
|
|
|
return fields.map((value, i, arr) => ({ value, description: getHelperStr(arr, i) }));
|
|
})();
|
|
|
|
$: inputValue = eventString.map((d) => d.value).join('.');
|
|
|
|
$: if (!show) {
|
|
resetSelected();
|
|
customInput = null;
|
|
showInput = false;
|
|
}
|
|
|
|
$: if (showInput) {
|
|
helper = getCustomInputHelperStr(customInput, customInputCursor);
|
|
}
|
|
</script>
|
|
|
|
<Modal title="Create event" bind:show onSubmit={create}>
|
|
<slot />
|
|
<Layout.Stack gap="s">
|
|
<Typography.Text variant="m-500">Choose a service</Typography.Text>
|
|
<Layout.Stack gap="s" wrap="wrap" direction="row">
|
|
{#each available.services as service}
|
|
<Tag
|
|
disabled={showInput}
|
|
selected={selected.service?.name === service.name}
|
|
on:click={() => select('service', service)}>
|
|
{service.name}
|
|
</Tag>
|
|
{/each}
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
|
|
{#if !empty(available.resources)}
|
|
<Layout.Stack gap="s">
|
|
<Typography.Text variant="m-500">Choose a resource (optional)</Typography.Text>
|
|
<Layout.Stack gap="s" wrap="wrap" direction="row">
|
|
{#each available.resources as resource}
|
|
<Tag
|
|
disabled={showInput}
|
|
selected={selected.resource?.name === resource.name}
|
|
on:click={() => select('resource', resource)}>
|
|
{resource.name}
|
|
</Tag>
|
|
{/each}
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
{/if}
|
|
|
|
{#if !empty(available.actions)}
|
|
<Layout.Stack gap="s">
|
|
<Typography.Text variant="m-500">Choose an action (optional)</Typography.Text>
|
|
<Layout.Stack gap="s" wrap="wrap" direction="row">
|
|
{#each available.actions as action}
|
|
<Tag
|
|
disabled={showInput}
|
|
selected={selected.action?.name === action.name}
|
|
on:click={() => select('action', action)}>
|
|
{action.name}
|
|
</Tag>
|
|
{/each}
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
{/if}
|
|
|
|
{#if !empty(available.attributes)}
|
|
<Layout.Stack gap="s">
|
|
<Typography.Text variant="m-500">Choose an attribute (optional)</Typography.Text>
|
|
<Layout.Stack gap="s" wrap="wrap" direction="row">
|
|
{#each available.attributes as attribute}
|
|
<Pill
|
|
disabled={showInput}
|
|
selected={selected.attribute === attribute}
|
|
button
|
|
on:click={() => select('attribute', attribute)}>
|
|
{attribute}
|
|
</Pill>
|
|
{/each}
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
{/if}
|
|
|
|
{#if showInput}
|
|
<Layout.Stack gap="s">
|
|
<Input.Text bind:value={customInput} placeholder="Enter custom event">
|
|
<svelte:fragment slot="end">
|
|
<Input.Action icon={IconCheck} on:click={toggleShowInput} />
|
|
<Input.Action icon={IconX} />
|
|
</svelte:fragment>
|
|
</Input.Text>
|
|
|
|
<!-- TODO: get checked by design -->
|
|
<Typography.Caption variant="400">{helper ?? ''}</Typography.Caption>
|
|
</Layout.Stack>
|
|
{:else}
|
|
<Input.Text
|
|
value={eventString.map((e) => e.value).join('.')}
|
|
placeholder="Enter custom event"
|
|
readonly>
|
|
<svelte:fragment slot="end">
|
|
<Tooltip>
|
|
<Input.Action
|
|
icon={IconPencil}
|
|
on:click={(e) => {
|
|
customInput = inputValue;
|
|
toggleShowInput(e);
|
|
}} />
|
|
<div slot="tooltip">Edit event</div>
|
|
</Tooltip>
|
|
<Input.Action icon={IconDuplicate} />
|
|
</svelte:fragment>
|
|
</Input.Text>
|
|
{/if}
|
|
|
|
<svelte:fragment slot="footer">
|
|
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
|
<Button disabled={showInput || !inputValue} submit
|
|
>{initialValue ? 'Update' : 'Create'}</Button>
|
|
</svelte:fragment>
|
|
</Modal>
|