initial v0.8 pre-release
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import providersList from "@/providers.js";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let record;
|
||||
|
||||
let externalAuths = [];
|
||||
let isLoading = false;
|
||||
|
||||
function getProviderTitle(provider) {
|
||||
return providersList[provider + "Auth"]?.title || CommonHelper.sentenize(auth.provider, false);
|
||||
}
|
||||
|
||||
function getProviderIcon(provider) {
|
||||
return providersList[provider + "Auth"]?.icon || `ri-${provider}-line`;
|
||||
}
|
||||
|
||||
async function loadExternalAuths() {
|
||||
if (!record?.id) {
|
||||
externalAuths = [];
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
externalAuths = await ApiClient.collection(record.collectionId).listExternalAuths(record.id);
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function unlinkExternalAuth(provider) {
|
||||
if (!record?.id || !provider) {
|
||||
return; // nothing to unlink
|
||||
}
|
||||
|
||||
confirm(`Do you really want to unlink the ${getProviderTitle(provider)} provider?`, () => {
|
||||
return ApiClient.collection(record.collectionId)
|
||||
.unlinkExternalAuth(record.id, provider)
|
||||
.then(() => {
|
||||
addSuccessToast(`Successfully unlinked the ${getProviderTitle(provider)} provider.`);
|
||||
dispatch("unlink", provider);
|
||||
loadExternalAuths(); // reload list
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadExternalAuths();
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="block txt-center">
|
||||
<span class="loader" />
|
||||
</div>
|
||||
{:else if record?.id && externalAuths.length}
|
||||
<div class="list">
|
||||
{#each externalAuths as auth}
|
||||
<div class="list-item">
|
||||
<i class={getProviderIcon(auth.provider)} />
|
||||
<span class="txt">{getProviderTitle(auth.provider)}</span>
|
||||
<div class="txt-hint">ID: {auth.providerId}</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary link-hint btn-circle btn-sm m-l-auto"
|
||||
on:click={() => unlinkExternalAuth(auth.provider)}
|
||||
>
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="txt-hint txt-center">No linked OAuth2 providers.</p>
|
||||
{/if}
|
||||
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let params;
|
||||
|
||||
let password = "";
|
||||
let isLoading = false;
|
||||
let success = false;
|
||||
|
||||
$: newEmail = CommonHelper.getJWTPayload(params?.token).newEmail || "";
|
||||
|
||||
async function submit() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
// init a custom client to avoid interfering with the admin state
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client.collection(payload.collectionId).confirmEmailChange(params?.token, password);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FullPage nobranding>
|
||||
{#if success}
|
||||
<div class="alert alert-success">
|
||||
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
|
||||
<div class="content txt-bold">
|
||||
<p>Successfully changed the user email address.</p>
|
||||
<p>You can now sign in with your new email address.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-block" on:click={() => window.close()}>
|
||||
Close
|
||||
</button>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<div class="content txt-center m-b-base">
|
||||
<h5>
|
||||
Type your password to confirm changing your email address
|
||||
{#if newEmail}
|
||||
to <strong class="txt-nowrap">{newEmail}</strong>
|
||||
{/if}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<Field class="form-field required" name="password" let:uniqueId>
|
||||
<label for={uniqueId}>Password</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="password" id={uniqueId} required autofocus bind:value={password} />
|
||||
</Field>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-lg btn-block"
|
||||
class:btn-loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<span class="txt">Confirm new email</span>
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</FullPage>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let params;
|
||||
|
||||
let newPassword = "";
|
||||
let newPasswordConfirm = "";
|
||||
let isLoading = false;
|
||||
let success = false;
|
||||
|
||||
$: email = CommonHelper.getJWTPayload(params?.token).email || "";
|
||||
|
||||
async function submit() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
// init a custom client to avoid interfering with the admin state
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client
|
||||
.collection(payload.collectionId)
|
||||
.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FullPage nobranding>
|
||||
{#if success}
|
||||
<div class="alert alert-success">
|
||||
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
|
||||
<div class="content txt-bold">
|
||||
<p>Successfully changed the user password.</p>
|
||||
<p>You can now sign in with your new password.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-block" on:click={() => window.close()}>
|
||||
Close
|
||||
</button>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<div class="content txt-center m-b-base">
|
||||
<h5>
|
||||
Reset your user password
|
||||
{#if email}
|
||||
for <strong>{email}</strong>
|
||||
{/if}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<Field class="form-field required" name="password" let:uniqueId>
|
||||
<label for={uniqueId}>New password</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="password" id={uniqueId} required autofocus bind:value={newPassword} />
|
||||
</Field>
|
||||
|
||||
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
|
||||
<label for={uniqueId}>New password confirm</label>
|
||||
<input type="password" id={uniqueId} required bind:value={newPasswordConfirm} />
|
||||
</Field>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-lg btn-block"
|
||||
class:btn-loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<span class="txt">Set new password</span>
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</FullPage>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
|
||||
export let params;
|
||||
|
||||
let success = false;
|
||||
let isLoading = false;
|
||||
|
||||
send();
|
||||
|
||||
async function send() {
|
||||
isLoading = true;
|
||||
|
||||
// init a custom client to avoid interfering with the admin state
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client.collection(payload.collectionId).confirmVerification(params?.token);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FullPage nobranding>
|
||||
{#if isLoading}
|
||||
<div class="txt-center">
|
||||
<div class="loader loader-lg">
|
||||
<em>Please wait...</em>
|
||||
</div>
|
||||
</div>
|
||||
{:else if success}
|
||||
<div class="alert alert-success">
|
||||
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
|
||||
<div class="content txt-bold">
|
||||
<p>Successfully verified email address.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-block" on:click={() => window.close()}>
|
||||
Close
|
||||
</button>
|
||||
{:else}
|
||||
<div class="alert alert-danger">
|
||||
<div class="icon"><i class="ri-error-warning-line" /></div>
|
||||
<div class="content txt-bold">
|
||||
<p>Invalid or expired verification token.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary btn-block" on:click={() => window.close()}>
|
||||
Close
|
||||
</button>
|
||||
{/if}
|
||||
</FullPage>
|
||||
@@ -5,6 +5,7 @@
|
||||
activeCollection,
|
||||
isCollectionsLoading,
|
||||
loadCollections,
|
||||
changeActiveCollectionById,
|
||||
} from "@/stores/collections";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { pageTitle, hideControls } from "@/stores/app";
|
||||
@@ -13,7 +14,7 @@
|
||||
import RefreshButton from "@/components/base/RefreshButton.svelte";
|
||||
import CollectionsSidebar from "@/components/collections/CollectionsSidebar.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
import CollectionDocsPanel from "@/components/collections/docs/CollectionDocsPanel.svelte";
|
||||
import CollectionDocsPanel from "@/components/collections/CollectionDocsPanel.svelte";
|
||||
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
|
||||
import RecordsList from "@/components/records/RecordsList.svelte";
|
||||
|
||||
@@ -29,7 +30,15 @@
|
||||
let sort = queryParams.get("sort") || "-created";
|
||||
let selectedCollectionId = queryParams.get("collectionId") || "";
|
||||
|
||||
$: viewableCollections = $collections.filter((c) => c.name != import.meta.env.PB_PROFILE_COLLECTION);
|
||||
$: reactiveParams = new URLSearchParams($querystring);
|
||||
|
||||
$: if (
|
||||
!$isCollectionsLoading &&
|
||||
reactiveParams.has("collectionId") &&
|
||||
reactiveParams.get("collectionId") != selectedCollectionId
|
||||
) {
|
||||
changeActiveCollectionById(reactiveParams.get("collectionId"));
|
||||
}
|
||||
|
||||
// reset filter and sort on collection change
|
||||
$: if ($activeCollection?.id && selectedCollectionId != $activeCollection.id) {
|
||||
@@ -62,7 +71,7 @@
|
||||
<h1>Loading collections...</h1>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
{:else if !viewableCollections.length}
|
||||
{:else if !$collections.length}
|
||||
<PageWrapper center>
|
||||
<div class="placeholder-section m-b-base">
|
||||
<div class="icon">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
class="txt-ellipsis"
|
||||
href={record[field.name]}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
use:tooltip={"Open in new tab"}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
@@ -45,9 +45,12 @@
|
||||
</div>
|
||||
{:else if field.type === "relation" || field.type === "user"}
|
||||
<div class="inline-flex">
|
||||
{#each CommonHelper.toArray(record[field.name]) as item, i (i + item)}
|
||||
{#each CommonHelper.toArray(record[field.name]).slice(0, 20) as item, i (i + item)}
|
||||
<IdLabel id={item} />
|
||||
{/each}
|
||||
{#if CommonHelper.toArray(record[field.name]).length > 20}
|
||||
...
|
||||
{/if}
|
||||
</div>
|
||||
{:else if field.type === "file"}
|
||||
<div class="inline-flex">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
$: hasPreview = CommonHelper.hasImageExtension(filename);
|
||||
|
||||
$: if (hasPreview) {
|
||||
originalUrl = ApiClient.records.getFileUrl(record, `${filename}`);
|
||||
originalUrl = ApiClient.getFileUrl(record, `${filename}`);
|
||||
}
|
||||
|
||||
$: thumbUrl = originalUrl ? originalUrl + "?thumb=100x100" : "";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import RecordSelectOption from "./RecordSelectOption.svelte";
|
||||
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
|
||||
|
||||
const uniqueId = "select_" + CommonHelper.randomString(5);
|
||||
|
||||
@@ -21,8 +22,12 @@
|
||||
let totalItems = 0;
|
||||
let isLoadingList = false;
|
||||
let isLoadingSelected = false;
|
||||
let isLoadingCollection = false;
|
||||
let collection = null;
|
||||
let upsertPanel;
|
||||
|
||||
$: if (collectionId) {
|
||||
loadCollection();
|
||||
loadSelected().then(() => {
|
||||
loadList(true);
|
||||
});
|
||||
@@ -32,6 +37,26 @@
|
||||
|
||||
$: canLoadMore = totalItems > list.length;
|
||||
|
||||
async function loadCollection() {
|
||||
if (!collectionId) {
|
||||
collection = null;
|
||||
isLoadingCollection = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingCollection = true;
|
||||
|
||||
try {
|
||||
collection = await ApiClient.collections.getOne(collectionId, {
|
||||
$cancelKey: "collection_" + uniqueId,
|
||||
});
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingCollection = false;
|
||||
}
|
||||
|
||||
async function loadSelected() {
|
||||
const selectedIds = CommonHelper.toArray(keyOfSelected);
|
||||
|
||||
@@ -41,21 +66,34 @@
|
||||
|
||||
isLoadingSelected = true;
|
||||
|
||||
try {
|
||||
let loadedItems = [];
|
||||
|
||||
// batch load all selected records to avoid parser stack overflow errors
|
||||
const filterIds = selectedIds.slice();
|
||||
const loadPromises = [];
|
||||
while (filterIds.length > 0) {
|
||||
const filters = [];
|
||||
for (const id of selectedIds) {
|
||||
for (const id of filterIds.splice(0, 50)) {
|
||||
filters.push(`id="${id}"`);
|
||||
}
|
||||
|
||||
const result = await ApiClient.records.getFullList(collectionId, 200, {
|
||||
filter: filters.join("||"),
|
||||
$cancelKey: uniqueId + "loadSelected",
|
||||
loadPromises.push(
|
||||
ApiClient.collection(collectionId).getFullList(200, {
|
||||
filter: filters.join("||"),
|
||||
$autoCancel: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(loadPromises).then((values) => {
|
||||
loadedItems = loadedItems.concat(...values);
|
||||
});
|
||||
|
||||
// preserve selected order
|
||||
selected = [];
|
||||
for (const id of selectedIds) {
|
||||
const item = CommonHelper.findByKey(result, "id", id);
|
||||
const item = CommonHelper.findByKey(loadedItems, "id", id);
|
||||
if (item) {
|
||||
selected.push(item);
|
||||
}
|
||||
@@ -80,7 +118,7 @@
|
||||
try {
|
||||
const page = reset ? 1 : currentPage + 1;
|
||||
|
||||
const result = await ApiClient.records.getList(collectionId, page, 200, {
|
||||
const result = await ApiClient.collection(collectionId).getList(page, 200, {
|
||||
sort: "-created",
|
||||
$cancelKey: uniqueId + "loadList",
|
||||
});
|
||||
@@ -108,6 +146,7 @@
|
||||
searchable={list.length > 5}
|
||||
selectionKey="id"
|
||||
labelComponent={optionComponent}
|
||||
disabled={isLoading}
|
||||
{optionComponent}
|
||||
{multiple}
|
||||
bind:keyOfSelected
|
||||
@@ -118,10 +157,19 @@
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
{#if !isLoadingCollection && collection}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-block btn-sm m-t-5"
|
||||
on:click={() => upsertPanel?.show()}
|
||||
>
|
||||
<span class="txt">New record</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if canLoadMore}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block btn-sm"
|
||||
class="btn btn-block btn-sm m-t-5"
|
||||
class:btn-loading={isLoadingList}
|
||||
class:btn-disabled={isLoadingList}
|
||||
on:click|stopPropagation={() => loadList()}
|
||||
@@ -131,3 +179,14 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
|
||||
<RecordUpsertPanel
|
||||
bind:this={upsertPanel}
|
||||
{collection}
|
||||
on:save={(e) => {
|
||||
if (e?.detail?.id) {
|
||||
keyOfSelected = CommonHelper.toArray(keyOfSelected).concat(e.detail.id);
|
||||
}
|
||||
loadList(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
|
||||
const props = [
|
||||
// prioritized common displayable props
|
||||
"name",
|
||||
"title",
|
||||
"name",
|
||||
"email",
|
||||
"username",
|
||||
"label",
|
||||
"key",
|
||||
"email",
|
||||
"heading",
|
||||
"content",
|
||||
"description",
|
||||
// fallback to the available props
|
||||
...Object.keys(model),
|
||||
];
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { Record } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import AuthFields from "@/components/records/fields/AuthFields.svelte";
|
||||
import TextField from "@/components/records/fields/TextField.svelte";
|
||||
import NumberField from "@/components/records/fields/NumberField.svelte";
|
||||
import BoolField from "@/components/records/fields/BoolField.svelte";
|
||||
@@ -19,10 +21,13 @@
|
||||
import JsonField from "@/components/records/fields/JsonField.svelte";
|
||||
import FileField from "@/components/records/fields/FileField.svelte";
|
||||
import RelationField from "@/components/records/fields/RelationField.svelte";
|
||||
import UserField from "@/components/records/fields/UserField.svelte";
|
||||
import ExternalAuthsList from "@/components/records/ExternalAuthsList.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const formId = "record_" + CommonHelper.randomString(5);
|
||||
const TAB_FORM = "form";
|
||||
const TAB_PROVIDERS = "providers";
|
||||
|
||||
export let collection;
|
||||
|
||||
@@ -34,6 +39,7 @@
|
||||
let uploadedFilesMap = {}; // eg.: {"field1":[File1, File2], ...}
|
||||
let deletedFileIndexesMap = {}; // eg.: {"field1":[0, 1], ...}
|
||||
let initialFormHash = "";
|
||||
let activeTab = TAB_FORM;
|
||||
|
||||
$: hasFileChanges =
|
||||
CommonHelper.hasNonEmptyProps(uploadedFilesMap) ||
|
||||
@@ -43,13 +49,13 @@
|
||||
|
||||
$: canSave = record.isNew || hasChanges;
|
||||
|
||||
$: isProfileCollection = collection?.name !== import.meta.env.PB_PROFILE_COLLECTION;
|
||||
|
||||
export function show(model) {
|
||||
load(model);
|
||||
|
||||
confirmClose = true;
|
||||
|
||||
activeTab = TAB_FORM;
|
||||
|
||||
return recordPanel?.show();
|
||||
}
|
||||
|
||||
@@ -60,7 +66,11 @@
|
||||
async function load(model) {
|
||||
setErrors({}); // reset errors
|
||||
original = model || {};
|
||||
record = model?.clone ? model.clone() : new Record();
|
||||
if (model?.clone) {
|
||||
record = model.clone();
|
||||
} else {
|
||||
record = new Record();
|
||||
}
|
||||
uploadedFilesMap = {};
|
||||
deletedFileIndexesMap = {};
|
||||
await tick(); // wait to populate the fields to get the normalized values
|
||||
@@ -72,7 +82,7 @@
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (isSaving || !canSave) {
|
||||
if (isSaving || !canSave || !collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,13 +92,13 @@
|
||||
|
||||
let request;
|
||||
if (record.isNew) {
|
||||
request = ApiClient.records.create(collection?.id, data);
|
||||
request = ApiClient.collection(collection.id).create(data);
|
||||
} else {
|
||||
request = ApiClient.records.update(collection?.id, record.id, data);
|
||||
request = ApiClient.collection(collection.id).update(record.id, data);
|
||||
}
|
||||
|
||||
request
|
||||
.then(async (result) => {
|
||||
.then((result) => {
|
||||
addSuccessToast(
|
||||
record.isNew ? "Successfully created record." : "Successfully updated record."
|
||||
);
|
||||
@@ -110,7 +120,8 @@
|
||||
}
|
||||
|
||||
confirm(`Do you really want to delete the selected record?`, () => {
|
||||
return ApiClient.records.delete(original["@collectionId"], original.id)
|
||||
return ApiClient.collection(original.collectionId)
|
||||
.delete(original.id)
|
||||
.then(() => {
|
||||
hide();
|
||||
addSuccessToast("Successfully deleted record.");
|
||||
@@ -126,15 +137,24 @@
|
||||
const data = record?.export() || {};
|
||||
const formData = new FormData();
|
||||
|
||||
const schemaMap = {};
|
||||
const exportableFields = {};
|
||||
for (const field of collection?.schema || []) {
|
||||
schemaMap[field.name] = field;
|
||||
exportableFields[field.name] = true;
|
||||
}
|
||||
|
||||
if (collection?.isAuth) {
|
||||
exportableFields["username"] = true;
|
||||
exportableFields["email"] = true;
|
||||
exportableFields["emailVisibility"] = true;
|
||||
exportableFields["password"] = true;
|
||||
exportableFields["passwordConfirm"] = true;
|
||||
exportableFields["verified"] = true;
|
||||
}
|
||||
|
||||
// export base fields
|
||||
for (const key in data) {
|
||||
// skip non-schema fields
|
||||
if (!schemaMap[key]) {
|
||||
if (!exportableFields[key]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -164,11 +184,45 @@
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function sendVerificationEmail() {
|
||||
if (!collection?.id || !original?.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirm(`Do you really want to sent verification email to ${original.email}?`, () => {
|
||||
return ApiClient.collection(collection.id)
|
||||
.requestVerification(original.email)
|
||||
.then(() => {
|
||||
addSuccessToast(`Successfully sent verification email to ${original.email}.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendPasswordResetEmail() {
|
||||
if (!collection?.id || !original?.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirm(`Do you really want to sent password reset email to ${original.email}?`, () => {
|
||||
return ApiClient.collection(collection.id)
|
||||
.requestPasswordReset(original.email)
|
||||
.then(() => {
|
||||
addSuccessToast(`Successfully sent password reset email to ${original.email}.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={recordPanel}
|
||||
class="overlay-panel-lg record-panel"
|
||||
class="overlay-panel-lg record-panel {collection?.isAuth && !record.isNew ? 'colored-header' : ''}"
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
@@ -177,6 +231,7 @@
|
||||
});
|
||||
return false;
|
||||
}
|
||||
setErrors({});
|
||||
return true;
|
||||
}}
|
||||
on:hide
|
||||
@@ -185,73 +240,143 @@
|
||||
<svelte:fragment slot="header">
|
||||
<h4>
|
||||
{record.isNew ? "New" : "Edit"}
|
||||
{collection.name} record
|
||||
<strong>{collection?.name}</strong> record
|
||||
</h4>
|
||||
|
||||
{#if !record.isNew && isProfileCollection}
|
||||
{#if !record.isNew}
|
||||
<div class="flex-fill" />
|
||||
<button type="button" class="btn btn-sm btn-circle btn-secondary">
|
||||
<div class="content">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right m-t-5">
|
||||
<div tabindex="0" class="dropdown-item closable" on:click={() => deleteConfirm()}>
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
||||
{#if collection.isAuth && !original.verified && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendVerificationEmail()}
|
||||
>
|
||||
<i class="ri-mail-check-line" />
|
||||
<span class="txt">Send verification email</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if collection.isAuth && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
on:click={() => sendPasswordResetEmail()}
|
||||
>
|
||||
<i class="ri-mail-lock-line" />
|
||||
<span class="txt">Send password reset email</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger closable"
|
||||
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
</Toggler>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if collection.isAuth && !record.isNew}
|
||||
<div class="tabs-header stretched">
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_FORM}
|
||||
on:click={() => (activeTab = TAB_FORM)}
|
||||
>
|
||||
Account
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_PROVIDERS}
|
||||
on:click={() => (activeTab = TAB_PROVIDERS)}
|
||||
>
|
||||
Authorized providers
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<form id={formId} class="block" on:submit|preventDefault={save}>
|
||||
{#if !record.isNew}
|
||||
<Field class="form-field disabled" name="id" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">id</span>
|
||||
<span class="flex-fill" />
|
||||
</label>
|
||||
<input type="text" id={uniqueId} value={record.id} disabled />
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
{#each collection?.schema || [] as field (field.name)}
|
||||
{#if field.type === "text"}
|
||||
<TextField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "number"}
|
||||
<NumberField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "bool"}
|
||||
<BoolField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "email"}
|
||||
<EmailField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "date"}
|
||||
<DateField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "select"}
|
||||
<SelectField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "json"}
|
||||
<JsonField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "file"}
|
||||
<FileField
|
||||
{field}
|
||||
{record}
|
||||
bind:value={record[field.name]}
|
||||
bind:uploadedFiles={uploadedFilesMap[field.name]}
|
||||
bind:deletedFileIndexes={deletedFileIndexesMap[field.name]}
|
||||
/>
|
||||
{:else if field.type === "relation"}
|
||||
<RelationField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "user"}
|
||||
<UserField {field} bind:value={record[field.name]} />
|
||||
<div class="tabs-content">
|
||||
<form
|
||||
id={formId}
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_FORM}
|
||||
on:submit|preventDefault={save}
|
||||
>
|
||||
{#if !record.isNew}
|
||||
<Field class="form-field disabled" name="id" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">id</span>
|
||||
<span class="flex-fill" />
|
||||
</label>
|
||||
<div class="form-field-addon">
|
||||
<i
|
||||
class="ri-calendar-event-line txt-disabled"
|
||||
use:tooltip={{
|
||||
text: `Created: ${record.created}\nUpdated: ${record.updated}`,
|
||||
position: "left",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input type="text" id={uniqueId} value={record.id} readonly />
|
||||
</Field>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="block txt-center txt-disabled">
|
||||
<h5>No custom fields to be set</h5>
|
||||
|
||||
{#if collection?.isAuth}
|
||||
<AuthFields bind:record {collection} />
|
||||
<hr />
|
||||
{/if}
|
||||
|
||||
{#each collection?.schema || [] as field (field.name)}
|
||||
{#if field.type === "text"}
|
||||
<TextField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "number"}
|
||||
<NumberField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "bool"}
|
||||
<BoolField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "email"}
|
||||
<EmailField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "url"}
|
||||
<UrlField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "date"}
|
||||
<DateField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "select"}
|
||||
<SelectField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "json"}
|
||||
<JsonField {field} bind:value={record[field.name]} />
|
||||
{:else if field.type === "file"}
|
||||
<FileField
|
||||
{field}
|
||||
{record}
|
||||
bind:value={record[field.name]}
|
||||
bind:uploadedFiles={uploadedFilesMap[field.name]}
|
||||
bind:deletedFileIndexes={deletedFileIndexesMap[field.name]}
|
||||
/>
|
||||
{:else if field.type === "relation"}
|
||||
<RelationField {field} bind:value={record[field.name]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="block txt-center txt-disabled">
|
||||
<h5>No custom fields to be set</h5>
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
{#if collection.isAuth && !record.isNew}
|
||||
<div class="tab-item" class:active={activeTab === TAB_PROVIDERS}>
|
||||
<ExternalAuthsList {record} />
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
import { fly } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
|
||||
import RecordFieldCell from "@/components/records/RecordFieldCell.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -22,8 +26,12 @@
|
||||
let bulkSelected = {};
|
||||
let isLoading = true;
|
||||
let isDeleting = false;
|
||||
let yieldedRecordsId = 0;
|
||||
let hiddenColumns = [];
|
||||
let columnsTrigger;
|
||||
|
||||
$: if (collection?.id) {
|
||||
loadStoredHiddenColumns();
|
||||
clearList();
|
||||
}
|
||||
|
||||
@@ -35,43 +43,84 @@
|
||||
|
||||
$: fields = collection?.schema || [];
|
||||
|
||||
$: visibleFields = fields.filter((field) => !hiddenColumns.includes(field.id));
|
||||
|
||||
$: totalBulkSelected = Object.keys(bulkSelected).length;
|
||||
|
||||
$: areAllRecordsSelected = records.length && totalBulkSelected === records.length;
|
||||
|
||||
$: if (hiddenColumns !== -1) {
|
||||
updateStoredHiddenColumns();
|
||||
}
|
||||
|
||||
function updateStoredHiddenColumns() {
|
||||
if (!collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(collection?.id + "@hiddenCollumns", JSON.stringify(hiddenColumns));
|
||||
}
|
||||
|
||||
function loadStoredHiddenColumns() {
|
||||
hiddenColumns = [];
|
||||
|
||||
if (!collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const encoded = localStorage.getItem(collection.id + "@hiddenCollumns");
|
||||
if (encoded) hiddenColumns = JSON.parse(encoded) || [];
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
export async function reloadLoadedPages() {
|
||||
const loadedPages = currentPage;
|
||||
|
||||
for (let i = 1; i <= loadedPages; i++) {
|
||||
if (i === 1 || canLoadMore) {
|
||||
await load(i);
|
||||
await load(i, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function load(page = 1) {
|
||||
export async function load(page = 1, breakTasks = true) {
|
||||
if (!collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.records
|
||||
.getList(collection.id, page, 50, {
|
||||
return ApiClient.collection(collection.id)
|
||||
.getList(page, 30, {
|
||||
sort: sort,
|
||||
filter: filter,
|
||||
})
|
||||
.then((result) => {
|
||||
.then(async (result) => {
|
||||
if (page <= 1) {
|
||||
clearList();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
records = records.concat(result.items);
|
||||
currentPage = result.page;
|
||||
totalRecords = result.totalItems;
|
||||
dispatch("load", records.concat(result.items));
|
||||
|
||||
dispatch("load", records);
|
||||
// optimize the records listing by rendering the rows in task batches
|
||||
if (breakTasks) {
|
||||
const currentYieldId = ++yieldedRecordsId;
|
||||
while (result.items.length) {
|
||||
if (yieldedRecordsId != currentYieldId) {
|
||||
break; // new yeild has been started
|
||||
}
|
||||
|
||||
records = records.concat(result.items.splice(0, 15));
|
||||
|
||||
await CommonHelper.yieldToMain();
|
||||
}
|
||||
} else {
|
||||
records = records.concat(result.items);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.isAbort) {
|
||||
@@ -128,13 +177,13 @@
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
if (isDeleting || !totalBulkSelected) {
|
||||
if (isDeleting || !totalBulkSelected || !collection?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (const recordId of Object.keys(bulkSelected)) {
|
||||
promises.push(ApiClient.records.delete(collection?.id, recordId));
|
||||
promises.push(ApiClient.collection(collection.id).delete(recordId));
|
||||
}
|
||||
|
||||
isDeleting = true;
|
||||
@@ -158,7 +207,31 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<HorizontalScroller class="table-wrapper">
|
||||
<svelte:fragment slot="before">
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap columns-dropdown" trigger={columnsTrigger}>
|
||||
<div class="txt-hint txt-sm p-5 m-b-5">Toggle Columns</div>
|
||||
{#each fields as field (field.id + field.name)}
|
||||
<Field class="form-field form-field-sm form-field-toggle m-0 p-5" let:uniqueId>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={uniqueId}
|
||||
checked={!hiddenColumns.includes(field.id)}
|
||||
on:change={(e) => {
|
||||
if (e.target.checked) {
|
||||
CommonHelper.removeByValue(hiddenColumns, field.id);
|
||||
} else {
|
||||
CommonHelper.pushUnique(hiddenColumns, field.id);
|
||||
}
|
||||
hiddenColumns = hiddenColumns;
|
||||
}}
|
||||
/>
|
||||
<label for={uniqueId}>{field.name}</label>
|
||||
</Field>
|
||||
{/each}
|
||||
</Toggler>
|
||||
</svelte:fragment>
|
||||
|
||||
<table class="table" class:table-loading={isLoading}>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -178,13 +251,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
</th>
|
||||
|
||||
<SortHeader class="col-type-text col-field-id" name="id" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">id</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
{#each fields as field (field.name)}
|
||||
|
||||
{#if collection.isAuth}
|
||||
<SortHeader class="col-type-text col-field-id" name="username" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("user")} />
|
||||
<span class="txt">username</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
<SortHeader class="col-type-email col-field-email" name="email" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("email")} />
|
||||
<span class="txt">email</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
{/if}
|
||||
|
||||
{#each visibleFields as field (field.name)}
|
||||
<SortHeader
|
||||
class="col-type-{field.type} col-field-{field.name}"
|
||||
name={field.name}
|
||||
@@ -196,19 +286,26 @@
|
||||
</div>
|
||||
</SortHeader>
|
||||
{/each}
|
||||
|
||||
<SortHeader class="col-type-date col-field-created" name="created" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("date")} />
|
||||
<span class="txt">created</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader class="col-type-date col-field-updated" name="updated" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("date")} />
|
||||
<span class="txt">updated</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
<th class="col-type-action min-width" />
|
||||
|
||||
<th class="col-type-action min-width">
|
||||
<button bind:this={columnsTrigger} type="button" class="btn btn-sm btn-secondary p-0">
|
||||
<i class="ri-more-line" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -237,10 +334,47 @@
|
||||
</td>
|
||||
|
||||
<td class="col-type-text col-field-id">
|
||||
<IdLabel id={record.id} />
|
||||
<div class="flex flex-gap-5">
|
||||
<IdLabel id={record.id} />
|
||||
|
||||
{#if collection.isAuth}
|
||||
{#if record.verified}
|
||||
<i
|
||||
class="ri-checkbox-circle-fill txt-sm txt-success"
|
||||
use:tooltip={"Verified"}
|
||||
/>
|
||||
{:else}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-sm txt-hint"
|
||||
use:tooltip={"Unverified"}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{#each fields as field (field.name)}
|
||||
{#if collection.isAuth}
|
||||
<td class="col-type-text col-field-username">
|
||||
{#if CommonHelper.isEmpty(record.username)}
|
||||
<span class="txt-hint">N/A</span>
|
||||
{:else}
|
||||
<span class="txt txt-ellipsis" title={record.username}>
|
||||
{record.username}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="col-type-text col-field-email">
|
||||
{#if CommonHelper.isEmpty(record.email)}
|
||||
<span class="txt-hint">N/A</span>
|
||||
{:else}
|
||||
<span class="txt txt-ellipsis" title={record.email}>
|
||||
{record.email}
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
{#each visibleFields as field (field.name)}
|
||||
<RecordFieldCell {record} {field} />
|
||||
{/each}
|
||||
|
||||
@@ -282,7 +416,7 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
|
||||
{#if records.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {records.length} of {totalRecords}</small>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import { Collection, Record } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { removeError } from "@/stores/errors";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
export let record = new Record();
|
||||
|
||||
let originalUsername = record.username || null;
|
||||
|
||||
let changePasswordToggle = false;
|
||||
|
||||
$: if (!record.username && record.username !== null) {
|
||||
record.username = null;
|
||||
}
|
||||
|
||||
$: if (!changePasswordToggle) {
|
||||
record.password = null;
|
||||
record.passwordConfirm = null;
|
||||
removeError("password");
|
||||
removeError("passwordConfirm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid m-b-base">
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field {!record.isNew ? 'required' : ''}" name="username" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("user")} />
|
||||
<span class="txt">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
requried={!record.isNew}
|
||||
placeholder={record.isNew ? "Leave empty to auto generate..." : originalUsername}
|
||||
id={uniqueId}
|
||||
bind:value={record.username}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<Field
|
||||
class="form-field {collection.options?.requireEmail ? 'required' : ''}"
|
||||
name="email"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("email")} />
|
||||
<span class="txt">Email</span>
|
||||
</label>
|
||||
<div class="form-field-addon email-visibility-addon">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary {record.emailVisibility ? 'btn-success' : 'btn-hint'}"
|
||||
use:tooltip={{
|
||||
text: "Make email public or private",
|
||||
position: "top-right",
|
||||
}}
|
||||
on:click={() => (record.emailVisibility = !record.emailVisibility)}
|
||||
>
|
||||
<span class="txt">Public: {record.emailVisibility ? "On" : "Off"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="email"
|
||||
autofocus={record.isNew}
|
||||
autocomplete="off"
|
||||
id={uniqueId}
|
||||
required={collection.options?.requireEmail}
|
||||
bind:value={record.email}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
{#if !record.isNew}
|
||||
<Field class="form-field form-field-toggle" name="verified" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
|
||||
<label for={uniqueId}>Change password</label>
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
{#if record.isNew || changePasswordToggle}
|
||||
<div class="block" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="grid" class:p-t-xs={changePasswordToggle}>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="password" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class="ri-lock-line" />
|
||||
<span class="txt">Password</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
id={uniqueId}
|
||||
required
|
||||
bind:value={record.password}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class="ri-lock-line" />
|
||||
<span class="txt">Password confirm</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
id={uniqueId}
|
||||
required
|
||||
bind:value={record.passwordConfirm}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<Field class="form-field form-field-toggle" name="verified" let:uniqueId>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={uniqueId}
|
||||
bind:checked={record.verified}
|
||||
on:change|preventDefault={(e) => {
|
||||
if (record.isNew) {
|
||||
return; // no confirmation required
|
||||
}
|
||||
confirm(
|
||||
`Do you really want to manually change the verified account state?`,
|
||||
() => {},
|
||||
() => {
|
||||
record.verified = !e.target.checked;
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label for={uniqueId}>Verified</label>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.email-visibility-addon ~ input {
|
||||
padding-right: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,13 @@
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
// strip ms and zone for backwards compatibility with the older format
|
||||
// and because flatpickr currently doesn't have integrated
|
||||
// zones support and requires manual parsing and formatting
|
||||
$: if (value && value.length > 19) {
|
||||
value = value.substring(0, 19);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
|
||||
@@ -83,12 +83,12 @@
|
||||
<RecordFilePreview {record} {filename} />
|
||||
</figure>
|
||||
<a
|
||||
href={ApiClient.records.getFileUrl(record, filename)}
|
||||
href={ApiClient.getFileUrl(record, filename)}
|
||||
class="filename link-hint"
|
||||
class:txt-strikethrough={deletedFileIndexes.includes(i)}
|
||||
use:tooltip={{ position: "right", text: "Download" }}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{filename}
|
||||
</a>
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
$: isMultiple = field.options?.maxSelect != 1;
|
||||
|
||||
$: if (isMultiple && Array.isArray(value) && value.length > field.options.maxSelect) {
|
||||
$: if (
|
||||
isMultiple &&
|
||||
Array.isArray(value) &&
|
||||
field.options?.maxSelect &&
|
||||
value.length > field.options.maxSelect
|
||||
) {
|
||||
value = value.slice(field.options.maxSelect - 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<script>
|
||||
import { SchemaField } from "pocketbase";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import UserSelect from "@/components/users/UserSelect.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let field = new SchemaField();
|
||||
export let value = undefined;
|
||||
|
||||
// to prevent accidental changes, disable editing system user field values from the UI
|
||||
$: isDisabled = !CommonHelper.isEmpty(value) && field.system;
|
||||
|
||||
$: isMultiple = field.options?.maxSelect > 1;
|
||||
|
||||
$: if (isMultiple && Array.isArray(value) && value.length > field.options.maxSelect) {
|
||||
value = value.slice(field.options.maxSelect - 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field
|
||||
class="form-field {field.required ? 'required' : ''} {isDisabled ? 'disabled' : ''}"
|
||||
name={field.name}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<UserSelect toggle id={uniqueId} multiple={isMultiple} disabled={isDisabled} bind:keyOfSelected={value} />
|
||||
{#if field.options?.maxSelect > 1}
|
||||
<div class="help-block">Select up to {field.options.maxSelect} users.</div>
|
||||
{/if}
|
||||
</Field>
|
||||
Reference in New Issue
Block a user