@adminforth/crud-approve-plugin 1.0.21 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +3 -2
- package/custom/ApprovalDiff.vue +192 -0
- package/custom/ListPageDiffView.vue +2 -76
- package/custom/ShowPageDiffView.vue +7 -83
- package/dist/custom/ApprovalDiff.vue +192 -0
- package/dist/custom/ListPageDiffView.vue +2 -76
- package/dist/custom/ShowPageDiffView.vue +7 -83
- package/package.json +2 -2
package/build.log
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
|
+
custom/ApprovalDiff.vue
|
|
7
8
|
custom/ListPageDiffView.vue
|
|
8
9
|
custom/ShowPageDiffView.vue
|
|
9
10
|
custom/package-lock.json
|
|
@@ -11,5 +12,5 @@ custom/package.json
|
|
|
11
12
|
custom/pnpm-lock.yaml
|
|
12
13
|
custom/tsconfig.json
|
|
13
14
|
|
|
14
|
-
sent
|
|
15
|
-
total size is
|
|
15
|
+
sent 35,137 bytes received 153 bytes 70,580.00 bytes/sec
|
|
16
|
+
total size is 34,571 speedup is 0.98
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useCoreStore } from '@/stores/core';
|
|
3
|
+
import { computed, ref, watch, defineAsyncComponent, onMounted, useTemplateRef } from 'vue';
|
|
4
|
+
import { useElementSize } from '@vueuse/core';
|
|
5
|
+
import { useI18n } from 'vue-i18n';
|
|
6
|
+
import { callAdminForthApi } from '@/utils';
|
|
7
|
+
import adminforth from '@/adminforth';
|
|
8
|
+
import { Button } from '@/afcl';
|
|
9
|
+
|
|
10
|
+
const DiffView = defineAsyncComponent(async () => {
|
|
11
|
+
// pure styles come without tailwind preflight, which would otherwise reset fonts of the whole SPA
|
|
12
|
+
await import("@git-diff-view/vue/styles/diff-view-pure.css");
|
|
13
|
+
const { DiffView } = await import("@git-diff-view/vue");
|
|
14
|
+
return DiffView;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// old and new records go side by side only when the container fits two JSON columns
|
|
18
|
+
const SPLIT_MIN_WIDTH_PX = 768;
|
|
19
|
+
const PENDING_STATUS = 1;
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
record: Object,
|
|
23
|
+
meta: Object,
|
|
24
|
+
resource: Object,
|
|
25
|
+
// only for a container whose width does not follow its content, a table cell of auto layout would toggle the modes
|
|
26
|
+
splitWhenWide: Boolean,
|
|
27
|
+
});
|
|
28
|
+
const emit = defineEmits(['decided']);
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
const coreStore = useCoreStore();
|
|
31
|
+
const theme = computed(() => coreStore.theme === 'dark' ? 'dark' : 'light');
|
|
32
|
+
|
|
33
|
+
const { oldRecord, newRecord } = props.record[props.meta.resourceColumns.dataColumnName];
|
|
34
|
+
const oldContent = JSON.stringify(oldRecord, null, 2);
|
|
35
|
+
const newContent = JSON.stringify(newRecord, null, 2);
|
|
36
|
+
const hasChanges = oldContent !== newContent;
|
|
37
|
+
const isPending = props.record[props.meta.resourceColumns.statusColumnName] === PENDING_STATUS;
|
|
38
|
+
|
|
39
|
+
const { width } = useElementSize(useTemplateRef('container'));
|
|
40
|
+
const split = computed(() => props.splitWhenWide && width.value >= SPLIT_MIN_WIDTH_PX);
|
|
41
|
+
|
|
42
|
+
const diffFile = ref();
|
|
43
|
+
const diffModes = ref();
|
|
44
|
+
const sending = ref(null);
|
|
45
|
+
|
|
46
|
+
function buildDiffLines() {
|
|
47
|
+
if (split.value) {
|
|
48
|
+
diffFile.value.buildSplitDiffLines();
|
|
49
|
+
} else {
|
|
50
|
+
diffFile.value.buildUnifiedDiffLines();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMounted(async () => {
|
|
55
|
+
if (!hasChanges) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const { DiffModeEnum } = await import("@git-diff-view/vue");
|
|
59
|
+
const { generateDiffFile } = await import("@git-diff-view/file");
|
|
60
|
+
diffModes.value = DiffModeEnum;
|
|
61
|
+
const file = generateDiffFile('diff.json', oldContent, 'diff.json', newContent, 'json', 'json');
|
|
62
|
+
file.initTheme(theme.value);
|
|
63
|
+
file.init();
|
|
64
|
+
diffFile.value = file;
|
|
65
|
+
buildDiffLines();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watch([split, theme], () => {
|
|
69
|
+
if (!diffFile.value) return;
|
|
70
|
+
diffFile.value.initTheme(theme.value);
|
|
71
|
+
buildDiffLines();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function sendApproveRequest(approved) {
|
|
75
|
+
sending.value = approved ? 'approve' : 'reject';
|
|
76
|
+
const data = await callAdminForthApi({
|
|
77
|
+
path: `/plugin/crud-approve/update-status`,
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: {
|
|
80
|
+
connectorId: props.resource.connectorId,
|
|
81
|
+
resourceId: props.resource.resourceId,
|
|
82
|
+
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
83
|
+
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
84
|
+
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
85
|
+
approved: approved
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
sending.value = null;
|
|
89
|
+
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
90
|
+
adminforth.alert({ message: t('Error: {error}', { error: data.error }), variant: 'danger' });
|
|
91
|
+
} else {
|
|
92
|
+
adminforth.alert({ message: approved ? t('Change approved') : t('Change rejected'), variant: 'success' });
|
|
93
|
+
emit('decided');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<div ref="container" class="af-crud-approve">
|
|
100
|
+
<div
|
|
101
|
+
v-if="diffFile"
|
|
102
|
+
class="af-crud-approve-diff overflow-hidden rounded-default border border-lightCardBorder dark:border-darkCardBorder"
|
|
103
|
+
>
|
|
104
|
+
<DiffView
|
|
105
|
+
:diff-file="diffFile"
|
|
106
|
+
:diff-view-mode="split ? diffModes.Split : diffModes.Unified"
|
|
107
|
+
:diff-view-theme="theme"
|
|
108
|
+
:diff-view-highlight="true"
|
|
109
|
+
:diff-view-wrap="true"
|
|
110
|
+
:diff-view-font-size="13"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<p v-else-if="!hasChanges" class="text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">
|
|
114
|
+
{{ $t('No changes to display') }}
|
|
115
|
+
</p>
|
|
116
|
+
<div v-if="isPending" class="af-crud-approve-actions mt-4 flex gap-2">
|
|
117
|
+
<Button
|
|
118
|
+
class="af-crud-approve-approve-button flex-1"
|
|
119
|
+
:loader="sending === 'approve'"
|
|
120
|
+
:disabled="!!sending"
|
|
121
|
+
@click="sendApproveRequest(true)"
|
|
122
|
+
>
|
|
123
|
+
{{ $t('Approve') }}
|
|
124
|
+
</Button>
|
|
125
|
+
<Button
|
|
126
|
+
class="af-crud-approve-reject-button flex-1"
|
|
127
|
+
variant="danger"
|
|
128
|
+
:loader="sending === 'reject'"
|
|
129
|
+
:disabled="!!sending"
|
|
130
|
+
@click="sendApproveRequest(false)"
|
|
131
|
+
>
|
|
132
|
+
{{ $t('Reject') }}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<style>
|
|
139
|
+
/* the diff view sets Menlo inline on the tables of both modes */
|
|
140
|
+
.af-crud-approve-diff .diff-table-scroll-container,
|
|
141
|
+
.af-crud-approve-diff .diff-table-wrapper {
|
|
142
|
+
font-family: theme('fontFamily.mono') !important;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.af-crud-approve-diff .diff-tailwindcss-wrapper[data-theme="light"] .diff-style-root {
|
|
146
|
+
--diff-border--: theme('colors.lightCardBorder');
|
|
147
|
+
--diff-plain-content--: theme('colors.lightCardBackground');
|
|
148
|
+
--diff-plain-lineNumber--: theme('colors.lightCardBackground');
|
|
149
|
+
--diff-plain-lineNumber-color--: theme('colors.lightInputPlaceholderText');
|
|
150
|
+
--diff-expand-content--: theme('colors.lightCardBackgroundHover');
|
|
151
|
+
--diff-expand-lineNumber--: theme('colors.lightCardBackgroundHover');
|
|
152
|
+
--diff-expand-lineNumber-color--: theme('colors.lightInputPlaceholderText');
|
|
153
|
+
--diff-empty-content--: theme('colors.lightCardBackgroundHover');
|
|
154
|
+
--diff-hunk-content--: theme('colors.lightCardBackgroundHover');
|
|
155
|
+
--diff-hunk-lineNumber--: theme('colors.lightCardBackgroundHover');
|
|
156
|
+
--diff-hunk-lineNumber-hover--: theme('colors.lightPrimary');
|
|
157
|
+
--diff-hunk-content-color--: theme('colors.lightShowTableUnderHeadingText');
|
|
158
|
+
--diff-add-widget--: theme('colors.lightPrimary');
|
|
159
|
+
--diff-add-widget-color--: theme('colors.lightPrimaryContrast');
|
|
160
|
+
--diff-add-content--: theme('colors.lightSuccessSoft');
|
|
161
|
+
--diff-add-lineNumber--: theme('colors.lightSuccessSoft');
|
|
162
|
+
--diff-add-content-highlight--: theme('colors.lightSuccessBorder');
|
|
163
|
+
--diff-del-content--: theme('colors.lightDangerSoft');
|
|
164
|
+
--diff-del-lineNumber--: theme('colors.lightDangerSoft');
|
|
165
|
+
--diff-del-content-highlight--: theme('colors.lightDangerBorder');
|
|
166
|
+
color: theme('colors.lightShowTableBodyText');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.af-crud-approve-diff .diff-tailwindcss-wrapper[data-theme="dark"] .diff-style-root {
|
|
170
|
+
--diff-border--: theme('colors.darkCardBorder');
|
|
171
|
+
--diff-plain-content--: theme('colors.darkCardBackground');
|
|
172
|
+
--diff-plain-lineNumber--: theme('colors.darkCardBackground');
|
|
173
|
+
--diff-plain-lineNumber-color--: theme('colors.darkInputPlaceholderText');
|
|
174
|
+
--diff-expand-content--: theme('colors.darkCardBackgroundHover');
|
|
175
|
+
--diff-expand-lineNumber--: theme('colors.darkCardBackgroundHover');
|
|
176
|
+
--diff-expand-lineNumber-color--: theme('colors.darkInputPlaceholderText');
|
|
177
|
+
--diff-empty-content--: theme('colors.darkCardBackgroundHover');
|
|
178
|
+
--diff-hunk-content--: theme('colors.darkCardBackgroundHover');
|
|
179
|
+
--diff-hunk-lineNumber--: theme('colors.darkCardBackgroundHover');
|
|
180
|
+
--diff-hunk-lineNumber-hover--: theme('colors.darkPrimary');
|
|
181
|
+
--diff-hunk-content-color--: theme('colors.darkShowTableUnderHeadingText');
|
|
182
|
+
--diff-add-widget--: theme('colors.darkPrimary');
|
|
183
|
+
--diff-add-widget-color--: theme('colors.darkPrimaryContrast');
|
|
184
|
+
--diff-add-content--: theme('colors.darkSuccessSoft');
|
|
185
|
+
--diff-add-lineNumber--: theme('colors.darkSuccessSoft');
|
|
186
|
+
--diff-add-content-highlight--: theme('colors.darkSuccessBorder');
|
|
187
|
+
--diff-del-content--: theme('colors.darkDangerSoft');
|
|
188
|
+
--diff-del-lineNumber--: theme('colors.darkDangerSoft');
|
|
189
|
+
--diff-del-content-highlight--: theme('colors.darkDangerBorder');
|
|
190
|
+
color: theme('colors.darkShowTableBodyText');
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -1,84 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useCoreStore } from '@/stores/core';
|
|
3
|
-
import { computed, ref, watch } from 'vue';
|
|
4
|
-
import "@git-diff-view/vue/styles/diff-view.css";
|
|
5
|
-
import { DiffView, DiffModeEnum } from "@git-diff-view/vue";
|
|
6
|
-
import { generateDiffFile } from "@git-diff-view/file";
|
|
7
|
-
import { callAdminForthApi } from '@/utils';
|
|
8
2
|
import adminforth from '@/adminforth';
|
|
9
|
-
import
|
|
10
|
-
|
|
3
|
+
import ApprovalDiff from './ApprovalDiff.vue';
|
|
11
4
|
|
|
12
5
|
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
13
|
-
const coreStore = useCoreStore();
|
|
14
|
-
const theme = computed(() => coreStore.theme);
|
|
15
|
-
|
|
16
|
-
const oldContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].oldRecord, null, 2)
|
|
17
|
-
const newContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].newRecord, null, 2)
|
|
18
|
-
const hasChanges = oldContent !== newContent;
|
|
19
|
-
|
|
20
|
-
const diffFile = ref();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async function sendApproveRequest(approved) {
|
|
24
|
-
const data = await callAdminForthApi({
|
|
25
|
-
path: `/plugin/crud-approve/update-status`,
|
|
26
|
-
method: 'POST',
|
|
27
|
-
body: {
|
|
28
|
-
connectorId: props.resource.connectorId,
|
|
29
|
-
resourceId: props.resource.resourceId,
|
|
30
|
-
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
31
|
-
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
32
|
-
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
33
|
-
approved: approved
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
37
|
-
adminforth.alert({ message: `Error: ${data.error}`, variant: 'warning' });
|
|
38
|
-
} else {
|
|
39
|
-
adminforth.alert({ message: `Successfully ${approved ? 'approved' : 'rejected'} the change.`, variant: 'success' });
|
|
40
|
-
// reload page
|
|
41
|
-
adminforth.list.refresh();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function initDiffFile() {
|
|
46
|
-
const file = generateDiffFile(
|
|
47
|
-
'diff.json', oldContent,
|
|
48
|
-
'diff.json', newContent,
|
|
49
|
-
'json', 'json'
|
|
50
|
-
);
|
|
51
|
-
file.initTheme(theme.value === 'dark' ? 'dark' : 'light');
|
|
52
|
-
file.init();
|
|
53
|
-
file.buildUnifiedDiffLines();
|
|
54
|
-
diffFile.value = file;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (hasChanges) {
|
|
58
|
-
initDiffFile();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
watch([theme], ([t]) => {
|
|
62
|
-
if (!diffFile.value) return;
|
|
63
|
-
diffFile.value.initTheme(t === 'dark' ? 'dark' : 'light');
|
|
64
|
-
diffFile.value.buildUnifiedDiffLines();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
6
|
</script>
|
|
68
7
|
|
|
69
8
|
<template>
|
|
70
|
-
<
|
|
71
|
-
v-if="hasChanges"
|
|
72
|
-
:diff-file="diffFile"
|
|
73
|
-
:diff-view-mode="DiffModeEnum.Unified"
|
|
74
|
-
:diff-view-theme="theme === 'dark' ? 'dark' : 'light'"
|
|
75
|
-
:diff-view-highlight="true"
|
|
76
|
-
:diff-view-wrap="true"
|
|
77
|
-
:diff-view-font-size="14"
|
|
78
|
-
/>
|
|
79
|
-
<div v-else>No changes</div>
|
|
80
|
-
<div v-if="record[meta.resourceColumns.statusColumnName] === 1" style="margin-top: 16px; display: flex; gap: 8px;">
|
|
81
|
-
<Button style="background-color: green; color: white;" @click="sendApproveRequest(true)" :loader="false" class="w-full">Approve</Button>
|
|
82
|
-
<Button style="background-color: red; color: white;" @click="sendApproveRequest(false)" :loader="false" class="w-full">Reject</Button>
|
|
83
|
-
</div>
|
|
9
|
+
<ApprovalDiff :record="record" :meta="meta" :resource="resource" @decided="adminforth.list.refresh()" />
|
|
84
10
|
</template>
|
|
@@ -1,93 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useCoreStore } from '@/stores/core';
|
|
3
|
-
import { computed, ref, watch } from 'vue';
|
|
4
|
-
import "@git-diff-view/vue/styles/diff-view.css";
|
|
5
|
-
import { DiffView, DiffModeEnum } from "@git-diff-view/vue";
|
|
6
|
-
import { generateDiffFile } from "@git-diff-view/file";
|
|
7
|
-
import { callAdminForthApi } from '@/utils';
|
|
8
|
-
import adminforth from '@/adminforth';
|
|
9
|
-
import { Button } from '@/afcl'
|
|
10
2
|
import { useRouter } from 'vue-router';
|
|
3
|
+
import ApprovalDiff from './ApprovalDiff.vue';
|
|
11
4
|
|
|
12
5
|
const router = useRouter();
|
|
13
6
|
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
14
|
-
const coreStore = useCoreStore();
|
|
15
|
-
const theme = computed(() => coreStore.theme);
|
|
16
|
-
const isMobile = computed(() => /(Android|iPhone|iPad|iPod)/i.test(navigator.userAgent));
|
|
17
|
-
const mode = computed(() => isMobile.value ? DiffModeEnum.Unified : DiffModeEnum.Split);
|
|
18
|
-
|
|
19
|
-
const oldContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].oldRecord, null, 2)
|
|
20
|
-
const newContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].newRecord, null, 2)
|
|
21
|
-
const hasChanges = oldContent !== newContent;
|
|
22
|
-
|
|
23
|
-
const diffFile = ref();
|
|
24
|
-
|
|
25
|
-
async function sendApproveRequest(approved) {
|
|
26
|
-
const data = await callAdminForthApi({
|
|
27
|
-
path: `/plugin/crud-approve/update-status`,
|
|
28
|
-
method: 'POST',
|
|
29
|
-
body: {
|
|
30
|
-
connectorId: props.resource.connectorId,
|
|
31
|
-
resourceId: props.resource.resourceId,
|
|
32
|
-
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
33
|
-
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
34
|
-
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
35
|
-
approved: approved
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
39
|
-
adminforth.alert({ message: `Error: ${data.error}`, variant: 'warning' });
|
|
40
|
-
} else {
|
|
41
|
-
adminforth.alert({ message: `Successfully ${approved ? 'approved' : 'rejected'} the change.`, variant: 'success' });
|
|
42
|
-
router.push(router.currentRoute.value.fullPath.split('/show')[0]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function initDiffFile() {
|
|
47
|
-
const file = generateDiffFile(
|
|
48
|
-
'diff.json', oldContent,
|
|
49
|
-
'diff.json', newContent,
|
|
50
|
-
'json', 'json'
|
|
51
|
-
);
|
|
52
|
-
file.initTheme(theme.value === 'dark' ? 'dark' : 'light');
|
|
53
|
-
file.init();
|
|
54
|
-
if (mode.value === DiffModeEnum.Split) {
|
|
55
|
-
file.buildSplitDiffLines();
|
|
56
|
-
} else {
|
|
57
|
-
file.buildUnifiedDiffLines();
|
|
58
|
-
}
|
|
59
|
-
diffFile.value = file;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (hasChanges) {
|
|
63
|
-
initDiffFile();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
watch([mode, theme], ([m, t]) => {
|
|
67
|
-
if (!diffFile.value) return;
|
|
68
|
-
diffFile.value.initTheme(t === 'dark' ? 'dark' : 'light');
|
|
69
|
-
if (m === DiffModeEnum.Split) {
|
|
70
|
-
diffFile.value.buildSplitDiffLines();
|
|
71
|
-
} else {
|
|
72
|
-
diffFile.value.buildUnifiedDiffLines();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
7
|
</script>
|
|
77
8
|
|
|
78
9
|
<template>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
:
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
:diff-view-wrap="true"
|
|
86
|
-
:diff-view-font-size="14"
|
|
10
|
+
<ApprovalDiff
|
|
11
|
+
:record="record"
|
|
12
|
+
:meta="meta"
|
|
13
|
+
:resource="resource"
|
|
14
|
+
splitWhenWide
|
|
15
|
+
@decided="router.push({ name: 'resource-list', params: { resourceId: resource.resourceId } })"
|
|
87
16
|
/>
|
|
88
|
-
<div v-else>No changes</div>
|
|
89
|
-
<div v-if="record[meta.resourceColumns.statusColumnName] === 1" style="margin-top: 16px; display: flex; gap: 8px;">
|
|
90
|
-
<Button style="background-color: green; color: white;" @click="sendApproveRequest(true)" :loader="false" class="w-full">Approve</Button>
|
|
91
|
-
<Button style="background-color: red; color: white;" @click="sendApproveRequest(false)" :loader="false" class="w-full">Reject</Button>
|
|
92
|
-
</div>
|
|
93
17
|
</template>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useCoreStore } from '@/stores/core';
|
|
3
|
+
import { computed, ref, watch, defineAsyncComponent, onMounted, useTemplateRef } from 'vue';
|
|
4
|
+
import { useElementSize } from '@vueuse/core';
|
|
5
|
+
import { useI18n } from 'vue-i18n';
|
|
6
|
+
import { callAdminForthApi } from '@/utils';
|
|
7
|
+
import adminforth from '@/adminforth';
|
|
8
|
+
import { Button } from '@/afcl';
|
|
9
|
+
|
|
10
|
+
const DiffView = defineAsyncComponent(async () => {
|
|
11
|
+
// pure styles come without tailwind preflight, which would otherwise reset fonts of the whole SPA
|
|
12
|
+
await import("@git-diff-view/vue/styles/diff-view-pure.css");
|
|
13
|
+
const { DiffView } = await import("@git-diff-view/vue");
|
|
14
|
+
return DiffView;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// old and new records go side by side only when the container fits two JSON columns
|
|
18
|
+
const SPLIT_MIN_WIDTH_PX = 768;
|
|
19
|
+
const PENDING_STATUS = 1;
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
record: Object,
|
|
23
|
+
meta: Object,
|
|
24
|
+
resource: Object,
|
|
25
|
+
// only for a container whose width does not follow its content, a table cell of auto layout would toggle the modes
|
|
26
|
+
splitWhenWide: Boolean,
|
|
27
|
+
});
|
|
28
|
+
const emit = defineEmits(['decided']);
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
const coreStore = useCoreStore();
|
|
31
|
+
const theme = computed(() => coreStore.theme === 'dark' ? 'dark' : 'light');
|
|
32
|
+
|
|
33
|
+
const { oldRecord, newRecord } = props.record[props.meta.resourceColumns.dataColumnName];
|
|
34
|
+
const oldContent = JSON.stringify(oldRecord, null, 2);
|
|
35
|
+
const newContent = JSON.stringify(newRecord, null, 2);
|
|
36
|
+
const hasChanges = oldContent !== newContent;
|
|
37
|
+
const isPending = props.record[props.meta.resourceColumns.statusColumnName] === PENDING_STATUS;
|
|
38
|
+
|
|
39
|
+
const { width } = useElementSize(useTemplateRef('container'));
|
|
40
|
+
const split = computed(() => props.splitWhenWide && width.value >= SPLIT_MIN_WIDTH_PX);
|
|
41
|
+
|
|
42
|
+
const diffFile = ref();
|
|
43
|
+
const diffModes = ref();
|
|
44
|
+
const sending = ref(null);
|
|
45
|
+
|
|
46
|
+
function buildDiffLines() {
|
|
47
|
+
if (split.value) {
|
|
48
|
+
diffFile.value.buildSplitDiffLines();
|
|
49
|
+
} else {
|
|
50
|
+
diffFile.value.buildUnifiedDiffLines();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMounted(async () => {
|
|
55
|
+
if (!hasChanges) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const { DiffModeEnum } = await import("@git-diff-view/vue");
|
|
59
|
+
const { generateDiffFile } = await import("@git-diff-view/file");
|
|
60
|
+
diffModes.value = DiffModeEnum;
|
|
61
|
+
const file = generateDiffFile('diff.json', oldContent, 'diff.json', newContent, 'json', 'json');
|
|
62
|
+
file.initTheme(theme.value);
|
|
63
|
+
file.init();
|
|
64
|
+
diffFile.value = file;
|
|
65
|
+
buildDiffLines();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watch([split, theme], () => {
|
|
69
|
+
if (!diffFile.value) return;
|
|
70
|
+
diffFile.value.initTheme(theme.value);
|
|
71
|
+
buildDiffLines();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function sendApproveRequest(approved) {
|
|
75
|
+
sending.value = approved ? 'approve' : 'reject';
|
|
76
|
+
const data = await callAdminForthApi({
|
|
77
|
+
path: `/plugin/crud-approve/update-status`,
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: {
|
|
80
|
+
connectorId: props.resource.connectorId,
|
|
81
|
+
resourceId: props.resource.resourceId,
|
|
82
|
+
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
83
|
+
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
84
|
+
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
85
|
+
approved: approved
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
sending.value = null;
|
|
89
|
+
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
90
|
+
adminforth.alert({ message: t('Error: {error}', { error: data.error }), variant: 'danger' });
|
|
91
|
+
} else {
|
|
92
|
+
adminforth.alert({ message: approved ? t('Change approved') : t('Change rejected'), variant: 'success' });
|
|
93
|
+
emit('decided');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<div ref="container" class="af-crud-approve">
|
|
100
|
+
<div
|
|
101
|
+
v-if="diffFile"
|
|
102
|
+
class="af-crud-approve-diff overflow-hidden rounded-default border border-lightCardBorder dark:border-darkCardBorder"
|
|
103
|
+
>
|
|
104
|
+
<DiffView
|
|
105
|
+
:diff-file="diffFile"
|
|
106
|
+
:diff-view-mode="split ? diffModes.Split : diffModes.Unified"
|
|
107
|
+
:diff-view-theme="theme"
|
|
108
|
+
:diff-view-highlight="true"
|
|
109
|
+
:diff-view-wrap="true"
|
|
110
|
+
:diff-view-font-size="13"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<p v-else-if="!hasChanges" class="text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">
|
|
114
|
+
{{ $t('No changes to display') }}
|
|
115
|
+
</p>
|
|
116
|
+
<div v-if="isPending" class="af-crud-approve-actions mt-4 flex gap-2">
|
|
117
|
+
<Button
|
|
118
|
+
class="af-crud-approve-approve-button flex-1"
|
|
119
|
+
:loader="sending === 'approve'"
|
|
120
|
+
:disabled="!!sending"
|
|
121
|
+
@click="sendApproveRequest(true)"
|
|
122
|
+
>
|
|
123
|
+
{{ $t('Approve') }}
|
|
124
|
+
</Button>
|
|
125
|
+
<Button
|
|
126
|
+
class="af-crud-approve-reject-button flex-1"
|
|
127
|
+
variant="danger"
|
|
128
|
+
:loader="sending === 'reject'"
|
|
129
|
+
:disabled="!!sending"
|
|
130
|
+
@click="sendApproveRequest(false)"
|
|
131
|
+
>
|
|
132
|
+
{{ $t('Reject') }}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<style>
|
|
139
|
+
/* the diff view sets Menlo inline on the tables of both modes */
|
|
140
|
+
.af-crud-approve-diff .diff-table-scroll-container,
|
|
141
|
+
.af-crud-approve-diff .diff-table-wrapper {
|
|
142
|
+
font-family: theme('fontFamily.mono') !important;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.af-crud-approve-diff .diff-tailwindcss-wrapper[data-theme="light"] .diff-style-root {
|
|
146
|
+
--diff-border--: theme('colors.lightCardBorder');
|
|
147
|
+
--diff-plain-content--: theme('colors.lightCardBackground');
|
|
148
|
+
--diff-plain-lineNumber--: theme('colors.lightCardBackground');
|
|
149
|
+
--diff-plain-lineNumber-color--: theme('colors.lightInputPlaceholderText');
|
|
150
|
+
--diff-expand-content--: theme('colors.lightCardBackgroundHover');
|
|
151
|
+
--diff-expand-lineNumber--: theme('colors.lightCardBackgroundHover');
|
|
152
|
+
--diff-expand-lineNumber-color--: theme('colors.lightInputPlaceholderText');
|
|
153
|
+
--diff-empty-content--: theme('colors.lightCardBackgroundHover');
|
|
154
|
+
--diff-hunk-content--: theme('colors.lightCardBackgroundHover');
|
|
155
|
+
--diff-hunk-lineNumber--: theme('colors.lightCardBackgroundHover');
|
|
156
|
+
--diff-hunk-lineNumber-hover--: theme('colors.lightPrimary');
|
|
157
|
+
--diff-hunk-content-color--: theme('colors.lightShowTableUnderHeadingText');
|
|
158
|
+
--diff-add-widget--: theme('colors.lightPrimary');
|
|
159
|
+
--diff-add-widget-color--: theme('colors.lightPrimaryContrast');
|
|
160
|
+
--diff-add-content--: theme('colors.lightSuccessSoft');
|
|
161
|
+
--diff-add-lineNumber--: theme('colors.lightSuccessSoft');
|
|
162
|
+
--diff-add-content-highlight--: theme('colors.lightSuccessBorder');
|
|
163
|
+
--diff-del-content--: theme('colors.lightDangerSoft');
|
|
164
|
+
--diff-del-lineNumber--: theme('colors.lightDangerSoft');
|
|
165
|
+
--diff-del-content-highlight--: theme('colors.lightDangerBorder');
|
|
166
|
+
color: theme('colors.lightShowTableBodyText');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.af-crud-approve-diff .diff-tailwindcss-wrapper[data-theme="dark"] .diff-style-root {
|
|
170
|
+
--diff-border--: theme('colors.darkCardBorder');
|
|
171
|
+
--diff-plain-content--: theme('colors.darkCardBackground');
|
|
172
|
+
--diff-plain-lineNumber--: theme('colors.darkCardBackground');
|
|
173
|
+
--diff-plain-lineNumber-color--: theme('colors.darkInputPlaceholderText');
|
|
174
|
+
--diff-expand-content--: theme('colors.darkCardBackgroundHover');
|
|
175
|
+
--diff-expand-lineNumber--: theme('colors.darkCardBackgroundHover');
|
|
176
|
+
--diff-expand-lineNumber-color--: theme('colors.darkInputPlaceholderText');
|
|
177
|
+
--diff-empty-content--: theme('colors.darkCardBackgroundHover');
|
|
178
|
+
--diff-hunk-content--: theme('colors.darkCardBackgroundHover');
|
|
179
|
+
--diff-hunk-lineNumber--: theme('colors.darkCardBackgroundHover');
|
|
180
|
+
--diff-hunk-lineNumber-hover--: theme('colors.darkPrimary');
|
|
181
|
+
--diff-hunk-content-color--: theme('colors.darkShowTableUnderHeadingText');
|
|
182
|
+
--diff-add-widget--: theme('colors.darkPrimary');
|
|
183
|
+
--diff-add-widget-color--: theme('colors.darkPrimaryContrast');
|
|
184
|
+
--diff-add-content--: theme('colors.darkSuccessSoft');
|
|
185
|
+
--diff-add-lineNumber--: theme('colors.darkSuccessSoft');
|
|
186
|
+
--diff-add-content-highlight--: theme('colors.darkSuccessBorder');
|
|
187
|
+
--diff-del-content--: theme('colors.darkDangerSoft');
|
|
188
|
+
--diff-del-lineNumber--: theme('colors.darkDangerSoft');
|
|
189
|
+
--diff-del-content-highlight--: theme('colors.darkDangerBorder');
|
|
190
|
+
color: theme('colors.darkShowTableBodyText');
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -1,84 +1,10 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useCoreStore } from '@/stores/core';
|
|
3
|
-
import { computed, ref, watch } from 'vue';
|
|
4
|
-
import "@git-diff-view/vue/styles/diff-view.css";
|
|
5
|
-
import { DiffView, DiffModeEnum } from "@git-diff-view/vue";
|
|
6
|
-
import { generateDiffFile } from "@git-diff-view/file";
|
|
7
|
-
import { callAdminForthApi } from '@/utils';
|
|
8
2
|
import adminforth from '@/adminforth';
|
|
9
|
-
import
|
|
10
|
-
|
|
3
|
+
import ApprovalDiff from './ApprovalDiff.vue';
|
|
11
4
|
|
|
12
5
|
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
13
|
-
const coreStore = useCoreStore();
|
|
14
|
-
const theme = computed(() => coreStore.theme);
|
|
15
|
-
|
|
16
|
-
const oldContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].oldRecord, null, 2)
|
|
17
|
-
const newContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].newRecord, null, 2)
|
|
18
|
-
const hasChanges = oldContent !== newContent;
|
|
19
|
-
|
|
20
|
-
const diffFile = ref();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async function sendApproveRequest(approved) {
|
|
24
|
-
const data = await callAdminForthApi({
|
|
25
|
-
path: `/plugin/crud-approve/update-status`,
|
|
26
|
-
method: 'POST',
|
|
27
|
-
body: {
|
|
28
|
-
connectorId: props.resource.connectorId,
|
|
29
|
-
resourceId: props.resource.resourceId,
|
|
30
|
-
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
31
|
-
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
32
|
-
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
33
|
-
approved: approved
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
37
|
-
adminforth.alert({ message: `Error: ${data.error}`, variant: 'warning' });
|
|
38
|
-
} else {
|
|
39
|
-
adminforth.alert({ message: `Successfully ${approved ? 'approved' : 'rejected'} the change.`, variant: 'success' });
|
|
40
|
-
// reload page
|
|
41
|
-
adminforth.list.refresh();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function initDiffFile() {
|
|
46
|
-
const file = generateDiffFile(
|
|
47
|
-
'diff.json', oldContent,
|
|
48
|
-
'diff.json', newContent,
|
|
49
|
-
'json', 'json'
|
|
50
|
-
);
|
|
51
|
-
file.initTheme(theme.value === 'dark' ? 'dark' : 'light');
|
|
52
|
-
file.init();
|
|
53
|
-
file.buildUnifiedDiffLines();
|
|
54
|
-
diffFile.value = file;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (hasChanges) {
|
|
58
|
-
initDiffFile();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
watch([theme], ([t]) => {
|
|
62
|
-
if (!diffFile.value) return;
|
|
63
|
-
diffFile.value.initTheme(t === 'dark' ? 'dark' : 'light');
|
|
64
|
-
diffFile.value.buildUnifiedDiffLines();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
6
|
</script>
|
|
68
7
|
|
|
69
8
|
<template>
|
|
70
|
-
<
|
|
71
|
-
v-if="hasChanges"
|
|
72
|
-
:diff-file="diffFile"
|
|
73
|
-
:diff-view-mode="DiffModeEnum.Unified"
|
|
74
|
-
:diff-view-theme="theme === 'dark' ? 'dark' : 'light'"
|
|
75
|
-
:diff-view-highlight="true"
|
|
76
|
-
:diff-view-wrap="true"
|
|
77
|
-
:diff-view-font-size="14"
|
|
78
|
-
/>
|
|
79
|
-
<div v-else>No changes</div>
|
|
80
|
-
<div v-if="record[meta.resourceColumns.statusColumnName] === 1" style="margin-top: 16px; display: flex; gap: 8px;">
|
|
81
|
-
<Button style="background-color: green; color: white;" @click="sendApproveRequest(true)" :loader="false" class="w-full">Approve</Button>
|
|
82
|
-
<Button style="background-color: red; color: white;" @click="sendApproveRequest(false)" :loader="false" class="w-full">Reject</Button>
|
|
83
|
-
</div>
|
|
9
|
+
<ApprovalDiff :record="record" :meta="meta" :resource="resource" @decided="adminforth.list.refresh()" />
|
|
84
10
|
</template>
|
|
@@ -1,93 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useCoreStore } from '@/stores/core';
|
|
3
|
-
import { computed, ref, watch } from 'vue';
|
|
4
|
-
import "@git-diff-view/vue/styles/diff-view.css";
|
|
5
|
-
import { DiffView, DiffModeEnum } from "@git-diff-view/vue";
|
|
6
|
-
import { generateDiffFile } from "@git-diff-view/file";
|
|
7
|
-
import { callAdminForthApi } from '@/utils';
|
|
8
|
-
import adminforth from '@/adminforth';
|
|
9
|
-
import { Button } from '@/afcl'
|
|
10
2
|
import { useRouter } from 'vue-router';
|
|
3
|
+
import ApprovalDiff from './ApprovalDiff.vue';
|
|
11
4
|
|
|
12
5
|
const router = useRouter();
|
|
13
6
|
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
14
|
-
const coreStore = useCoreStore();
|
|
15
|
-
const theme = computed(() => coreStore.theme);
|
|
16
|
-
const isMobile = computed(() => /(Android|iPhone|iPad|iPod)/i.test(navigator.userAgent));
|
|
17
|
-
const mode = computed(() => isMobile.value ? DiffModeEnum.Unified : DiffModeEnum.Split);
|
|
18
|
-
|
|
19
|
-
const oldContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].oldRecord, null, 2)
|
|
20
|
-
const newContent = JSON.stringify(props.record[props.meta.resourceColumns.dataColumnName].newRecord, null, 2)
|
|
21
|
-
const hasChanges = oldContent !== newContent;
|
|
22
|
-
|
|
23
|
-
const diffFile = ref();
|
|
24
|
-
|
|
25
|
-
async function sendApproveRequest(approved) {
|
|
26
|
-
const data = await callAdminForthApi({
|
|
27
|
-
path: `/plugin/crud-approve/update-status`,
|
|
28
|
-
method: 'POST',
|
|
29
|
-
body: {
|
|
30
|
-
connectorId: props.resource.connectorId,
|
|
31
|
-
resourceId: props.resource.resourceId,
|
|
32
|
-
action: props.record[props.meta.resourceColumns.actionColumnName],
|
|
33
|
-
recordId: props.record[props.meta.resourceColumns.recordIdColumnName],
|
|
34
|
-
diffId: props.record[props.meta.resourceColumns.idColumnName],
|
|
35
|
-
approved: approved
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
if (data.error && data.error !== 'Operation aborted by hook') {
|
|
39
|
-
adminforth.alert({ message: `Error: ${data.error}`, variant: 'warning' });
|
|
40
|
-
} else {
|
|
41
|
-
adminforth.alert({ message: `Successfully ${approved ? 'approved' : 'rejected'} the change.`, variant: 'success' });
|
|
42
|
-
router.push(router.currentRoute.value.fullPath.split('/show')[0]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function initDiffFile() {
|
|
47
|
-
const file = generateDiffFile(
|
|
48
|
-
'diff.json', oldContent,
|
|
49
|
-
'diff.json', newContent,
|
|
50
|
-
'json', 'json'
|
|
51
|
-
);
|
|
52
|
-
file.initTheme(theme.value === 'dark' ? 'dark' : 'light');
|
|
53
|
-
file.init();
|
|
54
|
-
if (mode.value === DiffModeEnum.Split) {
|
|
55
|
-
file.buildSplitDiffLines();
|
|
56
|
-
} else {
|
|
57
|
-
file.buildUnifiedDiffLines();
|
|
58
|
-
}
|
|
59
|
-
diffFile.value = file;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (hasChanges) {
|
|
63
|
-
initDiffFile();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
watch([mode, theme], ([m, t]) => {
|
|
67
|
-
if (!diffFile.value) return;
|
|
68
|
-
diffFile.value.initTheme(t === 'dark' ? 'dark' : 'light');
|
|
69
|
-
if (m === DiffModeEnum.Split) {
|
|
70
|
-
diffFile.value.buildSplitDiffLines();
|
|
71
|
-
} else {
|
|
72
|
-
diffFile.value.buildUnifiedDiffLines();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
7
|
</script>
|
|
77
8
|
|
|
78
9
|
<template>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
:
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
:diff-view-wrap="true"
|
|
86
|
-
:diff-view-font-size="14"
|
|
10
|
+
<ApprovalDiff
|
|
11
|
+
:record="record"
|
|
12
|
+
:meta="meta"
|
|
13
|
+
:resource="resource"
|
|
14
|
+
splitWhenWide
|
|
15
|
+
@decided="router.push({ name: 'resource-list', params: { resourceId: resource.resourceId } })"
|
|
87
16
|
/>
|
|
88
|
-
<div v-else>No changes</div>
|
|
89
|
-
<div v-if="record[meta.resourceColumns.statusColumnName] === 1" style="margin-top: 16px; display: flex; gap: 8px;">
|
|
90
|
-
<Button style="background-color: green; color: white;" @click="sendApproveRequest(true)" :loader="false" class="w-full">Approve</Button>
|
|
91
|
-
<Button style="background-color: red; color: white;" @click="sendApproveRequest(false)" :loader="false" class="w-full">Reject</Button>
|
|
92
|
-
</div>
|
|
93
17
|
</template>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/crud-approve-plugin",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dayjs": "^1.11.11"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"adminforth": "^
|
|
26
|
+
"adminforth": "^4.7.3"
|
|
27
27
|
},
|
|
28
28
|
"release": {
|
|
29
29
|
"plugins": [
|