Merge pull request #877 from appwrite/feat-file-picker

feat: file picker
This commit is contained in:
Torsten Dittmann
2024-03-08 00:40:21 +01:00
committed by GitHub
16 changed files with 732 additions and 28 deletions
+9 -9
View File
@@ -7,8 +7,8 @@
"name": "@appwrite/console",
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.17",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@appwrite.io/pink": "0.5.0",
"@appwrite.io/pink-icons": "0.5.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
@@ -167,19 +167,19 @@
}
},
"node_modules/@appwrite.io/pink": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.4.0.tgz",
"integrity": "sha512-48g2MQIFaEXPXobJ77wJK73YRUkpUZSAkRNa3XJhtC0N2WSTXD3poXBAVYb2QfjGW4e5U1tM/WYC0ZQMtjEzlQ==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.5.0.tgz",
"integrity": "sha512-UVnek/tigxoU0FksVT77cCx0+JTKA+43HAslqJIZFnV4kKIbQnCQiX0hrj2hhHzuAaWTCSvSQftq0SbGORDb/A==",
"dependencies": {
"@appwrite.io/pink-icons": "0.4.0",
"@appwrite.io/pink-icons": "0.5.0",
"normalize.css": "^8.0.1",
"the-new-css-reset": "^1.11.2"
}
},
"node_modules/@appwrite.io/pink-icons": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.4.0.tgz",
"integrity": "sha512-o1r101GRK72nBbeVL76H7NIwucrW8+vbzs41AMBPbnK+uAGhNb9ui5Na7gi6NtnBxHYnPDECg93bZhoLYlQ6XA=="
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.5.0.tgz",
"integrity": "sha512-IHob3TsvBFZo6Ya78+MPm5bKBHzFZtoJDgmClRSOOTtr1K74mditZj4sA+5haBN/4N0L3g8QtOpR4gd2vV+T3g=="
},
"node_modules/@babel/code-frame": {
"version": "7.23.5",
+2 -2
View File
@@ -19,8 +19,8 @@
},
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.17",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@appwrite.io/pink": "0.5.0",
"@appwrite.io/pink-icons": "0.5.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
+544
View File
@@ -0,0 +1,544 @@
<script lang="ts">
import { Id, ModalWrapper, Trim } from '.';
import { Button, Form } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { ID, Query, Permission, Role } from '@appwrite.io/console';
import type { Models } from '@appwrite.io/console';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { toLocaleDate } from '$lib/helpers/date';
import {
Table,
TableBody,
TableRowButton,
TableHeader,
TableCell,
TableCellText,
TableCellHead
} from '$lib/elements/table';
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import FormList from '$lib/elements/forms/formList.svelte';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import Heading from './heading.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
import Empty from './empty.svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
export let show: boolean;
export let mimeTypeQuery: string = 'image/';
export let selectedBucket: string = null;
export let selectedFile: string = null;
export let onSelect: (e: Models.File) => void;
let search = writable('');
let searchEnabled = false;
let fileSelector: HTMLInputElement;
let uploading = false;
let view: 'grid' | 'list' = 'list';
onMount(() => {
selectedBucket = currentBucket?.$id;
});
function submitForm() {
onSelect(currentFile);
closeModal();
}
function getPreview(bucketId: string, fileId: string, size: number = 64) {
return (
sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() +
'&mode=admin'
);
}
async function uploadFile() {
try {
uploading = true;
const file = await sdk.forProject.storage.createFile(
selectedBucket,
ID.unique(),
fileSelector.files[0],
[Permission.read(Role.any())]
);
search.set($search === null ? '' : null);
selectFile(file);
} catch (e) {
console.error(e);
} finally {
uploading = false;
}
}
function selectBucket(bucket: Models.Bucket | null) {
currentBucket = bucket;
selectedBucket = bucket?.$id ?? null;
resetFile();
}
function selectFile(file: Models.File) {
currentFile = file;
selectedBucket = currentFile.bucketId;
selectedFile = currentFile.$id;
}
function resetFile() {
selectedFile = null;
}
function resetBucket() {
selectedBucket = null;
}
function closeModal() {
show = false;
resetFile();
resetBucket();
}
function handleVisibilityChange() {
if (!document.hidden) {
buckets = loadBuckets();
}
}
let currentBucket: Models.Bucket = null;
let currentFile: Models.File = null;
let buckets: Promise<Models.BucketList> = loadBuckets();
async function loadBuckets() {
const response = await sdk.forProject.storage.listBuckets();
const bucket = response.buckets[0] ?? null;
if (bucket) {
currentBucket = bucket;
selectedBucket = bucket.$id;
}
return response;
}
$: files =
currentBucket &&
sdk.forProject.storage
.listFiles(
currentBucket.$id,
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
$search || undefined
)
.then((response) => {
if ($search === '') {
searchEnabled = response.total > 0;
}
return response;
});
$: if ($search) {
resetFile();
}
</script>
<svelte:document on:visibilitychange={handleVisibilityChange} />
<ModalWrapper bind:show size="huge">
<Form isModal onSubmit={submitForm} class="u-stretch">
<header class="modal-header u-margin-block-end-0">
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
<h4 class="modal-title heading-level-5">Select file</h4>
<button
type="button"
on:click={closeModal}
class="button is-text is-small is-only-icon"
aria-label="Close modal">
<span class="icon-x" aria-hidden="true"></span>
</button>
</div>
</header>
<div
class="modal-content u-stretch u-flex-vertical u-padding-0 u-margin-block-0 u-overflow-visible">
<div class="u-flex u-min-height-0 u-stretch">
<aside
class="drop-section u-width-200 u-padding-16
u-flex-vertical u-gap-8
u-flex-shrink-0 u-margin-inline-0 u-overflow-y-auto is-not-mobile">
<h6
class="eyebrow-heading-3"
style:--heading-text-color="var(--color-neutral-50)">
Buckets
</h6>
<ul class="drop-list">
{#await buckets}
<div class="u-flex u-main-center">
<div class="loader" />
</div>
{:then response}
{#each response.buckets as bucket}
{@const isSelected = bucket.$id === selectedBucket}
<li class="drop-list-item">
<button
type="button"
class="drop-button"
class:is-selected={isSelected}
on:click={() => selectBucket(bucket)}>
<span>{bucket.name}</span>
</button>
</li>
{:else}
<li class="drop-list-item">
<span class="drop-button">No buckets found</span>
</li>
{/each}
{/await}
</ul>
</aside>
<article
style:padding-inline="calc(var(--p-modal-padding))"
class="modal-content-main u-flex-vertical u-gap-24 u-sep-inline-start u-flex-basis-1000 u-padding-block-24 u-overflow-y-auto">
<div class="is-only-mobile">
{#await buckets}
loading
{:then response}
{#if currentBucket?.$id}
<FormList>
<InputSelect
wrapperTag="div"
label="Buckets"
options={response.buckets.map((n) => ({
value: n.$id,
label: n.name
}))}
bind:value={currentBucket.$id}
id="buckets" />
</FormList>
{/if}
{/await}
</div>
{#await buckets}
<div class="u-flex-vertical u-stretch u-position-relative u-main-center">
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading files...</p>
</div>
</div>
{:then response}
{#if response?.total}
{#if currentBucket}
<header class="u-flex-vertical u-gap-32">
<div class="u-flex u-gap-16">
<h5 class="heading-level-6 u-trim u-min-width-0">
{currentBucket?.name}
</h5>
<Id value={currentBucket?.$id} event="bucket">
{currentBucket?.$id}
</Id>
</div>
<div
class="u-flex u-main-space-between u-gap-16 u-flex-vertical-mobile">
<InputSearch
placeholder="Search files"
bind:value={$search}
disabled={!searchEnabled}
style="min-inline-size: 17.5rem; block-size: 100%" />
<div class="u-flex u-gap-16">
<div class="toggle-button">
<ul class="toggle-button-list">
<li class="toggle-button-item">
<button
on:click={() => (view = 'list')}
disabled={!searchEnabled}
type="button"
class="toggle-button-element"
class:is-selected={view === 'list'}
aria-label="List View">
<span
class="icon-view-list"
aria-hidden="true" />
</button>
</li>
<li class="toggle-button-item">
<button
on:click={() => (view = 'grid')}
disabled={!searchEnabled}
type="button"
class="toggle-button-element"
class:is-selected={view === 'grid'}
aria-label="Grid View">
<span
class="icon-view-grid"
aria-hidden="true" />
</button>
</li>
</ul>
</div>
<Button
secondary
class="is-full-width-in-stack-mobile u-height-100-percent"
disabled={uploading}
on:click={() => fileSelector.click()}>
<input
tabindex="-1"
type="file"
accept="image/*"
class="u-hide"
on:change={uploadFile}
bind:this={fileSelector} />
{#if uploading}
<div class="loader is-small"></div>
<span>Uploading</span>
{:else}
<span class="icon-upload" aria-hidden="true"
></span>
<span>Upload</span>
{/if}
</Button>
</div>
</div>
</header>
{#if files}
{#await files}
<div
class="u-flex-vertical u-stretch u-position-relative u-main-center">
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading files...</p>
</div>
</div>
{:then response}
<div class="u-flex-vertical u-stretch">
{#if response?.files?.length}
{#if view === 'grid'}
<ul
class="grid-box"
style="--grid-gap:40px; --grid-item-size:120px; --grid-item-size-small-screens:100px;">
{#each response?.files as file}
<li>
<div
class="u-flex-vertical u-gap-8">
<div
role="button"
style:background-size="cover"
style:background-image={`url(${getPreview(
currentBucket.$id,
file.$id,
360
)})`}
on:click={() =>
selectFile(file)}
on:keyup={clickOnEnter}
tabindex="0"
style:aspect-ratio="1/1"
style:display="flex"
style:align-items="flex-end"
style:flex-direction="row-reverse"
style:box-shadow="none"
class="card u-height-100-percent u-gap-16"
style="--card-padding:0.5rem;--card-padding-mobile:0.5rem; --card-border-radius:var(--border-radius-medium);">
<input
class="u-position-absolute is-small u-margin-block-start-2"
type="radio"
name="file"
value={file.$id}
style:pointer-events="none"
checked={selectedFile ===
file.$id} />
</div>
<span class="u-text-center"
><Trim alternativeTrim
>{file.name}</Trim
></span>
</div>
</li>
{/each}
</ul>
{/if}
{#if view === 'list'}
<Table noMargin noStyles transparent dense>
<TableHeader>
<TableCellHead
><span
class="u-margin-inline-start-8"
>Filename</span
></TableCellHead>
<TableCellHead width={140} onlyDesktop>
ID
</TableCellHead>
<TableCellHead width={100} onlyDesktop>
Type
</TableCellHead>
<TableCellHead width={100} onlyDesktop>
Size
</TableCellHead>
<TableCellHead width={120} onlyDesktop>
Created
</TableCellHead>
</TableHeader>
<TableBody>
{#each response?.files as file}
<TableRowButton
on:click={() =>
selectFile(file)}>
<TableCell title="Filename">
<div
class="u-inline-flex u-cross-center u-gap-12">
<input
type="radio"
class="is-small u-margin-inline-start-8"
name="file"
value={file.$id}
style:pointer-events="none"
checked={selectedFile ===
file.$id} />
<span class="image">
<img
class="avatar"
style:border-radius="var(--border-radius-xsmall)"
width="28"
height="28"
src={getPreview(
currentBucket.$id,
file.$id
)}
alt={file.name} />
</span>
<Trim alternativeTrim>
{file.name}
</Trim>
</div>
</TableCell>
<TableCellText
title="ID"
onlyDesktop>
<Id value={file.$id}
>{file.$id}</Id>
</TableCellText>
<TableCellText
title="Type"
onlyDesktop>
{file.mimeType}
</TableCellText>
<TableCellText
title="Size"
onlyDesktop>
{calculateSize(
file.sizeOriginal
)}
</TableCellText>
<TableCellText
title="Created"
onlyDesktop>
{toLocaleDate(
file.$createdAt
)}
</TableCellText>
</TableRowButton>
{/each}
</TableBody>
</Table>
{/if}
{:else if $search}
<article
style:--card-bg-color="transparent"
style:--shadow-small="none"
style:--color-border="var(--color-neutral-15)"
class="card u-grid u-cross-center u-width-full-line common-section is-border-dashed">
<div
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
<div class="common-section">
<div
class="u-text-center common-section">
<b class="body-text-2 u-bold"
>Sorry we couldn't find "{$search}"</b>
<p>
There are no files that match
your search.
</p>
</div>
<div
class="u-flex u-gap-16 common-section u-main-center">
<Button
secondary
on:click={() => ($search = '')}
>Clear search</Button>
</div>
</div>
</div>
</article>
{:else}
<Empty
single
noMedia
--card-bg-color="transparent"
--shadow-small="none"
--color-border="var(--color-neutral-15)">
<div class="common-section">
<div class="u-text-center common-section">
<Heading
size="7"
tag="h2"
trimmed={false}>
No files found within this bucket.
</Heading>
<p class="text u-line-height-1-5">
Need a hand? Learn more in our <a
class="link"
href="https://appwrite.io/docs/products/storage"
target="_blank"
rel="noopener noreferrer">
documentation</a
>.
</p>
</div>
</div>
</Empty>
{/if}
</div>
{/await}
{/if}
{/if}
{:else}
<Empty
single
noMedia
--card-bg-color="transparent"
--shadow-small="none"
--color-border="var(--color-neutral-15)">
<div class="u-text-center u-flex-vertical u-cross-center u-gap-24">
<Heading size="7" tag="h2" trimmed={false}>
No buckets found
</Heading>
<Button
secondary
external
href={`${base}/console/project-${$page.params.project}/storage`}>
Create bucket
</Button>
</div>
</Empty>
{/if}
{/await}
</article>
</div>
</div>
<div class="modal-footer u-margin-block-start-0">
<div class="u-flex u-main-end u-gap-16">
<Button text on:click={closeModal}>Cancel</Button>
<Button submit disabled={selectedBucket === null || selectedFile === null}
>Select</Button>
</div>
</div>
</Form>
</ModalWrapper>
<style lang="scss">
input[type='radio']:where(:indeterminate) {
--p-bg-color: var(--p-bg-color-default);
--p-border-color: var(--p-border-color-default);
}
</style>
+1 -1
View File
@@ -47,7 +47,7 @@
}
</script>
<Copy {value} {event}>
<Copy {value} {event} appendTo="parent">
<div
class="interactive-text-output is-buttons-on-top"
class:u-text-center={centered}
+1 -1
View File
@@ -5,7 +5,7 @@
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
export let size: 'small' | 'big' | 'huge' = null;
export let icon: string = null;
export let state: 'success' | 'warning' | 'error' | 'info' = null;
export let error: string = null;
+17 -1
View File
@@ -4,7 +4,7 @@
import { disableCommands } from '$lib/commandCenter';
export let show = false;
export let size: 'small' | 'big' = null;
export let size: 'small' | 'big' | 'huge' = null;
export let closable = true;
export let headerDivider = true;
export let style = '';
@@ -73,6 +73,7 @@
class:u-hide={!show}
class:is-small={size === 'small'}
class:is-big={size === 'big'}
class:is-huge={size === 'huge'}
class:is-separate-header={headerDivider}
bind:this={dialog}
on:cancel|preventDefault
@@ -81,3 +82,18 @@
<slot close={closeModal} />
{/if}
</dialog>
<style lang="scss">
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
.modal.is-huge {
block-size: 100%;
min-block-size: 80vh;
@media #{$break1}, #{$break2} {
min-inline-size: 100%;
min-block-size: 100%;
border-radius: 0;
}
}
</style>
+1
View File
@@ -31,3 +31,4 @@ export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
export { default as InputTime } from './inputTime.svelte';
export { default as InputDigits } from './inputDigits.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
@@ -0,0 +1,104 @@
<script lang="ts">
import { Drop, Trim } from '$lib/components';
import FilePicker from '$lib/components/filePicker.svelte';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import type { Models } from '@appwrite.io/console';
import { Label } from '.';
export let label: string = null;
export let value: Models.File = null;
export let disabled = false;
export let optionalText: string = null;
export let tooltip: string = null;
export let isPopoverDefined = true;
let show = false;
function onSelect(file: Models.File) {
value = file;
}
</script>
<div>
{#if label}
<Label {optionalText} {tooltip} hide={!label}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
<slot name="popover" />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
<div
role="region"
class="box is-no-shadow u-padding-24 is-border-dashed"
style:--box-border-radius="var(--border-radius-xsmall)">
<div class="upload-file-box">
<div class="upload-file-box-image">
<span class="icon-upload" aria-hidden="true" />
</div>
<div class="u-min-width-0 u-text-center">
<h5 class="upload-file-box-title heading-level-7 u-inline">
<span class="is-only-desktop">Select a file to upload</span>
<span class="is-not-desktop">Select a file to upload</span>
</h5>
</div>
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
Max file size: 1MB
<button
class="button is-secondary is-full-width-mobile"
type="button"
{disabled}
on:click={() => (show = true)}>
<span class="text">Browse</span>
</button>
</div>
{#if value}
{@const fileSize = humanFileSize(value.sizeOriginal)}
<ul class="upload-file-box-list u-min-width-0">
<li class="u-flex u-cross-center u-min-width-0">
<span class="icon-document" aria-hidden="true" />
<span class="upload-file-box-name u-min-width-0">
<Trim alternativeTrim>{value.name}</Trim>
</span>
<span
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
{fileSize.value + fileSize.unit}
</span>
<button
on:click={() => (value = null)}
type="button"
class="button is-text is-only-icon u-margin-inline-start-auto"
aria-label="remove file"
style="--button-size:1.5rem;">
<span class="icon-x" aria-hidden="true" />
</button>
</li>
</ul>
{/if}
</div>
</div>
</div>
{#if show}
<FilePicker selectedFile={value?.$id} selectedBucket={value?.bucketId} bind:show {onSelect} />
{/if}
@@ -9,6 +9,7 @@
export let disabled = false;
export let autofocus = false;
export let isWithEndButton = true;
export let style: string = '';
const dispatch = createEventDispatcher();
let element: HTMLInputElement;
@@ -53,6 +54,7 @@
{placeholder}
{disabled}
{required}
{style}
type="search"
class="input-text"
bind:this={element}
+2
View File
@@ -3,12 +3,14 @@
export let noStyles = false;
export let style = '';
export let transparent = false;
export let dense = false;
</script>
<div
class="table is-selected-columns-mobile"
class:u-margin-block-start-32={!noMargin}
class:is-remove-outer-styles={noStyles}
class:is-table-row-medium-size={dense}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
role="table"
@@ -5,6 +5,7 @@
export let noMargin = false;
export let style = '';
export let transparent = false;
export let dense = false;
let isOverflowing = false;
const hasOverflow: Action<HTMLDivElement, unknown> = (node) => {
@@ -45,6 +46,7 @@
<table
class="table"
class:is-sticky-scroll={isSticky && isOverflowing}
class:is-table-row-medium-size={dense}
{style}
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}>
<slot />
@@ -13,6 +13,7 @@
import { topicsSearcher } from '$lib/commandCenter/searchers/topics';
import { project } from '../store';
// TODO: finalize the commands
$: $registerCommands([
{
label: 'Create message',
@@ -49,6 +49,8 @@
break;
case MessagingProviderType.Push:
{
const file = $messageParams[MessagingProviderType.Push].file;
const fileCompoundId = `${file.bucketId}:${file.bucketId}`;
const customData: Record<string, string> = {};
const { data } = $messageParams[MessagingProviderType.Push];
if (data && data.length > 0) {
@@ -67,7 +69,7 @@
$messageParams[$providerType].targets,
customData,
undefined,
undefined,
fileCompoundId,
undefined,
undefined,
undefined,
@@ -7,6 +7,7 @@
FormItemPart,
FormList,
Helper,
InputFilePicker,
InputText,
InputTextarea,
Label
@@ -21,18 +22,26 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { validateData } from '../wizard/pushFormList.svelte';
export let message: Models.Message & { data: Record<string, string> };
/* eslint @typescript-eslint/no-explicit-any: 'off' */
export let message: Models.Message & { data: Record<string, any> };
let title = '';
let body = '';
let file: Models.File = null;
let originalCustomData: [string, string][] = [['', '']];
let customData: [string, string][] = [['', '']];
let dataError = '';
let disabled = true;
onMount(() => {
onMount(async () => {
title = message.data.title;
body = message.data.body;
if (message.data?.image) {
file = await sdk.forProject.storage.getFile(
message.data.image?.bucketId,
message.data.image?.fileId
);
}
const dataEntries: [string, string][] = [];
Object.entries(message.data['data'] ?? {}).forEach(([key, value]) => {
@@ -44,6 +53,7 @@
async function update() {
try {
const fileCompoundId = file ? `${file.bucketId}:${file.$id}` : undefined;
const data = customData.reduce((acc, [key, value]) => {
if (key) {
acc[key] = value;
@@ -57,7 +67,9 @@
undefined,
title,
body,
data
data,
undefined,
fileCompoundId
);
originalCustomData = structuredClone(customData);
await invalidate(Dependencies.MESSAGING_MESSAGE);
@@ -79,6 +91,8 @@
$: disabled =
title === message.data.title &&
body === message.data.body &&
file?.$id === message.data.image?.fileId &&
file?.bucketId === message.data.image?.bucketId &&
originalCustomData.length === customData.length &&
originalCustomData.every(
(ocd, i) =>
@@ -99,13 +113,20 @@
<InputText
id="title"
label="Title"
disabled={message.status != 'draft'}
bind:value={title}></InputText>
disabled={message.status !== 'draft'}
bind:value={title} />
<InputTextarea
id="message"
label="Message"
disabled={message.status != 'draft'}
bind:value={body}></InputTextarea>
disabled={message.status !== 'draft'}
bind:value={body} />
<FormItem>
<InputFilePicker
disabled={message.status !== 'draft'}
bind:value={file}
label="Media"
optionalText="(Optional)" />
</FormItem>
<form class="form">
<FormItem>
<Label
@@ -125,7 +146,6 @@
placeholder="Enter key"
label="Key"
showLabel={false} />
<InputText
id={`${rowIndex}-value`}
isMultiple
@@ -45,6 +45,7 @@
import { sdk } from '$lib/stores/sdk';
import PushPhone from '../pushPhone.svelte';
import { onMount } from 'svelte';
import InputFilePicker from '$lib/elements/forms/inputFilePicker.svelte';
let showCustomId = false;
let showTest = false;
@@ -150,11 +151,19 @@
</Modal>
</div>
<form class="form">
<FormItem>
<Label
tooltip="A key/value payload of additional metadata that's hidden from users. Use this to include information to support logic such as redirection and routing."
>Custom data <span class="u-color-text-gray">(Optional)</span></Label>
</FormItem>
<FormList>
<FormItem>
<InputFilePicker
bind:value={$messageParams[MessagingProviderType.Push].file}
label="Media" />
</FormItem>
<FormItem>
<Label
tooltip="A key/value payload of additional metadata that's hidden from users. Use this to include information to support logic such as redirection and routing."
>Custom data <span class="u-color-text-gray">(Optional)</span></Label>
</FormItem>
</FormList>
<div class=" u-grid u-gap-8">
<ul class="form-list" style="--p-form-list-gap: 1rem">
{#each customData || [] as _, rowIndex}
@@ -23,6 +23,7 @@ export type SMSMessageParams = MessageParams & {
export type PushMessageParams = MessageParams & {
title: string;
body: string;
file: Models.File;
data: [string, string][];
action?: string;
icon?: string;