initial v0.8 pre-release
This commit is contained in:
+2
-12
@@ -90,16 +90,6 @@
|
||||
>
|
||||
<i class="ri-database-2-line" />
|
||||
</a>
|
||||
<a
|
||||
href="/users"
|
||||
class="menu-item"
|
||||
aria-label="Users"
|
||||
use:link
|
||||
use:active={{ path: "/users/?.*", className: "current-route" }}
|
||||
use:tooltip={{ text: "Users", position: "right" }}
|
||||
>
|
||||
<i class="ri-group-line" />
|
||||
</a>
|
||||
<a
|
||||
href="/logs"
|
||||
class="menu-item"
|
||||
@@ -133,10 +123,10 @@
|
||||
<span class="txt">Manage admins</span>
|
||||
</a>
|
||||
<hr />
|
||||
<div tabindex="0" class="dropdown-item closable" on:click={logout}>
|
||||
<button type="button" class="dropdown-item closable" on:click={logout}>
|
||||
<i class="ri-logout-circle-line" />
|
||||
<span class="txt">Logout</span>
|
||||
</div>
|
||||
</button>
|
||||
</Toggler>
|
||||
</figure>
|
||||
</aside>
|
||||
|
||||
@@ -141,7 +141,7 @@ function showTooltip(node, data) {
|
||||
getTooltip().classList.add("active");
|
||||
|
||||
refreshTooltip(node, data);
|
||||
}, (!isNaN(data.delay) ? data.delay : 200));
|
||||
}, (!isNaN(data.delay) ? data.delay : 0));
|
||||
}
|
||||
|
||||
export default function tooltip(node, tooltipData) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Admin } 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";
|
||||
@@ -43,7 +44,6 @@
|
||||
}
|
||||
|
||||
function load(model) {
|
||||
setErrors({}); // reset errors
|
||||
admin = model?.clone ? model.clone() : new Admin();
|
||||
reset(); // reset form
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
avatar = admin?.avatar || 0;
|
||||
password = "";
|
||||
passwordConfirm = "";
|
||||
setErrors({}); // reset errors
|
||||
}
|
||||
|
||||
function save() {
|
||||
@@ -146,6 +147,15 @@
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">ID</span>
|
||||
</label>
|
||||
<div class="form-field-addon">
|
||||
<i
|
||||
class="ri-calendar-event-line txt-disabled"
|
||||
use:tooltip={{
|
||||
text: `Created: ${admin.created}\nUpdated: ${admin.updated}`,
|
||||
position: "left",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input type="text" id={uniqueId} value={admin.id} disabled />
|
||||
</Field>
|
||||
{/if}
|
||||
@@ -154,22 +164,16 @@
|
||||
<p class="section-title">Avatar</p>
|
||||
<div class="flex flex-gap-xs flex-wrap">
|
||||
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as index}
|
||||
<figure
|
||||
tabindex="0"
|
||||
<button
|
||||
type="button"
|
||||
class="link-fade thumb thumb-circle {index == avatar ? 'thumb-active' : 'thumb-sm'}"
|
||||
on:click={() => (avatar = index)}
|
||||
on:keydown={(e) => {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
e.preventDefault();
|
||||
avatar = index;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="{import.meta.env.BASE_URL}images/avatars/avatar{index}.svg"
|
||||
alt="Avatar {index}"
|
||||
/>
|
||||
</figure>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +238,7 @@
|
||||
<span />
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-upside dropdown-left dropdown-nowrap">
|
||||
<button type="button" class="dropdown-item" on:click={() => deleteConfirm()}>
|
||||
<button type="button" class="dropdown-item txt-danger" on:click={() => deleteConfirm()}>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import { addErrorToast } from "@/stores/toasts";
|
||||
import { addErrorToast, removeAllToasts } from "@/stores/toasts";
|
||||
|
||||
const queryParams = new URLSearchParams($querystring);
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.admins.authViaEmail(email, password)
|
||||
return ApiClient.admins
|
||||
.authWithPassword(email, password)
|
||||
.then(() => {
|
||||
removeAllToasts();
|
||||
replace("/");
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -37,7 +39,7 @@
|
||||
<h4>Admin sign in</h4>
|
||||
</div>
|
||||
|
||||
<Field class="form-field required" name="email" let:uniqueId>
|
||||
<Field class="form-field required" name="identity" let:uniqueId>
|
||||
<label for={uniqueId}>Email</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="email" id={uniqueId} bind:value={email} required autofocus />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import AdminUpsertPanel from "@/components/admins/AdminUpsertPanel.svelte";
|
||||
|
||||
@@ -86,7 +87,7 @@
|
||||
on:submit={(e) => (filter = e.detail)}
|
||||
/>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<HorizontalScroller class="table-wrapper">
|
||||
<table class="table" class:table-loading={isLoading}>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -145,6 +146,7 @@
|
||||
/>
|
||||
</figure>
|
||||
</td>
|
||||
|
||||
<td class="col-type-text col-field-id">
|
||||
<IdLabel id={admin.id} />
|
||||
{#if admin.id === $loggedAdmin.id}
|
||||
@@ -161,9 +163,11 @@
|
||||
<td class="col-type-date col-field-created">
|
||||
<FormattedDate date={admin.created} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-date col-field-updated">
|
||||
<FormattedDate date={admin.updated} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-action min-width">
|
||||
<i class="ri-arrow-right-line" />
|
||||
</td>
|
||||
@@ -194,7 +198,7 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
|
||||
{#if admins.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {admins.length} of {admins.length}</small>
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
export let draggable = false;
|
||||
export let active = false;
|
||||
export let interactive = true;
|
||||
export let single = false; // ensures that only one accordion is expanded in its given parent container
|
||||
|
||||
let isDragOver = false;
|
||||
|
||||
$: if (active) {
|
||||
clearTimeout(expandTimeoutId);
|
||||
expandTimeoutId = setTimeout(() => {
|
||||
@@ -49,10 +52,10 @@
|
||||
}
|
||||
|
||||
export function collapseSiblings() {
|
||||
if (single && accordionElem.parentElement) {
|
||||
const handlers = accordionElem.parentElement.querySelectorAll(
|
||||
".accordion.active .accordion-header.interactive"
|
||||
);
|
||||
if (single && accordionElem.closest(".accordions")) {
|
||||
const handlers = accordionElem
|
||||
.closest(".accordions")
|
||||
.querySelectorAll(".accordion.active .accordion-header.interactive");
|
||||
for (const handler of handlers) {
|
||||
handler.click(); // @todo consider using store or other more reliable approach
|
||||
}
|
||||
@@ -78,14 +81,36 @@
|
||||
<div
|
||||
bind:this={accordionElem}
|
||||
tabindex={interactive ? 0 : -1}
|
||||
class="accordion {classes}"
|
||||
class="accordion {isDragOver ? 'drag-over' : ''} {classes}"
|
||||
class:active
|
||||
on:keydown|self={keyToggle}
|
||||
>
|
||||
<header
|
||||
class="accordion-header"
|
||||
{draggable}
|
||||
class:interactive
|
||||
on:click|preventDefault={() => interactive && toggle()}
|
||||
on:drop|preventDefault={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = false;
|
||||
collapseSiblings();
|
||||
dispatch("drop", e);
|
||||
}
|
||||
}}
|
||||
on:dragstart={(e) => draggable && dispatch("dragstart", e)}
|
||||
on:dragenter={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = true;
|
||||
dispatch("dragenter", e);
|
||||
}
|
||||
}}
|
||||
on:dragleave={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = false;
|
||||
dispatch("dragleave", e);
|
||||
}
|
||||
}}
|
||||
on:dragover|preventDefault
|
||||
>
|
||||
<slot name="header" {active} />
|
||||
</header>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
// @todo consider replacing with readonly CodeEditor
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.js";
|
||||
import "prismjs/components/prism-dart.js";
|
||||
@@ -40,7 +39,7 @@
|
||||
code {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--xsSpacing);
|
||||
padding: 10px 15px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
let confirmationPopup;
|
||||
let isConfirmationBusy = false;
|
||||
let confirmed = false;
|
||||
|
||||
$: if ($confirmation?.text) {
|
||||
confirmed = false;
|
||||
confirmationPopup?.show();
|
||||
}
|
||||
</script>
|
||||
@@ -19,10 +21,11 @@
|
||||
btnClose={false}
|
||||
popup
|
||||
on:hide={async () => {
|
||||
if ($confirmation?.noCallback) {
|
||||
if (!confirmed && $confirmation?.noCallback) {
|
||||
$confirmation.noCallback();
|
||||
}
|
||||
await tick();
|
||||
confirmed = false;
|
||||
resetConfirmation();
|
||||
}}
|
||||
>
|
||||
@@ -36,9 +39,7 @@
|
||||
class="btn btn-secondary btn-expanded-sm"
|
||||
disabled={isConfirmationBusy}
|
||||
on:click={() => {
|
||||
if ($confirmation?.noCallback) {
|
||||
$confirmation.noCallback();
|
||||
}
|
||||
confirmed = false;
|
||||
confirmationPopup?.hide();
|
||||
}}
|
||||
>
|
||||
@@ -55,6 +56,7 @@
|
||||
await Promise.resolve($confirmation.yesCallback());
|
||||
isConfirmationBusy = false;
|
||||
}
|
||||
confirmed = true;
|
||||
confirmationPopup?.hide();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -185,17 +185,23 @@
|
||||
prefix + "updated",
|
||||
];
|
||||
|
||||
if (collection.isAuth) {
|
||||
result.push(prefix + "username");
|
||||
result.push(prefix + "email");
|
||||
result.push(prefix + "emailVisibility");
|
||||
result.push(prefix + "verified");
|
||||
}
|
||||
|
||||
for (const field of collection.schema) {
|
||||
const key = prefix + field.name;
|
||||
|
||||
result.push(key);
|
||||
|
||||
if (field.type === "relation" && field.options.collectionId) {
|
||||
const subKeys = getCollectionFieldKeys(field.options.collectionId, key + ".", level + 1);
|
||||
if (subKeys.length) {
|
||||
result = result.concat(subKeys);
|
||||
} else {
|
||||
result.push(key);
|
||||
}
|
||||
} else {
|
||||
result.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,28 +223,27 @@
|
||||
result.push("@request.method");
|
||||
result.push("@request.query.");
|
||||
result.push("@request.data.");
|
||||
result.push("@request.user.id");
|
||||
result.push("@request.user.email");
|
||||
result.push("@request.user.verified");
|
||||
result.push("@request.user.created");
|
||||
result.push("@request.user.updated");
|
||||
result.push("@request.auth.");
|
||||
result.push("@request.auth.id");
|
||||
result.push("@request.auth.collectionId");
|
||||
result.push("@request.auth.collectionName");
|
||||
result.push("@request.auth.username");
|
||||
result.push("@request.auth.email");
|
||||
result.push("@request.auth.emailVisibility");
|
||||
result.push("@request.auth.verified");
|
||||
result.push("@request.auth.created");
|
||||
result.push("@request.auth.updated");
|
||||
}
|
||||
|
||||
// add @collections and @request.user.profile keys
|
||||
// add @collections.* keys
|
||||
if (includeRequestKeys || includeIndirectCollectionsKeys) {
|
||||
for (const collection of mergedCollections) {
|
||||
let prefix = "";
|
||||
if (collection.name === import.meta.env.PB_PROFILE_COLLECTION) {
|
||||
if (!includeRequestKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@request.user.profile.";
|
||||
} else {
|
||||
if (!includeIndirectCollectionsKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@collection." + collection.name + ".";
|
||||
|
||||
if (!includeIndirectCollectionsKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@collection." + collection.name + ".";
|
||||
|
||||
const keys = getCollectionFieldKeys(collection.name, prefix);
|
||||
for (const key of keys) {
|
||||
@@ -258,8 +263,8 @@
|
||||
|
||||
// Returns object with all the completions matching the context.
|
||||
function completions(context) {
|
||||
let word = context.matchBefore(/[\@\w\.]*/);
|
||||
if (word.from == word.to && !context.explicit) {
|
||||
let word = context.matchBefore(/[\'\"\@\w\.]*/);
|
||||
if (word && word.from == word.to && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -269,18 +274,8 @@
|
||||
options.push({ label: "@collection.*", apply: "@collection." });
|
||||
}
|
||||
|
||||
const skipFields = [
|
||||
"@request.user.profile.userId",
|
||||
"@request.user.profile.created",
|
||||
"@request.user.profile.updated",
|
||||
];
|
||||
|
||||
const keys = getAllKeys(!disableRequestKeys, !disableRequestKeys && word.text.startsWith("@c"));
|
||||
for (const key of keys) {
|
||||
if (skipFields.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: key.endsWith(".") ? key + "*" : key,
|
||||
apply: key,
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<script>
|
||||
export let date = "";
|
||||
|
||||
// note: manual trim the ms without converting to DateTime
|
||||
// to help improving the rendering performance in large data sets
|
||||
$: shortDate = date.length > 19 ? date.substring(0, 19) : date;
|
||||
$: dateOnly = date ? date.substring(0, 10) : null;
|
||||
|
||||
$: timeOnly = date ? date.substring(10, 19) : null;
|
||||
</script>
|
||||
|
||||
{#if date}
|
||||
<span class="txt">{shortDate} UTC</span>
|
||||
<div class="datetime">
|
||||
<div class="date">{dateOnly}</div>
|
||||
<div class="time">{timeOnly} UTC</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="txt txt-hint">N/A</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.datetime {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: var(--smLineHeight);
|
||||
}
|
||||
.time {
|
||||
font-size: var(--smFontSize);
|
||||
color: var(--txtHintColor);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
let wrapper = null;
|
||||
let scrollClasses = "";
|
||||
let scrollTimeoutId = null;
|
||||
let observer;
|
||||
|
||||
export function refresh() {
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(scrollTimeoutId);
|
||||
|
||||
scrollTimeoutId = setTimeout(() => {
|
||||
const offsetWidth = wrapper.offsetWidth;
|
||||
const scrollWidth = wrapper.scrollWidth;
|
||||
|
||||
if (scrollWidth - offsetWidth) {
|
||||
scrollClasses = "scrollable";
|
||||
if (wrapper.scrollLeft === 0) {
|
||||
scrollClasses += " scroll-start";
|
||||
} else if (wrapper.scrollLeft + offsetWidth == scrollWidth) {
|
||||
scrollClasses += " scroll-end";
|
||||
}
|
||||
} else {
|
||||
scrollClasses = "";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refresh();
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
observer.observe(wrapper, {
|
||||
attributeFilter: ["width"],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
clearTimeout(scrollTimeoutId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={refresh} />
|
||||
|
||||
<div class="horizontal-scroller-wrapper">
|
||||
<slot name="before" />
|
||||
|
||||
<div bind:this={wrapper} class="horizontal-scroller {classes} {scrollClasses}" on:scroll={refresh}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<slot name="after" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.horizontal-scroller {
|
||||
width: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.horizontal-scroller-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
:global(.horizontal-scroller-wrapper .columns-dropdown) {
|
||||
top: 40px;
|
||||
z-index: 100;
|
||||
max-height: 340px;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
passwordConfirm,
|
||||
});
|
||||
|
||||
await ApiClient.admins.authViaEmail(email, password);
|
||||
await ApiClient.admins.authWithPassword(email, password);
|
||||
|
||||
dispatch("submit");
|
||||
} catch (err) {
|
||||
|
||||
@@ -86,10 +86,12 @@
|
||||
oldFocusedElem = document.activeElement;
|
||||
wrapper?.focus();
|
||||
dispatch("show");
|
||||
document.body.classList.add("overlay-active");
|
||||
} else {
|
||||
clearTimeout(contentScrollThrottle);
|
||||
oldFocusedElem?.focus();
|
||||
dispatch("hide");
|
||||
document.body.classList.remove("overlay-active");
|
||||
}
|
||||
|
||||
await tick();
|
||||
@@ -179,13 +181,17 @@
|
||||
// ensures that no artifacts remains
|
||||
// (currently there is a bug with svelte transition)
|
||||
wrapper?.classList?.add("hidden");
|
||||
|
||||
setTimeout(() => {
|
||||
wrapper?.remove();
|
||||
}, 0);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={handleResize} on:keydown={handleEscPress} />
|
||||
|
||||
<div class="overlay-panel-wrapper" bind:this={wrapper}>
|
||||
<div bind:this={wrapper} class="overlay-panel-wrapper">
|
||||
{#if active}
|
||||
<div class="overlay-panel-container" class:padded={popup} class:active>
|
||||
<div
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
href={import.meta.env.PB_RELEASES}
|
||||
class="inline-flex flex-gap-5"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
title="Releases"
|
||||
>
|
||||
<span class="txt">PocketBase {import.meta.env.PB_VERSION}</span>
|
||||
|
||||
@@ -21,15 +21,21 @@
|
||||
|
||||
<OverlayPanel bind:this={panel} class="image-preview" btnClose={false} popup on:show on:hide>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="overlay-close" on:click|preventDefault={hide}>
|
||||
<button type="button" class="overlay-close" on:click|preventDefault={hide}>
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
|
||||
<img src={url} alt="Preview {url}" />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<a href={url} title="Download" class="link-hint txt-ellipsis">
|
||||
<a
|
||||
href={url}
|
||||
title="Download"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="link-hint txt-ellipsis"
|
||||
>
|
||||
{url.substring(url.lastIndexOf("/") + 1)}
|
||||
</a>
|
||||
<div class="flex-fill" />
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
refreshTimeoutId = setTimeout(() => {
|
||||
refreshTimeoutId = null;
|
||||
tooltipData = oldTooltipData;
|
||||
}, 200);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -45,6 +45,6 @@
|
||||
}
|
||||
}
|
||||
.btn.refreshing i {
|
||||
animation: refresh 200ms ease-out;
|
||||
animation: refresh 150ms ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="searchbar-wrapper" on:click|stopPropagation>
|
||||
<form class="searchbar" on:submit|preventDefault={submit}>
|
||||
<div class="searchbar-wrapper">
|
||||
<form class="searchbar" on:click|stopPropagation on:submit|preventDefault={submit}>
|
||||
<label for={uniqueId} class="m-l-10 txt-xl">
|
||||
<i class="ri-search-line" />
|
||||
</label>
|
||||
|
||||
@@ -194,13 +194,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="select {classes}" class:multiple class:disabled bind:this={container}>
|
||||
<div tabindex={disabled ? "-1" : "0"} class="selected-container" class:disabled bind:this={labelDiv}>
|
||||
<div bind:this={container} class="select {classes}" class:multiple class:disabled>
|
||||
<div bind:this={labelDiv} tabindex={disabled ? "-1" : "0"} class="selected-container" class:disabled>
|
||||
{#each CommonHelper.toArray(selected) as item}
|
||||
<div class="option">
|
||||
{#if labelComponent}
|
||||
<svelte:component this={labelComponent} {item} {...labelComponentProps} />
|
||||
{:else}<span class="txt">{item}</span>{/if}
|
||||
{:else}
|
||||
<span class="txt">{item}</span>
|
||||
{/if}
|
||||
|
||||
{#if multiple || toggle}
|
||||
<span
|
||||
@@ -213,17 +215,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="txt-placeholder">{selectPlaceholder}</div>
|
||||
<div class="block txt-placeholder" class:link-hint={!disabled}>
|
||||
{selectPlaceholder}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if !disabled}
|
||||
<Toggler
|
||||
bind:this={toggler}
|
||||
class="dropdown dropdown-block options-dropdown dropdown-left"
|
||||
trigger={labelDiv}
|
||||
on:show={onDropdownShow}
|
||||
on:hide
|
||||
bind:this={toggler}
|
||||
>
|
||||
{#if searchable}
|
||||
<div class="form-field form-field-sm options-search">
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
<div class="content">{toast.message}</div>
|
||||
|
||||
<div class="close" on:click|preventDefault={() => removeToast(toast)}>
|
||||
<button type="button" class="close" on:click|preventDefault={() => removeToast(toast)}>
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,20 @@
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
let container;
|
||||
let container = undefined;
|
||||
let activeTrigger = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: if (container) {
|
||||
bindTrigger(trigger);
|
||||
}
|
||||
|
||||
$: if (active) {
|
||||
trigger?.classList?.add("active");
|
||||
activeTrigger?.classList?.add("active");
|
||||
dispatch("show");
|
||||
} else {
|
||||
trigger?.classList?.remove("active");
|
||||
activeTrigger?.classList?.remove("active");
|
||||
dispatch("hide");
|
||||
}
|
||||
|
||||
@@ -42,7 +47,7 @@
|
||||
!container ||
|
||||
elem.classList.contains(closableClass) ||
|
||||
// is the trigger itself (or a direct child)
|
||||
(trigger?.contains(elem) && !container.contains(elem)) ||
|
||||
(activeTrigger?.contains(elem) && !container.contains(elem)) ||
|
||||
// is closable toggler child
|
||||
(container.contains(elem) && elem.closest && elem.closest("." + closableClass))
|
||||
);
|
||||
@@ -51,6 +56,8 @@
|
||||
function handleClickToggle(e) {
|
||||
if (!active || isClosable(e.target)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
@@ -67,13 +74,13 @@
|
||||
}
|
||||
|
||||
function handleOutsideClick(e) {
|
||||
if (active && !container?.contains(e.target) && !trigger?.contains(e.target)) {
|
||||
if (active && !container?.contains(e.target) && !activeTrigger?.contains(e.target)) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscPress(e) {
|
||||
if (active && escClose && e.code == "Escape") {
|
||||
if (active && escClose && e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
hide();
|
||||
}
|
||||
@@ -83,16 +90,34 @@
|
||||
return handleOutsideClick(e);
|
||||
}
|
||||
|
||||
function bindTrigger(newTrigger) {
|
||||
cleanup();
|
||||
|
||||
activeTrigger = newTrigger || container?.parentNode;
|
||||
|
||||
if (!activeTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
container?.addEventListener("click", handleClickToggle);
|
||||
activeTrigger.addEventListener("click", handleClickToggle);
|
||||
activeTrigger.addEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (!activeTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
container?.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger.removeEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
trigger = trigger || container.parentNode;
|
||||
bindTrigger();
|
||||
|
||||
trigger.addEventListener("click", handleClickToggle);
|
||||
trigger.addEventListener("keydown", handleKeydownToggle);
|
||||
|
||||
return () => {
|
||||
trigger.removeEventListener("click", handleClickToggle);
|
||||
trigger.removeEventListener("keydown", handleKeydownToggle);
|
||||
};
|
||||
return () => cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<script>
|
||||
import { scale, slide } from "svelte/transition";
|
||||
import { Collection } from "pocketbase";
|
||||
import { errors } from "@/stores/errors";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
$: if (collection.isAuth && CommonHelper.isEmpty(collection.options)) {
|
||||
collection.options = {
|
||||
allowEmailAuth: true,
|
||||
allowUsernameAuth: true,
|
||||
allowOAuth2Auth: true,
|
||||
minPasswordLength: 8,
|
||||
};
|
||||
}
|
||||
|
||||
$: hasUsernameErrors = false;
|
||||
|
||||
$: hasEmailErrors =
|
||||
!CommonHelper.isEmpty($errors?.options?.allowEmailAuth) ||
|
||||
!CommonHelper.isEmpty($errors?.options?.onlyEmailDomains) ||
|
||||
!CommonHelper.isEmpty($errors?.options?.exceptEmailDomains);
|
||||
|
||||
$: hasOAuth2Errors = !CommonHelper.isEmpty($errors?.options?.allowOAuth2Auth);
|
||||
</script>
|
||||
|
||||
<h4 class="section-title">Auth methods</h4>
|
||||
|
||||
<div class="accordions">
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-user-star-line" />
|
||||
<span class="txt">Username/Password</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if collection.options.allowUsernameAuth}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
{#if hasUsernameErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-0" name="options.allowUsernameAuth" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={collection.options.allowUsernameAuth} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
</Accordion>
|
||||
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-mail-star-line" />
|
||||
<span class="txt">Email/Password</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if collection.options.allowEmailAuth}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
{#if hasEmailErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-0" name="options.allowEmailAuth" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={collection.options.allowEmailAuth} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
{#if collection.options.allowEmailAuth}
|
||||
<div class="grid grid-sm p-t-sm" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="col-lg-6">
|
||||
<Field
|
||||
class="form-field {!CommonHelper.isEmpty(collection.options.onlyEmailDomains)
|
||||
? 'disabled'
|
||||
: ''}"
|
||||
name="options.exceptEmailDomains"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Except domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Email domains that are NOT allowed to sign up. \n This field is disabled if "Only domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
disabled={!CommonHelper.isEmpty(collection.options.onlyEmailDomains)}
|
||||
bind:value={collection.options.exceptEmailDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field
|
||||
class="form-field {!CommonHelper.isEmpty(collection.options.exceptEmailDomains)
|
||||
? 'disabled'
|
||||
: ''}"
|
||||
name="options.onlyEmailDomains"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Only domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Email domains that are ONLY allowed to sign up. \n This field is disabled if "Except domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
disabled={!CommonHelper.isEmpty(collection.options.exceptEmailDomains)}
|
||||
bind:value={collection.options.onlyEmailDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Accordion>
|
||||
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-shield-star-line" />
|
||||
<span class="txt">OAuth2</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if collection.options.allowOAuth2Auth}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
{#if hasOAuth2Errors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-0" name="options.allowOAuth2Auth" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={collection.options.allowOAuth2Auth} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
{#if collection.options.allowOAuth2Auth}
|
||||
<div class="block" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="flex p-t-base">
|
||||
<a href="/_/#/settings/auth-providers" target="_blank" class="btn btn-sm btn-outline">
|
||||
<span class="txt">Manage OAuth2 providers</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4 class="section-title">General</h4>
|
||||
|
||||
<Field class="form-field required" name="options.minPasswordLength" let:uniqueId>
|
||||
<label for={uniqueId}>Minimum password length</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
required
|
||||
min="6"
|
||||
max="72"
|
||||
bind:value={collection.options.minPasswordLength}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-sm" name="options.requireEmail" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={collection.options.requireEmail} />
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Always require email</span>
|
||||
<i
|
||||
class="ri-information-line txt-sm link-hint"
|
||||
use:tooltip={{
|
||||
text: "The constraint is applied only for new records.\nAlso note that some OAuth2 providers (like Twitter), don't return an email and the authentication may fail if the email field is required.",
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
|
||||
const baseTabs = {
|
||||
list: {
|
||||
label: "List/Search",
|
||||
component: import("@/components/collections/docs/ListApiDocs.svelte"),
|
||||
},
|
||||
view: {
|
||||
label: "View",
|
||||
component: import("@/components/collections/docs/ViewApiDocs.svelte"),
|
||||
},
|
||||
create: {
|
||||
label: "Create",
|
||||
component: import("@/components/collections/docs/CreateApiDocs.svelte"),
|
||||
},
|
||||
update: {
|
||||
label: "Update",
|
||||
component: import("@/components/collections/docs/UpdateApiDocs.svelte"),
|
||||
},
|
||||
delete: {
|
||||
label: "Delete",
|
||||
component: import("@/components/collections/docs/DeleteApiDocs.svelte"),
|
||||
},
|
||||
realtime: {
|
||||
label: "Realtime",
|
||||
component: import("@/components/collections/docs/RealtimeApiDocs.svelte"),
|
||||
},
|
||||
};
|
||||
|
||||
const authTabs = {
|
||||
"auth-with-password": {
|
||||
label: "Auth with password",
|
||||
component: import("@/components/collections/docs/AuthWithPasswordDocs.svelte"),
|
||||
},
|
||||
"auth-with-oauth2": {
|
||||
label: "Auth with OAuth2",
|
||||
component: import("@/components/collections/docs/AuthWithOAuth2Docs.svelte"),
|
||||
},
|
||||
refresh: {
|
||||
label: "Auth refresh",
|
||||
component: import("@/components/collections/docs/AuthRefreshDocs.svelte"),
|
||||
},
|
||||
"request-verification": {
|
||||
label: "Request verification",
|
||||
component: import("@/components/collections/docs/RequestVerificationDocs.svelte"),
|
||||
},
|
||||
"confirm-verification": {
|
||||
label: "Confirm verification",
|
||||
component: import("@/components/collections/docs/ConfirmVerificationDocs.svelte"),
|
||||
},
|
||||
"request-password-reset": {
|
||||
label: "Request password reset",
|
||||
component: import("@/components/collections/docs/RequestPasswordResetDocs.svelte"),
|
||||
},
|
||||
"confirm-password-reset": {
|
||||
label: "Confirm password reset",
|
||||
component: import("@/components/collections/docs/ConfirmPasswordResetDocs.svelte"),
|
||||
},
|
||||
"request-email-change": {
|
||||
label: "Request email change",
|
||||
component: import("@/components/collections/docs/RequestEmailChangeDocs.svelte"),
|
||||
},
|
||||
"confirm-email-change": {
|
||||
label: "Confirm email change",
|
||||
component: import("@/components/collections/docs/ConfirmEmailChangeDocs.svelte"),
|
||||
},
|
||||
"list-auth-methods": {
|
||||
label: "List auth methods",
|
||||
component: import("@/components/collections/docs/AuthMethodsDocs.svelte"),
|
||||
},
|
||||
"list-linked-accounts": {
|
||||
label: "List OAuth2 accounts",
|
||||
component: import("@/components/collections/docs/ListExternalAuthsDocs.svelte"),
|
||||
},
|
||||
"unlink-account": {
|
||||
label: "Unlink OAuth2 account",
|
||||
component: import("@/components/collections/docs/UnlinkExternalAuthDocs.svelte"),
|
||||
},
|
||||
};
|
||||
|
||||
let docsPanel;
|
||||
let collection = new Collection();
|
||||
let activeTab;
|
||||
let tabs = [];
|
||||
|
||||
$: if (collection.isAuth) {
|
||||
tabs = Object.assign({}, baseTabs, authTabs);
|
||||
if (!collection?.options.allowUsernameAuth && !collection?.options.allowEmailAuth) {
|
||||
delete tabs["auth-with-password"];
|
||||
}
|
||||
if (!collection?.options.allowOAuth2Auth) {
|
||||
delete tabs["auth-with-oauth2"];
|
||||
}
|
||||
} else {
|
||||
tabs = Object.assign({}, baseTabs);
|
||||
}
|
||||
|
||||
// reset active tab on tabs list change
|
||||
if (tabs.length) {
|
||||
activeTab = Object.keys(tabs)[0];
|
||||
}
|
||||
|
||||
export function show(model) {
|
||||
collection = model;
|
||||
|
||||
changeTab(Object.keys(tabs)[0]);
|
||||
|
||||
return docsPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return docsPanel?.hide();
|
||||
}
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel bind:this={docsPanel} on:hide on:show class="docs-panel">
|
||||
<div class="docs-content-wrapper">
|
||||
<aside class="docs-sidebar" class:compact={collection?.isAuth}>
|
||||
<nav class="sidebar-content">
|
||||
{#each Object.entries(tabs) as [key, tab] (key)}
|
||||
<button
|
||||
type="button"
|
||||
class="sidebar-item"
|
||||
class:active={activeTab === key}
|
||||
on:click={() => changeTab(key)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="docs-content">
|
||||
{#each Object.entries(tabs) as [key, tab] (key)}
|
||||
{#if activeTab === key}
|
||||
{#await tab.component then { default: TabComponent }}
|
||||
<TabComponent {collection} />
|
||||
{/await}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- visible only on small screens -->
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" on:click={() => hide()}>
|
||||
<span class="txt">Close</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -2,10 +2,40 @@
|
||||
import { SchemaField } from "pocketbase";
|
||||
import FieldAccordion from "@/components/collections/FieldAccordion.svelte";
|
||||
|
||||
const reservedNames = ["id", "created", "updated"];
|
||||
|
||||
export let collection = {};
|
||||
|
||||
const baseReservedNames = [
|
||||
"id",
|
||||
"created",
|
||||
"updated",
|
||||
"collectionId",
|
||||
"collectionName",
|
||||
"expand",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
];
|
||||
|
||||
let reservedNames = [];
|
||||
|
||||
$: if (collection.isAuth) {
|
||||
reservedNames = baseReservedNames.concat([
|
||||
"username",
|
||||
"email",
|
||||
"emailVisibility",
|
||||
"verified",
|
||||
"tokenKey",
|
||||
"passwordHash",
|
||||
"lastResetSentAt",
|
||||
"lastVerificationSentAt",
|
||||
"password",
|
||||
"passwordConfirm",
|
||||
"oldPassword",
|
||||
]);
|
||||
} else {
|
||||
reservedNames = baseReservedNames.slice(0);
|
||||
}
|
||||
|
||||
$: if (typeof collection?.schema === "undefined") {
|
||||
collection = collection || {};
|
||||
collection.schema = [];
|
||||
@@ -58,15 +88,68 @@
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// fields drag&drop handling
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
function onFieldDrag(event, i) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setData("text/plain", i);
|
||||
}
|
||||
|
||||
function onFieldDrop(event, target) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
|
||||
const start = parseInt(event.dataTransfer.getData("text/plain"));
|
||||
const newSchema = collection.schema;
|
||||
|
||||
if (start < target) {
|
||||
newSchema.splice(target + 1, 0, newSchema[start]);
|
||||
newSchema.splice(start, 1);
|
||||
} else {
|
||||
newSchema.splice(target, 0, newSchema[start]);
|
||||
newSchema.splice(start + 1, 1);
|
||||
}
|
||||
|
||||
collection.schema = newSchema;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block m-b-25">
|
||||
<p class="txt-sm">
|
||||
System fields:
|
||||
<code class="txt-sm">id</code> ,
|
||||
<code class="txt-sm">created</code> ,
|
||||
<code class="txt-sm">updated</code>
|
||||
{#if collection.isAuth}
|
||||
,
|
||||
<code class="txt-sm">username</code> ,
|
||||
<code class="txt-sm">email</code> ,
|
||||
<code class="txt-sm">emailVisibility</code> ,
|
||||
<code class="txt-sm">verified</code>
|
||||
{/if}
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div class="accordions">
|
||||
{#each collection.schema as field, i (i)}
|
||||
{#each collection.schema as field, i (i + field.id)}
|
||||
<FieldAccordion
|
||||
bind:field
|
||||
key={i}
|
||||
excludeNames={reservedNames.concat(getSiblingsFieldNames(field))}
|
||||
on:remove={() => removeField(i)}
|
||||
on:dragstart={(e) => onFieldDrag(e?.detail, i)}
|
||||
on:drop={(e) => onFieldDrop(e?.detail, i)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -75,7 +158,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block {collection.schema?.length ? 'btn-secondary' : 'btn-success'}"
|
||||
class="btn btn-block {collection.schema?.length ? 'btn-secondary' : 'btn-warning'}"
|
||||
on:click={newField}
|
||||
>
|
||||
<i class="ri-add-line" />
|
||||
|
||||
@@ -1,52 +1,18 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import { Collection } from "pocketbase";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import RuleField from "@/components/collections/RuleField.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let tempValues = {};
|
||||
let showFiltersInfo = false;
|
||||
let editorRefs = {};
|
||||
let ruleInputComponent;
|
||||
let isRuleComponentLoading = false;
|
||||
|
||||
// all supported collection rules in "collection_rule_prop: label" format
|
||||
const ruleProps = {
|
||||
listRule: "List Action",
|
||||
viewRule: "View Action",
|
||||
createRule: "Create Action",
|
||||
updateRule: "Update Action",
|
||||
deleteRule: "Delete Action",
|
||||
};
|
||||
|
||||
function isAdminOnly(propVal) {
|
||||
return propVal === null;
|
||||
}
|
||||
|
||||
async function loadEditorComponent() {
|
||||
isRuleComponentLoading = true;
|
||||
try {
|
||||
ruleInputComponent = (await import("@/components/base/FilterAutocompleteInput.svelte")).default;
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
ruleInputComponent = null;
|
||||
}
|
||||
isRuleComponentLoading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadEditorComponent();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="block m-b-base">
|
||||
<div class="flex">
|
||||
<div class="flex txt-sm m-b-5">
|
||||
<p>
|
||||
All rules follow the
|
||||
<a href={import.meta.env.PB_RULES_SYNTAX_DOCS} target="_blank" rel="noopener">
|
||||
<a href={import.meta.env.PB_RULES_SYNTAX_DOCS} target="_blank" rel="noopener noreferrer">
|
||||
PocketBase filter syntax and operators
|
||||
</a>.
|
||||
</p>
|
||||
@@ -85,7 +51,7 @@
|
||||
<code>@request.method</code>
|
||||
<code>@request.query.*</code>
|
||||
<code>@request.data.*</code>
|
||||
<code>@request.user.*</code>
|
||||
<code>@request.auth.*</code>
|
||||
</div>
|
||||
|
||||
<hr class="m-t-10 m-b-5" />
|
||||
@@ -104,7 +70,7 @@
|
||||
<p>
|
||||
Example rule:
|
||||
<br />
|
||||
<code>@request.user.id!="" && created>"2022-01-01 00:00:00"</code>
|
||||
<code>@request.auth.id != "" && created > "2022-01-01 00:00:00"</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,78 +78,37 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isRuleComponentLoading}
|
||||
<div class="txt-center">
|
||||
<span class="loader" />
|
||||
</div>
|
||||
{:else}
|
||||
{#each Object.entries(ruleProps) as [prop, label] (prop)}
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<div class="rule-block">
|
||||
{#if isAdminOnly(collection[prop])}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline btn-success"
|
||||
use:tooltip={"Unlock and set custom rule"}
|
||||
on:click={async () => {
|
||||
collection[prop] = tempValues[prop] || "";
|
||||
await tick();
|
||||
editorRefs[prop]?.focus();
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-unlock-line" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline"
|
||||
use:tooltip={"Lock and set to Admins only"}
|
||||
on:click={() => {
|
||||
tempValues[prop] = collection[prop];
|
||||
collection[prop] = null;
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-line" />
|
||||
</button>
|
||||
{/if}
|
||||
<RuleField label="List/Search action" formKey="listRule" {collection} bind:rule={collection.listRule} />
|
||||
|
||||
<Field
|
||||
class="form-field rule-field m-0 {isAdminOnly(collection[prop]) ? 'disabled' : ''}"
|
||||
name={prop}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
{label} - {isAdminOnly(collection[prop]) ? "Admins only" : "Custom rule"}
|
||||
</label>
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<RuleField label="View action" formKey="viewRule" {collection} bind:rule={collection.viewRule} />
|
||||
|
||||
<svelte:component
|
||||
this={ruleInputComponent}
|
||||
id={uniqueId}
|
||||
bind:this={editorRefs[prop]}
|
||||
bind:value={collection[prop]}
|
||||
baseCollection={collection}
|
||||
disabled={isAdminOnly(collection[prop])}
|
||||
/>
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<RuleField label="Create action" formKey="createRule" {collection} bind:rule={collection.createRule} />
|
||||
|
||||
<div class="help-block">
|
||||
{#if isAdminOnly(collection[prop])}
|
||||
Only admins will be able to access (unlock to change)
|
||||
{:else}
|
||||
Leave empty to grant everyone access
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
{/each}
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<RuleField label="Update action" formKey="updateRule" {collection} bind:rule={collection.updateRule} />
|
||||
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<RuleField label="Delete action" formKey="deleteRule" {collection} bind:rule={collection.deleteRule} />
|
||||
|
||||
{#if collection?.isAuth}
|
||||
<hr class="m-t-sm m-b-sm" />
|
||||
<RuleField
|
||||
label="Manage action"
|
||||
formKey="options.manageRule"
|
||||
{collection}
|
||||
bind:rule={collection.options.manageRule}
|
||||
>
|
||||
<svelte:fragment>
|
||||
<p>
|
||||
This API rule gives admin-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.
|
||||
</p>
|
||||
<p>
|
||||
This rule is executed in addition to the <code>create</code> and <code>update</code> API rules.
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</RuleField>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.rule-block {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--xsSpacing);
|
||||
}
|
||||
.rule-toggle-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,17 +7,27 @@
|
||||
import { errors, setErrors } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import { addCollection, removeCollection, activeCollection } from "@/stores/collections";
|
||||
import { addCollection, removeCollection } from "@/stores/collections";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import CollectionFieldsTab from "@/components/collections/CollectionFieldsTab.svelte";
|
||||
import CollectionRulesTab from "@/components/collections/CollectionRulesTab.svelte";
|
||||
import CollectionAuthOptionsTab from "@/components/collections/CollectionAuthOptionsTab.svelte";
|
||||
import CollectionUpdateConfirm from "@/components/collections/CollectionUpdateConfirm.svelte";
|
||||
|
||||
const TAB_FIELDS = "fields";
|
||||
const TAB_RULES = "api_rules";
|
||||
const TAB_OPTIONS = "options";
|
||||
|
||||
const TYPE_BASE = "base";
|
||||
const TYPE_AUTH = "auth";
|
||||
|
||||
const collectionTypes = {};
|
||||
collectionTypes[TYPE_BASE] = "Base";
|
||||
collectionTypes[TYPE_AUTH] = "Auth";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let collectionPanel;
|
||||
@@ -42,6 +52,11 @@
|
||||
|
||||
$: canSave = collection.isNew || hasChanges;
|
||||
|
||||
$: if (activeTab === TAB_OPTIONS && collection.type !== TYPE_AUTH) {
|
||||
// reset selected tab
|
||||
changeTab(TAB_FIELDS);
|
||||
}
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
@@ -111,11 +126,10 @@
|
||||
);
|
||||
addCollection(result);
|
||||
|
||||
if (collection.isNew) {
|
||||
$activeCollection = result;
|
||||
}
|
||||
|
||||
dispatch("save", result);
|
||||
dispatch("save", {
|
||||
isNew: collection.isNew,
|
||||
collection: result,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
@@ -163,11 +177,15 @@
|
||||
function calculateFormHash(m) {
|
||||
return JSON.stringify(m);
|
||||
}
|
||||
|
||||
function setCollectionType(t) {
|
||||
collection.type = t;
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
class="overlay-panel-lg colored-header compact-header collection-panel"
|
||||
class="overlay-panel-lg colored-header collection-panel"
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
@@ -191,7 +209,11 @@
|
||||
<button type="button" class="btn btn-sm btn-circle btn-secondary flex-gap-0">
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right m-t-5">
|
||||
<button type="button" class="dropdown-item closable" on:click={() => deleteConfirm()}>
|
||||
<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>
|
||||
</button>
|
||||
@@ -206,11 +228,12 @@
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
class="form-field required m-b-0 {isSystemUpdate ? 'disabled' : ''}"
|
||||
class="form-field collection-field-name required m-b-0 {isSystemUpdate ? 'disabled' : ''}"
|
||||
name="name"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Name</label>
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
@@ -226,6 +249,35 @@
|
||||
e.target.value = collection.name;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="form-field-addon">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm p-r-10 p-l-10 {collection.isNew ? 'btn-hint' : 'btn-secondary'}"
|
||||
disabled={!collection.isNew}
|
||||
>
|
||||
<!-- empty span for alignment -->
|
||||
<span />
|
||||
<span class="txt">Type: {collectionTypes[collection.type] || "N/A"}</span>
|
||||
{#if collection.isNew}
|
||||
<i class="ri-arrow-down-s-fill" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap m-t-5">
|
||||
{#each Object.entries(collectionTypes) as [type, label]}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
class:selected={type == collection.type}
|
||||
on:click={() => setCollectionType(type)}
|
||||
>
|
||||
<i class={CommonHelper.getCollectionTypeIcon(type)} />
|
||||
<span class="txt">{label} collection</span>
|
||||
</button>
|
||||
{/each}
|
||||
</Toggler>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if collection.system}
|
||||
<div class="help-block">System collection</div>
|
||||
{/if}
|
||||
@@ -258,7 +310,7 @@
|
||||
on:click={() => changeTab(TAB_RULES)}
|
||||
>
|
||||
<span class="txt">API Rules</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.listRule) || !CommonHelper.isEmpty($errors?.viewRule) || !CommonHelper.isEmpty($errors?.createRule) || !CommonHelper.isEmpty($errors?.updateRule) || !CommonHelper.isEmpty($errors?.deleteRule)}
|
||||
{#if !CommonHelper.isEmpty($errors?.listRule) || !CommonHelper.isEmpty($errors?.viewRule) || !CommonHelper.isEmpty($errors?.createRule) || !CommonHelper.isEmpty($errors?.updateRule) || !CommonHelper.isEmpty($errors?.deleteRule) || !CommonHelper.isEmpty($errors?.options?.manageRule)}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale|local={{ duration: 150, start: 0.7 }}
|
||||
@@ -266,6 +318,24 @@
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if collection.isAuth}
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === TAB_OPTIONS}
|
||||
on:click={() => changeTab(TAB_OPTIONS)}
|
||||
>
|
||||
<span class="txt">Options</span>
|
||||
{#if !CommonHelper.isEmpty($errors?.options) && !$errors?.options?.manageRule}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale|local={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={"Has errors"}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -280,6 +350,12 @@
|
||||
<CollectionRulesTab bind:collection />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if collection.isAuth}
|
||||
<div class="tab-item" class:active={activeTab === TAB_OPTIONS}>
|
||||
<CollectionAuthOptionsTab bind:collection />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
@@ -302,6 +378,6 @@
|
||||
|
||||
<style>
|
||||
.tabs-content {
|
||||
z-index: 3; /* autocomplete dropdown overlay fix */
|
||||
z-index: auto; /* autocomplete dropdown overlay fix */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import { link } from "svelte-spa-router";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { hideControls } from "@/stores/app";
|
||||
import { collections, activeCollection } from "@/stores/collections";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
@@ -12,15 +14,27 @@
|
||||
|
||||
$: filteredCollections = $collections.filter((collection) => {
|
||||
return (
|
||||
collection.name != import.meta.env.PB_PROFILE_COLLECTION &&
|
||||
(collection.id == searchTerm ||
|
||||
collection.name.replace(/\s+/g, "").toLowerCase().includes(normalizedSearch))
|
||||
collection.id == searchTerm ||
|
||||
collection.name.replace(/\s+/g, "").toLowerCase().includes(normalizedSearch)
|
||||
);
|
||||
});
|
||||
|
||||
$: if ($collections) {
|
||||
scrollIntoView();
|
||||
}
|
||||
|
||||
function selectCollection(collection) {
|
||||
$activeCollection = collection;
|
||||
}
|
||||
|
||||
function scrollIntoView() {
|
||||
setTimeout(() => {
|
||||
const activeItem = document.querySelector(".collection-sidebar .sidebar-list-item.active");
|
||||
if (activeItem) {
|
||||
activeItem?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="page-sidebar collection-sidebar">
|
||||
@@ -42,21 +56,18 @@
|
||||
|
||||
<hr class="m-t-5 m-b-xs" />
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-content" class:sidebar-content-compact={filteredCollections.length > 20}>
|
||||
{#each filteredCollections as collection (collection.id)}
|
||||
<div
|
||||
tabindex="0"
|
||||
<a
|
||||
href="/collections?collectionId={collection.id}"
|
||||
class="sidebar-list-item"
|
||||
class:active={$activeCollection?.id === collection.id}
|
||||
on:click={() => selectCollection(collection)}
|
||||
use:link
|
||||
>
|
||||
{#if $activeCollection?.id === collection.id}
|
||||
<i class="ri-folder-open-line" />
|
||||
{:else}
|
||||
<i class="ri-folder-2-line" />
|
||||
{/if}
|
||||
<i class={CommonHelper.getCollectionTypeIcon(collection.type)} />
|
||||
|
||||
<span class="txt">{collection.name}</span>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
{#if normalizedSearch.length}
|
||||
<p class="txt-hint m-t-10 m-b-10 txt-center">No collections found.</p>
|
||||
@@ -74,4 +85,11 @@
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<CollectionUpsertPanel bind:this={collectionPanel} />
|
||||
<CollectionUpsertPanel
|
||||
bind:this={collectionPanel}
|
||||
on:save={(e) => {
|
||||
if (e.detail?.isNew && e.detail.collection) {
|
||||
selectCollection(e.detail.collection);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -121,9 +121,15 @@
|
||||
on:expand
|
||||
on:collapse
|
||||
on:toggle
|
||||
on:dragenter
|
||||
on:dragleave
|
||||
on:dragstart
|
||||
on:drop
|
||||
draggable
|
||||
single
|
||||
{interactive}
|
||||
class={disabled || field.toDelete || field.system ? "field-accordion disabled" : "field-accordion"}
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
@@ -144,7 +150,7 @@
|
||||
<span class="label" class:label-warning={interactive && !field.toDelete}>New</span>
|
||||
{/if}
|
||||
{#if field.required}
|
||||
<span class="label label-success">Required</span>
|
||||
<span class="label label-success">Nonempty</span>
|
||||
{/if}
|
||||
{#if field.unique}
|
||||
<span class="label label-success">Unique</span>
|
||||
@@ -177,6 +183,11 @@
|
||||
|
||||
<form
|
||||
class="field-form"
|
||||
on:dragstart={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}}
|
||||
on:submit|preventDefault={() => {
|
||||
canBeStored && collapse();
|
||||
}}
|
||||
@@ -192,6 +203,7 @@
|
||||
<FieldTypeSelect id={uniqueId} disabled={field.id} bind:value={field.type} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<Field
|
||||
class="
|
||||
@@ -257,7 +269,18 @@
|
||||
<div class="col-sm-4 flex">
|
||||
<Field class="form-field form-field-toggle m-0" name="requried" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={field.required} />
|
||||
<label for={uniqueId}>Required</label>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Nonempty</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `Requires the field value to be nonempty\n(aka. not ${CommonHelper.zeroDefaultStr(
|
||||
field
|
||||
)}).`,
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script context="module">
|
||||
let cachedRuleComponent;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { tick } from "svelte";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let collection = null;
|
||||
export let rule = null;
|
||||
export let label = "Rule";
|
||||
export let formKey = "rule";
|
||||
export let required = false;
|
||||
|
||||
let editorRef = null;
|
||||
let tempValue = null;
|
||||
let ruleInputComponent = cachedRuleComponent;
|
||||
let isRuleComponentLoading = false;
|
||||
|
||||
$: isAdminOnly = rule === null;
|
||||
|
||||
async function loadEditorComponent() {
|
||||
if (ruleInputComponent || isRuleComponentLoading) {
|
||||
return; // already loaded or in the process
|
||||
}
|
||||
|
||||
isRuleComponentLoading = true;
|
||||
|
||||
ruleInputComponent = (await import("@/components/base/FilterAutocompleteInput.svelte")).default;
|
||||
|
||||
cachedRuleComponent = ruleInputComponent;
|
||||
|
||||
isRuleComponentLoading = false;
|
||||
}
|
||||
|
||||
loadEditorComponent();
|
||||
</script>
|
||||
|
||||
{#if isRuleComponentLoading}
|
||||
<div class="txt-center">
|
||||
<span class="loader" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rule-block">
|
||||
{#if isAdminOnly}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline btn-success"
|
||||
use:tooltip={{
|
||||
text: "Unlock and set custom rule",
|
||||
position: "left",
|
||||
}}
|
||||
on:click={async () => {
|
||||
rule = tempValue || "";
|
||||
await tick();
|
||||
editorRef?.focus();
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-unlock-line" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="rule-toggle-btn btn btn-circle btn-outline"
|
||||
use:tooltip={{
|
||||
text: "Lock and set to Admins only",
|
||||
position: "left",
|
||||
}}
|
||||
on:click={() => {
|
||||
tempValue = rule;
|
||||
rule = null;
|
||||
}}
|
||||
>
|
||||
<i class="ri-lock-line" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Field
|
||||
class="form-field rule-field m-0 {required ? 'requied' : ''} {isAdminOnly ? 'disabled' : ''}"
|
||||
name={formKey}
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
{label} - {isAdminOnly ? "Admins only" : "Custom rule"}
|
||||
</label>
|
||||
|
||||
<svelte:component
|
||||
this={ruleInputComponent}
|
||||
id={uniqueId}
|
||||
bind:this={editorRef}
|
||||
bind:value={rule}
|
||||
baseCollection={collection}
|
||||
disabled={isAdminOnly}
|
||||
/>
|
||||
|
||||
<div class="help-block">
|
||||
<slot {isAdminOnly}>
|
||||
<p>
|
||||
{#if isAdminOnly}
|
||||
Only admins will be able to perform this action (unlock to change)
|
||||
{:else}
|
||||
Leave empty to grant everyone access
|
||||
{/if}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.rule-block {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--xsSpacing);
|
||||
}
|
||||
.rule-toggle-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: `
|
||||
{
|
||||
"usernamePassword": true,
|
||||
"emailPassword": true,
|
||||
"authProviders": [
|
||||
{
|
||||
"name": "github",
|
||||
"state": "3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd",
|
||||
"codeVerifier": "KxFDWz1B3fxscCDJ_9gHQhLuh__ie7",
|
||||
"codeChallenge": "NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://github.com/login/oauth/authorize?client_id=demo&code_challenge=NM1oVexB6Q6QH8uPtOUfK7tq4pmu4Jz6lNDIwoxHZNE%3D&code_challenge_method=S256&response_type=code&scope=user&state=3Yd8jNkK_6PJG6hPWwBjLqKwse6Ejd&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "gitlab",
|
||||
"state": "NeQSbtO5cShr_mk5__3CUukiMnymeb",
|
||||
"codeVerifier": "ahTFHOgua8mkvPAlIBGwCUJbWKR_xi",
|
||||
"codeChallenge": "O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://gitlab.com/oauth/authorize?client_id=demo&code_challenge=O-GATkTj4eXDCnfonsqGLCd6njvTixlpCMvy5kjgOOg%3D&code_challenge_method=S256&response_type=code&scope=read_user&state=NeQSbtO5cShr_mk5__3CUukiMnymeb&redirect_uri="
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"state": "zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ",
|
||||
"codeVerifier": "t3CmO5VObGzdXqieakvR_fpjiW0zdO",
|
||||
"codeChallenge": "KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk=",
|
||||
"codeChallengeMethod": "S256",
|
||||
"authUrl": "https://accounts.google.com/o/oauth2/auth?client_id=demo&code_challenge=KChwoQPKYlz2anAdqtgsSTdIo8hdwtc1fh2wHMwW2Yk%3D&code_challenge_method=S256&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=zB3ZPifV1TW2GMuvuFkamSXfSNkHPQ&redirect_uri="
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">List auth methods ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Returns a public list with all allowed <strong>{collection.name}</strong> authentication methods.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${collection?.name}').listAuthMethods();
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${collection?.name}').listAuthMethods();
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/auth-methods
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"identity": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 401,
|
||||
body: `
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Auth refresh ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>
|
||||
Returns a new auth response (token and account data) for an
|
||||
<strong>already authenticated record</strong>.
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
This method is usually called by users on page/screen reload to ensure that the previously stored
|
||||
data in <code>pb.authStore</code> is still valid and up-to-date.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/auth-refresh
|
||||
</p>
|
||||
</div>
|
||||
<p class="txt-hint txt-sm txt-right">Requires record <code>Authorization:TOKEN</code> header</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,261 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
meta: {
|
||||
id: "abc123",
|
||||
name: "John Doe",
|
||||
username: "john.doe",
|
||||
email: "test@example.com",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "An error occurred while submitting the form.",
|
||||
"data": {
|
||||
"provider": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Auth with OAuth2 ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Authenticate with an OAuth2 provider and returns a new auth token and account data.</p>
|
||||
<p>This action usually should be called right after the provider login page redirect.</p>
|
||||
<p>
|
||||
You could also check the
|
||||
<a href={import.meta.env.PB_OAUTH2_EXAMPLE} target="_blank" rel="noopener noreferrer">
|
||||
OAuth2 web integration example
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').authWithOAuth2(
|
||||
'google',
|
||||
'CODE',
|
||||
'VERIFIER',
|
||||
'REDIRECT_URL',
|
||||
// optional data that will be used for the new account on OAuth2 sign-up
|
||||
{
|
||||
'name': 'test',
|
||||
},
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').authWithOAuth2(
|
||||
'google',
|
||||
'CODE',
|
||||
'VERIFIER',
|
||||
'REDIRECT_URL',
|
||||
// optional data that will be used for the new account on OAuth2 sign-up
|
||||
createData: {
|
||||
'name': 'test',
|
||||
},
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/auth-with-oauth2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>provider</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The name of the OAuth2 client provider (eg. "google").</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>code</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The authorization code returned from the initial request.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>codeVerifier</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The code verifier sent with the initial request as part of the code_challenge.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>redirectUrl</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The redirect url sent with the initial request.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>createData</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">Object</span>
|
||||
</td>
|
||||
<td>
|
||||
<p>Optional data that will be used when creating the auth record on OAuth2 sign-up.</p>
|
||||
<p>
|
||||
The created auth record must comply with the same requirements and validations in the
|
||||
regular <strong>create</strong> action.
|
||||
<br />
|
||||
<em>
|
||||
The data can only be in <code>json</code>, aka. <code>multipart/form-data</code> and files
|
||||
upload currently are not supported during OAuth2 sign-ups.
|
||||
</em>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: allowEmail = collection?.options?.allowEmailAuth;
|
||||
|
||||
$: allowUsername = collection?.options?.allowUsernameAuth;
|
||||
|
||||
$: exampleIdentityLabel =
|
||||
allowUsername && allowEmail
|
||||
? "YOUR_USERNAME_OR_EMAIL"
|
||||
: allowUsername
|
||||
? "YOUR_USERNAME"
|
||||
: "YOUR_EMAIL";
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"identity": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Auth with password ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>
|
||||
Returns new auth token and account data by a combination of
|
||||
<strong>
|
||||
{#if allowUsername && allowEmail}
|
||||
username/email
|
||||
{:else if allowUsername}
|
||||
username
|
||||
{:else if allowEmail}
|
||||
email
|
||||
{/if}
|
||||
</strong>
|
||||
and <strong>password</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').authWithPassword(
|
||||
'${exampleIdentityLabel}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').authWithPassword(
|
||||
'${exampleIdentityLabel}',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.model.id);
|
||||
|
||||
// "logout" the last authenticated account
|
||||
pb.authStore.clear();
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/auth-with-password
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>identity</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
The
|
||||
{#if allowUsername}
|
||||
<strong>username</strong>
|
||||
{/if}
|
||||
{#if allowUsername && allowEmail}
|
||||
or
|
||||
{/if}
|
||||
{#if allowEmail}
|
||||
<strong>email</strong>
|
||||
{/if}
|
||||
of the record to authenticate.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>password</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The auth record password.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,111 +0,0 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import ListApiDocs from "@/components/collections/docs/ListApiDocs.svelte";
|
||||
import ViewApiDocs from "@/components/collections/docs/ViewApiDocs.svelte";
|
||||
import CreateApiDocs from "@/components/collections/docs/CreateApiDocs.svelte";
|
||||
import UpdateApiDocs from "@/components/collections/docs/UpdateApiDocs.svelte";
|
||||
import DeleteApiDocs from "@/components/collections/docs/DeleteApiDocs.svelte";
|
||||
import RealtimeApiDocs from "@/components/collections/docs/RealtimeApiDocs.svelte";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: "list",
|
||||
label: "List",
|
||||
component: ListApiDocs,
|
||||
},
|
||||
{
|
||||
id: "view",
|
||||
label: "View",
|
||||
component: ViewApiDocs,
|
||||
},
|
||||
{
|
||||
id: "create",
|
||||
label: "Create",
|
||||
component: CreateApiDocs,
|
||||
},
|
||||
{
|
||||
id: "update",
|
||||
label: "Update",
|
||||
component: UpdateApiDocs,
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
label: "Delete",
|
||||
component: DeleteApiDocs,
|
||||
},
|
||||
{
|
||||
id: "realtime",
|
||||
label: "Realtime",
|
||||
component: RealtimeApiDocs,
|
||||
},
|
||||
];
|
||||
|
||||
let collectionPanel;
|
||||
let collection = new Collection();
|
||||
let activeTab = tabs[0].id;
|
||||
|
||||
export function show(model) {
|
||||
collection = model;
|
||||
|
||||
changeTab(tabs[0].id);
|
||||
|
||||
return collectionPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return collectionPanel?.hide();
|
||||
}
|
||||
|
||||
export function changeTab(newTab) {
|
||||
activeTab = newTab;
|
||||
}
|
||||
|
||||
function changeTabViaKey(e, newTab) {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
e.preventDefault();
|
||||
changeTab(newTab);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
on:hide
|
||||
on:show
|
||||
class="overlay-panel-xl colored-header collection-panel"
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4><strong>{collection.name}</strong> records API</h4>
|
||||
|
||||
<div class="tabs-header stretched">
|
||||
{#each tabs as tab (tab.id)}
|
||||
<button
|
||||
tabindex="0"
|
||||
class="tab-item"
|
||||
class:active={activeTab === tab.id}
|
||||
on:click={() => changeTab(tab.id)}
|
||||
on:keydown|self={(e) => changeTabViaKey(e, tab.id)}
|
||||
>
|
||||
<span class="txt">{tab.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="tabs-content">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{#if activeTab === tab.id}
|
||||
<div class="tab-item active">
|
||||
<svelte:component this={tab.component} {collection} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" on:click={() => hide()}>
|
||||
<span class="txt">Close</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"token": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Confirm email change ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Confirms <strong>{collection.name}</strong> email change request.</p>
|
||||
<p>Returns the refreshed auth data.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').confirmEmailChange(
|
||||
'TOKEN',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').confirmEmailChange(
|
||||
'TOKEN',
|
||||
'YOUR_PASSWORD',
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/confirm-email-change
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>token</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The token from the change email request email.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>password</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The account password to confirm the email change.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,197 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"token": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Confirm password reset ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Confirms <strong>{collection.name}</strong> password reset request.</p>
|
||||
<p>Returns the refreshed auth data.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').confirmPasswordReset(
|
||||
'TOKEN',
|
||||
'NEW_PASSWORD',
|
||||
'NEW_PASSWORD_CONFIRM',
|
||||
);
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').confirmPasswordReset(
|
||||
'TOKEN',
|
||||
'NEW_PASSWORD',
|
||||
'NEW_PASSWORD_CONFIRM',
|
||||
);
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/confirm-password-reset
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>token</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The token from the password reset request email.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>password</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The new password to set.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>passwordConfirm</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The new password confirmation.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: JSON.stringify(
|
||||
{
|
||||
token: "JWT_TOKEN",
|
||||
record: CommonHelper.dummyCollectionRecord(collection),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"token": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Confirm verification ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Confirms <strong>{collection.name}</strong> account verification request.</p>
|
||||
<p>Returns the refreshed auth data.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${collection?.name}').confirmVerification('TOKEN');
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${collection?.name}').confirmVerification('TOKEN');
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.model.id);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/confirm-verification
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>token</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The token from the verification request email.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>expand</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
let baseData = {};
|
||||
|
||||
$: adminsOnly = collection?.createRule === null;
|
||||
|
||||
@@ -45,8 +46,66 @@
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
$: if (collection.isAuth) {
|
||||
baseData = {
|
||||
username: "test_username",
|
||||
email: "test@exampe.com",
|
||||
emailVisibility: true,
|
||||
password: "12345678",
|
||||
passwordConfirm: "12345678",
|
||||
};
|
||||
} else {
|
||||
baseData = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Create ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Create a new <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
<br />
|
||||
For more info and examples you could check the detailed
|
||||
<a href={import.meta.env.PB_FILE_UPLOAD_DOCS} target="_blank" rel="noopener noreferrer">
|
||||
Files upload and handling docs
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// example create data
|
||||
const data = ${JSON.stringify(Object.assign({}, baseData, CommonHelper.dummyCollectionSchemaData(collection)), null, 4)};
|
||||
|
||||
const record = await pb.collection('${collection?.name}').create(data);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// example create body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({}, baseData, CommonHelper.dummyCollectionSchemaData(collection)), null, 2)};
|
||||
|
||||
final record = await pb.collection('${collection?.name}').create(body: body);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
@@ -55,49 +114,12 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
<p class="txt-hint txt-sm txt-right">Requires admin <code>Authorization:TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Create a new <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const data = { ... };
|
||||
|
||||
const record = await client.records.create('${collection?.name}', data);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final body = <String, dynamic>{ ... };
|
||||
|
||||
final record = await client.records.create('${collection?.name}', body: body);
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -122,6 +144,100 @@
|
||||
If not set, it will be auto generated.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{#if collection?.isAuth}
|
||||
<tr>
|
||||
<td colspan="3" class="txt-hint">Auth fields</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>username</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
The username of the auth record.
|
||||
<br />
|
||||
If not set, it will be auto generated.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
{#if collection?.options?.requireEmail}
|
||||
<span class="label label-success">Required</span>
|
||||
{:else}
|
||||
<span class="label label-warning">Optional</span>
|
||||
{/if}
|
||||
<span>email</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>Auth record email address.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>emailVisibility</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">Boolean</span>
|
||||
</td>
|
||||
<td>Whether to show/hide the auth record email when fetching the record data.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>password</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>Auth record password.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>passwordConfirm</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>Auth record password confirmation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>verified</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">Boolean</span>
|
||||
</td>
|
||||
<td>
|
||||
Indicates whether the auth record is verified or not.
|
||||
<br />
|
||||
This field can be set only by admins or auth records with "Manage" access.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" class="txt-hint">Schema fields</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#each collection?.schema as field (field.name)}
|
||||
<tr>
|
||||
<td>
|
||||
@@ -152,9 +268,7 @@
|
||||
File object.<br />
|
||||
Set to <code>null</code> to delete already uploaded file(s).
|
||||
{:else if field.type === "relation"}
|
||||
Relation record {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
{:else if field.type === "user"}
|
||||
User {field.options?.maxSelect > 1 ? "ids" : "id"}.
|
||||
Relation record {field.options?.maxSelect === 1 ? "id" : "ids"}.
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -163,7 +277,7 @@
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -179,15 +293,12 @@
|
||||
</td>
|
||||
<td>
|
||||
Auto expand relations when returning the created record. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -59,6 +59,33 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Delete ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Delete a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').delete('RECORD_ID');
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').delete('RECORD_ID');
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-danger">
|
||||
<strong class="label label-primary">DELETE</strong>
|
||||
<div class="content">
|
||||
@@ -67,38 +94,12 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
<p class="txt-hint txt-sm txt-right">Requires admin <code>Authorization:TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Delete a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await client.records.delete('${collection?.name}', 'RECORD_ID');
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await client.records.delete('${collection?.name}', 'RECORD_ID');
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Path parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
|
||||
@@ -1,66 +1,86 @@
|
||||
<p>
|
||||
The syntax basically follows the format
|
||||
<code>
|
||||
<span class="txt-success">OPERAND</span>
|
||||
<span class="txt-danger">OPERATOR</span>
|
||||
<span class="txt-success">OPERAND</span></code
|
||||
>, where:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single or double
|
||||
quoted), number, null, true, false
|
||||
</li>
|
||||
<li>
|
||||
<code class="txt-danger">OPERATOR</code> - is one of:
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<code class="filter-op">{"="}</code>
|
||||
<span class="txt-hint">Equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!="}</code>
|
||||
<span class="txt-hint">NOT equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">"}</code>
|
||||
<span class="txt-hint">Greater than</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">="}</code>
|
||||
<span class="txt-hint">Greater than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<"}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<="}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"~"}</code>
|
||||
<span class="txt-hint">
|
||||
Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for wildcard
|
||||
match)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!~"}</code>
|
||||
<span class="txt-hint">
|
||||
NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<script>
|
||||
let expanded = false;
|
||||
|
||||
<p>
|
||||
To group and combine several expressions you could use brackets
|
||||
<code>(...)</code>, <code>&&</code> (AND) and <code>||</code> (OR) tokens.
|
||||
</p>
|
||||
function toggle() {
|
||||
expanded = !expanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="btn btn-sm btn-secondary m-t-5" on:click={toggle}>
|
||||
{#if expanded}
|
||||
<span class="txt">Hide details</span>
|
||||
<i class="ri-arrow-up-s-line" />
|
||||
{:else}
|
||||
<span class="txt">Show details</span>
|
||||
<i class="ri-arrow-down-s-line" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expanded}
|
||||
<p>
|
||||
The syntax basically follows the format
|
||||
<code>
|
||||
<span class="txt-success">OPERAND</span>
|
||||
<span class="txt-danger">OPERATOR</span>
|
||||
<span class="txt-success">OPERAND</span></code
|
||||
>, where:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code class="txt-success">OPERAND</code> - could be any of the above field literal, string (single
|
||||
or double quoted), number, null, true, false
|
||||
</li>
|
||||
<li>
|
||||
<code class="txt-danger">OPERATOR</code> - is one of:
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<code class="filter-op">{"="}</code>
|
||||
<span class="txt-hint">Equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!="}</code>
|
||||
<span class="txt-hint">NOT equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">"}</code>
|
||||
<span class="txt-hint">Greater than</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{">="}</code>
|
||||
<span class="txt-hint">Greater than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<"}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"<="}</code>
|
||||
<span class="txt-hint">Less than or equal</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"~"}</code>
|
||||
<span class="txt-hint">
|
||||
Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<code class="filter-op">{"!~"}</code>
|
||||
<span class="txt-hint">
|
||||
NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for
|
||||
wildcard match)
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
To group and combine several expressions you could use brackets
|
||||
<code>(...)</code>, <code>&&</code> (AND) and <code>||</code> (OR) tokens.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.filter-op {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
{
|
||||
page: 1,
|
||||
perPage: 30,
|
||||
totalPages: 1,
|
||||
totalItems: 2,
|
||||
items: [
|
||||
CommonHelper.dummyCollectionRecord(collection),
|
||||
@@ -70,6 +71,65 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">List/Search ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>
|
||||
Fetch a paginated <strong>{collection.name}</strong> records list, supporting sorting and filtering.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await pb.collection('${collection?.name}').getList(1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
});
|
||||
|
||||
// you can also fetch all records at once via getFullList:
|
||||
const records = await pb.collection('${collection?.name}').getFullList(200 /* batch size */, {
|
||||
sort: '-created'
|
||||
});
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
const record = await pb.collection('${collection?.name}').getFirstListItem('someField="test"', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final result = await pb.collection('${collection?.name}').getList(
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00" && someFiled1 != someField2',
|
||||
);
|
||||
|
||||
// alternatively you can also fetch all records at once via getFullList:
|
||||
final records = await pb.collection('${collection?.name}').getFullList(
|
||||
batch: 200,
|
||||
sort: '-created',
|
||||
);
|
||||
|
||||
// or fetch only the first record that matches the specified filter
|
||||
final record2 = await pb.collection('${collection?.name}').getFirstListItem(
|
||||
'someField="test"',
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
@@ -78,55 +138,12 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
<p class="txt-hint txt-sm txt-right">Requires admin <code>Authorization:TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Fetch a paginated <strong>{collection.name}</strong> records list.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
const resultList = await client.records.getList('${collection?.name}', 1, 50, {
|
||||
filter: 'created >= "2022-01-01 00:00:00"',
|
||||
});
|
||||
|
||||
// alternatively you can also fetch all records at once via getFullList:
|
||||
const records = await client.records.getFullList('${collection?.name}', 200 /* batch size */, {
|
||||
sort: '-created',
|
||||
});
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// fetch a paginated records list
|
||||
final result = await client.records.getList(
|
||||
'${collection?.name}',
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
filter: 'created >= "2022-01-01 00:00:00"',
|
||||
);
|
||||
|
||||
// alternatively you can also fetch all records at once via getFullList:
|
||||
final records = await client.records.getFullList('${collection?.name}', batch: 200, sort: '-created');
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -188,15 +205,12 @@
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to each individual record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 200,
|
||||
body: `
|
||||
[
|
||||
{
|
||||
"id": "8171022dc95a4e8",
|
||||
"created": "2022-09-01 10:24:18.434",
|
||||
"updated": "2022-09-01 10:24:18.889",
|
||||
"recordId": "e22581b6f1d44ea",
|
||||
"collectionId": "${collection.id}",
|
||||
"provider": "google",
|
||||
"providerId": "2da15468800514p",
|
||||
},
|
||||
{
|
||||
"id": "171022dc895a4e8",
|
||||
"created": "2022-09-01 10:24:18.434",
|
||||
"updated": "2022-09-01 10:24:18.889",
|
||||
"recordId": "e22581b6f1d44ea",
|
||||
"collectionId": "${collection.id}",
|
||||
"provider": "twitter",
|
||||
"providerId": "720688005140514",
|
||||
}
|
||||
]
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 401,
|
||||
body: `
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">List OAuth2 accounts ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>
|
||||
Returns a list with all OAuth2 providers linked to a single <strong>{collection.name}</strong>.
|
||||
</p>
|
||||
<p>Only admins and the account owner can access this action.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
const result = await pb.collection('${collection?.name}').listExternalAuths(
|
||||
pb.authStore.model.id
|
||||
);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
final result = await pb.collection('${collection?.name}').listExternalAuths(
|
||||
pb.authStore.model.id,
|
||||
);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records/<strong>:id</strong>/external-auths
|
||||
</p>
|
||||
</div>
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization:TOKEN</code> header</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Path Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>ID of the auth record.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,6 +10,87 @@
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Realtime ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Subscribe to realtime changes via Server-Sent Events (SSE).</p>
|
||||
<p>
|
||||
Events are sent for <strong>create</strong>, <strong>update</strong>
|
||||
and <strong>delete</strong> record operations (see "Event data format" section below).
|
||||
</p>
|
||||
</div>
|
||||
<div class="alert alert-info m-t-10 m-b-sm">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="contet">
|
||||
<p>
|
||||
<strong>You could subscribe to a single record or to an entire collection.</strong>
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to a <strong>single record</strong>, the collection's
|
||||
<strong>ViewRule</strong> will be used to determine whether the subscriber has access to receive the
|
||||
event message.
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to an <strong>entire collection</strong>, the collection's
|
||||
<strong>ListRule</strong> will be used to determine whether the subscriber has access to receive the
|
||||
event message.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// (Optionally) authenticate
|
||||
await pb.collection('users').authWithPassword('test@example.com', '123456');
|
||||
|
||||
// Subscribe to changes in any record from the collection
|
||||
pb.collection('${collection?.name}').subscribe(function (e) {
|
||||
console.log(e.record);
|
||||
});
|
||||
|
||||
// Subscribe to changes in a single record
|
||||
pb.collection('${collection?.name}').subscribeOne('RECORD_ID', function (e) {
|
||||
console.log(e.record);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
pb.collection('${collection?.name}').unsubscribe() // remove all collection subscriptions
|
||||
pb.collection('${collection?.name}').unsubscribe('RECORD_ID') // remove only the record subscription
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// (Optionally) authenticate
|
||||
await pb.collection('users').authWithPassword('test@example.com', '123456');
|
||||
|
||||
// Subscribe to changes in any record from the collection
|
||||
pb.collection('${collection?.name}').subscribe((e) {
|
||||
print(e.record);
|
||||
});
|
||||
|
||||
// Subscribe to changes in a single record
|
||||
pb.collection('${collection?.name}').subscribeOne('RECORD_ID', (e) {
|
||||
print(e.record);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
pb.collection('${collection?.name}').unsubscribe() // remove all collection subscriptions
|
||||
pb.collection('${collection?.name}').unsubscribe('RECORD_ID') // remove only the record subscription
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert">
|
||||
<strong class="label label-primary">SSE</strong>
|
||||
<div class="content">
|
||||
@@ -17,88 +98,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Subscribe to realtime changes via Server-Sent Events (SSE).</p>
|
||||
<p>
|
||||
Events are sent for <strong>create</strong>, <strong>update</strong>
|
||||
and <strong>delete</strong> record operations (see "Event data format" section below).
|
||||
</p>
|
||||
<div class="alert alert-info m-t-10">
|
||||
<div class="icon">
|
||||
<i class="ri-information-line" />
|
||||
</div>
|
||||
<div class="contet">
|
||||
<p>
|
||||
<strong>You could subscribe to a single record or to an entire collection.</strong>
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to a <strong>single record</strong>, the collection's
|
||||
<strong>ViewRule</strong> will be used to determine whether the subscriber has access to receive
|
||||
the event message.
|
||||
</p>
|
||||
<p>
|
||||
When you subscribe to an <strong>entire collection</strong>, the collection's
|
||||
<strong>ListRule</strong> will be used to determine whether the subscriber has access to receive
|
||||
the event message.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// (Optionally) authenticate
|
||||
client.users.authViaEmail('test@example.com', '123456');
|
||||
|
||||
// Subscribe to changes in any record from the collection
|
||||
client.realtime.subscribe('${collection?.name}', function (e) {
|
||||
console.log(e.record);
|
||||
});
|
||||
|
||||
// Subscribe to changes in a single record
|
||||
client.realtime.subscribe('${collection?.name}/RECORD_ID', function (e) {
|
||||
console.log(e.record);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
client.realtime.unsubscribe() // remove all subscriptions
|
||||
client.realtime.unsubscribe('${collection?.name}') // remove only the collection subscription
|
||||
client.realtime.unsubscribe('${collection?.name}/RECORD_ID') // remove only the record subscription
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// (Optionally) authenticate
|
||||
client.users.authViaEmail('test@example.com', '123456');
|
||||
|
||||
// Subscribe to changes in any record from the collection
|
||||
client.realtime.subscribe('${collection?.name}', (e) {
|
||||
print(e.record);
|
||||
});
|
||||
|
||||
// Subscribe to changes in a single record
|
||||
client.realtime.subscribe('${collection?.name}/RECORD_ID', (e) {
|
||||
print(e.record);
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
client.realtime.unsubscribe() // remove all subscriptions
|
||||
client.realtime.unsubscribe('${collection?.name}') // remove only the collection subscription
|
||||
client.realtime.unsubscribe('${collection?.name}/RECORD_ID') // remove only the record subscription
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Event data format</div>
|
||||
<CodeBlock
|
||||
content={JSON.stringify(
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 204;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 204,
|
||||
body: "null",
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"newEmail": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 401,
|
||||
body: `
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Request email change ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Sends <strong>{collection.name}</strong> email change request.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
await pb.collection('${collection?.name}').requestEmailChange('new@example.com');
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
await pb.collection('${collection?.name}').requestEmailChange('new@example.com');
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/confirm-email-change
|
||||
</p>
|
||||
</div>
|
||||
<p class="txt-hint txt-sm txt-right">Requires record <code>Authorization:TOKEN</code> header</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>newEmail</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The new email address to send the change email request.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 204;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 204,
|
||||
body: "null",
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"email": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Request password reset ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Sends <strong>{collection.name}</strong> password reset email request.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').requestPasswordReset('test@example.com');
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').requestPasswordReset('test@example.com');
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/request-password-reset
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>email</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The auth record email address to send the password reset request (if exists).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 204;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 204,
|
||||
body: "null",
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
body: `
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"email": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Request verification ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Sends <strong>{collection.name}</strong> verification email request.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').requestVerification('test@example.com');
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').requestVerification('test@example.com');
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-success">
|
||||
<strong class="label label-primary">POST</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/request-password-reset
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="50%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-success">Required</span>
|
||||
<span>email</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The auth record email address to send the verification request (if exists).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,16 +18,18 @@
|
||||
title: "JavaScript",
|
||||
language: "javascript",
|
||||
content: js,
|
||||
url: import.meta.env.PB_JS_SDK_URL,
|
||||
},
|
||||
{
|
||||
title: "Dart",
|
||||
language: "dart",
|
||||
content: dart,
|
||||
url: import.meta.env.PB_DART_SDK_URL,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="tabs sdk-tabs m-b-lg">
|
||||
<div class="tabs sdk-tabs m-b-base">
|
||||
<div class="tabs-header compact left">
|
||||
{#each sdkExamples as example (example.language)}
|
||||
<button
|
||||
@@ -43,6 +45,13 @@
|
||||
{#each sdkExamples as example (example.language)}
|
||||
<div class="tab-item" class:active={activeTab === example.language}>
|
||||
<CodeBlock language={example.language} content={example.content} />
|
||||
<div class="txt-right">
|
||||
<em class="txt-sm txt-hint">
|
||||
<a href={example.url} target="_blank" rel="noopener noreferrer">
|
||||
{example.title} SDK
|
||||
</a>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<script>
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import CodeBlock from "@/components/base/CodeBlock.svelte";
|
||||
import SdkTabs from "@/components/collections/docs/SdkTabs.svelte";
|
||||
|
||||
export let collection = new Collection();
|
||||
|
||||
let responseTab = 204;
|
||||
let responses = [];
|
||||
|
||||
$: backendAbsUrl = CommonHelper.getApiExampleUrl(ApiClient.baseUrl);
|
||||
|
||||
$: responses = [
|
||||
{
|
||||
code: 204,
|
||||
body: "null",
|
||||
},
|
||||
{
|
||||
code: 401,
|
||||
body: `
|
||||
{
|
||||
"code": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: 404,
|
||||
body: `
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested resource wasn't found.",
|
||||
"data": {}
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Unlink OAuth2 account ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>
|
||||
Unlink a single external OAuth2 provider from <strong>{collection.name}</strong> record.
|
||||
</p>
|
||||
<p>Only admins and the account owner can access this action.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
await pb.collection('${collection?.name}').unlinkExternalAuth(
|
||||
pb.authStore.model.id,
|
||||
'google'
|
||||
);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
await pb.collection('${collection?.name}').authViaEmail('test@example.com', '123456');
|
||||
|
||||
await pb.collection('${collection?.name}').unlinkExternalAuth(
|
||||
pb.authStore.model.id,
|
||||
'google',
|
||||
);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-danger">
|
||||
<strong class="label label-primary">DELETE</strong>
|
||||
<div class="content">
|
||||
<p>
|
||||
/api/collections/<strong>{collection.name}</strong>/records/<strong>:id</strong
|
||||
>/external-auths/<strong>:provider</strong>
|
||||
</p>
|
||||
</div>
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization:TOKEN</code> header</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Path Parameters</div>
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
<th>Type</th>
|
||||
<th width="60%">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>ID of the auth record.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>provider</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
The name of the auth provider to unlink, eg. <code>google</code>, <code>twitter</code>,
|
||||
<code>github</code>, etc.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="section-title">Responses</div>
|
||||
<div class="tabs">
|
||||
<div class="tabs-header compact left">
|
||||
{#each responses as response (response.code)}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={responseTab === response.code}
|
||||
on:click={() => (responseTab = response.code)}
|
||||
>
|
||||
{response.code}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
{#each responses as response (response.code)}
|
||||
<div class="tab-item" class:active={responseTab === response.code}>
|
||||
<CodeBlock content={response.body} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
let responseTab = 200;
|
||||
let responses = [];
|
||||
let baseData = {};
|
||||
|
||||
$: adminsOnly = collection?.updateRule === null;
|
||||
|
||||
@@ -55,8 +56,66 @@
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
$: if (collection.isAuth) {
|
||||
baseData = {
|
||||
username: "test_username_update",
|
||||
emailVisibility: false,
|
||||
password: "87654321",
|
||||
passwordConfirm: "87654321",
|
||||
oldPassword: "12345678",
|
||||
};
|
||||
} else {
|
||||
baseData = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">Update ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Update a single <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
<br />
|
||||
For more info and examples you could check the detailed
|
||||
<a href={import.meta.env.PB_FILE_UPLOAD_DOCS} target="_blank" rel="noopener noreferrer">
|
||||
Files upload and handling docs
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// example update data
|
||||
const data = ${JSON.stringify(Object.assign({}, baseData, CommonHelper.dummyCollectionSchemaData(collection)), null, 4)};
|
||||
|
||||
const record = await pb.collection('${collection?.name}').update('RECORD_ID', data);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
// example update body
|
||||
final body = <String, dynamic>${JSON.stringify(Object.assign({}, baseData, CommonHelper.dummyCollectionSchemaData(collection)), null, 2)};
|
||||
|
||||
final record = await pb.collection('${collection?.name}').update('RECORD_ID', body: body);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-warning">
|
||||
<strong class="label label-primary">PATCH</strong>
|
||||
<div class="content">
|
||||
@@ -65,49 +124,12 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
<p class="txt-hint txt-sm txt-right">Requires admin <code>Authorization:TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Update a single <strong>{collection.name}</strong> record.</p>
|
||||
<p>
|
||||
Body parameters could be sent as <code>application/json</code> or
|
||||
<code>multipart/form-data</code>.
|
||||
</p>
|
||||
<p>
|
||||
File upload is supported only via <code>multipart/form-data</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const data = { ... };
|
||||
|
||||
const record = await client.records.update('${collection?.name}', 'RECORD_ID', data);
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final body = <String, dynamic>{ ... };
|
||||
|
||||
final record = await client.records.update('${collection?.name}', 'RECORD_ID', body: body);
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Path parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -127,7 +149,7 @@
|
||||
</table>
|
||||
|
||||
<div class="section-title">Body Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -136,6 +158,114 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if collection?.isAuth}
|
||||
<tr>
|
||||
<td colspan="3" class="txt-hint">Auth fields</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>username</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>The username of the auth record.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>email</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
The auth record email address.
|
||||
<br />
|
||||
This field can be updated only by admins or auth records with "Manage" access.
|
||||
<br />
|
||||
Regular accounts can update their email by calling "Request email change".
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>emailVisibility</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">Boolean</span>
|
||||
</td>
|
||||
<td>Whether to show/hide the auth record email when fetching the record data.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>oldPassword</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>
|
||||
Old auth record password.
|
||||
<br />
|
||||
This field is required only when changing the record password. Admins and auth records with
|
||||
"Manage" access can skip this field.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>password</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>New auth record password.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>passwordConfirm</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">String</span>
|
||||
</td>
|
||||
<td>New auth record password confirmation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="inline-flex">
|
||||
<span class="label label-warning">Optional</span>
|
||||
<span>verified</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">Boolean</span>
|
||||
</td>
|
||||
<td>
|
||||
Indicates whether the auth record is verified or not.
|
||||
<br />
|
||||
This field can be set only by admins or auth records with "Manage" access.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" class="txt-hint">Schema fields</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#each collection?.schema as field (field.name)}
|
||||
<tr>
|
||||
<td>
|
||||
@@ -193,15 +323,11 @@
|
||||
</td>
|
||||
<td>
|
||||
Auto expand relations when returning the updated record. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField21`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>). Only
|
||||
the relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -46,6 +46,37 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3 class="m-b-sm">View ({collection.name})</h3>
|
||||
<div class="content txt-lg m-b-sm">
|
||||
<p>Fetch a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const record1 = await pb.collection('${collection?.name}').getOne('RECORD_ID', {
|
||||
expand: 'relField1,relField2.subRelField',
|
||||
});
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final record1 = await pb.collection('${collection?.name}').getOne('RECORD_ID',
|
||||
'expand': 'relField1,relField2.subRelField',
|
||||
);
|
||||
`}
|
||||
/>
|
||||
|
||||
<h6 class="m-b-xs">API details</h6>
|
||||
<div class="alert alert-info">
|
||||
<strong class="label label-primary">GET</strong>
|
||||
<div class="content">
|
||||
@@ -54,42 +85,12 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if adminsOnly}
|
||||
<p class="txt-hint txt-sm txt-right">Requires <code>Authorization: Admin TOKEN</code> header</p>
|
||||
<p class="txt-hint txt-sm txt-right">Requires admin <code>Authorization:TOKEN</code> header</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content m-b-base">
|
||||
<p>Fetch a single <strong>{collection.name}</strong> record.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Client SDKs example</div>
|
||||
<SdkTabs
|
||||
js={`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const client = new PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
const record = await client.records.getOne('${collection?.name}', 'RECORD_ID', {
|
||||
expand: 'some_relation'
|
||||
});
|
||||
`}
|
||||
dart={`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final client = PocketBase('${backendAbsUrl}');
|
||||
|
||||
...
|
||||
|
||||
final record = await client.records.getOne('${collection?.name}', 'RECORD_ID', query: {
|
||||
'expand': 'some_relation',
|
||||
});
|
||||
`}
|
||||
/>
|
||||
|
||||
<div class="section-title">Path Parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -109,7 +110,7 @@
|
||||
</table>
|
||||
|
||||
<div class="section-title">Query parameters</div>
|
||||
<table class="table-compact table-border m-b-lg">
|
||||
<table class="table-compact table-border m-b-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Param</th>
|
||||
@@ -125,15 +126,12 @@
|
||||
</td>
|
||||
<td>
|
||||
Auto expand record relations. Ex.:
|
||||
<CodeBlock
|
||||
content={`
|
||||
?expand=rel1,rel2.subrel21.subrel22
|
||||
`}
|
||||
/>
|
||||
<CodeBlock content={`?expand=relField1,relField2.subRelField`} />
|
||||
Supports up to 6-levels depth nested relations expansion. <br />
|
||||
The expanded relations will be appended to the record under the
|
||||
<code>@expand</code> property (eg. <code>{`"@expand": {"rel1": {...}, ...}`}</code>). Only the
|
||||
relations that the user has permissions to <strong>view</strong> will be expanded.
|
||||
<code>expand</code> property (eg. <code>{`"expand": {"relField1": {...}, ...}`}</code>).
|
||||
<br />
|
||||
Only the relations to which the account has permissions to <strong>view</strong> will be expanded.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
icon: CommonHelper.getFieldTypeIcon("date"),
|
||||
},
|
||||
{
|
||||
label: "Multiple choices",
|
||||
label: "Select",
|
||||
value: "select",
|
||||
icon: CommonHelper.getFieldTypeIcon("select"),
|
||||
},
|
||||
@@ -58,17 +58,11 @@
|
||||
value: "relation",
|
||||
icon: CommonHelper.getFieldTypeIcon("relation"),
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: "user",
|
||||
icon: CommonHelper.getFieldTypeIcon("user"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<ObjectSelect
|
||||
class="field-type-select {classes}"
|
||||
searchable
|
||||
items={types}
|
||||
bind:keyOfSelected={value}
|
||||
{...$$restProps}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let options = {};
|
||||
@@ -14,6 +16,7 @@
|
||||
|
||||
let isLoading = false;
|
||||
let collections = [];
|
||||
let upsertPanel = null;
|
||||
|
||||
// load defaults
|
||||
$: if (CommonHelper.isEmpty(options)) {
|
||||
@@ -26,19 +29,20 @@
|
||||
|
||||
loadCollections();
|
||||
|
||||
function loadCollections() {
|
||||
async function loadCollections() {
|
||||
isLoading = true;
|
||||
|
||||
ApiClient.collections.getFullList(200, { sort: "-created" })
|
||||
.then((items) => {
|
||||
collections = items;
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading = false;
|
||||
try {
|
||||
const result = await ApiClient.collections.getFullList(200, {
|
||||
sort: "created",
|
||||
});
|
||||
|
||||
collections = CommonHelper.sortCollections(result);
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -53,13 +57,32 @@
|
||||
selectionKey="id"
|
||||
items={collections}
|
||||
bind:keyOfSelected={options.collectionId}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-block btn-sm m-t-5"
|
||||
on:click={() => upsertPanel?.show()}
|
||||
>
|
||||
<span class="txt">New collection</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<Field class="form-field required" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>Max select</label>
|
||||
<input type="number" id={uniqueId} step="1" min="1" required bind:value={options.maxSelect} />
|
||||
<Field class="form-field" name="schema.{key}.options.maxSelect" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Max select</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Leave empty for no limit.",
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<input type="number" id={uniqueId} step="1" min="1" bind:value={options.maxSelect} />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
@@ -69,3 +92,13 @@
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollectionUpsertPanel
|
||||
bind:this={upsertPanel}
|
||||
on:save={(e) => {
|
||||
if (e?.detail?.collection?.id) {
|
||||
options.collectionId = e.detail.collection.id;
|
||||
}
|
||||
loadCollections();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { onMount } from "svelte";
|
||||
import { scale } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
@@ -44,7 +43,7 @@
|
||||
resetData();
|
||||
for (let item of result) {
|
||||
chartData.push({
|
||||
x: CommonHelper.getDateTime(item.date).toLocal().toJSDate(),
|
||||
x: new Date(item.date),
|
||||
y: item.total,
|
||||
});
|
||||
totalRequests += item.total;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import HorizontalScroller from "@/components/base/HorizontalScroller.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const labelMethodClass = {
|
||||
@@ -21,6 +22,7 @@
|
||||
let currentPage = 1;
|
||||
let totalItems = 0;
|
||||
let isLoading = false;
|
||||
let yieldedItemsId = 0;
|
||||
|
||||
$: if (typeof sort !== "undefined" || typeof filter !== "undefined" || typeof presets !== "undefined") {
|
||||
clearList();
|
||||
@@ -29,24 +31,39 @@
|
||||
|
||||
$: canLoadMore = totalItems > items.length;
|
||||
|
||||
export async function load(page = 1) {
|
||||
export async function load(page = 1, breakTasks = true) {
|
||||
isLoading = true;
|
||||
|
||||
return ApiClient.logs.getRequestsList(page, 40, {
|
||||
sort: sort,
|
||||
filter: [presets, filter].filter(Boolean).join("&&"),
|
||||
})
|
||||
.then((result) => {
|
||||
return ApiClient.logs
|
||||
.getRequestsList(page, 30, {
|
||||
sort: sort,
|
||||
filter: [presets, filter].filter(Boolean).join("&&"),
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (page <= 1) {
|
||||
clearList();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
items = items.concat(result.items);
|
||||
currentPage = result.page;
|
||||
totalItems = result.totalItems;
|
||||
dispatch("load", items.concat(result.items));
|
||||
|
||||
dispatch("load", items);
|
||||
// optimize the items listing by rendering the rows in task batches
|
||||
if (breakTasks) {
|
||||
const currentYieldId = ++yieldedItemsId;
|
||||
while (result.items.length) {
|
||||
if (yieldedItemsId != currentYieldId) {
|
||||
break; // new yeild has been started
|
||||
}
|
||||
|
||||
items = items.concat(result.items.splice(0, 10));
|
||||
|
||||
await CommonHelper.yieldToMain();
|
||||
}
|
||||
} else {
|
||||
items = items.concat(result.items);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.isAbort) {
|
||||
@@ -65,7 +82,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<HorizontalScroller class="table-wrapper">
|
||||
<table class="table" class:table-loading={isLoading}>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -90,6 +107,13 @@
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-number col-field-userIp" name="userIp" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("number")} />
|
||||
<span class="txt">User IP</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<SortHeader disable class="col-type-number col-field-status" name="status" bind:sort>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon("number")} />
|
||||
@@ -103,6 +127,7 @@
|
||||
<span class="txt">created</span>
|
||||
</div>
|
||||
</SortHeader>
|
||||
|
||||
<th class="col-type-action min-width" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -140,6 +165,12 @@
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="col-type-number col-field-userIp">
|
||||
<span class="txt txt-ellipsis" class:txt-hint={!item.userIp} title={item.userIp}>
|
||||
{item.userIp || "N/A"}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="col-type-number col-field-status">
|
||||
<span class="label" class:label-danger={item.status >= 400}>
|
||||
{item.status}
|
||||
@@ -180,7 +211,7 @@
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</HorizontalScroller>
|
||||
|
||||
{#if items.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {items.length} of {totalItems}</small>
|
||||
|
||||
+10
-12
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let user;
|
||||
export let record;
|
||||
|
||||
let externalAuths = [];
|
||||
let isLoading = false;
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
async function loadExternalAuths() {
|
||||
if (!user?.id) {
|
||||
if (!record?.id) {
|
||||
externalAuths = [];
|
||||
isLoading = false;
|
||||
return;
|
||||
@@ -31,7 +31,7 @@
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
externalAuths = await ApiClient.users.listExternalAuths(user.id);
|
||||
externalAuths = await ApiClient.collection(record.collectionId).listExternalAuths(record.id);
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
@@ -40,13 +40,13 @@
|
||||
}
|
||||
|
||||
function unlinkExternalAuth(provider) {
|
||||
if (!user?.id || !provider) {
|
||||
if (!record?.id || !provider) {
|
||||
return; // nothing to unlink
|
||||
}
|
||||
|
||||
confirm(`Do you really want to unlink the ${getProviderTitle(provider)} provider?`, () => {
|
||||
return ApiClient.users
|
||||
.unlinkExternalAuth(user.id, provider)
|
||||
return ApiClient.collection(record.collectionId)
|
||||
.unlinkExternalAuth(record.id, provider)
|
||||
.then(() => {
|
||||
addSuccessToast(`Successfully unlinked the ${getProviderTitle(provider)} provider.`);
|
||||
dispatch("unlink", provider);
|
||||
@@ -58,16 +58,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadExternalAuths();
|
||||
});
|
||||
loadExternalAuths();
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="block txt-center">
|
||||
<span class="loader" />
|
||||
</div>
|
||||
{:else if user?.id && externalAuths.length}
|
||||
{:else if record?.id && externalAuths.length}
|
||||
<div class="list">
|
||||
{#each externalAuths as auth}
|
||||
<div class="list-item">
|
||||
@@ -85,5 +83,5 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="txt-hint txt-center">No authorized OAuth2 providers.</p>
|
||||
<p class="txt-hint txt-center">No linked OAuth2 providers.</p>
|
||||
{/if}
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import PocketBase from "pocketbase";
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
@@ -24,7 +24,8 @@
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
await client.users.confirmEmailChange(params?.token, password);
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client.collection(payload.collectionId).confirmEmailChange(params?.token, password);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import PocketBase from "pocketbase";
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
@@ -25,7 +25,10 @@
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
await client.users.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client
|
||||
.collection(payload.collectionId)
|
||||
.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import PocketBase from "pocketbase";
|
||||
import PocketBase, { getTokenPayload } from "pocketbase";
|
||||
import FullPage from "@/components/base/FullPage.svelte";
|
||||
|
||||
export let params;
|
||||
@@ -16,7 +16,8 @@
|
||||
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
|
||||
|
||||
try {
|
||||
await client.users.confirmVerification(params?.token);
|
||||
const payload = getTokenPayload(params?.token);
|
||||
await client.collection(payload.collectionId).confirmVerification(params?.token);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
success = false;
|
||||
@@ -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>
|
||||
@@ -82,13 +82,6 @@
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<Field class="form-field" name="{key}.allowRegistrations" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={config.allowRegistrations} />
|
||||
<label for={uniqueId}>Allow registration for new users</label>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{#if showSelfHostedFields}
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title">Optional endpoints (if you self host the OAUTH2 service)</div>
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
<script>
|
||||
import { scale, slide } from "svelte/transition";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { errors } from "@/stores/errors";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
|
||||
export let config = {}; // EmailAuthConfig
|
||||
|
||||
let accordion;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty($errors?.emailAuth);
|
||||
|
||||
export function expand() {
|
||||
accordion?.expand();
|
||||
}
|
||||
|
||||
export function collapse() {
|
||||
accordion?.collapse();
|
||||
}
|
||||
|
||||
export function collapseSiblings() {
|
||||
accordion?.collapseSiblings();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion bind:this={accordion} on:expand on:collapse on:toggle {...$$restProps}>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-mail-lock-line" />
|
||||
<span class="txt">Email/Password</span>
|
||||
</div>
|
||||
|
||||
{#if config.enabled}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-0" name="emailAuth.enabled" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={config.enabled} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
{#if config.enabled}
|
||||
<div class="grid" transition:slide|local={{ duration: 150 }}>
|
||||
<div class="col-sm-12 m-t-sm">
|
||||
<Field class="form-field required" name="emailAuth.minPasswordLength" let:uniqueId>
|
||||
<label for={uniqueId}>Minimum password length</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
required
|
||||
min="5"
|
||||
max="200"
|
||||
bind:value={config.minPasswordLength}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field
|
||||
class="form-field {!CommonHelper.isEmpty(config.onlyDomains) ? 'disabled' : ''}"
|
||||
name="emailAuth.exceptDomains"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Except domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Email domains that are NOT allowed to sign up. \n This field is disabled if "Only domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
disabled={!CommonHelper.isEmpty(config.onlyDomains)}
|
||||
bind:value={config.exceptDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field
|
||||
class="form-field {!CommonHelper.isEmpty(config.exceptDomains) ? 'disabled' : ''}"
|
||||
name="emailAuth.onlyDomains"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for="{uniqueId}.config.onlyDomains">
|
||||
<span class="txt">Only domains</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: 'Email domains that are ONLY allowed to sign up. \n This field is disabled if "Except domains" is set.',
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<MultipleValueInput
|
||||
id="{uniqueId}.config.onlyDomains"
|
||||
disabled={!CommonHelper.isEmpty(config.exceptDomains)}
|
||||
bind:value={config.onlyDomains}
|
||||
/>
|
||||
<div class="help-block">Use comma as separator.</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Accordion>
|
||||
@@ -1,3 +1,7 @@
|
||||
<script context="module">
|
||||
let cachedEditorComponent;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { scale } from "svelte/transition";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
@@ -12,7 +16,7 @@
|
||||
export let config = {};
|
||||
|
||||
let accordion;
|
||||
let editorComponent;
|
||||
let editorComponent = cachedEditorComponent;
|
||||
let isEditorComponentLoading = false;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, key));
|
||||
@@ -42,6 +46,8 @@
|
||||
|
||||
editorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
|
||||
|
||||
cachedEditorComponent = editorComponent;
|
||||
|
||||
isEditorComponentLoading = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import EmailAuthAccordion from "@/components/settings/EmailAuthAccordion.svelte";
|
||||
import AuthProviderAccordion from "@/components/settings/AuthProviderAccordion.svelte";
|
||||
import providersList from "@/providers.js";
|
||||
|
||||
$pageTitle = "Auth providers";
|
||||
|
||||
let emailAuthAccordion;
|
||||
let accordions = {};
|
||||
let originalFormSettings = {};
|
||||
let formSettings = {};
|
||||
let isLoading = false;
|
||||
@@ -48,7 +47,8 @@
|
||||
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
initSettings(result);
|
||||
setErrors({});
|
||||
emailAuthAccordion?.collapseSiblings();
|
||||
|
||||
accordions[Object.keys(accordions)[0]]?.collapseSiblings();
|
||||
addSuccessToast("Successfully updated auth providers.");
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
@@ -60,15 +60,10 @@
|
||||
function initSettings(data) {
|
||||
data = data || {};
|
||||
|
||||
formSettings = {
|
||||
emailAuth: Object.assign({ enabled: true }, data.emailAuth),
|
||||
};
|
||||
formSettings = {};
|
||||
|
||||
for (const providerKey in providersList) {
|
||||
formSettings[providerKey] = Object.assign(
|
||||
{ enabled: false, allowRegistrations: true },
|
||||
data[providerKey]
|
||||
);
|
||||
formSettings[providerKey] = Object.assign({ enabled: false }, data[providerKey]);
|
||||
}
|
||||
|
||||
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
|
||||
@@ -97,14 +92,9 @@
|
||||
<div class="loader" />
|
||||
{:else}
|
||||
<div class="accordions">
|
||||
<EmailAuthAccordion
|
||||
bind:this={emailAuthAccordion}
|
||||
single
|
||||
bind:config={formSettings.emailAuth}
|
||||
/>
|
||||
|
||||
{#each Object.entries(providersList) as [key, provider]}
|
||||
<AuthProviderAccordion
|
||||
bind:this={accordions[key]}
|
||||
single
|
||||
{key}
|
||||
title={provider.title}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
@@ -42,24 +43,6 @@
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function testS3() {
|
||||
testS3Error = null;
|
||||
|
||||
if (!formSettings.s3.enabled) {
|
||||
return; // nothing to test
|
||||
}
|
||||
|
||||
isTesting = true;
|
||||
|
||||
try {
|
||||
await ApiClient.settings.testS3({ $cancelKey: testRequestKey });
|
||||
} catch (err) {
|
||||
testS3Error = err;
|
||||
}
|
||||
|
||||
isTesting = false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving || !hasChanges) {
|
||||
return;
|
||||
@@ -67,13 +50,6 @@
|
||||
|
||||
isSaving = true;
|
||||
|
||||
// auto cancel the test request after 30sec
|
||||
clearTimeout(testS3TimeoutId);
|
||||
testS3TimeoutId = setTimeout(() => {
|
||||
ApiClient.cancelRequest(testRequestKey);
|
||||
addErrorToast("S3 test connection timeout.");
|
||||
}, 30000);
|
||||
|
||||
try {
|
||||
ApiClient.cancelRequest(testRequestKey);
|
||||
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
@@ -92,8 +68,6 @@
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
clearTimeout(testS3TimeoutId);
|
||||
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
@@ -111,6 +85,39 @@
|
||||
|
||||
await testS3();
|
||||
}
|
||||
|
||||
async function testS3() {
|
||||
testS3Error = null;
|
||||
|
||||
if (!formSettings.s3.enabled) {
|
||||
return; // nothing to test
|
||||
}
|
||||
|
||||
// auto cancel the test request after 30sec
|
||||
ApiClient.cancelRequest(testRequestKey);
|
||||
clearTimeout(testS3TimeoutId);
|
||||
testS3TimeoutId = setTimeout(() => {
|
||||
ApiClient.cancelRequest(testRequestKey);
|
||||
addErrorToast("S3 test connection timeout.");
|
||||
}, 30000);
|
||||
|
||||
isTesting = true;
|
||||
|
||||
try {
|
||||
await ApiClient.settings.testS3({ $cancelKey: testRequestKey });
|
||||
} catch (err) {
|
||||
testS3Error = err;
|
||||
}
|
||||
|
||||
isTesting = false;
|
||||
clearTimeout(testS3TimeoutId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
clearTimeout(testS3TimeoutId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingsSidebar />
|
||||
@@ -160,7 +167,7 @@
|
||||
<a
|
||||
href="https://github.com/rclone/rclone"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
class="txt-bold"
|
||||
>
|
||||
rclone
|
||||
@@ -168,7 +175,7 @@
|
||||
<a
|
||||
href="https://github.com/peak/s5cmd"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
class="txt-bold"
|
||||
>
|
||||
s5cmd
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
|
||||
const tokensList = [
|
||||
{ key: "userAuthToken", label: "Users auth token" },
|
||||
{ key: "userVerificationToken", label: "Users email verification token" },
|
||||
{ key: "userPasswordResetToken", label: "Users password reset token" },
|
||||
{ key: "userEmailChangeToken", label: "Users email change token" },
|
||||
{ key: "recordAuthToken", label: "Auth record authentication token" },
|
||||
{ key: "recordVerificationToken", label: "Auth record email verification token" },
|
||||
{ key: "recordPasswordResetToken", label: "Auth record password reset token" },
|
||||
{ key: "recordEmailChangeToken", label: "Auth record email change token" },
|
||||
{ key: "adminAuthToken", label: "Admins auth token" },
|
||||
{ key: "adminPasswordResetToken", label: "Admins password reset token" },
|
||||
];
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
<script>
|
||||
import { replace, querystring } from "svelte-spa-router";
|
||||
import { Collection } from "pocketbase";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { pageTitle, hideControls } from "@/stores/app";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import Searchbar from "@/components/base/Searchbar.svelte";
|
||||
import RefreshButton from "@/components/base/RefreshButton.svelte";
|
||||
import SortHeader from "@/components/base/SortHeader.svelte";
|
||||
import IdLabel from "@/components/base/IdLabel.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import UserUpsertPanel from "@/components/users/UserUpsertPanel.svelte";
|
||||
import CollectionUpsertPanel from "@/components/collections/CollectionUpsertPanel.svelte";
|
||||
import RecordUpsertPanel from "@/components/records/RecordUpsertPanel.svelte";
|
||||
import RecordFieldCell from "@/components/records/RecordFieldCell.svelte";
|
||||
|
||||
$pageTitle = "Users";
|
||||
|
||||
const queryParams = new URLSearchParams($querystring);
|
||||
const excludedProfileFields = ["id", "userId", "created", "updated"];
|
||||
|
||||
let userUpsertPanel;
|
||||
let collectionUpsertPanel;
|
||||
let recordUpsertPanel;
|
||||
let users = [];
|
||||
let currentPage = 1;
|
||||
let totalItems = 0;
|
||||
let isLoadingUsers = false;
|
||||
let filter = queryParams.get("filter") || "";
|
||||
let sort = queryParams.get("sort") || "-created";
|
||||
let profileCollection = new Collection();
|
||||
let isLoadingProfileCollection = false;
|
||||
|
||||
$: if (sort !== -1 && filter !== -1) {
|
||||
// keep query params
|
||||
const query = new URLSearchParams({ filter, sort }).toString();
|
||||
replace("/users?" + query);
|
||||
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
$: canLoadMore = totalItems > users.length;
|
||||
|
||||
$: profileFields = profileCollection?.schema?.filter(
|
||||
(field) => !excludedProfileFields.includes(field.name)
|
||||
);
|
||||
|
||||
loadProfilesCollection();
|
||||
|
||||
export async function loadUsers(page = 1) {
|
||||
isLoadingUsers = true;
|
||||
|
||||
if (page <= 1) {
|
||||
clearList();
|
||||
}
|
||||
|
||||
return ApiClient.users
|
||||
.getList(page, 50, {
|
||||
sort: sort || "-created",
|
||||
filter: filter,
|
||||
})
|
||||
.then((result) => {
|
||||
isLoadingUsers = false;
|
||||
users = users.concat(result.items);
|
||||
currentPage = result.page;
|
||||
totalItems = result.totalItems;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.isAbort) {
|
||||
isLoadingUsers = false;
|
||||
console.warn(err);
|
||||
clearList();
|
||||
ApiClient.errorResponseHandler(err, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearList() {
|
||||
users = [];
|
||||
currentPage = 1;
|
||||
totalItems = 0;
|
||||
}
|
||||
|
||||
function setUserProfile(profile) {
|
||||
const user = users.find((u) => u.id === profile?.userId);
|
||||
if (user) {
|
||||
user.profile = profile;
|
||||
}
|
||||
users = users;
|
||||
}
|
||||
|
||||
async function loadProfilesCollection() {
|
||||
isLoadingProfileCollection = true;
|
||||
|
||||
try {
|
||||
profileCollection = await ApiClient.collections.getOne(import.meta.env.PB_PROFILE_COLLECTION);
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingProfileCollection = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<PageWrapper>
|
||||
{#if isLoadingProfileCollection}
|
||||
<div class="placeholder-section m-b-base">
|
||||
<span class="loader loader-lg" />
|
||||
<h1>Loading users...</h1>
|
||||
</div>
|
||||
{:else}
|
||||
<header class="page-header">
|
||||
<nav class="breadcrumbs">
|
||||
<div class="breadcrumb-item">{$pageTitle}</div>
|
||||
</nav>
|
||||
|
||||
{#if !$hideControls}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-circle"
|
||||
use:tooltip={{ text: "Edit profile collection", position: "right" }}
|
||||
on:click={() => collectionUpsertPanel?.show(profileCollection)}
|
||||
>
|
||||
<i class="ri-settings-4-line" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<RefreshButton on:refresh={() => loadUsers()} />
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
<button type="button" class="btn btn-expanded" on:click={() => userUpsertPanel?.show()}>
|
||||
<i class="ri-add-line" />
|
||||
<span class="txt">New user</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<Searchbar
|
||||
value={filter}
|
||||
placeholder={"Search filter, eg. verified=1"}
|
||||
extraAutocompleteKeys={["verified", "email"]}
|
||||
on:submit={(e) => (filter = e.detail)}
|
||||
/>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" class:table-loading={isLoadingUsers}>
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{#each profileFields as field (field.name)}
|
||||
<th class="col-type-{field.type} col-field-{field.name}" name={field.name}>
|
||||
<div class="col-header-content">
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">profile.{field.name}</span>
|
||||
</div>
|
||||
</th>
|
||||
{/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" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user (user.id)}
|
||||
<tr>
|
||||
<td class="col-type-text col-field-id">
|
||||
<IdLabel id={user.id} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-email col-field-email">
|
||||
<div class="inline-flex">
|
||||
{#if user.email}
|
||||
<span class="txt" title={user.email}>{user.email}</span>
|
||||
<span
|
||||
class="label"
|
||||
class:label-success={user.verified}
|
||||
class:label-warning={!user.verified}
|
||||
>
|
||||
{user.verified ? "Verified" : "Unverified"}
|
||||
</span>
|
||||
{:else}
|
||||
<div class="txt-hint">N/A</div>
|
||||
{#if user.verified}
|
||||
<span class="label label-success">OAuth2 verified</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{#each profileFields as field (field.name)}
|
||||
<RecordFieldCell {field} record={user.profile || {}} />
|
||||
{/each}
|
||||
|
||||
<td class="col-type-date col-field-created">
|
||||
<FormattedDate date={user.created} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-date col-field-updated">
|
||||
<FormattedDate date={user.updated} />
|
||||
</td>
|
||||
|
||||
<td class="col-type-action min-width">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline"
|
||||
on:click|stopPropagation={() => userUpsertPanel?.show(user)}
|
||||
>
|
||||
<i class="ri-user-settings-line" />
|
||||
<span class="txt">Edit user</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm m-l-10"
|
||||
on:click|stopPropagation={() => recordUpsertPanel?.show(user.profile)}
|
||||
>
|
||||
<i class="ri-profile-line" />
|
||||
<span class="txt">Edit profile</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#if isLoadingUsers}
|
||||
<tr>
|
||||
<td colspan="99" class="p-xs">
|
||||
<span class="skeleton-loader" />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="99" class="txt-center txt-hint p-xs">
|
||||
<h6>No users found.</h6>
|
||||
{#if filter?.length}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-hint btn-expanded m-t-sm"
|
||||
on:click={() => (filter = "")}
|
||||
>
|
||||
<span class="txt">Clear filters</span>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#if users.length}
|
||||
<small class="block txt-hint txt-right m-t-sm">Showing {users.length} of {totalItems}</small>
|
||||
{/if}
|
||||
|
||||
{#if users.length && canLoadMore}
|
||||
<div class="block txt-center m-t-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-lg btn-secondary btn-expanded"
|
||||
class:btn-loading={isLoadingUsers}
|
||||
class:btn-disabled={isLoadingUsers}
|
||||
on:click={() => loadUsers(currentPage + 1)}
|
||||
>
|
||||
<span class="txt">Load more ({totalItems - users.length})</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</PageWrapper>
|
||||
|
||||
<UserUpsertPanel bind:this={userUpsertPanel} on:save={() => loadUsers()} on:delete={() => loadUsers()} />
|
||||
|
||||
<CollectionUpsertPanel bind:this={collectionUpsertPanel} on:save={(e) => (profileCollection = e.detail)} />
|
||||
|
||||
<RecordUpsertPanel
|
||||
bind:this={recordUpsertPanel}
|
||||
collection={profileCollection}
|
||||
on:save={(e) => setUserProfile(e.detail)}
|
||||
/>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import UserSelectOption from "./UserSelectOption.svelte";
|
||||
|
||||
const uniqueId = "select_" + CommonHelper.randomString(5);
|
||||
|
||||
// original select props
|
||||
export let multiple = false;
|
||||
export let selected = multiple ? [] : undefined;
|
||||
export let keyOfSelected = multiple ? [] : undefined;
|
||||
export let selectPlaceholder = "- Select -";
|
||||
export let optionComponent = UserSelectOption; // custom component to use for each dropdown option item
|
||||
|
||||
let list = [];
|
||||
let currentPage = 1;
|
||||
let totalItems = 0;
|
||||
let isLoadingList = false;
|
||||
let isLoadingSelected = false;
|
||||
|
||||
$: isLoading = isLoadingList || isLoadingSelected;
|
||||
|
||||
$: canLoadMore = totalItems > list.length;
|
||||
|
||||
loadList(true).then(() => {
|
||||
loadSelected();
|
||||
});
|
||||
|
||||
async function loadSelected() {
|
||||
const selectedIds = CommonHelper.toArray(keyOfSelected);
|
||||
if (!selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingSelected = true;
|
||||
|
||||
try {
|
||||
const filters = [];
|
||||
for (const id of selectedIds) {
|
||||
filters.push(`id="${id}"`);
|
||||
}
|
||||
|
||||
const result = await ApiClient.users.getFullList(100, {
|
||||
filter: filters.join("||"),
|
||||
$cancelKey: uniqueId + "loadSelected",
|
||||
});
|
||||
|
||||
// preserve selected order
|
||||
selected = [];
|
||||
for (const id of selectedIds) {
|
||||
const item = CommonHelper.findByKey(result, "id", id);
|
||||
if (item) {
|
||||
selected.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// add the selected models to the list (if not already)
|
||||
list = CommonHelper.filterDuplicatesByKey(selected.concat(list));
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingSelected = false;
|
||||
}
|
||||
|
||||
async function loadList(reset = false) {
|
||||
isLoadingList = true;
|
||||
|
||||
try {
|
||||
const page = reset ? 1 : currentPage + 1;
|
||||
|
||||
const result = await ApiClient.users.getList(page, 200, {
|
||||
sort: "-created",
|
||||
$cancelKey: uniqueId + "loadList",
|
||||
});
|
||||
|
||||
if (reset) {
|
||||
list = CommonHelper.toArray(selected).slice();
|
||||
}
|
||||
|
||||
list = CommonHelper.filterDuplicatesByKey(
|
||||
list.concat(result.items, CommonHelper.toArray(selected))
|
||||
);
|
||||
currentPage = result.page;
|
||||
totalItems = result.totalItems;
|
||||
} catch (err) {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
}
|
||||
|
||||
isLoadingList = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ObjectSelect
|
||||
selectPlaceholder={isLoading ? "Loading..." : selectPlaceholder}
|
||||
items={list}
|
||||
searchable={list.length > 5}
|
||||
selectionKey="id"
|
||||
labelComponent={UserSelectOption}
|
||||
{optionComponent}
|
||||
{multiple}
|
||||
bind:keyOfSelected
|
||||
bind:selected
|
||||
on:show
|
||||
on:hide
|
||||
class="users-select block-options"
|
||||
{...$$restProps}
|
||||
>
|
||||
<svelte:fragment slot="afterOptions">
|
||||
{#if canLoadMore}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-block btn-sm"
|
||||
class:btn-loading={isLoadingList}
|
||||
class:btn-disabled={isLoadingList}
|
||||
on:click|stopPropagation={() => loadList()}
|
||||
>
|
||||
<span class="txt">Load more</span>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ObjectSelect>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
|
||||
export let item = {}; // model
|
||||
</script>
|
||||
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{ text: JSON.stringify(item, null, 2), position: "left", class: "code" }}
|
||||
/>
|
||||
|
||||
<div class="content">
|
||||
<div class="block txt-ellipsis">{item.id}</div>
|
||||
{#if item.email}
|
||||
<small class="block txt-hint txt-ellipsis">{item.email}</small>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,308 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { slide } from "svelte/transition";
|
||||
import { User } 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 ExternalAuthsList from "./ExternalAuthsList.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const formId = "user_" + CommonHelper.randomString(5);
|
||||
const accountTab = "account";
|
||||
const providersTab = "providers";
|
||||
|
||||
let panel;
|
||||
let user = new User();
|
||||
let isSaving = false;
|
||||
let confirmClose = false; // prevent close recursion
|
||||
let email = "";
|
||||
let password = "";
|
||||
let passwordConfirm = "";
|
||||
let changePasswordToggle = false;
|
||||
let verificationEmailToggle = true;
|
||||
let activeTab = accountTab;
|
||||
|
||||
$: hasChanges = (user.isNew && email != "") || changePasswordToggle || email !== user.email;
|
||||
|
||||
export function show(model) {
|
||||
load(model);
|
||||
|
||||
confirmClose = true;
|
||||
|
||||
activeTab = user.isNew || user.email ? accountTab : providersTab;
|
||||
|
||||
return panel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return panel?.hide();
|
||||
}
|
||||
|
||||
function load(model) {
|
||||
setErrors({}); // reset errors
|
||||
|
||||
user = model?.clone ? model.clone() : new User();
|
||||
|
||||
reset(); // reset form
|
||||
}
|
||||
|
||||
function reset() {
|
||||
changePasswordToggle = false;
|
||||
verificationEmailToggle = true;
|
||||
email = user?.email || "";
|
||||
password = "";
|
||||
passwordConfirm = "";
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (isSaving || !hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
const data = { email: email };
|
||||
if (user.isNew || changePasswordToggle) {
|
||||
data["password"] = password;
|
||||
data["passwordConfirm"] = passwordConfirm;
|
||||
}
|
||||
|
||||
let request;
|
||||
if (user.isNew) {
|
||||
request = ApiClient.users.create(data);
|
||||
} else {
|
||||
request = ApiClient.users.update(user.id, data);
|
||||
}
|
||||
|
||||
request
|
||||
.then(async (result) => {
|
||||
user = result;
|
||||
|
||||
if (verificationEmailToggle) {
|
||||
sendVerificationEmail(false);
|
||||
}
|
||||
|
||||
confirmClose = false;
|
||||
hide();
|
||||
addSuccessToast(user.isNew ? "Successfully created user." : "Successfully updated user.");
|
||||
dispatch("save", result);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
})
|
||||
.finally(() => {
|
||||
isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteConfirm() {
|
||||
if (!user?.id) {
|
||||
return; // nothing to delete
|
||||
}
|
||||
|
||||
confirm(`Do you really want to delete the selected user?`, () => {
|
||||
return ApiClient.users
|
||||
.delete(user.id)
|
||||
.then(() => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
addSuccessToast("Successfully deleted user.");
|
||||
dispatch("delete", user);
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendVerificationEmail(notify = true) {
|
||||
return ApiClient.users
|
||||
.requestVerification(user.email || email)
|
||||
.then(() => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
if (notify) {
|
||||
addSuccessToast(`Successfully sent verification email to ${user.email}.`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ApiClient.errorResponseHandler(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={panel}
|
||||
class="user-panel"
|
||||
popup={user.isNew}
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
on:hide
|
||||
on:show
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4>{user.isNew ? "New user" : "Edit user"}</h4>
|
||||
|
||||
{#if !user.isNew}
|
||||
<button type="button" class="btn btn-sm btn-circle btn-secondary m-l-auto">
|
||||
<!-- empty span for alignment -->
|
||||
<span />
|
||||
<i class="ri-more-line" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
||||
{#if !user.verified && user.email}
|
||||
<button type="button" class="dropdown-item" on:click={() => sendVerificationEmail()}>
|
||||
<i class="ri-mail-check-line" />
|
||||
<span class="txt">Send verification email</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button type="button" class="dropdown-item" on:click={() => deleteConfirm()}>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="tabs user-tabs">
|
||||
{#if !user.isNew}
|
||||
<div class="tabs-header stretched">
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === accountTab}
|
||||
on:click={() => (activeTab = accountTab)}
|
||||
>
|
||||
Account
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tab-item"
|
||||
class:active={activeTab === providersTab}
|
||||
on:click={() => (activeTab = providersTab)}
|
||||
>
|
||||
Authorized providers
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="tabs-content">
|
||||
<div class="tab-item" class:active={activeTab === accountTab}>
|
||||
<form id={formId} class="grid" autocomplete="off" on:submit|preventDefault={save}>
|
||||
{#if !user.isNew}
|
||||
<Field class="form-field disabled" name="id" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("primary")} />
|
||||
<span class="txt">ID</span>
|
||||
</label>
|
||||
<input type="text" id={uniqueId} value={user.id} disabled />
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
<Field class="form-field required" name="email" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon("email")} />
|
||||
<span class="txt">Email</span>
|
||||
</label>
|
||||
{#if user.verified && user.email}
|
||||
<div class="form-field-addon txt-success" use:tooltip={"Verified"}>
|
||||
<i class="ri-shield-check-line" />
|
||||
</div>
|
||||
{/if}
|
||||
<input type="email" autocomplete="off" id={uniqueId} required bind:value={email} />
|
||||
</Field>
|
||||
|
||||
{#if !user.isNew && user.email}
|
||||
<Field class="form-field form-field-toggle" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
|
||||
<label for={uniqueId}>Change password</label>
|
||||
</Field>
|
||||
{/if}
|
||||
|
||||
{#if user.isNew || !user.email || changePasswordToggle}
|
||||
<div class="col-12">
|
||||
<div class="grid" transition:slide|local={{ duration: 150 }}>
|
||||
<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={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={passwordConfirm}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if user.isNew || !user.email}
|
||||
<Field class="form-field form-field-toggle" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={verificationEmailToggle} />
|
||||
<label for={uniqueId}>Send verification email</label>
|
||||
</Field>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#if !user.isNew}
|
||||
<div class="tab-item" class:active={activeTab === providersTab}>
|
||||
<ExternalAuthsList {user} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
|
||||
{#if activeTab === accountTab}
|
||||
<button
|
||||
type="submit"
|
||||
form={formId}
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSaving}
|
||||
disabled={!hasChanges || isSaving}
|
||||
>
|
||||
<span class="txt">{user.isNew ? "Create" : "Save changes"}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
+41
-26
@@ -4,7 +4,6 @@ import ApiClient from "@/utils/ApiClient";
|
||||
import PageIndex from "@/components/PageIndex.svelte";
|
||||
import PageLogs from "@/components/logs/PageLogs.svelte";
|
||||
import PageRecords from "@/components/records/PageRecords.svelte";
|
||||
import PageUsers from "@/components/users/PageUsers.svelte";
|
||||
import PageAdmins from "@/components/admins/PageAdmins.svelte";
|
||||
import PageAdminLogin from "@/components/admins/PageAdminLogin.svelte";
|
||||
import PageApplication from "@/components/settings/PageApplication.svelte";
|
||||
@@ -58,30 +57,6 @@ const routes = {
|
||||
userData: { showAppSidebar: true },
|
||||
}),
|
||||
|
||||
"/users": wrap({
|
||||
component: PageUsers,
|
||||
conditions: baseConditions.concat([(_) => ApiClient.authStore.isValid]),
|
||||
userData: { showAppSidebar: true },
|
||||
}),
|
||||
|
||||
"/users/confirm-password-reset/:token": wrap({
|
||||
asyncComponent: () => import("@/components/users/PageUserConfirmPasswordReset.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
"/users/confirm-verification/:token": wrap({
|
||||
asyncComponent: () => import("@/components/users/PageUserConfirmVerification.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
"/users/confirm-email-change/:token": wrap({
|
||||
asyncComponent: () => import("@/components/users/PageUserConfirmEmailChange.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
"/settings": wrap({
|
||||
component: PageApplication,
|
||||
conditions: baseConditions.concat([(_) => ApiClient.authStore.isValid]),
|
||||
@@ -130,7 +105,47 @@ const routes = {
|
||||
userData: { showAppSidebar: true },
|
||||
}),
|
||||
|
||||
// fallback
|
||||
// ---------------------------------------------------------------
|
||||
// Records email confirmation actions
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// @deprecated
|
||||
"/users/confirm-password-reset/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmPasswordReset.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
"/auth/confirm-password-reset/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmPasswordReset.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
// @deprecated
|
||||
"/users/confirm-verification/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmVerification.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
"/auth/confirm-verification/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmVerification.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
// @deprecated
|
||||
"/users/confirm-email-change/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmEmailChange.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
"/auth/confirm-email-change/:token": wrap({
|
||||
asyncComponent: () => import("@/components/records/PageRecordConfirmEmailChange.svelte"),
|
||||
conditions: baseConditions,
|
||||
userData: { showAppSidebar: false },
|
||||
}),
|
||||
|
||||
// catch-all fallback
|
||||
"*": wrap({
|
||||
component: PageIndex,
|
||||
userData: { showAppSidebar: false },
|
||||
|
||||
+27
-22
@@ -56,6 +56,7 @@
|
||||
}
|
||||
}
|
||||
&:hover,
|
||||
&.focus,
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
color: var(--txtPrimaryColor);
|
||||
@@ -81,16 +82,20 @@
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
}
|
||||
&.drag-over {
|
||||
.accordion-header {
|
||||
background: var(--bodyColor);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
@include shadowize();
|
||||
.accordion-header {
|
||||
color: var(--baseColor);
|
||||
box-shadow: 0px 0px 0px 1px var(--primaryColor);
|
||||
box-shadow: 0px 0px 0px 1px var(--baseAlt2Color);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: var(--primaryColor);
|
||||
background: var(--bodyColor);
|
||||
&.interactive{
|
||||
background: var(--primaryColor);
|
||||
background: var(--bodyColor);
|
||||
&:after {
|
||||
color: inherit;
|
||||
content: '\ea78';
|
||||
@@ -107,28 +112,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// wrapper
|
||||
.accordions {
|
||||
.accordion {
|
||||
border-radius: 0;
|
||||
margin: -1px 0 0;
|
||||
&.active {
|
||||
border-radius: var(--baseRadius);
|
||||
margin: var(--smSpacing) 0;
|
||||
+ .accordion {
|
||||
border-top-left-radius: var(--baseRadius);
|
||||
border-top-right-radius: var(--baseRadius);
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
border-top-left-radius: var(--baseRadius);
|
||||
border-top-right-radius: var(--baseRadius);
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom-left-radius: var(--baseRadius);
|
||||
border-bottom-right-radius: var(--baseRadius);
|
||||
}
|
||||
}
|
||||
& > .accordion.active,
|
||||
& > .accordion-wrapper > .accordion.active {
|
||||
margin: var(--smSpacing) 0;
|
||||
border-radius: var(--baseRadius);
|
||||
}
|
||||
& > .accordion:first-child,
|
||||
& > .accordion-wrapper:first-child > .accordion {
|
||||
margin-top: 0;
|
||||
border-top-left-radius: var(--baseRadius);
|
||||
border-top-right-radius: var(--baseRadius);
|
||||
}
|
||||
& > .accordion:last-child,
|
||||
& > .accordion-wrapper:last-child > .accordion {
|
||||
margin-bottom: 0;
|
||||
border-bottom-left-radius: var(--baseRadius);
|
||||
border-bottom-right-radius: var(--baseRadius);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -122,7 +122,8 @@ blockquote {
|
||||
code {
|
||||
display: inline-block;
|
||||
font-family: var(--monospaceFontFamily);
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
font-size: var(--lgFontSize);
|
||||
line-height: 1.379rem;
|
||||
padding: 0px 4px;
|
||||
white-space: nowrap;
|
||||
@@ -165,6 +166,9 @@ hr {
|
||||
width: 100%;
|
||||
background: var(--baseAlt1Color);
|
||||
margin: var(--baseSpacing) 0;
|
||||
&.dark {
|
||||
background: var(--baseAlt2Color);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -341,6 +345,14 @@ a,
|
||||
@include shadowize();
|
||||
}
|
||||
|
||||
.sub-panel {
|
||||
@extend .content;
|
||||
background: var(--baseColor);
|
||||
border-radius: var(--baseRadius);
|
||||
padding: calc(var(--smSpacing) - 5px) var(--smSpacing);
|
||||
border: 1px solid var(--baseAlt1Color);
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
@extend %block;
|
||||
clear: both;
|
||||
@@ -676,7 +688,7 @@ a,
|
||||
.list {
|
||||
@extend %block;
|
||||
position: relative;
|
||||
border: 1px solid var(--baseAlt1Color);
|
||||
border: 1px solid var(--baseAlt2Color);
|
||||
border-radius: var(--baseRadius);
|
||||
.list-item {
|
||||
word-break: break-word;
|
||||
@@ -686,7 +698,7 @@ a,
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--baseAlt1Color);
|
||||
border-bottom: 1px solid var(--baseAlt2Color);
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
.docs-sidebar {
|
||||
--itemsSpacing: 10px;
|
||||
--itemsHeight: 40px;
|
||||
|
||||
position: relative;
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; // fallback
|
||||
overflow-y: overlay;
|
||||
background: var(--bodyColor);
|
||||
padding: var(--smSpacing) var(--xsSpacing);
|
||||
border-right: 1px solid var(--baseAlt1Color);
|
||||
|
||||
.sidebar-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar-item {
|
||||
position: relative;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
justify-content: start;
|
||||
padding: 5px 15px;
|
||||
margin: 0 0 var(--itemsSpacing) 0;
|
||||
font-size: var(--lgFontSize);
|
||||
min-height: var(--itemsHeight);
|
||||
border-radius: var(--baseRadius);
|
||||
user-select: none;
|
||||
color: var(--txtHintColor);
|
||||
transition: background var(--baseAnimationSpeed),
|
||||
color var(--baseAnimationSpeed);
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// states
|
||||
&:focus-visible,
|
||||
&:hover,
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--txtPrimaryColor);
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
&:active {
|
||||
background: var(--baseAlt2Color);
|
||||
transition-duration: var(--activeAnimationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
.sidebar-item {
|
||||
--itemsSpacing: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: calc(var(--baseSpacing) - 3px) var(--baseSpacing);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.docs-content-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.docs-panel {
|
||||
width: 960px;
|
||||
height: 100%;
|
||||
.overlay-panel-section.panel-header {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.overlay-panel-section.panel-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.overlay-panel-section.panel-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.overlay-panel-section.panel-footer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: $spacing;
|
||||
margin: 10px 0 0;
|
||||
margin: 5px 0 0;
|
||||
width: auto;
|
||||
min-width: 140px;
|
||||
max-width: 450px;
|
||||
@@ -90,7 +90,7 @@
|
||||
&.dropdown-upside {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
&.dropdown-left {
|
||||
right: auto;
|
||||
|
||||
+116
-21
@@ -1,3 +1,5 @@
|
||||
@use "sass:math";
|
||||
|
||||
button {
|
||||
outline: 0;
|
||||
border: 0;
|
||||
@@ -491,7 +493,7 @@ select {
|
||||
}
|
||||
}
|
||||
label ~ .form-field-addon {
|
||||
min-height: calc(var(--inputHeight) + var(--baseLineHeight));
|
||||
min-height: calc(26px + var(--inputHeight));
|
||||
}
|
||||
|
||||
// hints
|
||||
@@ -647,23 +649,44 @@ select {
|
||||
|
||||
// toggle
|
||||
&.form-field-toggle {
|
||||
$toggleWidth: 40px;
|
||||
$toggleHeight: 24px;
|
||||
$toggleSize: $toggleHeight - 8;
|
||||
$toggleOffset: ($toggleHeight - $toggleSize) * 0.5;
|
||||
@mixin toggleSize($toggleWidth: 40px, $toggleHeight: 24px) {
|
||||
$toggleSize: $toggleHeight - 8;
|
||||
$toggleOffset: ($toggleHeight - $toggleSize) * 0.5;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
& ~ label {
|
||||
min-height: $toggleHeight;
|
||||
padding-left: $toggleWidth + 7px;
|
||||
&:empty {
|
||||
padding-left: $toggleWidth;
|
||||
}
|
||||
&:before {
|
||||
width: $toggleWidth;
|
||||
height: $toggleHeight;
|
||||
border-radius: $toggleHeight;
|
||||
}
|
||||
&:after {
|
||||
top: $toggleOffset;
|
||||
left: $toggleOffset;
|
||||
width: $toggleSize;
|
||||
height: $toggleSize;
|
||||
border-radius: $toggleSize;
|
||||
}
|
||||
}
|
||||
&:checked ~ label {
|
||||
&:after {
|
||||
left: $toggleWidth - $toggleSize - $toggleOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
& ~ label {
|
||||
min-height: $toggleHeight;
|
||||
padding-left: $toggleWidth + 7px;
|
||||
&:empty {
|
||||
padding-left: $toggleWidth;
|
||||
}
|
||||
position: relative;
|
||||
&:before {
|
||||
content: '';
|
||||
width: $toggleWidth;
|
||||
height: $toggleHeight;
|
||||
border-radius: $toggleHeight;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
background: var(--baseAlt3Color);
|
||||
@@ -673,13 +696,8 @@ select {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: $toggleOffset;
|
||||
left: $toggleOffset;
|
||||
width: $toggleSize;
|
||||
height: $toggleSize;
|
||||
cursor: pointer;
|
||||
background: var(--baseColor);
|
||||
border-radius: $toggleSize;
|
||||
transition: left var(--activeAnimationSpeed),
|
||||
transform var(--activeAnimationSpeed),
|
||||
background var(--activeAnimationSpeed);
|
||||
@@ -707,11 +725,56 @@ select {
|
||||
background: var(--successColor);
|
||||
}
|
||||
&:after {
|
||||
left: $toggleWidth - $toggleSize - $toggleOffset;
|
||||
background: var(--baseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include toggleSize(40px, 24px);
|
||||
|
||||
&.form-field-sm {
|
||||
@include toggleSize(32px, 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
> .form-field {
|
||||
flex-grow: 1;
|
||||
border-left: 1px solid var(--baseAlt2Color);
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
> label {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
> %input,
|
||||
> .select .selected-container {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
> label {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
> %input,
|
||||
> .select .selected-container {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@for $i from 12 through 1 {
|
||||
.form-field.col-#{$i} {
|
||||
width: math.div(100%, math.div(12, $i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -942,8 +1005,39 @@ select {
|
||||
}
|
||||
}
|
||||
|
||||
.field-type-select .options-dropdown .options-list {
|
||||
max-height: 490px;
|
||||
.field-type-select {
|
||||
.options-dropdown {
|
||||
padding: 2px;
|
||||
.form-field.options-search {
|
||||
margin: 0;
|
||||
}
|
||||
.options-list {
|
||||
max-height: 490px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.dropdown-item {
|
||||
width: 50%;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding-left: 12px; // visual align with the label
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--baseAlt2Color);
|
||||
&:nth-child(2n) {
|
||||
border-left: 1px solid var(--baseAlt2Color);
|
||||
}
|
||||
&:nth-last-child(-n+2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
&.selected {
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1017,6 +1111,7 @@ select {
|
||||
border-radius: var(--baseRadius);
|
||||
background: var(--baseColor);
|
||||
border: 0;
|
||||
z-index: 9999;
|
||||
padding: 0 3px;
|
||||
font-size: 0.92rem;
|
||||
ul {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
row-gap: var(--smSpacing);
|
||||
font-size: 16px;
|
||||
font-size: var(--xlFontSize);
|
||||
color: var(--txtPrimaryColor);
|
||||
i {
|
||||
font-size: 24px;
|
||||
@@ -80,7 +80,6 @@
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-x: overlay;
|
||||
.app-body {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
@@ -97,6 +96,8 @@
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
.page-sidebar {
|
||||
--sidebarListItemMargin: 10px;
|
||||
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -174,11 +175,12 @@
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
margin: 10px 0;
|
||||
margin: var(--sidebarListItemMargin) 0;
|
||||
padding: 3px 10px;
|
||||
font-size: 16px;
|
||||
font-size: var(--xlFontSize);
|
||||
min-height: var(--btnHeight);
|
||||
min-width: 0;
|
||||
color: var(--txtHintColor);
|
||||
@@ -208,8 +210,14 @@
|
||||
transition-duration: var(--activeAnimationSpeed);
|
||||
}
|
||||
}
|
||||
.sidebar-content-compact .sidebar-list-item {
|
||||
--sidebarListItemMargin: 5px;
|
||||
}
|
||||
|
||||
// responsive
|
||||
@media screen and (max-height: 600px) {
|
||||
--sidebarListItemMargin: 5px;
|
||||
}
|
||||
@media screen and (max-width: 1100px) {
|
||||
--pageSidebarWidth: 190px;
|
||||
& > * {
|
||||
@@ -332,8 +340,11 @@
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* fallback */
|
||||
overflow-y: auto; // fallback
|
||||
overflow-y: overlay;
|
||||
.overlay-active & {
|
||||
overflow-y: hidden; // prevent double scrollbar
|
||||
}
|
||||
&.full-page {
|
||||
background: var(--baseColor);
|
||||
}
|
||||
|
||||
@@ -41,10 +41,6 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
background: var(--baseAlt2Color);
|
||||
}
|
||||
|
||||
// header
|
||||
.panel-header {
|
||||
position: relative;
|
||||
@@ -105,7 +101,7 @@
|
||||
margin-right: -10px;
|
||||
}
|
||||
.tabs-header {
|
||||
margin-bottom: -23px;
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +178,23 @@
|
||||
border-bottom: 1px solid var(--baseAlt1Color);
|
||||
.tabs-header {
|
||||
border-bottom: 0;
|
||||
.tab-item {
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 0;
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: var(--baseAlt1Color);
|
||||
}
|
||||
&:after {
|
||||
content: none;
|
||||
display: none;;
|
||||
}
|
||||
}
|
||||
.tab-item.active {
|
||||
background: var(--baseColor);
|
||||
border-color: var(--baseAlt1Color);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-header ~ .panel-content {
|
||||
|
||||
+27
-3
@@ -1,17 +1,20 @@
|
||||
table {
|
||||
--entranceAnimationSpeed: 0.3s;
|
||||
|
||||
border-collapse: separate;
|
||||
min-width: 100%;
|
||||
transition: opacity var(--baseAnimationSpeed);
|
||||
.form-field {
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
td, th {
|
||||
outline: 0;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--baseAlt2Color);
|
||||
&:first-child {
|
||||
padding-left: 20px;
|
||||
@@ -150,7 +153,8 @@ table {
|
||||
}
|
||||
|
||||
// field name specific columns
|
||||
td.col-field-id {
|
||||
td.col-field-id,
|
||||
td.col-field-username {
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -205,6 +209,11 @@ table {
|
||||
}
|
||||
|
||||
// states
|
||||
&.table-animate {
|
||||
tr {
|
||||
animation: entranceTop var(--entranceAnimationSpeed);
|
||||
}
|
||||
}
|
||||
&.table-loading {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
@@ -214,7 +223,6 @@ table {
|
||||
.table-wrapper {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
overflow-x: auto;
|
||||
max-width: calc(100% + 2*var(--baseSpacing));
|
||||
margin-left: calc(var(--baseSpacing) * -1);
|
||||
margin-right: calc(var(--baseSpacing) * -1);
|
||||
@@ -230,6 +238,7 @@ table {
|
||||
}
|
||||
}
|
||||
td, th {
|
||||
position: relative;
|
||||
&:first-child {
|
||||
padding-left: calc(var(--baseSpacing) + 3px);
|
||||
}
|
||||
@@ -243,6 +252,7 @@ table {
|
||||
.col-type-action {
|
||||
position: sticky;
|
||||
z-index: 99;
|
||||
transition: box-shadow var(--baseAnimationSpeed);
|
||||
}
|
||||
.bulk-select-col {
|
||||
left: 0px;
|
||||
@@ -258,4 +268,18 @@ table {
|
||||
th.col-type-action {
|
||||
background: var(--bodyColor);
|
||||
}
|
||||
|
||||
// scrolling styles
|
||||
&.scrollable {
|
||||
.bulk-select-col {
|
||||
box-shadow: 3px 0px 5px 0px var(--shadowColor);
|
||||
}
|
||||
.col-type-action {
|
||||
box-shadow: -3px 0px 5px 0px var(--shadowColor);
|
||||
}
|
||||
&.scroll-start .bulk-select-col,
|
||||
&.scroll-end .col-type-action {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$tabHeaderAnimationSpeed: 0.25s;
|
||||
$tabContentAnimationSpeed: 0.3s;
|
||||
$tabHeaderAnimationSpeed: 0.2s;
|
||||
$tabContentAnimationSpeed: 0.2s;
|
||||
|
||||
@keyframes tabChange {
|
||||
0% {
|
||||
|
||||
@@ -19,18 +19,10 @@
|
||||
transition: opacity var(--baseAnimationSpeed),
|
||||
visibility var(--baseAnimationSpeed),
|
||||
transform var(--baseAnimationSpeed);
|
||||
transform: translateY(2px);
|
||||
transform: scale(0.98);
|
||||
white-space: pre-line;
|
||||
@include hide();
|
||||
|
||||
// positions
|
||||
&.left {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
&.right {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
// styles
|
||||
&.code {
|
||||
font-family: monospace;
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
--warningColor: #ff8e3c;
|
||||
--warningAltColor: #ffe7d6;
|
||||
|
||||
--overlayColor: rgba(65, 82, 105, 0.25);
|
||||
--overlayColor: rgba(65, 80, 105, 0.25);
|
||||
--tooltipColor: rgba(0, 0, 0, 0.85);
|
||||
--shadowColor: rgba(0, 0, 0, 0.05);
|
||||
--shadowColor: rgba(0, 0, 0, 0.06);
|
||||
|
||||
--baseFontSize: 14.5px;
|
||||
--xsFontSize: 12px;
|
||||
|
||||
@@ -37,3 +37,5 @@
|
||||
@import 'bulkbar';
|
||||
|
||||
@import 'flatpickr';
|
||||
|
||||
@import 'docs_panel';
|
||||
|
||||
@@ -6,6 +6,20 @@ export const collections = writable([]);
|
||||
export const activeCollection = writable({});
|
||||
export const isCollectionsLoading = writable(false);
|
||||
|
||||
export function changeActiveCollectionById(collectionId) {
|
||||
collections.update((list) => {
|
||||
const found = CommonHelper.findByKey(list, "id", collectionId);
|
||||
|
||||
if (found) {
|
||||
activeCollection.set(found);
|
||||
} else if (list.length) {
|
||||
activeCollection.set(list[0]);
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
// add or update collection
|
||||
export function addCollection(collection) {
|
||||
activeCollection.update((current) => {
|
||||
@@ -14,7 +28,7 @@ export function addCollection(collection) {
|
||||
|
||||
collections.update((list) => {
|
||||
CommonHelper.pushOrReplaceByKey(list, collection, "id");
|
||||
return list;
|
||||
return CommonHelper.sortCollections(list);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,15 +38,13 @@ export function removeCollection(collection) {
|
||||
|
||||
activeCollection.update((current) => {
|
||||
if (current.id === collection.id) {
|
||||
// fallback to the first non-profile collection item
|
||||
return list.find((c) => c.name != import.meta.env.PB_PROFILE_COLLECTION) || {}
|
||||
return list[0];
|
||||
}
|
||||
return current;
|
||||
});
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// load all collections (excluding the user profile)
|
||||
@@ -46,17 +58,13 @@ export async function loadCollections(activeId = null) {
|
||||
"sort": "+created",
|
||||
})
|
||||
.then((items) => {
|
||||
collections.set(items);
|
||||
collections.set(CommonHelper.sortCollections(items));
|
||||
|
||||
const item = activeId && CommonHelper.findByKey(items, "id", activeId);
|
||||
if (item) {
|
||||
activeCollection.set(item);
|
||||
} else if (items.length) {
|
||||
// fallback to the first non-profile collection item
|
||||
const nonProfile = items.find((c) => c.name != import.meta.env.PB_PROFILE_COLLECTION)
|
||||
if (nonProfile) {
|
||||
activeCollection.set(nonProfile);
|
||||
}
|
||||
activeCollection.set(items[0]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@@ -88,7 +88,6 @@ class AppAuthStore extends LocalAuthStore {
|
||||
|
||||
const client = new PocketBase(
|
||||
import.meta.env.PB_BACKEND_URL,
|
||||
'en-US',
|
||||
new AppAuthStore("pb_admin_auth")
|
||||
);
|
||||
|
||||
|
||||
+151
-23
@@ -28,7 +28,7 @@ export default class CommonHelper {
|
||||
(value === "") ||
|
||||
(value === null) ||
|
||||
(value === "00000000-0000-0000-0000-000000000000") || // zero uuid
|
||||
(value === "0001-01-01T00:00:00Z") || // zero time
|
||||
(value === "0001-01-01 00:00:00.000Z") || // zero datetime
|
||||
(value === "0001-01-01") || // zero date
|
||||
(typeof value === "undefined") ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
@@ -314,7 +314,7 @@ export default class CommonHelper {
|
||||
* @param {String} delimiter
|
||||
*/
|
||||
static setByPath(data, path, newValue, delimiter = ".") {
|
||||
if (!CommonHelper.isObject(data) && !Array.isArray(data)) {
|
||||
if (data === null || typeof data !== 'object') {
|
||||
console.warn("setByPath: data not an object or array.");
|
||||
return
|
||||
}
|
||||
@@ -562,9 +562,13 @@ export default class CommonHelper {
|
||||
*/
|
||||
static getDateTime(date) {
|
||||
if (typeof date === 'string') {
|
||||
const sFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
const msFormat = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
const format = date.length === msFormat.length ? msFormat : sFormat;
|
||||
const formats = {
|
||||
19: "yyyy-MM-dd HH:mm:ss",
|
||||
23: "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
20: "yyyy-MM-dd HH:mm:ssZ",
|
||||
24: "yyyy-MM-dd HH:mm:ss.SSSZ",
|
||||
}
|
||||
const format = formats[date.length] || formats[19];
|
||||
return DateTime.fromFormat(date, format, { zone: 'UTC' });
|
||||
}
|
||||
|
||||
@@ -764,19 +768,26 @@ export default class CommonHelper {
|
||||
const fields = collection?.schema || [];
|
||||
|
||||
const dummy = {
|
||||
"@collectionId": collection?.id,
|
||||
"@collectionName": collection?.name,
|
||||
"id": "RECORD_ID",
|
||||
"created": "2022-01-01 01:00:00",
|
||||
"updated": "2022-01-01 23:59:59",
|
||||
"collectionId": collection?.id,
|
||||
"collectionName": collection?.name,
|
||||
"created": "2022-01-01 01:00:00Z",
|
||||
"updated": "2022-01-01 23:59:59Z",
|
||||
};
|
||||
|
||||
if (collection?.isAuth) {
|
||||
dummy["username"] = "username123";
|
||||
dummy["verified"] = false;
|
||||
dummy["emailVisibility"] = true;
|
||||
dummy["email"] = "test@example.com";
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
let val = null;
|
||||
if (field.type === 'number') {
|
||||
if (field.type === "number") {
|
||||
val = 123;
|
||||
} else if (field.type === "date") {
|
||||
val = "2022-01-01 10:00:00";
|
||||
val = "2022-01-01 10:00:00.123Z";
|
||||
} else if (field.type === "bool") {
|
||||
val = true;
|
||||
} else if (field.type === "email") {
|
||||
@@ -784,20 +795,20 @@ export default class CommonHelper {
|
||||
} else if (field.type === "url") {
|
||||
val = "https://example.com";
|
||||
} else if (field.type === "json") {
|
||||
val = 'JSON (array/object)';
|
||||
val = 'JSON';
|
||||
} else if (field.type === "file") {
|
||||
val = 'filename.jpg';
|
||||
if (field.options?.maxSelect > 1) {
|
||||
if (field.options?.maxSelect !== 1) {
|
||||
val = [val];
|
||||
}
|
||||
} else if (field.type === "select") {
|
||||
val = field.options?.values?.[0];
|
||||
if (field.options?.maxSelect > 1) {
|
||||
if (field.options?.maxSelect !== 1) {
|
||||
val = [val];
|
||||
}
|
||||
} else if (field.type === "relation" || field.type === "user") {
|
||||
} else if (field.type === "relation") {
|
||||
val = 'RELATION_RECORD_ID';
|
||||
if (field.options?.maxSelect > 1) {
|
||||
if (field.options?.maxSelect !== 1) {
|
||||
val = [val];
|
||||
}
|
||||
} else {
|
||||
@@ -810,6 +821,71 @@ export default class CommonHelper {
|
||||
return dummy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a dummy collection schema data object.
|
||||
*
|
||||
* @param {Object} collection
|
||||
* @return {Object}
|
||||
*/
|
||||
static dummyCollectionSchemaData(collection) {
|
||||
const fields = collection?.schema || [];
|
||||
|
||||
const dummy = {};
|
||||
|
||||
for (const field of fields) {
|
||||
let val = null;
|
||||
|
||||
if (field.type === "number") {
|
||||
val = 123;
|
||||
} else if (field.type === "date") {
|
||||
val = "2022-01-01 10:00:00.123Z";
|
||||
} else if (field.type === "bool") {
|
||||
val = true;
|
||||
} else if (field.type === "email") {
|
||||
val = "test@example.com";
|
||||
} else if (field.type === "url") {
|
||||
val = "https://example.com";
|
||||
} else if (field.type === "json") {
|
||||
val = 'JSON';
|
||||
} else if (field.type === "file") {
|
||||
continue; // currently file upload is supported only via FormData
|
||||
} else if (field.type === "select") {
|
||||
val = field.options?.values?.[0];
|
||||
if (field.options?.maxSelect !== 1) {
|
||||
val = [val];
|
||||
}
|
||||
} else if (field.type === "relation") {
|
||||
val = 'RELATION_RECORD_ID';
|
||||
if (field.options?.maxSelect !== 1) {
|
||||
val = [val];
|
||||
}
|
||||
} else {
|
||||
val = "test";
|
||||
}
|
||||
|
||||
dummy[field.name] = val;
|
||||
}
|
||||
|
||||
return dummy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection type icon.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {String}
|
||||
*/
|
||||
static getCollectionTypeIcon(type) {
|
||||
switch (type?.toLowerCase()) {
|
||||
case "auth":
|
||||
return "ri-group-line";
|
||||
case "single":
|
||||
return "ri-file-list-2-line";
|
||||
default:
|
||||
return "ri-folder-2-line";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a field type icon.
|
||||
*
|
||||
@@ -854,9 +930,7 @@ export default class CommonHelper {
|
||||
* @return {String}
|
||||
*/
|
||||
static getFieldValueType(field) {
|
||||
field = field || {};
|
||||
|
||||
switch (field.type) {
|
||||
switch (field?.type) {
|
||||
case 'bool':
|
||||
return 'Boolean';
|
||||
case 'number':
|
||||
@@ -865,16 +939,34 @@ export default class CommonHelper {
|
||||
return 'File';
|
||||
case 'select':
|
||||
case 'relation':
|
||||
case 'user':
|
||||
if (field.options?.maxSelect > 1) {
|
||||
return 'Array<String>';
|
||||
if (field?.options?.maxSelect === 1) {
|
||||
return 'String';
|
||||
}
|
||||
return 'String';
|
||||
return 'Array<String>';
|
||||
default:
|
||||
return 'String';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zero-default string value of the provided field.
|
||||
*
|
||||
* @param {Object} field
|
||||
* @return {String}
|
||||
*/
|
||||
static zeroDefaultStr(field) {
|
||||
if (field?.type === "number") {
|
||||
return "0";
|
||||
}
|
||||
|
||||
// array value
|
||||
if (["select", "relation", "file"].includes(field?.type) && field?.options?.maxSelect != 1) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
return '""';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an API url address extract from the current running instance.
|
||||
*
|
||||
@@ -940,4 +1032,40 @@ export default class CommonHelper {
|
||||
(withDeleteMissing && removedFields.length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups and sorts collections array by type (auth, single, base).
|
||||
*
|
||||
* @param {Array} collections
|
||||
* @return {Array}
|
||||
*/
|
||||
static sortCollections(collections = []) {
|
||||
const authCollections = [];
|
||||
const singleCollections = [];
|
||||
const baseCollections = [];
|
||||
|
||||
for (const colelction of collections) {
|
||||
if (colelction.type == 'auth') {
|
||||
authCollections.push(colelction);
|
||||
} else if (colelction.type == 'single') {
|
||||
singleCollections.push(colelction);
|
||||
} else {
|
||||
baseCollections.push(colelction);
|
||||
}
|
||||
}
|
||||
|
||||
return [].concat(authCollections, singleCollections, baseCollections);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* "Yield" to the main thread to break long runing task into smaller ones.
|
||||
*
|
||||
* (see https://web.dev/optimize-long-tasks/)
|
||||
*/
|
||||
static yieldToMain() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user