pocketbase/ui/src/components/settings/PageImportCollections.svelte

411 lines
15 KiB
Svelte
Raw Normal View History

2022-08-05 11:00:38 +08:00
<script>
2022-08-06 04:25:16 +08:00
import { tick } from "svelte";
2022-08-05 11:00:38 +08:00
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
2022-08-06 13:03:34 +08:00
import { addErrorToast } from "@/stores/toasts";
import { setErrors } from "@/stores/errors";
2022-08-09 21:16:09 +08:00
import PageWrapper from "@/components/base/PageWrapper.svelte";
2022-08-05 11:00:38 +08:00
import Field from "@/components/base/Field.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
2022-08-06 13:03:34 +08:00
import ImportPopup from "@/components/settings/ImportPopup.svelte";
2022-08-05 11:00:38 +08:00
$pageTitle = "Import collections";
let fileInput;
2022-08-06 13:03:34 +08:00
let importPopup;
2022-08-05 11:00:38 +08:00
let schemas = "";
2022-08-05 11:00:38 +08:00
let isLoadingFile = false;
let newCollections = [];
let oldCollections = [];
let deleteMissing = true;
let collectionsToUpdate = [];
2022-08-05 11:00:38 +08:00
let isLoadingOldCollections = false;
$: if (typeof schemas !== "undefined") {
loadNewCollections(schemas);
2022-08-05 11:00:38 +08:00
}
$: isValid =
!!schemas &&
2022-08-05 11:00:38 +08:00
newCollections.length &&
newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length;
$: collectionsToDelete = oldCollections.filter((collection) => {
return isValid && deleteMissing && !CommonHelper.findByKey(newCollections, "id", collection.id);
2022-08-05 11:00:38 +08:00
});
$: collectionsToAdd = newCollections.filter((collection) => {
2022-08-06 04:25:16 +08:00
return isValid && !CommonHelper.findByKey(oldCollections, "id", collection.id);
2022-08-05 11:00:38 +08:00
});
$: if (typeof newCollections !== "undefined" || typeof deleteMissing !== "undefined") {
loadCollectionsToUpdate();
2022-08-06 04:25:16 +08:00
}
2022-08-05 11:00:38 +08:00
2022-08-06 04:25:16 +08:00
$: hasChanges =
!!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToUpdate.length);
2022-08-06 04:25:16 +08:00
$: canImport = !isLoadingOldCollections && isValid && hasChanges;
2022-08-05 11:00:38 +08:00
2022-08-10 18:22:27 +08:00
$: idReplacableCollections = newCollections.filter((collection) => {
let old =
CommonHelper.findByKey(oldCollections, "name", collection.name) ||
CommonHelper.findByKey(oldCollections, "id", collection.id);
if (!old) {
return false; // new
2022-08-10 18:22:27 +08:00
}
if (old.id != collection.id) {
return true;
}
// check for matching schema fields
2022-08-10 18:22:27 +08:00
const oldSchema = Array.isArray(old.schema) ? old.schema : [];
const newSchema = Array.isArray(collection.schema) ? collection.schema : [];
for (const field of newSchema) {
const oldFieldById = CommonHelper.findByKey(oldSchema, "id", field.id);
if (oldFieldById) {
continue; // no need to do any replacements
}
const oldFieldByName = CommonHelper.findByKey(oldSchema, "name", field.name);
if (oldFieldByName && field.id != oldFieldByName.id) {
2022-08-10 18:22:27 +08:00
return true;
}
}
return false;
});
2022-08-05 11:00:38 +08:00
loadOldCollections();
async function loadOldCollections() {
isLoadingOldCollections = true;
try {
2022-08-06 04:25:16 +08:00
oldCollections = await ApiClient.collections.getFullList(200);
2022-08-05 11:00:38 +08:00
// delete timestamps
for (let collection of oldCollections) {
delete collection.created;
delete collection.updated;
}
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoadingOldCollections = false;
}
function loadCollectionsToUpdate() {
collectionsToUpdate = [];
2022-08-06 04:25:16 +08:00
if (!isValid) {
return;
}
for (let newCollection of newCollections) {
const oldCollection = CommonHelper.findByKey(oldCollections, "id", newCollection.id);
if (
// no old collection
!oldCollection?.id ||
// no changes
!CommonHelper.hasCollectionChanges(oldCollection, newCollection, deleteMissing)
2022-08-06 04:25:16 +08:00
) {
continue;
}
collectionsToUpdate.push({
2022-08-06 04:25:16 +08:00
new: newCollection,
old: oldCollection,
});
}
}
2022-08-05 11:00:38 +08:00
function loadNewCollections() {
newCollections = [];
try {
newCollections = JSON.parse(schemas);
2022-08-05 11:00:38 +08:00
} catch (_) {}
if (!Array.isArray(newCollections)) {
newCollections = [];
2022-08-06 04:25:16 +08:00
} else {
newCollections = CommonHelper.filterDuplicatesByKey(newCollections);
2022-08-05 11:00:38 +08:00
}
2022-08-10 18:22:27 +08:00
// normalizations
2022-08-05 11:00:38 +08:00
for (let collection of newCollections) {
2022-08-10 18:22:27 +08:00
// delete timestamps
2022-08-05 11:00:38 +08:00
delete collection.created;
delete collection.updated;
2022-08-10 18:22:27 +08:00
// merge fields with duplicated ids
collection.schema = CommonHelper.filterDuplicatesByKey(collection.schema);
2022-08-05 11:00:38 +08:00
}
}
2022-08-10 18:22:27 +08:00
function replaceIds() {
for (let collection of newCollections) {
const old =
CommonHelper.findByKey(oldCollections, "name", collection.name) ||
CommonHelper.findByKey(oldCollections, "id", collection.id);
if (!old) {
2022-08-10 18:22:27 +08:00
continue;
}
const originalId = collection.id;
const replacedId = old.id;
collection.id = replacedId;
// replace field ids
const oldSchema = Array.isArray(old.schema) ? old.schema : [];
const newSchema = Array.isArray(collection.schema) ? collection.schema : [];
for (const field of newSchema) {
const oldField = CommonHelper.findByKey(oldSchema, "name", field.name);
if (oldField && oldField.id) {
field.id = oldField.id;
}
2022-08-10 18:22:27 +08:00
}
// update references
for (let ref of newCollections) {
if (!Array.isArray(ref.schema)) {
continue;
}
for (let field of ref.schema) {
if (field.options?.collectionId && field.options?.collectionId === originalId) {
2022-08-10 18:22:27 +08:00
field.options.collectionId = replacedId;
}
}
}
}
schemas = JSON.stringify(newCollections, null, 4);
}
2022-08-05 11:00:38 +08:00
function loadFile(file) {
isLoadingFile = true;
const reader = new FileReader();
2022-08-06 04:25:16 +08:00
reader.onload = async (event) => {
2022-08-05 11:00:38 +08:00
isLoadingFile = false;
fileInput.value = ""; // reset
2022-08-06 04:25:16 +08:00
schemas = event.target.result;
2022-08-06 04:25:16 +08:00
await tick();
if (!newCollections.length) {
2022-08-07 16:14:49 +08:00
addErrorToast("Invalid collections configuration.");
2022-08-06 04:25:16 +08:00
clear();
}
2022-08-05 11:00:38 +08:00
};
reader.onerror = (err) => {
console.warn(err);
2022-08-05 11:00:38 +08:00
addErrorToast("Failed to load the imported JSON.");
isLoadingFile = false;
fileInput.value = ""; // reset
};
reader.readAsText(file);
}
2022-08-06 04:25:16 +08:00
function clear() {
schemas = "";
2022-08-06 04:25:16 +08:00
fileInput.value = "";
2022-08-06 13:03:34 +08:00
setErrors({});
2022-08-06 04:25:16 +08:00
}
2022-08-05 11:00:38 +08:00
</script>
<SettingsSidebar />
2022-08-09 21:16:09 +08:00
<PageWrapper>
2022-08-05 11:00:38 +08:00
<header class="page-header">
<nav class="breadcrumbs">
<div class="breadcrumb-item">Settings</div>
<div class="breadcrumb-item">{$pageTitle}</div>
</nav>
</header>
<div class="wrapper">
<div class="panel">
2022-08-06 04:25:16 +08:00
{#if isLoadingOldCollections}
<div class="loader" />
{:else}
2022-08-09 21:16:09 +08:00
<input
bind:this={fileInput}
type="file"
class="hidden"
accept=".json"
on:change={() => {
if (fileInput.files.length) {
loadFile(fileInput.files[0]);
}
}}
/>
2022-08-06 04:25:16 +08:00
2022-08-09 21:16:09 +08:00
<div class="content txt-xl m-b-base">
2022-08-06 04:25:16 +08:00
<p>
2022-08-07 16:14:49 +08:00
Paste below the collections configuration you want to import or
2022-08-06 04:25:16 +08:00
<button
class="btn btn-outline btn-sm m-l-5"
class:btn-loading={isLoadingFile}
on:click={() => {
fileInput.click();
}}
>
<span class="txt">Load from JSON file</span>
</button>
</p>
</div>
<Field class="form-field {!isValid ? 'field-error' : ''}" name="collections" let:uniqueId>
<label for={uniqueId} class="p-b-10">Collections</label>
2022-08-06 04:25:16 +08:00
<textarea
id={uniqueId}
class="code"
spellcheck="false"
rows="15"
required
bind:value={schemas}
2022-08-06 04:25:16 +08:00
/>
{#if !!schemas && !isValid}
2022-08-07 16:14:49 +08:00
<div class="help-block help-block-error">Invalid collections configuration.</div>
2022-08-06 04:25:16 +08:00
{/if}
</Field>
{#if false}
2022-08-11 01:37:41 +08:00
<!-- for now hide the delete control and eventually enable/remove based on the users feedback -->
<Field class="form-field form-field-toggle" let:uniqueId>
<input
type="checkbox"
id={uniqueId}
bind:checked={deleteMissing}
disabled={!isValid}
/>
<label for={uniqueId}>Delete missing collections and schema fields</label>
</Field>
{/if}
2022-08-10 18:22:27 +08:00
2022-08-06 04:25:16 +08:00
{#if isValid && newCollections.length && !hasChanges}
<div class="alert alert-info">
<div class="icon">
<i class="ri-information-line" />
</div>
<div class="content">
2022-08-07 16:14:49 +08:00
<string>Your collections configuration is already up-to-date!</string>
2022-08-06 04:25:16 +08:00
</div>
</div>
{/if}
{#if isValid && newCollections.length && hasChanges}
2022-08-06 13:03:34 +08:00
<h5 class="section-title">Detected changes</h5>
2022-08-06 04:25:16 +08:00
2022-08-06 13:03:34 +08:00
<div class="list">
2022-08-06 04:25:16 +08:00
{#if collectionsToDelete.length}
{#each collectionsToDelete as collection (collection.id)}
<div class="list-item">
<span class="label label-danger list-label">Deleted</span>
<strong>{collection.name}</strong>
2022-08-06 13:03:34 +08:00
{#if collection.id}
<small class="txt-hint">({collection.id})</small>
{/if}
2022-08-06 04:25:16 +08:00
</div>
{/each}
{/if}
{#if collectionsToUpdate.length}
{#each collectionsToUpdate as pair (pair.old.id + pair.new.id)}
2022-08-06 04:25:16 +08:00
<div class="list-item">
2022-08-10 18:22:27 +08:00
<span class="label label-warning list-label">Changed</span>
<div class="inline-flex flex-gap-5">
{#if pair.old.name !== pair.new.name}
<strong class="txt-strikethrough txt-hint">{pair.old.name}</strong
>
<i class="ri-arrow-right-line txt-sm" />
2022-08-06 04:25:16 +08:00
{/if}
<strong>
{pair.new.name}
</strong>
{#if pair.new.id}
<small class="txt-hint">({pair.new.id})</small>
{/if}
</div>
2022-08-06 04:25:16 +08:00
</div>
{/each}
{/if}
{#if collectionsToAdd.length}
{#each collectionsToAdd as collection (collection.id)}
<div class="list-item">
2022-08-10 18:22:27 +08:00
<span class="label label-success list-label">Added</span>
2022-08-06 04:25:16 +08:00
<strong>{collection.name}</strong>
2022-08-06 13:03:34 +08:00
{#if collection.id}
<small class="txt-hint">({collection.id})</small>
{/if}
2022-08-06 04:25:16 +08:00
</div>
{/each}
{/if}
</div>
{/if}
2022-08-10 18:22:27 +08:00
{#if idReplacableCollections.length}
<div class="alert alert-warning m-t-base">
2022-08-10 18:22:27 +08:00
<div class="icon">
<i class="ri-error-warning-line" />
</div>
<div class="content">
<string>
Some of the imported collections shares the same name and/or fields but are
imported with different IDs. You can replace them in the import if you want
to.
2022-08-10 18:22:27 +08:00
</string>
</div>
<button
type="button"
class="btn btn-warning btn-sm btn-outline"
on:click={() => replaceIds()}
>
<span class="txt">Replace with original ids</span>
2022-08-10 18:22:27 +08:00
</button>
</div>
{/if}
2022-08-06 04:25:16 +08:00
<div class="flex m-t-base">
{#if !!schemas}
2022-08-06 13:03:34 +08:00
<button type="button" class="btn btn-secondary link-hint" on:click={() => clear()}>
<span class="txt">Clear</span>
</button>
{/if}
2022-08-06 04:25:16 +08:00
<div class="flex-fill" />
<button
type="button"
2022-08-06 13:03:34 +08:00
class="btn btn-expanded btn-warning m-l-auto"
2022-08-06 04:25:16 +08:00
disabled={!canImport}
2022-08-10 18:22:27 +08:00
on:click={() => importPopup?.show(oldCollections, newCollections, deleteMissing)}
2022-08-06 04:25:16 +08:00
>
2022-08-06 13:03:34 +08:00
<span class="txt">Review</span>
2022-08-06 04:25:16 +08:00
</button>
</div>
{/if}
2022-08-05 11:00:38 +08:00
</div>
</div>
2022-08-09 21:16:09 +08:00
</PageWrapper>
2022-08-05 11:00:38 +08:00
2022-08-06 13:03:34 +08:00
<ImportPopup bind:this={importPopup} on:submit={() => clear()} />
2022-08-06 04:25:16 +08:00
2022-08-05 11:00:38 +08:00
<style>
2022-08-06 04:25:16 +08:00
.list-label {
min-width: 65px;
2022-08-05 11:00:38 +08:00
}
</style>