Compare commits

...
54 Commits
Author SHA1 Message Date
Gani Georgiev 44bf55097a updated changelogs 2026-04-27 09:30:32 +03:00
Gani Georgiev 338d672bee updated ui/dist 2026-04-27 09:04:38 +03:00
Gani Georgiev 5bd9d87bad reorder editor buttons to avoid dropdowns text wrapping 2026-04-27 08:47:14 +03:00
Gani Georgiev 6ba78d5218 updated gitlab userinfo doc reference 2026-04-27 08:03:20 +03:00
Gani Georgiev 260bd59c5b updated jstypes 2026-04-27 07:57:43 +03:00
Gani Georgiev 006566478a added explicit gitlab confirmed_at check 2026-04-27 07:55:51 +03:00
Gani Georgiev 419f335f5b various minor ui fixes 2026-04-27 01:13:08 +03:00
Gani Georgiev 326f150db2 added more tests for internal record hooks 2026-04-26 20:47:47 +03:00
Gani Georgiev 1c86addc4c [#7665] added BaseURL to the ghupdate plugin configuration 2026-04-26 20:47:02 +03:00
Gani Georgiev 494f47efb8 bumped go deps 2026-04-26 16:50:24 +03:00
Gani Georgiev 555a4f1a1e lowered the default mfa duration and reorganized internal record pre/post handling 2026-04-26 16:46:16 +03:00
Gani Georgiev 37b258810a updated gitea displayName 2026-04-26 14:23:23 +03:00
Gani Georgiev ca7cf1162f added App.DeleteAllExternalAuthsByRecord 2026-04-26 11:40:09 +03:00
Gani Georgiev dddb0a029f updated bitbucket,github and gitea oauth2 providers 2026-04-25 17:51:28 +03:00
Gani Georgiev 5d55fc18ee added dummy bcrypt check 2026-04-25 16:16:23 +03:00
Gani Georgiev 449e5af590 adjust dark text color 2026-04-25 11:41:09 +03:00
Gani Georgiev 1e460d3f96 updated changelog and rebuild ui/dist 2026-04-24 22:27:26 +03:00
Gani Georgiev b5030ddfa1 [#7664] fixed codeEditor not firing the change and input events on autocomplete selection 2026-04-24 22:08:58 +03:00
Gani Georgiev fbeb09c40b [#7660] added missing type:button attribute and replaced form tag with div to minimize future regressions 2026-04-24 15:42:47 +03:00
Gani Georgiev 69cdda4bf3 [#7659] fixed SMTP IPv6 format 2026-04-23 21:25:58 +03:00
Gani Georgiev e708f39e1b updated erd styles 2026-04-23 21:18:32 +03:00
Gani Georgiev 52eccb3aac fade non-focused erd tables 2026-04-23 17:43:15 +03:00
Gani Georgiev 1d593476b0 updated view autocomplete keywords 2026-04-23 17:32:07 +03:00
Gani Georgiev 8a04904de1 fixed godoc example typo 2026-04-23 16:57:26 +03:00
Gani Georgiev a3ac674f36 removed title loader to minimize layout jumps 2026-04-23 14:12:59 +03:00
Gani Georgiev ae7041a889 preload the record preview to minimize content jumps 2026-04-23 00:37:05 +03:00
Gani Georgiev 257f03e1fa removed lazy tinymce mount since the relation are now preloaded 2026-04-23 00:01:43 +03:00
Gani Georgiev 3566ba3729 exclude expand from the record draft 2026-04-22 23:15:46 +03:00
Gani Georgiev a6002c4622 optimized record upsert panel loading to minimize layout jumps 2026-04-22 23:02:56 +03:00
Gani Georgiev 2ddf161314 renamed list-group to list-content for consistency with the others 2026-04-22 17:59:11 +03:00
Gani Georgiev b15f358fc9 [#7655] added backups list scroll container 2026-04-22 17:22:22 +03:00
Gani Georgiev 83e44a7cfb added view query sample loading indicator 2026-04-22 16:04:57 +03:00
Gani Georgiev 866b8b8029 added missing name attribute, fixed initial collections page load routing params persistence and removed unnecessery sub label required mark 2026-04-22 15:42:17 +03:00
Gani Georgiev 857214e10d reordered number settings for consistency with the other fields 2026-04-22 14:46:19 +03:00
Gani Georgiev 592b13913f added min-height to the page tables and adjusted light surface colors 2026-04-22 07:00:36 +03:00
Gani Georgiev 84b50c4869 minor color and styles improvements 2026-04-21 17:40:11 +03:00
Gani Georgiev 3c33868ea8 [#7653] updated tinymce options to fix its dialogs position 2026-04-21 14:16:23 +03:00
Gani Georgiev 223ac7a64a enabled text wrapping for the API rule fileds 2026-04-21 11:52:45 +03:00
Gani Georgiev 0cee0662f6 fixed 0 total count on page back/forward navigation 2026-04-21 10:26:37 +03:00
Gani Georgiev bf1745fa13 fixed changelog typo 2026-04-20 17:40:11 +03:00
Gani Georgiev efc095f7d0 updated changelog and ui/dist 2026-04-20 17:26:00 +03:00
Gani Georgiev 14e7286840 updated color vars and hide Safari negative margin horizontal scroll 2026-04-20 16:51:20 +03:00
Gani Georgiev 4ace75b3d5 [#7650] workarounded Safari position-try-fallbacks freeze 2026-04-20 16:49:55 +03:00
Gani Georgiev d23963aaca updated dark complementary colors 2026-04-20 12:25:04 +03:00
Gani Georgiev d35a0d841c [#7649] renamed the stringify util and removed its unnecessery tags stripping 2026-04-20 11:48:44 +03:00
Gani Georgiev 1b18ab9bec [#7648] darken the surface colors a little bit more 2026-04-20 10:32:23 +03:00
Gani Georgiev 93e6ebfe49 [#7648] fixed firefox autoexpandable input and updated dark theme colors 2026-04-19 23:44:04 +03:00
Gani Georgiev c3a53cb183 fallback to 0 2026-04-19 15:36:38 +03:00
Gani Georgiev ba554b8470 [#7646] fixed number field min/max input value normalization 2026-04-19 15:31:33 +03:00
Gani Georgiev 61ce760e0f show collection name in the page title on initial load 2026-04-19 12:29:45 +03:00
Gani Georgiev 7b92b7c857 updated ui/dist 2026-04-19 12:20:37 +03:00
Gani Georgiev 8c127b2849 updated changelog 2026-04-19 11:54:57 +03:00
Gani Georgiev e6b8841421 allow to open collections page in new tab on middle click and minor flickering improvements 2026-04-19 11:53:24 +03:00
Gani Georgiev 862064e061 enable back npm build check 2026-04-19 09:45:41 +03:00
83 changed files with 4664 additions and 3528 deletions

No files matched your search

+2 -4
View File
@@ -30,13 +30,11 @@ jobs:
with:
go-version: '>=1.26.2'
# Temporary disable until the issue with npm ci is resolved.
#
# This step usually is not needed because the /ui/dist is pregenerated locally
# but its here to ensure that each release embeds the latest admin ui artifacts.
# If the artificats differs, a "dirty error" is thrown - https://goreleaser.com/errors/dirty/
# - name: Build Admin dashboard UI
# run: npm --prefix=./ui install && npm --prefix=./ui run build
- name: Build Admin dashboard UI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
# Temporary disable as the types can have random generated identifiers making it non-deterministic.
#
+55 -1
View File
@@ -1,6 +1,60 @@
## v0.37.4
- Added backups list scroll container ([#7655](https://github.com/pocketbase/pocketbase/issues/7655)).
- Optimized record upsert and preview modals data loading to minimize layout jumps.
- Fixed SMTP IPv6 network address format ([#7659](https://github.com/pocketbase/pocketbase/issues/7659)).
- Fixed autocomplete selection not properly updating the underlying input value ([#7664](https://github.com/pocketbase/pocketbase/issues/7664)).
- Added `ghupdate.BaseURL` config option ([#7665](https://github.com/pocketbase/pocketbase/issues/7665)).
- Added dummy bcrypt password check for the failure auth path to minimize enumeration timing attacks when registrations are disabled.
- Adjusted Bitbucket, GitHub, GitLab and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references.
_In case the userinfo data is not sufficient, some of the providers now send a separate list emails request in order to minimize eventual linking security issues caused by custom onpremise setups (e.g. Gitea/Forgejo allows skipping the email verification if an ENV variable is configured)._
- ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662); thanks @Alardiians for reporting it privately).
- Bumped Go and npm dependencies.
## v0.37.3
- Fixed total count load on page back/forward navigation.
- Fixed `editor` floating dialogs position when scrolling ([#7653](https://github.com/pocketbase/pocketbase/issues/7653)).
- Enabled text wrapping for the API rule fields.
- Added view query sample loading indicator.
- Other minor light UI contrast and styles improvements.
## v0.37.2
- Fixed autoexpandable input in Firefox ([#7648](https://github.com/pocketbase/pocketbase/discussions/7648)).
- Slightly adjusted the dark theme colors for better readability ([#7648](https://github.com/pocketbase/pocketbase/discussions/7648)).
- Removed unnecessary tags stripping from the displayed log attributes ([#7649](https://github.com/pocketbase/pocketbase/issues/7649)).
- Workarounded Safari freeze caused by a buggy CSS popover property ([#7650](https://github.com/pocketbase/pocketbase/issues/7650)).
## v0.37.1
- Minor UI bugfixes:
- Fixed `number` field input values normalization ([#7646](https://github.com/pocketbase/pocketbase/issues/7646)).
- Allow opening collections in new tab with middle click.
- Show collection name in the page title on initial load.
## v0.37.0
- New UI rewritten from scratch and with support for external customizations in mind.
- New UI rewritten from scratch and with support for external customization in mind.
> Note that as explained in [#7612](https://github.com/pocketbase/pocketbase/discussions/7612) the new UI kit and extensions APIs will intentionally remain undocumented until "Stage 2 completion" _(there no ETAs)_.
The new UI also introduced several other small improvements:
+8
View File
@@ -2,6 +2,14 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---
## v0.22.42
- (_Backported from v0.37.4_) Adjusted Bitbucket, GitHub, GitLab and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references.
_In case the userinfo data is not sufficient, some of the providers now send a separate list emails request in order to minimize eventual linking security issues caused by custom onpremise setups (e.g. Gitea/Forgejo allows skipping the email verification if an ENV variable is configured)._
- (_Backported from v0.37.4_) ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662)).
## v0.22.41
- (_Backported from v0.36.9_) Updated the Discord `AuthUser.Name` field to use `global_name`.
+2 -2
View File
@@ -85,8 +85,8 @@ func TestDefaultRateLimitMiddleware(t *testing.T) {
{"/norate", 0, false, 200},
{"/rate/a", 0, false, 200},
{"/rate/a", 700, false, 200}, // (fixed window check) wait enough to ensure that it can't fit more than 2 requests in 1s
{"/rate/a", 800, false, 200},
{"/rate/a", 800, false, 200}, // (fixed window check) wait enough to ensure that it can't fit more than 2 requests in 1s
{"/rate/a", 500, false, 200},
{"/rate/a", 800, false, 200},
{"/rate/a", 0, false, 200},
{"/rate/a", 0, false, 429},
+40 -1
View File
@@ -111,12 +111,51 @@ func TestRecordConfirmEmailChange(t *testing.T) {
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnRecordValidate": 1,
// unverified->verified external auths removal
"OnModelDelete": 2,
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
if user.Verified() {
t.Fatalf("Expected the user to be unverified before the confirmation")
}
// ensure that there is at least one pre-existing OAuth2 link
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) == 0 {
t.Fatal("Expected at least one external auths")
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
_, err := app.FindAuthRecordByEmail("users", "change@example.com")
user, err := app.FindAuthRecordByEmail("users", "change@example.com")
if err != nil {
t.Fatalf("Expected to find user with email %q, got error: %v", "change@example.com", err)
}
if !user.Verified() {
t.Fatalf("Expected the user to be verified after the confirmation")
}
// ensure that all pre-existing OAuth2 links are cleared
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) > 0 {
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
}
},
},
{
@@ -114,11 +114,18 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
// ---
"OnModelDelete": 2, // pre-existing OAuth2 links
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
@@ -151,6 +158,15 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
if !user.ValidatePassword("1234567!") {
t.Fatal("Password wasn't changed")
}
// ensure that all pre-existing OAuth2 links are cleared
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) > 0 {
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
}
},
},
{
@@ -221,6 +237,15 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
if !user.ValidatePassword("1234567!") {
t.Fatal("Password wasn't changed")
}
// ensure that all pre-existing OAuth2 were NOT deleted
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) != 2 {
t.Fatalf("Expected 2 external auths, found %d", len(externalAuths))
}
},
},
{
@@ -251,11 +276,20 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
t.Fatalf("Failed to fetch confirm password user: %v", err)
}
oldTokenKey := user.TokenKey()
// ensure that the user is already verified
user.SetVerified(true)
if err := app.Save(user); err != nil {
t.Fatalf("Failed to update user verified state")
}
// resave with the old token key since the verified change above
// would refresh it and will make the password token invalid
user.SetTokenKey(oldTokenKey)
if err = app.Save(user); err != nil {
t.Fatalf("Failed to restore original user tokenKey: %v", err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
_, err := app.FindAuthRecordByToken(
@@ -105,6 +105,51 @@ func TestRecordConfirmVerification(t *testing.T) {
"OnRecordValidate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// unverified->verified external auths removal
"OnModelDelete": 2,
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
if user.Verified() {
t.Fatalf("Expected the user to be unverified before the confirmation")
}
// ensure that there is at least one pre-existing OAuth2 link
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) == 0 {
t.Fatal("Expected at least one external auths")
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
if !user.Verified() {
t.Fatalf("Expected the user to be verified after the confirmation")
}
// ensure that all pre-existing OAuth2 links are cleared
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) > 0 {
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
}
},
},
{
+23 -6
View File
@@ -338,26 +338,43 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor
e.Auth.Id == e.Record.Id &&
e.Auth.Collection().Id == e.Record.Collection().Id
// set random password for users with unverified email
// (this is in case a malicious actor has registered previously with the user email)
if !isLoggedAuthRecord && e.Record.Email() != "" && !e.Record.Verified() {
e.Record.SetRandomPassword()
// prevent pre-hijacking with password auth
//
// reset the unverified user password in case the record was precreated by a malicious actor
if !isLoggedAuthRecord && !e.Record.Verified() {
needUpdate = true
e.Record.SetRandomPassword()
}
// prevent pre-hijacking with different OAuth2 provider
//
// delete all other previous OAuth2 record links for the cases
// when the user was precreated by malicious OAuth2 auth with custom payload data
//
// while this would be also done automatically on unverified -> verified upgrade,
// doing it manually here ensures that a single unverified record could have
// max 1 OAuth2 link to prevent further abuse when mixed with other auth flows
if !e.Record.Verified() {
err := txApp.DeleteAllExternalAuthsByRecord(e.Record)
if err != nil {
return err
}
optExternalAuth = nil // clear to allow recreate below
}
// update the existing auth record empty email if the data.OAuth2User has one
// (this is in case previously the auth record was created
// with an OAuth2 provider that didn't return an email address)
if e.Record.Email() == "" && e.OAuth2User.Email != "" {
e.Record.SetEmail(e.OAuth2User.Email)
needUpdate = true
e.Record.SetEmail(e.OAuth2User.Email)
}
// update the existing auth record verified state
// (only if the auth record doesn't have an email or the auth record email match with the one in data.OAuth2User)
if !e.Record.Verified() && (e.Record.Email() == "" || e.Record.Email() == e.OAuth2User.Email) {
e.Record.SetVerified(true)
needUpdate = true
e.Record.SetVerified(true)
}
if needUpdate {
+233 -13
View File
@@ -178,6 +178,20 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
t.Fatal(err)
}
// ensure that there is at least one other external auth different than test
// so that later we can verify that it was deleted
var hasAtLeastOneOtherEA = false
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
for _, rel := range externalAuths {
if rel.Id != ea.Id {
hasAtLeastOneOtherEA = true
break
}
}
if !hasAtLeastOneOtherEA {
t.Fatal("Expected at least one non-test external auth linked")
}
// test at least once that the correct request info context is properly loaded
app.OnRecordAuthRequest().BindFunc(func(e *core.RecordAuthRequestEvent) error {
info, err := e.RequestInfo()
@@ -213,12 +227,12 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordAuthRequest": 1,
"OnRecordEnrich": 1,
// ---
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelCreate": 2, // user + recreated external auth
"OnModelCreateExecute": 2,
"OnModelAfterCreateSuccess": 2,
"OnRecordCreate": 2,
"OnRecordCreateExecute": 2,
"OnRecordAfterCreateSuccess": 2,
// ---
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
@@ -227,8 +241,15 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// ---
"OnModelValidate": 2, // create + update
"OnRecordValidate": 2,
"OnModelDelete": 3, // pre-existing external auths
"OnModelDeleteExecute": 3,
"OnModelAfterDeleteSuccess": 3,
"OnRecordDelete": 3,
"OnRecordDeleteExecute": 3,
"OnRecordAfterDeleteSuccess": 3,
// ---
"OnModelValidate": 3, // user create/update + recreated external auth
"OnRecordValidate": 3,
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
@@ -248,6 +269,24 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
if len(devices) != 1 {
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
}
// ensure that other linked external auths have been deleted
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
if len(externalAuths) != 1 {
t.Fatalf("Expected only 1 external auth to remain, got %d", len(externalAuths))
}
if provider := externalAuths[0].Provider(); provider != "test" {
t.Fatalf("Expected %q external auth, got %q", "test", provider)
}
if providerId := externalAuths[0].ProviderId(); providerId != "test_id" {
t.Fatalf("Expected %q providerId, got %q", "test_id", providerId)
}
if recordRef := externalAuths[0].RecordRef(); recordRef != user.Id {
t.Fatalf("Expected %q recordRef, got %q", user.Id, recordRef)
}
if collectionRef := externalAuths[0].CollectionRef(); collectionRef != user.Collection().Id {
t.Fatalf("Expected %q collectionRef, got %q", user.Collection().Id, collectionRef)
}
},
},
{
@@ -343,7 +382,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
}
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected old password %q to be valid", "1234567890")
t.Fatalf("Expected old password %q to remain valid", "1234567890")
}
devices, err := app.FindAllAuthOriginsByRecord(user)
@@ -353,7 +392,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
},
},
{
Name: "link by email",
Name: "link by email (unverified user)",
Method: http.MethodPost,
URL: "/api/collections/users/auth-with-oauth2",
Body: strings.NewReader(`{
@@ -376,6 +415,20 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
t.Fatalf("Expected password %q to be valid", "1234567890")
}
// ensure that there is at least one other external auth different than test
// so that later we can verify that it was deleted
var hasAtLeastOneOtherEA = false
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
for _, rel := range externalAuths {
if rel.Provider() != "test" {
hasAtLeastOneOtherEA = true
break
}
}
if !hasAtLeastOneOtherEA {
t.Fatal("Expected at least one non-test external auth linked")
}
// register the test provider
auth.Providers["test"] = func() auth.Provider {
return &oauth2MockProvider{
@@ -432,6 +485,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// ---
"OnModelDelete": 2, // pre-existing external auths
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
// ---
"OnModelValidate": 3, // record + authOrigins + externalAuths
"OnRecordValidate": 3,
},
@@ -449,6 +509,145 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
if len(devices) != 1 {
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
}
// ensure that other linked external auths have been deleted
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
if len(externalAuths) != 1 {
t.Fatalf("Expected only 1 external auth to remain, got %d", len(externalAuths))
}
if provider := externalAuths[0].Provider(); provider != "test" {
t.Fatalf("Expected %q external auth, got %q", "test", provider)
}
if providerId := externalAuths[0].ProviderId(); providerId != "test_id" {
t.Fatalf("Expected %q providerId, got %q", "test_id", providerId)
}
if recordRef := externalAuths[0].RecordRef(); recordRef != user.Id {
t.Fatalf("Expected %q recordRef, got %q", user.Id, recordRef)
}
if collectionRef := externalAuths[0].CollectionRef(); collectionRef != user.Collection().Id {
t.Fatalf("Expected %q collectionRef, got %q", user.Collection().Id, collectionRef)
}
},
},
{
Name: "link by email (verified user)",
Method: http.MethodPost,
URL: "/api/collections/users/auth-with-oauth2",
Body: strings.NewReader(`{
"provider": "test",
"code":"123",
"redirectURL": "https://example.com"
}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
if err != nil {
t.Fatal(err)
}
if !user.Verified() {
t.Fatalf("Expected user %q to be verified", user.Email())
}
// ensure that the old password works
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected password %q to be valid", "1234567890")
}
// register the test provider
auth.Providers["test"] = func() auth.Provider {
return &oauth2MockProvider{
AuthUser: &auth.AuthUser{Id: "test_id", Email: "test3@example.com"},
Token: &oauth2.Token{AccessToken: "abc"},
}
}
// ensure that there is at least one other external auth different than test
// so that later we can verify that they are not deleted
var hasAtLeastOneOtherEA = false
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
for _, rel := range externalAuths {
if rel.Provider() != "test" {
hasAtLeastOneOtherEA = true
break
}
}
if !hasAtLeastOneOtherEA {
t.Fatal("Expected at least one non-test external auth linked")
}
// add the test provider in the collection
user.Collection().MFA.Enabled = false
user.Collection().OAuth2.Enabled = true
user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{
Name: "test",
ClientId: "123",
ClientSecret: "456",
}}
if err := app.Save(user.Collection()); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"record":{`,
`"token":"`,
`"meta":{`,
`"isNew":false`,
`"email":"test3@example.com"`,
`"id":"bgs820n361vj1qd"`,
`"id":"test_id"`,
`"verified":true`,
},
NotExpectedContent: []string{
// hidden fields
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordAuthWithOAuth2Request": 1,
"OnRecordAuthRequest": 1,
"OnRecordEnrich": 1,
// ---
"OnModelCreate": 2, // authOrigins + externalAuths
"OnModelCreateExecute": 2,
"OnModelAfterCreateSuccess": 2,
"OnRecordCreate": 2,
"OnRecordCreateExecute": 2,
"OnRecordAfterCreateSuccess": 2,
// ---
"OnModelValidate": 2, // authOrigins + externalAuths
"OnRecordValidate": 2,
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
if err != nil {
t.Fatal(err)
}
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected old password %q to remain valid", "1234567890")
}
devices, err := app.FindAllAuthOriginsByRecord(user)
if len(devices) != 1 {
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
}
var hasTestEA = false
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if len(externalAuths) <= 1 {
t.Fatalf("Expected to have 2+ ExternalAuth records, got %d (%v)", len(externalAuths), err)
}
for _, rel := range externalAuths {
if rel.Provider() == "test" {
hasTestEA = true
break
}
}
if !hasTestEA {
t.Fatal("Expected test external auth to be linked")
}
},
},
{
@@ -531,6 +730,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordCreateExecute": 2,
"OnRecordAfterCreateSuccess": 2,
// ---
"OnModelDelete": 2, // pre-existing external auths
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
// ---
"OnModelValidate": 2,
"OnRecordValidate": 2,
},
@@ -541,7 +747,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
}
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected password %q not to be changed", "1234567890")
t.Fatalf("Expected old password %q to remain valid", "1234567890")
}
devices, err := app.FindAllAuthOriginsByRecord(user)
@@ -652,6 +858,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// ---
"OnModelDelete": 2, // pre-existing external auths
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
// ---
"OnModelValidate": 3, // record + authOrigins + externalAuths
"OnRecordValidate": 3,
},
@@ -662,7 +875,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
}
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected password %q not to be changed", "1234567890")
t.Fatalf("Expected old password %q to remain valid", "1234567890")
}
devices, err := app.FindAllAuthOriginsByRecord(user)
@@ -758,6 +971,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
// ---
"OnModelDelete": 2, // pre-existing external auths
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
// ---
"OnModelValidate": 3, // record + authOrigins + externalAuths
"OnRecordValidate": 3,
},
@@ -768,7 +988,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
}
if !user.ValidatePassword("1234567890") {
t.Fatalf("Expected password %q not to be changed", "1234567890")
t.Fatalf("Expected old password %q to remain valid", "1234567890")
}
devices, err := app.FindAllAuthOriginsByRecord(user)
+20 -10
View File
@@ -65,28 +65,38 @@ func recordAuthWithOTP(e *core.RequestEvent) error {
// ---
return e.App.OnRecordAuthWithOTPRequest().Trigger(event, func(e *core.RecordAuthWithOTPRequestEvent) error {
otpId := e.OTP.Id
otpSentTo := e.OTP.SentTo()
// eagerly delete the OTP to avoid unnecessery double delete model hook calls
// triggered by the password change below
err := e.App.Delete(e.OTP)
if err != nil {
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
}
// update the user email verified state in case the OTP originate from an email address matching the current record one
//
// note: don't wait for success auth response (it could fail because of MFA) and because we already validated the OTP above
otpSentTo := e.OTP.SentTo()
if !e.Record.Verified() && otpSentTo != "" && e.Record.Email() == otpSentTo {
e.Record.SetVerified(true)
err = e.App.Save(e.Record)
if err != nil {
// this is technically not required but we enforce password
// reset on verified upgrades in case the OTP is used on its own
// since this makes it less error prone to pre-hijacking attacks
if !e.Record.Collection().MFA.Enabled {
e.Record.SetRandomPassword()
}
if err := e.App.Save(e.Record); err != nil {
e.App.Logger().Error("Failed to update record verified state after successful OTP validation",
"error", err,
"otpId", e.OTP.Id,
"otpId", otpId,
"recordId", e.Record.Id,
)
}
}
// try to delete the used otp
err = e.App.Delete(e.OTP)
if err != nil {
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
}
return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
})
}
+43 -7
View File
@@ -327,6 +327,15 @@ func TestRecordAuthWithOTP(t *testing.T) {
if user.Verified() {
t.Fatal("Expected the user to remain unverified because sentTo != email")
}
// ensure that all pre-existing OAuth2 were NOT deleted
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) != 2 {
t.Fatalf("Expected 2 external auths, found %d", len(externalAuths))
}
},
},
{
@@ -364,6 +373,15 @@ func TestRecordAuthWithOTP(t *testing.T) {
if err := app.Save(otp); err != nil {
t.Fatal(err)
}
// verify that there are at least one pre-existing OAuth2 link
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) == 0 {
t.Fatal("Expected at least one external auth")
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
@@ -388,10 +406,10 @@ func TestRecordAuthWithOTP(t *testing.T) {
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
// OTP delete
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
// record OTP + 2 ExternalAuths delete
"OnModelDelete": 3,
"OnModelDeleteExecute": 3,
"OnModelAfterDeleteSuccess": 3,
// user verified update
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
@@ -401,9 +419,9 @@ func TestRecordAuthWithOTP(t *testing.T) {
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
"OnRecordDelete": 3,
"OnRecordDeleteExecute": 3,
"OnRecordAfterDeleteSuccess": 3,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
@@ -417,6 +435,24 @@ func TestRecordAuthWithOTP(t *testing.T) {
if !user.Verified() {
t.Fatal("Expected the user to be marked as verified")
}
// ensure that all pre-existing OTPs are cleared
otps, err := app.FindAllOTPsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(otps) > 0 {
t.Fatalf("Expected all OTPs to be cleared, found %d", len(otps))
}
// ensure that all pre-existing OAuth2 links are cleared
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(externalAuths) > 0 {
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
}
},
},
{
+20
View File
@@ -85,6 +85,11 @@ func recordAuthWithPassword(e *core.RequestEvent) error {
return e.App.OnRecordAuthWithPasswordRequest().Trigger(event, func(e *core.RecordAuthWithPasswordRequestEvent) error {
if e.Record == nil || !e.Record.ValidatePassword(e.Password) {
// dummy password check to minimize enumeration side-channel attacks
if e.Record == nil {
dummyPasswordCheck(e.App, e.Collection)
}
return e.BadRequestError("Failed to authenticate.", errors.New("invalid login credentials"))
}
@@ -115,6 +120,21 @@ func (form *authWithPasswordForm) validate(collection *core.Collection) error {
)
}
// dummy password check to minimize side-channel attacks
// (performed with the collection configured field cost)
func dummyPasswordCheck(app core.App, collection *core.Collection) {
record := &core.Record{}
// find any random existing record
err := app.RecordQuery(collection).Limit(1).One(record)
if err != nil {
return
}
// the value and result doesn't matter, we just need a constant-time check
_ = record.ValidatePassword("")
}
func findRecordByIdentityField(app core.App, collection *core.Collection, field string, value any) (*core.Record, error) {
if !slices.Contains(collection.PasswordAuth.IdentityFields, field) {
return nil, errors.New("invalid identity field " + field)
+5
View File
@@ -502,6 +502,11 @@ type App interface {
// ExternalAuth model that satisfies the non-nil expression.
FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalAuth, error)
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
//
// Returns a combined error with the failed deletes.
DeleteAllExternalAuthsByRecord(authRecord *Record) error
// ---------------------------------------------------------------
// FindAllMFAsByRecord returns all MFA models linked to the provided auth record.
+1 -1
View File
@@ -60,7 +60,7 @@ func (m *Collection) setDefaultAuthOptions() {
},
MFA: MFAConfig{
Enabled: false,
Duration: 1800, // 30min
Duration: 600, // 10min
},
OTP: OTPConfig{
Enabled: false,
+38
View File
@@ -137,4 +137,42 @@ func (app *BaseApp) registerExternalAuthHooks() {
},
Priority: 99,
})
// delete all pre-existing external auths on verified upgrade
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
Func: func(e *RecordEvent) error {
if !e.Record.Collection().IsAuth() {
return e.Next()
}
hasUpgradedVerified := !e.Record.Original().IsNew() && !e.Record.Original().Verified() && e.Record.Verified()
if !hasUpgradedVerified {
return e.Next()
}
originalApp := e.App
return e.App.RunInTransaction(func(txApp App) error {
e.App = txApp
defer func() { e.App = originalApp }()
externalAuths, err := txApp.FindAllExternalAuthsByRecord(e.Record)
if err != nil {
return err
}
if len(externalAuths) > 0 {
// delete all pre-existing external auths
if err := txApp.DeleteAllExternalAuthsByRecord(e.Record); err != nil {
return err
}
// force refresh tokens reset (if not already)
e.Record.RefreshTokenKey()
}
return e.Next()
})
},
Priority: 99,
})
}
+101
View File
@@ -308,3 +308,104 @@ func TestExternalAuthValidateHook(t *testing.T) {
})
}
}
func TestExternalAuthClearOnVerfiedUpgrade(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
t.Run("unverified->no changes", func(t *testing.T) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
if user.Verified() {
t.Fatal("Expected user to be unverified")
}
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil || len(beforeAuths) == 0 {
t.Fatalf("Expected at least one external auth (%v)", err)
}
oldTokenKey := user.TokenKey()
if err = app.Save(user); err != nil {
t.Fatal(err)
}
if oldTokenKey != user.TokenKey() {
t.Fatal("Expected tokenKey to remain unchanged")
}
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil || len(afterAuths) != len(beforeAuths) {
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
}
})
t.Run("unverified->verified", func(t *testing.T) {
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
if user.Verified() {
t.Fatal("Expected user to be unverified")
}
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil || len(externalAuths) == 0 {
t.Fatalf("Expected at least one external auth (%v)", err)
}
oldTokenKey := user.TokenKey()
user.SetVerified(true)
if err = app.Save(user); err != nil {
t.Fatal(err)
}
if oldTokenKey == user.TokenKey() {
t.Fatal("Expected tokenKey to be renewed")
}
externalAuths, err = app.FindAllExternalAuthsByRecord(user)
if err != nil || len(externalAuths) != 0 {
t.Fatalf("Expected all user external auths to be deleted, found %d (%v)", len(externalAuths), err)
}
})
t.Run("verified->no changes", func(t *testing.T) {
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
if err != nil {
t.Fatal(err)
}
if !user.Verified() {
t.Fatal("Expected user to be verified")
}
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil || len(beforeAuths) == 0 {
t.Fatalf("Expected at least one external auth (%v)", err)
}
oldTokenKey := user.TokenKey()
if err = app.Save(user); err != nil {
t.Fatal(err)
}
if oldTokenKey != user.TokenKey() {
t.Fatal("Expected tokenKey to remain unchanged")
}
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
if err != nil || len(afterAuths) != len(beforeAuths) {
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
}
})
}
+24
View File
@@ -1,6 +1,8 @@
package core
import (
"errors"
"github.com/pocketbase/dbx"
)
@@ -59,3 +61,25 @@ func (app *BaseApp) FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalA
return model, nil
}
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
//
// Returns a combined error with the failed deletes.
func (app *BaseApp) DeleteAllExternalAuthsByRecord(authRecord *Record) error {
models, err := app.FindAllExternalAuthsByRecord(authRecord)
if err != nil {
return err
}
var errs []error
for _, m := range models {
if err := app.Delete(m); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
+66
View File
@@ -2,6 +2,7 @@ package core_test
import (
"fmt"
"slices"
"testing"
"github.com/pocketbase/dbx"
@@ -174,3 +175,68 @@ func TestFindFirstExternalAuthByExpr(t *testing.T) {
})
}
}
func TestDeleteAllExternalAuthsByRecord(t *testing.T) {
t.Parallel()
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t")
if err != nil {
t.Fatal(err)
}
user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
client1, err := testApp.FindAuthRecordByEmail("clients", "test@example.com")
if err != nil {
t.Fatal(err)
}
client2, err := testApp.FindAuthRecordByEmail("clients", "test2@example.com")
if err != nil {
t.Fatal(err)
}
scenarios := []struct {
record *core.Record
deletedIds []string
}{
{demo1, nil}, // non-auth record
{user1, []string{"dlmflokuq1xl342", "clmflokuq1xl341"}},
{client1, []string{"f1z5b3843pzc964"}},
{client2, nil},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
deletedIds := []string{}
app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error {
deletedIds = append(deletedIds, e.Record.Id)
return e.Next()
})
err := app.DeleteAllExternalAuthsByRecord(s.record)
if err != nil {
t.Fatal(err)
}
if len(deletedIds) != len(s.deletedIds) {
t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds)
}
for _, id := range s.deletedIds {
if !slices.Contains(deletedIds, id) {
t.Errorf("Expected to find deleted id %q in %v", id, deletedIds)
}
}
})
}
}
+6 -5
View File
@@ -3,6 +3,7 @@ package core
import (
"context"
"errors"
"fmt"
"time"
"github.com/pocketbase/pocketbase/tools/hook"
@@ -141,11 +142,11 @@ func (app *BaseApp) registerMFAHooks() {
if old != new {
err = e.App.DeleteAllMFAsByRecord(e.Record)
if err != nil {
e.App.Logger().Warn(
"Failed to delete all previous mfas",
"error", err,
"recordId", e.Record.Id,
"collectionId", e.Record.Collection().Id,
return fmt.Errorf(
"[%s] failed to delete all previos MFAs for record %q: %w",
e.Record.Collection().Name,
e.Record.Id,
err,
)
}
}
+61
View File
@@ -300,3 +300,64 @@ func TestMFAValidateHook(t *testing.T) {
})
}
}
func TestMFAClearOnPasswordChange(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
if err != nil {
t.Fatal(err)
}
mfasToCreate := map[*core.Record]int{
user1: 3,
user2: 2,
}
for user, total := range mfasToCreate {
for range total {
mfa := core.NewMFA(app)
mfa.SetCollectionRef(user.Collection().Id)
mfa.SetRecordRef(user.Id)
mfa.SetMethod(core.MFAMethodPassword)
if err := app.Save(mfa); err != nil {
t.Fatal(err)
}
}
}
// update both users
err = app.Save(user1)
if err != nil {
t.Fatal(err)
}
user2.SetRandomPassword()
err = app.Save(user2)
if err != nil {
t.Fatal(err)
}
expectedMFAs := map[*core.Record]int{
user1: 3,
user2: 0,
}
for user, expected := range expectedMFAs {
mfas, err := app.FindAllMFAsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(mfas) != expected {
t.Fatalf("Expected %d MFAs, got %d", expected, len(mfas))
}
}
}
+27
View File
@@ -3,8 +3,10 @@ package core
import (
"context"
"errors"
"fmt"
"time"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/types"
)
@@ -124,4 +126,29 @@ func (app *BaseApp) registerOTPHooks() {
app.Logger().Warn("Failed to delete expired OTP sessions", "error", err)
}
})
// delete all record OTPs on tokenKey change to minimize the risk of hijacking attacks
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
Func: func(e *RecordEvent) error {
err := e.Next()
if err != nil || !e.Record.Collection().IsAuth() {
return err
}
if !e.Record.Original().IsNew() && e.Record.Original().TokenKey() != e.Record.TokenKey() {
err := e.App.DeleteAllOTPsByRecord(e.Record)
if err != nil {
return fmt.Errorf(
"[%s] failed to delete all previos OTPs for record %q: %w",
e.Record.Collection().Name,
e.Record.Id,
err,
)
}
}
return nil
},
Priority: 99,
})
}
+61
View File
@@ -300,3 +300,64 @@ func TestOTPValidateHook(t *testing.T) {
})
}
}
func TestOTPClearOnTokenKeyChange(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
if err != nil {
t.Fatal(err)
}
otpsToCreate := map[*core.Record]int{
user1: 3,
user2: 2,
}
for user, total := range otpsToCreate {
for range total {
otp := core.NewOTP(app)
otp.SetCollectionRef(user.Collection().Id)
otp.SetRecordRef(user.Id)
otp.SetPassword("123456")
if err := app.Save(otp); err != nil {
t.Fatal(err)
}
}
}
// update both users
err = app.Save(user1)
if err != nil {
t.Fatal(err)
}
user2.RefreshTokenKey()
err = app.Save(user2)
if err != nil {
t.Fatal(err)
}
expectedOTPs := map[*core.Record]int{
user1: 3,
user2: 0,
}
for user, expected := range expectedOTPs {
otps, err := app.FindAllOTPsByRecord(user)
if err != nil {
t.Fatal(err)
}
if len(otps) != expected {
t.Fatalf("Expected %d OTPs, got %d", expected, len(otps))
}
}
}
+3 -2
View File
@@ -1428,13 +1428,13 @@ func onRecordValidate(e *RecordEvent) error {
func onRecordSaveExecute(e *RecordEvent) error {
if e.Record.Collection().IsAuth() {
// ensure that the token key is regenerated on password change or email change
if !e.Record.IsNew() {
lastSavedRecord, err := e.App.FindRecordById(e.Record.Collection(), e.Record.Id)
if err != nil {
return err
}
// ensure that the token key is regenerated on password change or email change
if lastSavedRecord.TokenKey() == e.Record.TokenKey() &&
(lastSavedRecord.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) ||
lastSavedRecord.Email() != e.Record.Email()) {
@@ -1442,7 +1442,8 @@ func onRecordSaveExecute(e *RecordEvent) error {
}
}
// cross-check that the auth record id is unique across all auth collections.
// loosely cross-check that the auth record id is unique across all auth collections
// to minimize impact of mistakes in API rules when multiple auth collections are used
authCollections, err := e.App.FindAllCollections(CollectionTypeAuth)
if err != nil {
return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err)
+1 -1
View File
@@ -22,7 +22,7 @@ require (
golang.org/x/net v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
modernc.org/sqlite v1.49.1
modernc.org/sqlite v1.50.0
)
require (
+2 -2
View File
@@ -134,8 +134,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
const (
expectedDriverVersion = "v1.49.1"
expectedDriverVersion = "v1.50.0"
expectedLibcVersion = "v1.72.0"
// ModerncDepsCheckHookId is the id of the hook that performs the modernc.org/* deps checks.
+16 -10
View File
@@ -46,6 +46,12 @@ type Config struct {
// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
ArchiveExecutable string
// BaseURL is the base URL of the GitHub API (or similar compatible)
// used to fetch the latest releases information.
//
// Defaults to "https://api.github.com".
BaseURL string
// Optional context to use when fetching and downloading the latest release.
Context context.Context
@@ -82,6 +88,12 @@ func Register(app core.App, rootCmd *cobra.Command, config Config) error {
p.config.ArchiveExecutable = "pocketbase"
}
if p.config.BaseURL == "" {
p.config.BaseURL = "https://api.github.com"
} else {
p.config.BaseURL = strings.TrimRight(p.config.BaseURL, "/")
}
if p.config.HttpClient == nil {
p.config.HttpClient = http.DefaultClient
}
@@ -145,12 +157,9 @@ func (p *plugin) updateCmd() *cobra.Command {
func (p *plugin) update(withBackup bool) error {
color.Yellow("Fetching release information...")
latest, err := fetchLatestRelease(
p.config.Context,
p.config.HttpClient,
p.config.Owner,
p.config.Repo,
)
url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", p.config.BaseURL, p.config.Owner, p.config.Repo)
latest, err := fetchLatestRelease(p.config.Context, p.config.HttpClient, url)
if err != nil {
return err
}
@@ -260,11 +269,8 @@ func (p *plugin) update(withBackup bool) error {
func fetchLatestRelease(
ctx context.Context,
client HttpClient,
owner string,
repo string,
url string,
) (*release, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -137,7 +137,7 @@ migrate((app) => {
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
"manageRule": "1 != 2",
"mfa": {
"duration": 1800,
"duration": 600,
"enabled": false,
"rule": ""
},
@@ -319,7 +319,7 @@ func init() {
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
"manageRule": "1 != 2",
"mfa": {
"duration": 1800,
"duration": 600,
"enabled": false,
"rule": ""
},
@@ -590,7 +590,7 @@ migrate((app) => {
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
"manageRule": "1 != 2",
"mfa": {
"duration": 1800,
"duration": 600,
"enabled": false,
"rule": ""
},
@@ -775,7 +775,7 @@ func init() {
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
"manageRule": "1 != 2",
"mfa": {
"duration": 1800,
"duration": 600,
"enabled": false,
"rule": ""
},
+4 -3
View File
@@ -120,8 +120,9 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
expected := struct {
Values []struct {
Email string `json:"email"`
IsPrimary bool `json:"is_primary"`
Email string `json:"email"`
IsPrimary bool `json:"is_primary"`
IsConfirmed bool `json:"is_confirmed"`
} `json:"values"`
}{}
if err := json.Unmarshal(data, &expected); err != nil {
@@ -129,7 +130,7 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
}
for _, v := range expected.Values {
if v.IsPrimary {
if v.IsPrimary && v.IsConfirmed {
return v.Email, nil
}
}
+65 -7
View File
@@ -3,6 +3,9 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"github.com/pocketbase/pocketbase/tools/types"
@@ -15,10 +18,10 @@ func init() {
var _ Provider = (*Gitea)(nil)
// NameGitea is the unique name of the Gitea provider.
// NameGitea is the unique name of the Gitea/Forgejo provider.
const NameGitea string = "gitea"
// Gitea allows authentication via Gitea OAuth2.
// Gitea allows authentication via Gitea/Forgejo OAuth2.
type Gitea struct {
BaseProvider
}
@@ -29,7 +32,7 @@ func NewGiteaProvider() *Gitea {
ctx: context.Background(),
order: 11,
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640"><path d="m396 484-127-61c-12-6-18-21-12-34l61-127c6-12 21-17 34-11l27 13V154h17v118s57 24 83 40q7 3 13 14 3 10-1 19l-61 127c-6 13-22 18-34 12" style="fill:#fff"/><path d="M623 150c-4-4-10-4-10-4l-178 8-39 1v117l-17-8V155l-89-3-157-8q-15-2-39 1c-9 2-34 8-54 27C-5 212 7 276 8 286c2 12 7 44 32 72 46 56 144 55 144 55s12 29 31 56c25 33 50 59 75 62h189s12 0 29-11c14-8 26-23 26-23s13-14 31-45l14-28s55-118 55-232c-1-34-9-40-11-42M126 354c-26-9-37-19-37-19s-19-13-29-40c-16-44-1-71-1-71s8-22 38-30c14-4 31-3 31-3s7 59 16 94c7 30 25 78 25 78s-26-3-43-9m300 108s-6 14-20 15l-10-1-5-2-113-55s-11-6-13-16c-2-8 3-18 3-18l54-112s5-10 12-13l5-1c8-3 18 2 18 2l110 54s13 6 16 16q1 12-2 17c-6 16-55 114-55 114" style="fill:#609926"/><path d="M327 380q-14 1-17 14-2 13 9 20c7 4 17 2 22-5q8-13-1-24l24-49h6l7-4 29 16 5 5c2 6-2 15-2 15-2 7-18 40-18 40q-13 1-18 13-4 14 9 21a18 18 0 0 0 21-28l6-11 13-30c1-2 6-11 3-22-2-11-13-16-13-16-12-8-29-16-29-16l-1-7-4-6 14-29-12-6-14 29q-11 0-16 10t1 20z" style="fill:#609926"/></svg>`,
displayName: "Gitea",
displayName: "Gitea/Forgejo",
pkce: true,
scopes: []string{"read:user", "user:email"},
authURL: "https://gitea.com/login/oauth/authorize",
@@ -38,9 +41,9 @@ func NewGiteaProvider() *Gitea {
}}
}
// FetchAuthUser returns an AuthUser instance based on Gitea's user api.
// FetchAuthUser returns an AuthUser instance based on Gitea/Forgejo's user api.
//
// API reference: https://try.gitea.io/api/swagger#/user/userGetCurrent
// API reference: https://codeberg.org/api/swagger#/user/userGetCurrent
func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
@@ -55,26 +58,81 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
extracted := struct {
Name string `json:"full_name"`
Username string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
Active bool `json:"active"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
if !extracted.Active {
return nil, errors.New("user account is not active")
}
user := &AuthUser{
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Username,
Email: extracted.Email,
AvatarURL: extracted.AvatarURL,
RawUser: rawUser,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
}
email, err := p.fetchVerifiedPrimaryEmail(token)
if err != nil {
return nil, fmt.Errorf("failed to fetch primary email: %w", err)
}
user.Email = email
user.Expiry, _ = types.ParseDateTime(token.Expiry)
return user, nil
}
// fetchVerifiedPrimaryEmail sends an API request to retrieve the verified
// primary email, in case "Keep my email address private" was set.
//
// NB! This method can succeed and still return an empty email.
// Error responses that are result of insufficient scopes permissions are ignored.
//
// API reference: https://codeberg.org/api/swagger#/user/userListEmails
func (p *Gitea) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) {
client := p.Client(token)
response, err := client.Get(p.userInfoURL + "/emails")
if err != nil {
return "", err
}
defer response.Body.Close()
// ignore common http errors caused by insufficient scope permissions
// (the email field is optional, aka. return the auth user without it)
if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 {
return "", nil
}
content, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
emails := []struct {
Email string
Verified bool
Primary bool
}{}
if err := json.Unmarshal(content, &emails); err != nil {
return "", err
}
// extract the verified primary email
for _, email := range emails {
if email.Verified && email.Primary {
return email.Email, nil
}
}
return "", nil
}
+12 -15
View File
@@ -42,7 +42,7 @@ func NewGithubProvider() *Github {
// FetchAuthUser returns an AuthUser instance based the Github's user api.
//
// API reference: https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
// API reference: https://docs.github.com/en/rest/users/users?apiVersion=2026-03-10#get-the-authenticated-user
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
@@ -55,9 +55,8 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
}{}
@@ -69,7 +68,6 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Login,
Email: extracted.Email,
AvatarURL: extracted.AvatarURL,
RawUser: rawUser,
AccessToken: token.AccessToken,
@@ -78,27 +76,26 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user.Expiry, _ = types.ParseDateTime(token.Expiry)
// in case user has set "Keep my email address private", send an
// **optional** API request to retrieve the verified primary email
if user.Email == "" {
email, err := p.fetchPrimaryEmail(token)
if err != nil {
return nil, err
}
user.Email = email
// always send a primary email request even though the email is
// returned in the userinfo endpoint since the API may change and
// enterprise setups may have configuration that could allow unverified emails
email, err := p.fetchVerifiedPrimaryEmail(token)
if err != nil {
return nil, err
}
user.Email = email
return user, nil
}
// fetchPrimaryEmail sends an API request to retrieve the verified
// fetchVerifiedPrimaryEmail sends an API request to retrieve the verified
// primary email, in case "Keep my email address private" was set.
//
// NB! This method can succeed and still return an empty email.
// Error responses that are result of insufficient scopes permissions are ignored.
//
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28
func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user
func (p *Github) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) {
client := p.Client(token)
response, err := client.Get(p.userInfoURL + "/emails")
+13 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/pocketbase/pocketbase/tools/types"
"golang.org/x/oauth2"
@@ -40,7 +41,7 @@ func NewGitlabProvider() *Gitlab {
// FetchAuthUser returns an AuthUser instance based the Gitlab's user api.
//
// API reference: https://docs.gitlab.com/ee/api/users.html#for-admin
// API reference: https://docs.gitlab.com/api/users/#retrieve-the-current-user
func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token)
if err != nil {
@@ -53,11 +54,12 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
}
extracted := struct {
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"`
ConfirmedAt string `json:"confirmed_at"`
Id int64 `json:"id"`
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
@@ -67,7 +69,6 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name,
Username: extracted.Username,
Email: extracted.Email,
AvatarURL: extracted.AvatarURL,
RawUser: rawUser,
AccessToken: token.AccessToken,
@@ -76,5 +77,10 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user.Expiry, _ = types.ParseDateTime(token.Expiry)
confirmedAt, err := time.Parse(time.RFC3339, extracted.ConfirmedAt)
if err == nil && !confirmedAt.IsZero() {
user.Email = extracted.Email
}
return user, nil
}
+12 -7
View File
@@ -3,7 +3,9 @@ package mailer
import (
"errors"
"fmt"
"net"
"net/smtp"
"strconv"
"strings"
"github.com/domodwyer/mailyak/v3"
@@ -70,16 +72,18 @@ func (c *SMTPClient) send(m *Message) error {
}
}
hostWithPort := net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
// create mail instance
var yak *mailyak.MailYak
if c.TLS {
var tlsErr error
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil)
yak, tlsErr = mailyak.NewWithTLS(hostWithPort, smtpAuth, nil)
if tlsErr != nil {
return tlsErr
}
} else {
yak = mailyak.New(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth)
yak = mailyak.New(hostWithPort, smtpAuth)
}
if c.LocalName != "" {
@@ -133,15 +137,16 @@ func (c *SMTPClient) send(m *Message) error {
}
// add custom headers (if any)
var hasMessageId bool
var hasMessageIdHeader bool
for k, v := range m.Headers {
if strings.EqualFold(k, "Message-ID") {
hasMessageId = true
if !hasMessageIdHeader && strings.EqualFold(k, "Message-ID") {
hasMessageIdHeader = true
}
yak.AddHeader(k, v)
}
if !hasMessageId {
// add a default message id if missing
// add a default message id if missing
if !hasMessageIdHeader {
fromParts := strings.Split(m.From.Address, "@")
if len(fromParts) == 2 {
yak.AddHeader("Message-ID", fmt.Sprintf("<%s@%s>",
+1 -1
View File
@@ -349,7 +349,7 @@ const DefaultMaxMemory = 16 << 20 // 16mb
//
// Title string `json:"title" form:"title"`
// Total int `json:"total" form:"total"`
// }
// }{}
// err := e.BindBody(&data)
func (e *Event) BindBody(dst any) error {
if e.Request.ContentLength == 0 {
+1 -1
View File
@@ -11,4 +11,4 @@ PB_DOCS_URL = "https://pocketbase.io/docs"
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
PB_VERSION = "v0.37.0"
PB_VERSION = "v0.37.4"
+81
View File
@@ -0,0 +1,81 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./docsList-BAfVNUIM.js","./expandInfo-DGS0CLSa.js","./fieldsInfo-Bz62125-.js","./filterSyntax-Df6AnI5x.js","./docsView-Be6Uhvcf.js","./docsCreate-Be3S3y5K.js","./docsUpdate-LtD8DbWI.js","./docsListAuthMethods-9feSopQX.js","./docsAuthWithPassword-DEWj8Jyn.js","./docsAuthWithOAuth2-DUIE4EoY.js","./docsAuthWithOTP-B7z2VJzp.js","./docsAuthRefresh-UjveHHwo.js","./pageInstaller-CboSj8bL.js","./pocketbase.es-B_4DUNUU.js","./pageConfirmPasswordReset-xSsXQyFi.js","./pageConfirmVerification-BSn_UexA.js","./pageConfirmEmailChange-DWXtM27Q.js"])))=>i.map(i=>d[i]);
import{a as e,n,r,t as i}from"./pocketbase.es-B_4DUNUU.js";var a=Object.create,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,l=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,d=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),f=(e,n,r,i)=>{if(n&&typeof n==`object`||typeof n==`function`)for(var a=c(n),l=0,d=a.length,f;l<d;l++)f=a[l],!u.call(e,f)&&f!==r&&o(e,f,{get:(e=>n[e]).bind(null,f),enumerable:!(i=s(n,f))||i.enumerable});return e},p=(e,n,r)=>(r=e==null?{}:a(l(e)),f(n||!e||!e.__esModule?o(r,`default`,{value:e,enumerable:!0}):r,e));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))r(e);new MutationObserver(e=>{for(let n of e)if(n.type===`childList`)for(let e of n.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&r(e)}).observe(document,{childList:!0,subtree:!0});function n(e){let n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?n.credentials=`include`:e.crossOrigin===`anonymous`?n.credentials=`omit`:n.credentials=`same-origin`,n}function r(e){if(e.ep)return;e.ep=!0;let r=n(e);fetch(e.href,r)}})();var m=`abcdefghijklmnopqrstuvwxyz0123456789`,h=`pb_redirect`,g=store({hash:window.location.hash});window.addEventListener(`hashchange`,()=>{g.hash=window.location.hash}),Prism.languages.pbrule={string:Prism.languages.js.string,number:Prism.languages.js.number,function:Prism.languages.js.function,boolean:/\b(?:true|false)\b/i,constant:/\b(?:null)\b/i,comment:{pattern:/\/\/.*/,greedy:!0},italic:/_via_|\:\w+/,keyword:/&&|\|\||\??(?:!~|!=|>=|<=|=|~|>|>|<)(?=[@\w\s]|$)/};var _={isObject(e){return typeof e==`object`&&!!e&&e.constructor===Object},isEmpty(e){return e==null||e===``||Array.isArray(e)&&e.length===0||typeof e==`object`&&app.utils.isEmptyObject(e)},isEmptyObject(e){for(let n in e)return!1;return!0},toArray(e,n=!1){return Array.isArray(e)?e.slice():(n||!_.isEmpty(e))&&e!==void 0?[e]:[]},removeByValue(e,n){if(!Array.isArray(e)){console.warn(`[removeByValue] not an array:`,e);return}for(let r=e.length-1;r>=0;r--)if(e[r]==n){e.splice(r,1);break}},removeByKey(e,n,r){if(!Array.isArray(e)){console.warn(`[removeByKey] not an array:`,e);return}for(let i in e)if(e[i][n]==r){e.splice(i,1);break}},pushUnique(e,n){if(!Array.isArray(e)){console.warn(`[pushUnique] not an array:`,e);return}e.includes(n)||e.push(n)},mergeUnique(e,n){for(let r of n)app.utils.pushUnique(e,r);return e},pushOrReplaceObject(e,n,r=`id`){for(let i=e.length-1;i>=0;i--)if(e[i][r]==n[r]){e[i]=n;return}e.push(n)},filterDuplicatesByKey(e,n=`id`){e=Array.isArray(e)?e:[];let r={};for(let i of e)r[i[n]]=i;return Object.values(r)},filterRedactedProps(e,n=`******`){let r=JSON.parse(JSON.stringify(e||{}));for(let e in r)typeof r[e]==`object`&&r[e]!==null?r[e]=_.filterRedactedProps(r[e],n):r[e]===n&&delete r[e];return r},getByPath(e,n,r=null,i=`.`){let a=e||{},o=(n||``).split(i);for(let e of o){if(!_.isObject(a)&&!Array.isArray(a)||a[e]===void 0)return r;a=a[e]}return a},setByPath(e,n,r,i=`.`){if(typeof e!=`object`||!e){console.warn(`setByPath: data not an object or array.`);return}let a=e,o=n.split(i),s=o.pop();for(let e of o)(!_.isObject(a)&&!Array.isArray(a)||!_.isObject(a[e])&&!Array.isArray(a[e]))&&(a[e]={}),a=a[e];a[s]=r},deleteByPath(e,n,r=`.`){let i=e||{},a=(n||``).split(r),o=a.pop();for(let e of a)(!_.isObject(i)&&!Array.isArray(i)||!_.isObject(i[e])&&!Array.isArray(i[e]))&&(i[e]={}),i=i[e];Array.isArray(i)?i.splice(o,1):_.isObject(i)&&delete i[o],a.length>0&&(Array.isArray(i)&&!i.length||_.isObject(i)&&!Object.keys(i).length)&&(Array.isArray(e)&&e.length>0||_.isObject(e)&&Object.keys(e).length>0)&&_.deleteByPath(e,a.join(r),r)},emptyClone(e,n=[]){let r=JSON.parse(JSON.stringify(e));for(let e in r)n.includes(e)||(typeof r[e]==`string`?r[e]=``:typeof r[e]==`number`?r[e]=0:typeof r[e]==`boolean`?r[e]=!1:Array.isArray(r[e])?r[e]=[]:app.utils.isObject(r[e])&&(r[e]={}));return r},randomString(e=8,n=m){let r=``;for(let i=0;i<e;i++)r+=n.charAt(Math.floor(Math.random()*n.length));return r},randomSecret(e=30){if(typeof crypto>`u`)return app.utils.randomString(e);let n=new Uint8Array(e);crypto.getRandomValues(n);let r=``;for(let i=0;i<e;i++)r+=`-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(n[i]%64);return r},sentenize(e,n=!0){if(typeof e!=`string`)return``;if(e=e.trim().split(`_`).join(` `),e===``)return e;if(e=e[0].toUpperCase()+e.substring(1),n){let n=e[e.length-1];n!==`.`&&n!==`?`&&n!==`!`&&(e+=`.`)}return e},trimQuotedValue(e){return typeof e==`string`&&(e[0]==`"`||e[0]==`'`||e[0]=="`")&&e[0]==e[e.length-1]?e.slice(1,-1):e},slugify(e,n=`_`,r=[]){if(e===``)return``;let i={a:/а|à|á|å|â/gi,b:/б/gi,c:/ц|ç/gi,d:/д/gi,e:/е|è|é|ê|ẽ|ë/gi,f:/ф/gi,g:/г/gi,h:/х/gi,i:/й|и|ì|í|î/gi,j:/ж/gi,k:/к/gi,l:/л/gi,m:/м/gi,n:/н|ñ/gi,o:/о|ò|ó|ô|øLine truncated
`),n+=r.map(e=>{let n=``;return e.name.includes(`(`)||e.name.includes(` `)?n+=e.name:n+="`"+e.name+"`",e.collate&&(n+=` COLLATE `+e.collate),e.sort&&(n+=` `+e.sort.toUpperCase()),n}).join(`,
`),r.length>1&&(n+=`
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=_,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
`+Fe:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.classList.add(`rotate`),a.addEventListener(`animationend`,()=>{a.classList.remove(`rotate`)}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.classList.remove(`rotate`)},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id Line truncated
<video controls width="300">
<source src="${a}" />
<p>Download: <a href="${a}" download="${i}">${i}</a>.</p>
</video>
`)}})}},{type:`menuitem`,text:`Manual embed`,onAction:()=>{tinymce.activeEditor.execCommand(`mceMedia`)}}])}})}var Ke=`DIV.P.A.EM.B.STRONG.H1.H2.H3.H4.H5.H6.TABLE.TR.TD.TH.TBODY.THEAD.TFOOT.BR.HR.Q.SUP.SUB.DEL.IMG.OL.UL.LI.CODE`.split(`.`);function qe(e){if(e){for(let n of e.children)qe(n);Ke.includes(e.tagName)?(e.removeAttribute(`style`),e.removeAttribute(`class`)):Je(e)}}function Je(e){let n=e.parentNode;for(;e.firstChild;)n.insertBefore(e.firstChild,e);n.removeChild(e)}async function Ye(){if(window.tinymce!==void 0)return;let e=`lazy-tinymce-js`;return document.getElementById(e)?new Promise((e,n)=>{function r(){document.removeEventListener(`tinymceLoadSuccess`,i),document.removeEventListener(`tinymceLoadError`,a)}let i=function(){r(),e()},a=function(e){r(),n(e?.details)};document.addEventListener(`tinymceLoadSuccess`,i),document.addEventListener(`tinymceLoadError`,a)}):new Promise((n,r)=>{document.head.querySelector(`#shablon-script`).after(t.script({id:e,src:`./libs/tinymce/tinymce.min.js`,onload:()=>{n()},onerror:e=>{console.warn(`failed to load tinymce.min.js:`,e),r(e)}}))}).then(()=>{document.dispatchEvent(new CustomEvent(`tinymceLoadSuccess`))}).catch(e=>{document.dispatchEvent(new CustomEvent(`tinymceLoadError`,{detail:e}))})}window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.colorPicker=function(e={}){let n=`picker_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,name:``,required:!1,disabled:!1,value:``,predefinedColors:[],onchange:e=>{},onmount:e=>{},onunmount:e=>{}}),i=app.utils.extendStore(r,e),a=store({inputValue:`#ffffff`});function o(e){e=e?.toLowerCase()||``,r.onchange?.(e)}let s,c=t.input({type:`color`,className:`color-picker-input`,id:()=>r.id,name:()=>r.name,required:()=>r.required,disabled:()=>r.disabled,value:()=>(a.inputValue=r.value||`#ffffff`,r.value||void 0),oninput:e=>{a.inputValue=e.target.value,clearTimeout(s),s=setTimeout(()=>{o(e.target.value)},50)}});return t.div({rid:r.rid,hidden:()=>r.hidden,inert:()=>r.inert,className:()=>`color-picker ${r.className}`,onmount:e=>{r.onmount?.(e)},onunmount:e=>{clearTimeout(s),r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.div({className:`color-picker-input-wrapper`},c,t.output({className:`result`,style:()=>`color: lch(from ${a.inputValue||`#ffffff`} calc((49 - l) * infinity) 0 0);`,textContent:()=>a.inputValue})),t.button({hidden:()=>!r.predefinedColors.length,type:`button`,title:`Predefined colors`,className:`link-hint predefined-colors-btn`,"html-popovertarget":n+`predefined-colors-dropdown`},t.i({className:`ri-arrow-down-s-line`,ariaHidden:!0})),t.div({pbEvent:`predefinedColorsDropdown`,id:n+`predefined-colors-dropdown`,className:`dropdown predefined-colors-dropdown`,popover:`auto`},t.div({className:`predefined-colors-list`},()=>r.predefinedColors?.map(e=>t.button({type:`button`,className:()=>`color ${r.value==e?`active`:``}`,style:`background:${e}`,onclick:n=>{c&&(n.target.closest(`.dropdown`)?.hidePopover(),c.value=e||void 0,c.dispatchEvent(new Event(`input`,{bubbles:!0})))}})))))},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.ruleField=function(e={}){let n=`rule_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,required:!1,disabled:!1,name:void 0,label:void 0,help:void 0,value:null,nullable:!0,placeholder:`Leave empty to grant everyone access...`,autocomplete:e=>[],oninput:e=>{},onmount:e=>{},onunmount:e=>{},get isLocked(){return r.value==null}}),i=app.utils.extendStore(r,e,`isLocked`),a,o=``;function s(e){r.value=e,r.oninput?.(e),a?.dispatchEvent(new CustomEvent(`change`,{detail:e}))}function c(){r.value!==null&&(o=r.value,s(null))}function l(){s(o??``),setTimeout(()=>{document.getElementById(n)?.focus()},0)}return a=t.div({rid:r.rid,id:()=>r.id,hidden:()=>r.hidden,inert:()=>r.inert,"html-name":()=>r.name,className:()=>[`field`,`rule-field`,r.required?`required`:null,r.value===null?`locked`:null,r.disabled?`disabled`:null].filter(Boolean).join(` `),onmount:e=>{r.onmount?.(e)},onunmount:e=>{r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},e=>r.label?typeof r.label==`function`?r.label(e):typeof r.label==`string`?t.span({className:`txt`},r.label):r.label:t.span({className:`txt`},`Rule`),t.span({hidden:()=>!r.isLocked,className:`txt superusers-label`},`(Superusers only)`)),e=>r.isLocked?t.button({type:`button`,className:`unlock-overlay`,disabled:()=>r.disabled,onclick:l},t.span({className:`txt`},`Unlock and set custom rule`),t.i({className:`ri-lock-unlock-line`,ariaHidden:!0})):[app.components.codeEditor({id:n,language:`pbrule`,required:()=>r.required,disabled:()=>r.disabled,value:()=>r.value,oninput:s,placeholder:()=>r.placeholder,autocomplete:r.autocomplete,autocompleteContainer:e}),t.button({hidden:()=>!r.nullable,type:`button`,className:`superuser-toggle`,disabled:()=>r.disabled,onclick:c},t. Line truncated
<defs>
<marker id="${s}_arrow1" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<g class="paths-group" marker-end="url(#${s}_arrow1)"></g>
`;let h=m.querySelector(`.paths-group`),g=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);g.classList.add(`erd-paths`,`front`),g.innerHTML=`
<defs>
<marker id="${s}_arrow2" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<g class="paths-group" marker-end="url(#${s}_arrow2)"></g>
`;let _=g.querySelector(`.paths-group`);return a=t.div({tabIndex:-1,rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,className:()=>`erd ${n.className} ${i.isUpdating?`updating`:``} ${i.isPanning?`panning`:``} ${i.activeCollection?`active`:``}`,onkeydown:e=>{(e.ctrlKey||e.metaKey)&&e.key==`0`&&f()},onmount:async e=>{e.addEventListener(`wheel`,r=>{r.preventDefault();let a=e.getBoundingClientRect(),o=r.clientX-a.left,s=r.clientY-a.top,c=(o-i.viewX)/n.scale,l=(s-i.viewY)/n.scale,u=Math.min(Math.max(-r.deltaY*.001+n.scale,Qe),Ze);i.viewX=o-c*u,i.viewY=s-l*u,n.scale=u}),e.addEventListener(`pointerdown`,e=>{e.buttons==1&&(i.isPanning=!0,i.panStartX=e.clientX-i.viewX,i.panStartY=e.clientY-i.viewY)}),e._ondragging=function(e){i.isPanning&&(i.viewX=e.clientX-i.panStartX,i.viewY=e.clientY-i.panStartY)},e._ondragstop=function(){i.isPanning=!1},window.addEventListener(`pointermove`,e._ondragging),window.addEventListener(`pointerup`,e._ondragstop),c()},onunmount:e=>{window.removeEventListener(`pointermove`,e._ondragging),window.removeEventListener(`pointerup`,e._ondragstop),r.forEach(e=>e?.unwatch())}},t.div({className:`erd-area`,style:()=>`transform: translate(${i.viewX}px, ${i.viewY}px) scale(${n.scale});`},m,t.div({className:`erd-tables`},()=>n.collections.map(e=>t.div({style:()=>`left:${i.positions[e.id]?.x||0}px;top:${i.positions[e.id]?.y||0}px`,className:()=>`erd-table type-${e.type} ${p(e)?`active`:``} ${e.system?`system`:``}`,"html-data-collection-id":()=>e.id,"html-data-collection-name":()=>e.name,onmouseenter:()=>{h.querySelectorAll(`[data-to="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-to`),_.append(e)}),h.querySelectorAll(`[data-from="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-from`),_.append(e)}),i.activeCollection=e},onmouseleave:()=>{for(let e of _.children)e.classList.remove(`active-from`,`active-to`);h.append(..._.children),i.activeCollection=null}},t.div({className:`erd-table-row header`},()=>e.name),()=>e.fields?.map(e=>t.div({className:`erd-table-row type-${e.type} ${e.primaryKey?`primary-key`:``}`,"html-data-field-id":()=>e.id,"html-data-field-name":()=>e.name},t.i({ariaHidden:!0,title:()=>e.type,className:()=>`field-icon ${app.fieldTypes[e.type].icon||app.utils.fallbackFieldIcon}`}),t.span({className:`field-name`},()=>e.name),()=>{if(e.hidden)return t.span({className:`label danger field-hidden-label`},`Hidden`)},()=>{if(e.maxSelect!==void 0)return t.span({className:`meta`},e.maxSelect>1?`multiple`:`single`)}))))),g),t.nav({className:`erd-nav`,onmousedown:e=>{e.stopImmediatePropagation()}},t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom in`,onclick:()=>{n.scale+=.05}},t.i({className:`ri-add-line`,ariaHidden:!0})),t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom out`,onclick:()=>{n.scale-=.05}},t.i({className:`ri-subtract-line`,ariaHidden:!0})))),a};function et(e,n,r=1,i=``,a=``,o=2){if(!e||!n)return;let s=e.closest(`.erd-area`).getBoundingClientRect(),c=e.getBoundingClientRect(),l=n.getBoundingClientRect();o*=r;let u=c.top-s.top+c.height/2,d=l.top-s.top+l.height/2,f,p;c.left<l.left?(f=c.left-s.left+c.width+o,p=l.left-s.left-o):c.left>l.left?(f=c.left-s.left-o,p=l.left-s.left+l.width+o):(f=c.left-s.left-o,p=l.left-s.left-o),f/=r,p/=r,u/=r,d/=r;let m=f+(p-f)/2;f==p&&(m-=20);let h=`M ${f} ${u}
L ${m} ${u}
L ${m} ${d}
L ${p} ${d}`,g=document.createElementNS(`http://www.w3.org/2000/svg`,`path`);return g.setAttribute(`class`,`relation-path`),g.setAttribute(`data-from`,i||``),g.setAttribute(`data-to`,a||``),g.setAttribute(`d`,h),g}window.app=window.app||{},window.app.components=window.app.components||{};var P=1e3;window.app.components.pageSidebar=function(e={},...n){let r,i=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,widthHistoryKey:`pbPageSidebarWidth`,onmount:void 0,onunmount:void 0}),a=app.utils.extendStore(i,e),o=store({responsiveShow:!1}),s;function c(){if(r){if(window.innerWidth>P){o.responsiveShow=!1,r.dataset.responsive=!1,s?.remove(),s=null;return}r.dataset.responsive=!0,s||(s=t.button({type:`button`,className:`btn transparent secondary responsive-sidebar-btn`,title:`Toggle sidebar`,onclick:e=>{e.stopPropagation(),o.responsiveShow=!o.responsiveShow}},t.i({className:`ri-menu-2-line`,ariaHidden:!0})),document.body.querySelector(`.page-header .breadcrumbs`).before(s))}}function l(e){if(e.target.closest(`.responsive-close`)){o.responsiveShow=!1;return}if(!(e.target.closest(`.page-sidebar`)||e.target.closest(`.app-header`)||e.target.closest(`.modal`)))return e.preventDefault(),e.stopImmediatePropagation(),o.responsiveShow=!1,!1}return a.push(watch(()=>o.responsiveShow,e=>{e?window.addEventListener(`click`,l,!0):window.removeEventListener(`click`,l,!0)})),r=t.aside({pbEvent:`pageSidebar`,rid:i.rid,id:()=>i.id,hidden:()=>i.hidden,inert:()=>i.inert,className:()=>`page-sidebar ${i.className} ${o.responsiveShow?`active`:``}`,onmount:e=>{c(e),window.addEventListener(`resize`,c),i.onmount?.(e)},onunmount:e=>{i.onunmount?.(e),window.removeEventListener(`click`,l,!0),window.removeEventListener(`resize`,c),s?.remove(),a.forEach(e=>e?.unwatch())}},e=>{let n;return i.widthHistoryKey&&(n=localStorage.getItem(i.widthHistoryKey),n&&(e.style.width=n)),app.components.dragline({ondragstart:n=>{e._startWidth=e.offsetWidth},ondragging:(r,a,o)=>{n=e._startWidth+a+`px`,e.style.width=n,i.widthHistoryKey&&localStorage.setItem(i.widthHistoryKey,n)}})},...n),r};var F=`modulepreload`,tt=function(e,n){return new URL(e,n).href},nt={},I=function(e,n,r){let i=Promise.resolve();if(n&&n.length>0){let e=document.getElementsByTagName(`link`),a=document.querySelector(`meta[property=csp-nonce]`),o=a?.nonce||a?.getAttribute(`nonce`);function s(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}i=s(n.map(n=>{if(n=tt(n,r),n in nt)return;nt[n]=!0;let i=n.endsWith(`.css`),a=i?`[rel="stylesheet"]`:``;if(r)for(let r=e.length-1;r>=0;r--){let a=e[r];if(a.href===n&&(!i||a.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${n}"]${a}`))return;let s=document.createElement(`link`);if(s.rel=i?`stylesheet`:F,i||(s.as=`script`),s.crossOrigin=``,s.href=n,o&&s.setAttribute(`nonce`,o),document.head.appendChild(s),i)return new Promise((e,r)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>r(Error(`Unable to preload CSS for ${n}`)))})}))}function a(e){let n=new Event(`vite:preloadError`,{cancelable:!0});if(n.payload=e,window.dispatchEvent(n),!n.defaultPrevented)throw e}return i.then(n=>{for(let e of n||[])e.status===`rejected`&&a(e.reason);return e().catch(a)})};window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openApiPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=rt(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};function rt(e,n){if(!e){console.warn(`[apiPreviewModal] missing required collection`);return}let r,i=store({activeTab:`List/Search`,tabEl:null,isLoading:!1}),a={"List/Search":async e=>{let{docsList:n}=await I(async()=>{let{docsList:e}=await import(`./docsList-BAfVNUIM.js`);return{docsList:e}},__vite__mapDeps([0,1,2,3]),import.meta.url);return i.tabEl=n(e)},View:async e=>{let{docsView:n}=await I(async()=>{let{docsView:e}=await import(`./docsView-Be6Uhvcf.js`);return{docsView:e}},__vite__mapDeps([4,1,2]),import.meta.url);return i.tabEl=n(e)}};e.type!=`view`&&(a.Create=async e=>{let{docsCreate:n}=await I(async()=>{let{docsCreate:e}=await import(`./docsCreate-Be3S3y5K.js`);return{docsCreate:e}},__vite__mapDeps([5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Update=async e=>{let{docsUpdate:n}=await I(async()=>{let{docsUpdate:e}=await import(`./docsUpdate-LtD8DbWI.js`);return{docsUpdate:e}},__vite__mapDeps([6,5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Delete=async e=>{let{docsDelete:n}=await I(async()=>{let{docsDelete:e}=await import(`./docsDelete-CybOn5jy.js`);return{docsDelete:e}},[],import.meta.url);return i.tabEl=n(e)},a.Realtime=async e=>{let{docsRealtime:n}=await I(async()=>{let{docsRealtime:e}=await import(`./docsRealtime-PMESvmJN.js`);return{docsRealtime:e}},[],import.meta.url);return i.tabEl=n(e)},a.Batch=async e=>{let{docsBatch:n}=await I(async()=>{let{docsBatch:e}=await import(`./docsBatch-DNJl1NTn.js`);return{docsBatch: Line truncated
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i}');
// load the token into the store
const token = '...';
pb.authStore.save(token, null);
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/js-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`JS SDK docs`}))},{title:`Dart SDK`,language:`dart`,value:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${i}');
// load the token into the store
final token = '...';
pb.authStore.save(token, null);
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=V(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};function Tt(e,n){switch(e){case`expand`:case`password`:case`passwordConfirm`:case`tokenKey`:return}return n}function Et(e){app.utils.copyToClipboard(JSON.stringify(e,Tt,2)),app.toasts.success(`Record copied to clipboard!`)}function Dt(e){return e?JSON.stringify(e,Tt):``}var Ot=`main`,B=`authProviders`;function V(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:Ot,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return Dt(s.initialDraft)},get recordHash(){return Dt(s.record)},get originalRecordHash(){return Dt(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function l(){return window.localStorage.getItem(c())||``}function u(){try{let e=l();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),f()}return null}function d(e){try{window.localStorage.setItem(c(),e)}catch(e){console.warn(`saveDraft failure:`,e),f()}}function f(){window.localStorage.removeItem(c()),s.initialDraft=null}function p(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));f(),s.record=e}let m;function h(){s.initialDraft=u(),m?.unwatch(),m=watch(()=>s.recordHash,(e,n)=>{n!==void 0&&(s.hasChanges?d(e):f())})}async function g(n){if(s.isLoading=!0,m?.unwatch(),n=app.utils.isObject(n)?n:{id:n||``},!n.id){s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1,s.isLocked=!1,h();return}try{s.isLocked=!!app.store.settings?.meta?.hideControls,s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n));let r=await app.pb.collection(e.name).getOne(n.id,{requestKey:`upsert_load_`+n.id});n.expand&&(r.expand=JSON.parse(JSON.stringify(n.expand))),Object.assign(s.originalRecord,JSON.parse(JSON.stringify(r))),Object.assign(s.record,JSON.parse(JSON.stringify(r))),s.isLoading=!1,setTimeout(()=>{h()},0)}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}function _(e){for(let n in e)n.startsWith(`@@`)&&delete e[n]}async function v(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function ee(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await v(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),f(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c)),_(s.originalRecord),_(s.record)),r.onsave?.(c,o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0)}catch(e){e?.isAbort||(s.isSaving=!1,app.checkApiErr Line truncated
The field cannot be presentable if hidden.`),n})})))},()=>{if(i.showHidden)return t.div({className:`field prop-hidden`},t.input({type:`checkbox`,id:r+`.hidden`,className:`sm`,name:()=>`fields.${e.fieldIndex}.hidden`,checked:()=>!!e.field.hidden,onchange:n=>e.field.hidden=n.target.checked}),t.label({htmlFor:r+`.hidden`},t.span({className:`txt`},`Hidden`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Hide from the JSON API response and filters.`)})))},t.button({hidden:()=>!i.showDuplicate&&(!i.showRemove||e.field.system),type:`button`,title:`More options`,className:`btn sm circle transparent secondary more-btn m-l-auto`,"html-popovertarget":r+`_options_dropdown`},t.i({className:`ri-more-line`,ariaHidden:!0})),t.div({id:r+`_options_dropdown`,className:`dropdown sm field-options-dropdown`,popover:`auto`},()=>{if(i.showDuplicate)return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Duplicate`,onclick:e=>{o(),e.target.closest(`.dropdown`).hidePopover()}})},()=>{if(!(!i.showRemove||e.field.system))return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Remove`,onclick:e=>{s(),e.target.closest(`.dropdown`).hidePopover(),e.target.closest(`details`).open=!1}})})))};function G(e,n=`field`){let r=n,i=2,a=n.match(/\d+$/)?.[0]||``,o=a?n.substring(0,n.length-a.length):n;for(;e?.find(e=>e.name.toLowerCase()==r.toLowerCase());)r=o+((a<<0)+i),i++;return r}function K(e,n,r){if(!r||n===void 0||n===r||!e?.passwordAuth?.identityFields?.length)return;let i=e.passwordAuth.identityFields;for(let e=0;e<i.length;e++)i[e]==n&&(i[e]=r)}function Jt(e,n,r){!r||n===void 0||n===r||!e?.indexes?.length||!e?.fields?.length||e.fields.find(e=>!e[`@toDelete`]&&e.name==n)||(e.indexes=e.indexes.map(e=>app.utils.replaceIndexColumn(e,n,r)))}function Yt(e){let n=`text_`+app.utils.randomString(),r=store({get hasAutogenerate(){return!app.utils.isEmpty(e.field.autogeneratePattern)&&app.utils.isEmpty(e.originalRecord?.id)},get isDisabled(){return!app.utils.isEmpty(e.originalRecord?.id)&&e.field.primaryKey},get isRequired(){return e.field.required&&!r.hasAutogenerate&&!r.isDisabled}});return t.div({className:`record-field-input field-type-text`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:n},t.i({ariaHidden:!0,className:()=>e.field.primaryKey?`ri-key-line`:app.fieldTypes.text.icon}),t.span({className:`txt`},()=>e.field.name)),t.div({className:`autoexpand-wrapper`},t.textarea({id:n,rows:1,name:()=>e.field.name,required:()=>r.isRequired,disabled:()=>r.isDisabled,placeholder:()=>r.hasAutogenerate?`Leave empty to autogenerate...`:``,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value||``}),t.div({className:`input`},()=>e.record[e.field.name],` `))),()=>{if(!e.field.primaryKey||!e.originalRecord?.id)return;let n=e.collection?.fields?.filter(e=>e.type==`autodate`)||[];if(!n.length)return;let r=[];for(let i of n)r.push(`${i.name}: ${app.utils.displayValue(e.record[i.name])}`);return t.div({className:`field addon`},t.i({className:`ri-information-line txt-hint link-faded`,ariaDescription:app.attrs.tooltip(r.join(`
`),`left`)}))}),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Xt(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 for no limit.`)})),t.input({type:`number`,id:n+`.min`,name:()=>`fields.${e.fieldIndex}.min`,step:1,min:0,max:2**53-1,placeholder:`No min limit`,value:()=>e.field.min||``,oninput:n=>{e.field.min=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 to fallback to the default limit.`)})),t.input({type:`number`,id:n+`.max`,name:()=>`fields.${e.fieldIndex}.max`,step:1,min:()=>e.field.min||0,max:2**53-1,placeholder:`Default to max 5000 characters`,value:()=>e.field.max||``,oninput:n=>{e.field.max=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.pattern`},t.span({className:`txt`},`Validation pattern`),()=>{if(e.field.primaryKey)return t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`All record ids have forbidden characters and unique case-insensitive (ASCII) validations in addition to the user defined regex pattern.`)})}),t.input({type:`text`,id:n+`.pattern`,name:()=>`fields.${e.fieldIndex}.pattern`,value:()=>e.field.pattern||``,oninput:n=>e.field.pattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`^[a-z0-9]+$`))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.autogeneratePattern`},t.span({className:`txt`},`Autogenerate pattern`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Set and autogenerate text matching the pattern on missing record create value.`)})),t.input({type:`text`,id:n+`.autogeneratePattern`,name:()=>`fields.${e.fieldIndex}.autogeneratePattern`,value:()=>e.field.autogeneratePattern||``,oninput:n=>e.field.autogeneratePattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`[a-z0-9]{30}`))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function Zt(e){return t.div({className:`record-field-view field-type-text`},()=>{let n=e.record[e.field.name]||``;if(n==``)return t.span({className:`missing-value`});if(e.field.primaryKey){let r=null;return e.record?.collectionName==`_superusers`&&app.store.superuser?.id==e.record?.id&&(r=t.strong({className:`txt`,textContent:` (you)`})),t.span({className:`label`},app.components.copyButton(n),t.span({className:`txt-ellipsis`},app.utils.truncate(n),r))}return e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.text={icon:`ri-text`,label:`Plain text`,settings:Xt,input:Yt,view:Zt,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function Qt(e){let n=`editor_`+app.utils.randomString(),r=store({lazyEditor:``}),i;return t.div({className:`record-field-input field-type-editor large-modal`,onmount:()=>{i=setTimeout(()=>{r.lazyEditor=app.components.tinymce({id:n,name:()=>e.field.name,required:()=>e.field.required,convertURLs:()=>e.field.convertURLs,value:()=>e.record[e.field.name]||``,onchange:n=>{e.record[e.field.name]=n}})},0)},onunmount:()=>{clearTimeout(i)}},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.editor.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),()=>r.lazyEditor),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function $t(e){let n=`f_`+app.utils.randomString();return store({showInfo:!1}),app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1,max:2**53-1,placeholder: Line truncated
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function ln(e){return t.div({className:`record-field-view field-type-email`},()=>{let n=e.record[e.field.name]||``;return n?e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n:t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.email={icon:`ri-mail-line`,label:`Email`,settings:cn,input:sn,view:ln,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>`test_${app.utils.randomString(3,`123567890`)}@example.com`};function J(e){let n=`url_`+app.utils.randomString();return t.div({className:`record-field-input field-type-url`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.url.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({type:`url`,id:n,spellcheck:!1,name:()=>e.field.name,required:()=>e.field.required,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function un(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.exceptDomains`},t.span({className:`txt`},`Except domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are NOT allowed.
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function Y(e){return t.div({className:`record-field-view field-type-url`},()=>{let n=e.record[e.field.name]||``;return n?t.a({href:()=>n,className:`txt txt-ellipsis`,rel:`noopener noreferrer`,target:`_blank`,textContent:app.utils.truncate(n),ariaDescription:app.attrs.tooltip(`Open in new tab`),onclick:e=>{e.stopPropagation()}}):t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.url={icon:`ri-link`,label:`URL`,settings:un,input:J,view:Y,dummyData:(e,n=!1)=>`https://example.com`};function dn(e){let n=`date_`+app.utils.randomString();return t.div({className:`record-field-input field-type-date`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.date.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({id:n,step:1,type:`datetime-local`,name:()=>e.field.name,required:()=>e.field.required,value:()=>app.utils.toDatetimeLocalInputValue(e.record[e.field.name]),onchange:n=>{e.record[e.field.name]=app.utils.toRFC3339Datetime(n.target.value)}})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function fn(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min date (Local)`)),t.input({type:`datetime-local`,id:n+`.min`,step:1,name:()=>`fields.${e.fieldIndex}.min`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.min),onchange:n=>{e.field.min=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max date (Local)`)),t.input({type:`datetime-local`,id:n+`.max`,step:1,name:()=>`fields.${e.fieldIndex}.max`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.max),onchange:n=>{e.field.max=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function pn(e){return t.div({className:`record-field-view field-type-date`},app.components.formattedDate({value:()=>e.record[e.field.name],short:()=>e.short}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.date={icon:`ri-calendar-line`,label:`Datetime`,settings:fn,input:dn,view:pn,dummyData:(e,n=!1)=>new Date().toISOString().replaceAll(`T`,` `)};function mn(e){delete e.clone[e.field.name]}function hn(e){let n=[{label:`Create`,value:1},{label:`Update`,value:2},{label:`Create/Update`,value:3}];function r(e){return e.onCreate&&e.onUpdate?3:e.onUpdate?2:1}function i(n){switch(n){case 1:e.field.onCreate=!0,e.field.onUpdate=!1;break;case 2:e.field.onCreate=!1,e.field.onUpdate=!0;break;case 3:e.field.onCreate=!0,e.field.onUpdate=!0;break}}let a=store({isDropdownOpen:!1});return app.components.fieldSettings(e,{header:t.div({className:`field header-select autodate-select`,ariaDescription:app.attrs.tooltip(`Auto set on`,`left`),onmount:()=>{i(r(e.field))}},app.components.select({required:!0,options:n,disabled:()=>e.originalCollection?.system,value:()=>r(e.field),onchange:e=>i(e?.[0]?.value),ondropdowntoggle:e=>{a.isDropdownOpen=e.newState==`open`}}))})}functi Line truncated
Leave empty for no restriction.`)})),app.components.select({max:99,placeholder:`No restriction`,options:app.utils.mimeTypes.map(e=>({value:e.mimeType,label:()=>t.div({className:`inline-flex gap-10`},t.span({className:`txt`},e.ext||`-`),t.small({className:`txt-hint`},e.mimeType))})),name:()=>`fields.${e.fieldIndex}.mimeTypes`,value:()=>app.utils.toArray(e.field.mimeTypes),onchange:n=>e.field.mimeTypes=n.map(e=>e.value)})),t.div({className:`field-help`},t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`mimeTypesDropdown`},t.span({className:`txt`},`Choose presets`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`mimeTypesDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`image/jpeg`,`image/png`,`image/svg+xml`,`image/gif`,`image/webp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Images (jpg, png, svg, gif, webp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Documents (pdf, doc/docx, xls/xlsx)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`video/mp4`,`video/mpeg`,`video/x-msvideo`,`video/quicktime`,`video/3gpp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Videos (mp4, mpeg, avi, mov, 3gp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/zip`,`application/x-7z-compressed`,`application/x-rar-compressed`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Archives (zip, 7zip, rar)`})))),t.div({className:()=>e.field.maxSelect>1?`col-sm-6`:`col-sm-9`},t.div({className:`field`},t.label({htmlFor:n+`.thumbs`},t.span({className:`txt`},`Thumb sizes`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.`)})),t.input({type:`text`,id:n+`.thumbs`,placeholder:`e.g. 50x50, 480x720`,name:()=>`fields.${e.fieldIndex}.thumbs`,value:()=>app.utils.joinNonEmpty(e.field.thumbs),onchange:n=>e.field.thumbs=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},t.span({className:`txt m-r-5`},`Use comma as separator.`),t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`thumbFormatsDropdown`},t.span({className:`txt`},`Supported formats`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`thumbFormatsDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.ul({className:`m-0 p-l-sm`},t.li(null,t.strong(null,`WxH`),t.span(null,` (e.g. 100x50) - crop to WxH viewbox (from center)`)),t.li(null,t.strong(null,`WxHt`),t.span(null,` (e.g. 100x50t) - crop to WxH viewbox (from top)`)),t.li(null,t.strong(null,`WxHb`),t.span(null,` (e.g. 100x50b) - crop to WxH viewbox (from bottom)`)),t.li(null,t.strong(null,`WxHf`),t.span(null,` (e.g. 100x50f) - fit inside a WxH viewbox (without cropping)`)),t.li(null,t.strong(null,`0xH`),t.span(null,` (e.g. 0x50) - resize to H height preserving the aspect ratio`)),t.li(null,t.strong(null,`Wx0`),t.span(null,` (e.g. 100x0) - resize to W width preserving the aspect ratio`)))))),t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},`Max size`),t.input({type:`number`,id:n+`.maxSize`,step:1,min:0,max:2**53-1,placeholder:`~5MB default`,name:()=>`fields.${e.fieldIndex}.maxSize`,value:()=>e.field.maxSize||``,oninput:n=>e.field.maxSize=parseInt(n.target.value,10)})),t.div({className:`field-help`},`In bytes.`)),()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,required:!0,max:2**53-1,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:e=>{let n=parseInt(e.target.value,10);n>1?props.field.maxSelect=n:props.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field m-t-5 m-b-5`},t.input({className:`switch`,type:`checkbox`,id:n+`.protected`,name:()=>`fields.${e.fieldIndex}.protected`,checked:()=>!!e.field.protected,onchange:n=>e.field.protected=n.target.checked}),t.label({htmlFor:n+`.protected`},t.span({className:`txt`},`Protected`),t.small({className:`txt-hint`},`File download requests will need to satisfy the View API rule (`,t.a({href:`https://pocketbase.io/docs/files-handling#protected-files`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Learn more`}),`).`)))),t.div({className:`col-sm-12`},t. Line truncated
`),oninput:n=>{let r=n.target.value.trimStart().replaceAll(`
`,`
`).split(`
`);e.field.values=r,app.utils.deleteByPath(app.store.errors,`fields.${e.fieldIndex}.values`)},onchange:n=>{let r=new Set,i=n.target.value.split(`
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function On(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:Dn,input:En,view:On,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function kn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>An(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!An(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function An(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function jn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function Mn(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1 Line truncated
To target the newly submitted ones you can use @request.body.*.`)})],name:`updateRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.updateRule,oninput:n=>e.collection.updateRule=n})),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Delete rule`,name:`deleteRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.deleteRule,oninput:n=>e.collection.deleteRule=n}))]}),()=>{if(e.collection.type==`auth`)return[t.hr({className:`m-t-base m-b-base`}),t.button({type:`button`,onmount:()=>{n.showAuthRules=e.collection.manageRule!==null||e.collection.authRule!==``},className:()=>`btn secondary sm ${n.showAuthRules?``:`transparent`}`,onclick:()=>{n.showAuthRules=!n.showAuthRules}},t.span({className:`txt`},`Additional auth collection rules`),t.i({ariaHidden:!0,className:()=>n.showAuthRules?`ri-arrow-drop-up-line`:`ri-arrow-drop-down-line`})),app.components.slide(()=>n.showAuthRules,t.div({className:`grid sm m-t-sm`},t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Authentication rule`,name:`authRule`,placeholder:``,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.authRule,oninput:n=>e.collection.authRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed every time `,t.strong(null,`before authentication`),` allowing you to restrict who can authenticate.`),t.p(null,`For example, to allow only verified users you can set it to `,t.code(null,`verified = true`),`.`),t.p(null,`Leave it empty to allow anyone with an account to authenticate.`),t.p(null,`To disable authentication entirely you can change it to "Set superusers only".`))),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Manage rule`,name:`manageRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.manageRule,oninput:n=>e.collection.manageRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed in addition to the `,t.strong(null,`create`),` and `,t.strong(null,`update`),` API rules.`),t.p(null,`It enables superuser-like permissions to allow fully managing the auth record(s), eg. changing the password without requiring to enter the old one, directly updating the verified state or email, etc.`)))))]})}var Jn=`test_view_query`;function Yn(e){let n=`query_`+app.utils.randomString(),r=`SELECT.FROM.WHERE.LEFT JOIN.INNER JOIN.ON.AS.GROUP BY.HAVING.ORDER BY.ASC.DESC.LIMIT.OFFSET.WITH.NOT.IN.AND.OR.EXISTS.LIKE.CAST.REAL.DECIMAL.NUMERIC.INT.TEXT.BOOL`.split(`.`),i=store({testRecords:[],testError:``,isTesting:!1});async function a(n){if(i.isTesting=!0,i.testRecords=[],(app.store.errors?.viewQuery||app.store.errors?.fields)&&(delete app.store.errors.viewQuery,delete app.store.errors.fields),!n){i.testError=``,i.isTesting=!1;return}try{let r=await app.pb.send(`/api/collections/meta/dry-run-view`,{method:`POST`,body:{query:n},requestKey:Jn});e.collection?.id?i.testRecords=r.sample.map(n=>(n.collectionId=e.collection?.id,n.collectionName=e.collection?.name,n)):i.testRecords=r.sample,i.testError=``,i.isTesting=!1}catch(e){e.isAbort||(i.testError=e.message||`Invalid query.`,i.isTesting=!1)}}let o,s=[watch(()=>e.collection?.viewQuery,e=>{clearTimeout(o),o=setTimeout(()=>a(e),200)})];return t.div({pbEvent:`collectionViewQueryTabContent`,className:`collection-tab-content collection-view-query-tab-content`,onunmount:()=>{clearTimeout(o),app.pb.cancelRequest(Jn),s.forEach(e=>e?.unwatch())}},t.div({className:`grid`},t.div({className:`col-12`},t.div({className:`txt-right txt-sm m-b-10`},t.button({type:`button`,className:`txt-bold link-hint`,"html-popovertarget":n+`caveats_dropdown`},()=>`Query caveats`)),t.div({id:n+`caveats_dropdown`,className:`dropdown sm query-caveats-dropdown`,popover:`auto`},t.ul(null,t.li(null,`Wildcard columns (*) are not supported.`),t.li(null,`The query must have a unique `,t.code(null,`id`),` column.`,t.br(),`If your query doesn't have a suitable one, you can use the universal `,t.code(null,`(ROW_NUMBER() OVER()) as id`),`.`),t.li(null,`Expressions must be aliased with a valid formatted field name, e.g. `,t.code(null,`MAX(balance) as maxBalance`),`.`),t.li(null,`Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. `,t.code(null,`(MAX(balance) + 1) as maxBalance`),`.`),t.li(null,`UNION expressions are supported but the entire query must be wrapped in parenthesis.`))),t.div({className:`field`},t.label({htmlFor:n+`.viewQuery`},t.span({className:`txt`},`Select query`),t.span({hidden:()=>!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Invalid query`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Valid query`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({id:n+`.viewQuery`,name:`viewQuery`,language:`sql`Line truncated
`+e)})},t.div({className:`btns`},t.button({type:`button`,className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.canSave,onclick:()=>s(!0)},t.span({className:`txt`},()=>a.isNew?`Create`:`Save changes`)),t.button({type:`button`,title:`Save options`,className:()=>`btn p-5`,disabled:()=>!a.canSave,"html-popovertarget":i+`save_options`},t.i({className:`ri-arrow-up-s-line`,ariaHidden:!0})),t.div({id:i+`save_options`,className:`dropdown nowrap`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),s(!1)}},t.span({className:`txt`},`Save and continue`),t.small({className:`txt-hint`},`(Ctrl+S)`)),t.hr(),t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),u()}},t.span({className:`txt`},`Reset form`)))))),r}function Zn(e){let n=JSON.parse(JSON.stringify(app.store.collectionScaffolds[e.type])),r=JSON.parse(JSON.stringify(e.fields))||[],i=r.filter(e=>!e.system);e.fields=n.fields||[];for(let n of r){if(!n.system)continue;let r=e.fields.find(e=>e.name==n.name);r&&Object.assign(r,n)}for(let n of i)e.fields.push(n);if(e.indexes=e.indexes||[],e.indexes.length){let r=n?.indexes||[];indexesLoop:for(let n=e.indexes.length-1;n>=0;n--){let i=app.utils.parseIndex(e.indexes[n]),a=i.indexName.toLowerCase();for(let i of r)if(a==app.utils.parseIndex(i).indexName.toLowerCase()){e.indexes.splice(n,1);continue indexesLoop}for(let r of i.columns)if(!e.fields.find(e=>e.name.toLowerCase()==r.name.toLowerCase())){e.indexes.splice(n,1);continue indexesLoop}}}app.utils.mergeUnique(e.indexes,n.indexes)}function Qn(e,n){let r=`truncate_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.truncate(e.originalCollection.name),n.ontruncate?.(JSON.parse(JSON.stringify(e.originalCollection))),app.toasts.success(`Successfully truncated collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover(),app.modals.confirm(t.div(null,t.h6({className:`block txt-center`},`Do you really want to delete all records of the collection?`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(n.target.closest(`.modal`))},()=>{i.nameConfirm=``})}},t.i({className:`ri-eraser-line`,ariaHidden:!0}),t.span({className:`txt`},`Truncate`))}function $n(e,n){let r=`delete_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.delete(e.originalCollection.name),n.ondelete?.(JSON.parse(JSON.stringify(e.originalCollection))),app.utils.removeByKey(app.store.collections,`id`,e.originalCollection.id),app.toasts.success(`Successfully deleted collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover();let o=n.target.closest(`.modal`);app.modals.confirm(t.div({className:`block`},t.h6({className:`block txt-center`},()=>e.originalCollection.type==`view`?`Do you really want to delete the selected collection?`:`Do you really want to delete the selected collection and all its records`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(o)},()=>{i.nameConfirm=``})}},t.i({className:`ri-delete-bin-7-line`,ariaHidden:!0}),t.span({className:`txt`},`Delete`))}window.app=window.app||{},window.app.components=window.a Line truncated
...
-----END PRIVATE KEY-----`,value:()=>a.privateKey||``,oninput:e=>a.privateKey=e.target.value})),t.div({className:`field-help`},`The key is not stored on the server and it is used only for generating the signed JWT.`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Close`)),t.button({"html-form":r+`_form`,type:`submit`,className:`btn expanded`},t.i({className:`ri-key-line`,ariaHidden:!0}),t.span({className:`txt`},`Generate secret`)))),n}function dr(){return t.div({className:`inline-flex gap-5`},t.span(null,`Default log levels:`),()=>{let e=[];for(let n in app.utils.logLevels)e.push(t.code(null,`${n}:${app.utils.logLevels[n].label}`));return e})}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogsSettings=function(e={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null}){let n=fr(e);n&&(document.body.appendChild(n),app.modals.open(n))};function fr(e){let n,r=store({isLoading:!1,isSaving:!1,formSettings:{},initFormSettingsHash:``,get hasChanges(){return r.initFormSettingsHash!=JSON.stringify(r.formSettings)}});function i(e={}){r.formSettings={logs:e?.logs||{}},r.initFormSettingsHash=JSON.stringify(r.formSettings)}async function a(){r.isLoading=!0;try{i(await app.pb.settings.getAll({requestKey:`logsSettings`})),r.isLoading=!1}catch(e){e.isAbort||(r.isLoading=!1,app.checkApiError(e))}}async function o(){if(r.hasChanges){r.isSaving=!0;try{let a=await app.pb.settings.update(app.utils.filterRedactedProps(r.formSettings));e.onsave?.(a),i(a),app.toasts.success(`Successfully saved logs settings.`),r.isSaving=!1,app.modals.close(n)}catch(e){e.isAbort||(r.isSaving=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`logsSettingsModal`,className:`modal popup sm logs-settings-modal`,onbeforeopen:n=>(a(),e.onbeforeopen?.(n)),onafteropen:n=>{e.onafteropen?.(n)},onbeforeclose:n=>e.onbeforeclose?.(n),onafterclose:n=>{e.onafterclose?.(n),n?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto`},`Logs settings`)),()=>r.isLoading?t.div({className:`modal-content flex`,style:`min-height: 200px`},t.span({className:`loader m-auto`})):[t.form({pbEvent:`logsSettingsForm`,id:`logsSettingsForm`,className:`modal-content logs-settings-form`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.maxDays`},`Max days retention`),t.input({type:`number`,id:`logs.maxDays`,name:`logs.maxDays`,min:0,required:!0,value:()=>r.formSettings.logs.maxDays,oninput:e=>r.formSettings.logs.maxDays=e.target.value<<0})),t.div({className:`field-help`},`Set to `,t.code(null,0),` to disable logs persistence.`)),t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.minLevel`},`Min log level`),t.input({type:`number`,id:`logs.minLevel`,name:`logs.minLevel`,min:-100,max:100,required:!0,value:()=>r.formSettings.logs.minLevel,oninput:e=>r.formSettings.logs.minLevel=e.target.value<<0})),t.div({className:`field-help`},t.div(null,`Logs with level below the minimum will be ignored.`),dr())),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logIP`,name:`logs.logIP`,className:`switch`,checked:()=>r.formSettings.logs.logIP,onchange:e=>r.formSettings.logs.logIP=e.target.checked}),t.label({htmlFor:`logs.logIP`},`Enable IP logging`))),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logAuthId`,name:`logs.logAuthId`,className:`switch`,checked:()=>r.formSettings.logs.logAuthId,onchange:e=>r.formSettings.logs.logAuthId=e.target.checked}),t.label({htmlFor:`logs.logAuthId`},`Enable Auth Id logging`))))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n),disabled:()=>r.isSaving},t.span({className:`txt`},`Close`)),t.button({type:`submit`,"html-form":`logsSettingsForm`,className:()=>`btn ${r.isSaving?`loading`:``}`,disabled:()=>!r.hasChanges||r.isSaving},t.span({className:`txt`},`Save changes`)))]),n}function Q(e){return t.div({className:()=>`label log-level-label level-${e.level}`},t.span({className:`txt`},()=>`${app.utils.logLevels[e.level]?.label||`UNKN`} (${e.level})`))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=gr(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};var pr=[`execTime`,`type`,`auth`,`authId`,`status`,`method`,`url`,`referer`,`remoteIP`,`userIP`,`userAgent`,`error`,`details`];function mr(e){app.utils.downloadJSON(e,`log_`+e.created.replaceAll(/[-:\. ]/gi,``)+`.json`)}function hr(e){app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Log copied to clipboard!`)}function gr(e,n){let r,i=store({isLoading:!1,log:null,get isReque Line truncated
{MMM} {DD}`,null,`
{MMM} {DD}`,null,null,null,1],[60,`{h}:{mm}{aa}`,`
{MMM} {DD}`,null,`
{MMM} {DD}`,null,null,null,1]],grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}},{show:!0,stroke:a,grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}}],plugins:[zr(),Lr(r),Rr(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function Lr(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Fr(r):r.setScale(`x`,{min:e.zoom.min,max:e.zoom.max})})},destroy:e=>{n?.unwatch()},setSelect:n=>{n.select.width>0&&(e.zoom={min:n.posToVal(n.select.left,`x`),max:n.posToVal(n.select.left+n.select.width,`x`)})}}}}function Rr(e){let n;return{hooks:{init:r=>{let i=r.root.querySelectorAll(`.u-axis`);if(!i.length){console.warn(`xPanPlugin requires x axis to be defined`);return}i[0].addEventListener(`mousedown`,i=>{if(!e.zoom?.min)return;let a=i.clientX,{min:o,max:s}=r.scales.x,c=r.bbox.width,l=(s-o)/(c/uPlot.pxRatio),u=i=>{let c=(a-i.clientX)*l;r.setScale(`x`,{min:o+c,max:s+c}),clearTimeout(n),n=setTimeout(()=>{r?.scales?.x&&(e.zoom={min:r.scales.x.min,max:r.scales.x.max})},100)},d=e=>{document.removeEventListener(`mousemove`,u),document.removeEventListener(`mouseup`,d)};document.addEventListener(`mousemove`,u),document.addEventListener(`mouseup`,d)})},destroy:e=>{n&&clearTimeout(n)}}}}function zr(e){let n;return{hooks:{init:e=>{let r=e.over;n=store({date:``,total:0,left:0,top:0,show:!1});let i=t.div({className:()=>`chart-tooltip ${n.show?``:`hidden`}`,onmount(e){e._positionWatcher?.unwatch(),e._positionWatcher=watch(()=>[n.left,n.top],()=>{if(!e)return;let i=e.getBoundingClientRect(),a=n.left;a<0?a=0:a+i.width>r.clientWidth&&(a=r.clientWidth-i.width),e.style.left=a+`px`;let o=n.top-i.height-5;o<0&&(o=n.top+5,o+i.high>r.clientHeight&&(o=r.clientHeight-i.height)),e.style.top=o+`px`})},onunmount(e){e._positionWatcher?.unwatch()}},t.div({className:`content-primary`},()=>`${n.total} ${n.total==1?`request`:`requests`}`),t.div({className:`content-secondary`},()=>n.date));r.appendChild(i),r.addEventListener(`mouseleave`,()=>{n&&(n.show=!1)})},destroy:()=>{n.show=!1},setCursor:e=>{if(!n)return;let r=e.data[0][e.cursor.idx]||0,i=e.data[1][e.cursor.idx]||0;if(r==0||i==0){n.show=!1;return}n.show=!0,n.total=i;let a=new Date(r*1e3),o=new Date(r*1e3+36e5),s=a.toLocaleString(`default`,{month:`short`}),c=a.getDate().toString().padStart(2,`0`);n.date=`${s} ${c} ${Br(a)}-${Br(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function Br(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Vr=50;function Hr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Vr},get totalSelected(){return Object.keys(n.bulkSelected).length},get areAllSelected(){return n.logs.length&&n.logs.length==n.totalSelected}});async function r(r=!1){e.isListLoading=!0;try{let i=r?1:n.lastPage+1,a=(e.presets||[]).concat(app.utils.normalizeSearchFilter(e.filter,[`level`,`message`,`data`]));if(e.zoom?.min&&e.zoom?.max){let n=new Date(e.zoom.min*1e3);n.setSeconds(0),n.setMilliseconds(0);let r=app.utils.toRFC3339Datetime(n),i=new Date(e.zoom.max*1e3);i.setSeconds(59),i.setMilliseconds(999);let o=app.utils.toRFC3339Datetime(i);a.push(`created >= "${r}" && created <= "${o}"`)}else i>1&&a.push(`created <= "${n.logs[n.logs.length-1].created}"`);let o=await app.pb.logs.getList(i,Vr,{skipTotal:1,sort:`-@rowid`,requestKey:`logs_list`,filter:a.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)});o.page==1&&(n.logs=[],n.bulkSelected={}),n.lastPage=o.page,n.lastLoadCount=o.items.length;for(let e=0;e<o.items.length;e++)app.utils.pushOrReplaceObject(n.logs,o.items[e]),e>1&&e%20==0&&await new Promise(e=>setTimeout(e,20));e.isListLoading=!1,e.isFirstLoadReady||=!0}catch(n){n.isAbort||(e.isListLoading=!1,app.checkApiError(n))}}function i(e=!0){let r={};if(e)for(let e of n.logs)r[e.id]=e;n.bulkSelected=r}function a(e){let n=[];if(!e.data)return n;if(e.data.type==`request`){for(let r of[`status`,`execTime`,`auth`,`authId`,`userIP`])e.data[r]!==void 0&&n.push({key:r});e.data.referer&&!e.data.referer.includes(window.location.host)&&n.push({key:`referer`})}else{let r=Object.keys(e.data);for(let e of r)e!=`error`&&e!=`details`&&n.length<6&&n.push({key:e})}return e.data.error&&n.push({key:`error`,label:`danger`}),e.data.details&&n.push({key:`details`,label:`warning`}),n}let o=/[-:\. ]/gi;function s(){let e=Object.values(n.bulkSelected).sort((e,n)=>e.created<n.created?1:e.created>n.created?-1:0);if(!e.length)return;if(e.length==1)return app.utils.downloadJSON(e[0],`log_`+e[0].created.replaceAll(o,``)+`.json`);let r=e[0].created.replaceAll(o,``),i=e[e.length-1].created.replaceAll(o,``);return app.utils.downloadJSON(e,`${e.length}_logs_${i}_to_${r}.json`)}let c=[];return t.div({pbEvent:`logsList`,className:`page-table-wrapper`,onmount(n){c.push(watch(()=>[e.filter,e.presets?.length],()=>{e.zoom={}}),watch(()=>[e.reset,e.filter,e.presets?.length,e.zoom?.min,e.zoom?.max],()=>{r(!0),n&&(n.scrollTop=0)}))},onunmount() Line truncated
It is recommend to list it as trusted.`,`right`)});if(i.isEnabled&&i.possibleProxyHeader&&!e.formSettings.trustedProxy.headers.includes(i.possibleProxyHeader))return t.i({className:`ri-alert-line txt-hint`,ariaDescription:app.attrs.tooltip(`The configured proxy header doesn't match with the detected one.`,`right`)})},t.div({className:`flex-fill`}),()=>i.isEnabled?t.span({className:`label success`},`Enabled`):t.span({className:`label`},`Disabled`),()=>{if(!app.utils.isEmpty(app.store.errors?.trustedProxy))return t.i({className:`ri-error-warning-fill txt-danger`,ariaDescription:app.attrs.tooltip(`Has errors`,`left`)})}),t.p({className:`m-t-0`},`Below you should see your real IP. If not - configure the correct proxy header for your environment.`),t.div({hidden:()=>i.isLoading,className:`alert info m-b-sm`},t.div({className:`flex gap-5`},t.span(null,`Resolved user IP:`),t.strong(null,()=>i.realIP||`N/A`)),t.div({className:`flex gap-5`},t.span(null,`Detected proxy header:`),t.strong(null,()=>i.possibleProxyHeader||`N/A`))),t.div({className:`content m-b-sm`},t.p(null,`
When PocketBase is deployed on platforms like Fly or it is accessible through proxies such as
NGINX, requests from different users will originate from the same IP address (the IP of the proxy
connecting to your PocketBase app).
`),t.p(null,`
In this case to retrieve the actual user IP (used for rate limiting, logging, etc.) you need to
properly configure your proxy and list below the trusted headers that PocketBase could use to
extract the user IP.
`),t.p({className:`txt-bold`},`When using such proxy, to avoid spoofing it is recommended to:`),t.ul({className:`txt-bold`},t.li(null,`use headers that are controlled only by the proxy and cannot be manually set by the users`),t.li(null,`make sure that the PocketBase server can be accessed ONLY through the proxy`)),t.p(null,`You can clear the headers field if PocketBase is not deployed behind a proxy.`)),t.div({className:`grid sm`},t.div({className:`col-lg-9`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.headers`},`Trusted IP proxy headers`),t.input({type:`text`,id:`trustedProxy.headers`,name:`trustedProxy.headers`,placeholder:`Leave empty to disable`,value:()=>app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers),oninput:n=>{let r=app.utils.splitNonEmpty(n.target.value,`,`),i=app.utils.joinNonEmpty(r);app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers)!=i&&(e.formSettings.trustedProxy.headers=r)}})),t.div({className:`field addon`},t.button({type:`button`,className:()=>`btn sm secondary transparent ${app.utils.isEmpty(e.formSettings.trustedProxy.headers)?`hidden`:``}`,onclick:()=>{e.formSettings.trustedProxy.headers=[]}},t.span({className:`txt`},`Clear`)))),t.div({className:`field-help`},`Comma separated list of headers such as: `,t.div({className:`inline-flex gap-5`},()=>i.suggestedProxyHeaders.map(n=>t.div({type:`button`,className:`label sm link-hint`,onclick:()=>{e.formSettings.trustedProxy.headers=[n]},textContent:n}))))),t.div({className:`col-lg-3`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.useLeftmostIP`},t.span({className:`txt`},`IP priority`),t.i({className:`ri-information-line tooltip-right`,ariaDescription:app.attrs.tooltip(`This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.`)})),app.components.select({id:`trustedProxy.useLeftmostIP`,name:`trustedProxy.useLeftmostIP`,options:r,required:!0,value:()=>e.formSettings.trustedProxy.useLeftmostIP||!1,onchange:n=>{e.formSettings.trustedProxy.useLeftmostIP=n?.[0]?.value}})))))}function Qr(){app.store.title=`Application settings`;let e=store({isLoading:!1,isSaving:!1,formSettings:null,originalFormSettings:null,get originalFormSettingsHash(){return JSON.stringify(e.originalFormSettings)},get hasChanges(){return e.originalFormSettingsHash!=JSON.stringify(e.formSettings)}});n();async function n(){e.isLoading=!0;try{i(await app.pb.settings.getAll()),e.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function r(){if(!(e.isSaving||!e.hasChanges)){e.isSaving=!0,e.formSettings.rateLimits.rules=Yr(e.formSettings.rateLimits.rules);try{let n=app.utils.filterRedactedProps(e.formSettings);i(await app.pb.settings.update(n)),app.toasts.success(`Successfully saved application settings.`)}catch(e){app.checkApiError(e)}e.isSaving=!1}}function i(n={}){if(app.store.settings=JSON.parse(JSON.stringify(n)),!n.meta?.accentColor){let e=window.getComputedStyle(document.documentElement)?.getPropertyValue(`--accentColor`);e?.startsWith(`#`)&&(n.meta=n.meta||{},n.meta.accentColor=e.toLowerCase()||``)}e.originalFormSettings={meta:n.meta||{},batch:n.batch||{},trustedProxy:n.trustedProxy||{headers:[]},rateLimits:n.rateLimits||{rules:[]}},Yr(e.originalFormSettings.rateLimits.rules),e.formSettings=JSON.parse(JSON.stringify(e.originalFormSettings))}function a(){e.formSettings=JSON.parse(e.originalFormSettingsHash)}return t.div({pbEvent:`pageApplicationSettings`,className:`page page-application-settings`},Wr(),t.div({className:`page-content full-height`},t.header({className:`page-header`},t.nav({className:`breadcrumbs`},t.div({className:`breadcrumb-item`},`Settings`),t.div({className:`breadcrumb-item`},`Application`))),t.div({className:`wrapper m-b-base`},()=>e.isLoading?t.div({className:`block txt-center`},t.span({className:`loader lg`})):t.form({pbEvent:`applicationSettingsForm`,className:`grid application-settings-form`,inert:()=>e.isSaving,onsubmit:e=>{e.preventDefault(),r()}},t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appName`},`Application name`),t.input({id:`meta.appName`,name:`meta.appName`,type:`text`,required:!0,value:()=>e.formSettings.meta.appName||``,oninput:n=>e.formSettings.meta.appName=n.target.value}))),t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appURL`},`Application URL`),t.input({id:`meta.appURL`,name:`meta.appURL`,type:`url`,required:!0,value:()=>e.formSettings.meta.appURL||``,oninput:n=>e.formSettings.meta.appURL=n.target.value}))),t.div({className:`col-md-2`},()=>$r(e,e.isSaving)),t.div({className:`col-lg-12`},()=>Zr(e),()=>Xr(e),()=>Gr(e)),t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`meta.hideControls`,name:`meta.hideControls`,type:`checkbox`,className:`switch`,checked:()=>e.formSettings.meta.hideControls,onchange:n=>e.formSettings.meta.hideControls=n.target.check Line truncated
Records update will also require an extra unlock step before save.`)})))),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({type:`button`,className:`btn transparent secondary`,disabled:()=>e.isSaving,hidden:()=>!e.hasChanges,onclick:a},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${e.isSaving?`loading`:``}`,disabled:()=>!e.hasChanges||e.isSaving},t.span({className:`txt`},`Save changes`)))))),t.footer({className:`page-footer`},app.components.credits())))}function $r(e){let n=`accent_`+app.utils.randomString(),r=store({isTooLight:!1}),i,a;function o(e){clearTimeout(a),document.documentElement.style.setProperty(`--animationSpeed`,`0`),e?document.documentElement.style.setProperty(`--accentColor`,e.toLowerCase()):document.documentElement.style.removeProperty(`--accentColor`),a=setTimeout(()=>{document.documentElement.style.removeProperty(`--animationSpeed`)},100)}let s=[watch(()=>e.formSettings?.meta?.accentColor,e=>{clearTimeout(i),i=setTimeout(()=>{o(e)},100)})];return t.div({className:`field`,ariaDescription:app.attrs.tooltip(()=>r.isTooLight?`Invalid - color is too light`:``),onunmount:()=>{clearTimeout(i),o(e.formSettings.meta.accentColor),s.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.span({className:`txt`},`Accent`),t.i({hidden:()=>!r.isTooLight,className:`txt-warning ri-alert-line`})),app.components.colorPicker({id:n,name:`meta.accentColor`,predefinedColors:()=>app.store.predefinedAccentColors,value:()=>e.formSettings.meta.accentColor,onchange:n=>{if(r.isTooLight=!1,!app.utils.isDarkEnoughForWhiteText(n)){r.isTooLight=!0;return}e.formSettings.meta.accentColor=n}}))}function ei(e={}){let n=store({onsave:null}),r=app.utils.extendStore(n,e),i=[{cron:`0 0 * * *`,label:`Every day at 00:00h`},{cron:`0 0 * * 0`,label:`Every sunday at 00:00h`},{cron:`0 0 * * 1,3`,label:`Every Mon and Wed at 00:00h`},{cron:`0 0 1 * *`,label:`Every first day of the month at 00:00h`}],a=store({showForm:!1,isLoading:!1,isSaving:!1,formSettings:null,initSerialized:`null`,enableAutoBackups:!1,get hasChanges(){return a.initSerialized!=JSON.stringify(a.formSettings)}});async function o(){a.isLoading=!0;try{c(await app.pb.settings.getAll()),a.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function s(){if(!(a.isSaving||!a.hasChanges)){a.isSaving=!0;try{let e=app.utils.filterRedactedProps(a.formSettings),r=await app.pb.settings.update(e);n.onsave?.(r),c(r),app.toasts.success(`Successfully saved backups settings.`)}catch(e){app.checkApiError(e)}a.isSaving=!1}}function c(e={}){app.store.settings=JSON.parse(JSON.stringify(e)),a.formSettings={backups:e?.backups||{}},a.enableAutoBackups=!!a.formSettings.backups.cron,a.initSerialized=JSON.stringify(a.formSettings)}function l(){a.formSettings=JSON.parse(a.initSerialized),a.enableAutoBackups=!!a.formSettings.backups.cron}return r.push(watch(()=>{!a.enableAutoBackups&&a.formSettings?.backups?.cron&&(a.formSettings.backups.cron=``)})),t.div({className:`block backups-settings-form-wrapper`,onmount:()=>{o()},onunmount:()=>{r.forEach(e=>e?.unwatch())}},t.button({type:`button`,className:()=>`btn secondary ${a.isLoading?`loading`:``}`,disabled:()=>a.isLoading||a.hasChanges,onclick:()=>a.showForm=!a.showForm},t.span({className:`txt`},`Backup options`),t.i({className:()=>a.showForm?`ri-arrow-up-s-line`:`ri-arrow-down-s-line`,ariaHidden:!0})),app.components.slide(()=>a.showForm,t.form({pbEvent:`backupsSettingsForm`,className:`grid backups-settings-form m-t-base`,inert:()=>a.isSaving,onsubmit:e=>{e.preventDefault(),s()}},()=>a.isLoading?t.div({className:`col-lg-12 txt-center`},t.span({className:`loader lg`})):[t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`enableAutoBackupsToggle`,type:`checkbox`,className:`switch`,checked:()=>a.enableAutoBackups,onchange:e=>{a.enableAutoBackups=e.target.checked,a.formSettings.backups.cron||(a.formSettings.backups.cron=i[0].cron)}}),t.label({htmlFor:`enableAutoBackupsToggle`},`Enable auto backups`)),app.components.slide(()=>a.enableAutoBackups,t.div({className:`grid m-t-base m-b-base`},t.div({className:`col-lg-6`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`backups.cron`},`Cron expression`),t.input({id:`backups.cron`,name:`backups.cron`,className:`txt-code`,type:`text`,placeholder:`e.g. 0 0 * * *`,required:()=>a.enableAutoBackups,value:()=>a.formSettings.backups.cron,oninput:e=>a.formSettings.backups.cron=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,className:`btn outline sm`,"html-popovertarget":`cron-presets-dropdown`},t.span({className:`txt`},`Presets`),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0})),t.div({id:`cron-presets-dropdown`,className:`dropdown sm txt-nowrap`,popover:`auto`},()=>i.map(e=>t.button({type:`button`,className:()=>`dropdown-item ${a.formSettings.backups.cron==e.cron?`active`:``}`,textContent:e.label,onclick:n=>{a.formSettings.backups.cro Line truncated
@annually
@monthly
@weekly
@daily
@midnight
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function ti(e={oncreated:null}){let n=ni(e);n&&(document.body.appendChild(n),app.modals.open(n))}function ni(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup other concurrent write requests may fail since the database will be temporary "locked" (this usually happens only during the ZIP generation).`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function ri(e){let n=ii(e);document.body.appendChild(n),app.modals.open(n)}function ii(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an overLine truncated
-81
View File
@@ -1,81 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./docsList-BAfVNUIM.js","./expandInfo-DGS0CLSa.js","./fieldsInfo-Bz62125-.js","./filterSyntax-Df6AnI5x.js","./docsView-Be6Uhvcf.js","./docsCreate-Be3S3y5K.js","./docsUpdate-LtD8DbWI.js","./docsListAuthMethods-9feSopQX.js","./docsAuthWithPassword-DEWj8Jyn.js","./docsAuthWithOAuth2-DUIE4EoY.js","./docsAuthWithOTP-B7z2VJzp.js","./docsAuthRefresh-UjveHHwo.js","./pageInstaller-CboSj8bL.js","./pocketbase.es-B_4DUNUU.js","./pageConfirmPasswordReset-xSsXQyFi.js","./pageConfirmVerification-BSn_UexA.js","./pageConfirmEmailChange-DWXtM27Q.js"])))=>i.map(i=>d[i]);
import{a as e,n,r,t as i}from"./pocketbase.es-B_4DUNUU.js";var a=Object.create,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,l=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,d=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),f=(e,n,r,i)=>{if(n&&typeof n==`object`||typeof n==`function`)for(var a=c(n),l=0,d=a.length,f;l<d;l++)f=a[l],!u.call(e,f)&&f!==r&&o(e,f,{get:(e=>n[e]).bind(null,f),enumerable:!(i=s(n,f))||i.enumerable});return e},p=(e,n,r)=>(r=e==null?{}:a(l(e)),f(n||!e||!e.__esModule?o(r,`default`,{value:e,enumerable:!0}):r,e));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))r(e);new MutationObserver(e=>{for(let n of e)if(n.type===`childList`)for(let e of n.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&r(e)}).observe(document,{childList:!0,subtree:!0});function n(e){let n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?n.credentials=`include`:e.crossOrigin===`anonymous`?n.credentials=`omit`:n.credentials=`same-origin`,n}function r(e){if(e.ep)return;e.ep=!0;let r=n(e);fetch(e.href,r)}})();var m=`abcdefghijklmnopqrstuvwxyz0123456789`,h=`pb_redirect`,g=store({hash:window.location.hash});window.addEventListener(`hashchange`,()=>{g.hash=window.location.hash}),Prism.languages.pbrule={string:Prism.languages.js.string,number:Prism.languages.js.number,function:Prism.languages.js.function,boolean:/\b(?:true|false)\b/i,constant:/\b(?:null)\b/i,comment:{pattern:/\/\/.*/,greedy:!0},italic:/_via_|\:\w+/,keyword:/&&|\|\||\??(?:!~|!=|>=|<=|=|~|>|>|<)(?=[@\w\s]|$)/};var _={isObject(e){return typeof e==`object`&&!!e&&e.constructor===Object},isEmpty(e){return e==null||e===``||Array.isArray(e)&&e.length===0||typeof e==`object`&&app.utils.isEmptyObject(e)},isEmptyObject(e){for(let n in e)return!1;return!0},toArray(e,n=!1){return Array.isArray(e)?e.slice():(n||!_.isEmpty(e))&&e!==void 0?[e]:[]},removeByValue(e,n){if(!Array.isArray(e)){console.warn(`[removeByValue] not an array:`,e);return}for(let r=e.length-1;r>=0;r--)if(e[r]==n){e.splice(r,1);break}},removeByKey(e,n,r){if(!Array.isArray(e)){console.warn(`[removeByKey] not an array:`,e);return}for(let i in e)if(e[i][n]==r){e.splice(i,1);break}},pushUnique(e,n){if(!Array.isArray(e)){console.warn(`[pushUnique] not an array:`,e);return}e.includes(n)||e.push(n)},mergeUnique(e,n){for(let r of n)app.utils.pushUnique(e,r);return e},pushOrReplaceObject(e,n,r=`id`){for(let i=e.length-1;i>=0;i--)if(e[i][r]==n[r]){e[i]=n;return}e.push(n)},filterDuplicatesByKey(e,n=`id`){e=Array.isArray(e)?e:[];let r={};for(let i of e)r[i[n]]=i;return Object.values(r)},filterRedactedProps(e,n=`******`){let r=JSON.parse(JSON.stringify(e||{}));for(let e in r)typeof r[e]==`object`&&r[e]!==null?r[e]=_.filterRedactedProps(r[e],n):r[e]===n&&delete r[e];return r},getByPath(e,n,r=null,i=`.`){let a=e||{},o=(n||``).split(i);for(let e of o){if(!_.isObject(a)&&!Array.isArray(a)||a[e]===void 0)return r;a=a[e]}return a},setByPath(e,n,r,i=`.`){if(typeof e!=`object`||!e){console.warn(`setByPath: data not an object or array.`);return}let a=e,o=n.split(i),s=o.pop();for(let e of o)(!_.isObject(a)&&!Array.isArray(a)||!_.isObject(a[e])&&!Array.isArray(a[e]))&&(a[e]={}),a=a[e];a[s]=r},deleteByPath(e,n,r=`.`){let i=e||{},a=(n||``).split(r),o=a.pop();for(let e of a)(!_.isObject(i)&&!Array.isArray(i)||!_.isObject(i[e])&&!Array.isArray(i[e]))&&(i[e]={}),i=i[e];Array.isArray(i)?i.splice(o,1):_.isObject(i)&&delete i[o],a.length>0&&(Array.isArray(i)&&!i.length||_.isObject(i)&&!Object.keys(i).length)&&(Array.isArray(e)&&e.length>0||_.isObject(e)&&Object.keys(e).length>0)&&_.deleteByPath(e,a.join(r),r)},emptyClone(e,n=[]){let r=JSON.parse(JSON.stringify(e));for(let e in r)n.includes(e)||(typeof r[e]==`string`?r[e]=``:typeof r[e]==`number`?r[e]=0:Array.isArray(r[e])?r[e]=[]:app.utils.isObject(r[e])&&(r[e]={}));return r},randomString(e=8,n=m){let r=``;for(let i=0;i<e;i++)r+=n.charAt(Math.floor(Math.random()*n.length));return r},randomSecret(e=30){if(typeof crypto>`u`)return app.utils.randomString(e);let n=new Uint8Array(e);crypto.getRandomValues(n);let r=``;for(let i=0;i<e;i++)r+=`-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(n[i]%64);return r},sentenize(e,n=!0){if(typeof e!=`string`)return``;if(e=e.trim().split(`_`).join(` `),e===``)return e;if(e=e[0].toUpperCase()+e.substring(1),n){let n=e[e.length-1];n!==`.`&&n!==`?`&&n!==`!`&&(e+=`.`)}return e},trimQuotedValue(e){return typeof e==`string`&&(e[0]==`"`||e[0]==`'`||e[0]=="`")&&e[0]==e[e.length-1]?e.slice(1,-1):e},slugify(e,n=`_`,r=[]){if(e===``)return``;let i={a:/а|à|á|å|â/gi,b:/б/gi,c:/ц|ç/gi,d:/д/gi,e:/е|è|é|ê|ẽ|ë/gi,f:/ф/gi,g:/г/gi,h:/х/gi,i:/й|и|ì|í|î/gi,j:/ж/gi,k:/к/gi,l:/л/gi,m:/м/gi,n:/н|ñ/gi,o:/о|ò|ó|ô|ø/gi,p:/п/gi,q:/я/gi,r:/р/gi,Line truncated
`),n+=r.map(e=>{let n=``;return e.name.includes(`(`)||e.name.includes(` `)?n+=e.name:n+="`"+e.name+"`",e.collate&&(n+=` COLLATE `+e.collate),e.sort&&(n+=` `+e.sort.toUpperCase()),n}).join(`,
`),r.length>1&&(n+=`
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=_,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
`+Ie:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.classList.add(`rotate`),a.addEventListener(`animationend`,()=>{a.classList.remove(`rotate`)}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.classList.remove(`rotate`)},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id Line truncated
<video controls width="300">
<source src="${a}" />
<p>Download: <a href="${a}" download="${i}">${i}</a>.</p>
</video>
`)}})}},{type:`menuitem`,text:`Manual embed`,onAction:()=>{tinymce.activeEditor.execCommand(`mceMedia`)}}])}})}var qe=`DIV.P.A.EM.B.STRONG.H1.H2.H3.H4.H5.H6.TABLE.TR.TD.TH.TBODY.THEAD.TFOOT.BR.HR.Q.SUP.SUB.DEL.IMG.OL.UL.LI.CODE`.split(`.`);function Je(e){if(e){for(let n of e.children)Je(n);qe.includes(e.tagName)?(e.removeAttribute(`style`),e.removeAttribute(`class`)):Ye(e)}}function Ye(e){let n=e.parentNode;for(;e.firstChild;)n.insertBefore(e.firstChild,e);n.removeChild(e)}async function Xe(){if(window.tinymce!==void 0)return;let e=`lazy-tinymce-js`;return document.getElementById(e)?new Promise((e,n)=>{function r(){document.removeEventListener(`tinymceLoadSuccess`,i),document.removeEventListener(`tinymceLoadError`,a)}let i=function(){r(),e()},a=function(e){r(),n(e?.details)};document.addEventListener(`tinymceLoadSuccess`,i),document.addEventListener(`tinymceLoadError`,a)}):new Promise((n,r)=>{document.head.querySelector(`#shablon-script`).after(t.script({id:e,src:`./libs/tinymce/tinymce.min.js`,onload:()=>{n()},onerror:e=>{console.warn(`failed to load tinymce.min.js:`,e),r(e)}}))}).then(()=>{document.dispatchEvent(new CustomEvent(`tinymceLoadSuccess`))}).catch(e=>{document.dispatchEvent(new CustomEvent(`tinymceLoadError`,{detail:e}))})}window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.colorPicker=function(e={}){let n=`picker_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,name:``,required:!1,disabled:!1,value:``,predefinedColors:[],onchange:e=>{},onmount:e=>{},onunmount:e=>{}}),i=app.utils.extendStore(r,e),a=store({inputValue:`#ffffff`});function o(e){e=e?.toLowerCase()||``,r.onchange?.(e)}let s,c=t.input({type:`color`,className:`color-picker-input`,id:()=>r.id,name:()=>r.name,required:()=>r.required,disabled:()=>r.disabled,value:()=>(a.inputValue=r.value||`#ffffff`,r.value||void 0),oninput:e=>{a.inputValue=e.target.value,clearTimeout(s),s=setTimeout(()=>{o(e.target.value)},50)}});return t.div({rid:r.rid,hidden:()=>r.hidden,inert:()=>r.inert,className:()=>`color-picker ${r.className}`,onmount:e=>{r.onmount?.(e)},onunmount:e=>{clearTimeout(s),r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.div({className:`color-picker-input-wrapper`},c,t.output({className:`result`,style:()=>`color: lch(from ${a.inputValue||`#ffffff`} calc((49 - l) * infinity) 0 0);`,textContent:()=>a.inputValue})),t.button({hidden:()=>!r.predefinedColors.length,type:`button`,title:`Predefined colors`,className:`link-hint predefined-colors-btn`,"html-popovertarget":n+`predefined-colors-dropdown`},t.i({className:`ri-arrow-down-s-line`,ariaHidden:!0})),t.div({pbEvent:`predefinedColorsDropdown`,id:n+`predefined-colors-dropdown`,className:`dropdown predefined-colors-dropdown`,popover:`auto`},t.div({className:`predefined-colors-list`},()=>r.predefinedColors?.map(e=>t.button({type:`button`,className:()=>`color ${r.value==e?`active`:``}`,style:`background:${e}`,onclick:n=>{c&&(n.target.closest(`.dropdown`)?.hidePopover(),c.value=e||void 0,c.dispatchEvent(new Event(`input`,{bubbles:!0})))}})))))},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.ruleField=function(e={}){let n=`rule_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,required:!1,disabled:!1,name:void 0,label:void 0,help:void 0,value:null,nullable:!0,placeholder:`Leave empty to grant everyone access...`,autocomplete:e=>[],oninput:e=>{},onmount:e=>{},onunmount:e=>{},get isLocked(){return r.value==null}}),i=app.utils.extendStore(r,e,`isLocked`),a,o=``;function s(e){r.value=e,r.oninput?.(e),a?.dispatchEvent(new CustomEvent(`change`,{detail:e}))}function c(){r.value!==null&&(o=r.value,s(null))}function l(){s(o??``),setTimeout(()=>{document.getElementById(n)?.focus()},0)}return a=t.div({rid:r.rid,id:()=>r.id,hidden:()=>r.hidden,inert:()=>r.inert,"html-name":()=>r.name,className:()=>[`field`,`rule-field`,r.required?`required`:null,r.value===null?`locked`:null,r.disabled?`disabled`:null].filter(Boolean).join(` `),onmount:e=>{r.onmount?.(e)},onunmount:e=>{r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},e=>r.label?typeof r.label==`function`?r.label(e):typeof r.label==`string`?t.span({className:`txt`},r.label):r.label:t.span({className:`txt`},`Rule`),t.span({hidden:()=>!r.isLocked,className:`txt superusers-label`},`(Superusers only)`)),e=>r.isLocked?t.button({type:`button`,className:`unlock-overlay`,disabled:()=>r.disabled,onclick:l},t.span({className:`txt`},`Unlock and set custom rule`),t.i({className:`ri-lock-unlock-line`,ariaHidden:!0})):[app.components.codeEditor({id:n,language:`pbrule`,required:()=>r.required,disabled:()=>r.disabled,value:()=>r.value,oninput:s,placeholder:()=>r.placeholder,autocomplete:r.autocomplete,autocompleteContainer:e}),t.button({hidden:()=>!r.nullable,type:`button`,className:`superuser-toggle`,disabled:()=>r.disabled,onclick:c},t. Line truncated
<defs>
<marker id="${s}_arrow1" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<g class="paths-group" marker-end="url(#${s}_arrow1)"></g>
`;let h=m.querySelector(`.paths-group`),g=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);g.classList.add(`erd-paths`,`front`),g.innerHTML=`
<defs>
<marker id="${s}_arrow2" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
</defs>
<g class="paths-group" marker-end="url(#${s}_arrow2)"></g>
`;let _=g.querySelector(`.paths-group`);return a=t.div({tabIndex:-1,rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,className:()=>`erd ${n.className} ${i.isUpdating?`updating`:``} ${i.isPanning?`panning`:``} ${i.activeCollection?`active`:``}`,onkeydown:e=>{(e.ctrlKey||e.metaKey)&&e.key==`0`&&f()},onmount:async e=>{e.addEventListener(`wheel`,r=>{r.preventDefault();let a=e.getBoundingClientRect(),o=r.clientX-a.left,s=r.clientY-a.top,c=(o-i.viewX)/n.scale,l=(s-i.viewY)/n.scale,u=Math.min(Math.max(-r.deltaY*.001+n.scale,$e),Qe);i.viewX=o-c*u,i.viewY=s-l*u,n.scale=u}),e.addEventListener(`pointerdown`,e=>{e.buttons==1&&(i.isPanning=!0,i.panStartX=e.clientX-i.viewX,i.panStartY=e.clientY-i.viewY)}),e._ondragging=function(e){i.isPanning&&(i.viewX=e.clientX-i.panStartX,i.viewY=e.clientY-i.panStartY)},e._ondragstop=function(){i.isPanning=!1},window.addEventListener(`pointermove`,e._ondragging),window.addEventListener(`pointerup`,e._ondragstop),c()},onunmount:e=>{window.removeEventListener(`pointermove`,e._ondragging),window.removeEventListener(`pointerup`,e._ondragstop),r.forEach(e=>e?.unwatch())}},t.div({className:`erd-area`,style:()=>`transform: translate(${i.viewX}px, ${i.viewY}px) scale(${n.scale});`},m,t.div({className:`erd-tables`},()=>n.collections.map(e=>t.div({style:()=>`left:${i.positions[e.id]?.x||0}px;top:${i.positions[e.id]?.y||0}px`,className:()=>`erd-table type-${e.type} ${p(e)?`active`:``} ${e.system?`system`:``}`,"html-data-collection-id":()=>e.id,"html-data-collection-name":()=>e.name,onmouseenter:()=>{h.querySelectorAll(`[data-to="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-to`),_.append(e)}),h.querySelectorAll(`[data-from="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-from`),_.append(e)}),i.activeCollection=e},onmouseleave:()=>{for(let e of _.children)e.classList.remove(`active-from`,`active-to`);h.append(..._.children),i.activeCollection=null}},t.div({className:`erd-table-row header`},()=>e.name),()=>e.fields?.map(e=>t.div({className:`erd-table-row type-${e.type} ${e.primaryKey?`primary-key`:``}`,"html-data-field-id":()=>e.id,"html-data-field-name":()=>e.name},t.i({ariaHidden:!0,title:()=>e.type,className:()=>`field-icon ${app.fieldTypes[e.type].icon||app.utils.fallbackFieldIcon}`}),t.span({className:`field-name`},()=>e.name),()=>{if(e.hidden)return t.span({className:`label danger field-hidden-label`},`Hidden`)},()=>{if(e.maxSelect!==void 0)return t.span({className:`meta`},e.maxSelect>1?`multiple`:`single`)}))))),g),t.nav({className:`erd-nav`,onmousedown:e=>{e.stopImmediatePropagation()}},t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom in`,onclick:()=>{n.scale+=.05}},t.i({className:`ri-add-line`,ariaHidden:!0})),t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom out`,onclick:()=>{n.scale-=.05}},t.i({className:`ri-subtract-line`,ariaHidden:!0})))),a};function tt(e,n,r=1,i=``,a=``,o=2){if(!e||!n)return;let s=e.closest(`.erd-area`).getBoundingClientRect(),c=e.getBoundingClientRect(),l=n.getBoundingClientRect();o*=r;let u=c.top-s.top+c.height/2,d=l.top-s.top+l.height/2,f,p;c.left<l.left?(f=c.left-s.left+c.width+o,p=l.left-s.left-o):c.left>l.left?(f=c.left-s.left-o,p=l.left-s.left+l.width+o):(f=c.left-s.left-o,p=l.left-s.left-o),f/=r,p/=r,u/=r,d/=r;let m=f+(p-f)/2;f==p&&(m-=20);let h=`M ${f} ${u}
L ${m} ${u}
L ${m} ${d}
L ${p} ${d}`,g=document.createElementNS(`http://www.w3.org/2000/svg`,`path`);return g.setAttribute(`class`,`relation-path`),g.setAttribute(`data-from`,i||``),g.setAttribute(`data-to`,a||``),g.setAttribute(`d`,h),g}window.app=window.app||{},window.app.components=window.app.components||{};var N=1e3;window.app.components.pageSidebar=function(e={},...n){let r,i=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,widthHistoryKey:`pbPageSidebarWidth`,onmount:void 0,onunmount:void 0}),a=app.utils.extendStore(i,e),o=store({responsiveShow:!1}),s;function c(){if(r){if(window.innerWidth>N){o.responsiveShow=!1,r.dataset.responsive=!1,s?.remove(),s=null;return}r.dataset.responsive=!0,s||(s=t.button({type:`button`,className:`btn transparent secondary responsive-sidebar-btn`,title:`Toggle sidebar`,onclick:e=>{e.stopPropagation(),o.responsiveShow=!o.responsiveShow}},t.i({className:`ri-menu-2-line`,ariaHidden:!0})),document.body.querySelector(`.page-header .breadcrumbs`).before(s))}}function l(e){if(e.target.closest(`.responsive-close`)){o.responsiveShow=!1;return}if(!(e.target.closest(`.page-sidebar`)||e.target.closest(`.app-header`)||e.target.closest(`.modal`)))return e.preventDefault(),e.stopImmediatePropagation(),o.responsiveShow=!1,!1}return a.push(watch(()=>o.responsiveShow,e=>{e?window.addEventListener(`click`,l,!0):window.removeEventListener(`click`,l,!0)})),r=t.aside({pbEvent:`pageSidebar`,rid:i.rid,id:()=>i.id,hidden:()=>i.hidden,inert:()=>i.inert,className:()=>`page-sidebar ${i.className} ${o.responsiveShow?`active`:``}`,onmount:e=>{c(e),window.addEventListener(`resize`,c),i.onmount?.(e)},onunmount:e=>{i.onunmount?.(e),window.removeEventListener(`click`,l,!0),window.removeEventListener(`resize`,c),s?.remove(),a.forEach(e=>e?.unwatch())}},e=>{let n;return i.widthHistoryKey&&(n=localStorage.getItem(i.widthHistoryKey),n&&(e.style.width=n)),app.components.dragline({ondragstart:n=>{e._startWidth=e.offsetWidth},ondragging:(r,a,o)=>{n=e._startWidth+a+`px`,e.style.width=n,i.widthHistoryKey&&localStorage.setItem(i.widthHistoryKey,n)}})},...n),r};var P=`modulepreload`,nt=function(e,n){return new URL(e,n).href},rt={},F=function(e,n,r){let i=Promise.resolve();if(n&&n.length>0){let e=document.getElementsByTagName(`link`),a=document.querySelector(`meta[property=csp-nonce]`),o=a?.nonce||a?.getAttribute(`nonce`);function s(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}i=s(n.map(n=>{if(n=nt(n,r),n in rt)return;rt[n]=!0;let i=n.endsWith(`.css`),a=i?`[rel="stylesheet"]`:``;if(r)for(let r=e.length-1;r>=0;r--){let a=e[r];if(a.href===n&&(!i||a.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${n}"]${a}`))return;let s=document.createElement(`link`);if(s.rel=i?`stylesheet`:P,i||(s.as=`script`),s.crossOrigin=``,s.href=n,o&&s.setAttribute(`nonce`,o),document.head.appendChild(s),i)return new Promise((e,r)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>r(Error(`Unable to preload CSS for ${n}`)))})}))}function a(e){let n=new Event(`vite:preloadError`,{cancelable:!0});if(n.payload=e,window.dispatchEvent(n),!n.defaultPrevented)throw e}return i.then(n=>{for(let e of n||[])e.status===`rejected`&&a(e.reason);return e().catch(a)})};window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openApiPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=it(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};function it(e,n){if(!e){console.warn(`[apiPreviewModal] missing required collection`);return}let r,i=store({activeTab:`List/Search`,tabEl:null,isLoading:!1}),a={"List/Search":async e=>{let{docsList:n}=await F(async()=>{let{docsList:e}=await import(`./docsList-BAfVNUIM.js`);return{docsList:e}},__vite__mapDeps([0,1,2,3]),import.meta.url);return i.tabEl=n(e)},View:async e=>{let{docsView:n}=await F(async()=>{let{docsView:e}=await import(`./docsView-Be6Uhvcf.js`);return{docsView:e}},__vite__mapDeps([4,1,2]),import.meta.url);return i.tabEl=n(e)}};e.type!=`view`&&(a.Create=async e=>{let{docsCreate:n}=await F(async()=>{let{docsCreate:e}=await import(`./docsCreate-Be3S3y5K.js`);return{docsCreate:e}},__vite__mapDeps([5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Update=async e=>{let{docsUpdate:n}=await F(async()=>{let{docsUpdate:e}=await import(`./docsUpdate-LtD8DbWI.js`);return{docsUpdate:e}},__vite__mapDeps([6,5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Delete=async e=>{let{docsDelete:n}=await F(async()=>{let{docsDelete:e}=await import(`./docsDelete-CybOn5jy.js`);return{docsDelete:e}},[],import.meta.url);return i.tabEl=n(e)},a.Realtime=async e=>{let{docsRealtime:n}=await F(async()=>{let{docsRealtime:e}=await import(`./docsRealtime-PMESvmJN.js`);return{docsRealtime:e}},[],import.meta.url);return i.tabEl=n(e)},a.Batch=async e=>{let{docsBatch:n}=await F(async()=>{let{docsBatch:e}=await import(`./docsBatch-DNJl1NTn.js`);return{docsBatch: Line truncated
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i}');
// load the token into the store
const token = '...';
pb.authStore.save(token, null);
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/js-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`JS SDK docs`}))},{title:`Dart SDK`,language:`dart`,value:`
import 'package:pocketbase/pocketbase.dart';
final pb = PocketBase('${i}');
// load the token into the store
final token = '...';
pb.authStore.save(token, null);
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=kt(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var Tt=[`expand`];function Et(e,n=Tt){if(n.find(n=>e[n]!==void 0)){e=Object.assign({},e);for(let r of n)delete e[r]}return e}function Dt(e){e=Et(e),app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Record copied to clipboard!`)}function Ot(e){return e?JSON.stringify(e):``}var B=`main`,V=`authProviders`;function kt(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:B,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return Ot(s.initialDraft)},get recordHash(){return Ot(s.record)},get originalRecordHash(){return Ot(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function l(){return window.localStorage.getItem(c())||``}function u(){try{let e=l();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),f()}return null}function d(e){try{window.localStorage.setItem(c(),e)}catch(e){console.warn(`saveDraft failure:`,e),f()}}function f(){window.localStorage.removeItem(c()),s.initialDraft=null}function p(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));f(),s.record=e}async function m(n){s.isLoading=!0;let r=typeof n==`string`?n:n?.id;if(!r){let e=app.utils.isObject(n)?JSON.parse(JSON.stringify(n)):{};s.originalRecord=app.utils.emptyClone(e,[`collectionId`,`collectionName`]),s.initialDraft=u(),s.record=e,s.isLoading=!1,s.isLocked=!1;return}s.isLocked=!!app.store.settings?.meta?.hideControls;try{s.originalRecord={id:r};let n=await app.pb.collection(e.name).getOne(r,{requestKey:`upsert_load_`+r});s.originalRecord=n,s.initialDraft=u(),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}async function h(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function g(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await h(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),f(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c))),r.onsave?.(structuredClone(c),o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0)}catch(e){e?.isAbort||(s.isSaving=!1,app.checkApiError(e,!1),app.toasts.error(e.message||`Failed to save record.`,{key:`recordSave`}))}}}function _(){f(),s.record=JSON.parse(JSON.stringify(s.originalRecord))}async function v(){let n=s.originalRecord?JSON.parse(JSON.stringify(s.originalRecord)):{};n.id=``;for(let r of e.fields){let i=app.fieldTypes[r.type]?.o Line truncated
The field cannot be presentable if hidden.`),n})})))},()=>{if(i.showHidden)return t.div({className:`field prop-hidden`},t.input({type:`checkbox`,id:r+`.hidden`,className:`sm`,name:()=>`fields.${e.fieldIndex}.hidden`,checked:()=>!!e.field.hidden,onchange:n=>e.field.hidden=n.target.checked}),t.label({htmlFor:r+`.hidden`},t.span({className:`txt`},`Hidden`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Hide from the JSON API response and filters.`)})))},t.button({hidden:()=>!i.showDuplicate&&(!i.showRemove||e.field.system),type:`button`,title:`More options`,className:`btn sm circle transparent secondary more-btn m-l-auto`,"html-popovertarget":r+`_options_dropdown`},t.i({className:`ri-more-line`,ariaHidden:!0})),t.div({id:r+`_options_dropdown`,className:`dropdown sm field-options-dropdown`,popover:`auto`},()=>{if(i.showDuplicate)return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Duplicate`,onclick:e=>{o(),e.target.closest(`.dropdown`).hidePopover()}})},()=>{if(!(!i.showRemove||e.field.system))return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Remove`,onclick:e=>{s(),e.target.closest(`.dropdown`).hidePopover(),e.target.closest(`details`).open=!1}})})))};function K(e,n=`field`){let r=n,i=2,a=n.match(/\d+$/)?.[0]||``,o=a?n.substring(0,n.length-a.length):n;for(;e?.find(e=>e.name.toLowerCase()==r.toLowerCase());)r=o+((a<<0)+i),i++;return r}function Jt(e,n,r){if(!r||n===void 0||n===r||!e?.passwordAuth?.identityFields?.length)return;let i=e.passwordAuth.identityFields;for(let e=0;e<i.length;e++)i[e]==n&&(i[e]=r)}function Yt(e,n,r){!r||n===void 0||n===r||!e?.indexes?.length||!e?.fields?.length||e.fields.find(e=>!e[`@toDelete`]&&e.name==n)||(e.indexes=e.indexes.map(e=>app.utils.replaceIndexColumn(e,n,r)))}function Xt(e){let n=`text_`+app.utils.randomString(),r=store({get hasAutogenerate(){return!app.utils.isEmpty(e.field.autogeneratePattern)&&app.utils.isEmpty(e.originalRecord?.id)},get isDisabled(){return!app.utils.isEmpty(e.originalRecord?.id)&&e.field.primaryKey},get isRequired(){return e.field.required&&!r.hasAutogenerate&&!r.isDisabled}});return t.div({className:`record-field-input field-type-text`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:n},t.i({ariaHidden:!0,className:()=>e.field.primaryKey?`ri-key-line`:app.fieldTypes.text.icon}),t.span({className:`txt`},()=>e.field.name)),t.textarea({id:n,className:`autoexpand`,rows:1,name:()=>e.field.name,required:()=>r.isRequired,disabled:()=>r.isDisabled,placeholder:()=>r.hasAutogenerate?`Leave empty to autogenerate...`:``,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value||``})),()=>{if(!e.field.primaryKey||!e.originalRecord?.id)return;let n=e.collection?.fields?.filter(e=>e.type==`autodate`)||[];if(!n.length)return;let r=[];for(let i of n)r.push(`${i.name}: ${app.utils.stringifyValue(e.record[i.name])}`);return t.div({className:`field addon`},t.i({className:`ri-information-line txt-hint link-faded`,ariaDescription:app.attrs.tooltip(r.join(`
`),`left`)}))}),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Zt(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 for no limit.`)})),t.input({type:`number`,id:n+`.min`,name:()=>`fields.${e.fieldIndex}.min`,step:1,min:0,max:2**53-1,placeholder:`No min limit`,value:()=>e.field.min||``,oninput:n=>{e.field.min=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 to fallback to the default limit.`)})),t.input({type:`number`,id:n+`.max`,name:()=>`fields.${e.fieldIndex}.max`,step:1,min:()=>e.field.min||0,max:2**53-1,placeholder:`Default to max 5000 characters`,value:()=>e.field.max||``,oninput:n=>{e.field.max=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.pattern`},t.span({className:`txt`},`Validation pattern`),()=>{if(e.field.primaryKey)return t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`All record ids have forbidden characters and unique case-insensitive (ASCII) validations in addition to the user defined regex pattern.`)})}),t.input({type:`text`,id:n+`.pattern`,name:()=>`fields.${e.fieldIndex}.pattern`,value:()=>e.field.pattern||``,oninput:n=>e.field.pattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`^[a-z0-9]+$`))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.autogeneratePattern`},t.span({className:`txt`},`Autogenerate pattern`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Set and autogenerate text matching the pattern on missing record create value.`)})),t.input({type:`text`,id:n+`.autogeneratePattern`,name:()=>`fields.${e.fieldIndex}.autogeneratePattern`,value:()=>e.field.autogeneratePattern||``,oninput:n=>e.field.autogeneratePattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`[a-z0-9]{30}`))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function Qt(e){return t.div({className:`record-field-view field-type-text`},()=>{let n=e.record[e.field.name]||``;if(n==``)return t.span({className:`missing-value`});if(e.field.primaryKey){let r=null;return e.record?.collectionName==`_superusers`&&app.store.superuser?.id==e.record?.id&&(r=t.strong({className:`txt`,textContent:` (you)`})),t.span({className:`label`},app.components.copyButton(n),t.span({className:`txt-ellipsis`},app.utils.truncate(n),r))}return e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.text={icon:`ri-text`,label:`Plain text`,settings:Zt,input:Xt,view:Qt,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function $t(e){let n=`editor_`+app.utils.randomString(),r=store({lazyEditor:null});return t.div({className:`record-field-input field-type-editor large-modal`,onmount:()=>{requestAnimationFrame(()=>{r.lazyEditor=app.components.tinymce({id:n,required:()=>e.field.required,convertURLs:()=>e.field.convertURLs,name:()=>e.field.name,value:()=>e.record[e.field.name]||``,onchange:n=>{e.record[e.field.name]=n}})})}},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.editor.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),()=>r.lazyEditor),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function en(e){let n=`f_`+app.utils.randomString();return store({showInfo:!1}),app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1,max:2**53-1,placeholder:`Default to max ~5MB`,val Line truncated
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function J(e){return t.div({className:`record-field-view field-type-email`},()=>{let n=e.record[e.field.name]||``;return n?e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n:t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.email={icon:`ri-mail-line`,label:`Email`,settings:ln,input:cn,view:J,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>`test_${app.utils.randomString(3,`123567890`)}@example.com`};function un(e){let n=`url_`+app.utils.randomString();return t.div({className:`record-field-input field-type-url`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.url.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({type:`url`,id:n,spellcheck:!1,name:()=>e.field.name,required:()=>e.field.required,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Y(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.exceptDomains`},t.span({className:`txt`},`Except domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are NOT allowed.
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function dn(e){return t.div({className:`record-field-view field-type-url`},()=>{let n=e.record[e.field.name]||``;return n?t.a({href:()=>n,className:`txt txt-ellipsis`,rel:`noopener noreferrer`,target:`_blank`,textContent:app.utils.truncate(n),ariaDescription:app.attrs.tooltip(`Open in new tab`),onclick:e=>{e.stopPropagation()}}):t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.url={icon:`ri-link`,label:`URL`,settings:Y,input:un,view:dn,dummyData:(e,n=!1)=>`https://example.com`};function fn(e){let n=`date_`+app.utils.randomString();return t.div({className:`record-field-input field-type-date`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.date.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({id:n,step:1,type:`datetime-local`,name:()=>e.field.name,required:()=>e.field.required,value:()=>app.utils.toDatetimeLocalInputValue(e.record[e.field.name]),onchange:n=>{e.record[e.field.name]=app.utils.toRFC3339Datetime(n.target.value)}})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function pn(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min date (Local)`)),t.input({type:`datetime-local`,id:n+`.min`,step:1,name:()=>`fields.${e.fieldIndex}.min`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.min),onchange:n=>{e.field.min=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max date (Local)`)),t.input({type:`datetime-local`,id:n+`.max`,step:1,name:()=>`fields.${e.fieldIndex}.max`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.max),onchange:n=>{e.field.max=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function mn(e){return t.div({className:`record-field-view field-type-date`},app.components.formattedDate({value:()=>e.record[e.field.name],short:()=>e.short}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.date={icon:`ri-calendar-line`,label:`Datetime`,settings:pn,input:fn,view:mn,dummyData:(e,n=!1)=>new Date().toISOString().replaceAll(`T`,` `)};function hn(e){delete e.clone[e.field.name]}function gn(e){let n=[{label:`Create`,value:1},{label:`Update`,value:2},{label:`Create/Update`,value:3}];function r(e){return e.onCreate&&e.onUpdate?3:e.onUpdate?2:1}function i(n){switch(n){case 1:e.field.onCreate=!0,e.field.onUpdate=!1;break;case 2:e.field.onCreate=!1,e.field.onUpdate=!0;break;case 3:e.field.onCreate=!0,e.field.onUpdate=!0;break}}let a=store({isDropdownOpen:!1});return app.components.fieldSettings(e,{header:t.div({className:`field header-select autodate-select`,ariaDescription:app.attrs.tooltip(`Auto set on`,`left`),onmount:()=>{i(r(e.field))}},app.components.select({required:!0,options:n,disabled:()=>e.originalCollection?.system,value:()=>r(e.field),onchange:e=>i(e?.[0]?.value),ondropdowntoggle:e=>{a.isDropdownOpen=e.newState==`open`}}))})}func Line truncated
Leave empty for no restriction.`)})),app.components.select({max:99,placeholder:`No restriction`,options:app.utils.mimeTypes.map(e=>({value:e.mimeType,label:()=>t.div({className:`inline-flex gap-10`},t.span({className:`txt`},e.ext||`-`),t.small({className:`txt-hint`},e.mimeType))})),name:()=>`fields.${e.fieldIndex}.mimeTypes`,value:()=>app.utils.toArray(e.field.mimeTypes),onchange:n=>e.field.mimeTypes=n.map(e=>e.value)})),t.div({className:`field-help`},t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`mimeTypesDropdown`},t.span({className:`txt`},`Choose presets`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`mimeTypesDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`image/jpeg`,`image/png`,`image/svg+xml`,`image/gif`,`image/webp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Images (jpg, png, svg, gif, webp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Documents (pdf, doc/docx, xls/xlsx)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`video/mp4`,`video/mpeg`,`video/x-msvideo`,`video/quicktime`,`video/3gpp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Videos (mp4, mpeg, avi, mov, 3gp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/zip`,`application/x-7z-compressed`,`application/x-rar-compressed`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Archives (zip, 7zip, rar)`})))),t.div({className:()=>e.field.maxSelect>1?`col-sm-6`:`col-sm-9`},t.div({className:`field`},t.label({htmlFor:n+`.thumbs`},t.span({className:`txt`},`Thumb sizes`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.`)})),t.input({type:`text`,id:n+`.thumbs`,placeholder:`e.g. 50x50, 480x720`,name:()=>`fields.${e.fieldIndex}.thumbs`,value:()=>app.utils.joinNonEmpty(e.field.thumbs),onchange:n=>e.field.thumbs=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},t.span({className:`txt m-r-5`},`Use comma as separator.`),t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`thumbFormatsDropdown`},t.span({className:`txt`},`Supported formats`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`thumbFormatsDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.ul({className:`m-0 p-l-sm`},t.li(null,t.strong(null,`WxH`),t.span(null,` (e.g. 100x50) - crop to WxH viewbox (from center)`)),t.li(null,t.strong(null,`WxHt`),t.span(null,` (e.g. 100x50t) - crop to WxH viewbox (from top)`)),t.li(null,t.strong(null,`WxHb`),t.span(null,` (e.g. 100x50b) - crop to WxH viewbox (from bottom)`)),t.li(null,t.strong(null,`WxHf`),t.span(null,` (e.g. 100x50f) - fit inside a WxH viewbox (without cropping)`)),t.li(null,t.strong(null,`0xH`),t.span(null,` (e.g. 0x50) - resize to H height preserving the aspect ratio`)),t.li(null,t.strong(null,`Wx0`),t.span(null,` (e.g. 100x0) - resize to W width preserving the aspect ratio`)))))),t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},`Max size`),t.input({type:`number`,id:n+`.maxSize`,step:1,min:0,max:2**53-1,placeholder:`~5MB default`,name:()=>`fields.${e.fieldIndex}.maxSize`,value:()=>e.field.maxSize||``,oninput:n=>e.field.maxSize=parseInt(n.target.value,10)})),t.div({className:`field-help`},`In bytes.`)),()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,required:!0,max:2**53-1,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:e=>{let n=parseInt(e.target.value,10);n>1?props.field.maxSelect=n:props.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field m-t-5 m-b-5`},t.input({className:`switch`,type:`checkbox`,id:n+`.protected`,name:()=>`fields.${e.fieldIndex}.protected`,checked:()=>!!e.field.protected,onchange:n=>e.field.protected=n.target.checked}),t.label({htmlFor:n+`.protected`},t.span({className:`txt`},`Protected`),t.small({className:`txt-hint`},`Files will require View API rule permissions and file token (`,t.a({href:`https://pocketbase.io/docs/files-handling#protected-files`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Learn more`}),`).`)))),t.div({className:`col-sm-12`},t.di Line truncated
`),oninput:n=>{let r=n.target.value.trimStart().replaceAll(`
`,`
`).split(`
`);e.field.values=r,app.utils.deleteByPath(app.store.errors,`fields.${e.fieldIndex}.values`)},onchange:n=>{let r=new Set,i=n.target.value.split(`
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function On(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:Dn,input:En,view:On,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function kn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>An(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!An(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function An(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function jn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function Mn(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1 Line truncated
To target the newly submitted ones you can use @request.body.*.`)})],name:`updateRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.updateRule,oninput:n=>e.collection.updateRule=n})),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Delete rule`,name:`deleteRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.deleteRule,oninput:n=>e.collection.deleteRule=n}))]}),()=>{if(e.collection.type==`auth`)return[t.hr({className:`m-t-base m-b-base`}),t.button({type:`button`,onmount:()=>{n.showAuthRules=e.collection.manageRule!==null||e.collection.authRule!==``},className:()=>`btn secondary sm ${n.showAuthRules?``:`transparent`}`,onclick:()=>{n.showAuthRules=!n.showAuthRules}},t.span({className:`txt`},`Additional auth collection rules`),t.i({ariaHidden:!0,className:()=>n.showAuthRules?`ri-arrow-drop-up-line`:`ri-arrow-drop-down-line`})),app.components.slide(()=>n.showAuthRules,t.div({className:`grid sm m-t-sm`},t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Authentication rule`,name:`authRule`,placeholder:``,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.authRule,oninput:n=>e.collection.authRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed every time `,t.strong(null,`before authentication`),` allowing you to restrict who can authenticate.`),t.p(null,`For example, to allow only verified users you can set it to `,t.code(null,`verified = true`),`.`),t.p(null,`Leave it empty to allow anyone with an account to authenticate.`),t.p(null,`To disable authentication entirely you can change it to "Set superusers only".`))),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Manage rule`,name:`manageRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.manageRule,oninput:n=>e.collection.manageRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed in addition to the `,t.strong(null,`create`),` and `,t.strong(null,`update`),` API rules.`),t.p(null,`It enables superuser-like permissions to allow fully managing the auth record(s), eg. changing the password without requiring to enter the old one, directly updating the verified state or email, etc.`)))))]})}var Jn=`test_view_query`;function Yn(e){let n=`query_`+app.utils.randomString(),r=[`SELECT`,`FROM`,`WHERE`,`LEFT JOIN`,`INNER JOIN`,`ON`,`GROUP BY`,`HAVING`,`ORDER BY`,`LIMIT`,`OFFSET`,`AS`,`WITH`,`NOT`,`IN`,`EXISTS`,`LIKE`,`CAST`],i=store({testRecords:[],testError:``,isTesting:!1});async function a(n){if(i.isTesting=!0,i.testRecords=[],(app.store.errors?.viewQuery||app.store.errors?.fields)&&(delete app.store.errors.viewQuery,delete app.store.errors.fields),!n){i.testError=``,i.isTesting=!1;return}try{let r=await app.pb.send(`/api/collections/meta/dry-run-view`,{method:`POST`,body:{query:n},requestKey:Jn});e.collection?.id?i.testRecords=r.sample.map(n=>(n.collectionId=e.collection?.id,n.collectionName=e.collection?.name,n)):i.testRecords=r.sample,i.testError=``,i.isTesting=!1}catch(e){e.isAbort||(i.testError=e.message||`Invalid query.`,i.isTesting=!1)}}let o,s=[watch(()=>e.collection?.viewQuery,e=>{clearTimeout(o),o=setTimeout(()=>a(e),200)})];return t.div({pbEvent:`collectionViewQueryTabContent`,className:`collection-tab-content collection-view-query-tab-content`,onunmount:()=>{clearTimeout(o),app.pb.cancelRequest(Jn),s.forEach(e=>e?.unwatch())}},t.div({className:`grid`},t.div({className:`col-12`},t.div({className:`txt-right txt-sm m-b-10`},t.button({type:`button`,className:`txt-bold link-hint`,"html-popovertarget":n+`caveats_dropdown`},()=>`Query caveats`)),t.div({id:n+`caveats_dropdown`,className:`dropdown sm query-caveats-dropdown`,popover:`auto`},t.ul(null,t.li(null,`Wildcard columns (*) are not supported.`),t.li(null,`The query must have a unique `,t.code(null,`id`),` column.`,t.br(),`If your query doesn't have a suitable one, you can use the universal `,t.code(null,`(ROW_NUMBER() OVER()) as id`),`.`),t.li(null,`Expressions must be aliased with a valid formatted field name, e.g. `,t.code(null,`MAX(balance) as maxBalance`),`.`),t.li(null,`Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. `,t.code(null,`(MAX(balance) + 1) as maxBalance`),`.`),t.li(null,`UNION expressions are supported but the entire query must be wrapped in parenthesis.`))),t.div({className:`field`},t.label({htmlFor:n+`.viewQuery`},t.span({className:`txt`},`Select query`),t.span({hidden:()=>!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Invalid query`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Valid query`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({id:n+`.viewQuery`,name:`viewQuery`,language:`sql`,required:!0,autocomplete: Line truncated
`+e)})},t.div({className:`btns`},t.button({type:`button`,className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.canSave,onclick:()=>s(!0)},t.span({className:`txt`},()=>a.isNew?`Create`:`Save changes`)),t.button({type:`button`,title:`Save options`,className:()=>`btn p-5`,disabled:()=>!a.canSave,"html-popovertarget":i+`save_options`},t.i({className:`ri-arrow-up-s-line`,ariaHidden:!0})),t.div({id:i+`save_options`,className:`dropdown nowrap`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),s(!1)}},t.span({className:`txt`},`Save and continue`),t.small({className:`txt-hint`},`(Ctrl+S)`)),t.hr(),t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),u()}},t.span({className:`txt`},`Reset form`)))))),r}function Zn(e){let n=JSON.parse(JSON.stringify(app.store.collectionScaffolds[e.type])),r=JSON.parse(JSON.stringify(e.fields))||[],i=r.filter(e=>!e.system);e.fields=n.fields||[];for(let n of r){if(!n.system)continue;let r=e.fields.find(e=>e.name==n.name);r&&Object.assign(r,n)}for(let n of i)e.fields.push(n);if(e.indexes=e.indexes||[],e.indexes.length){let r=n?.indexes||[];indexesLoop:for(let n=e.indexes.length-1;n>=0;n--){let i=app.utils.parseIndex(e.indexes[n]),a=i.indexName.toLowerCase();for(let i of r)if(a==app.utils.parseIndex(i).indexName.toLowerCase()){e.indexes.splice(n,1);continue indexesLoop}for(let r of i.columns)if(!e.fields.find(e=>e.name.toLowerCase()==r.name.toLowerCase())){e.indexes.splice(n,1);continue indexesLoop}}}app.utils.mergeUnique(e.indexes,n.indexes)}function Qn(e,n){let r=`truncate_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.truncate(e.originalCollection.name),n.ontruncate?.(JSON.parse(JSON.stringify(e.originalCollection))),app.toasts.success(`Successfully truncated collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover(),app.modals.confirm(t.div(null,t.h6({className:`block txt-center`},`Do you really want to delete all records of the collection?`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(n.target.closest(`.modal`))},()=>{i.nameConfirm=``})}},t.i({className:`ri-eraser-line`,ariaHidden:!0}),t.span({className:`txt`},`Truncate`))}function $n(e,n){let r=`delete_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.delete(e.originalCollection.name),n.ondelete?.(JSON.parse(JSON.stringify(e.originalCollection))),app.utils.removeByKey(app.store.collections,`id`,e.originalCollection.id),app.toasts.success(`Successfully deleted collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover();let o=n.target.closest(`.modal`);app.modals.confirm(t.div({className:`block`},t.h6({className:`block txt-center`},()=>e.originalCollection.type==`view`?`Do you really want to delete the selected collection?`:`Do you really want to delete the selected collection and all its records`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(o)},()=>{i.nameConfirm=``})}},t.i({className:`ri-delete-bin-7-line`,ariaHidden:!0}),t.span({className:`txt`},`Delete`))}window.app=window.app||{},window.app.components=window.a Line truncated
...
-----END PRIVATE KEY-----`,value:()=>a.privateKey||``,oninput:e=>a.privateKey=e.target.value})),t.div({className:`field-help`},`The key is not stored on the server and it is used only for generating the signed JWT.`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Close`)),t.button({"html-form":r+`_form`,type:`submit`,className:`btn expanded`},t.i({className:`ri-key-line`,ariaHidden:!0}),t.span({className:`txt`},`Generate secret`)))),n}function dr(){return t.div({className:`inline-flex gap-5`},t.span(null,`Default log levels:`),()=>{let e=[];for(let n in app.utils.logLevels)e.push(t.code(null,`${n}:${app.utils.logLevels[n].label}`));return e})}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogsSettings=function(e={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null}){let n=fr(e);n&&(document.body.appendChild(n),app.modals.open(n))};function fr(e){let n,r=store({isLoading:!1,isSaving:!1,formSettings:{},initFormSettingsHash:``,get hasChanges(){return r.initFormSettingsHash!=JSON.stringify(r.formSettings)}});function i(e={}){r.formSettings={logs:e?.logs||{}},r.initFormSettingsHash=JSON.stringify(r.formSettings)}async function a(){r.isLoading=!0;try{i(await app.pb.settings.getAll({requestKey:`logsSettings`})),r.isLoading=!1}catch(e){e.isAbort||(r.isLoading=!1,app.checkApiError(e))}}async function o(){if(r.hasChanges){r.isSaving=!0;try{let a=await app.pb.settings.update(app.utils.filterRedactedProps(r.formSettings));e.onsave?.(a),i(a),app.toasts.success(`Successfully saved logs settings.`),r.isSaving=!1,app.modals.close(n)}catch(e){e.isAbort||(r.isSaving=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`logsSettingsModal`,className:`modal popup sm logs-settings-modal`,onbeforeopen:n=>(a(),e.onbeforeopen?.(n)),onafteropen:n=>{e.onafteropen?.(n)},onbeforeclose:n=>e.onbeforeclose?.(n),onafterclose:n=>{e.onafterclose?.(n),n?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto`},`Logs settings`)),()=>r.isLoading?t.div({className:`modal-content flex`,style:`min-height: 200px`},t.span({className:`loader m-auto`})):[t.form({pbEvent:`logsSettingsForm`,id:`logsSettingsForm`,className:`modal-content logs-settings-form`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.maxDays`},`Max days retention`),t.input({type:`number`,id:`logs.maxDays`,name:`logs.maxDays`,min:0,required:!0,value:()=>r.formSettings.logs.maxDays,oninput:e=>r.formSettings.logs.maxDays=e.target.value<<0})),t.div({className:`field-help`},`Set to `,t.code(null,0),` to disable logs persistence.`)),t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.minLevel`},`Min log level`),t.input({type:`number`,id:`logs.minLevel`,name:`logs.minLevel`,min:-100,max:100,required:!0,value:()=>r.formSettings.logs.minLevel,oninput:e=>r.formSettings.logs.minLevel=e.target.value<<0})),t.div({className:`field-help`},t.div(null,`Logs with level below the minimum will be ignored.`),dr())),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logIP`,name:`logs.logIP`,className:`switch`,checked:()=>r.formSettings.logs.logIP,onchange:e=>r.formSettings.logs.logIP=e.target.checked}),t.label({htmlFor:`logs.logIP`},`Enable IP logging`))),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logAuthId`,name:`logs.logAuthId`,className:`switch`,checked:()=>r.formSettings.logs.logAuthId,onchange:e=>r.formSettings.logs.logAuthId=e.target.checked}),t.label({htmlFor:`logs.logAuthId`},`Enable Auth Id logging`))))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n),disabled:()=>r.isSaving},t.span({className:`txt`},`Close`)),t.button({type:`submit`,"html-form":`logsSettingsForm`,className:()=>`btn ${r.isSaving?`loading`:``}`,disabled:()=>!r.hasChanges||r.isSaving},t.span({className:`txt`},`Save changes`)))]),n}function pr(e){return t.div({className:()=>`label log-level-label level-${e.level}`},t.span({className:`txt`},()=>`${app.utils.logLevels[e.level]?.label||`UNKN`} (${e.level})`))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=_r(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};var mr=[`execTime`,`type`,`auth`,`authId`,`status`,`method`,`url`,`referer`,`remoteIP`,`userIP`,`userAgent`,`error`,`details`];function hr(e){app.utils.downloadJSON(e,`log_`+e.created.replaceAll(/[-:\. ]/gi,``)+`.json`)}function gr(e){app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Log copied to clipboard!`)}function _r(e,n){let r,i=store({isLoading:!1,log:null,get isRequ Line truncated
{MMM} {DD}`,null,`
{MMM} {DD}`,null,null,null,1],[60,`{h}:{mm}{aa}`,`
{MMM} {DD}`,null,`
{MMM} {DD}`,null,null,null,1]],grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}},{show:!0,stroke:a,grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}}],plugins:[Vr(),zr(r),Br(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function zr(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Lr(r):r.setScale(`x`,{min:e.zoom.min,max:e.zoom.max})})},destroy:e=>{n?.unwatch()},setSelect:n=>{n.select.width>0&&(e.zoom={min:n.posToVal(n.select.left,`x`),max:n.posToVal(n.select.left+n.select.width,`x`)})}}}}function Br(e){let n;return{hooks:{init:r=>{let i=r.root.querySelectorAll(`.u-axis`);if(!i.length){console.warn(`xPanPlugin requires x axis to be defined`);return}i[0].addEventListener(`mousedown`,i=>{if(!e.zoom?.min)return;let a=i.clientX,{min:o,max:s}=r.scales.x,c=r.bbox.width,l=(s-o)/(c/uPlot.pxRatio),u=i=>{let c=(a-i.clientX)*l;r.setScale(`x`,{min:o+c,max:s+c}),clearTimeout(n),n=setTimeout(()=>{r?.scales?.x&&(e.zoom={min:r.scales.x.min,max:r.scales.x.max})},100)},d=e=>{document.removeEventListener(`mousemove`,u),document.removeEventListener(`mouseup`,d)};document.addEventListener(`mousemove`,u),document.addEventListener(`mouseup`,d)})},destroy:e=>{n&&clearTimeout(n)}}}}function Vr(e){let n;return{hooks:{init:e=>{let r=e.over;n=store({date:``,total:0,left:0,top:0,show:!1});let i=t.div({className:()=>`chart-tooltip ${n.show?``:`hidden`}`,onmount(e){e._positionWatcher?.unwatch(),e._positionWatcher=watch(()=>[n.left,n.top],()=>{if(!e)return;let i=e.getBoundingClientRect(),a=n.left;a<0?a=0:a+i.width>r.clientWidth&&(a=r.clientWidth-i.width),e.style.left=a+`px`;let o=n.top-i.height-5;o<0&&(o=n.top+5,o+i.high>r.clientHeight&&(o=r.clientHeight-i.height)),e.style.top=o+`px`})},onunmount(e){e._positionWatcher?.unwatch()}},t.div({className:`content-primary`},()=>`${n.total} ${n.total==1?`request`:`requests`}`),t.div({className:`content-secondary`},()=>n.date));r.appendChild(i),r.addEventListener(`mouseleave`,()=>{n&&(n.show=!1)})},destroy:()=>{n.show=!1},setCursor:e=>{if(!n)return;let r=e.data[0][e.cursor.idx]||0,i=e.data[1][e.cursor.idx]||0;if(r==0||i==0){n.show=!1;return}n.show=!0,n.total=i;let a=new Date(r*1e3),o=new Date(r*1e3+36e5),s=a.toLocaleString(`default`,{month:`short`}),c=a.getDate().toString().padStart(2,`0`);n.date=`${s} ${c} ${Hr(a)}-${Hr(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function Hr(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Ur=50;function Wr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Ur},get totalSelected(){return Object.keys(n.bulkSelected).length},get areAllSelected(){return n.logs.length&&n.logs.length==n.totalSelected}});async function r(r=!1){e.isListLoading=!0;try{let i=r?1:n.lastPage+1,a=(e.presets||[]).concat(app.utils.normalizeSearchFilter(e.filter,[`level`,`message`,`data`]));if(e.zoom?.min&&e.zoom?.max){let n=new Date(e.zoom.min*1e3);n.setSeconds(0),n.setMilliseconds(0);let r=app.utils.toRFC3339Datetime(n),i=new Date(e.zoom.max*1e3);i.setSeconds(59),i.setMilliseconds(999);let o=app.utils.toRFC3339Datetime(i);a.push(`created >= "${r}" && created <= "${o}"`)}else i>1&&a.push(`created <= "${n.logs[n.logs.length-1].created}"`);let o=await app.pb.logs.getList(i,Ur,{skipTotal:1,sort:`-@rowid`,requestKey:`logs_list`,filter:a.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)});o.page==1&&(n.logs=[],n.bulkSelected={}),n.lastPage=o.page,n.lastLoadCount=o.items.length;for(let e=0;e<o.items.length;e++)app.utils.pushOrReplaceObject(n.logs,o.items[e]),e>1&&e%20==0&&await new Promise(e=>setTimeout(e,20));e.isListLoading=!1,e.isFirstLoadReady||=!0}catch(n){n.isAbort||(e.isListLoading=!1,app.checkApiError(n))}}function i(e=!0){let r={};if(e)for(let e of n.logs)r[e.id]=e;n.bulkSelected=r}function a(e){let n=[];if(!e.data)return n;if(e.data.type==`request`){for(let r of[`status`,`execTime`,`auth`,`authId`,`userIP`])e.data[r]!==void 0&&n.push({key:r});e.data.referer&&!e.data.referer.includes(window.location.host)&&n.push({key:`referer`})}else{let r=Object.keys(e.data);for(let e of r)e!=`error`&&e!=`details`&&n.length<6&&n.push({key:e})}return e.data.error&&n.push({key:`error`,label:`danger`}),e.data.details&&n.push({key:`details`,label:`warning`}),n}let o=/[-:\. ]/gi;function s(){let e=Object.values(n.bulkSelected).sort((e,n)=>e.created<n.created?1:e.created>n.created?-1:0);if(!e.length)return;if(e.length==1)return app.utils.downloadJSON(e[0],`log_`+e[0].created.replaceAll(o,``)+`.json`);let r=e[0].created.replaceAll(o,``),i=e[e.length-1].created.replaceAll(o,``);return app.utils.downloadJSON(e,`${e.length}_logs_${i}_to_${r}.json`)}let c=[];return t.div({pbEvent:`logsList`,className:`page-table-wrapper`,onmount(n){c.push(watch(()=>[e.filter,e.presets?.length],()=>{e.zoom={}}),watch(()=>[e.reset,e.filter,e.presets?.length,e.zoom?.min,e.zoom?.max],()=>{r(!0),n&&(n.scrollTop=0)}))},onunmount() Line truncated
It is recommend to list it as trusted.`,`right`)});if(i.isEnabled&&i.possibleProxyHeader&&!e.formSettings.trustedProxy.headers.includes(i.possibleProxyHeader))return t.i({className:`ri-alert-line txt-hint`,ariaDescription:app.attrs.tooltip(`The configured proxy header doesn't match with the detected one.`,`right`)})},t.div({className:`flex-fill`}),()=>i.isEnabled?t.span({className:`label success`},`Enabled`):t.span({className:`label`},`Disabled`),()=>{if(!app.utils.isEmpty(app.store.errors?.trustedProxy))return t.i({className:`ri-error-warning-fill txt-danger`,ariaDescription:app.attrs.tooltip(`Has errors`,`left`)})}),t.p({className:`m-t-0`},`Below you should see your real IP. If not - configure the correct proxy header for your environment.`),t.div({hidden:()=>i.isLoading,className:`alert info m-b-sm`},t.div({className:`flex gap-5`},t.span(null,`Resolved user IP:`),t.strong(null,()=>i.realIP||`N/A`)),t.div({className:`flex gap-5`},t.span(null,`Detected proxy header:`),t.strong(null,()=>i.possibleProxyHeader||`N/A`))),t.div({className:`content m-b-sm`},t.p(null,`
When PocketBase is deployed on platforms like Fly or it is accessible through proxies such as
NGINX, requests from different users will originate from the same IP address (the IP of the proxy
connecting to your PocketBase app).
`),t.p(null,`
In this case to retrieve the actual user IP (used for rate limiting, logging, etc.) you need to
properly configure your proxy and list below the trusted headers that PocketBase could use to
extract the user IP.
`),t.p({className:`txt-bold`},`When using such proxy, to avoid spoofing it is recommended to:`),t.ul({className:`txt-bold`},t.li(null,`use headers that are controlled only by the proxy and cannot be manually set by the users`),t.li(null,`make sure that the PocketBase server can be accessed ONLY through the proxy`)),t.p(null,`You can clear the headers field if PocketBase is not deployed behind a proxy.`)),t.div({className:`grid sm`},t.div({className:`col-lg-9`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.headers`},`Trusted IP proxy headers`),t.input({type:`text`,id:`trustedProxy.headers`,name:`trustedProxy.headers`,placeholder:`Leave empty to disable`,value:()=>app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers),oninput:n=>{let r=app.utils.splitNonEmpty(n.target.value,`,`),i=app.utils.joinNonEmpty(r);app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers)!=i&&(e.formSettings.trustedProxy.headers=r)}})),t.div({className:`field addon`},t.button({type:`button`,className:()=>`btn sm secondary transparent ${app.utils.isEmpty(e.formSettings.trustedProxy.headers)?`hidden`:``}`,onclick:()=>{e.formSettings.trustedProxy.headers=[]}},t.span({className:`txt`},`Clear`)))),t.div({className:`field-help`},`Comma separated list of headers such as: `,t.div({className:`inline-flex gap-5`},()=>i.suggestedProxyHeaders.map(n=>t.div({type:`button`,className:`label sm link-hint`,onclick:()=>{e.formSettings.trustedProxy.headers=[n]},textContent:n}))))),t.div({className:`col-lg-3`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.useLeftmostIP`},t.span({className:`txt`},`IP priority`),t.i({className:`ri-information-line tooltip-right`,ariaDescription:app.attrs.tooltip(`This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.`)})),app.components.select({id:`trustedProxy.useLeftmostIP`,name:`trustedProxy.useLeftmostIP`,options:r,required:!0,value:()=>e.formSettings.trustedProxy.useLeftmostIP||!1,onchange:n=>{e.formSettings.trustedProxy.useLeftmostIP=n?.[0]?.value}})))))}function ei(){app.store.title=`Application settings`;let e=store({isLoading:!1,isSaving:!1,formSettings:null,originalFormSettings:null,get originalFormSettingsHash(){return JSON.stringify(e.originalFormSettings)},get hasChanges(){return e.originalFormSettingsHash!=JSON.stringify(e.formSettings)}});n();async function n(){e.isLoading=!0;try{i(await app.pb.settings.getAll()),e.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function r(){if(!(e.isSaving||!e.hasChanges)){e.isSaving=!0,e.formSettings.rateLimits.rules=Zr(e.formSettings.rateLimits.rules);try{let n=app.utils.filterRedactedProps(e.formSettings);i(await app.pb.settings.update(n)),app.toasts.success(`Successfully saved application settings.`)}catch(e){app.checkApiError(e)}e.isSaving=!1}}function i(n={}){if(app.store.settings=JSON.parse(JSON.stringify(n)),!n.meta?.accentColor){let e=window.getComputedStyle(document.documentElement)?.getPropertyValue(`--accentColor`);e?.startsWith(`#`)&&(n.meta=n.meta||{},n.meta.accentColor=e.toLowerCase()||``)}e.originalFormSettings={meta:n.meta||{},batch:n.batch||{},trustedProxy:n.trustedProxy||{headers:[]},rateLimits:n.rateLimits||{rules:[]}},Zr(e.originalFormSettings.rateLimits.rules),e.formSettings=JSON.parse(JSON.stringify(e.originalFormSettings))}function a(){e.formSettings=JSON.parse(e.originalFormSettingsHash)}return t.div({pbEvent:`pageApplicationSettings`,className:`page page-application-settings`},Kr(),t.div({className:`page-content full-height`},t.header({className:`page-header`},t.nav({className:`breadcrumbs`},t.div({className:`breadcrumb-item`},`Settings`),t.div({className:`breadcrumb-item`},`Application`))),t.div({className:`wrapper m-b-base`},()=>e.isLoading?t.div({className:`block txt-center`},t.span({className:`loader lg`})):t.form({pbEvent:`applicationSettingsForm`,className:`grid application-settings-form`,inert:()=>e.isSaving,onsubmit:e=>{e.preventDefault(),r()}},t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appName`},`Application name`),t.input({id:`meta.appName`,name:`meta.appName`,type:`text`,required:!0,value:()=>e.formSettings.meta.appName||``,oninput:n=>e.formSettings.meta.appName=n.target.value}))),t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appURL`},`Application URL`),t.input({id:`meta.appURL`,name:`meta.appURL`,type:`url`,required:!0,value:()=>e.formSettings.meta.appURL||``,oninput:n=>e.formSettings.meta.appURL=n.target.value}))),t.div({className:`col-md-2`},()=>ti(e,e.isSaving)),t.div({className:`col-lg-12`},()=>$r(e),()=>Qr(e),()=>qr(e)),t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`meta.hideControls`,name:`meta.hideControls`,type:`checkbox`,className:`switch`,checked:()=>e.formSettings.meta.hideControls,onchange:n=>e.formSettings.meta.hideControls=n.target.check Line truncated
Records update will also require an extra unlock step before save.`)})))),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({type:`button`,className:`btn transparent secondary`,disabled:()=>e.isSaving,hidden:()=>!e.hasChanges,onclick:a},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${e.isSaving?`loading`:``}`,disabled:()=>!e.hasChanges||e.isSaving},t.span({className:`txt`},`Save changes`)))))),t.footer({className:`page-footer`},app.components.credits())))}function ti(e){let n=`accent_`+app.utils.randomString(),r=store({isTooLight:!1}),i,a;function o(e){clearTimeout(a),document.documentElement.style.setProperty(`--animationSpeed`,`0`),e?document.documentElement.style.setProperty(`--accentColor`,e.toLowerCase()):document.documentElement.style.removeProperty(`--accentColor`),a=setTimeout(()=>{document.documentElement.style.removeProperty(`--animationSpeed`)},100)}let s=[watch(()=>e.formSettings?.meta?.accentColor,e=>{clearTimeout(i),i=setTimeout(()=>{o(e)},100)})];return t.div({className:`field`,ariaDescription:app.attrs.tooltip(()=>r.isTooLight?`Invalid - color is too light`:``),onunmount:()=>{clearTimeout(i),o(e.formSettings.meta.accentColor),s.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.span({className:`txt`},`Accent`),t.i({hidden:()=>!r.isTooLight,className:`txt-warning ri-alert-line`})),app.components.colorPicker({id:n,name:`meta.accentColor`,predefinedColors:()=>app.store.predefinedAccentColors,value:()=>e.formSettings.meta.accentColor,onchange:n=>{if(r.isTooLight=!1,!app.utils.isDarkEnoughForWhiteText(n)){r.isTooLight=!0;return}e.formSettings.meta.accentColor=n}}))}function ni(e={}){let n=store({onsave:null}),r=app.utils.extendStore(n,e),i=[{cron:`0 0 * * *`,label:`Every day at 00:00h`},{cron:`0 0 * * 0`,label:`Every sunday at 00:00h`},{cron:`0 0 * * 1,3`,label:`Every Mon and Wed at 00:00h`},{cron:`0 0 1 * *`,label:`Every first day of the month at 00:00h`}],a=store({showForm:!1,isLoading:!1,isSaving:!1,formSettings:null,initSerialized:`null`,enableAutoBackups:!1,get hasChanges(){return a.initSerialized!=JSON.stringify(a.formSettings)}});async function o(){a.isLoading=!0;try{c(await app.pb.settings.getAll()),a.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function s(){if(!(a.isSaving||!a.hasChanges)){a.isSaving=!0;try{let e=app.utils.filterRedactedProps(a.formSettings),r=await app.pb.settings.update(e);n.onsave?.(r),c(r),app.toasts.success(`Successfully saved backups settings.`)}catch(e){app.checkApiError(e)}a.isSaving=!1}}function c(e={}){app.store.settings=JSON.parse(JSON.stringify(e)),a.formSettings={backups:e?.backups||{}},a.enableAutoBackups=!!a.formSettings.backups.cron,a.initSerialized=JSON.stringify(a.formSettings)}function l(){a.formSettings=JSON.parse(a.initSerialized),a.enableAutoBackups=!!a.formSettings.backups.cron}return r.push(watch(()=>{!a.enableAutoBackups&&a.formSettings?.backups?.cron&&(a.formSettings.backups.cron=``)})),t.div({className:`block backups-settings-form-wrapper`,onmount:()=>{o()},onunmount:()=>{r.forEach(e=>e?.unwatch())}},t.button({type:`button`,className:()=>`btn secondary ${a.isLoading?`loading`:``}`,disabled:()=>a.isLoading||a.hasChanges,onclick:()=>a.showForm=!a.showForm},t.span({className:`txt`},`Backup options`),t.i({className:()=>a.showForm?`ri-arrow-up-s-line`:`ri-arrow-down-s-line`,ariaHidden:!0})),app.components.slide(()=>a.showForm,t.form({pbEvent:`backupsSettingsForm`,className:`grid backups-settings-form m-t-base`,inert:()=>a.isSaving,onsubmit:e=>{e.preventDefault(),s()}},()=>a.isLoading?t.div({className:`col-lg-12 txt-center`},t.span({className:`loader lg`})):[t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`enableAutoBackupsToggle`,type:`checkbox`,className:`switch`,checked:()=>a.enableAutoBackups,onchange:e=>{a.enableAutoBackups=e.target.checked,a.formSettings.backups.cron||(a.formSettings.backups.cron=i[0].cron)}}),t.label({htmlFor:`enableAutoBackupsToggle`},`Enable auto backups`)),app.components.slide(()=>a.enableAutoBackups,t.div({className:`grid m-t-base m-b-base`},t.div({className:`col-lg-6`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`backups.cron`},`Cron expression`),t.input({id:`backups.cron`,name:`backups.cron`,className:`txt-code`,type:`text`,placeholder:`e.g. 0 0 * * *`,required:()=>a.enableAutoBackups,value:()=>a.formSettings.backups.cron,oninput:e=>a.formSettings.backups.cron=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,className:`btn outline sm`,"html-popovertarget":`cron-presets-dropdown`},t.span({className:`txt`},`Presets`),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0})),t.div({id:`cron-presets-dropdown`,className:`dropdown sm txt-nowrap`,popover:`auto`},()=>i.map(e=>t.button({type:`button`,className:()=>`dropdown-item ${a.formSettings.backups.cron==e.cron?`active`:``}`,textContent:e.label,onclick:n=>{a.formSettings.backups.cro Line truncated
@annually
@monthly
@weekly
@daily
@midnight
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function ri(e={oncreated:null}){let n=ii(e);n&&(document.body.appendChild(n),app.modals.open(n))}function ii(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup other concurrent write requests may fail since the database will be temporary "locked" (this usually happens only during the ZIP generation).`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function ai(e){let n=oi(e);document.body.appendChild(n),app.modals.open(n)}function oi(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an overLine truncated
@@ -1 +1 @@
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
+2 -2
View File
@@ -13,9 +13,9 @@
<!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-OxsdchXY.js"></script>
<script type="module" crossorigin src="./assets/index-BB3JTWj3.js"></script>
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
<link rel="stylesheet" crossorigin href="./assets/index-BLIFQr7L.css">
<link rel="stylesheet" crossorigin href="./assets/index-ouas71Vg.css">
</head>
<body>
</body>
+1 -1
View File
@@ -1 +1 @@
.tox,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-size:var(--fontSize);font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-control-wrap__status-icon-invalid svg,.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox Line truncated
.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-button-link,.tox .tox-button-link--sm{font-size:var(--fontSize)}.tox,.tox .tox-button,.tox .tox-button-link,.tox .tox-dialog__title,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-fLine truncated
+74 -24
View File
@@ -50,6 +50,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -64,6 +67,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -78,6 +84,9 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -92,6 +101,9 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -106,6 +118,9 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -120,6 +135,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -134,6 +152,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -168,6 +189,29 @@
"win32"
]
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -301,6 +345,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -318,6 +365,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -335,6 +385,9 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -352,6 +405,9 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -369,6 +425,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -386,6 +445,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -431,29 +493,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
@@ -722,6 +761,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -743,6 +785,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -764,6 +809,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -785,6 +833,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -872,7 +923,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+1 -1
View File
@@ -1 +1 @@
.tox,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-size:var(--fontSize);font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-control-wrap__status-icon-invalid svg,.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox Line truncated
.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-button-link,.tox .tox-button-link--sm{font-size:var(--fontSize)}.tox,.tox .tox-button,.tox .tox-button-link,.tox .tox-dialog__title,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-fLine truncated
+2
View File
@@ -59,6 +59,7 @@ export function appHeader() {
colorSchemeButton(),
t.button(
{
type: "button",
className: "header-link logged-user txt-normal",
"html-popovertarget": "logged-user-dropdown",
},
@@ -108,6 +109,7 @@ function colorSchemeButton() {
return [
t.button(
{
type: "button",
className: "header-link color-scheme-picker",
"html-popovertarget": "color-scheme-dropdown",
title: "Color scheme",
+18 -15
View File
@@ -41,9 +41,9 @@ window.app.components.codeEditor = function(propsArg = {}) {
// ]
autocomplete: undefined, // Array<string|Object> | function(word): Array<string|Object>,
// ---
oninput: function(val, e) {},
onfocus: function(val, e) {},
onblur: function(val, e) {},
oninput: function(val) {},
onfocus: function(val) {},
onblur: function(val) {},
});
const extendWatchers = app.utils.extendStore(props, propsArg, "autocomplete");
@@ -121,6 +121,12 @@ window.app.components.codeEditor = function(propsArg = {}) {
isFieldVisible = true;
}
function updateValue(newVal) {
props.value = newVal;
props.oninput?.(newVal);
editorContent.dispatchEvent(new CustomEvent("change", { detail: newVal }));
}
let isCtrlOrCmdKey = false;
let valueWatcher;
@@ -155,8 +161,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
valueWatcher?.unwatch();
closeAutocompleteDropdown();
},
onfocus: (e) => {
props.onfocus?.(props.value, e);
onfocus: () => {
props.onfocus?.(props.value);
},
onblur: (e) => {
// not blurred because of dropdown click
@@ -164,16 +170,12 @@ window.app.components.codeEditor = function(propsArg = {}) {
closeAutocompleteDropdown();
}
props.onblur?.(props.value, e);
props.onblur?.(props.value);
},
oninput: (e) => {
closeAutocompleteDropdown();
props.value = editorContent.textContent;
props.oninput?.(props.value, e);
editorContent.dispatchEvent(new CustomEvent("change", { detail: props.value }));
updateValue(editorContent.textContent);
if (!props.value?.length) {
editorContent.textContent = ""; // ensure that no comments, br, etc. tags are left
@@ -235,7 +237,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
editorContent.textContent = editorContent.textContent.substring(0, match.start)
+ word
+ editorContent.textContent.substring(match.end + 1);
props.value = editorContent.textContent;
updateValue(editorContent.textContent);
try {
window
@@ -316,13 +319,13 @@ window.app.components.codeEditor = function(propsArg = {}) {
selection.modify("extend", "backward", "character");
if (selection.toString()[0] == "\t") {
selection.deleteFromDocument();
props.value = editorContent.textContent;
updateValue(editorContent.textContent);
} else {
// check ahead and restore
selection.modify("extend", "forward", "character");
if (selection.toString()[0] == "\t") {
selection.deleteFromDocument();
props.value = editorContent.textContent;
updateValue(editorContent.textContent);
}
}
@@ -335,7 +338,7 @@ window.app.components.codeEditor = function(propsArg = {}) {
range.deleteContents();
range.insertNode(document.createTextNode("\t"));
range.collapse();
props.value = editorContent.textContent;
updateValue(editorContent.textContent);
}
return;
+1 -1
View File
@@ -3,7 +3,7 @@ const optionClass = "dropdown-item";
document.addEventListener(
"toggle",
(e) => {
if (e.newState != "open" || !e.target?.matches(".dropdown") || e.target.__keyboardNavRegistered) {
if (e.newState != "open" || !e.target || e.target.__keyboardNavRegistered || !e.target.matches(".dropdown")) {
return;
}
+1
View File
@@ -247,6 +247,7 @@ function initSearch(selectFunc = null) {
if (data.searchTerm.length) {
return t.button(
{
type: "button",
className: "link-hint",
title: "Clear search",
onclick: () => reset(),
+1 -1
View File
@@ -249,7 +249,7 @@ window.app.components.select = function(propsArg = {}) {
tabIndex: -1,
role: "button",
className: "ri-close-line link-hint btn-option-unset",
ariaLabel: app.attrs.tooltip("Unset", "left"),
ariaLabel: app.attrs.tooltip("Unset"),
onclick: () => {
toggle(opt);
return false;
+5 -4
View File
@@ -124,10 +124,10 @@ window.app.components.tinymce = function(propsArg = {}) {
clearTimeout(changeTimeoutId);
// workaround for https://github.com/tinymce/tinymce/issues/9377
editorRef.dom?.unbind(document);
catchError(() => {
// workaround for https://github.com/tinymce/tinymce/issues/9377
editorRef.dom?.unbind(document);
window.tinymce?.remove(editorRef);
});
editorRef = null;
@@ -162,6 +162,7 @@ window.app.components.tinymce = function(propsArg = {}) {
autoresize_bottom_margin: 30,
media_poster: false,
media_alt_source: false,
ui_mode: "split",
codesample_languages: [
{ text: "HTML/XML", value: "markup" },
{ text: "CSS", value: "css" },
@@ -202,7 +203,7 @@ window.app.components.tinymce = function(propsArg = {}) {
"wordcount",
],
toolbar:
"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link table media_picker codesample | direction code",
"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link media_picker table codesample | direction code",
paste_postprocess: (editor, args) => {
cleanupPastedNode(args.node);
},
+1
View File
@@ -133,6 +133,7 @@ function addToast(textOrElem, options = {}) {
textOrElem,
t.button(
{
type: "button",
className: "m-l-auto btn circle sm transparent secondary toast-remove",
title: "Clear",
onclick: () => removeToast(toastRef),
+8 -3
View File
@@ -6,8 +6,9 @@ export function collectionViewQueryTab(upsertData) {
// dprint-ignore
const autocomplete = [
"SELECT", "FROM", "WHERE", "LEFT JOIN", "INNER JOIN", "ON",
"GROUP BY", "HAVING", "ORDER BY", "LIMIT", "OFFSET", "AS",
"WITH", "NOT", "IN", "EXISTS", "LIKE", "CAST",
"AS", "GROUP BY", "HAVING", "ORDER BY", "ASC", "DESC", "LIMIT",
"OFFSET", "WITH", "NOT", "IN", "AND", "OR", "EXISTS", "LIKE",
"CAST", "REAL", "DECIMAL", "NUMERIC", "INT", "TEXT", "BOOL",
];
const local = store({
@@ -171,9 +172,13 @@ export function collectionViewQueryTab(upsertData) {
),
t.div(
{ className: "col-12" },
t.p({ className: "txt-sm txt-bold" }, "Sample output:"),
t.p(
{ className: "txt-sm txt-bold" },
"Sample output:",
),
t.div(
{ className: "view-query-sample-wrapper" },
t.span({ hidden: () => !local.isTesting, className: "loader sm" }),
app.components.codeBlock({
language: () => local.testError ? "plain" : "js",
className: () => `view-query-sample ${local.testError ? "txt-danger" : ""}`,
+17 -5
View File
@@ -94,9 +94,12 @@ export function collectionsSidebar() {
),
t.button(
{
hidden: () => app.store.isLoadingCollections,
type: "button",
className: "btn sm circle transparent secondary link-faded",
disabled: () => app.store.isLoadingCollections,
className: () =>
`btn sm circle transparent secondary link-faded ${
app.store.isLoadingCollections ? "loading" : ""
}`,
ariaDescription: app.attrs.tooltip("Collections overview", "left"),
onclick: () => app.modals.openCollectionsOverview(),
},
@@ -125,11 +128,13 @@ export function collectionsSidebar() {
}),
);
},
// show the standalone loader only when there are no other collections loaded
() => {
if (app.store.isLoadingCollections) {
if (app.store.isLoadingCollections && !data.filteredCollections.length) {
return t.div({ className: "sidebar-content txt-center" }, t.span({ className: "loader sm" }));
}
},
() => {
return [
t.nav(
{
@@ -208,7 +213,14 @@ function collectionItem(collection, data) {
className: () =>
`nav-item responsive-close ${collection.id == app.store.activeCollection?.id ? "active" : ""}`,
title: () => collection.name,
onclick: () => app.store.activeCollection = collection.name,
onauxclick: (e) => {
e.preventDefault();
window.open(`#/collections?collection=${collection.name}`, "_blank", "noreferrer,noopener");
},
onclick: (e) => {
e.preventDefault();
app.store.activeCollection = collection.name;
},
},
t.i({
className: () => app.collectionTypes[collection.type]?.icon || app.utils.fallbackCollectionIcon,
+1 -1
View File
@@ -6,7 +6,7 @@ export function mfaAccordion(collection) {
if (!collection.mfa) {
collection.mfa = {
enabled: false,
duration: 900,
duration: 600,
rule: "",
};
}
+10 -11
View File
@@ -5,9 +5,10 @@ const FILTER_QUERY_KEY = "filter";
const COLLECTION_QUERY_KEY = "collection";
const RECORD_QUERY_KEY = "record";
const LAST_ACTIVE_STORAGE_KEY = "pbLastActiveCollection";
const TOTAL_COUNT_REQUEST_KEY = "recordsTotalCountRequest";
export function pageCollections(route) {
const uniqueId = "page_collections_" + app.utils.randomString();
app.store.activeCollection = route.query[COLLECTION_QUERY_KEY]?.[0]
|| window.localStorage.getItem(LAST_ACTIVE_STORAGE_KEY);
@@ -34,7 +35,8 @@ export function pageCollections(route) {
);
const result = await app.pb.collection(app.store.activeCollection.name).getList(1, 1, {
requestKey: TOTAL_COUNT_REQUEST_KEY,
// use a per page unique id and not a global constant to prevent race issues with the async unmount
requestKey: uniqueId,
filter: normalizedFilter,
fields: "id",
});
@@ -58,24 +60,21 @@ export function pageCollections(route) {
watch(
() => (app.store.activeCollection?.name || "") + (app.store.activeCollection?.updated || ""),
(newVal, oldVal) => {
// skip unnecessery initial params replacement
if (!oldVal) {
return;
}
app.store.title = app.store.activeCollection?.name || "Collections";
const hasChanged = oldVal && oldVal != newVal;
// reset filter and sort params on collection change
if (oldVal != newVal) {
if (hasChanged) {
pageData.filter = "";
pageData.sort = "";
}
app.store.title = app.store.activeCollection?.name || "Collections";
app.utils.replaceHashQueryParams({
[COLLECTION_QUERY_KEY]: app.store.activeCollection?.name,
[FILTER_QUERY_KEY]: pageData.filter || null,
[SORT_QUERY_KEY]: pageData.sort || null,
}, newVal != oldVal ? true : null);
}, hasChanged ? true : null);
if (app.store.activeCollection?.id) {
window.localStorage.setItem(LAST_ACTIVE_STORAGE_KEY, app.store.activeCollection.id);
@@ -184,7 +183,7 @@ export function pageCollections(route) {
}
},
onunmount: () => {
app.pb.cancelRequest(TOTAL_COUNT_REQUEST_KEY);
app.pb.cancelRequest(uniqueId);
watchers.forEach((w) => w?.unwatch());
+5 -5
View File
@@ -113,7 +113,7 @@ video {
}
::selection {
background: rgba(0, 0, 0, 0.2);
background: var(--selectionColor);
}
:root {
@@ -894,14 +894,14 @@ hr {
background: var(--surfaceAlt1Color);
background: linear-gradient(
90deg,
var(--surfaceAlt1Color) 8%,
var(--surfaceAlt2Color) 18%,
var(--surfaceAlt1Color) 33%
var(--surfaceAlt1Color) 10%,
var(--surfaceAlt2Color) 25%,
var(--surfaceAlt1Color) 60%
);
background-size: 200% 100%;
animation:
fadeIn var(--animationSpeed),
shine 0.8s linear infinite;
shine 1s linear infinite;
&.sm {
height: 8px;
}
+10
View File
@@ -88,7 +88,17 @@
padding: var(--smSpacing);
}
.view-query-sample-wrapper {
position: relative;
.loader {
position: absolute;
z-index: 1;
right: 10px;
top: 10px;
}
}
.view-query-sample code {
z-index: 0;
max-height: 300px;
background: none;
border: 1px solid var(--surfaceAlt2Color);
+20 -24
View File
@@ -1,24 +1,3 @@
@keyframes popoverHide {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translateY(3px);
}
}
@keyframes popoverShow {
from {
transform: translateY(3px);
opacity: 0;
}
to {
transform: translateY(0px);
opacity: 1;
}
}
.dropdown {
display: inline-flex;
flex-direction: column;
@@ -41,17 +20,34 @@
border-radius: var(--borderRadius);
border: 1px solid var(--surfaceAlt3Color);
&[popover] {
display: none;
position-area: bottom span-left;
position-try-fallbacks: flip-block;
display: none;
opacity: 0;
transform: translateY(3px);
transition:
opacity var(--animationSpeed),
transform var(--animationSpeed),
display var(--animationSpeed) allow-discrete,
overlay var(--animationSpeed) allow-discrete;
animation: popoverHide var(--animationSpeed) forwards;
&:popover-open {
animation: popoverShow var(--animationSpeed) forwards;
display: inline-flex;
opacity: 1;
transform: translateY(0);
}
@starting-style {
&:popover-open {
opacity: 0;
transform: translateY(3px);
}
}
/* @todo temp Safari freeze workaround (#7650) */
@supports not (fill: context-stroke) {
position-try-fallbacks: none !important;
transition: none !important;
}
&.left {
position-area: bottom span-right;
}
+8 -3
View File
@@ -108,7 +108,7 @@
width: 100%;
height: auto;
min-height: 300px;
background-image: radial-gradient(var(--surfaceAlt3Color) 1px, transparent 1px);
background-image: radial-gradient(var(--surfaceAlt2Color) 1px, transparent 1px);
background-size: 20px 20px;
border-radius: var(--borderRadius);
touch-action: none; /* handled by js */
@@ -138,7 +138,6 @@
width: 2000px; /* something large enough to fit the lines */
height: 8000px;
pointer-events: none;
transition: opacity var(--animationSpeed);
}
.paths-group {
fill: none;
@@ -146,7 +145,7 @@
stroke: var(--pathColor);
}
.erd-paths.front {
--pathColor: var(--surfaceAlt5Color);
--pathColor: var(--surfaceTxtDisabledColor);
z-index: 2;
.active-from {
@@ -176,6 +175,12 @@
cursor: grabbing;
}
&.active {
.erd-table {
opacity: 0.7;
&.active {
opacity: 1;
}
}
.erd-paths.back {
opacity: 0.3;
}
+30 -7
View File
@@ -487,17 +487,38 @@ button {
border-radius: var(--borderRadius);
}
}
.autoexpand-wrapper > .input,
textarea {
padding-top: 8px;
padding-bottom: 9px;
line-height: 1.5;
resize: vertical;
min-height: 38px;
max-height: 300px;
&.autoexpand {
/* @todo check firefox support */
field-sizing: content;
@supports (field-sizing: content) {
field-sizing: content;
resize: none;
}
}
}
/* @todo remove after Firefox add support for "field-sizing:content" */
/* note: based on https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ */
.autoexpand-wrapper {
display: grid;
> textarea {
resize: none;
min-height: 38px;
max-height: 300px;
}
> .input {
white-space: pre-wrap;
visibility: hidden;
overflow: auto;
}
> .input,
> textarea {
/* Place on top of each other */
grid-area: 1 / 1 / 2 / 2;
}
}
@@ -564,6 +585,7 @@ button {
&.required label,
label.required,
.required > label,
label:has(~ .autoexpand-wrapper textarea[required]),
label:has(~ [required]),
label:has(~ .input.required) {
&::after {
@@ -578,6 +600,7 @@ button {
&.disabled label,
label.disabled,
.disabled > label,
label:has(~ .autoexpand-wrapper textarea[disabled]),
label:has(~ [disabled]),
label:has(~ .input.disabled) {
color: var(--surfaceTxtDisabledColor);
@@ -651,15 +674,15 @@ button {
}
.field-list {
label {
padding-bottom: 9px;
}
.field-list-content {
overflow: auto;
max-height: 500px;
scrollbar-width: thin;
color: var(--surfaceTxtColor);
}
label + .field-list-content {
margin-top: 8px;
}
.field-list-item {
display: flex;
width: 100%;
+5
View File
@@ -613,4 +613,9 @@
flex-direction: column;
align-items: normal;
gap: 0;
/* Safari negative margin workaround to hide the extra horizontal scroll */
@supports not (fill: context-stroke) {
overflow-x: hidden;
}
}
+8
View File
@@ -15,6 +15,14 @@
display: none;
}
.list-content {
display: block;
width: 100%;
max-height: 425px;
overflow: auto;
scrollbar-width: thin;
}
.list-item {
word-break: break-word;
position: relative;
+5 -6
View File
@@ -196,12 +196,6 @@
}
}
.record-field-input {
.record-summary {
animation: fadeIn var(--animationSpeed);
}
}
.field-type-bool {
&.record-field-view .label {
min-width: 48px;
@@ -229,6 +223,11 @@
&.record-field-view {
white-space: nowrap;
}
/* disable multiple unnecessery required marks */
&.record-field-input .field-list-item label::after {
content: none;
display: none;
}
}
.field-type-select {
+14 -6
View File
@@ -3,11 +3,14 @@
.code-editor {
z-index: 0;
min-height: 41px;
.editor-content:empty::before {
font-family: var(--baseFontFamily);
}
.highlight-overlay {
color: var(--surfaceTxtColor);
}
.editor-content:empty::before {
font-family: var(--baseFontFamily);
.editor-content, .highlight-overlay {
white-space: pre-wrap;
}
}
.superuser-toggle,
@@ -32,8 +35,10 @@
font-size: var(--smFontSize);
padding: 8px 10px;
color: var(--surfaceTxtHintColor);
background: var(--surfaceAlt4Color);
box-shadow: inset 1px -1px 2px rgba(0,0,0,0.03);
background: var(--inputFocusColor);
border: 1px solid var(--surfaceAlt5Color);
border-top: 0;
border-right: 0;
transition:
color var(--animationSpeed),
background var(--animationSpeed);
@@ -41,14 +46,17 @@
&:active,
&:focus-visible {
color: var(--successColor);
background: var(--surfaceAlt5Color);
background: var(--inputBorderColor);
}
&[disabled] {
cursor: not-allowed;
color: var(--surfaceTxtDisabledColor);
background: var(--surfaceAlt4Color);
background: var(--surfaceAlt3Color);
}
}
.input:focus-within ~ .superuser-toggle {
background: var(--inputBorderColor);
}
label {
position: relative;
z-index: 2;
+1
View File
@@ -298,6 +298,7 @@ table {
.page-table-wrapper {
display: block;
width: auto;
min-height: 130px;
overflow: auto;
margin-left: calc(-1 * var(--spacing));
margin-right: calc(-1 * var(--spacing));
+3
View File
@@ -60,6 +60,9 @@
border-top-left-radius: var(--borderRadius);
border-bottom-left-radius: var(--borderRadius);
border-right: 1px solid color-mix(in srgb, var(--toastAccentColor), transparent 90%);
[data-color-scheme="dark"] & {
border-color: color-mix(in srgb, var(--toastAccentColor), transparent 75%);
}
&::before {
content: "\F414";
font-family: var(--iconFontFamily);
+30 -29
View File
@@ -12,9 +12,9 @@
--primaryTxtHintColor: color-mix(in srgb, var(--primaryTxtColor), transparent 35%);
--primaryTxtDisabledColor: color-mix(in srgb, var(--primaryTxtColor), transparent 50%);
--secondaryColor: #e8eaee;
--secondaryAlt1Color: #dcdfe5;
--secondaryAlt2Color: #c4cbd4;
--secondaryColor: #e4e8ec;
--secondaryAlt1Color: #dce0e5;
--secondaryAlt2Color: #d3d9df;
--secondaryTxtColor: var(--primaryColor);
--secondaryTxtHintColor: color-mix(in srgb, var(--secondaryTxtColor), transparent 35%);
--secondaryTxtDisabledColor: color-mix(in srgb, var(--secondaryTxtColor), transparent 50%);
@@ -55,20 +55,20 @@
--accentTxtDisabledColor: color-mix(in srgb, var(--accentTxtColor), transparent 50%);
--surfaceColor: #fff;
--surfaceAlt1Color: #f6f7f9;
--surfaceAlt2Color: #e8eaee;
--surfaceAlt3Color: #dfe2e7;
--surfaceAlt4Color: #d9dde2;
--surfaceAlt5Color: #d1d6dc;
--surfaceAlt1Color: #f8f9fa;
--surfaceAlt2Color: #e4e8ec;
--surfaceAlt3Color: #dce0e5;
--surfaceAlt4Color: #d3d9df;
--surfaceAlt5Color: #ccd4db;
--surfaceTxtColor: #25272d;
--surfaceTxtHintColor: #6b747b;
--surfaceTxtDisabledColor: #abaeba;
--surfaceTxtHintColor: #687278;
--surfaceTxtDisabledColor: #a2a9ae;
--surfaceAccentColor: color-mix(in srgb, var(--accentColor), white 80%);
--surfaceInfoColor: color-mix(in srgb, var(--infoColor), white 80%);
--surfaceSuccessColor: color-mix(in srgb, var(--successColor), white 80%);
--surfaceWarningColor: color-mix(in srgb, var(--warningColor), white 80%);
--surfaceDangerColor: color-mix(in srgb, var(--dangerColor), white 80%);
--selectionColor: var(--surfaceAlt3Color);
--selectionColor: rgba(70, 90, 115, 0.25);
--inputColor: var(--surfaceAlt2Color);
--inputFocusColor: var(--surfaceAlt3Color);
--inputBorderColor: var(--surfaceAlt4Color);
@@ -85,7 +85,7 @@
/* ---------------- */
--modalAnimationSpeed: 200ms;
--modalOverlayColor: rgba(34, 36, 42, 0.22);
--modalOverlayColor: rgba(70, 90, 115, 0.25);
--tooltipTxtColor: #fff;
--tooltipSurfaceColor: rgba(34, 36, 36, 0.9);
@@ -138,28 +138,27 @@
/* dark */
[data-color-scheme="dark"],
[data-color-scheme="dark"] .dropdown {
--surfaceColor: #282828;
--surfaceColor: #1f1f1f;
--surfaceAlt1Color: color-mix(in srgb, var(--surfaceColor), white 2%);
--surfaceAlt2Color: color-mix(in srgb, var(--surfaceColor), white 4%);
--surfaceAlt3Color: color-mix(in srgb, var(--surfaceColor), white 6%);
--surfaceAlt4Color: color-mix(in srgb, var(--surfaceColor), white 9%);
--surfaceAlt5Color: color-mix(in srgb, var(--surfaceColor), white 12%);
--surfaceAlt2Color: color-mix(in srgb, var(--surfaceColor), white 7%);
--surfaceAlt3Color: color-mix(in srgb, var(--surfaceColor), white 12%);
--surfaceAlt4Color: color-mix(in srgb, var(--surfaceColor), white 14%);
--surfaceAlt5Color: color-mix(in srgb, var(--surfaceColor), white 16%);
--surfaceTxtColor: #dedede;
--surfaceTxtColor: #e3e3e3;
--tooltipTxtColor: var(--surfaceTxtColor);
--surfaceTxtHintColor: color-mix(in srgb, var(--surfaceTxtColor), transparent 45%);
--surfaceTxtDisabledColor: color-mix(in srgb, var(--surfaceTxtColor), transparent 65%);
--secondaryColor: var(--surfaceAlt3Color);
--secondaryColor: var(--surfaceAlt2Color);
--secondaryTxtColor: var(--surfaceTxtColor);
--secondaryTxtColor: var(--surfaceTxtColor);
--secondaryAlt1Color: var(--surfaceAlt4Color);
--secondaryAlt2Color: var(--surfaceAlt5Color);
--secondaryAlt1Color: var(--surfaceAlt3Color);
--secondaryAlt2Color: var(--surfaceAlt4Color);
--primaryColor: #1e1e1e;
--primaryColor: #161616;
--primaryTxtColor: var(--surfaceTxtColor);
--primaryAlt1Color: color-mix(in srgb, var(--primaryColor), black 8%);
--primaryAlt2Color: color-mix(in srgb, var(--primaryColor), black 16%);
--primaryAlt1Color: color-mix(in srgb, var(--primaryColor), white 1%);
--primaryAlt2Color: color-mix(in srgb, var(--primaryColor), white 1.5%);
--primaryTxtHintColor: color-mix(in srgb, var(--primaryTxtColor), transparent 35%);
--primaryTxtDisabledColor: color-mix(in srgb, var(--primaryTxtColor), transparent 50%);
@@ -169,30 +168,32 @@
--infoAlt2Color: color-mix(in srgb, var(--infoColor), black 15%);
--surfaceInfoColor: color-mix(in srgb, var(--infoColor), var(--surfaceColor) 45%);
--successColor: #257e58;
--successColor: #27865d;
--successTxtColor: var(--surfaceTxtColor);
--successAlt1Color: color-mix(in srgb, var(--successColor), black 10%);
--successAlt2Color: color-mix(in srgb, var(--successColor), black 15%);
--surfaceSuccessColor: color-mix(in srgb, var(--successColor), var(--surfaceColor) 45%);
--warningColor: #ae4d0e;
--warningColor: #cf4e17;
--warningTxtColor: var(--surfaceTxtColor);
--warningAlt1Color: color-mix(in srgb, var(--warningColor), black 10%);
--warningAlt2Color: color-mix(in srgb, var(--warningColor), black 15%);
--surfaceWarningColor: color-mix(in srgb, var(--warningColor), var(--surfaceColor) 45%);
--dangerColor: #b6354c;
--dangerColor: #bb303e;
--dangerTxtColor: var(--surfaceTxtColor);
--dangerAlt1Color: color-mix(in srgb, var(--dangerColor), black 10%);
--dangerAlt2Color: color-mix(in srgb, var(--dangerColor), black 15%);
--surfaceDangerColor: color-mix(in srgb, var(--dangerColor), var(--surfaceColor) 45%);
--selectionColor: rgba(255,255,255, 0.22);
--inputColor: var(--surfaceAlt2Color);
--inputFocusColor: var(--surfaceAlt3Color);
--inputBorderColor: var(--surfaceAlt4Color);
scrollbar-color: var(--surfaceAlt5Color) transparent;
--modalOverlayColor: rgb(0,0,0,0.5);
--modalOverlayColor: rgb(0,0,0, 0.45);
--tooltipSurfaceColor: rgb(22,22,22, 0.9);
--boxShadow: 0px 8px 5px -5px rgba(0,0,0, 0.3);
--leftBoxShadow: -1px 0px 5px 0 rgba(0,0,0, 0.3);
+12 -7
View File
@@ -7,26 +7,31 @@
export function input(props) {
const uniqueId = "editor_" + app.utils.randomString();
const local = store({
lazyEditor: null,
const data = store({
lazyEditor: "",
});
let lazyEditorTimeoutId;
return t.div(
{
className: "record-field-input field-type-editor large-modal",
onmount: () => {
requestAnimationFrame(() => {
local.lazyEditor = app.components.tinymce({
lazyEditorTimeoutId = setTimeout(() => {
data.lazyEditor = app.components.tinymce({
id: uniqueId,
name: () => props.field.name,
required: () => props.field.required,
convertURLs: () => props.field.convertURLs,
name: () => props.field.name,
value: () => props.record[props.field.name] || "",
onchange: (val) => {
props.record[props.field.name] = val;
},
});
});
}, 0);
},
onunmount: () => {
clearTimeout(lazyEditorTimeoutId);
},
},
t.div(
@@ -36,7 +41,7 @@ export function input(props) {
t.i({ className: app.fieldTypes.editor.icon, ariaHidden: true }),
t.span({ className: "txt" }, () => props.field.name),
),
() => local.lazyEditor,
() => data.lazyEditor,
),
() => {
if (props.field.help) {
+1 -1
View File
@@ -240,7 +240,7 @@ export function input(props) {
},
},
t.div(
{ className: () => `field ${props.field.required ? "required" : ""}` },
{ className: () => `field-list ${props.field.required ? "required" : ""}` },
t.label(
{ htmlFor: uniqueId },
t.i({ className: app.fieldTypes.file.icon, ariaHidden: true }),
+1 -1
View File
@@ -304,7 +304,7 @@ export function settings(data) {
t.span({ className: "txt" }, "Protected"),
t.small(
{ className: "txt-hint" },
"Files will require View API rule permissions and file token (",
"File download requests will need to satisfy the View API rule (",
t.a({
href: import.meta.env.PB_PROTECTED_FILE_DOCS,
target: "_blank",
+4 -4
View File
@@ -33,8 +33,8 @@ export function input(data) {
id: uniqueId + ".lon",
type: "number",
step: "any",
min: "-180",
max: "180",
min: -180,
max: 180,
placeholder: 0,
name: () => data.field.name,
required: () => data.field.required,
@@ -53,8 +53,8 @@ export function input(data) {
id: uniqueId + ".lat",
type: "number",
step: "any",
min: "-90",
max: "90",
min: -90,
max: 90,
placeholder: 0,
name: () => data.field.name,
required: () => data.field.required,
+2 -2
View File
@@ -24,8 +24,8 @@ export function input(props) {
required: () => props.field.required,
min: () => props.field.min,
max: () => props.field.max,
value: () => props.record[props.field.name] || "",
oninput: (e) => (props.record[props.field.name] = Number(e.target.value)),
value: () => props.record[props.field.name] || 0,
oninput: (e) => props.record[props.field.name] = Number(e.target.value),
}),
),
() => {
+35 -23
View File
@@ -21,8 +21,14 @@ export function settings(data) {
type: "text",
id: uniqueId + ".min",
name: () => `fields.${data.fieldIndex}.min`,
value: () => data.field.min || "",
oninput: (e) => (data.field.min = Number(e.target.value)),
value: () => typeof data.field.min == "number" ? data.field.min : "",
oninput: (e) => {
if (!e.target.value) {
data.field.min = null;
} else {
data.field.min = Number(e.target.value);
}
},
}),
),
),
@@ -36,8 +42,14 @@ export function settings(data) {
id: uniqueId + ".max",
min: () => data.field.min,
name: () => `fields.${data.fieldIndex}.max`,
value: () => data.field.max || "",
oninput: (e) => (data.field.max = Number(e.target.value)),
value: () => typeof data.field.max == "number" ? data.field.max : "",
oninput: (e) => {
if (!e.target.value) {
data.field.max = null;
} else {
data.field.max = Number(e.target.value);
}
},
}),
),
),
@@ -57,25 +69,6 @@ export function settings(data) {
),
),
footer: () => [
t.div(
{ className: "field" },
t.input({
className: "sm",
type: "checkbox",
id: uniqueId + ".onlyInt",
name: () => `fields.${data.fieldIndex}.onlyInt`,
checked: () => !!data.field.onlyInt,
onchange: (e) => (data.field.onlyInt = e.target.checked),
}),
t.label(
{ htmlFor: uniqueId + ".onlyInt" },
t.span({ className: "txt" }, "No decimals"),
t.i({
className: "ri-information-line link-hint",
ariaDescription: app.attrs.tooltip("Existing decimal numbers will not be affected."),
}),
),
),
t.div(
{ className: "field" },
t.input({
@@ -96,6 +89,25 @@ export function settings(data) {
}),
),
),
t.div(
{ className: "field" },
t.input({
className: "sm",
type: "checkbox",
id: uniqueId + ".onlyInt",
name: () => `fields.${data.fieldIndex}.onlyInt`,
checked: () => !!data.field.onlyInt,
onchange: (e) => (data.field.onlyInt = e.target.checked),
}),
t.label(
{ htmlFor: uniqueId + ".onlyInt" },
t.span({ className: "txt" }, "No decimals"),
t.i({
className: "ri-information-line link-hint",
ariaDescription: app.attrs.tooltip("Existing decimal numbers will not be affected."),
}),
),
),
],
});
}
+40 -22
View File
@@ -36,34 +36,51 @@ export function input(props) {
return;
}
try {
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
const resultRecords = [];
const idsToLoad = [];
// eagerly expand first level presentable relations (if any and the collections are loaded)
const relExpands = [];
const presentableRelationFields = fieldCollection?.fields?.filter(
(f) => !f.hidden && f.presentable && f.type == "relation",
) || [];
for (const field of presentableRelationFields) {
relExpands.push(field.name);
// check for preloaded expand
const expanded = app.utils.toArray(props.record.expand?.[props.field.name]);
for (const id of ids) {
const found = expanded.find((r) => r.id == id);
if (found) {
resultRecords.push(found);
} else {
idsToLoad.push(id);
}
}
const records = await app.pb.collection(props.field.collectionId).getFullList({
requestKey: null,
filter: ids.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
expand: relExpands.join(",") || undefined,
});
try {
if (idsToLoad.length) {
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
// preserve the original order
const orderedRecords = [];
for (let id of ids) {
const record = records.find((r) => r.id == id);
if (record) {
orderedRecords.push(record);
// eagerly expand first level presentable relations (if any and the collections are loaded)
const relExpands = [];
const presentableRelationFields = fieldCollection?.fields?.filter(
(f) => !f.hidden && f.presentable && f.type == "relation",
) || [];
for (const field of presentableRelationFields) {
relExpands.push(field.name);
}
const records = await app.pb.collection(props.field.collectionId).getFullList({
requestKey: null,
filter: idsToLoad.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
expand: relExpands.join(",") || undefined,
});
// preserve the original order
for (const id of idsToLoad) {
const found = records.find((r) => r.id == id);
if (found) {
resultRecords.push(found);
} else {
console.warn("missing relation id:", id);
}
}
}
local.selected = orderedRecords;
local.selected = resultRecords;
local.isLoading = false;
} catch (err) {
if (!err.isAbort) {
@@ -135,6 +152,7 @@ export function input(props) {
{ className: "actions" },
t.button(
{
type: "button",
className: "btn sm secondary transparent circle",
ariaLabel: app.attrs.tooltip("Remove"),
onclick: () => remove(record.id),
@@ -180,7 +198,7 @@ export function input(props) {
},
},
t.div(
{ className: () => `field ${props.field.required ? "required" : ""}` },
{ className: () => `field-list ${props.field.required ? "required" : ""}` },
t.label(
{ htmlFor: uniqueId },
t.i({ className: app.fieldTypes.relation.icon, ariaHidden: true }),
+12 -11
View File
@@ -4,7 +4,7 @@
// record: undefined,
// field: undefined,
// }
export function input(data) {
export function input(props) {
const uniqueId = "select_" + app.utils.randomString();
return t.div(
@@ -14,33 +14,34 @@ export function input(data) {
t.label(
{ htmlFor: uniqueId },
t.i({ className: app.fieldTypes.select.icon, ariaHidden: true }),
t.span({ className: "txt" }, () => data.field.name),
t.span({ className: "txt" }, () => props.field.name),
),
app.components.select({
id: uniqueId,
max: () => data.field.maxSelect || 1,
required: () => data.field.required,
name: () => props.field.name,
max: () => props.field.maxSelect || 1,
required: () => props.field.required,
options: () => {
return data.field.values.map((v) => {
return props.field.values.map((v) => {
return { value: v };
});
},
value: () => {
return app.utils.toArray(data.record[data.field.name]);
return app.utils.toArray(props.record[props.field.name]);
},
onchange: (opts) => {
if (data.field.maxSelect <= 1) {
data.record[data.field.name] = opts?.[0]?.value || "";
if (props.field.maxSelect <= 1) {
props.record[props.field.name] = opts?.[0]?.value || "";
return;
}
data.record[data.field.name] = opts.map((o) => o.value);
props.record[props.field.name] = opts.map((o) => o.value);
},
}),
),
() => {
if (data.field.help) {
return t.div({ className: "field-help" }, data.field.help);
if (props.field.help) {
return t.div({ className: "field-help" }, props.field.help);
}
},
);
+27 -12
View File
@@ -33,17 +33,32 @@ export function input(props) {
}),
t.span({ className: "txt" }, () => props.field.name),
),
t.textarea({
id: uniqueId,
className: "autoexpand",
rows: 1,
name: () => props.field.name,
required: () => data.isRequired,
disabled: () => data.isDisabled,
placeholder: () => (data.hasAutogenerate ? "Leave empty to autogenerate..." : ""),
value: () => props.record[props.field.name] || "",
oninput: (e) => (props.record[props.field.name] = e.target.value || ""),
}),
// @todo remove after Firefox add support for "field-sizing:content"
//
// note1: not using contenteditable because it requires keeping
// track of the cursor offset when replacing the content in oninput
//
// note2: based on https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/
t.div(
{ className: "autoexpand-wrapper" },
t.textarea({
id: uniqueId,
// className: "autoexpand",
rows: 1,
name: () => props.field.name,
required: () => data.isRequired,
disabled: () => data.isDisabled,
placeholder: () => (data.hasAutogenerate ? "Leave empty to autogenerate..." : ""),
value: () => props.record[props.field.name] || "",
oninput: (e) => (props.record[props.field.name] = e.target.value || ""),
}),
t.div(
{ className: "input" },
() => props.record[props.field.name],
// the empty space is necessary to prevent jumpy behavior
" ",
),
),
),
// list the autodate field values in a tooltip next to the primary key
() => {
@@ -58,7 +73,7 @@ export function input(props) {
const autodateValues = [];
for (let f of autodateFields) {
autodateValues.push(`${f.name}: ${app.utils.stringifyValue(props.record[f.name])}`);
autodateValues.push(`${f.name}: ${app.utils.displayValue(props.record[f.name])}`);
}
return t.div(
+1 -1
View File
@@ -239,7 +239,7 @@ function logPreviewModal(logIdOrModel, settings) {
return t.span({
className: "txt",
textContent: app.utils.stringifyValue(value, "N/A", 1000),
textContent: app.utils.displayValue(value, 1000),
});
}),
t.td({ className: "col-copy min-width" }, app.components.copyButton(value)),
+2 -8
View File
@@ -374,18 +374,12 @@ export function logsList(logsSettings) {
if (app.utils.logDataFormatters[keyItem.key]) {
value = app.utils.logDataFormatters[keyItem.key](log);
} else {
value = app.utils.stringifyValue(
log.data[keyItem.key],
"N/A",
80,
);
value = app.utils.displayValue(log.data[keyItem.key], 80);
}
labels.push(
t.span(
{
className: `label sm ${keyItem.label || ""}`,
},
{ className: `label sm ${keyItem.label || ""}` },
`${keyItem.key}: ${value}`,
),
);
+47 -59
View File
@@ -54,6 +54,18 @@ function copyJSON(record) {
}
function recordPreviewModal(rawRecord, modalSettings) {
if (!rawRecord?.id) {
app.toasts.error("Failed to load record.");
console.warn("[recordPreviewModal] missing required record id field:", rawRecord);
return;
}
if (!rawRecord.collectionId && !rawRecord.collectionName) {
app.toasts.error("Failed to load record.");
console.warn("[recordPreviewModal] missing required collectionId or collectionName field:", rawRecord);
return;
}
let modal;
const uniqueId = app.utils.randomString();
@@ -69,39 +81,31 @@ function recordPreviewModal(rawRecord, modalSettings) {
});
async function loadRecord() {
if (!rawRecord?.id) {
app.toasts.error("Failed to load record.");
setTimeout(() => app.modals.close(modal), 0);
console.warn("[recordPreviewModal] missing required record id field:", rawRecord);
return;
}
if (!rawRecord.collectionId && !rawRecord.collectionName) {
app.toasts.error("Failed to load record.");
setTimeout(() => app.modals.close(modal), 0);
console.warn("[recordPreviewModal] missing required collectionId or collectionName field:", rawRecord);
return;
}
data.isLoading = true;
try {
// eagerly expand first level presentable relations (if any and the collections are loaded)
// preload to minimize content jumps
data.record = JSON.parse(JSON.stringify(rawRecord));
// eagerly expand first level relations (if any and the collections are loaded)
let relExpands = [];
const presentableRelationFields = data.collection?.fields?.filter(
(f) => !f.hidden && f.presentable && f.type == "relation",
(f) => !f.hidden && f.type == "relation",
) || [];
for (let field of presentableRelationFields) {
relExpands.push(field.name);
}
data.record = await app.pb
.collection(rawRecord.collectionId || rawRecord.collectionName)
const record = await app.pb
.collection(rawRecord.collectionName || rawRecord.collectionId)
.getOne(rawRecord.id, {
requestKey: "record_preview_" + rawRecord.id,
expand: relExpands.join(",") || undefined,
});
// populate with an up-to-date fields
Object.assign(data.record, record);
data.isLoading = false;
} catch (err) {
if (!err?.isAbort) {
@@ -130,10 +134,6 @@ function recordPreviewModal(rawRecord, modalSettings) {
modalSettings.onafterclose?.(el);
el?.remove();
},
onmount: (el) => {
},
onunmount: (el) => {
},
},
t.header(
{ className: "modal-header" },
@@ -145,45 +145,32 @@ function recordPreviewModal(rawRecord, modalSettings) {
t.button(
{
title: "More options",
className: "btn sm circle transparent m-l-auto",
className: () => `btn sm circle transparent m-l-auto ${data.isLoading ? "loading" : ""}`,
disabled: () => data.isLoading,
"html-popovertarget": uniqueId + "preview-dropdown",
},
t.i({ className: "ri-more-line", ariaHidden: true }),
),
t.div({ id: uniqueId + "preview-dropdown", className: "dropdown", popover: "auto" }, (el) => {
return t.button(
{
className: "dropdown-item",
onclick: () => {
copyJSON(data.record);
el.hidePopover();
t.div(
{ id: uniqueId + "preview-dropdown", className: "dropdown", popover: "auto" },
(el) => {
return t.button(
{
className: "dropdown-item",
onclick: () => {
copyJSON(data.record);
el.hidePopover();
},
},
},
t.i({ className: "ri-braces-line", ariaHidden: true }),
t.span({ className: "txt" }, "Copy JSON"),
);
}),
t.i({ className: "ri-braces-line", ariaHidden: true }),
t.span({ className: "txt" }, "Copy JSON"),
);
},
),
),
t.div({ className: "modal-content" }, () => {
// loader
if (data.isLoading || !data.record?.id || !data.collection?.id) {
return t.table(
null,
t.tbody(null, () => {
const totalRows = data.collection?.fields?.filter((f) => f.type != "password").length || 1;
const rows = [];
for (let i = 0; i < totalRows; i++) {
rows.push(t.tr(null, t.td(null, t.span({ className: "skeleton-loader" }))));
}
return rows;
}),
);
}
// attrs
return t.table(
t.div(
{ className: "modal-content" },
t.table(
{
pbEvent: "recordPreviewTable",
className: "record-preview-table responsive-table",
@@ -213,14 +200,14 @@ function recordPreviewModal(rawRecord, modalSettings) {
});
}
return app.utils.stringifyValue(data.record[f.name]);
return app.utils.displayValue(data.record[f.name], 1000);
},
),
);
});
}),
);
}),
),
),
t.footer(
{ className: "modal-footer" },
t.button(
@@ -234,7 +221,8 @@ function recordPreviewModal(rawRecord, modalSettings) {
t.button(
{
type: "button",
className: "btn",
className: () => `btn ${data.isLoading ? "loading" : ""}`,
disabled: () => data.isLoading,
onclick: () => downloadJSON(data.record),
},
t.i({ className: "ri-download-line", ariaHidden: true }),
+94 -79
View File
@@ -43,28 +43,27 @@ window.app.modals.openRecordUpsert = function(collection, record = null, modalSe
app.modals.open(modal);
};
const defaultRedactFields = ["expand"];
function redacted(record, redactFields = defaultRedactFields) {
// create redacted clone only if necessery
if (redactFields.find((f) => typeof record[f] !== "undefined")) {
record = Object.assign({}, record);
for (let f of redactFields) {
delete record[f];
}
// redact common sensitive fields
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
function redactedReplacer(key, val) {
switch (key) {
case "expand":
case "password":
case "passwordConfirm":
case "tokenKey":
return undefined;
}
return record;
return val;
}
function downloadJSON(record) {
record = redacted(record);
app.utils.downloadJSON(record, record.collectionName + "_" + record.id + ".json");
const pojo = JSON.parse(JSON.stringify(record, redactedReplacer));
app.utils.downloadJSON(pojo, record.collectionName + "_" + record.id + ".json");
}
function copyJSON(record) {
record = redacted(record);
app.utils.copyToClipboard(JSON.stringify(record, null, 2));
app.utils.copyToClipboard(JSON.stringify(record, redactedReplacer, 2));
app.toasts.success("Record copied to clipboard!");
}
@@ -73,7 +72,7 @@ function serializeRecord(record) {
return "";
}
return JSON.stringify(record);
return JSON.stringify(record, redactedReplacer);
}
const TAB_MAIN = "main";
@@ -193,36 +192,71 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
data.record = draftClone;
}
let draftWatcher;
function initDraftWatcher() {
data.initialDraft = getDraft();
draftWatcher?.unwatch();
draftWatcher = watch(() => data.recordHash, (newHash, oldHash) => {
if (typeof oldHash == "undefined") {
return;
}
if (data.hasChanges) {
saveDraft(newHash);
} else {
deleteDraft();
}
});
}
async function initRecord(rawRecord) {
data.isLoading = true;
const recordId = typeof rawRecord == "string" ? rawRecord : rawRecord?.id;
draftWatcher?.unwatch();
// normalize rawRecord (could be plain id string)
rawRecord = app.utils.isObject(rawRecord) ? rawRecord : { id: rawRecord || "" };
// new record
if (!recordId) {
const record = app.utils.isObject(rawRecord) ? JSON.parse(JSON.stringify(rawRecord)) : {};
data.originalRecord = app.utils.emptyClone(record, ["collectionId", "collectionName"]);
data.initialDraft = getDraft();
data.record = record;
if (!rawRecord.id) {
data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
data.record = JSON.parse(JSON.stringify(rawRecord));
data.isLoading = false;
data.isLocked = false;
initDraftWatcher();
return;
}
data.isLocked = !!app.store.settings?.meta?.hideControls;
try {
// eagerly load to allow elements to show their "update" UI and minimize flickering
data.originalRecord = { id: recordId };
data.isLocked = !!app.store.settings?.meta?.hideControls;
const record = await app.pb.collection(collection.name).getOne(recordId, {
requestKey: "upsert_load_" + recordId,
// preload to minimize content jumps
data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
data.record = JSON.parse(JSON.stringify(rawRecord));
// fetch to ensure that the main record fields are up-to-date
let record = await app.pb.collection(collection.name).getOne(rawRecord.id, {
requestKey: "upsert_load_" + rawRecord.id,
});
data.originalRecord = record;
data.initialDraft = getDraft();
data.record = JSON.parse(JSON.stringify(record));
// preload existing expands (if any)
if (rawRecord.expand) {
record.expand = JSON.parse(JSON.stringify(rawRecord.expand));
}
// extend, not overwrite, to prevent reseting the reference passed down to the inputs
Object.assign(data.originalRecord, JSON.parse(JSON.stringify(record)));
Object.assign(data.record, JSON.parse(JSON.stringify(record)));
data.isLoading = false;
// schedule a macro task to allow fields to populate their reactive values
setTimeout(() => {
initDraftWatcher();
}, 0);
} catch (err) {
if (!err?.isAbort) {
app.checkApiError(err);
@@ -232,24 +266,32 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
}
}
function deleteInternalKeys(record) {
for (let key in record) {
if (key.startsWith("@@")) {
delete record[key];
}
}
}
async function exportPayload() {
const payload = {};
// shallow copy of the record fields
for (const prop in data.record) {
for (const key in data.record) {
// skip expand and internal dynamic enumerable props
if (prop == "expand" || prop.startsWith("@@")) {
if (key == "expand" || key.startsWith("@@")) {
continue;
}
let val = data.record[prop]?.__raw || data.record[prop];
let val = data.record[key]?.__raw || data.record[key];
// normalize undefined values
if (typeof val == "undefined") {
val = null;
}
payload[prop] = val;
payload[key] = val;
}
// apply fields save normalization funcs
@@ -297,12 +339,14 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
data.originalRecord = structuredClone(record);
data.record = structuredClone(record);
} else {
// don't overwrite to prevent loosing the reference passed down to the inputs
// extend, not overwrite, to prevent reseting the reference passed down to the inputs
Object.assign(data.originalRecord, structuredClone(record));
Object.assign(data.record, structuredClone(record));
deleteInternalKeys(data.originalRecord);
deleteInternalKeys(data.record);
}
modalSettings.onsave?.(structuredClone(record), isNew);
modalSettings.onsave?.(record, isNew);
// reset all errors
app.store.errors = null;
@@ -360,21 +404,15 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
initRecord(clone);
}
const watchers = [];
function mainTab() {
return [
t.div(
{ className: "modal-content" },
t.form(
t.div(
{
id: uniqueId + "form",
className: "grid",
inert: () => data.isLoading || data.isSaving,
onsubmit: (e) => {
e.preventDefault();
// save(); // don't allow to prevent accidental save on input enter
},
onmount: (el) => {
el._quickSaveHandler = (e) => {
if ((e.ctrlKey || e.metaKey) && e.code == "KeyS") {
@@ -605,31 +643,12 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
onbeforeopen: () => {
initRecord(rawRecord);
watchers.push(
watch(() => data.recordHash, (newHash, oldHash) => {
if (!oldHash || !newHash || newHash == "{}" || oldHash == "{}") {
return;
}
saveDraft(newHash);
}),
);
return modalSettings.onbeforeopen?.(el);
},
onafteropen: (el) => {
modalSettings.onafteropen?.(el);
},
onbeforeclose: (el, forceClosed) => {
if (
// there are no unsaved changes
!data.hasChanges
// the form has been edited
&& data.initialDraftHash != getDraftHash()
) {
deleteDraft();
}
if (forceClosed) {
return modalSettings.onbeforeclose?.(el);
}
@@ -656,11 +675,10 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
},
onafterclose: (el) => {
modalSettings.onafterclose?.(el);
watchers.forEach((w) => w?.unwatch());
el?.remove();
},
onunmount: () => {
watchers.forEach((w) => w?.unwatch());
draftWatcher?.unwatch();
},
},
t.header(
@@ -669,20 +687,15 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
{ className: "grid" },
t.div(
{ className: "col-12 flex" },
t.h6({ className: "modal-title" }, () => {
if (data.isLoading) {
return t.span({ className: "loader sm" });
}
return [
t.span(null, () => (data.isNew ? "Create " : "Edit ")),
t.strong(
{ className: "txt-ellipsis collection-name", style: "max-width: 220px" },
() => collection.name,
),
t.span(null, " record"),
];
}),
t.h6(
{ className: "modal-title" },
t.span(null, () => (data.isNew ? "Create " : "Edit ")),
t.strong(
{ className: "txt-ellipsis collection-name", style: "max-width: 220px" },
() => collection.name,
),
t.span(null, " record"),
),
t.div({ className: "flex-fill" }),
() => {
if (app.utils.isEmpty(data.originalRecord?.id)) {
@@ -693,8 +706,8 @@ function recordUpsertModal(collection, rawRecord, modalSettings) {
t.button(
{
type: "button",
className: "btn sm circle transparent",
title: "More options",
className: () => `btn sm circle transparent ${data.isLoading ? "loading" : ""}`,
disabled: () => data.isLoading,
"html-popovertarget": uniqueId + "modal-header-dropdown",
},
@@ -1242,6 +1255,7 @@ function authFieldPassword(collection, data) {
id: uniqueId + "_password",
spellcheck: false,
name: "password",
className: "inline-error",
autocomplete: "new-password",
required: () => local.isNew || local.changePassword,
value: () => data.record.password || "",
@@ -1268,6 +1282,7 @@ function authFieldPassword(collection, data) {
id: uniqueId + "_password_confirm",
spellcheck: false,
name: "passwordConfirm",
className: "inline-error",
autocomplete: "new-password",
required: () => local.isNew || local.changePassword,
value: () => data.record.passwordConfirm || "",
+2 -2
View File
@@ -94,9 +94,9 @@ window.app.components.recordsList = function(propsArg = {}) {
// eagerly expand first level relations
// (to prevent too many relation queries)
const relExpands = [];
const relationFields = props.collection.fields.filter(
const relationFields = props.collection.fields?.filter(
(f) => !f.hidden && f.type == "relation",
);
) || [];
for (const field of relationFields) {
relExpands.push(field.name);
}
+80 -76
View File
@@ -100,7 +100,8 @@ export function backupsList(propsArg = {}) {
return t.div(
{
className: "list",
pbEvent: "backupsList",
className: "list backups-list",
onmount: (el) => {
watchers.push(watch(() => props.reset, () => {
loadBackups();
@@ -116,83 +117,86 @@ export function backupsList(propsArg = {}) {
},
},
t.div(
{
hidden: () => !data.isLoading || data.backups.length,
className: "list-item",
{ className: "list-content" },
t.div(
{
hidden: () => !data.isLoading || data.backups.length,
className: "list-item",
},
t.div({ className: "skeleton-loader" }),
),
t.div(
{
hidden: () => data.isLoading || data.backups.length,
className: () => "list-item",
},
t.div({ className: "content block txt-hint" }, "No backups found."),
),
() => {
return data.backups.map((backup) => {
return t.div(
{ className: () => `list-item ${data.isLoading ? "faded" : ""}` },
t.i({ className: "ri-folder-zip-line", ariaHidden: true }),
t.div(
{ className: "content" },
t.span({
className: "backup-name txt-ellipsis",
title: () => backup.key,
textContent: () => backup.key,
}),
t.small(
{ className: "backup-size txt-hint txt-nowrap" },
"(",
() => app.utils.formattedFileSize(backup.size),
")",
),
),
t.nav(
{
hidden: () => data.isLoading,
className: "actions autohide",
},
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Download"),
className: () =>
`btn sm circle secondary transparent ${
data.isDownloading[backup.key] ? "loading" : ""
}`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => downloadBackup(backup.key),
},
t.i({ className: "ri-download-line", ariaHidden: true }),
),
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Restore"),
className: () => `btn sm circle secondary transparent`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => openBackupRestoreModal(backup.key),
},
t.i({ className: "ri-restart-line", ariaHidden: true }),
),
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Delete"),
className: () =>
`btn sm circle secondary transparent ${
data.isDeleting[backup.key] ? "loading" : ""
}`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => confirmBackupDelete(backup.key),
},
t.i({ className: "ri-delete-bin-7-line", ariaHidden: true }),
),
),
);
});
},
t.div({ className: "skeleton-loader" }),
),
t.div(
{
hidden: () => data.isLoading || data.backups.length,
className: () => "list-item",
},
t.div({ className: "content block txt-hint" }, "No backups found."),
),
() => {
return data.backups.map((backup) => {
return t.div(
{ className: () => `list-item ${data.isLoading ? "faded" : ""}` },
t.i({ className: "ri-folder-zip-line", ariaHidden: true }),
t.div(
{ className: "content" },
t.span({
className: "backup-name txt-ellipsis",
title: () => backup.key,
textContent: () => backup.key,
}),
t.small(
{ className: "backup-size txt-hint txt-nowrap" },
"(",
() => app.utils.formattedFileSize(backup.size),
")",
),
),
t.nav(
{
hidden: () => data.isLoading,
className: "actions autohide",
},
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Download"),
className: () =>
`btn sm circle secondary transparent ${
data.isDownloading[backup.key] ? "loading" : ""
}`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => downloadBackup(backup.key),
},
t.i({ className: "ri-download-line", ariaHidden: true }),
),
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Restore"),
className: () => `btn sm circle secondary transparent`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => openBackupRestoreModal(backup.key),
},
t.i({ className: "ri-restart-line", ariaHidden: true }),
),
t.button(
{
type: "button",
ariaLabel: app.attrs.tooltip("Delete"),
className: () =>
`btn sm circle secondary transparent ${
data.isDeleting[backup.key] ? "loading" : ""
}`,
disabled: () => data.isDeleting[backup.key] || data.isDownloading[backup.key],
onclick: () => confirmBackupDelete(backup.key),
},
t.i({ className: "ri-delete-bin-7-line", ariaHidden: true }),
),
),
);
});
},
t.div(
{ className: "list-item" },
t.button(
+11 -4
View File
@@ -179,12 +179,18 @@ window.app.store = store({
app.store.isLoadingCollections = true;
try {
const [resultScaffolds, resultCollections] = await Promise.all([
let [resultScaffolds, resultCollections] = await Promise.all([
app.pb.collections.getScaffolds({ requestKey: "appStore.loadCollections.getScaffolds" }),
app.pb.collections.getFullList({ requestKey: "appStore.loadCollections.getFullList" }),
]);
app.store.collections = app.utils.sortedCollectionsByType(resultCollections);
resultCollections = app.utils.sortedCollectionsByType(resultCollections);
// replace only if there are changes to minimize flickering
if (JSON.stringify(app.store.collections) != JSON.stringify(resultCollections)) {
app.store.collections = resultCollections;
}
app.store.collectionScaffolds = resultScaffolds;
app.store._activeCollectionIdOrName = activeIdOrName || app.store._activeCollectionIdOrName
|| app.store.collections[0]?.id || "";
@@ -351,7 +357,7 @@ function removeErrorState(input, container) {
watch(
() => JSON.stringify(app.store.errors) && app.store.errors,
(errs) => {
// search for input or other elements wiht "name" attribute
// search for input or other elements with "name" attribute
const inputs = document.querySelectorAll(`[name]`);
for (let input of inputs) {
@@ -359,7 +365,8 @@ watch(
continue;
}
const container = input.closest(".fields") || input.closest(".field");
// find the top-most wrapper field element
const container = input.closest(".field-list") || input.closest(".fields") || input.closest(".field");
if (!container) {
continue;
}
+17 -26
View File
@@ -348,6 +348,8 @@ const utils = {
clone[prop] = "";
} else if (typeof clone[prop] == "number") {
clone[prop] = 0;
} else if (typeof clone[prop] == "boolean") {
clone[prop] = false;
} else if (Array.isArray(clone[prop])) {
clone[prop] = [];
} else if (app.utils.isObject(clone[prop])) {
@@ -606,47 +608,36 @@ const utils = {
},
/**
* Stringifies the provided value or fallback to missingValue in case it is empty.
* Returns a stringified truncated version of the provided value
* or fallback to `missingValue` in case it is empty.
*
* @param {Mixed} val
* @param {number} [truncateLength]
* @param {string} [missingValue]
* @param {string} [truncateLength]
* @return {string}
*/
stringifyValue(val, missingValue = "N/A", truncateLength = 150) {
displayValue(val, truncateLength = 150, missingValue = "N/A") {
// check the raw value for "emptiness"
if (utils.isEmpty(val)) {
return missingValue;
}
if (typeof val == "number") {
return "" + val;
}
if (typeof val == "boolean") {
return val ? "True" : "False";
}
if (typeof val == "string") {
val = val.indexOf("<") >= 0 ? utils.plainText(val) : val;
return utils.truncate(val, truncateLength) || missingValue;
}
// plain array
if (Array.isArray(val) && typeof val[0] != "object") {
return utils.truncate(val.join(","), truncateLength);
}
// json
if (typeof val == "object") {
// already a string
} else if (typeof val == "boolean") {
val = val ? "True" : "False";
} else if (Array.isArray(val) && typeof val[0] != "object") {
// assuming primitive array values
val = val.map((child) => utils.displayValue(child, truncateLength, missingValue)).join(", ");
} else {
try {
return utils.truncate(JSON.stringify(val), truncateLength) || missingValue;
val = JSON.stringify(val) || "";
} catch (_) {
return missingValue;
val = "" + val;
}
}
// return as it is
return val;
return val ? utils.truncate(val, truncateLength) : missingValue;
},
/**