updated CollectionsImport and CollectionUpsert forms
This commit is contained in:
parent
4e58e7ad6a
commit
5fb45a1864
|
@ -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 {
|
||||||
|
|
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
Loading…
Reference in New Issue