diff --git a/apis/collection.go b/apis/collection.go index cff3afe0..e06ec81a 100644 --- a/apis/collection.go +++ b/apis/collection.go @@ -169,19 +169,37 @@ func (api *collectionApi) delete(c echo.Context) error { return handlerErr } -// @todo add event func (api *collectionApi) bulkImport(c echo.Context) error { form := forms.NewCollectionsImport(api.app) - // load request + // load request data if err := c.Bind(form); err != nil { return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } - submitErr := form.Submit() - if submitErr != nil { - return rest.NewBadRequestError("Failed to import the submitted collections.", submitErr) + event := &core.CollectionsImportEvent{ + HttpContext: c, + Collections: form.Collections, } - return c.NoContent(http.StatusNoContent) + // import collections + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnCollectionsBeforeImportRequest().Trigger(event, func(e *core.CollectionsImportEvent) error { + form.Collections = e.Collections // ensures that the form always has the latest changes + + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to import the submitted collections.", err) + } + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + } + }) + + if submitErr == nil { + api.app.OnCollectionsAfterImportRequest().Trigger(event) + } + + return submitErr } diff --git a/apis/collection_test.go b/apis/collection_test.go index 79d8a63c..1f75aacb 100644 --- a/apis/collection_test.go +++ b/apis/collection_test.go @@ -440,3 +440,57 @@ func TestCollectionUpdate(t *testing.T) { scenario.Test(t) } } + +func TestCollectionImport(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPut, + Url: "/api/collections/import", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPut, + Url: "/api/collections/import", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + empty collections", + Method: http.MethodPut, + Url: "/api/collections/import", + Body: strings.NewReader(`{"collections":[]}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"collections":{"code":"validation_required"`, + }, + }, + { + Name: "authorized as admin + deleting collections", + Method: http.MethodPut, + Url: "/api/collections/import", + Body: strings.NewReader(`{"collections":[]}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"collections":{"code":"validation_required"`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/core/app.go b/core/app.go index f89ad853..c19c06b6 100644 --- a/core/app.go +++ b/core/app.go @@ -119,7 +119,7 @@ type App interface { // sending a password reset email to an admin. // // Could be used to send your own custom email template if - // hook.StopPropagation is returned in one of its listeners. + // [hook.StopPropagation] is returned in one of its listeners. OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] // OnMailerAfterAdminResetPasswordSend hook is triggered after @@ -130,7 +130,7 @@ type App interface { // sending a password reset email to a user. // // Could be used to send your own custom email template if - // hook.StopPropagation is returned in one of its listeners. + // [hook.StopPropagation] is returned in one of its listeners. OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent] // OnMailerAfterUserResetPasswordSend hook is triggered after @@ -141,7 +141,7 @@ type App interface { // sending a verification email to a user. // // Could be used to send your own custom email template if - // hook.StopPropagation is returned in one of its listeners. + // [hook.StopPropagation] is returned in one of its listeners. OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent] // OnMailerAfterUserVerificationSend hook is triggered after a user @@ -152,7 +152,7 @@ type App interface { // sending a confirmation new address email to a a user. // // Could be used to send your own custom email template if - // hook.StopPropagation is returned in one of its listeners. + // [hook.StopPropagation] is returned in one of its listeners. OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent] // OnMailerAfterUserChangeEmailSend hook is triggered after a user @@ -192,7 +192,7 @@ type App interface { // // Could be used to additionally validate the request data or // implement completely different persistence behavior - // (returning hook.StopPropagation). + // (returning [hook.StopPropagation]). OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] // OnSettingsAfterUpdateRequest hook is triggered after each @@ -227,7 +227,7 @@ type App interface { // Admin create request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] // OnAdminAfterCreateRequest hook is triggered after each @@ -238,7 +238,7 @@ type App interface { // Admin update request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] // OnAdminAfterUpdateRequest hook is triggered after each @@ -249,7 +249,7 @@ type App interface { // Admin delete request (after model load and before actual deletion). // // Could be used to additionally validate the request data or implement - // completely different delete behavior (returning hook.StopPropagation). + // completely different delete behavior (returning [hook.StopPropagation]). OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] // OnAdminAfterDeleteRequest hook is triggered after each @@ -281,7 +281,7 @@ type App interface { // create request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent] // OnUserAfterCreateRequest hook is triggered after each @@ -292,7 +292,7 @@ type App interface { // update request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent] // OnUserAfterUpdateRequest hook is triggered after each @@ -303,7 +303,7 @@ type App interface { // delete request (after model load and before actual deletion). // // Could be used to additionally validate the request data or implement - // completely different delete behavior (returning hook.StopPropagation). + // completely different delete behavior (returning [hook.StopPropagation]). OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent] // OnUserAfterDeleteRequest hook is triggered after each @@ -346,7 +346,7 @@ type App interface { // create request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent] // OnRecordAfterCreateRequest hook is triggered after each @@ -357,7 +357,7 @@ type App interface { // update request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent] // OnRecordAfterUpdateRequest hook is triggered after each @@ -368,7 +368,7 @@ type App interface { // delete request (after model load and before actual deletion). // // Could be used to additionally validate the request data or implement - // completely different delete behavior (returning hook.StopPropagation). + // completely different delete behavior (returning [hook.StopPropagation]). OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent] // OnRecordAfterDeleteRequest hook is triggered after each @@ -393,7 +393,7 @@ type App interface { // create request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] // OnCollectionAfterCreateRequest hook is triggered after each @@ -404,7 +404,7 @@ type App interface { // update request (after request data load and before model persistence). // // Could be used to additionally validate the request data or implement - // completely different persistence behavior (returning hook.StopPropagation). + // completely different persistence behavior (returning [hook.StopPropagation]). OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] // OnCollectionAfterUpdateRequest hook is triggered after each @@ -415,10 +415,21 @@ type App interface { // Collection delete request (after model load and before actual deletion). // // Could be used to additionally validate the request data or implement - // completely different delete behavior (returning hook.StopPropagation). + // completely different delete behavior (returning [hook.StopPropagation]). OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] // OnCollectionAfterDeleteRequest hook is triggered after each // successful API Collection delete request. OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] + + // OnCollectionsBeforeImportRequest hook is triggered before each API + // collections import request (after request data load and before the actual import). + // + // Could be used to additionally validate the imported collections or + // to implement completely different import behavior (returning [hook.StopPropagation]). + OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent] + + // OnCollectionsAfterImportRequest hook is triggered after each + // successful API collections import request. + OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent] } diff --git a/core/base.go b/core/base.go index 8c31230b..d17d93fe 100644 --- a/core/base.go +++ b/core/base.go @@ -109,14 +109,16 @@ type BaseApp struct { onRecordAfterDeleteRequest *hook.Hook[*RecordDeleteEvent] // collection api event hooks - onCollectionsListRequest *hook.Hook[*CollectionsListEvent] - onCollectionViewRequest *hook.Hook[*CollectionViewEvent] - onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent] - onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent] - onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent] - onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent] - onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent] - onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent] + onCollectionsListRequest *hook.Hook[*CollectionsListEvent] + onCollectionViewRequest *hook.Hook[*CollectionViewEvent] + onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent] + onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent] + onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent] + onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent] + onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent] + onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent] + onCollectionsBeforeImportRequest *hook.Hook[*CollectionsImportEvent] + onCollectionsAfterImportRequest *hook.Hook[*CollectionsImportEvent] } // NewBaseApp creates and returns a new BaseApp instance @@ -201,14 +203,16 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp { onRecordAfterDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, // collection API event hooks - onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{}, - onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{}, - onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, - onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, - onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, - onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, - onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, - onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, + onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{}, + onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{}, + onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, + onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, + onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, + onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, + onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, + onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, + onCollectionsBeforeImportRequest: &hook.Hook[*CollectionsImportEvent]{}, + onCollectionsAfterImportRequest: &hook.Hook[*CollectionsImportEvent]{}, } app.registerDefaultHooks() @@ -687,6 +691,14 @@ func (app *BaseApp) OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDelet return app.onCollectionAfterDeleteRequest } +func (app *BaseApp) OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent] { + return app.onCollectionsBeforeImportRequest +} + +func (app *BaseApp) OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent] { + return app.onCollectionsAfterImportRequest +} + // ------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------- diff --git a/core/events.go b/core/events.go index 5c3a854e..1d9bd927 100644 --- a/core/events.go +++ b/core/events.go @@ -216,6 +216,11 @@ type CollectionDeleteEvent struct { Collection *models.Collection } +type CollectionsImportEvent struct { + HttpContext echo.Context + Collections []*models.Collection +} + // ------------------------------------------------------------------- // File API events data // ------------------------------------------------------------------- diff --git a/daos/base.go b/daos/base.go index 9c9c1e5a..b0cba86c 100644 --- a/daos/base.go +++ b/daos/base.go @@ -128,11 +128,11 @@ func (dao *Dao) Delete(m models.Model) error { // Save upserts (update or create if primary key is not set) the provided model. func (dao *Dao) Save(m models.Model) error { - if !m.IsNew() { - return dao.update(m) + if m.IsNew() { + return dao.create(m) } - return dao.create(m) + return dao.update(m) } func (dao *Dao) update(m models.Model) error { diff --git a/daos/base_test.go b/daos/base_test.go index 31d12fc9..3d89765e 100644 --- a/daos/base_test.go +++ b/daos/base_test.go @@ -158,6 +158,33 @@ func TestDaoSaveCreate(t *testing.T) { } } +func TestDaoSaveWithInsertId(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + model := &models.Admin{} + model.Id = "test" + model.Email = "test_new@example.com" + model.MarkAsNew() + if err := testApp.Dao().Save(model); err != nil { + t.Fatal(err) + } + + // refresh + model, _ = testApp.Dao().FindAdminById("test") + + if model == nil { + t.Fatal("Failed to find admin with id 'test'") + } + + expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"} + for _, h := range expectedHooks { + if v, ok := testApp.EventCalls[h]; !ok || v != 1 { + t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) + } + } +} + func TestDaoSaveUpdate(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() @@ -184,6 +211,61 @@ func TestDaoSaveUpdate(t *testing.T) { } } +type dummyColumnValueMapper struct { + models.Admin +} + +func (a *dummyColumnValueMapper) ColumnValueMap() map[string]any { + return map[string]any{ + "email": a.Email, + "passwordHash": a.PasswordHash, + "tokenKey": "custom_token_key", + } +} + +func TestDaoSaveWithColumnValueMapper(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + model := &dummyColumnValueMapper{} + model.Id = "test_mapped_id" // explicitly set an id + model.Email = "test_mapped_create@example.com" + model.TokenKey = "test_unmapped_token_key" // not used in the map + model.SetPassword("123456") + model.MarkAsNew() + if err := testApp.Dao().Save(model); err != nil { + t.Fatal(err) + } + + createdModel, _ := testApp.Dao().FindAdminById("test_mapped_id") + if createdModel == nil { + t.Fatal("[create] Failed to find model with id 'test_mapped_id'") + } + if createdModel.Email != model.Email { + t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email) + } + if createdModel.TokenKey != "custom_token_key" { + t.Fatalf("Expected model with tokenKey %q, got %q", "custom_token_key", createdModel.TokenKey) + } + + model.Email = "test_mapped_update@example.com" + model.Avatar = 9 // not mapped and expect to be ignored + if err := testApp.Dao().Save(model); err != nil { + t.Fatal(err) + } + + updatedModel, _ := testApp.Dao().FindAdminById("test_mapped_id") + if updatedModel == nil { + t.Fatal("[update] Failed to find model with id 'test_mapped_id'") + } + if updatedModel.Email != model.Email { + t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email) + } + if updatedModel.Avatar != 0 { + t.Fatalf("Expected model avatar 0, got %v", updatedModel.Avatar) + } +} + func TestDaoDelete(t *testing.T) { testApp, _ := tests.NewTestApp() defer testApp.Cleanup() diff --git a/forms/collections_import.go b/forms/collections_import.go index 7c3fe301..195ee93e 100644 --- a/forms/collections_import.go +++ b/forms/collections_import.go @@ -70,11 +70,18 @@ func (form *CollectionsImport) Validate() error { // // All operations are wrapped in a single transaction that are // rollbacked on the first encountered error. -func (form *CollectionsImport) Submit() error { +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } + return runInterceptors(form.submit, interceptors...) +} + +func (form *CollectionsImport) submit() error { return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error { oldCollections := []*models.Collection{} if err := txDao.CollectionQuery().All(&oldCollections); err != nil { diff --git a/models/base_test.go b/models/base_test.go index 9af48de0..c56e5284 100644 --- a/models/base_test.go +++ b/models/base_test.go @@ -34,25 +34,55 @@ func TestBaseModelHasId(t *testing.T) { } } -func TestBaseModelGetId(t *testing.T) { - m0 := models.BaseModel{} - if m0.GetId() != "" { - t.Fatalf("Expected zero id value, got %v", m0.GetId()) +func TestBaseModelId(t *testing.T) { + m := models.BaseModel{} + + if m.GetId() != "" { + t.Fatalf("Expected empty id value, got %v", m.GetId()) } - id := "abc" - m1 := models.BaseModel{Id: id} - if m1.GetId() != id { - t.Fatalf("Expected id %v, got %v", id, m1.GetId()) + m.SetId("test") + + if m.GetId() != "test" { + t.Fatalf("Expected %q id, got %v", "test", m.GetId()) + } + + m.RefreshId() + + if len(m.GetId()) != 15 { + t.Fatalf("Expected 15 chars id, got %v", m.GetId()) } } -func TestBaseModelRefreshId(t *testing.T) { - m := models.BaseModel{} - m.RefreshId() +func TestBaseModelIsNew(t *testing.T) { + m0 := models.BaseModel{} + m1 := models.BaseModel{Id: ""} + m2 := models.BaseModel{Id: "test"} + m3 := models.BaseModel{} + m3.MarkAsNew() + m4 := models.BaseModel{Id: "test"} + m4.MarkAsNew() + m5 := models.BaseModel{Id: "test"} + m5.MarkAsNew() + m5.UnmarkAsNew() - if m.GetId() == "" { - t.Fatalf("Expected nonempty id value, got %v", m.GetId()) + scenarios := []struct { + model models.BaseModel + expected bool + }{ + {m0, true}, + {m1, true}, + {m2, false}, + {m3, true}, + {m4, true}, + {m5, false}, + } + + for i, s := range scenarios { + result := s.model.IsNew() + if result != s.expected { + t.Errorf("(%d) Expected IsNew %v, got %v", i, s.expected, result) + } } } diff --git a/tests/app.go b/tests/app.go index 435ee1d8..41df9cc9 100644 --- a/tests/app.go +++ b/tests/app.go @@ -313,6 +313,16 @@ func NewTestApp() (*TestApp, error) { return nil }) + t.OnCollectionsBeforeImportRequest().Add(func(e *core.CollectionsImportEvent) error { + t.EventCalls["OnCollectionsBeforeImportRequest"]++ + return nil + }) + + t.OnCollectionsAfterImportRequest().Add(func(e *core.CollectionsImportEvent) error { + t.EventCalls["OnCollectionsAfterImportRequest"]++ + return nil + }) + t.OnAdminsListRequest().Add(func(e *core.AdminsListEvent) error { t.EventCalls["OnAdminsListRequest"]++ return nil diff --git a/ui/.env.development b/ui/.env.development index 657341af..b4615bf4 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1 +1 @@ -PB_BACKEND_URL = http://localhost:8090 +PB_BACKEND_URL = http://127.0.0.1:8090 diff --git a/ui/dist/index.html b/ui/dist/index.html index 12e2a416..26aca7c2 100644 --- a/ui/dist/index.html +++ b/ui/dist/index.html @@ -5,7 +5,7 @@ PocketBase diff --git a/ui/index.html b/ui/index.html index 07ed5596..7ec1e697 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,7 @@ PocketBase diff --git a/ui/package-lock.json b/ui/package-lock.json index 03a614c8..add0dfeb 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -938,9 +938,9 @@ } }, "node_modules/regexparam": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.0.tgz", - "integrity": "sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz", + "integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==", "dev": true, "engines": { "node": ">=8" @@ -1062,12 +1062,12 @@ } }, "node_modules/svelte-spa-router": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.2.0.tgz", - "integrity": "sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.3.0.tgz", + "integrity": "sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==", "dev": true, "dependencies": { - "regexparam": "2.0.0" + "regexparam": "2.0.1" }, "funding": { "url": "https://github.com/sponsors/ItalyPaleAle" @@ -1714,9 +1714,9 @@ } }, "regexparam": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.0.tgz", - "integrity": "sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz", + "integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==", "dev": true }, "resolve": { @@ -1797,12 +1797,12 @@ "requires": {} }, "svelte-spa-router": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.2.0.tgz", - "integrity": "sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.3.0.tgz", + "integrity": "sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==", "dev": true, "requires": { - "regexparam": "2.0.0" + "regexparam": "2.0.1" } }, "to-regex-range": { diff --git a/ui/public/libs/diff/diff.js b/ui/public/libs/diff/diff.js index 48036223..63ce154a 100644 --- a/ui/public/libs/diff/diff.js +++ b/ui/public/libs/diff/diff.js @@ -1,5 +1,8 @@ -// https://github.com/google/diff-match-patch -// https://github.com/google/diff-match-patch/blob/master/LICENSE +/** + * diff-match-patch + * https://github.com/google/diff-match-patch + * https://github.com/google/diff-match-patch/blob/master/LICENSE + */ var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]}; diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0, b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a}; diff --git a/ui/src/components/collections/docs/CreateApiDocs.svelte b/ui/src/components/collections/docs/CreateApiDocs.svelte index 72cef15a..2bf02a70 100644 --- a/ui/src/components/collections/docs/CreateApiDocs.svelte +++ b/ui/src/components/collections/docs/CreateApiDocs.svelte @@ -107,6 +107,22 @@ + + +
+ Optional + id +
+ + + String + + + 15 characters string to store as record ID. +
+ If not set, it will be auto generated. + + {#each collection?.schema as field (field.name)}