pocketbase/ui/src/components/collections/CollectionUpdateConfirm.svelte

281 lines
9.6 KiB
Svelte

<script>
import { createEventDispatcher, tick } from "svelte";
import ApiClient from "@/utils/ApiClient";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
const dispatch = createEventDispatcher();
let panel;
let oldCollection;
let newCollection;
let hideAfterSave;
let conflictingOIDCs = [];
let changedRules = [];
$: isCollectionRenamed = oldCollection?.name != newCollection?.name;
$: isNewCollectionView = newCollection?.type === "view";
$: isNewCollectionAuth = newCollection?.type === "auth";
$: renamedFields =
(!isNewCollectionView &&
newCollection?.fields?.filter(
(field) => field.id && !field._toDelete && field._originalName != field.name,
)) ||
[];
$: deletedFields =
(!isNewCollectionView && newCollection?.fields?.filter((field) => field.id && field._toDelete)) || [];
$: multipleToSingleFields =
newCollection?.fields?.filter((field) => {
const old = oldCollection?.fields?.find((f) => f.id == field.id);
if (!old) {
return false;
}
return old.maxSelect != 1 && field.maxSelect == 1;
}) || [];
$: showChanges = !isNewCollectionView || isCollectionRenamed || changedRules.length;
export async function show(original, changed, hideAfterSaveArg = true) {
oldCollection = original;
newCollection = changed;
hideAfterSave = hideAfterSaveArg;
await detectConflictingOIDCs();
detectRulesChange();
await tick();
if (
isCollectionRenamed ||
renamedFields.length ||
deletedFields.length ||
multipleToSingleFields.length ||
conflictingOIDCs.length ||
changedRules.length
) {
panel?.show();
} else {
// no changes to review -> confirm directly
confirm();
}
}
export function hide() {
panel?.hide();
}
function confirm() {
hide();
dispatch("confirm", hideAfterSave);
}
const oidcProviders = ["oidc", "oidc2", "oidc3"];
async function detectConflictingOIDCs() {
conflictingOIDCs = [];
for (let name of oidcProviders) {
let oldProvider = oldCollection?.oauth2?.providers?.find((p) => p.name == name);
let newProvider = newCollection?.oauth2?.providers?.find((p) => p.name == name);
if (!oldProvider || !newProvider) {
continue;
}
let oldHost = new URL(oldProvider.authURL).host;
let newHost = new URL(newProvider.authURL).host;
if (oldHost == newHost) {
continue;
}
// check if there are existing externalAuths
if (await haveExternalAuths(name)) {
conflictingOIDCs.push({ name, oldHost, newHost });
}
}
}
async function haveExternalAuths(provider) {
try {
await ApiClient.collection("_externalAuths").getFirstListItem(
ApiClient.filter("collectionRef={:collectionId} && provider={:provider}", {
collectionId: newCollection?.id,
provider: provider,
}),
);
return true;
} catch {}
return false;
}
function getExternalAuthsFilterLink(provider) {
return `#/collections?collection=_externalAuths&filter=collectionRef%3D%22${newCollection?.id}%22+%26%26+provider%3D%22${provider}%22`;
}
function detectRulesChange() {
changedRules = [];
// for now enable only for "production"
if (window.location.protocol != "https:") {
return;
}
const ruleProps = ["listRule", "viewRule"];
if (!isNewCollectionView) {
ruleProps.push("createRule", "updateRule", "deleteRule");
}
if (isNewCollectionAuth) {
ruleProps.push("manageRule", "authRule");
}
let oldRule, newRule;
for (let prop of ruleProps) {
oldRule = oldCollection?.[prop];
newRule = newCollection?.[prop];
if (oldRule === newRule) {
continue;
}
changedRules.push({ prop, oldRule, newRule });
}
}
</script>
<OverlayPanel bind:this={panel} class="confirm-changes-panel" popup on:hide on:show>
<svelte:fragment slot="header">
<h4>Confirm collection changes</h4>
</svelte:fragment>
{#if isCollectionRenamed || deletedFields.length || renamedFields.length}
<div class="alert alert-warning">
<div class="icon">
<i class="ri-error-warning-line" />
</div>
<div class="content txt-bold">
<p>
If any of the collection changes is part of another collection rule, filter or view query,
you'll have to update it manually!
</p>
{#if deletedFields.length}
<p>All data associated with the removed fields will be permanently deleted!</p>
{/if}
</div>
</div>
{/if}
{#if showChanges}
<h6>Changes:</h6>
<ul class="changes-list">
{#if isCollectionRenamed}
<li>
<div class="inline-flex">
Renamed collection
<strong class="txt-strikethrough txt-hint">{oldCollection?.name}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt"> {newCollection?.name}</strong>
</div>
</li>
{/if}
{#if !isNewCollectionView}
{#each multipleToSingleFields as field}
<li>
Multiple to single value conversion of field
<strong>{field.name}</strong>
<em class="txt-sm">(will keep only the last array item)</em>
</li>
{/each}
{#each renamedFields as field}
<li>
<div class="inline-flex">
Renamed field
<strong class="txt-strikethrough txt-hint">{field._originalName}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt">{field.name}</strong>
</div>
</li>
{/each}
{#each deletedFields as field}
<li class="txt-danger">
Removed field <span class="txt-bold">{field.name}</span>
</li>
{/each}
{/if}
{#each changedRules as ruleChange}
<li>
Changed API rule <code class="txt-sm">{ruleChange.prop}</code>:
<br />
<small class="txt-mono txt-hint">
<strong>Old</strong>:
<span class="txt-preline">
{ruleChange.oldRule === null
? "null (superusers only)"
: ruleChange.oldRule || '""'}
</span>
</small>
<br />
<small class="txt-mono txt-success">
<strong>New</strong>:
<span class="txt-preline">
{ruleChange.newRule === null
? "null (superusers only)"
: ruleChange.newRule || '""'}
</span>
</small>
</li>
{/each}
{#each conflictingOIDCs as oidc}
<li>
Changed <code>{oidc.name}</code> host
<div class="inline-flex m-l-5">
<strong class="txt-strikethrough txt-hint">{oidc.oldHost}</strong>
<i class="ri-arrow-right-line txt-sm" />
<strong class="txt">{oidc.newHost}</strong>
</div>
<br />
<em class="txt-hint">
If the old and new OIDC configuration is not for the same provider consider deleting
all old <code class="txt-sm">_externalAuths</code> records associated to the current
collection and provider, otherwise it may result in account linking errors.
<a href={getExternalAuthsFilterLink(oidc.name)} target="_blank">
Review existing <code class="txt-sm">_externalAuths</code> records
<i class="ri-external-link-line txt-sm"></i>
</a>.
</em>
</li>
{/each}
</ul>
{/if}
<svelte:fragment slot="footer">
<!-- svelte-ignore a11y-autofocus -->
<button autofocus type="button" class="btn btn-transparent" on:click={() => hide()}>
<span class="txt">Cancel</span>
</button>
<button type="button" class="btn btn-expanded" on:click={() => confirm()}>
<span class="txt">Confirm</span>
</button>
</svelte:fragment>
</OverlayPanel>
<style lang="scss">
.changes-list {
word-break: break-word;
line-height: var(--smLineHeight);
li {
margin-top: 10px;
margin-bottom: 10px;
}
}
</style>