added support for linking to the record preview/update form and some other minor improvements
This commit is contained in:
@@ -223,6 +223,7 @@
|
||||
{#if active}
|
||||
<div class="overlay-panel-container" class:padded={popup} class:active>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="overlay"
|
||||
on:click|preventDefault={() => (overlayClose ? hide() : true)}
|
||||
@@ -237,7 +238,12 @@
|
||||
>
|
||||
<div class="overlay-panel-section panel-header">
|
||||
{#if btnClose && !popup}
|
||||
<button type="button" class="overlay-close" on:click|preventDefault={hide}>
|
||||
<button
|
||||
type="button"
|
||||
class="overlay-close"
|
||||
transition:fade={{ duration: transitionSpeed }}
|
||||
on:click|preventDefault={hide}
|
||||
>
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
}
|
||||
:global(.scroller-wrapper .columns-dropdown) {
|
||||
top: 40px;
|
||||
z-index: 100;
|
||||
z-index: 101;
|
||||
max-height: 340px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,12 @@
|
||||
<i class="ri-close-line" />
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" placeholder="Search collections..." bind:value={searchTerm} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search collections..."
|
||||
name="collections-search"
|
||||
bind:value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { replace, querystring } from "svelte-spa-router";
|
||||
import { tick } from "svelte";
|
||||
import { querystring } from "svelte-spa-router";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import {
|
||||
collections,
|
||||
@@ -21,7 +22,7 @@
|
||||
import RecordsList from "@/components/records/RecordsList.svelte";
|
||||
import RecordsCount from "@/components/records/RecordsCount.svelte";
|
||||
|
||||
const queryParams = new URLSearchParams($querystring);
|
||||
const initialQueryParams = new URLSearchParams($querystring);
|
||||
|
||||
let collectionUpsertPanel;
|
||||
let collectionDocsPanel;
|
||||
@@ -29,11 +30,13 @@
|
||||
let recordPreviewPanel;
|
||||
let recordsList;
|
||||
let recordsCount;
|
||||
let filter = queryParams.get("filter") || "";
|
||||
let sort = queryParams.get("sort") || "-created";
|
||||
let selectedCollectionId = queryParams.get("collectionId") || $activeCollection?.id;
|
||||
let filter = initialQueryParams.get("filter") || "";
|
||||
let sort = initialQueryParams.get("sort") || "-created";
|
||||
let selectedCollectionId = initialQueryParams.get("collectionId") || $activeCollection?.id;
|
||||
let totalCount = 0; // used to manully change the count without the need of reloading the recordsCount component
|
||||
|
||||
loadCollections(selectedCollectionId);
|
||||
|
||||
$: reactiveParams = new URLSearchParams($querystring);
|
||||
|
||||
$: if (
|
||||
@@ -53,23 +56,32 @@
|
||||
normalizeSort();
|
||||
}
|
||||
|
||||
$: if (!$isCollectionsLoading && initialQueryParams.get("recordId")) {
|
||||
showRecordById(initialQueryParams.get("recordId"));
|
||||
}
|
||||
|
||||
// keep the url params in sync
|
||||
$: if (sort || filter || $activeCollection?.id) {
|
||||
const query = new URLSearchParams({
|
||||
collectionId: $activeCollection?.id || "",
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
}).toString();
|
||||
replace("/collections?" + query);
|
||||
$: if (!$isCollectionsLoading && (sort || filter || $activeCollection?.id)) {
|
||||
updateQueryParams();
|
||||
}
|
||||
|
||||
$: $pageTitle = $activeCollection?.name || "Collections";
|
||||
|
||||
async function showRecordById(recordId) {
|
||||
await tick(); // ensure that the reactive component params are resolved
|
||||
|
||||
$activeCollection?.type === "view"
|
||||
? recordPreviewPanel.show(recordId)
|
||||
: recordUpsertPanel?.show(recordId);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedCollectionId = $activeCollection?.id;
|
||||
filter = "";
|
||||
sort = "-created";
|
||||
|
||||
updateQueryParams({ recordId: null });
|
||||
|
||||
normalizeSort();
|
||||
}
|
||||
|
||||
@@ -98,7 +110,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
loadCollections(selectedCollectionId);
|
||||
function updateQueryParams(extra = {}) {
|
||||
const query = Object.assign(
|
||||
{
|
||||
collectionId: $activeCollection?.id || "",
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
},
|
||||
extra
|
||||
);
|
||||
|
||||
CommonHelper.replaceQueryParams(query);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $isCollectionsLoading && !$collections.length}
|
||||
@@ -188,9 +211,15 @@
|
||||
bind:filter
|
||||
bind:sort
|
||||
on:select={(e) => {
|
||||
updateQueryParams({
|
||||
recordId: e.detail.id,
|
||||
});
|
||||
|
||||
let showModel = e.detail._partial ? e.detail.id : e.detail;
|
||||
|
||||
$activeCollection.type === "view"
|
||||
? recordPreviewPanel.show(e?.detail)
|
||||
: recordUpsertPanel?.show(e?.detail);
|
||||
? recordPreviewPanel?.show(showModel)
|
||||
: recordUpsertPanel?.show(showModel);
|
||||
}}
|
||||
on:delete={() => {
|
||||
recordsCount?.reload();
|
||||
@@ -217,16 +246,33 @@
|
||||
<RecordUpsertPanel
|
||||
bind:this={recordUpsertPanel}
|
||||
collection={$activeCollection}
|
||||
on:hide={() => {
|
||||
updateQueryParams({ recordId: null });
|
||||
}}
|
||||
on:save={(e) => {
|
||||
recordsList?.reloadLoadedPages();
|
||||
if (e.detail?.isNew) {
|
||||
if (filter) {
|
||||
// if there is applied filter, reload the count since we
|
||||
// don't know after the save whether the record satisfies it
|
||||
recordsCount?.reload();
|
||||
} else if (e.detail.isNew) {
|
||||
totalCount++;
|
||||
}
|
||||
}}
|
||||
on:delete={() => {
|
||||
|
||||
recordsList?.reloadLoadedPages();
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if (!filter || recordsList?.hasRecord(e.detail.id)) {
|
||||
totalCount--;
|
||||
}
|
||||
|
||||
recordsList?.reloadLoadedPages();
|
||||
totalCount--;
|
||||
}}
|
||||
/>
|
||||
|
||||
<RecordPreviewPanel bind:this={recordPreviewPanel} collection={$activeCollection} />
|
||||
<RecordPreviewPanel
|
||||
bind:this={recordPreviewPanel}
|
||||
collection={$activeCollection}
|
||||
on:hide={() => {
|
||||
updateQueryParams({ recordId: null });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,58 @@
|
||||
<script>
|
||||
import { addErrorToast } from "@/stores/toasts";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import RecordFieldValue from "./RecordFieldValue.svelte";
|
||||
import CopyIcon from "@/components/base/CopyIcon.svelte";
|
||||
import FormattedDate from "@/components/base/FormattedDate.svelte";
|
||||
import RecordFieldValue from "@/components/records/RecordFieldValue.svelte";
|
||||
|
||||
export let collection;
|
||||
|
||||
let recordPanel;
|
||||
let record = {};
|
||||
let isLoading = false;
|
||||
|
||||
$: hasEditorField = !!collection?.schema?.find((f) => f.type === "editor");
|
||||
|
||||
export function show(model) {
|
||||
record = model;
|
||||
load(model);
|
||||
|
||||
return recordPanel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
isLoading = false;
|
||||
return recordPanel?.hide();
|
||||
}
|
||||
|
||||
async function load(model) {
|
||||
record = {}; // reset
|
||||
|
||||
isLoading = true;
|
||||
|
||||
record = (await resolveModel(model)) || {};
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function resolveModel(model) {
|
||||
if (model && typeof model === "string") {
|
||||
// load from id
|
||||
try {
|
||||
return await ApiClient.collection(collection.id).getOne(model);
|
||||
} catch (err) {
|
||||
if (!err.isAbort) {
|
||||
hide();
|
||||
console.warn("resolveModel:", err);
|
||||
addErrorToast(`Unable to load record with id "${model}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
@@ -32,14 +65,14 @@
|
||||
<h4><strong>{collection?.name}</strong> record preview</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<table class="table-border preview-table">
|
||||
<table class="table-border preview-table" class:table-loading={isLoading}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="min-width txt-hint txt-bold">id</td>
|
||||
<td class="col-field">
|
||||
<div class="label">
|
||||
<CopyIcon value={record.id} />
|
||||
<span class="txt">{record.id}</span>
|
||||
<span class="txt">{record.id || "..."}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import { addSuccessToast, addErrorToast } from "@/stores/toasts";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Toggler from "@/components/base/Toggler.svelte";
|
||||
import ModelDateIcon from "@/components/base/ModelDateIcon.svelte";
|
||||
@@ -38,14 +38,14 @@
|
||||
let record = null;
|
||||
let initialDraft = null;
|
||||
let isSaving = false;
|
||||
let confirmClose = false; // prevent close recursion
|
||||
let confirmHide = false; // prevent close recursion
|
||||
let uploadedFilesMap = {}; // eg.: {"field1":[File1, File2], ...}
|
||||
let deletedFileNamesMap = {}; // eg.: {"field1":[0, 1], ...}
|
||||
let originalSerializedData = JSON.stringify(null);
|
||||
let originalSerializedData = JSON.stringify(original);
|
||||
let serializedData = originalSerializedData;
|
||||
let activeTab = tabFormKey;
|
||||
let isNew = true;
|
||||
let isLoaded = false;
|
||||
let isLoading = true;
|
||||
|
||||
$: isAuthCollection = collection?.type === "auth";
|
||||
|
||||
@@ -60,16 +60,16 @@
|
||||
|
||||
$: isNew = !original || !original.id;
|
||||
|
||||
$: canSave = isNew || hasChanges;
|
||||
$: canSave = !isLoading && (isNew || hasChanges);
|
||||
|
||||
$: if (isLoaded) {
|
||||
$: if (!isLoading) {
|
||||
updateDraft(serializedData);
|
||||
}
|
||||
|
||||
export function show(model) {
|
||||
load(model);
|
||||
|
||||
confirmClose = true;
|
||||
confirmHide = true;
|
||||
|
||||
activeTab = tabFormKey;
|
||||
|
||||
@@ -80,14 +80,49 @@
|
||||
return recordPanel?.hide();
|
||||
}
|
||||
|
||||
function forceHide() {
|
||||
confirmHide = false;
|
||||
hide();
|
||||
}
|
||||
|
||||
async function resolveModel(model) {
|
||||
if (model && typeof model === "string") {
|
||||
// load from id
|
||||
try {
|
||||
return await ApiClient.collection(collection.id).getOne(model);
|
||||
} catch (err) {
|
||||
if (!err.isAbort) {
|
||||
forceHide();
|
||||
console.warn("resolveModel:", err);
|
||||
addErrorToast(`Unable to load record with id "${model}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
async function load(model) {
|
||||
isLoaded = false;
|
||||
setErrors({}); // reset errors
|
||||
original = model || {};
|
||||
record = structuredClone(original);
|
||||
isLoading = true;
|
||||
|
||||
// resets
|
||||
setErrors({});
|
||||
uploadedFilesMap = {};
|
||||
deletedFileNamesMap = {};
|
||||
|
||||
// load the minimum model data if possible to minimize layout shifts
|
||||
original =
|
||||
typeof model === "string"
|
||||
? { id: model, collectionId: collection?.id, collectionName: collection?.name }
|
||||
: model || {};
|
||||
record = structuredClone(original);
|
||||
|
||||
// resolve the complete model
|
||||
original = (await resolveModel(model)) || {};
|
||||
record = structuredClone(original);
|
||||
|
||||
// wait to populate the fields to get the normalized values
|
||||
await tick();
|
||||
|
||||
@@ -100,7 +135,8 @@
|
||||
}
|
||||
|
||||
originalSerializedData = JSON.stringify(record);
|
||||
isLoaded = true;
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function replaceOriginal(newOriginal) {
|
||||
@@ -204,8 +240,7 @@
|
||||
deleteDraft();
|
||||
|
||||
if (hidePanel) {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
forceHide();
|
||||
} else {
|
||||
replaceOriginal(result);
|
||||
}
|
||||
@@ -406,11 +441,13 @@
|
||||
{hasEditorField ? 'overlay-panel-xl' : 'overlay-panel-lg'}
|
||||
{isAuthCollection && !isNew ? 'colored-header' : ''}
|
||||
"
|
||||
btnClose={!isLoading}
|
||||
escClose={!isLoading}
|
||||
overlayClose={!isLoading}
|
||||
beforeHide={() => {
|
||||
if (hasChanges && confirmClose) {
|
||||
if (hasChanges && confirmHide) {
|
||||
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
|
||||
confirmClose = false;
|
||||
hide();
|
||||
forceHide();
|
||||
});
|
||||
|
||||
return false;
|
||||
@@ -425,10 +462,15 @@
|
||||
on:show
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4 class="panel-title">
|
||||
{isNew ? "New" : "Edit"}
|
||||
<strong>{collection?.name}</strong> record
|
||||
</h4>
|
||||
{#if isLoading}
|
||||
<span class="loader loader-sm" />
|
||||
<h4 class="panel-title txt-hint">Loading...</h4>
|
||||
{:else}
|
||||
<h4 class="panel-title">
|
||||
{isNew ? "New" : "Edit"}
|
||||
<strong>{collection?.name}</strong> record
|
||||
</h4>
|
||||
{/if}
|
||||
|
||||
{#if !isNew}
|
||||
<div class="flex-fill" />
|
||||
@@ -502,7 +544,7 @@
|
||||
on:submit|preventDefault={save}
|
||||
on:keydown={handleFormKeydown}
|
||||
>
|
||||
{#if !hasChanges && initialDraft}
|
||||
{#if !hasChanges && initialDraft && !isLoading}
|
||||
<div class="block" out:slide={{ duration: 150 }}>
|
||||
<div class="alert alert-info m-0">
|
||||
<div class="icon">
|
||||
@@ -546,7 +588,7 @@
|
||||
<input
|
||||
type="text"
|
||||
id={uniqueId}
|
||||
placeholder="Leave empty to auto generate..."
|
||||
placeholder={!isLoading ? "Leave empty to auto generate..." : ""}
|
||||
minlength="15"
|
||||
readonly={!isNew}
|
||||
bind:value={record.id}
|
||||
@@ -602,7 +644,12 @@
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-transparent" disabled={isSaving} on:click={() => hide()}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent"
|
||||
disabled={isSaving || isLoading}
|
||||
on:click={() => hide()}
|
||||
>
|
||||
<span class="txt">Cancel</span>
|
||||
</button>
|
||||
|
||||
@@ -610,7 +657,7 @@
|
||||
type="submit"
|
||||
form={formId}
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSaving}
|
||||
class:btn-loading={isSaving || isLoading}
|
||||
disabled={!canSave || isSaving}
|
||||
>
|
||||
<span class="txt">{isNew ? "Create" : "Save changes"}</span>
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
export function hasRecord(id) {
|
||||
return !!records.find((r) => r.id);
|
||||
}
|
||||
|
||||
export async function reloadLoadedPages() {
|
||||
const loadedPages = currentPage;
|
||||
|
||||
@@ -157,8 +161,7 @@
|
||||
skipTotal: 1,
|
||||
filter: CommonHelper.normalizeSearchFilter(filter, fallbackSearchFields),
|
||||
expand: relFields.map((field) => field.name).join(","),
|
||||
// @todo temp disable the :excerpt fields until individual RecordUpsert loader is implemented
|
||||
// fields: listFields.join(","),
|
||||
fields: listFields.join(","),
|
||||
requestKey: "records_list",
|
||||
})
|
||||
.then(async (result) => {
|
||||
@@ -171,6 +174,13 @@
|
||||
lastTotal = result.items.length;
|
||||
dispatch("load", records.concat(result.items));
|
||||
|
||||
// mark the records as "partial" because of the excerpt
|
||||
if (editorFields.length) {
|
||||
for (let record of result.items) {
|
||||
record._partial = true;
|
||||
}
|
||||
}
|
||||
|
||||
// optimize the records listing by rendering the rows in task batches
|
||||
if (breakTasks) {
|
||||
const currentYieldId = ++yieldedRecordsId;
|
||||
|
||||
@@ -81,9 +81,11 @@
|
||||
}
|
||||
|
||||
loadPromises.push(
|
||||
ApiClient.collection(collectionId).getFullList(batchSize, {
|
||||
ApiClient.collection(collectionId).getFullList({
|
||||
batch: batchSize,
|
||||
filter: filters.join("||"),
|
||||
$autoCancel: false,
|
||||
fields: "*:excerpt(200)",
|
||||
requestKey: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -140,8 +142,9 @@
|
||||
const result = await ApiClient.collection(collectionId).getList(page, batchSize, {
|
||||
filter: CommonHelper.normalizeSearchFilter(filter, fallbackSearchFields),
|
||||
sort: !isView ? "-created" : "",
|
||||
fields: "*:excerpt(200)",
|
||||
skipTotal: 1,
|
||||
$cancelKey: uniqueId + "loadList",
|
||||
requestKey: uniqueId + "loadList",
|
||||
});
|
||||
|
||||
list = CommonHelper.filterDuplicatesByKey(list.concat(result.items));
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
export let record;
|
||||
export let collection;
|
||||
export let isNew = !record.id;
|
||||
export let isNew = !record?.id;
|
||||
|
||||
let originalUsername = record.username || null;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
text: "Make email public or private",
|
||||
position: "top-right",
|
||||
}}
|
||||
on:click={() => (record.emailVisibility = !record.emailVisibility)}
|
||||
on:click|preventDefault={() => (record.emailVisibility = !record.emailVisibility)}
|
||||
>
|
||||
<span class="txt">Public: {record.emailVisibility ? "On" : "Off"}</span>
|
||||
</button>
|
||||
|
||||
@@ -2,25 +2,49 @@
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import TinyMCE from "@tinymce/tinymce-svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let field;
|
||||
export let value = undefined;
|
||||
|
||||
let mounted = false;
|
||||
let mountedTimeoutId = null;
|
||||
|
||||
$: conf = Object.assign(CommonHelper.defaultEditorOptions(), {
|
||||
convert_urls: field.options?.convertUrls,
|
||||
relative_urls: false,
|
||||
});
|
||||
|
||||
// normalize value
|
||||
// (depending on the editor plugins, `undefined` may throw an error in case the TinyMCE text functions are used)
|
||||
$: if (typeof value == "undefined") {
|
||||
value = "";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mountedTimeoutId = setTimeout(() => {
|
||||
mounted = true;
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(mountedTimeoutId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<Field class="form-field form-field-editor {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
|
||||
<span class="txt">{field.name}</span>
|
||||
</label>
|
||||
<TinyMCE
|
||||
id={uniqueId}
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
{conf}
|
||||
bind:value
|
||||
/>
|
||||
{#if mounted}
|
||||
<TinyMCE
|
||||
id={uniqueId}
|
||||
scriptSrc="{import.meta.env.BASE_URL}libs/tinymce/tinymce.min.js"
|
||||
{conf}
|
||||
bind:value
|
||||
/>
|
||||
{:else}
|
||||
<div class="tinymce-wrapper" />
|
||||
{/if}
|
||||
</Field>
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="block"
|
||||
on:dragover|preventDefault={() => {
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
loadPromises.push(
|
||||
ApiClient.collection(field?.options?.collectionId).getFullList(batchSize, {
|
||||
filter: filters.join("||"),
|
||||
$autoCancel: false,
|
||||
fields: "*:excerpt(200)",
|
||||
requestKey: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,11 @@
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -670,7 +670,7 @@ a.thumb:not(.thumb-active) {
|
||||
font-family: var(--iconFontFamily);
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
animation: loaderShow var(--baseAnimationSpeed),
|
||||
animation: loaderShow var(--activeAnimationSpeed),
|
||||
rotate 0.9s var(--baseAnimationSpeed) infinite linear;
|
||||
}
|
||||
|
||||
@@ -855,6 +855,9 @@ a.thumb:not(.thumb-active) {
|
||||
.entrance-right {
|
||||
animation: entranceRight var(--entranceAnimationSpeed);
|
||||
}
|
||||
.entrance-fade {
|
||||
animation: fadeIn var(--entranceAnimationSpeed);
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
$boxSize: 32px;
|
||||
|
||||
@@ -59,14 +59,14 @@
|
||||
.markers {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
.marker {
|
||||
$size: 4px;
|
||||
$size: 5px;
|
||||
display: block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
|
||||
@@ -271,7 +271,7 @@ table {
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
z-index: 100;
|
||||
transition: box-shadow var(--baseAnimationSpeed);
|
||||
}
|
||||
tbody {
|
||||
|
||||
@@ -1923,4 +1923,63 @@ export default class CommonHelper {
|
||||
options: {},
|
||||
}, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the query parameters from the current url.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static getQueryParams() {
|
||||
let query = "";
|
||||
|
||||
let url = window.location.href
|
||||
|
||||
const queryStart = url.indexOf("?");
|
||||
if (queryStart > -1) {
|
||||
query = url.substring(queryStart + 1);
|
||||
url = url.substring(0, queryStart);
|
||||
}
|
||||
|
||||
return Object.fromEntries(new URLSearchParams(query))
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current query parameters without triggering
|
||||
* the router navigation.
|
||||
*
|
||||
* @param {Object} params
|
||||
*/
|
||||
static replaceQueryParams(params) {
|
||||
params = params || {};
|
||||
|
||||
let query = "";
|
||||
|
||||
let url = window.location.href
|
||||
|
||||
const queryStart = url.indexOf("?");
|
||||
if (queryStart > -1) {
|
||||
query = url.substring(queryStart + 1);
|
||||
url = url.substring(0, queryStart);
|
||||
}
|
||||
|
||||
const parsed = new URLSearchParams(query)
|
||||
|
||||
for (let key in params) {
|
||||
const val = params[key];
|
||||
|
||||
if (val === null) {
|
||||
parsed.delete(key);
|
||||
} else {
|
||||
parsed.set(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
query = parsed.toString();
|
||||
|
||||
if (query != "") {
|
||||
url += ("?" + query);
|
||||
}
|
||||
|
||||
window.location.replace(url);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user