updated import popup handling and api preview examples

This commit is contained in:
Gani Georgiev 2022-08-10 16:16:59 +03:00
parent 65b830198b
commit d56b8fcb90
13 changed files with 172 additions and 99 deletions

View File

@ -190,22 +190,28 @@ 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 ids // generate id if not set
if !imported.HasId() { if !imported.HasId() {
// generate id if not set
imported.MarkAsNew() imported.MarkAsNew()
imported.RefreshId() imported.RefreshId()
} else if _, ok := mappedExisting[imported.GetId()]; !ok {
imported.MarkAsNew()
} }
// extend existing schema if existing, ok := mappedExisting[imported.GetId()]; ok {
if existing, ok := mappedExisting[imported.GetId()]; ok && !deleteMissing { // preserve original created date
schema, _ := existing.Schema.Clone() if !existing.Created.IsZero() {
for _, f := range imported.Schema.Fields() { imported.Created = existing.Created
schema.AddField(f) // add or replace
} }
imported.Schema = *schema
// extend existing schema
if !deleteMissing {
schema, _ := existing.Schema.Clone()
for _, f := range imported.Schema.Fields() {
schema.AddField(f) // add or replace
}
imported.Schema = *schema
}
} else {
imported.MarkAsNew()
} }
mappedImported[imported.GetId()] = imported mappedImported[imported.GetId()] = imported

View File

