Skip to content

Commit f6bfd70

Browse files
committed
feat: show list of changed fields in before create/edit views leave popup
https://web.tracklify.com/project/2b7ZVgE5/AdminForth/1280/cFz46RGr/is-it-possible-to-remove-this-
1 parent b783a04 commit f6bfd70

File tree

4 files changed

+58
-15
lines changed

4 files changed

+58
-15
lines changed

adminforth/spa/src/components/AcceptModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
1515
</svg>
1616
<h3 class="afcl-confirmation-title mb-5 text-lg font-normal text-lightAcceptModalText dark:text-darkAcceptModalText">{{ modalStore?.modalContent?.content }}</h3>
17-
<h3 class="prose afcl-confirmation-title mb-5 text-lg font-normal text-lightAcceptModalText dark:text-darkAcceptModalText" v-html="modalStore?.modalContent?.contentHTML"></h3>
17+
<h3 class=" afcl-confirmation-title mb-5 text-lg font-normal text-lightAcceptModalText dark:text-darkAcceptModalText" v-html="modalStore?.modalContent?.contentHTML"></h3>
1818

1919
<button @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}" type="button" class="afcl-confirmation-accept-button text-lightAcceptModalConfirmButtonText bg-lightAcceptModalConfirmButtonBackground hover:bg-lightAcceptModalConfirmButtonBackgroundHover focus:ring-4 focus:outline-none focus:ring-lightAcceptModalConfirmButtonFocus font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center dark:text-darkAcceptModalConfirmButtonText dark:bg-darkAcceptModalConfirmButtonBackground dark:hover:bg-darkAcceptModalConfirmButtonBackgroundHover dark:focus:ring-darkAcceptModalConfirmButtonFocus">
2020
{{ modalStore?.modalContent?.acceptText }}

adminforth/spa/src/utils/utils.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import sanitizeHtml from 'sanitize-html'
1010
import debounce from 'debounce';
1111
import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
1212
import { i18nInstance } from '../i18n'
13+
import { useI18n } from 'vue-i18n';
1314

1415

1516

@@ -524,9 +525,10 @@ export function atob_function(source: string): string {
524525
return atob(source);
525526
}
526527

527-
export function compareOldAndNewRecord(oldRecord: Record<string, any>, newRecord: Record<string, any>): boolean {
528+
export function compareOldAndNewRecord(oldRecord: Record<string, any>, newRecord: Record<string, any>): {ok: boolean, changedFields: Record<string, {oldValue: any, newValue: any}>} {
528529
const newKeys = Object.keys(newRecord);
529530
const coreStore = useCoreStore();
531+
const changedColumns: Record<string, { oldValue: any, newValue: any }> = {};
530532

531533
for (const key of newKeys) {
532534
const oldValue = typeof oldRecord[key] === 'object' && oldRecord[key] !== null ? JSON.stringify(oldRecord[key]) : oldRecord[key];
@@ -537,30 +539,63 @@ export function compareOldAndNewRecord(oldRecord: Record<string, any>, newRecord
537539
oldValue === undefined ||
538540
oldValue === null ||
539541
oldValue === '' ||
540-
(Array.isArray(oldValue) && oldValue.length === 0)
542+
(Array.isArray(oldValue) && oldValue.length === 0) ||
543+
oldValue === '[]'
541544
)
542545
&&
543546
(
544547
newValue === undefined ||
545548
newValue === null ||
546549
newValue === '' ||
547-
(Array.isArray(newValue) && newValue.length === 0)
550+
(Array.isArray(newValue) && newValue.length === 0) ||
551+
newValue === '[]'
548552
)
549553
) {
550554
// console.log(`Value for key ${key} is considered equal (empty)`)
551555
continue;
552556
}
553557

554-
const column = coreStore.resource.columns.find((c) => c.name === key);
558+
const column = coreStore.resource?.columns.find((c) => c.name === key);
555559
if (column?.foreignResource) {
556560
if (newRecord[key] === oldRecord[key]?.pk) {
557561
// console.log(`Value for key ${key} is considered equal (foreign key)`)
558562
continue;
559563
}
560564
}
561565
// console.log(`Value for key ${key} is different`, { oldValue: oldValue, newValue: newValue });
562-
return false;
566+
changedColumns[key] = { oldValue, newValue };
563567
}
564568
}
565-
return true;
569+
return { ok: Object.keys(changedColumns).length !== 0, changedFields: changedColumns };
570+
}
571+
572+
export function generateMessageHtmlForRecordChange(changedFields: Record<string, { oldValue: any, newValue: any }>, t: ReturnType<typeof useI18n>['t']): string {
573+
const coreStore = useCoreStore();
574+
575+
const escapeHtml = (value: any) => {
576+
if (value === null || value === undefined || value === '') return `<em>${t('empty')}</em>`;
577+
let s: string;
578+
if (typeof value === 'object') {
579+
try {
580+
s = JSON.stringify(value);
581+
} catch (e) {
582+
s = String(value);
583+
}
584+
} else {
585+
s = String(value);
586+
}
587+
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
588+
};
589+
590+
const items = Object.keys(changedFields || {}).map(key => {
591+
const column = coreStore.resource?.columns?.find((c: any) => c.name === key);
592+
const label = column?.label || key;
593+
const oldV = escapeHtml(changedFields[key].oldValue);
594+
const newV = escapeHtml(changedFields[key].newValue);
595+
return `<li class="truncate"><strong>${escapeHtml(label)}</strong>: <span class="af-old-value text-muted">${oldV}</span> &#8594; <span class="af-new-value">${newV}</span></li>`;
596+
}).join('');
597+
598+
const listHtml = items ? `<ul class="mt-2 list-disc list-inside">${items}</ul>` : '';
599+
const messageHtml = `<div>${escapeHtml(t('There are unsaved changes. Are you sure you want to leave this page?'))}${listHtml}</div>`;
600+
return messageHtml;
566601
}

