mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #877 from appwrite/feat-file-picker
feat: file picker
This commit is contained in:
Generated
+9
-9
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
<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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+28
-8
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user