@ -5,14 +5,25 @@
export let collectionA = new Collection(); export let collectionA = new Collection();
export let collectionB = new Collection(); export let collectionB = new Collection();
export let deleteMissing = false; export let deleteMissing = false;
let schemaA = [];
let schemaB = [];
let removedFields = [];
let sharedFields = [];
let addedFields = [];
$: isDeleteDiff = !collectionB?.id && !collectionB?.name; $: isDeleteDiff = !collectionB?.id && !collectionB?.name;
$: isCreateDiff = !isDeleteDiff && !collectionA?.id; $: isCreateDiff = !isDeleteDiff && !collectionA?.id;
$: schemaA = Array.isArray(collectionA?.schema) ? collectionA?.schema : []; $: schemaA = Array.isArray(collectionA?.schema) ? collectionA?.schema.concat() : [];
$: schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : []; $: if (
typeof collectionA?.schema !== "undefined" ||
typeof collectionB?.schema !== "undefined" ||
typeof deleteMissing !== "undefined"
) {
setSchemaB();
}
$: removedFields = schemaA.filter((fieldA) => { $: removedFields = schemaA.filter((fieldA) => {
return !schemaB.find((fieldB) => fieldA.id == fieldB.id); return !schemaB.find((fieldB) => fieldA.id == fieldB.id);
@ -26,20 +37,21 @@
return !schemaA.find((fieldA) => fieldA.id == fieldB.id); return !schemaA.find((fieldA) => fieldA.id == fieldB.id);
}); });
$: if (typeof deleteMissing !== "undefined") { $: hasAnyChange = CommonHelper.hasCollectionChanges(collectionA, collectionB, deleteMissing);
normalizeSchemaB();
}
$: hasAnyChange = detectChanges(collectionA, collectionB);
const mainModelProps = Object.keys(new Collection().export()).filter( const mainModelProps = Object.keys(new Collection().export()).filter(
(key) => !["schema", "created", "updated"].includes(key) (key) => !["schema", "created", "updated"].includes(key)
); );
function normalizeSchemaB() { function setSchemaB() {
schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema : []; schemaB = Array.isArray(collectionB?.schema) ? collectionB?.schema.concat() : [];
if (!deleteMissing) { if (!deleteMissing) {
schemaB = schemaB.concat(removedFields); schemaB = schemaB.concat(
schemaA.filter((fieldA) => {
return !schemaB.find((fieldB) => fieldA.id == fieldB.id);
})
);
} }
} }
@ -54,29 +66,6 @@
return null; 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) { function hasChanges(valA, valB) {
// direct match // direct match
if (valA === valB) { if (valA === valB) {
@ -88,7 +77,7 @@
function displayValue(value) { function displayValue(value) {
if (typeof value === "undefined") { if (typeof value === "undefined") {
return "N/A"; return "";
} }
return CommonHelper.isObject(value) ? JSON.stringify(value, null, 4) : value; return CommonHelper.isObject(value) ? JSON.stringify(value, null, 4) : value;
@ -97,21 +86,21 @@
<div class="section-title"> <div class="section-title">
{#if !collectionA?.id} {#if !collectionA?.id}
<strong>{collectionB?.name}</strong>
<span class="label label-success">Added</span> <span class="label label-success">Added</span>
<strong>{collectionB?.name}</strong>
{:else if !collectionB?.id} {:else if !collectionB?.id}
<span class="label label-danger">Deleted</span>
<strong>{collectionA?.name}</strong> <strong>{collectionA?.name}</strong>
<span class="label label-danger">Removed</span>
{:else} {:else}
<div class="inline-flex fleg-gap-5"> <div class="inline-flex fleg-gap-5">
{#if hasAnyChange}
<span class="label label-warning">Changed</span>
{/if}
{#if collectionA.name !== collectionB.name} {#if collectionA.name !== collectionB.name}
<strong class="txt-strikethrough txt-hint">{collectionA.name}</strong> <strong class="txt-strikethrough txt-hint">{collectionA.name}</strong>
<i class="ri-arrow-right-line txt-sm" /> <i class="ri-arrow-right-line txt-sm" />
{/if} {/if}
<strong class="txt">{collectionB.name}</strong> <strong class="txt">{collectionB.name}</strong>
{#if hasAnyChange}
<span class="label label-warning">Changed</span>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@ -152,9 +141,9 @@
{#each removedFields as field} {#each removedFields as field}
<tr> <tr>
<th class="min-width" colspan="3"> <th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span> <span class="txt">field: {field.name}</span>
<span class="label label-danger m-l-5"> <span class="label label-danger m-l-5">
Removed - <small> Deleted - <small>
All stored data related to <strong>{field.name}</strong> will be deleted! All stored data related to <strong>{field.name}</strong> will be deleted!
</small> </small>
</span> </span>
@ -176,7 +165,7 @@
{#each sharedFields as field} {#each sharedFields as field}
<tr> <tr>
<th class="min-width" colspan="3"> <th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span> <span class="txt">field: {field.name}</span>
{#if hasChanges(getFieldById(schemaA, field.id), getFieldById(schemaB, field.id))} {#if hasChanges(getFieldById(schemaA, field.id), getFieldById(schemaB, field.id))}
<span class="label label-warning m-l-5">Changed</span> <span class="label label-warning m-l-5">Changed</span>
{/if} {/if}
@ -199,7 +188,7 @@
{#each addedFields as field} {#each addedFields as field}
<tr> <tr>
<th class="min-width" colspan="3"> <th class="min-width" colspan="3">
<span class="txt">schema.{field.name}</span> <span class="txt">field: {field.name}</span>
<span class="label label-success m-l-5">Added</span> <span class="label label-success m-l-5">Added</span>
</th> </th>
</tr> </tr>

View File

@ -12,8 +12,7 @@
$: adminsOnly = collection?.createRule === null; $: adminsOnly = collection?.createRule === null;
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
$: responses = [ $: responses = [
{ {

View File

@ -11,8 +11,7 @@
$: adminsOnly = collection?.deleteRule === null; $: adminsOnly = collection?.deleteRule === null;
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
$: if (collection?.id) { $: if (collection?.id) {
responses.push({ responses.push({

View File

@ -13,8 +13,7 @@
$: adminsOnly = collection?.listRule === null; $: adminsOnly = collection?.listRule === null;
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
$: if (collection?.id) { $: if (collection?.id) {
responses.push({ responses.push({

View File

@ -7,8 +7,7 @@
export let collection = new Collection(); export let collection = new Collection();
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
</script> </script>
<div class="alert"> <div class="alert">

View File

@ -12,8 +12,7 @@
$: adminsOnly = collection?.updateRule === null; $: adminsOnly = collection?.updateRule === null;
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
$: responses = [ $: responses = [
{ {

View File

@ -12,8 +12,7 @@
$: adminsOnly = collection?.viewRule === null; $: adminsOnly = collection?.viewRule === null;
$: backendAbsUrl = $: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
window.location.href.substring(0, window.location.href.indexOf("/_")) || ApiClient.baseUrl;
$: if (collection?.id) { $: if (collection?.id) {
responses.push({ responses.push({

View File

@ -35,13 +35,19 @@
function loadPairs() { function loadPairs() {
pairs = []; pairs = [];
// add deleted and modified collections // add modified and deleted (if deleteMissing is set)
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;
pairs.push({ if (
old: oldCollection, (deleteMissing && !newCollection?.id) ||
new: newCollection, (newCollection?.id &&
}); CommonHelper.hasCollectionChanges(oldCollection, newCollection, deleteMissing))
) {
pairs.push({
old: oldCollection,
new: newCollection,
});
}
} }
// add only new collections // add only new collections
@ -61,7 +67,7 @@
const deletedFieldNames = []; const deletedFieldNames = [];
if (deleteMissing) { if (deleteMissing) {
for (const old of oldCollections) { for (const old of oldCollections) {
const imported = !CommonHelper.findByKey(newCollections, "id", old.id); const imported = CommonHelper.findByKey(newCollections, "id", old.id);
if (!imported) { if (!imported) {
// add all fields // add all fields
deletedFieldNames.push(old.name + ".*"); deletedFieldNames.push(old.name + ".*");
@ -70,7 +76,7 @@
const schema = Array.isArray(old.schema) ? old.schema : []; const schema = Array.isArray(old.schema) ? old.schema : [];
for (const field of schema) { for (const field of schema) {
if (!CommonHelper.findByKey(imported.schema, "id", field.id)) { if (!CommonHelper.findByKey(imported.schema, "id", field.id)) {
deletedFieldNames.push(old.name + "." + field.name); deletedFieldNames.push(`${old.name}.${field.name} (${field.id})`);
} }
} }
} }

View File

@ -19,8 +19,8 @@
let isLoadingFile = false; let isLoadingFile = false;
let newCollections = []; let newCollections = [];
let oldCollections = []; let oldCollections = [];
let deleteMissing = false; let deleteMissing = true;
let collectionsToChange = []; let collectionsToUpdate = [];
let isLoadingOldCollections = false; let isLoadingOldCollections = false;
$: if (typeof schemas !== "undefined") { $: if (typeof schemas !== "undefined") {
@ -33,19 +33,19 @@
newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length; newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length;
$: collectionsToDelete = oldCollections.filter((collection) => { $: collectionsToDelete = oldCollections.filter((collection) => {
return isValid && !CommonHelper.findByKey(newCollections, "id", collection.id); return isValid && deleteMissing && !CommonHelper.findByKey(newCollections, "id", collection.id);
}); });
$: collectionsToAdd = newCollections.filter((collection) => { $: collectionsToAdd = newCollections.filter((collection) => {
return isValid && !CommonHelper.findByKey(oldCollections, "id", collection.id); return isValid && !CommonHelper.findByKey(oldCollections, "id", collection.id);
}); });
$: if (typeof newCollections !== "undefined") { $: if (typeof newCollections !== "undefined" || typeof deleteMissing !== "undefined") {
loadCollectionsToModify(); loadCollectionsToUpdate();
} }
$: hasChanges = $: hasChanges =
!!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToChange.length); !!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToUpdate.length);
$: canImport = !isLoadingOldCollections && isValid && hasChanges; $: canImport = !isLoadingOldCollections && isValid && hasChanges;
@ -62,8 +62,13 @@
const oldSchema = Array.isArray(old.schema) ? old.schema : []; const oldSchema = Array.isArray(old.schema) ? old.schema : [];
const newSchema = Array.isArray(collection.schema) ? collection.schema : []; const newSchema = Array.isArray(collection.schema) ? collection.schema : [];
for (const field of newSchema) { for (const field of newSchema) {
const oldField = CommonHelper.findByKey(oldSchema, "name", field.name); const oldFieldById = CommonHelper.findByKey(oldSchema, "id", field.id);
if (oldField && field.id != oldField.id) { if (oldFieldById) {
continue;
}
const oldFieldByName = CommonHelper.findByKey(oldSchema, "name", field.name);
if (oldFieldByName && field.id != oldFieldByName.id) {
return true; return true;
} }
} }
@ -90,8 +95,8 @@
isLoadingOldCollections = false; isLoadingOldCollections = false;
} }
function loadCollectionsToModify() { function loadCollectionsToUpdate() {
collectionsToChange = []; collectionsToUpdate = [];
if (!isValid) { if (!isValid) {
return; return;
@ -103,12 +108,12 @@
// no old collection // no old collection
!oldCollection?.id || !oldCollection?.id ||
// no changes // no changes
JSON.stringify(oldCollection) === JSON.stringify(newCollection) !CommonHelper.hasCollectionChanges(oldCollection, newCollection, deleteMissing)
) { ) {
continue; continue;
} }
collectionsToChange.push({ collectionsToUpdate.push({
new: newCollection, new: newCollection,
old: oldCollection, old: oldCollection,
}); });
@ -194,7 +199,7 @@
}; };
reader.onerror = (err) => { reader.onerror = (err) => {
console.log(err); console.warn(err);
addErrorToast("Failed to load the imported JSON."); addErrorToast("Failed to load the imported JSON.");
isLoadingFile = false; isLoadingFile = false;
@ -269,13 +274,18 @@
{/if} {/if}
</Field> </Field>
<Field class="form-field form-field-toggle" let:uniqueId> {#if false}
<input type="checkbox" id={uniqueId} bind:checked={deleteMissing} disabled={!isValid} /> <!-- for now hide the delete control and enable/remove based on users feedback -->
<label for={uniqueId}> <Field class="form-field form-field-toggle" let:uniqueId>
Delete all collections and fields that are not present in the above imported <input
configuration type="checkbox"
</label> id={uniqueId}
</Field> bind:checked={deleteMissing}
disabled={!isValid}
/>
<label for={uniqueId}>Delete missing collections and schema fields</label>
</Field>
{/if}
{#if isValid && newCollections.length && !hasChanges} {#if isValid && newCollections.length && !hasChanges}
<div class="alert alert-info"> <div class="alert alert-info">
@ -304,8 +314,8 @@
{/each} {/each}
{/if} {/if}
{#if collectionsToChange.length} {#if collectionsToUpdate.length}
{#each collectionsToChange as pair (pair.old.id + pair.new.id)} {#each collectionsToUpdate as pair (pair.old.id + pair.new.id)}
<div class="list-item"> <div class="list-item">
<span class="label label-warning list-label">Changed</span> <span class="label label-warning list-label">Changed</span>
<strong> <strong>
@ -336,13 +346,15 @@
{/if} {/if}
{#if idReplacableCollections.length} {#if idReplacableCollections.length}
<div class="alert alert-warning"> <div class="alert alert-warning m-t-base">
<div class="icon"> <div class="icon">
<i class="ri-error-warning-line" /> <i class="ri-error-warning-line" />
</div> </div>
<div class="content"> <div class="content">
<string> <string>
Some of the imported collections shares the same name but has different IDs. 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.
</string> </string>
</div> </div>
<button <button
@ -350,7 +362,7 @@
class="btn btn-warning btn-sm btn-outline" class="btn btn-warning btn-sm btn-outline"
on:click={() => replaceIds()} on:click={() => replaceIds()}
> >
<span class="txt">Replace and keep old ids</span> <span class="txt">Replace with original ids</span>
</button> </button>
</div> </div>
{/if} {/if}

View File

@ -1,6 +1,6 @@
.bulkbar { .bulkbar {
position: sticky; position: sticky;
bottom: -10px; bottom: var(--baseSpacing);
z-index: 101; z-index: 101;
gap: 10px; gap: 10px;
display: flex; display: flex;

View File

@ -26,7 +26,7 @@
--warningColor: #ff8e3c; --warningColor: #ff8e3c;
--warningAltColor: #ffe7d6; --warningAltColor: #ffe7d6;
--overlayColor: rgba(88, 95, 101, 0.3); --overlayColor: rgba(70, 85, 100, 0.3);
--tooltipColor: rgba(0, 0, 0, 0.85); --tooltipColor: rgba(0, 0, 0, 0.85);
--shadowColor: rgba(0, 0, 0, 0.05); --shadowColor: rgba(0, 0, 0, 0.05);

View File

@ -874,4 +874,70 @@ export default class CommonHelper {
return 'String'; return 'String';
} }
} }
/**
* Returns an API url address extract from the current running instance.
*
* @param {String} fallback Fallback url that will be used if the extractions fail.
* @return {String}
*/
static getApiExampleUrl(fallback) {
let url = window.location.href.substring(0, window.location.href.indexOf("/_")) || fallback || '/';
// for broader compatibility replace localhost with 127.0.0.1
// (see https://github.com/pocketbase/js-sdk/issues/21)
return url.replace('//localhost', '//127.0.0.1');
}
/**
* Checks if the provided 2 collections has any change (ignoring root schema fields order).
*
* @param {Collection} oldCollection
* @param {Collection} newCollection
* @param {Boolean} withDeleteMissing Skip missing schema fields from the newCollection.
* @return {Boolean}
*/
static hasCollectionChanges(oldCollection, newCollection, withDeleteMissing = false) {
oldCollection = oldCollection || {};
newCollection = newCollection || {};
if (oldCollection.id != newCollection.id) {
return true;
}
for (let prop in oldCollection) {
if (prop !== 'schema' && JSON.stringify(oldCollection[prop]) !== JSON.stringify(newCollection[prop])) {
return true;
}
}
const oldSchema = Array.isArray(oldCollection.schema) ? oldCollection.schema : [];
const newSchema = Array.isArray(newCollection.schema) ? newCollection.schema : [];
const removedFields = oldSchema.filter((oldField) => {
return oldField?.id && !CommonHelper.findByKey(newSchema, "id", oldField.id);
});
const addedFields = newSchema.filter((newField) => {
return newField?.id && !CommonHelper.findByKey(oldSchema, "id", newField.id);
});
const changedFields = newSchema.filter((newField) => {
const oldField = CommonHelper.isObject(newField) && CommonHelper.findByKey(oldSchema, "id", newField.id);
if (!oldField) {
return false;
}
for (let prop in oldField) {
if (JSON.stringify(newField[prop]) != JSON.stringify(oldField[prop])) {
return true;
}
}
return false;
});
return !!(
addedFields.length ||
changedFields.length ||
(withDeleteMissing && removedFields.length)
);
}
} }