import scaffoldings

This commit is contained in:
Gani Georgiev
2022-08-05 06:00:38 +03:00
parent 95f9d685dc
commit f459dd8812
25 changed files with 1362 additions and 261 deletions
+7 -1
View File
@@ -4,6 +4,9 @@
import "prismjs/components/prism-dart.js";
import "@/scss/prism_light.scss";
let classes = "";
export { classes as class }; // export reserved keyword
export let content = "";
export let language = "javascript"; // javascript, html
@@ -28,7 +31,7 @@
}
</script>
<div class="code-wrapper prism-light">
<div class="code-wrapper prism-light {classes}">
<code>{@html formattedContent}</code>
</div>
@@ -43,6 +46,9 @@
.code-wrapper {
display: block;
width: 100%;
max-height: 100%;
overflow: auto; /* fallback */
overflow: overlay;
}
.prism-light code {
color: var(--txtPrimaryColor);
@@ -0,0 +1,352 @@
<script>
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import Field from "@/components/base/Field.svelte";
import CodeBlock from "@/components/base/CodeBlock.svelte";
import { onMount } from "svelte";
const uniqueId = "exports_" + CommonHelper.randomString(5);
let collections = [];
let isLoadingCollections = false;
loadCollections();
async function loadCollections() {
isLoadingCollections = true;
try {
collections = await ApiClient.collections.getFullList(100, {
$cancelKey: uniqueId,
});
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoadingCollections = false;
}
let oldSchema = "";
let newSchema = "";
function diff_prettyHtml(diffs, showInsert) {
const html = [];
const pattern_amp = /&/g;
const pattern_lt = /</g;
const pattern_gt = />/g;
const pattern_para = /\n/g;
for (let x = 0; x < diffs.length; x++) {
let op = diffs[x][0]; // Operation (insert, delete, equal)
let data = diffs[x][1]; // Text of change.
let text = data
.replace(pattern_amp, "&amp;")
.replace(pattern_lt, "&lt;")
.replace(pattern_gt, "&gt;")
.replace(pattern_para, "<br>");
// text = CommonHelper.stripTags(text);
switch (op) {
case DIFF_INSERT:
if (showInsert) {
html[x] = '<ins class="block">' + text + "</ins>";
}
break;
case DIFF_DELETE:
if (!showInsert) {
html[x] = '<del class="block">' + text + "</del>";
}
break;
case DIFF_EQUAL:
html[x] = "<span>" + text + "</span>";
break;
}
}
return html.join("");
}
onMount(() => {
var dmp = new diff_match_patch();
const text1 = [
{
id: "zwWlxR46txtoAwx",
created: "2022-08-01 17:32:24.329",
updated: "2022-08-04 10:19:57.248",
name: "profia sdles",
system: true,
listRule: "userId = @request.user.id",
viewRule: "userId = @request.user.id",
createRule: "userId = @request.user.id",
updateRule: "userId = @request.user.id",
deleteRule: null,
schema: [
{
id: "nsght7oy",
name: "userId",
type: "user",
system: true,
required: true,
unique: true,
options: {
maxSelect: 1,
cascadeDelete: true,
},
},
{
id: "atpc4yjm",
name: "name",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "akb4s9de",
name: "avatar",
type: "file",
system: false,
required: false,
unique: false,
options: {
maxSelect: 1,
maxSize: 5242880,
mimeTypes: ["image/jpg", "image/jpeg", "image/png", "image/svg+xml", "image/gif"],
thumbs: null,
},
},
],
},
{
id: "IV8FbE78jmXF56d",
created: "",
updated: "2022-08-04 10:21:54.100",
name: "abc",
system: false,
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
schema: [
{
id: "t2pukeas",
name: "demo",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "dddddd",
name: "aaaa",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "squmamtm",
name: "test",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
},
},
],
},
];
const text2 = [
{
id: "zwWlxR46txtoAwx",
created: "2022-08-01 17:32:24.329",
updated: "2022-08-04 10:19:57.248",
name: "Demo",
system: true,
listRule: "userId = @request.user.id",
viewRule: "userId = @request.user.id",
createRule: "userId = @request.user.id",
updateRule: "userId = @request.user.id",
deleteRule: null,
schema: [
{
id: "nsght7oy",
name: "userId",
type: "user",
system: true,
required: true,
unique: true,
options: {
maxSelect: 1,
cascadeDelete: true,
},
},
{
id: "atpc4yjm",
name: "name",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "akb4s9de",
name: "avatar",
type: "file",
system: false,
required: false,
unique: true,
options: {
maxSelect: 1,
maxSize: 5242880,
mimeTypes: ["image/jpg", "image/jpeg", "image/png", "image/svg+xml", "image/gif"],
thumbs: null,
},
},
],
},
{
id: "IV8FbE78jmXF56d",
created: "",
updated: "2022-08-04 10:21:54.100",
name: "abc",
system: false,
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
schema: [
{
id: "t2pukeas",
name: "demo",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "dddddd",
name: "aaaa",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
{
id: "squmamtm",
name: "test",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
},
},
],
},
{
id: "GGACt8sa1tcJp7T",
created: "2022-08-04 10:22:15.871",
updated: "2022-08-04 10:22:15.871",
name: "asdasd",
system: true,
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
schema: [
{
id: "0eklwfvl",
name: "field",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: "",
},
},
],
},
];
// var diffs = dmp.diff_main(JSON.stringify(text1, null, 2), JSON.stringify(text2, null, 2));
var a = dmp.diff_linesToChars_(JSON.stringify(text1, null, 2), JSON.stringify(text2, null, 2));
var lineText1 = a.chars1;
var lineText2 = a.chars2;
var lineArray = a.lineArray;
var diffs = dmp.diff_main(lineText1, lineText2, false);
dmp.diff_charsToLines_(diffs, lineArray);
oldSchema = diff_prettyHtml(diffs, false);
newSchema = diff_prettyHtml(diffs, true);
});
</script>
<br />
<div class="grid">
<div class="col-6">
<code>
{@html oldSchema}
</code>
</div>
<div class="col-6">
<code>
{@html newSchema}
</code>
</div>
</div>
<style lang="scss">
.collections-list {
column-count: 2;
column-gap: var(--baseSpacing);
}
code {
display: block;
width: 100%;
overflow: auto;
padding: var(--xsSpacing);
white-space: pre;
background: var(--baseAlt1Color);
}
</style>
@@ -0,0 +1,29 @@
<script>
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import CollectionsExportForm from "@/components/collections/CollectionsExportForm.svelte";
let overlayPanel;
export function show() {
return overlayPanel?.show();
}
export function hide() {
return overlayPanel?.hide();
}
</script>
<OverlayPanel
bind:this={overlayPanel}
class="overlay-panel-xl collections-export-panel"
on:hide
on:show
popup
active
>
<svelte:fragment slot="header">
<h4>Export collections schema</h4>
</svelte:fragment>
<CollectionsExportForm />
</OverlayPanel>
+15 -9
View File
@@ -13,6 +13,7 @@
import CollectionsSidebar from "@/components/collections/CollectionsSidebar.svelte";
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
import CollectionDocsPanel from "@/components/collections/docs/CollectionDocsPanel.svelte";
import CollectionsExportPanel from "@/components/collections/CollectionsExportPanel.svelte";
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
import RecordsList from "@/components/records/RecordsList.svelte";
@@ -20,6 +21,7 @@
const queryParams = new URLSearchParams($querystring);
let collectionsExportPanel;
let collectionUpsertPanel;
let collectionDocsPanel;
let recordPanel;
@@ -84,16 +86,18 @@
<div class="breadcrumb-item">{$activeCollection.name}</div>
</nav>
<button
type="button"
class="btn btn-secondary btn-circle"
use:tooltip={{ text: "Edit collection", position: "right" }}
on:click={() => collectionUpsertPanel?.show($activeCollection)}
>
<i class="ri-settings-4-line" />
</button>
<div class="inline-flex gap-5">
<button
type="button"
class="btn btn-secondary btn-circle"
use:tooltip={{ text: "Edit collection", position: "right" }}
on:click={() => collectionUpsertPanel?.show($activeCollection)}
>
<i class="ri-settings-4-line" />
</button>
<RefreshButton on:refresh={() => recordsList?.load()} />
<RefreshButton on:refresh={() => recordsList?.load()} />
</div>
<div class="btns-group">
<button
@@ -128,6 +132,8 @@
</main>
{/if}
<CollectionsExportPanel bind:this={collectionsExportPanel} />
<CollectionUpsertPanel bind:this={collectionUpsertPanel} />
<CollectionDocsPanel bind:this={collectionDocsPanel} />
@@ -0,0 +1,105 @@
<script>
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
import { addInfoToast } from "@/stores/toasts";
import CodeBlock from "@/components/base/CodeBlock.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
$pageTitle = "Export collections";
const uniqueId = "export_" + CommonHelper.randomString(5);
let collections = [];
let isLoadingCollections = false;
$: schema = JSON.stringify(collections, null, 2);
loadCollections();
async function loadCollections() {
isLoadingCollections = true;
try {
collections = await ApiClient.collections.getFullList(100, {
$cancelKey: uniqueId,
});
// delete timestamps
for (let collection of collections) {
delete collection.created;
delete collection.updated;
}
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoadingCollections = false;
}
function download() {
CommonHelper.downloadJson(collections, "pb_schema");
}
function copy() {
CommonHelper.copyToClipboard(schema);
addInfoToast("The schema was copied to your clipboard!", 3000);
}
</script>
<SettingsSidebar />
<main class="page-wrapper">
<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">
{#if isLoadingCollections}
<div class="loader" />
{:else}
<div class="content txt-xl m-b-base">
<p>
Below you'll find your current collections schema that you could import later in
another PocketBase environment.
</p>
</div>
<div class="export-preview">
<button
type="button"
class="btn btn-sm btn-secondary fade copy-schema"
on:click={() => copy()}
>
<span class="txt">Copy</span>
</button>
<CodeBlock content={schema} />
</div>
<div class="flex m-t-base">
<div class="flex-fill" />
<button type="button" class="btn btn-expanded" on:click={() => download()}>
<i class="ri-download-line" />
<span class="txt">Download as JSON</span>
</button>
</div>
{/if}
</div>
</div>
</main>
<style>
.export-preview {
position: relative;
height: 500px;
}
.export-preview .copy-schema {
position: absolute;
right: 15px;
top: 15px;
}
</style>
@@ -0,0 +1,221 @@
<script>
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
import { addInfoToast, addErrorToast } from "@/stores/toasts";
import Field from "@/components/base/Field.svelte";
import CodeBlock from "@/components/base/CodeBlock.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
$pageTitle = "Import collections";
let uniquePageId = "import_" + CommonHelper.randomString(5);
let fileInput;
let schema = "";
let isImporting = false;
let isLoadingFile = false;
let newCollections = [];
let oldCollections = [];
let isLoadingOldCollections = false;
$: if (typeof schema !== "undefined") {
loadNewCollections(schema);
}
$: isValid =
!!schema &&
newCollections.length &&
newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length;
$: canImport = isValid && !isLoadingOldCollections;
$: collectionsToDelete = oldCollections.filter((collection) => {
return !CommonHelper.findByKey(newCollections, "id", collection.id);
});
$: collectionsToAdd = newCollections.filter((collection) => {
return !CommonHelper.findByKey(oldCollections, "id", collection.id);
});
$: collectionsToModify = newCollections.filter((newCollection) => {
const oldCollection = CommonHelper.findByKey(oldCollections, "id", newCollection.id);
if (!oldCollection?.id) {
return false;
}
return JSON.stringify(oldCollection) !== JSON.stringify(newCollection);
});
loadOldCollections();
async function loadOldCollections() {
isLoadingOldCollections = true;
try {
oldCollections = await ApiClient.collections.getFullList(100, {
$cancelKey: uniquePageId,
});
// delete timestamps
for (let collection of oldCollections) {
delete collection.created;
delete collection.updated;
}
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoadingOldCollections = false;
}
function loadNewCollections() {
newCollections = [];
try {
newCollections = JSON.parse(schema);
} catch (_) {}
if (!Array.isArray(newCollections)) {
newCollections = [];
}
// delete timestamps
for (let collection of newCollections) {
delete collection.created;
delete collection.updated;
}
}
function loadFile(file) {
isLoadingFile = true;
const reader = new FileReader();
reader.onload = (event) => {
schema = event.target.result;
isLoadingFile = false;
fileInput.value = ""; // reset
};
reader.onerror = (err) => {
console.log(err);
addErrorToast("Failed to load the imported JSON.");
isLoadingFile = false;
fileInput.value = ""; // reset
};
reader.readAsText(file);
}
function submitImport() {
isImporting = true;
try {
const newCollections = JSON.parse(schema);
ApiClient.collections.import(newCollections);
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isImporting = false;
}
</script>
<SettingsSidebar />
<main class="page-wrapper">
<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">
<div class="content txt-xl m-b-base">
<input
bind:this={fileInput}
type="file"
class="hidden"
accept=".json"
on:change={() => {
if (fileInput.files.length) {
loadFile(fileInput.files[0]);
}
}}
/>
<p>
Paste below the collections schema you want to import or
<button
class="btn btn-outline btn-sm"
class:btn-loading={isLoadingFile}
on:click={() => {
fileInput.click();
}}
>
<span class="txt">Import from JSON file</span>
</button>
</p>
</div>
<Field class="form-field {!isValid ? 'field-error' : ''}" name="collections" let:uniqueId>
<label for={uniqueId}>Collections schema</label>
<textarea
id={uniqueId}
class="json-editor"
spellcheck="false"
rows="20"
required
bind:value={schema}
/>
{#if !!schema && !isValid}
<div class="help-block help-block-error">Invalid collections schema.</div>
{/if}
</Field>
<div class="section-title">Detected changes</div>
<p>No changes to your current collections schema were found.</p>
{#each collectionsToDelete as collection (collection.id)}
Delete {collection.name}
<br />
{/each}
{#each collectionsToModify as collection (collection.id)}
Modify {collection.name}
<br />
{/each}
{#each collectionsToAdd as collection (collection.id)}
Add {collection.name}
<br />
{/each}
<div class="flex m-t-base">
<div class="flex-fill" />
<button
type="button"
class="btn btn-expanded"
class:btn-loading={isImporting}
disabled={!canImport}
on:click={() => submitImport()}
>
<span class="txt">Import</span>
</button>
</div>
</div>
</div>
</main>
<style>
.json-editor {
font-size: 15px;
line-height: 1.379rem;
font-family: var(--monospaceFontFamily);
}
</style>
@@ -29,6 +29,26 @@
<span class="txt">Files storage</span>
</a>
<div class="sidebar-title">Sync</div>
<a
href="/settings/export-collections"
class="sidebar-list-item"
use:active={{ path: "/settings/export-collections/?.*" }}
use:link
>
<i class="ri-uninstall-line" />
<span class="txt">Export collections</span>
</a>
<a
href="/settings/import-collections"
class="sidebar-list-item"
use:active={{ path: "/settings/import-collections/?.*" }}
use:link
>
<i class="ri-install-line" />
<span class="txt">Import collections</span>
</a>
<div class="sidebar-title">Authentication</div>
<a
href="/settings/auth-providers"
+5 -4
View File
@@ -55,10 +55,11 @@
clearList();
}
return ApiClient.users.getList(page, 50, {
sort: sort || "-created",
filter: filter,
})
return ApiClient.users
.getList(page, 50, {
sort: sort || "-created",
filter: filter,
})
.then((result) => {
isLoadingUsers = false;
users = users.concat(result.items);