Skip to content

Commit 86e1421

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents e4f92ca + bec462b commit 86e1421

File tree

6 files changed

+128
-31
lines changed

6 files changed

+128
-31
lines changed

adminforth/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -568,17 +568,21 @@ class AdminForth implements IAdminForth {
568568
response,
569569
extra,
570570
});
571-
if (!resp || (typeof resp.ok !== 'boolean' && (!resp.error && !resp.newRecordId))) {
571+
if (resp.newRecordId) {
572+
afLogger.warn(`Deprecation warning: beforeSave hook returned 'newRecordId'. Since AdminForth v1.2.9 use 'redirectToRecordId' instead. 'newRecordId' will be removed in v2.0.0`);
573+
}
574+
if (!resp || (typeof resp.ok !== 'boolean' && (!resp.error && !resp.newRecordId && !resp.redirectToRecordId))) {
572575
throw new Error(
573-
`Invalid return value from beforeSave hook. Expected: { ok: boolean, error?: string | null, newRecordId?: any }.\n` +
574-
`Note: Return { ok: false, error: null, newRecordId } to stop creation and redirect to an existing record.`
576+
`Invalid return value from beforeSave hook. Expected: { ok: boolean, error?: string | null, newRecordId?: any, redirectToRecordId?: any }.\n` +
577+
`Note: Return { ok: false, error: null, redirectToRecordId } (preferred) or { ok: false, error: null, newRecordId } (deprecated) to stop creation and redirect to an existing record.`
575578
);
576579
}
577580
if (resp.ok === false && !resp.error) {
578-
const { error, ok, newRecordId } = resp;
581+
const { error, ok, newRecordId, redirectToRecordId } = resp;
579582
return {
580583
error: error ?? 'Operation aborted by hook',
581-
newRecordId: newRecordId
584+
newRecordId: redirectToRecordId ? redirectToRecordId : newRecordId,
585+
redirectToRecordId: redirectToRecordId
582586
};
583587
}
584588
if (resp.error) {

adminforth/modules/restApi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
12801280
extra: { body, query, headers, cookies, requestUrl, response }
12811281
});
12821282
if (createRecordResponse.error) {
1283-
return { error: createRecordResponse.error, ok: false, newRecordId: createRecordResponse.newRecordId };
1283+
return {
1284+
error: createRecordResponse.error,
1285+
ok: false,
1286+
newRecordId: createRecordResponse.redirectToRecordId ? createRecordResponse.redirectToRecordId :createRecordResponse.newRecordId,
1287+
redirectToRecordId: createRecordResponse.redirectToRecordId };
12841288
}
12851289
const connector = this.adminforth.connectors[resource.dataSource];
12861290

adminforth/spa/src/utils/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,12 @@ export function protectAgainstXSS(value: string) {
237237
],
238238
allowedAttributes: {
239239
'li': [ 'data-list' ],
240-
'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
241-
}
240+
'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ],
241+
// Allow markup on spans (classes & styles), and
242+
// generic data/aria/style attributes on any element. (e.g. for KaTeX-related previews)
243+
'span': [ 'class', 'style' ],
244+
'*': [ 'data-*', 'aria-*', 'style' ]
245+
},
242246
});
243247
}
244248

