updated CollectionsImport and CollectionUpsert forms

This commit is contained in:
Gani Georgiev 2022-08-06 18:15:18 +03:00
parent 4e58e7ad6a
commit 5fb45a1864
10 changed files with 224 additions and 93 deletions

View File

@ -183,6 +183,9 @@ func (dao *Dao) create(m models.Model) error {
m.RefreshId() m.RefreshId()
} }
// mark the model as "new" since the model now always has an ID
m.MarkAsNew()
if m.GetCreated().IsZero() { if m.GetCreated().IsZero() {
m.RefreshCreated() m.RefreshCreated()
} }
@ -213,7 +216,7 @@ func (dao *Dao) create(m models.Model) error {
} }
} }
// clears the internal isNewFlag // clears the "new" model flag
m.UnmarkAsNew() m.UnmarkAsNew()
if dao.AfterCreateFunc != nil { if dao.AfterCreateFunc != nil {

View File

@ -6,6 +6,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema" "github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/resolvers" "github.com/pocketbase/pocketbase/resolvers"
@ -14,12 +15,12 @@ import (
var collectionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`) var collectionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`)
// CollectionUpsert defines a collection upsert (create/update) form. // CollectionUpsert specifies a [models.Collection] upsert (create/update) form.
type CollectionUpsert struct { type CollectionUpsert struct {
app core.App config CollectionUpsertConfig
collection *models.Collection collection *models.Collection
isCreate bool
Id string `form:"id" json:"id"`
Name string `form:"name" json:"name"` Name string `form:"name" json:"name"`
System bool `form:"system" json:"system"` System bool `form:"system" json:"system"`
Schema schema.Schema `form:"schema" json:"schema"` Schema schema.Schema `form:"schema" json:"schema"`
@ -30,25 +31,48 @@ type CollectionUpsert struct {
DeleteRule *string `form:"deleteRule" json:"deleteRule"` DeleteRule *string `form:"deleteRule" json:"deleteRule"`
} }
// NewCollectionUpsert creates new collection upsert form for the provided Collection model // CollectionUpsertConfig is the [CollectionUpsert] factory initializer config.
// (pass an empty Collection model instance (`&models.Collection{}`) for create). //
// NB! Dao is a required struct member.
type CollectionUpsertConfig struct {
Dao *daos.Dao
}
// NewCollectionUpsert creates a new [CollectionUpsert] form with initializer
// config created from the provided [core.App] and [models.Collection] instances.
//
// This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use
// [NewCollectionUpsertWithConfig] with Dao configured to your txDao.
func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert { func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert {
form := &CollectionUpsert{ form := NewCollectionUpsertWithConfig(CollectionUpsertConfig{
app: app, Dao: app.Dao(),
collection: collection, }, collection)
isCreate: collection.IsNew(),
return form
}
// NewCollectionUpsertWithConfig creates a new [CollectionUpsert] form
// with the provided config and [models.Collection] instance or panics on invalid configuration
// (for create you could pass a pointer to an empty Collection instance - `&models.Collection{}`).
func NewCollectionUpsertWithConfig(config CollectionUpsertConfig, collection *models.Collection) *CollectionUpsert {
form := &CollectionUpsert{config: config}
if form.config.Dao == nil || collection == nil {
panic("Invalid initializer config or nil upsert model.")
} }
// load defaults // load defaults
form.Name = collection.Name form.Id = form.collection.Id
form.System = collection.System form.Name = form.collection.Name
form.ListRule = collection.ListRule form.System = form.collection.System
form.ViewRule = collection.ViewRule form.ListRule = form.collection.ListRule
form.CreateRule = collection.CreateRule form.ViewRule = form.collection.ViewRule
form.UpdateRule = collection.UpdateRule form.CreateRule = form.collection.CreateRule
form.DeleteRule = collection.DeleteRule form.UpdateRule = form.collection.UpdateRule
form.DeleteRule = form.collection.DeleteRule
clone, _ := collection.Schema.Clone() clone, _ := form.collection.Schema.Clone()
if clone != nil { if clone != nil {
form.Schema = *clone form.Schema = *clone
} else { } else {
@ -61,6 +85,10 @@ func NewCollectionUpsert(app core.App, collection *models.Collection) *Collectio
// Validate makes the form validatable by implementing [validation.Validatable] interface. // Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *CollectionUpsert) Validate() error { func (form *CollectionUpsert) Validate() error {
return validation.ValidateStruct(form, return validation.ValidateStruct(form,
validation.Field(
&form.Id,
validation.Length(models.DefaultIdLength, models.DefaultIdLength),
),
validation.Field( validation.Field(
&form.System, &form.System,
validation.By(form.ensureNoSystemFlagChange), validation.By(form.ensureNoSystemFlagChange),
@ -90,11 +118,11 @@ func (form *CollectionUpsert) Validate() error {
func (form *CollectionUpsert) checkUniqueName(value any) error { func (form *CollectionUpsert) checkUniqueName(value any) error {
v, _ := value.(string) v, _ := value.(string)
if !form.app.Dao().IsCollectionNameUnique(v, form.collection.Id) { if !form.config.Dao.IsCollectionNameUnique(v, form.collection.Id) {
return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).")
} }
if (form.isCreate || !strings.EqualFold(v, form.collection.Name)) && form.app.Dao().HasTable(v) { if (form.collection.IsNew() || !strings.EqualFold(v, form.collection.Name)) && form.config.Dao.HasTable(v) {
return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.") return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.")
} }
@ -104,7 +132,7 @@ func (form *CollectionUpsert) checkUniqueName(value any) error {
func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error { func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error {
v, _ := value.(string) v, _ := value.(string)
if form.isCreate || !form.collection.System || v == form.collection.Name { if form.collection.IsNew() || !form.collection.System || v == form.collection.Name {
return nil return nil
} }
@ -114,7 +142,7 @@ func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error {
func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error { func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error {
v, _ := value.(bool) v, _ := value.(bool)
if form.isCreate || v == form.collection.System { if form.collection.IsNew() || v == form.collection.System {
return nil return nil
} }
@ -161,7 +189,7 @@ func (form *CollectionUpsert) checkRule(value any) error {
} }
dummy := &models.Collection{Schema: form.Schema} dummy := &models.Collection{Schema: form.Schema}
r := resolvers.NewRecordFieldResolver(form.app.Dao(), dummy, nil) r := resolvers.NewRecordFieldResolver(form.config.Dao, dummy, nil)
_, err := search.FilterData(*v).BuildExpr(r) _, err := search.FilterData(*v).BuildExpr(r)
if err != nil { if err != nil {
@ -182,13 +210,19 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
return err return err
} }
// system flag can be set only for create if form.collection.IsNew() {
if form.isCreate { // system flag can be set only on create
form.collection.System = form.System form.collection.System = form.System
// custom insertion id can be set only on create
if form.Id != "" {
form.collection.MarkAsNew()
form.collection.SetId(form.Id)
}
} }
// system collections cannot be renamed // system collections cannot be renamed
if form.isCreate || !form.collection.System { if form.collection.IsNew() || !form.collection.System {
form.collection.Name = form.Name form.collection.Name = form.Name
} }
@ -200,6 +234,6 @@ func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error {
form.collection.DeleteRule = form.DeleteRule form.collection.DeleteRule = form.DeleteRule
return runInterceptors(func() error { return runInterceptors(func() error {
return form.app.Dao().SaveCollection(form.collection) return form.config.Dao.SaveCollection(form.collection)
}, interceptors...) }, interceptors...)
} }

View File

@ -11,24 +11,48 @@ import (
"github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/models"
) )
// CollectionsImport defines a bulk collections import form. // CollectionsImport specifies a form model to bulk import
// (create, replace and delete) collections from a user provided list.
type CollectionsImport struct { type CollectionsImport struct {
app core.App config CollectionsImportConfig
Collections []*models.Collection `form:"collections" json:"collections"` Collections []*models.Collection `form:"collections" json:"collections"`
DeleteOthers bool `form:"deleteOthers" json:"deleteOthers"` DeleteOthers bool `form:"deleteOthers" json:"deleteOthers"`
} }
// NewCollectionsImport bulk imports (create, replace and delete) // CollectionsImportConfig is the [CollectionsImport] factory initializer config.
// a user provided list with collections data. //
func NewCollectionsImport(app core.App) *CollectionsImport { // NB! Dao is a required struct member.
form := &CollectionsImport{ type CollectionsImportConfig struct {
app: app, Dao *daos.Dao
IsDebug bool
}
// NewCollectionsImportWithConfig creates a new [CollectionsImport]
// form with the provided config or panics on invalid configuration.
func NewCollectionsImportWithConfig(config CollectionsImportConfig) *CollectionsImport {
form := &CollectionsImport{config: config}
if form.config.Dao == nil {
panic("Invalid initializer config.")
} }
return form return form
} }
// NewCollectionsImport creates a new [CollectionsImport] form with
// initializer config created from the provided [core.App] instance.
//
// This factory method is used primarily for convenience (and backward compatibility).
// If you want to submit the form as part of another transaction, use
// [NewCollectionsImportWithConfig] with Dao configured to your txDao.
func NewCollectionsImport(app core.App) *CollectionsImport {
return NewCollectionsImportWithConfig(CollectionsImportConfig{
Dao: app.Dao(),
IsDebug: app.IsDebug(),
})
}
// Validate makes the form validatable by implementing [validation.Validatable] interface. // Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *CollectionsImport) Validate() error { func (form *CollectionsImport) Validate() error {
return validation.ValidateStruct(form, return validation.ValidateStruct(form,
@ -49,12 +73,12 @@ func (form *CollectionsImport) Submit() error {
return err return err
} }
// @todo validate id length in the form return form.config.Dao.RunInTransaction(func(txDao *daos.Dao) error {
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
oldCollections := []*models.Collection{} oldCollections := []*models.Collection{}
if err := txDao.CollectionQuery().All(&oldCollections); err != nil { if err := txDao.CollectionQuery().All(&oldCollections); err != nil {
return err return err
} }
mappedOldCollections := make(map[string]*models.Collection, len(oldCollections)) mappedOldCollections := make(map[string]*models.Collection, len(oldCollections))
for _, old := range oldCollections { for _, old := range oldCollections {
mappedOldCollections[old.GetId()] = old mappedOldCollections[old.GetId()] = old
@ -71,7 +95,7 @@ func (form *CollectionsImport) Submit() error {
if mappedFormCollections[old.GetId()] == nil { if mappedFormCollections[old.GetId()] == nil {
// delete the collection // delete the collection
if err := txDao.DeleteCollection(old); err != nil { if err := txDao.DeleteCollection(old); err != nil {
if form.app.IsDebug() { if form.config.IsDebug {
log.Println("[CollectionsImport] DeleteOthers failure", old.Name, err) log.Println("[CollectionsImport] DeleteOthers failure", old.Name, err)
} }
return validation.Errors{"collections": validation.NewError( return validation.Errors{"collections": validation.NewError(
@ -79,6 +103,8 @@ func (form *CollectionsImport) Submit() error {
fmt.Sprintf("Failed to delete collection %q (%s). Make sure that the collection is not system or referenced by other collections.", old.Name, old.Id), fmt.Sprintf("Failed to delete collection %q (%s). Make sure that the collection is not system or referenced by other collections.", old.Name, old.Id),
)} )}
} }
delete(mappedOldCollections, old.GetId())
} }
} }
} }
@ -91,7 +117,7 @@ func (form *CollectionsImport) Submit() error {
} }
if err := txDao.Save(collection); err != nil { if err := txDao.Save(collection); err != nil {
if form.app.IsDebug() { if form.config.IsDebug {
log.Println("[CollectionsImport] Save failure", collection.Name, err) log.Println("[CollectionsImport] Save failure", collection.Name, err)
} }
return validation.Errors{"collections": validation.NewError( return validation.Errors{"collections": validation.NewError(
@ -112,10 +138,13 @@ func (form *CollectionsImport) Submit() error {
for _, collection := range refreshedCollections { for _, collection := range refreshedCollections {
upsertModel := mappedOldCollections[collection.GetId()] upsertModel := mappedOldCollections[collection.GetId()]
if upsertModel == nil { if upsertModel == nil {
upsertModel = &models.Collection{} upsertModel = collection
} }
upsertForm := NewCollectionUpsert(form.app, upsertModel) upsertForm := NewCollectionUpsertWithConfig(CollectionUpsertConfig{
Dao: txDao,
}, upsertModel)
// load form fields with the refreshed collection state // load form fields with the refreshed collection state
upsertForm.Id = collection.Id
upsertForm.Name = collection.Name upsertForm.Name = collection.Name
upsertForm.System = collection.System upsertForm.System = collection.System
upsertForm.ListRule = collection.ListRule upsertForm.ListRule = collection.ListRule
@ -125,10 +154,6 @@ func (form *CollectionsImport) Submit() error {
upsertForm.DeleteRule = collection.DeleteRule upsertForm.DeleteRule = collection.DeleteRule
upsertForm.Schema = collection.Schema upsertForm.Schema = collection.Schema
if err := upsertForm.Validate(); err != nil { if err := upsertForm.Validate(); err != nil {
if form.app.IsDebug() {
log.Println("[CollectionsImport] Validate failure", collection.Name, err)
}
// serialize the validation error(s) // serialize the validation error(s)
serializedErr, _ := json.Marshal(err) serializedErr, _ := json.Marshal(err)
@ -143,7 +168,7 @@ func (form *CollectionsImport) Submit() error {
for _, collection := range form.Collections { for _, collection := range form.Collections {
oldCollection := mappedOldCollections[collection.GetId()] oldCollection := mappedOldCollections[collection.GetId()]
if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil { if err := txDao.SyncRecordTableSchema(collection, oldCollection); err != nil {
if form.app.IsDebug() { if form.config.IsDebug {
log.Println("[CollectionsImport] Records table sync failure", collection.Name, err) log.Println("[CollectionsImport] Records table sync failure", collection.Name, err)
} }
return validation.Errors{"collections": validation.NewError( return validation.Errors{"collections": validation.NewError(

View File

@ -9,6 +9,9 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// DefaultIdLength is the default length of the generated model id.
const DefaultIdLength = 15
// ColumnValueMapper defines an interface for custom db model data serialization. // ColumnValueMapper defines an interface for custom db model data serialization.
type ColumnValueMapper interface { type ColumnValueMapper interface {
// ColumnValueMap returns the data to be used when persisting the model. // ColumnValueMap returns the data to be used when persisting the model.
@ -96,7 +99,7 @@ func (m *BaseModel) GetUpdated() types.DateTime {
// //
// The generated id is a cryptographically random 15 characters length string. // The generated id is a cryptographically random 15 characters length string.
func (m *BaseModel) RefreshId() { func (m *BaseModel) RefreshId() {
m.Id = security.RandomString(15) m.Id = security.RandomString(DefaultIdLength)
} }
// RefreshCreated updates the model's Created field with the current datetime. // RefreshCreated updates the model's Created field with the current datetime.

View File

@ -14,7 +14,6 @@
"devDependencies": { "devDependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0", "@codemirror/commands": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.2",
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.0.0", "@codemirror/legacy-modes": "^6.0.0",
"@codemirror/search": "^6.0.0", "@codemirror/search": "^6.0.0",

View File

@ -101,7 +101,8 @@
} }
confirm(`Do you really want to delete the selected admin?`, () => { confirm(`Do you really want to delete the selected admin?`, () => {
return ApiClient.admins.delete(admin.id) return ApiClient.admins
.delete(admin.id)
.then(() => { .then(() => {
confirmClose = false; confirmClose = false;
hide(); hide();
@ -190,7 +191,7 @@
{#if admin.isNew || changePasswordToggle} {#if admin.isNew || changePasswordToggle}
<div class="col-12"> <div class="col-12">
<div class="grid" transition:slide={{ duration: 150 }}> <div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-sm-6"> <div class="col-sm-6">
<Field class="form-field required" name="password" let:uniqueId> <Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}> <label for={uniqueId}>

View File

@ -36,7 +36,8 @@
export async function load() { export async function load() {
isLoading = true; isLoading = true;
return ApiClient.logs.getRequestsStats({ return ApiClient.logs
.getRequestsStats({
filter: [presets, filter].filter(Boolean).join("&&"), filter: [presets, filter].filter(Boolean).join("&&"),
}) })
.then((result) => { .then((result) => {
@ -147,7 +148,7 @@
<div class="chart-wrapper" class:loading={isLoading}> <div class="chart-wrapper" class:loading={isLoading}>
{#if isLoading} {#if isLoading}
<div class="chart-loader loader" transition:scale={{ duration: 150 }} /> <div class="chart-loader loader" transition:scale|local={{ duration: 150 }} />
{/if} {/if}
<canvas bind:this={chartCanvas} class="chart-canvas" style="height: 250px; width: 100%;" /> <canvas bind:this={chartCanvas} class="chart-canvas" style="height: 250px; width: 100%;" />
</div> </div>

View File

@ -1,6 +1,7 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import ApiClient from "@/utils/ApiClient"; import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import OverlayPanel from "@/components/base/OverlayPanel.svelte"; import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import { addSuccessToast } from "@/stores/toasts"; import { addSuccessToast } from "@/stores/toasts";
@ -9,8 +10,13 @@
let panel; let panel;
let oldCollections = []; let oldCollections = [];
let newCollections = []; let newCollections = [];
let changes = [];
let isImporting = false; let isImporting = false;
$: if (Array.isArray(oldCollections) && Array.isArray(newCollections)) {
loadChanges();
}
export function show(a, b) { export function show(a, b) {
oldCollections = a; oldCollections = a;
newCollections = b; newCollections = b;
@ -22,6 +28,32 @@
return panel?.hide(); return panel?.hide();
} }
function loadChanges() {
changes = [];
// add deleted and modified collections
for (const oldCollection of oldCollections) {
const newCollection = CommonHelper.findByKey(newCollections, "id", oldCollection.id) || null;
if (!newCollection?.id || JSON.stringify(oldCollection) != JSON.stringify(newCollection)) {
changes.push({
old: oldCollection,
new: newCollection,
});
}
}
// add only new collections
for (const newCollection of newCollections) {
const oldCollection = CommonHelper.findByKey(oldCollections, "id", newCollection.id) || null;
if (!oldCollection?.id) {
changes.push({
old: oldCollection,
new: newCollection,
});
}
}
}
function diffsToHtml(diffs, ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) { function diffsToHtml(diffs, ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) {
const html = []; const html = [];
const pattern_amp = /&/g; const pattern_amp = /&/g;
@ -58,11 +90,11 @@
return html.join(""); return html.join("");
} }
function diff(ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) { function diff(obj1, obj2, ops = [window.DIFF_INSERT, window.DIFF_DELETE, window.DIFF_EQUAL]) {
const dmp = new diff_match_patch(); const dmp = new diff_match_patch();
const lines = dmp.diff_linesToChars_( const lines = dmp.diff_linesToChars_(
JSON.stringify(oldCollections, null, 4), obj1 ? JSON.stringify(obj1, null, 4) : "",
JSON.stringify(newCollections, null, 4) obj2 ? JSON.stringify(obj2, null, 4) : ""
); );
const diffs = dmp.diff_main(lines.chars1, lines.chars2, false); const diffs = dmp.diff_main(lines.chars1, lines.chars2, false);
@ -80,7 +112,7 @@
try { try {
await ApiClient.collections.import(newCollections); await ApiClient.collections.import(newCollections);
addSuccessToast("Successfully imported the provided schema."); addSuccessToast("Successfully imported the provided collections.");
dispatch("submit"); dispatch("submit");
} catch (err) { } catch (err) {
ApiClient.errorResponseHandler(err); ApiClient.errorResponseHandler(err);
@ -92,27 +124,58 @@
} }
</script> </script>
<OverlayPanel bind:this={panel} class="full-width-popup import-popup" popup on:show on:hide> <OverlayPanel
bind:this={panel}
class="full-width-popup import-popup"
overlayClose={false}
popup
on:show
on:hide
>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<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 m-b-base"> <div class="grid grid-sm m-b-sm">
<div class="col-6"> {#each changes as pair (pair.old?.id + pair.new?.id)}
<div class="section-title">Old schema</div> <div class="col-12">
<code class="code-block">{@html diff([window.DIFF_DELETE, window.DIFF_EQUAL])}</code> <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> </div>
<div class="col-6"> {/if}
<div class="section-title">New schema</div>
<code class="code-block">{@html diff([window.DIFF_INSERT, window.DIFF_EQUAL])}</code>
</div> </div>
</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])}
</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])}
</code>
</div>
{/each}
</div>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<button type="button" class="btn btn-secondary" on:click={hide}>Close</button> <button type="button" class="btn btn-secondary" on:click={hide}>Close</button>
<button <button
type="button" type="button"
class="btn btn-expanded m-l-auto" class="btn btn-expanded"
class:btn-loading={isImporting} class:btn-loading={isImporting}
on:click={() => submitImport()} on:click={() => submitImport()}
> >

View File

@ -14,19 +14,19 @@
let fileInput; let fileInput;
let importPopup; let importPopup;
let schema = ""; let schemas = "";
let isLoadingFile = false; let isLoadingFile = false;
let newCollections = []; let newCollections = [];
let oldCollections = []; let oldCollections = [];
let collectionsToModify = []; let collectionsToModify = [];
let isLoadingOldCollections = false; let isLoadingOldCollections = false;
$: if (typeof schema !== "undefined") { $: if (typeof schemas !== "undefined") {
loadNewCollections(schema); loadNewCollections(schemas);
} }
$: isValid = $: isValid =
!!schema && !!schemas &&
newCollections.length && newCollections.length &&
newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length; newCollections.length === newCollections.filter((item) => !!item.id && !!item.name).length;
@ -43,7 +43,7 @@
} }
$: hasChanges = $: hasChanges =
!!schema && (collectionsToDelete.length || collectionsToAdd.length || collectionsToModify.length); !!schemas && (collectionsToDelete.length || collectionsToAdd.length || collectionsToModify.length);
$: canImport = !isLoadingOldCollections && isValid && hasChanges; $: canImport = !isLoadingOldCollections && isValid && hasChanges;
@ -95,7 +95,7 @@
newCollections = []; newCollections = [];
try { try {
newCollections = JSON.parse(schema); newCollections = JSON.parse(schemas);
} catch (_) {} } catch (_) {}
if (!Array.isArray(newCollections)) { if (!Array.isArray(newCollections)) {
@ -120,12 +120,12 @@
isLoadingFile = false; isLoadingFile = false;
fileInput.value = ""; // reset fileInput.value = ""; // reset
schema = event.target.result; schemas = event.target.result;
await tick(); await tick();
if (!newCollections.length) { if (!newCollections.length) {
addErrorToast("Invalid collections schema."); addErrorToast("Invalid collections list.");
clear(); clear();
} }
}; };
@ -142,7 +142,7 @@
} }
function clear() { function clear() {
schema = ""; schemas = "";
fileInput.value = ""; fileInput.value = "";
setErrors({}); setErrors({});
} }
@ -177,7 +177,7 @@
/> />
<p> <p>
Paste below the collections schema you want to import or Paste below the collections you want to import or
<button <button
class="btn btn-outline btn-sm m-l-5" class="btn btn-outline btn-sm m-l-5"
class:btn-loading={isLoadingFile} class:btn-loading={isLoadingFile}
@ -191,18 +191,18 @@
</div> </div>
<Field class="form-field {!isValid ? 'field-error' : ''}" name="collections" let:uniqueId> <Field class="form-field {!isValid ? 'field-error' : ''}" name="collections" let:uniqueId>
<label for={uniqueId} class="p-b-10">Collections schema</label> <label for={uniqueId} class="p-b-10">Collections</label>
<textarea <textarea
id={uniqueId} id={uniqueId}
class="code" class="code"
spellcheck="false" spellcheck="false"
rows="15" rows="15"
required required
bind:value={schema} bind:value={schemas}
/> />
{#if !!schema && !isValid} {#if !!schemas && !isValid}
<div class="help-block help-block-error">Invalid collections schema.</div> <div class="help-block help-block-error">Invalid collections schemas.</div>
{/if} {/if}
</Field> </Field>
@ -212,7 +212,7 @@
<i class="ri-information-line" /> <i class="ri-information-line" />
</div> </div>
<div class="content"> <div class="content">
<string>Your collections schema is already up-to-date!</string> <string>Your collections structure is already up-to-date!</string>
</div> </div>
</div> </div>
{/if} {/if}
@ -234,17 +234,17 @@
{/if} {/if}
{#if collectionsToModify.length} {#if collectionsToModify.length}
{#each collectionsToModify as entry (entry.old.id + entry.new.id)} {#each collectionsToModify 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">Modified</span>
<strong> <strong>
{#if entry.old.name !== entry.new.name} {#if pair.old.name !== pair.new.name}
<span class="txt-strikethrough txt-hint">{entry.old.name}</span> - <span class="txt-strikethrough txt-hint">{pair.old.name}</span> -
{/if} {/if}
{entry.new.name} {pair.new.name}
</strong> </strong>
{#if entry.new.id} {#if pair.new.id}
<small class="txt-hint">({entry.new.id})</small> <small class="txt-hint">({pair.new.id})</small>
{/if} {/if}
</div> </div>
{/each} {/each}
@ -265,7 +265,7 @@
{/if} {/if}
<div class="flex m-t-base"> <div class="flex m-t-base">
{#if !!schema} {#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()}>
<span class="txt">Clear</span> <span class="txt">Clear</span>
</button> </button>

View File

@ -98,7 +98,8 @@
} }
confirm(`Do you really want to delete the selected user?`, () => { confirm(`Do you really want to delete the selected user?`, () => {
return ApiClient.users.delete(user.id) return ApiClient.users
.delete(user.id)
.then(() => { .then(() => {
confirmClose = false; confirmClose = false;
hide(); hide();
@ -112,7 +113,8 @@
} }
function sendVerificationEmail(notify = true) { function sendVerificationEmail(notify = true) {
return ApiClient.users.requestVerification(user.isNew ? email : user.email) return ApiClient.users
.requestVerification(user.isNew ? email : user.email)
.then(() => { .then(() => {
confirmClose = false; confirmClose = false;
hide(); hide();
@ -182,7 +184,7 @@
{#if user.isNew || changePasswordToggle} {#if user.isNew || changePasswordToggle}
<div class="col-12"> <div class="col-12">
<div class="grid" transition:slide={{ duration: 150 }}> <div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-sm-6"> <div class="col-sm-6">
<Field class="form-field required" name="password" let:uniqueId> <Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}> <label for={uniqueId}>