Files
pocketbase/ui/src/collections/collectionChangesConfirmationModal.js
T

351 lines
14 KiB
JavaScript

import { toDeleteProp } from "@/base/fieldSettings";
window.app = window.app || {};
window.app.modals = window.app.modals || {};
window.app.modals.openCollectionChangesConfirmation = async function(
oldCollection,
newCollection,
yesCallback,
noCallback,
) {
const data = store({
isLoadingConflictingOIDCProviders: false,
conflictingOIDCProviders: [],
// ---
get isCollectionRenamed() {
return oldCollection?.name != newCollection?.name;
},
get isNewCollectionAuth() {
return newCollection?.type === "auth";
},
get isNewCollectionView() {
return newCollection?.type === "view";
},
get renamedFields() {
if (data.isNewCollectionView) {
return [];
}
return newCollection?.fields?.filter?.((f) => {
let oldField;
if (f.id && !f[toDeleteProp]) {
oldField = oldCollection.fields?.find?.((old) => old.id == f.id);
}
return oldField && oldField.name != f.name;
}) || [];
},
get deletedFields() {
if (data.isNewCollectionView) {
return [];
}
return newCollection?.fields?.filter?.((f) => {
return f.id && f[toDeleteProp];
}) || [];
},
get multipleToSingleFields() {
if (data.isNewCollectionView) {
return [];
}
return newCollection?.fields?.filter?.((newField) => {
const oldField = oldCollection?.fields?.find?.((f) => f.id == newField.id);
if (!oldField || typeof oldField.maxSelect == "undefined") {
return false;
}
// normalize
const oldMaxSelect = oldField.maxSelect || 1;
const newMaxSelect = newField.maxSelect || 1;
return oldMaxSelect > 1 && newMaxSelect == 1;
}) || [];
},
get changedRules() {
// for now enable only for "production"
if (window.location.protocol != "https:") {
return [];
}
const result = [];
const ruleProps = ["listRule", "viewRule"];
if (!data.isNewCollectionView) {
ruleProps.push("createRule", "updateRule", "deleteRule");
}
if (data.isNewCollectionAuth) {
ruleProps.push("manageRule", "authRule");
}
let oldRule, newRule;
for (let prop of ruleProps) {
oldRule = oldCollection?.[prop];
newRule = newCollection?.[prop];
if (oldRule === newRule) {
continue;
}
result.push({ prop, oldRule, newRule });
}
return result;
},
get needConfirmation() {
return !app.utils.isEmpty(oldCollection?.id) && (
data.isCollectionRenamed
|| data.renamedFields.length
|| data.deletedFields.length
|| data.multipleToSingleFields.length
|| data.changedRules.length
|| data.conflictingOIDCProviders.length
);
},
});
const knownOIDCProviders = ["oidc", "oidc2", "oidc3"];
async function detectConflictingOIDCProviders() {
if (app.utils.isEmpty(oldCollection?.id) || !data.isNewCollectionAuth) {
return;
}
data.isLoadingConflictingOIDCProviders = true;
try {
data.conflictingOIDCProviders = [];
for (const name of knownOIDCProviders) {
const oldProvider = oldCollection?.oauth2?.providers?.find?.((p) => p.name == name);
const newProvider = newCollection?.oauth2?.providers?.find?.((p) => p.name == name);
if (!oldProvider || !newProvider) {
continue;
}
const oldHost = new URL(oldProvider.authURL).host;
const newHost = new URL(newProvider.authURL).host;
if (oldHost == newHost) {
continue;
}
// check if there are existing externalAuths
const haveExternalAuths = await app.pb.collection("_externalAuths").getFirstListItem(
app.pb.filter("collectionRef={:collectionId} && provider={:provider}", {
collectionId: newCollection?.id,
provider: name,
}),
{
requestKey: null,
},
);
if (haveExternalAuths) {
data.conflictingOIDCProviders.push({ name, oldHost, newHost });
}
}
data.isLoadingConflictingOIDCProviders = false;
} catch (err) {
if (err.isAbort) {
data.isLoadingConflictingOIDCProviders = false;
app.checkApiError(err);
}
}
}
await detectConflictingOIDCProviders();
if (!data.needConfirmation) {
return yesCallback();
}
app.modals.confirm(
t.div(
{ className: "dangerous-collection-changes-list" },
t.h5({ className: "block txt-center m-b-base" }, "Do you really want to save the collection changes?"),
// general collection warning
() => {
if (!data.isCollectionRenamed && !data.deletedFields.length && !data.renamedFields.length) {
return;
}
return t.div(
{ className: "alert warning m-b-base" },
t.p(
null,
"If the collection participate in another collection rule, filter or view query, you'll have to update it manually!",
),
() => {
if (data.deletedFields.length) {
return t.p(
null,
"All data associated with the removed fields will be permanently deleted!",
);
}
},
);
},
// renamed collection
() => {
if (!data.isCollectionRenamed) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-renamed-collection" },
t.li(
{ className: "list-item" },
"Renamed collection ",
t.strong({ className: "label warning" }, oldCollection?.name),
t.i({ className: "ri-arrow-right-line txt-sm", ariaHidden: true }),
t.strong({ className: "label success" }, newCollection?.name || "N/A"),
),
);
},
// renamed fields
() => {
if (!data.renamedFields.length) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-renamed-fields" },
() => {
return data.renamedFields.map((newField) => {
const oldField = oldCollection?.fields?.find?.((f) => f.id == newField.id);
return t.li(
{ className: "list-item" },
"Renamed field ",
t.strong({ className: "label warning" }, oldField?.name),
t.i({ className: "ri-arrow-right-line txt-sm", ariaHidden: true }),
t.strong({ className: "label success" }, newField.name || "N/A"),
);
});
},
);
},
// deleted fields
() => {
if (!data.deletedFields.length) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-deleted-fields" },
() => {
return data.deletedFields.map((field) => {
return t.li(
{ className: "list-item" },
"Deleted field ",
t.strong({ className: "label danger" }, field.name || "N/A"),
);
});
},
);
},
// multiple->single fields
() => {
if (!data.multipleToSingleFields.length) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-multiple-to-single-fields" },
() => {
return data.multipleToSingleFields.map((field) => {
return t.li(
{ className: "list-item" },
"Multiple to single value conversion of field ",
t.strong({ className: "label warning" }, field.name || field.id),
t.em({ className: "txt-sm" }, " (will keep only the last array item)"),
);
});
},
);
},
// API rule changes
() => {
if (!data.changedRules.length) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-api-rules" },
() => {
return data.changedRules.map((ruleChange) => {
return t.li(
{ className: "list-item" },
t.div(
{ className: "content" },
t.span({ className: "txt" }, "Changed API rule for "),
t.code(null, ruleChange.prop),
),
t.small({ className: "txt-bold" }, "Old:"),
t.div(
{ className: "rule-content old-rule" },
ruleChange.oldRule === null
? "null (superusers only)"
: (ruleChange.oldRule || "\"\""),
),
t.small({ className: "txt-bold" }, "New:"),
t.div(
{ className: "rule-content new-rule" },
ruleChange.newRule === null
? "null (superusers only)"
: (ruleChange.newRule || "\"\""),
),
);
});
},
);
},
// Conflicting OIDC changes
() => {
if (!data.conflictingOIDCProviders.length) {
return;
}
return t.ul(
{ className: "collection-changes-list changes-api-rules" },
() => {
return data.conflictingOIDCProviders.map((oidc) => {
return t.li(
{ className: "list-item" },
"Changed OIDC ",
oidc.name,
" host ",
t.strong({ className: "label warning" }, oidc.oldHost),
t.i({ className: "ri-arrow-right-line txt-sm", ariaHidden: true }),
t.strong({ className: "label success" }, oidc.newHost),
t.br(),
t.span(
{ className: "txt-hint" },
"If the old and new OIDC configuration is not for the same provider consider deleting",
" all old _externalAuths records associated to the current collection and provider,",
" otherwise it may result in account linking errors.",
),
" ",
t.a({
rel: "noopenener noreferrer",
target: "_blank",
href: () => {
return `#/collections?collection=_externalAuths&filter=collectionRef%3D%22${newCollection?.id}%22+%26%26+provider%3D%22${oidc.name}%22`;
},
textContent: "Review existing _externalAuths records",
}),
);
});
},
);
},
),
yesCallback,
noCallback,
{
className: "collection-changes-confirm-modal",
yesButton: "Yes, save changes",
},
);
};