adminforth/spa/src/views/CreateView.vue

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<BreadcrumbsWithButtons>
1414
<!-- save and cancle -->
15-
<button @click="$router.back()"
15+
<button @click="() => {cancelButtonClicked = true; $router.back()}"
1616
class="af-cancel-button flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-lightCreateViewButtonText focus:outline-none bg-lightCreateViewButtonBackground rounded border border-lightCreateViewButtonBorder hover:bg-lightCreateViewButtonBackgroundHover hover:text-lightCreateViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightCreateViewButtonFocusRing dark:focus:ring-darkCreateViewButtonFocusRing dark:bg-darkCreateViewButtonBackground dark:text-darkCreateViewButtonText dark:border-darkCreateViewButtonBorder dark:hover:text-darkCreateViewButtonTextHover dark:hover:bg-darkCreateViewButtonBackgroundHover"
1717
>
1818
{{ $t('Cancel') }}
@@ -81,8 +81,8 @@ import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
8181
import { useCoreStore } from '@/stores/core';
8282
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf } from '@/utils';
8383
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
84-
import { onMounted, onBeforeMount, ref, watch, nextTick } from 'vue';
85-
import { useRoute, useRouter } from 'vue-router';
84+
import { onMounted, onBeforeMount, onBeforeUnmount, ref, watch, nextTick } from 'vue';
85+
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
8686
import { computed } from 'vue';
8787
import { showErrorTost } from '@/composables/useFrontendApi';
8888
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
@@ -103,7 +103,7 @@ const router = useRouter();
103103
const record = ref({});
104104
105105
const coreStore = useCoreStore();
106-
const { clearSaveInterceptors, runSaveInterceptors, alert } = useAdminforth();
106+
const { clearSaveInterceptors, runSaveInterceptors, alert, confirm } = useAdminforth();
107107
108108
const { t } = useI18n();
109109
@@ -113,11 +113,38 @@ const initialValues = ref({});
113113
114114
const readonlyColumns = ref([]);
115115
116+
const cancelButtonClicked = ref(false);
117+
const wasSaveSuccessful = ref(false);
116118
117119
async function onUpdateRecord(newRecord: any) {
118120
record.value = newRecord;
119121
}
120122
123+
function checkIfWeCanLeavePage() {
124+
return wasSaveSuccessful.value || cancelButtonClicked.value || JSON.stringify(record.value) === JSON.stringify(initialValues.value);
125+
}
126+
127+
function onBeforeUnload(event: BeforeUnloadEvent) {
128+
if (!checkIfWeCanLeavePage()) {
129+
event.preventDefault();
130+
event.returnValue = '';
131+
}
132+
}
133+
134+
window.addEventListener('beforeunload', onBeforeUnload);
135+
136+
onBeforeUnmount(() => {
137+
window.removeEventListener('beforeunload', onBeforeUnload);
138+
});
139+
140+
onBeforeRouteLeave(async (to, from, next) => {
141+
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);
144+
}
145+
next();
146+
});
147+
121148
onBeforeMount(() => {
122149
clearSaveInterceptors(route.params.resourceId as string);
123150
});
@@ -202,6 +229,7 @@ async function saveRecord() {
202229
showErrorTost(response.error);
203230
} else {
204231
saving.value = false;
232+
wasSaveSuccessful.value = true;
205233
if (route.query.returnTo) {
206234
router.push(<string>route.query.returnTo);
207235
} else {

adminforth/spa/src/views/EditView.vue

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<BreadcrumbsWithButtons>
1313
<!-- save and cancle -->
14-
<button @click="$router.back()"
14+
<button @click="() => {cancelButtonClicked = true; $router.back()}"
1515
class="flex items-center py-1 px-3 me-2 text-sm font-medium text-lightEditViewButtonText rounded-default focus:outline-none bg-lightEditViewButtonBackground rounded border border-lightEditViewButtonBorder hover:bg-lightEditViewButtonBackgroundHover hover:text-lightEditViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightEditViewButtonFocusRing dark:focus:ring-darkEditViewButtonFocusRing dark:bg-darkEditViewButtonBackground dark:text-darkEditViewButtonText dark:border-darkEditViewButtonBorder dark:hover:text-darkEditViewButtonTextHover dark:hover:bg-darkEditViewButtonBackgroundHover"
1616
>
1717
{{ $t('Cancel') }}
@@ -76,8 +76,8 @@ import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
7676
import { useCoreStore } from '@/stores/core';
7777
import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown } from '@/utils';
7878
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
79-
import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch } from 'vue';
80-
import { useRoute, useRouter } from 'vue-router';
79+
import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch, onBeforeUnmount } from 'vue';
80+
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
8181
import { showErrorTost } from '@/composables/useFrontendApi';
8282
import ThreeDotsMenu from '@/components/ThreeDotsMenu.vue';
8383
import { useAdminforth } from '@/adminforth';
@@ -87,7 +87,7 @@ import type { AdminForthResourceColumn } from '@/types/Back';
8787
8888
const { t } = useI18n();
8989
const coreStore = useCoreStore();
90-
const { clearSaveInterceptors, runSaveInterceptors, alert } = useAdminforth();
90+
const { clearSaveInterceptors, runSaveInterceptors, alert, confirm } = useAdminforth();
9191
9292
const isValid = ref(false);
9393
const validating = ref(false);
@@ -101,6 +101,36 @@ const saving = ref(false);
101101
102102
const record: Ref<Record<string, any>> = ref({});
103103
104+
const initialRecord = computed(() => coreStore.record);
105+
const wasSaveSuccessful = ref(false);
106+
const cancelButtonClicked = ref(false);
107+
108+
function onBeforeUnload(event: BeforeUnloadEvent) {
109+
if (!checkIfWeCanLeavePage()) {
110+
event.preventDefault();
111+
event.returnValue = '';
112+
}
113+
}
114+
115+
function checkIfWeCanLeavePage() {
116+
return wasSaveSuccessful.value || cancelButtonClicked.value || JSON.stringify(record.value) === JSON.stringify(initialRecord.value);
117+
}
118+
119+
window.addEventListener('beforeunload', onBeforeUnload);
120+
121+
onBeforeUnmount(() => {
122+
window.removeEventListener('beforeunload', onBeforeUnload);
123+
});
124+
125+
onBeforeRouteLeave(async (to, from, next) => {
126+
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);
129+
}
130+
next();
131+
});
132+
133+
104134
watch(record, (newVal) => {
105135
console.log('Record updated:', newVal);
106136
}, { deep: true });
@@ -198,24 +228,28 @@ async function saveRecord() {
198228
if (columnIsUpdated) {
199229
updates[key] = record.value[key];
200230
}
201-
saving.value = false;
202231
}
203-
204-
const resp = await callAdminForthApi({
205-
method: 'POST',
206-
path: `/update_record`,
207-
body: {
208-
resourceId: route.params.resourceId,
209-
recordId: route.params.primaryKey,
210-
record: updates,
211-
meta: {
212-
...(interceptorConfirmationResult ? { confirmationResult: interceptorConfirmationResult } : {}),
232+
let resp = null;
233+
try {
234+
resp = await callAdminForthApi({
235+
method: 'POST',
236+
path: `/update_record`,
237+
body: {
238+
resourceId: route.params.resourceId,
239+
recordId: route.params.primaryKey,
240+
record: updates,
241+
meta: {
242+
...(interceptorConfirmationResult ? { confirmationResult: interceptorConfirmationResult } : {}),
243+
},
213244
},
214-
},
215-
});
245+
});
246+
} finally {
247+
saving.value = false;
248+
}
216249
if (resp.error && resp.error !== 'Operation aborted by hook') {
217250
showErrorTost(resp.error);
218251
} else {
252+
wasSaveSuccessful.value = true;
219253
alert({
220254
message: t('Record updated successfully'),
221255
variant: 'success',

adminforth/types/Back.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,15 @@ export type BeforeDataSourceRequestFunction = (params: {
505505
requestUrl: string,
506506
},
507507
adminforth: IAdminForth,
508-
}) => Promise<{ok: boolean, error?: string, newRecordId?: string}>;
508+
}) => Promise<{
509+
ok: boolean,
510+
error?: string | null,
511+
/**
512+
* @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
513+
*/
514+
newRecordId?: string,
515+
redirectToRecordId?: string
516+
}>;
509517

510518
/**
511519
* Modify response to change how data is returned after fetching from database.
@@ -549,8 +557,15 @@ export type CreateResourceRecordResult = {
549557
/**
550558
* Optional id of an existing record to redirect to
551559
* (used when a beforeSave hook aborts creation and supplies newRecordId, allows to implement programmatic creation via API).
560+
* @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
552561
*/
553562
newRecordId?: any;
563+
564+
/**
565+
* Optional id of an existing record to redirect to
566+
* (used when a beforeSave hook aborts creation and supplies redirectToRecordId, allows to implement programmatic creation via API).
567+
*/
568+
redirectToRecordId?: any;
554569
};
555570

556571
/**
@@ -838,7 +853,15 @@ export type BeforeCreateSaveFunction = (params: {
838853
response: IAdminForthHttpResponse,
839854

840855
extra?: HttpExtra,
841-
}) => Promise<{ok: boolean, error?: string | null, newRecordId?: string}>;
856+
}) => Promise<{
857+
ok: boolean,
858+
error?: string | null,
859+
/**
860+
* @deprecated Since 1.2.9. Will be removed in 2.0.0. Use redirectToRecordId instead.
861+
*/
862+
newRecordId?: string,
863+
redirectToRecordId?: string
864+
}>;
842865

843866
export type AfterCreateSaveFunction = (params: {
844867
/**

0 commit comments

Comments
 (0)