test deleteMissing with schema changes
This commit is contained in:
parent
ac0c23ff64
commit
65b830198b
|
@ -27,7 +27,7 @@ Supported arguments are:
|
||||||
- up - runs all available migrations.
|
- up - runs all available migrations.
|
||||||
- down [number] - reverts the last [number] applied migrations.
|
- down [number] - reverts the last [number] applied migrations.
|
||||||
- create name [folder] - creates new migration template file.
|
- create name [folder] - creates new migration template file.
|
||||||
- collections [folder] - creates new migration file with the current collections configuration.
|
- collections [folder] - (Experimental) creates new migration file with the most recent local collections configuration.
|
||||||
`
|
`
|
||||||
var databaseFlag string
|
var databaseFlag string
|
||||||
|
|
||||||
|
|
|
@ -190,7 +190,7 @@ func (dao *Dao) ImportCollections(
|
||||||
|
|
||||||
mappedImported := make(map[string]*models.Collection, len(importedCollections))
|
mappedImported := make(map[string]*models.Collection, len(importedCollections))
|
||||||
for _, imported := range importedCollections {
|
for _, imported := range importedCollections {
|
||||||
// normalize
|
// normalize ids
|
||||||
if !imported.HasId() {
|
if !imported.HasId() {
|
||||||
// generate id if not set
|
// generate id if not set
|
||||||
imported.MarkAsNew()
|
imported.MarkAsNew()
|
||||||
|
@ -199,6 +199,15 @@ func (dao *Dao) ImportCollections(
|
||||||
imported.MarkAsNew()
|
imported.MarkAsNew()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extend existing schema
|
||||||
|
if existing, ok := mappedExisting[imported.GetId()]; ok && !deleteMissing {
|
||||||
|
schema, _ := existing.Schema.Clone()
|
||||||
|
for _, f := range imported.Schema.Fields() {
|
||||||
|
schema.AddField(f) // add or replace
|
||||||
|
}
|
||||||
|
imported.Schema = *schema
|
||||||
|
}
|
||||||
|
|
||||||
mappedImported[imported.GetId()] = imported
|
mappedImported[imported.GetId()] = imported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package forms
|
package forms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -115,6 +116,7 @@ func (form *CollectionUpsert) Validate() error {
|
||||||
&form.Schema,
|
&form.Schema,
|
||||||
validation.By(form.ensureNoSystemFieldsChange),
|
validation.By(form.ensureNoSystemFieldsChange),
|
||||||
validation.By(form.ensureNoFieldsTypeChange),
|
validation.By(form.ensureNoFieldsTypeChange),
|
||||||
|
validation.By(form.ensureExistingRelationCollectionId),
|
||||||
),
|
),
|
||||||
validation.Field(&form.ListRule, validation.By(form.checkRule)),
|
validation.Field(&form.ListRule, validation.By(form.checkRule)),
|
||||||
validation.Field(&form.ViewRule, validation.By(form.checkRule)),
|
validation.Field(&form.ViewRule, validation.By(form.checkRule)),
|
||||||
|
@ -161,11 +163,38 @@ func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error {
|
||||||
func (form *CollectionUpsert) ensureNoFieldsTypeChange(value any) error {
|
func (form *CollectionUpsert) ensureNoFieldsTypeChange(value any) error {
|
||||||
v, _ := value.(schema.Schema)
|
v, _ := value.(schema.Schema)
|
||||||
|
|
||||||
for _, field := range v.Fields() {
|
for i, field := range v.Fields() {
|
||||||
oldField := form.collection.Schema.GetFieldById(field.Id)
|
oldField := form.collection.Schema.GetFieldById(field.Id)
|
||||||
|
|
||||||
if oldField != nil && oldField.Type != field.Type {
|
if oldField != nil && oldField.Type != field.Type {
|
||||||
return validation.NewError("validation_field_type_change", "Field type cannot be changed.")
|
return validation.Errors{fmt.Sprint(i): validation.NewError(
|
||||||
|
"validation_field_type_change",
|
||||||
|
"Field type cannot be changed.",
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (form *CollectionUpsert) ensureExistingRelationCollectionId(value any) error {
|
||||||
|
v, _ := value.(schema.Schema)
|
||||||
|
|
||||||
|
for i, field := range v.Fields() {
|
||||||
|
if field.Type != schema.FieldTypeRelation {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options, _ := field.Options.(*schema.RelationOptions)
|
||||||
|
if options == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := form.config.TxDao.FindCollectionByNameOrId(options.CollectionId); err != nil {
|
||||||
|
return validation.Errors{fmt.Sprint(i): validation.NewError(
|
||||||
|
"validation_field_invalid_relation",
|
||||||
|
"The relation collection doesn't exist.",
|
||||||
|
)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,11 +139,11 @@ func (form *CollectionsImport) beforeRecordsSync(txDao *daos.Dao, mappedNew, map
|
||||||
|
|
||||||
if err := upsertForm.Validate(); err != nil {
|
if err := upsertForm.Validate(); err != nil {
|
||||||
// serialize the validation error(s)
|
// serialize the validation error(s)
|
||||||
serializedErr, _ := json.Marshal(err)
|
serializedErr, _ := json.MarshalIndent(err, "", " ")
|
||||||
|
|
||||||
return validation.Errors{"collections": validation.NewError(
|
return validation.Errors{"collections": validation.NewError(
|
||||||
"collections_import_validate_failure",
|
"collections_import_validate_failure",
|
||||||
fmt.Sprintf("Data validations failed for collection %q (%s): %s", collection.Name, collection.Id, serializedErr),
|
fmt.Sprintf("Data validations failed for collection %q (%s):\n%s", collection.Name, collection.Id, serializedErr),
|
||||||
)}
|
)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
{#each fieldErrors as error}
|
{#each fieldErrors as error}
|
||||||
<div class="help-block help-block-error">
|
<div class="help-block help-block-error">
|
||||||
{#if typeof error === "object"}
|
{#if typeof error === "object"}
|
||||||
{error?.message || error?.code || defaultError}
|
<pre>{error?.message || error?.code || defaultError}</pre>
|
||||||
{:else}
|
{:else}
|
||||||
{error || defaultError}
|
{error || defaultError}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
<i class="ri-information-line" />
|
<i class="ri-information-line" />
|
||||||
{:else if toast.type === "success"}
|
{:else if toast.type === "success"}
|
||||||
<i class="ri-checkbox-circle-line" />
|
<i class="ri-checkbox-circle-line" />
|
||||||
|
{:else if toast.type === "warning"}
|
||||||
|
<i class="ri-error-warning-line" />
|
||||||
{:else}
|
{:else}
|
||||||
<i class="ri-alert-line" />
|
<i class="ri-alert-line" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,264 @@
|
||||||
|
<script>
|
||||||
|
import { Collection } from "pocketbase";
|
||||||
|
import CommonHelper from "@/utils/CommonHelper";
|
||||||
|
|
||||||
|
export let collectionA = new Collection();
|
||||||
|
export let collectionB = new Collection();
|
||||||
|
export let deleteMissing = false;
|
||||||
|
|
||||||
|
$: isDeleteDiff = !collectionB?.id && !collectionB?.name;
|
||||||
|
|
||||||
|
$: isCreateDiff = !isDeleteDiff && !collectionA?.id;
|
||||||
|
|
||||||
|
$: schemaA = Array.isArray(collectionA?.schema) ? collectionA?.schema : [];
|
||||||
|
|
||||||
|
$: schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : [];
|
||||||
|
|
||||||
|
$: removedFields = schemaA.filter((fieldA) => {
|
||||||
|
return !schemaB.find((fieldB) => fieldA.id == fieldB.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: sharedFields = schemaB.filter((fieldB) => {
|
||||||
|
return schemaA.find((fieldA) => fieldA.id == fieldB.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: addedFields = schemaB.filter((fieldB) => {
|
||||||
|
return !schemaA.find((fieldA) => fieldA.id == fieldB.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (typeof deleteMissing !== "undefined") {
|
||||||
|
normalizeSchemaB();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: hasAnyChange = detectChanges(collectionA, collectionB);
|
||||||
|
|
||||||
|
const mainModelProps = Object.keys(new Collection().export()).filter(
|
||||||
|
(key) => !["schema", "created", "updated"].includes(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
function normalizeSchemaB() {
|
||||||
|
schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : [];
|
||||||
|
if (!deleteMissing) {
|
||||||
|
schemaB = schemaB.concat(removedFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldById(schema, id) {
|
||||||
|
schema = schema || [];
|
||||||
|
|
||||||
|
for (let field of schema) {
|
||||||
|
if (field.id == id) {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectChanges() {
|
||||||
|
// added or removed fields
|
||||||
|
if (addedFields?.length || (deleteMissing && removedFields?.length)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// changes in the main model props
|
||||||
|
for (let prop of mainModelProps) {
|
||||||
|
if (hasChanges(collectionA?.[prop], collectionB?.[prop])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// changes in the schema fields
|
||||||
|
for (let field of sharedFields) {
|
||||||
|
if (hasChanges(field, CommonHelper.findByKey(schemaA, "id", field.id))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasChanges(valA, valB) {
|
||||||
|
// direct match
|
||||||
|
if (valA === valB) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(valA) !== JSON.stringify(valB);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayValue(value) {
|
||||||
|
if (typeof value === "undefined") {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommonHelper.isObject(value) ? JSON.stringify(value, null, 4) : value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="section-title">
|
||||||
|
{#if !collectionA?.id}
|
||||||
|
<strong>{collectionB?.name}</strong>
|
||||||
|
<span class="label label-success">Added</span>
|
||||||
|
{:else if !collectionB?.id}
|
||||||
|
<strong>{collectionA?.name}</strong>
|
||||||
|
<span class="label label-danger">Removed</span>
|
||||||
|
{:else}
|
||||||
|
<div class="inline-flex fleg-gap-5">
|
||||||
|
{#if collectionA.name !== collectionB.name}
|
||||||
|
<strong class="txt-strikethrough txt-hint">{collectionA.name}</strong>
|
||||||
|
<i class="ri-arrow-right-line txt-sm" />
|
||||||
|
{/if}
|
||||||
|
<strong class="txt">{collectionB.name}</strong>
|
||||||
|
{#if hasAnyChange}
|
||||||
|
<span class="label label-warning">Changed</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table collections-diff-table m-b-base">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Props</th>
|
||||||
|
<th width="10%">Old</th>
|
||||||
|
<th width="10%">New</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each mainModelProps as prop}
|
||||||
|
<tr class:txt-primary={hasChanges(collectionA?.[prop], collectionB?.[prop])}>
|
||||||
|
<td class="min-width">
|
||||||
|
<span>{prop}</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class:changed-old-col={!isCreateDiff &&
|
||||||
|
hasChanges(collectionA?.[prop], collectionB?.[prop])}
|
||||||
|
class:changed-none-col={isCreateDiff}
|
||||||
|
>
|
||||||
|
<pre class="txt">{displayValue(collectionA?.[prop])}</pre>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class:changed-new-col={!isDeleteDiff &&
|
||||||
|
hasChanges(collectionA?.[prop], collectionB?.[prop])}
|
||||||
|
class:changed-none-col={isDeleteDiff}
|
||||||
|
>
|
||||||
|
<pre class="txt">{displayValue(collectionB?.[prop])}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if deleteMissing || isDeleteDiff}
|
||||||
|
{#each removedFields as field}
|
||||||
|
<tr>
|
||||||
|
<th class="min-width" colspan="3">
|
||||||
|
<span class="txt">schema.{field.name}</span>
|
||||||
|
<span class="label label-danger m-l-5">
|
||||||
|
Removed - <small>
|
||||||
|
All stored data related to <strong>{field.name}</strong> will be deleted!
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{#each Object.entries(field) as [key, value]}
|
||||||
|
<tr class="txt-primary">
|
||||||
|
<td class="min-width field-key-col">{key}</td>
|
||||||
|
<td class="changed-old-col">
|
||||||
|
<pre class="txt">{displayValue(value)}</pre>
|
||||||
|
</td>
|
||||||
|
<td class="changed-none-col" />
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each sharedFields as field}
|
||||||
|
<tr>
|
||||||
|
<th class="min-width" colspan="3">
|
||||||
|
<span class="txt">schema.{field.name}</span>
|
||||||
|
{#if hasChanges(getFieldById(schemaA, field.id), getFieldById(schemaB, field.id))}
|
||||||
|
<span class="label label-warning m-l-5">Changed</span>
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{#each Object.entries(field) as [key, newValue]}
|
||||||
|
<tr class:txt-primary={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
|
||||||
|
<td class="min-width field-key-col">{key}</td>
|
||||||
|
<td class:changed-old-col={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
|
||||||
|
<pre class="txt">{displayValue(getFieldById(schemaA, field.id)?.[key])}</pre>
|
||||||
|
</td>
|
||||||
|
<td class:changed-new-col={hasChanges(getFieldById(schemaA, field.id)?.[key], newValue)}>
|
||||||
|
<pre class="txt">{displayValue(newValue)}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each addedFields as field}
|
||||||
|
<tr>
|
||||||
|
<th class="min-width" colspan="3">
|
||||||
|
<span class="txt">schema.{field.name}</span>
|
||||||
|
<span class="label label-success m-l-5">Added</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{#each Object.entries(field) as [key, value]}
|
||||||
|
<tr class="txt-primary">
|
||||||
|
<td class="min-width field-key-col">{key}</td>
|
||||||
|
<td class="changed-none-col" />
|
||||||
|
<td class="changed-new-col">
|
||||||
|
<pre class="txt">{displayValue(value)}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.collections-diff-table {
|
||||||
|
color: var(--txtHintColor);
|
||||||
|
border: 2px solid var(--primaryColor);
|
||||||
|
tr {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
height: auto;
|
||||||
|
padding: 2px 15px;
|
||||||
|
border-bottom: 1px solid rgba(#000, 0.07);
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
height: 35px;
|
||||||
|
padding: 4px 15px;
|
||||||
|
color: var(--txtPrimaryColor);
|
||||||
|
}
|
||||||
|
thead tr {
|
||||||
|
background: var(--primaryColor);
|
||||||
|
th {
|
||||||
|
color: var(--baseColor);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.changed-none-col {
|
||||||
|
color: var(--txtDisabledColor);
|
||||||
|
background: var(--baseAlt1Color);
|
||||||
|
}
|
||||||
|
.changed-old-col {
|
||||||
|
color: var(--txtPrimaryColor);
|
||||||
|
background: var(--dangerAltColor);
|
||||||
|
}
|
||||||
|
.changed-new-col {
|
||||||
|
color: var(--txtPrimaryColor);
|
||||||
|
background: var(--successAltColor);
|
||||||
|
}
|
||||||
|
.field-key-col {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,29 +2,28 @@
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import ApiClient from "@/utils/ApiClient";
|
import ApiClient from "@/utils/ApiClient";
|
||||||
import CommonHelper from "@/utils/CommonHelper";
|
import CommonHelper from "@/utils/CommonHelper";
|
||||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
|
||||||
import { addSuccessToast } from "@/stores/toasts";
|
import { addSuccessToast } from "@/stores/toasts";
|
||||||
import { confirm } from "@/stores/confirmation";
|
import { confirm } from "@/stores/confirmation";
|
||||||
|
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||||
|
import CollectionsDiffTable from "@/components/collections/CollectionsDiffTable.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let panel;
|
let panel;
|
||||||
let oldCollections = [];
|
let oldCollections = [];
|
||||||
let newCollections = [];
|
let newCollections = [];
|
||||||
let changes = [];
|
let pairs = [];
|
||||||
|
let deleteMissing = false;
|
||||||
let isImporting = false;
|
let isImporting = false;
|
||||||
|
|
||||||
$: if (Array.isArray(oldCollections) && Array.isArray(newCollections)) {
|
$: if (Array.isArray(oldCollections) && Array.isArray(newCollections)) {
|
||||||
loadChanges();
|
loadPairs();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: deletedCollections = oldCollections.filter((old) => {
|
export function show(oldCollectionsArg, newCollectionsArg, deleteMissingArg = false) {
|
||||||
return !CommonHelper.findByKey(newCollections, "id", old.id)?.id;
|
oldCollections = oldCollectionsArg;
|
||||||
});
|
newCollections = newCollectionsArg;
|
||||||
|
deleteMissing = deleteMissingArg;
|
||||||
export function show(a, b) {
|
|
||||||
oldCollections = a;
|
|
||||||
newCollections = b;
|
|
||||||
|
|
||||||
panel?.show();
|
panel?.show();
|
||||||
}
|
}
|
||||||
|
@ -33,25 +32,23 @@
|
||||||
return panel?.hide();
|
return panel?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadChanges() {
|
function loadPairs() {
|
||||||
changes = [];
|
pairs = [];
|
||||||
|
|
||||||
// add deleted and modified collections
|
// add deleted and modified collections
|
||||||
for (const oldCollection of oldCollections) {
|
for (const oldCollection of oldCollections) {
|
||||||
const newCollection = CommonHelper.findByKey(newCollections, "id", oldCollection.id) || null;
|
const newCollection = CommonHelper.findByKey(newCollections, "id", oldCollection.id) || null;
|
||||||
if (!newCollection?.id || JSON.stringify(oldCollection) != JSON.stringify(newCollection)) {
|
pairs.push({
|
||||||
changes.push({
|
old: oldCollection,
|
||||||
old: oldCollection,
|
new: newCollection,
|
||||||
new: newCollection,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add only new collections
|
// add only new collections
|
||||||
for (const newCollection of newCollections) {
|
for (const newCollection of newCollections) {
|
||||||
const oldCollection = CommonHelper.findByKey(oldCollections, "id", newCollection.id) || null;
|
const oldCollection = CommonHelper.findByKey(oldCollections, "id", newCollection.id) || null;
|
||||||
if (!oldCollection?.id) {
|
if (!oldCollection?.id) {
|
||||||
changes.push({
|
pairs.push({
|
||||||
old: oldCollection,
|
old: oldCollection,
|
||||||
new: newCollection,
|
new: newCollection,
|
||||||
});
|
});
|
||||||
|
@ -59,63 +56,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function diffsToHtml(diffs, ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) {
|
function submitWithConfirm() {
|
||||||
const html = [];
|
// find deleted fields
|
||||||
const pattern_amp = /&/g;
|
const deletedFieldNames = [];
|
||||||
const pattern_lt = /</g;
|
if (deleteMissing) {
|
||||||
const pattern_gt = />/g;
|
for (const old of oldCollections) {
|
||||||
const pattern_para = /\n/g;
|
const imported = !CommonHelper.findByKey(newCollections, "id", old.id);
|
||||||
|
if (!imported) {
|
||||||
for (let i = 0; i < diffs.length; i++) {
|
// add all fields
|
||||||
const op = diffs[i][0]; // operation (insert, delete, equal)
|
deletedFieldNames.push(old.name + ".*");
|
||||||
|
} else {
|
||||||
if (!ops.includes(op)) {
|
// add only deleted fields
|
||||||
continue;
|
const schema = Array.isArray(old.schema) ? old.schema : [];
|
||||||
}
|
for (const field of schema) {
|
||||||
|
if (!CommonHelper.findByKey(imported.schema, "id", field.id)) {
|
||||||
const text = diffs[i][1]
|
deletedFieldNames.push(old.name + "." + field.name);
|
||||||
.replace(pattern_amp, "&")
|
}
|
||||||
.replace(pattern_lt, "<")
|
}
|
||||||
.replace(pattern_gt, ">")
|
}
|
||||||
.replace(pattern_para, "<br>");
|
|
||||||
|
|
||||||
switch (op) {
|
|
||||||
case DIFF_INSERT:
|
|
||||||
html[i] = '<ins class="block">' + text + "</ins>";
|
|
||||||
break;
|
|
||||||
case DIFF_DELETE:
|
|
||||||
html[i] = '<del class="block">' + text + "</del>";
|
|
||||||
break;
|
|
||||||
case DIFF_EQUAL:
|
|
||||||
html[i] = text;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html.join("");
|
if (deletedFieldNames.length) {
|
||||||
}
|
|
||||||
|
|
||||||
function diff(obj1, obj2, ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) {
|
|
||||||
const dmp = new diff_match_patch();
|
|
||||||
const lines = dmp.diff_linesToChars_(
|
|
||||||
obj1 ? JSON.stringify(obj1, null, 4) : "",
|
|
||||||
obj2 ? JSON.stringify(obj2, null, 4) : ""
|
|
||||||
);
|
|
||||||
const diffs = dmp.diff_main(lines.chars1, lines.chars2, false);
|
|
||||||
|
|
||||||
dmp.diff_charsToLines_(diffs, lines.lineArray);
|
|
||||||
|
|
||||||
return diffsToHtml(diffs, ops);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitWithConfirm() {
|
|
||||||
if (deletedCollections.length) {
|
|
||||||
const deletedNames = deletedCollections.map((c) => c.name);
|
|
||||||
|
|
||||||
confirm(
|
confirm(
|
||||||
`Do you really want to delete the following collections and their related records data:\n- ${deletedNames.join(
|
`Do you really want to delete the following collection fields and their related records data:\n- ${deletedFieldNames.join(
|
||||||
"\n- "
|
"\n- "
|
||||||
)}?`,
|
)}`,
|
||||||
() => {
|
() => {
|
||||||
submit();
|
submit();
|
||||||
}
|
}
|
||||||
|
@ -133,8 +99,8 @@
|
||||||
isImporting = true;
|
isImporting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ApiClient.collections.import(newCollections, true);
|
await ApiClient.collections.import(newCollections, deleteMissing);
|
||||||
addSuccessToast("Successfully imported the collections configuration.");
|
addSuccessToast("Successfully imported collections configuration.");
|
||||||
dispatch("submit");
|
dispatch("submit");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ApiClient.errorResponseHandler(err);
|
ApiClient.errorResponseHandler(err);
|
||||||
|
@ -148,7 +114,7 @@
|
||||||
|
|
||||||
<OverlayPanel
|
<OverlayPanel
|
||||||
bind:this={panel}
|
bind:this={panel}
|
||||||
class="full-width-popup import-popup"
|
class="full-width-popup import-popup"
|
||||||
overlayClose={false}
|
overlayClose={false}
|
||||||
escClose={!isImporting}
|
escClose={!isImporting}
|
||||||
beforeHide={() => !isImporting}
|
beforeHide={() => !isImporting}
|
||||||
|
@ -160,40 +126,9 @@
|
||||||
<h4 class="center txt-break">Side-by-side diff</h4>
|
<h4 class="center txt-break">Side-by-side diff</h4>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<div class="grid grid-sm m-b-sm">
|
{#each pairs as pair}
|
||||||
{#each changes as pair (pair.old?.id + pair.new?.id)}
|
<CollectionsDiffTable collectionA={pair.old} collectionB={pair.new} {deleteMissing} />
|
||||||
<div class="col-12">
|
{/each}
|
||||||
<div class="flex flex-gap-10">
|
|
||||||
{#if !pair.old?.id}
|
|
||||||
<span class="label label-success">New</span>
|
|
||||||
<strong>{pair.new?.name}</strong>
|
|
||||||
{:else if !pair.new?.id}
|
|
||||||
<span class="label label-danger">Deleted</span>
|
|
||||||
<strong>{pair.old?.name}</strong>
|
|
||||||
{:else}
|
|
||||||
<span class="label label-warning">Modified</span>
|
|
||||||
<div class="inline-flex fleg-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" />
|
|
||||||
{/if}
|
|
||||||
<strong class="txt">{pair.new.name}</strong>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 p-b-10">
|
|
||||||
<code class="code-block">
|
|
||||||
{@html diff(pair.old, pair.new, [window.DIFF_DELETE, window.DIFF_EQUAL]) || "N/A"}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 p-b-10">
|
|
||||||
<code class="code-block">
|
|
||||||
{@html diff(pair.old, pair.new, [window.DIFF_INSERT, window.DIFF_EQUAL]) || "N/A"}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<button type="button" class="btn btn-secondary" on:click={hide} disabled={isImporting}>Close</button>
|
<button type="button" class="btn btn-secondary" on:click={hide} disabled={isImporting}>Close</button>
|
||||||
|
@ -208,10 +143,3 @@
|
||||||
</button>
|
</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</OverlayPanel>
|
</OverlayPanel>
|
||||||
|
|
||||||
<style>
|
|
||||||
code {
|
|
||||||
color: var(--txtHintColor);
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
let collections = [];
|
let collections = [];
|
||||||
let isLoadingCollections = false;
|
let isLoadingCollections = false;
|
||||||
|
|
||||||
$: schema = JSON.stringify(collections, null, 2);
|
$: schema = JSON.stringify(collections, null, 4);
|
||||||
|
|
||||||
loadCollections();
|
loadCollections();
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@
|
||||||
let isLoadingFile = false;
|
let isLoadingFile = false;
|
||||||
let newCollections = [];
|
let newCollections = [];
|
||||||
let oldCollections = [];
|
let oldCollections = [];
|
||||||
let collectionsToModify = [];
|
let deleteMissing = false;
|
||||||
|
let collectionsToChange = [];
|
||||||
let isLoadingOldCollections = false;
|
let isLoadingOldCollections = false;
|
||||||
|
|
||||||
$: if (typeof schemas !== "undefined") {
|
$: if (typeof schemas !== "undefined") {
|
||||||
|
@ -44,10 +45,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: hasChanges =
|
$: hasChanges =
|
||||||
!!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToModify.length);
|
!!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToChange.length);
|
||||||
|
|
||||||
$: canImport = !isLoadingOldCollections && isValid && hasChanges;
|
$: canImport = !isLoadingOldCollections && isValid && hasChanges;
|
||||||
|
|
||||||
|
$: idReplacableCollections = newCollections.filter((collection) => {
|
||||||
|
const old = CommonHelper.findByKey(oldCollections, "name", collection.name);
|
||||||
|
if (!old?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old.id != collection.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && field.id != oldField.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
loadOldCollections();
|
loadOldCollections();
|
||||||
|
|
||||||
async function loadOldCollections() {
|
async function loadOldCollections() {
|
||||||
|
@ -68,7 +91,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCollectionsToModify() {
|
function loadCollectionsToModify() {
|
||||||
collectionsToModify = [];
|
collectionsToChange = [];
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return;
|
return;
|
||||||
|
@ -85,7 +108,7 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionsToModify.push({
|
collectionsToChange.push({
|
||||||
new: newCollection,
|
new: newCollection,
|
||||||
old: oldCollection,
|
old: oldCollection,
|
||||||
});
|
});
|
||||||
|
@ -105,13 +128,52 @@
|
||||||
newCollections = CommonHelper.filterDuplicatesByKey(newCollections);
|
newCollections = CommonHelper.filterDuplicatesByKey(newCollections);
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete timestamps
|
// normalizations
|
||||||
for (let collection of newCollections) {
|
for (let collection of newCollections) {
|
||||||
|
// delete timestamps
|
||||||
delete collection.created;
|
delete collection.created;
|
||||||
delete collection.updated;
|
delete collection.updated;
|
||||||
|
|
||||||
|
// merge fields with duplicated ids
|
||||||
|
collection.schema = CommonHelper.filterDuplicatesByKey(collection.schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceIds() {
|
||||||
|
for (let collection of newCollections) {
|
||||||
|
const old = CommonHelper.findByKey(oldCollections, "name", collection.name);
|
||||||
|
if (!old?.id) {
|
||||||
|
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);
|
||||||
|
field.id = oldField.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update references
|
||||||
|
for (let ref of newCollections) {
|
||||||
|
if (!Array.isArray(ref.schema)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let field of ref.schema) {
|
||||||
|
if (field.options?.collectionId === originalId) {
|
||||||
|
field.options.collectionId = replacedId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemas = JSON.stringify(newCollections, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
function loadFile(file) {
|
function loadFile(file) {
|
||||||
isLoadingFile = true;
|
isLoadingFile = true;
|
||||||
|
|
||||||
|
@ -207,6 +269,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field class="form-field form-field-toggle" let:uniqueId>
|
||||||
|
<input type="checkbox" id={uniqueId} bind:checked={deleteMissing} disabled={!isValid} />
|
||||||
|
<label for={uniqueId}>
|
||||||
|
Delete all collections and fields that are not present in the above imported
|
||||||
|
configuration
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
|
||||||
{#if isValid && newCollections.length && !hasChanges}
|
{#if isValid && newCollections.length && !hasChanges}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
|
@ -234,10 +304,10 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if collectionsToModify.length}
|
{#if collectionsToChange.length}
|
||||||
{#each collectionsToModify as pair (pair.old.id + pair.new.id)}
|
{#each collectionsToChange as pair (pair.old.id + pair.new.id)}
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
<span class="label label-warning list-label">Modified</span>
|
<span class="label label-warning list-label">Changed</span>
|
||||||
<strong>
|
<strong>
|
||||||
{#if pair.old.name !== pair.new.name}
|
{#if pair.old.name !== pair.new.name}
|
||||||
<span class="txt-strikethrough txt-hint">{pair.old.name}</span> -
|
<span class="txt-strikethrough txt-hint">{pair.old.name}</span> -
|
||||||
|
@ -254,7 +324,7 @@
|
||||||
{#if collectionsToAdd.length}
|
{#if collectionsToAdd.length}
|
||||||
{#each collectionsToAdd as collection (collection.id)}
|
{#each collectionsToAdd as collection (collection.id)}
|
||||||
<div class="list-item">
|
<div class="list-item">
|
||||||
<span class="label label-success list-label">New</span>
|
<span class="label label-success list-label">Added</span>
|
||||||
<strong>{collection.name}</strong>
|
<strong>{collection.name}</strong>
|
||||||
{#if collection.id}
|
{#if collection.id}
|
||||||
<small class="txt-hint">({collection.id})</small>
|
<small class="txt-hint">({collection.id})</small>
|
||||||
|
@ -265,6 +335,26 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if idReplacableCollections.length}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="ri-error-warning-line" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<string>
|
||||||
|
Some of the imported collections shares the same name but has different IDs.
|
||||||
|
</string>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning btn-sm btn-outline"
|
||||||
|
on:click={() => replaceIds()}
|
||||||
|
>
|
||||||
|
<span class="txt">Replace and keep old ids</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex m-t-base">
|
<div class="flex m-t-base">
|
||||||
{#if !!schemas}
|
{#if !!schemas}
|
||||||
<button type="button" class="btn btn-secondary link-hint" on:click={() => clear()}>
|
<button type="button" class="btn btn-secondary link-hint" on:click={() => clear()}>
|
||||||
|
@ -276,7 +366,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-expanded btn-warning m-l-auto"
|
class="btn btn-expanded btn-warning m-l-auto"
|
||||||
disabled={!canImport}
|
disabled={!canImport}
|
||||||
on:click={() => importPopup?.show(oldCollections, newCollections)}
|
on:click={() => importPopup?.show(oldCollections, newCollections, deleteMissing)}
|
||||||
>
|
>
|
||||||
<span class="txt">Review</span>
|
<span class="txt">Review</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -29,7 +29,10 @@
|
||||||
<span class="txt">Files storage</span>
|
<span class="txt">Files storage</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="sidebar-title">Sync</div>
|
<div class="sidebar-title">
|
||||||
|
<span class="txt">Sync</span>
|
||||||
|
<small class="label label-danger label-compact">Experimental</small>
|
||||||
|
</div>
|
||||||
<a
|
<a
|
||||||
href="/settings/export-collections"
|
href="/settings/export-collections"
|
||||||
class="sidebar-list-item"
|
class="sidebar-list-item"
|
||||||
|
|
|
@ -443,6 +443,12 @@ a,
|
||||||
background: var(--baseAlt2Color);
|
background: var(--baseAlt2Color);
|
||||||
color: var(--txtPrimaryColor);
|
color: var(--txtPrimaryColor);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
&.label-compact {
|
||||||
|
font-size: var(--xsFontSize);
|
||||||
|
padding: 3px 5px;
|
||||||
|
min-height: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
&.label-primary {
|
&.label-primary {
|
||||||
color: var(--baseColor);
|
color: var(--baseColor);
|
||||||
background: var(--primaryColor);
|
background: var(--primaryColor);
|
||||||
|
@ -508,6 +514,7 @@ a,
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
row-gap: 5px;
|
row-gap: 5px;
|
||||||
|
|
|
@ -496,6 +496,9 @@ select {
|
||||||
line-height: var(--smLineHeight);
|
line-height: var(--smLineHeight);
|
||||||
color: var(--txtHintColor);
|
color: var(--txtHintColor);
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.help-block-error {
|
.help-block-error {
|
||||||
color: var(--dangerColor);
|
color: var(--dangerColor);
|
||||||
|
|
|
@ -155,12 +155,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
@extend %block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
margin: var(--baseSpacing) 0 var(--xsSpacing);
|
margin: var(--baseSpacing) 0 var(--xsSpacing);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: var(--smLineHeight);
|
line-height: var(--smLineHeight);
|
||||||
color: var(--txtHintColor);
|
color: var(--txtHintColor);
|
||||||
|
.label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sidebar-list-item {
|
.sidebar-list-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -110,6 +110,8 @@ export default class CommonHelper {
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
static inArray(arr, value) {
|
static inArray(arr, value) {
|
||||||
|
arr = Array.isArray(arr) ? arr : [];
|
||||||
|
|
||||||
for (let i = arr.length - 1; i >= 0; i--) {
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
if (arr[i] == value) {
|
if (arr[i] == value) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -126,6 +128,8 @@ export default class CommonHelper {
|
||||||
* @param {Mixed} value
|
* @param {Mixed} value
|
||||||
*/
|
*/
|
||||||
static removeByValue(arr, value) {
|
static removeByValue(arr, value) {
|
||||||
|
arr = Array.isArray(arr) ? arr : [];
|
||||||
|
|
||||||
for (let i = arr.length - 1; i >= 0; i--) {
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
if (arr[i] == value) {
|
if (arr[i] == value) {
|
||||||
arr.splice(i, 1);
|
arr.splice(i, 1);
|
||||||
|
@ -155,6 +159,8 @@ export default class CommonHelper {
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
static findByKey(objectsArr, key, value) {
|
static findByKey(objectsArr, key, value) {
|
||||||
|
objectsArr = Array.isArray(objectsArr) ? objectsArr : [];
|
||||||
|
|
||||||
for (let i in objectsArr) {
|
for (let i in objectsArr) {
|
||||||
if (objectsArr[i][key] == value) {
|
if (objectsArr[i][key] == value) {
|
||||||
return objectsArr[i];
|
return objectsArr[i];
|
||||||
|
@ -172,7 +178,9 @@ export default class CommonHelper {
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
static groupByKey(objectsArr, key) {
|
static groupByKey(objectsArr, key) {
|
||||||
let result = {};
|
objectsArr = Array.isArray(objectsArr) ? objectsArr : [];
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
|
||||||
for (let i in objectsArr) {
|
for (let i in objectsArr) {
|
||||||
result[objectsArr[i][key]] = result[objectsArr[i][key]] || [];
|
result[objectsArr[i][key]] = result[objectsArr[i][key]] || [];
|
||||||
|
@ -226,7 +234,10 @@ export default class CommonHelper {
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
static filterDuplicatesByKey(objectsArr, key = "id") {
|
static filterDuplicatesByKey(objectsArr, key = "id") {
|
||||||
|
objectsArr = Array.isArray(objectsArr) ? objectsArr : [];
|
||||||
|
|
||||||
const uniqueMap = {};
|
const uniqueMap = {};
|
||||||
|
|
||||||
for (const item of objectsArr) {
|
for (const item of objectsArr) {
|
||||||
uniqueMap[item[key]] = item;
|
uniqueMap[item[key]] = item;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue