feat: basic export data modal functionality

This commit is contained in:
tglide
2023-07-17 12:23:16 +01:00
parent d02fd1c8bd
commit 09a9be3e3d
12 changed files with 192 additions and 68 deletions
+2
View File
@@ -13,3 +13,5 @@ node_modules
node_modules/
dist/
.vercel
*.swp
+1 -1
View File
@@ -80,7 +80,7 @@
<div class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
<span>{percentage}%</span>
</div>
<div class="progress-bar-container" style="--graph-size:50%" />
<div class="progress-bar-container" style="--graph-size:{percentage}%" />
<span>Importing users...</span>
</section>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@
export let error: string = null;
export let closable = true;
export let headerDivider = true;
export let onSubmit: () => Promise<void> | void = function () {
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
};
+3 -3
View File
@@ -13,15 +13,15 @@
export let noMargin = false;
export let noStyle = false;
export let isModal = false;
export let onSubmit: () => Promise<void> | void;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void;
const { isSubmitting } = setContext<FormContext>('form', {
isSubmitting: writable(false)
});
async function submit() {
async function submit(e: SubmitEvent) {
isSubmitting.set(true);
await onSubmit();
await onSubmit(e);
isSubmitting.set(false);
}
</script>
+14 -3
View File
@@ -8,6 +8,7 @@
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
@@ -29,6 +30,7 @@
});
const handleInvalid = (event: Event) => {
console.log('Invalid');
event.preventDefault();
if (element.validity.valueMissing) {
@@ -39,8 +41,11 @@
error = element.validationMessage;
};
$: if (value) {
error = null;
$: {
value;
if (element?.validity?.valid) {
error = null;
}
}
let prevValue = '';
@@ -56,6 +61,10 @@
$: showTextCounter = !!maxlength;
$: showNullCheckbox = nullable && !required;
type $$Events = {
input: Event & { target: HTMLInputElement };
};
</script>
<FormItem>
@@ -66,6 +75,7 @@
<div class="input-text-wrapper">
<input
{id}
{name}
{placeholder}
{disabled}
{readonly}
@@ -77,7 +87,8 @@
bind:value
class:u-padding-inline-end-56={typeof maxlength === 'number'}
bind:this={element}
on:invalid={handleInvalid} />
on:invalid={handleInvalid}
on:input />
{#if showTextCounter || showNullCheckbox}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
@@ -7,6 +7,7 @@
export let label: string;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let placeholder = '';
export let required = false;
@@ -63,6 +64,7 @@
<div class="input-text-wrapper">
<textarea
{id}
{name}
{placeholder}
{disabled}
{readonly}
+14
View File
@@ -0,0 +1,14 @@
export function getFormData<T extends Record<string, unknown> = Record<string, unknown>>(
e: SubmitEvent
): T {
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
// TODO: Add validation? Maybe with zod
return data as T;
}
+1 -1
View File
@@ -14,7 +14,7 @@ export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
if (url.searchParams.has('migrate')) {
requestedMigration.set(true);
requestedMigration.set(url.searchParams.get('migrate'));
}
try {
@@ -1,10 +1,8 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Arrow, CardGrid, Heading } from '$lib/components';
import Alert from '$lib/components/alert.svelte';
import Modal from '$lib/components/modal.svelte';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import InputText from '$lib/elements/forms/inputText.svelte';
import InputTextarea from '$lib/elements/forms/inputTextarea.svelte';
import {
TableBody,
TableCell,
@@ -14,20 +12,20 @@
} from '$lib/elements/table';
import Table from '$lib/elements/table/table.svelte';
import { isSameDay, toLocaleDate } from '$lib/helpers/date';
import { parseIfString } from '$lib/helpers/object';
import { capitalize } from '$lib/helpers/string';
import { Container } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { isSelfHosted } from '$lib/system';
import { onMount } from 'svelte';
import { openImportWizard } from './(import)';
import Details from './details.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { parseIfString } from '$lib/helpers/object';
import ExportModal from './exportModal.svelte';
import { registerCommands } from '$lib/commandCenter';
export let data;
let details: (typeof data.migrations)[number] | null = null;
let showCloudExport = false;
let showExport = false;
const getStatus = (status: string) => {
if (status === 'failed') {
@@ -46,6 +44,21 @@
}
});
});
$: $registerCommands([
{
label: 'Import data',
icon: 'upload',
keys: ['i', 'd'],
callback: openImportWizard
},
{
label: 'Export data',
icon: 'download',
keys: ['e', 'd'],
callback: () => (showExport = true)
}
]);
</script>
<Container>
@@ -173,52 +186,14 @@
<Button
class="u-margin-block-start-48"
secondary
on:click={() => (showCloudExport = true)}>Export data</Button>
on:click={() => (showExport = true)}>Export data</Button>
</div>
</svelte:fragment>
</CardGrid>
{/if}
</Container>
<Modal bind:show={showCloudExport}>
<svelte:fragment slot="header">Export to self-hosted instance</svelte:fragment>
<div class="modal-contents">
<Alert standalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background,
which you can delete after completion
</Alert>
<div class="u-margin-block-start-24">
<InputText
label="Endpoint self-hosted instance"
required
id="endpoint"
placeholder="https://[YOUR_APPWRITE_HOSTNAME]" />
</div>
<div class="box u-margin-block-start-24">
<p class="u-bold">
Share your feedback: why our self-hosted solution works better for you
</p>
<p class="u-margin-block-start-8">
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why
it works better for you. Your feedback is important to us and we'll use it to make
our services better.
</p>
<div class="u-margin-block-start-24">
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</div>
</div>
</div>
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
<span> You will be redirected to your self-hosted instance </span>
<Button on:click={() => (showCloudExport = false)}>Continue TODO: SUBMIT</Button>
</div>
</Modal>
<ExportModal bind:show={showExport} />
<Details bind:details />
@@ -228,15 +203,10 @@
place-items: center;
border: 1px dashed hsl(var(--color-border));
/* border-color: red; */
border-radius: 1rem;
height: 100%;
padding: 1.5rem;
}
.modal-contents :global(.alert) {
grid-area: initial !important;
}
</style>
@@ -4,9 +4,15 @@ import { sdk } from '$lib/stores/sdk';
export async function load({ depends }) {
depends(Dependencies.MIGRATIONS);
const { migrations } = await sdk.forProject.migrations.list();
try {
const { migrations } = await sdk.forProject.migrations.list();
return {
migrations
};
return {
migrations
};
} catch {
return {
migrations: []
};
}
}
@@ -0,0 +1,119 @@
<script lang="ts">
import { Alert, Modal } from '$lib/components';
import { Button, InputText, InputTextarea } from '$lib/elements/forms';
import { getFormData } from '$lib/helpers/form';
export let show = false;
let submitted = false;
const isValidEndpoint = (endpoint: string) => {
try {
// Endpoint should be a valid URL. It may not have a path, nor a query string
const url = new URL(endpoint);
return (
url.protocol &&
url.hostname &&
(url.pathname === '/' || !url.pathname) &&
!url.search
);
} catch {
return false;
}
};
const getCurrentEndpoint = () => {
// Remove subpaths and query strings from the current URL. Add a /v1 suffix
const url = new URL(window.location.href);
url.pathname = '';
url.search = '';
url.hash = '';
url.pathname = '/v1';
return url.toString();
};
const removeTrailingSlash = (endpoint: string) => {
if (endpoint.endsWith('/')) {
return endpoint.slice(0, -1);
}
return endpoint;
};
const onSubmit = (e: SubmitEvent) => {
e.preventDefault();
submitted = true;
const formData = getFormData<{ endpoint: string; feedback: string }>(e);
// TODO: Send feedback somewhere
const { endpoint, feedback: _feedback } = formData;
if (!isValidEndpoint(endpoint)) {
const endpointInput = document.getElementById('endpoint') as HTMLInputElement;
endpointInput.setCustomValidity('Please enter a valid endpoint');
endpointInput.reportValidity();
return;
}
const currEndpoint = getCurrentEndpoint();
// URI encode the current endpoint, so that it can be passed as a query string
const encodedCurrEndpoint = encodeURIComponent(currEndpoint);
const dest = `${removeTrailingSlash(endpoint)}/?migrate=${encodedCurrEndpoint}`;
window.location.href = dest;
};
</script>
<Modal bind:show {onSubmit}>
<svelte:fragment slot="header">Export to self-hosted instance</svelte:fragment>
<div class="modal-contents">
<Alert standalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background,
which you can delete after completion
</Alert>
<div class="u-margin-block-start-24">
<InputText
label="Endpoint self-hosted instance"
required
id="endpoint"
placeholder="https://[YOUR_APPWRITE_HOSTNAME]"
autofocus
value="http://localhost:3000/"
on:input={(e) => {
if (!submitted) return;
const input = e.target;
const value = input.value;
if (!isValidEndpoint(value)) {
input.setCustomValidity('Please enter a valid endpoint');
} else {
input.setCustomValidity('');
}
input.reportValidity();
}} />
</div>
<div class="box u-margin-block-start-24">
<p class="u-bold">
Share your feedback: why our self-hosted solution works better for you
</p>
<p class="u-margin-block-start-8">
We appreciate your continued support and we understand that our self-hosted solution
might better fit your needs. To help us improve our Cloud solution, please share why
it works better for you. Your feedback is important to us and we'll use it to make
our services better.
</p>
<div class="u-margin-block-start-24">
<InputTextarea id="feedback" label="Your feedback" placeholder="Type here..." />
</div>
</div>
</div>
<div class="u-flex u-gap-16 u-cross-center" slot="footer">
<span> You will be redirected to your self-hosted instance </span>
<Button submit>Continue</Button>
</div>
</Modal>
+1 -1
View File
@@ -7,4 +7,4 @@ export const loading = writable(true);
export const organizations = derived(page, ($page) => {
return $page.data.organizations as Models.TeamList<Models.Preferences>;
});
export const requestedMigration = writable(false);
export const requestedMigration = writable<string | null>(null);