adminforth/spa/src/views/CreateView.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
7979
import ResourceForm from '@/components/ResourceForm.vue';
8080
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
8181
import { useCoreStore } from '@/stores/core';
82-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord } from '@/utils';
82+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, generateMessageHtmlForRecordChange } from '@/utils';
8383
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
8484
import { onMounted, onBeforeMount, onBeforeUnmount, ref, watch, nextTick } from 'vue';
8585
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
@@ -121,7 +121,7 @@ async function onUpdateRecord(newRecord: any) {
121121
}
122122
123123
function checkIfWeCanLeavePage() {
124-
return wasSaveSuccessful.value || cancelButtonClicked.value || compareOldAndNewRecord(initialValues.value, record.value);
124+
return wasSaveSuccessful.value || cancelButtonClicked.value || compareOldAndNewRecord(initialValues.value, record.value).ok === false;
125125
}
126126
127127
function onBeforeUnload(event: BeforeUnloadEvent) {
@@ -139,8 +139,12 @@ onBeforeUnmount(() => {
139139
140140
onBeforeRouteLeave(async (to, from, next) => {
141141
if (!checkIfWeCanLeavePage()) {
142-
const answer = await confirm({message: t('There are unsaved changes. Are you sure you want to leave this page?'), yes: 'Yes', no: 'No'});
143-
if (!answer) return next(false);
142+
const { changedFields } = compareOldAndNewRecord(initialValues.value, record.value);
143+
144+
const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
145+
146+
const answer = await confirm({ messageHtml: messageHtml, yes: t('Yes'), no: t('No') });
147+
if (!answer) return next(false);
144148
}
145149
next();
146150
});

adminforth/spa/src/views/EditView.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
7474
import ResourceForm from '@/components/ResourceForm.vue';
7575
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
7676
import { useCoreStore } from '@/stores/core';
77-
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord } from '@/utils';
77+
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord, generateMessageHtmlForRecordChange } from '@/utils';
7878
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
7979
import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch, onBeforeUnmount } from 'vue';
8080
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
@@ -113,7 +113,7 @@ function onBeforeUnload(event: BeforeUnloadEvent) {
113113
}
114114
115115
function checkIfWeCanLeavePage() {
116-
return wasSaveSuccessful.value || cancelButtonClicked.value || compareOldAndNewRecord(initialRecord.value, record.value);
116+
return wasSaveSuccessful.value || cancelButtonClicked.value || compareOldAndNewRecord(initialRecord.value, record.value).ok === false;
117117
}
118118
119119
window.addEventListener('beforeunload', onBeforeUnload);
@@ -124,8 +124,12 @@ onBeforeUnmount(() => {
124124
125125
onBeforeRouteLeave(async (to, from, next) => {
126126
if (!checkIfWeCanLeavePage()) {
127-
const answer = await confirm({message: t('There are unsaved changes. Are you sure you want to leave this page?'), yes: 'Yes', no: 'No'});
128-
if (!answer) return next(false);
127+
const { changedFields } = compareOldAndNewRecord(initialRecord.value, record.value);
128+
129+
const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
130+
131+
const answer = await confirm({ messageHtml: messageHtml, yes: t('Yes'), no: t('No') });
132+
if (!answer) return next(false);
129133
}
130134
next();
131135
});

0 commit comments

Comments
 (0)