@adminforth/inline-create 1.0.1
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/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +42 -0
- package/build.log +11 -0
- package/custom/InlineCreateForm.vue +221 -0
- package/custom/tsconfig.json +19 -0
- package/dist/custom/InlineCreateForm.vue +221 -0
- package/dist/custom/tsconfig.json +19 -0
- package/dist/index.js +87 -0
- package/dist/types.js +1 -0
- package/index.ts +94 -0
- package/package.json +58 -0
- package/tsconfig.json +13 -0
- package/types.ts +3 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
#!/bin/bash
|
|
3
|
+
|
|
4
|
+
# write npm run output both to console and to build.log
|
|
5
|
+
npm run build 2>&1 | tee build.log
|
|
6
|
+
build_status=${PIPESTATUS[0]}
|
|
7
|
+
|
|
8
|
+
# if exist status from the npm run build is not 0
|
|
9
|
+
# then exit with the status code from the npm run build
|
|
10
|
+
if [ $build_status -ne 0 ]; then
|
|
11
|
+
echo "Build failed. Exiting with status code $build_status"
|
|
12
|
+
exit $build_status
|
|
13
|
+
fi
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
set -x
|
|
4
|
+
|
|
5
|
+
COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if [ "$CI_STEP_STATUS" = "success" ]; then
|
|
9
|
+
MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
10
|
+
|
|
11
|
+
curl -s -X POST -H "Content-Type: application/json" -d '{
|
|
12
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
13
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
14
|
+
"attachments": [
|
|
15
|
+
{
|
|
16
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
17
|
+
"color": "#36a64f",
|
|
18
|
+
"text": "'"$MESSAGE"'"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
export BUILD_LOG=$(cat ./build.log)
|
|
25
|
+
|
|
26
|
+
BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
|
|
27
|
+
|
|
28
|
+
MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
|
|
29
|
+
CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
|
|
30
|
+
|
|
31
|
+
echo "Sending slack message to developers $MESSAGE"
|
|
32
|
+
# Send the message
|
|
33
|
+
curl -sS -X POST -H "Content-Type: application/json" -d '{
|
|
34
|
+
"username": "'"$CI_COMMIT_AUTHOR"'",
|
|
35
|
+
"icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
|
|
36
|
+
"attachments": [
|
|
37
|
+
{
|
|
38
|
+
"mrkdwn_in": ["text", "pretext"],
|
|
39
|
+
"color": "#8A1C12",
|
|
40
|
+
"text": "'"$CODE_BLOCK"'",
|
|
41
|
+
"pretext": "'"$MESSAGE"'"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
clone:
|
|
2
|
+
git:
|
|
3
|
+
image: woodpeckerci/plugin-git
|
|
4
|
+
settings:
|
|
5
|
+
partial: false
|
|
6
|
+
depth: 5
|
|
7
|
+
|
|
8
|
+
steps:
|
|
9
|
+
init-secrets:
|
|
10
|
+
when:
|
|
11
|
+
- event: push
|
|
12
|
+
image: infisical/cli
|
|
13
|
+
environment:
|
|
14
|
+
INFISICAL_TOKEN:
|
|
15
|
+
from_secret: VAULT_TOKEN
|
|
16
|
+
commands:
|
|
17
|
+
- infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
|
|
18
|
+
secrets:
|
|
19
|
+
- VAULT_TOKEN
|
|
20
|
+
|
|
21
|
+
release:
|
|
22
|
+
image: node:20
|
|
23
|
+
when:
|
|
24
|
+
- event: push
|
|
25
|
+
commands:
|
|
26
|
+
- apt update && apt install -y rsync
|
|
27
|
+
- export $(cat /woodpecker/deploy.vault.env | xargs)
|
|
28
|
+
- npm clean-install
|
|
29
|
+
- /bin/bash ./.woodpecker/buildRelease.sh
|
|
30
|
+
- npm audit signatures
|
|
31
|
+
- npx semantic-release
|
|
32
|
+
|
|
33
|
+
slack-on-failure:
|
|
34
|
+
when:
|
|
35
|
+
- event: push
|
|
36
|
+
status: [failure, success]
|
|
37
|
+
- event: push
|
|
38
|
+
image: curlimages/curl
|
|
39
|
+
commands:
|
|
40
|
+
- export $(cat /woodpecker/deploy.vault.env | xargs)
|
|
41
|
+
- /bin/sh ./.woodpecker/buildSlackNotify.sh
|
|
42
|
+
|
package/build.log
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
> @adminforth/inline-create@1.0.0 build
|
|
3
|
+
> tsc && rsync -av --exclude 'node_modules' custom dist/
|
|
4
|
+
|
|
5
|
+
sending incremental file list
|
|
6
|
+
custom/
|
|
7
|
+
custom/InlineCreateForm.vue
|
|
8
|
+
custom/tsconfig.json
|
|
9
|
+
|
|
10
|
+
sent 7,474 bytes received 58 bytes 15,064.00 bytes/sec
|
|
11
|
+
total size is 7,257 speedup is 0.96
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-if="isCreating">
|
|
3
|
+
<td class="px-4 py-2"></td>
|
|
4
|
+
<td v-for="column in allVisibleColumns" :key="column.name" class="px-2 md:px-3 lg:px-6 py-2">
|
|
5
|
+
<div v-if="isEditableColumn(column)" class="flex gap-2">
|
|
6
|
+
<ColumnValueInputWrapper
|
|
7
|
+
ref="input"
|
|
8
|
+
class="min-w-24"
|
|
9
|
+
:source="'create'"
|
|
10
|
+
:column="column"
|
|
11
|
+
:currentValues="formData"
|
|
12
|
+
:mode="'create'"
|
|
13
|
+
:columnOptions="columnOptions"
|
|
14
|
+
:unmasked="unmasked"
|
|
15
|
+
:setCurrentValue="setCurrentValue"
|
|
16
|
+
@update:unmasked="handleUnmasked"
|
|
17
|
+
/>
|
|
18
|
+
<div v-if="column.required?.create" ><IconExclamationCircleSolid class="w-4 h-4"/></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div v-else></div>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="px-4 pt-4 flex gap-2 items-center">
|
|
23
|
+
<button
|
|
24
|
+
@click="handleSave"
|
|
25
|
+
class="text-green-600 hover:text-green-800 disabled:opacity-50"
|
|
26
|
+
:disabled="saving || !isValid"
|
|
27
|
+
>
|
|
28
|
+
<IconCheckOutline v-if="!saving" class="w-5 h-5"/>
|
|
29
|
+
</button>
|
|
30
|
+
<button
|
|
31
|
+
@click="cancelCreate"
|
|
32
|
+
class="text-red-600 hover:text-red-800"
|
|
33
|
+
>
|
|
34
|
+
<IconXOutline class="w-5 h-5"/>
|
|
35
|
+
</button>
|
|
36
|
+
</td>
|
|
37
|
+
</template>
|
|
38
|
+
<template v-else>
|
|
39
|
+
<td :colspan="visibleColumns.length + 1" class="px-4 py-2">
|
|
40
|
+
<button
|
|
41
|
+
@click="startCreate"
|
|
42
|
+
class="w-full text-left text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
|
43
|
+
>
|
|
44
|
+
<div class="flex items-center">
|
|
45
|
+
<IconPlusOutline class="w-4 h-4 mr-2"/>
|
|
46
|
+
{{ t('Add new record') }}
|
|
47
|
+
</div>
|
|
48
|
+
</button>
|
|
49
|
+
</td>
|
|
50
|
+
</template>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
import { ref, computed } from 'vue';
|
|
55
|
+
import { useCoreStore } from '@/stores/core';
|
|
56
|
+
import { callAdminForthApi } from '@/utils';
|
|
57
|
+
import { useI18n } from 'vue-i18n';
|
|
58
|
+
import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
|
|
59
|
+
import { IconCheckOutline, IconXOutline, IconPlusOutline, IconExclamationCircleSolid} from '@iconify-prerendered/vue-flowbite';
|
|
60
|
+
import { computedAsync } from '@vueuse/core';
|
|
61
|
+
import { useRouter } from 'vue-router';
|
|
62
|
+
|
|
63
|
+
const props = defineProps(['meta']);
|
|
64
|
+
const emit = defineEmits(['update:records']);
|
|
65
|
+
const { t } = useI18n();
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
|
|
68
|
+
const coreStore = useCoreStore();
|
|
69
|
+
const isCreating = ref(false);
|
|
70
|
+
const saving = ref(false);
|
|
71
|
+
const formData = ref({});
|
|
72
|
+
const unmasked = ref({});
|
|
73
|
+
const invalidFields = ref({});
|
|
74
|
+
const emptyFields = ref({});
|
|
75
|
+
|
|
76
|
+
const visibleColumns = computed(() =>
|
|
77
|
+
coreStore.resource.columns.filter(c => !c.backendOnly && c.showIn?.create !== false && !c.primaryKey)
|
|
78
|
+
);
|
|
79
|
+
function handleUnmasked(columnName) {
|
|
80
|
+
unmasked.value[columnName] = !unmasked.value[columnName];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const allVisibleColumns = computed(() => {
|
|
84
|
+
const columnsMap = new Map();
|
|
85
|
+
|
|
86
|
+
coreStore.resource.columns.filter(c => c.showIn?.list).forEach(column => {
|
|
87
|
+
columnsMap.set(column.label, column);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
visibleColumns.value.forEach(column => {
|
|
91
|
+
columnsMap.set(column.label, column);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Array.from(columnsMap.values());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Function to check if a column should be editable
|
|
98
|
+
function isEditableColumn(column) {
|
|
99
|
+
return !column.backendOnly && column.showIn?.create !== false && !column.primaryKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const columnOptions = computedAsync(async () => {
|
|
103
|
+
return (await Promise.all(
|
|
104
|
+
Object.values(coreStore.resource.columns).map(async (column) => {
|
|
105
|
+
if (column.foreignResource) {
|
|
106
|
+
const list = await callAdminForthApi({
|
|
107
|
+
method: 'POST',
|
|
108
|
+
path: `/get_resource_foreign_data`,
|
|
109
|
+
body: {
|
|
110
|
+
resourceId: coreStore.resource.resourceId,
|
|
111
|
+
column: column.name,
|
|
112
|
+
limit: 1000,
|
|
113
|
+
offset: 0,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!column.required?.create) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
|
|
118
|
+
|
|
119
|
+
return { [column.name]: list.items };
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
123
|
+
}, {});
|
|
124
|
+
|
|
125
|
+
const isValid = computed(() => {
|
|
126
|
+
return !Object.values(invalidFields.value).some(invalid => invalid);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
function initializeFormData() {
|
|
130
|
+
const newFormData = {};
|
|
131
|
+
visibleColumns.value.forEach(column => {
|
|
132
|
+
if (column.isArray?.enabled) {
|
|
133
|
+
newFormData[column.name] = []; // Initialize as empty array
|
|
134
|
+
} else if (column.type === 'json') {
|
|
135
|
+
newFormData[column.name] = null;
|
|
136
|
+
} else if (column.suggestOnCreate !== undefined) {
|
|
137
|
+
newFormData[column.name] = column.suggestOnCreate;
|
|
138
|
+
} else {
|
|
139
|
+
newFormData[column.name] = null;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
formData.value = newFormData;
|
|
143
|
+
invalidFields.value = {};
|
|
144
|
+
emptyFields.value = {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function startCreate() {
|
|
148
|
+
isCreating.value = true;
|
|
149
|
+
initializeFormData();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cancelCreate() {
|
|
153
|
+
isCreating.value = false;
|
|
154
|
+
formData.value = {};
|
|
155
|
+
invalidFields.value = {};
|
|
156
|
+
emptyFields.value = {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setCurrentValue(field, value, arrayIndex = undefined) {
|
|
160
|
+
if (arrayIndex !== undefined) {
|
|
161
|
+
// Handle array updates
|
|
162
|
+
if (!Array.isArray(formData.value[field])) {
|
|
163
|
+
formData.value[field] = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const column = coreStore.resource.columns.find(c => c.name === field);
|
|
167
|
+
const newArray = [...formData.value[field]];
|
|
168
|
+
|
|
169
|
+
if (arrayIndex >= newArray.length) {
|
|
170
|
+
// When adding a new item, always add null
|
|
171
|
+
newArray.push(null);
|
|
172
|
+
} else {
|
|
173
|
+
// For existing items, handle type conversion
|
|
174
|
+
if (column?.isArray?.itemType && ['integer', 'float', 'decimal'].includes(column.isArray.itemType)) {
|
|
175
|
+
newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
|
|
176
|
+
} else {
|
|
177
|
+
newArray[arrayIndex] = value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Assign the new array
|
|
182
|
+
formData.value[field] = newArray;
|
|
183
|
+
} else {
|
|
184
|
+
// Handle non-array updates
|
|
185
|
+
formData.value[field] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleSave() {
|
|
190
|
+
if (!isValid.value) return;
|
|
191
|
+
|
|
192
|
+
saving.value = true;
|
|
193
|
+
try {
|
|
194
|
+
const response = await callAdminForthApi({
|
|
195
|
+
method: 'POST',
|
|
196
|
+
path: `/plugin/${props.meta.pluginInstanceId}/create`,
|
|
197
|
+
body: {
|
|
198
|
+
resourceId: coreStore.resource.resourceId,
|
|
199
|
+
record: formData.value
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (response.error) {
|
|
204
|
+
adminforth.alert({
|
|
205
|
+
message: response.error,
|
|
206
|
+
variant: 'error'
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
cancelCreate();
|
|
211
|
+
|
|
212
|
+
adminforth.alert({
|
|
213
|
+
message: t('Record created successfully!'),
|
|
214
|
+
variant: 'success'
|
|
215
|
+
});
|
|
216
|
+
await adminforth.list.refresh();
|
|
217
|
+
} finally {
|
|
218
|
+
saving.value = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".", // This should point to your project root
|
|
4
|
+
"paths": {
|
|
5
|
+
"@/*": [
|
|
6
|
+
// "node_modules/adminforth/dist/spa/src/*"
|
|
7
|
+
"../../../spa/src/*"
|
|
8
|
+
],
|
|
9
|
+
"*": [
|
|
10
|
+
// "node_modules/adminforth/dist/spa/node_modules/*"
|
|
11
|
+
"../../../spa/node_modules/*"
|
|
12
|
+
],
|
|
13
|
+
"@@/*": [
|
|
14
|
+
// "node_modules/adminforth/dist/spa/src/*"
|
|
15
|
+
"."
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-if="isCreating">
|
|
3
|
+
<td class="px-4 py-2"></td>
|
|
4
|
+
<td v-for="column in allVisibleColumns" :key="column.name" class="px-2 md:px-3 lg:px-6 py-2">
|
|
5
|
+
<div v-if="isEditableColumn(column)" class="flex gap-2">
|
|
6
|
+
<ColumnValueInputWrapper
|
|
7
|
+
ref="input"
|
|
8
|
+
class="min-w-24"
|
|
9
|
+
:source="'create'"
|
|
10
|
+
:column="column"
|
|
11
|
+
:currentValues="formData"
|
|
12
|
+
:mode="'create'"
|
|
13
|
+
:columnOptions="columnOptions"
|
|
14
|
+
:unmasked="unmasked"
|
|
15
|
+
:setCurrentValue="setCurrentValue"
|
|
16
|
+
@update:unmasked="handleUnmasked"
|
|
17
|
+
/>
|
|
18
|
+
<div v-if="column.required?.create" ><IconExclamationCircleSolid class="w-4 h-4"/></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div v-else></div>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="px-4 pt-4 flex gap-2 items-center">
|
|
23
|
+
<button
|
|
24
|
+
@click="handleSave"
|
|
25
|
+
class="text-green-600 hover:text-green-800 disabled:opacity-50"
|
|
26
|
+
:disabled="saving || !isValid"
|
|
27
|
+
>
|
|
28
|
+
<IconCheckOutline v-if="!saving" class="w-5 h-5"/>
|
|
29
|
+
</button>
|
|
30
|
+
<button
|
|
31
|
+
@click="cancelCreate"
|
|
32
|
+
class="text-red-600 hover:text-red-800"
|
|
33
|
+
>
|
|
34
|
+
<IconXOutline class="w-5 h-5"/>
|
|
35
|
+
</button>
|
|
36
|
+
</td>
|
|
37
|
+
</template>
|
|
38
|
+
<template v-else>
|
|
39
|
+
<td :colspan="visibleColumns.length + 1" class="px-4 py-2">
|
|
40
|
+
<button
|
|
41
|
+
@click="startCreate"
|
|
42
|
+
class="w-full text-left text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
|
43
|
+
>
|
|
44
|
+
<div class="flex items-center">
|
|
45
|
+
<IconPlusOutline class="w-4 h-4 mr-2"/>
|
|
46
|
+
{{ t('Add new record') }}
|
|
47
|
+
</div>
|
|
48
|
+
</button>
|
|
49
|
+
</td>
|
|
50
|
+
</template>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
import { ref, computed } from 'vue';
|
|
55
|
+
import { useCoreStore } from '@/stores/core';
|
|
56
|
+
import { callAdminForthApi } from '@/utils';
|
|
57
|
+
import { useI18n } from 'vue-i18n';
|
|
58
|
+
import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
|
|
59
|
+
import { IconCheckOutline, IconXOutline, IconPlusOutline, IconExclamationCircleSolid} from '@iconify-prerendered/vue-flowbite';
|
|
60
|
+
import { computedAsync } from '@vueuse/core';
|
|
61
|
+
import { useRouter } from 'vue-router';
|
|
62
|
+
|
|
63
|
+
const props = defineProps(['meta']);
|
|
64
|
+
const emit = defineEmits(['update:records']);
|
|
65
|
+
const { t } = useI18n();
|
|
66
|
+
const router = useRouter();
|
|
67
|
+
|
|
68
|
+
const coreStore = useCoreStore();
|
|
69
|
+
const isCreating = ref(false);
|
|
70
|
+
const saving = ref(false);
|
|
71
|
+
const formData = ref({});
|
|
72
|
+
const unmasked = ref({});
|
|
73
|
+
const invalidFields = ref({});
|
|
74
|
+
const emptyFields = ref({});
|
|
75
|
+
|
|
76
|
+
const visibleColumns = computed(() =>
|
|
77
|
+
coreStore.resource.columns.filter(c => !c.backendOnly && c.showIn?.create !== false && !c.primaryKey)
|
|
78
|
+
);
|
|
79
|
+
function handleUnmasked(columnName) {
|
|
80
|
+
unmasked.value[columnName] = !unmasked.value[columnName];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const allVisibleColumns = computed(() => {
|
|
84
|
+
const columnsMap = new Map();
|
|
85
|
+
|
|
86
|
+
coreStore.resource.columns.filter(c => c.showIn?.list).forEach(column => {
|
|
87
|
+
columnsMap.set(column.label, column);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
visibleColumns.value.forEach(column => {
|
|
91
|
+
columnsMap.set(column.label, column);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Array.from(columnsMap.values());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Function to check if a column should be editable
|
|
98
|
+
function isEditableColumn(column) {
|
|
99
|
+
return !column.backendOnly && column.showIn?.create !== false && !column.primaryKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const columnOptions = computedAsync(async () => {
|
|
103
|
+
return (await Promise.all(
|
|
104
|
+
Object.values(coreStore.resource.columns).map(async (column) => {
|
|
105
|
+
if (column.foreignResource) {
|
|
106
|
+
const list = await callAdminForthApi({
|
|
107
|
+
method: 'POST',
|
|
108
|
+
path: `/get_resource_foreign_data`,
|
|
109
|
+
body: {
|
|
110
|
+
resourceId: coreStore.resource.resourceId,
|
|
111
|
+
column: column.name,
|
|
112
|
+
limit: 1000,
|
|
113
|
+
offset: 0,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!column.required?.create) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
|
|
118
|
+
|
|
119
|
+
return { [column.name]: list.items };
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
123
|
+
}, {});
|
|
124
|
+
|
|
125
|
+
const isValid = computed(() => {
|
|
126
|
+
return !Object.values(invalidFields.value).some(invalid => invalid);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
function initializeFormData() {
|
|
130
|
+
const newFormData = {};
|
|
131
|
+
visibleColumns.value.forEach(column => {
|
|
132
|
+
if (column.isArray?.enabled) {
|
|
133
|
+
newFormData[column.name] = []; // Initialize as empty array
|
|
134
|
+
} else if (column.type === 'json') {
|
|
135
|
+
newFormData[column.name] = null;
|
|
136
|
+
} else if (column.suggestOnCreate !== undefined) {
|
|
137
|
+
newFormData[column.name] = column.suggestOnCreate;
|
|
138
|
+
} else {
|
|
139
|
+
newFormData[column.name] = null;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
formData.value = newFormData;
|
|
143
|
+
invalidFields.value = {};
|
|
144
|
+
emptyFields.value = {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function startCreate() {
|
|
148
|
+
isCreating.value = true;
|
|
149
|
+
initializeFormData();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cancelCreate() {
|
|
153
|
+
isCreating.value = false;
|
|
154
|
+
formData.value = {};
|
|
155
|
+
invalidFields.value = {};
|
|
156
|
+
emptyFields.value = {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setCurrentValue(field, value, arrayIndex = undefined) {
|
|
160
|
+
if (arrayIndex !== undefined) {
|
|
161
|
+
// Handle array updates
|
|
162
|
+
if (!Array.isArray(formData.value[field])) {
|
|
163
|
+
formData.value[field] = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const column = coreStore.resource.columns.find(c => c.name === field);
|
|
167
|
+
const newArray = [...formData.value[field]];
|
|
168
|
+
|
|
169
|
+
if (arrayIndex >= newArray.length) {
|
|
170
|
+
// When adding a new item, always add null
|
|
171
|
+
newArray.push(null);
|
|
172
|
+
} else {
|
|
173
|
+
// For existing items, handle type conversion
|
|
174
|
+
if (column?.isArray?.itemType && ['integer', 'float', 'decimal'].includes(column.isArray.itemType)) {
|
|
175
|
+
newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
|
|
176
|
+
} else {
|
|
177
|
+
newArray[arrayIndex] = value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Assign the new array
|
|
182
|
+
formData.value[field] = newArray;
|
|
183
|
+
} else {
|
|
184
|
+
// Handle non-array updates
|
|
185
|
+
formData.value[field] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleSave() {
|
|
190
|
+
if (!isValid.value) return;
|
|
191
|
+
|
|
192
|
+
saving.value = true;
|
|
193
|
+
try {
|
|
194
|
+
const response = await callAdminForthApi({
|
|
195
|
+
method: 'POST',
|
|
196
|
+
path: `/plugin/${props.meta.pluginInstanceId}/create`,
|
|
197
|
+
body: {
|
|
198
|
+
resourceId: coreStore.resource.resourceId,
|
|
199
|
+
record: formData.value
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (response.error) {
|
|
204
|
+
adminforth.alert({
|
|
205
|
+
message: response.error,
|
|
206
|
+
variant: 'error'
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
cancelCreate();
|
|
211
|
+
|
|
212
|
+
adminforth.alert({
|
|
213
|
+
message: t('Record created successfully!'),
|
|
214
|
+
variant: 'success'
|
|
215
|
+
});
|
|
216
|
+
await adminforth.list.refresh();
|
|
217
|
+
} finally {
|
|
218
|
+
saving.value = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".", // This should point to your project root
|
|
4
|
+
"paths": {
|
|
5
|
+
"@/*": [
|
|
6
|
+
// "node_modules/adminforth/dist/spa/src/*"
|
|
7
|
+
"../../../spa/src/*"
|
|
8
|
+
],
|
|
9
|
+
"*": [
|
|
10
|
+
// "node_modules/adminforth/dist/spa/node_modules/*"
|
|
11
|
+
"../../../spa/node_modules/*"
|
|
12
|
+
],
|
|
13
|
+
"@@/*": [
|
|
14
|
+
// "node_modules/adminforth/dist/spa/src/*"
|
|
15
|
+
"."
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { AdminForthPlugin } from "adminforth";
|
|
11
|
+
export default class InlineCreatePlugin extends AdminForthPlugin {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super(options, import.meta.url);
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
17
|
+
const _super = Object.create(null, {
|
|
18
|
+
modifyResourceConfig: { get: () => super.modifyResourceConfig }
|
|
19
|
+
});
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
_super.modifyResourceConfig.call(this, adminforth, resourceConfig);
|
|
22
|
+
if (!resourceConfig.options.pageInjections) {
|
|
23
|
+
resourceConfig.options.pageInjections = {};
|
|
24
|
+
}
|
|
25
|
+
if (!resourceConfig.options.pageInjections.list) {
|
|
26
|
+
resourceConfig.options.pageInjections.list = {};
|
|
27
|
+
}
|
|
28
|
+
resourceConfig.options.pageInjections.list.tableBodyStart = [{
|
|
29
|
+
file: this.componentPath('InlineCreateForm.vue'),
|
|
30
|
+
meta: {
|
|
31
|
+
pluginInstanceId: this.pluginInstanceId
|
|
32
|
+
}
|
|
33
|
+
}];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
37
|
+
var _a, _b, _c;
|
|
38
|
+
// Check each column for potential configuration issues
|
|
39
|
+
for (const column of resourceConfig.columns) {
|
|
40
|
+
if (column.backendOnly)
|
|
41
|
+
continue;
|
|
42
|
+
const isRequiredForCreate = ((_a = column.required) === null || _a === void 0 ? void 0 : _a.create) === true;
|
|
43
|
+
const isVisibleInList = ((_b = column.showIn) === null || _b === void 0 ? void 0 : _b.list) !== false;
|
|
44
|
+
const hasFillOnCreate = column.fillOnCreate !== undefined;
|
|
45
|
+
const isVisibleInCreate = ((_c = column.showIn) === null || _c === void 0 ? void 0 : _c.create) !== false;
|
|
46
|
+
if (isRequiredForCreate && !isVisibleInList && !hasFillOnCreate) {
|
|
47
|
+
throw new Error(`Column "${column.name}" in resource "${resourceConfig.resourceId}" is required for create but not visible in list view. ` +
|
|
48
|
+
'Either:\n' +
|
|
49
|
+
'1) Set showIn.list: true, or\n' +
|
|
50
|
+
'2) Set required.create: false and ensure a database default exists, or\n' +
|
|
51
|
+
'3) Add fillOnCreate property and set showIn.create: false');
|
|
52
|
+
}
|
|
53
|
+
if (hasFillOnCreate && isVisibleInCreate) {
|
|
54
|
+
throw new Error(`Column "${column.name}" in resource "${resourceConfig.resourceId}" has fillOnCreate but is still visible in create form. ` +
|
|
55
|
+
'When using fillOnCreate, set showIn.create: false');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
instanceUniqueRepresentation() {
|
|
60
|
+
return 'inline-create';
|
|
61
|
+
}
|
|
62
|
+
setupEndpoints(server) {
|
|
63
|
+
server.endpoint({
|
|
64
|
+
method: 'POST',
|
|
65
|
+
path: `/plugin/${this.pluginInstanceId}/create`,
|
|
66
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminforth, adminUser }) {
|
|
67
|
+
const { record, resourceId } = body;
|
|
68
|
+
const resource = this.adminforth.config.resources.find(r => r.resourceId === resourceId);
|
|
69
|
+
const cleanRecord = resource.columns.reduce((acc, field) => {
|
|
70
|
+
if (record[field.name] !== undefined && record[field.name] !== null) {
|
|
71
|
+
acc[field.name] = record[field.name];
|
|
72
|
+
}
|
|
73
|
+
return acc;
|
|
74
|
+
}, {});
|
|
75
|
+
const result = yield this.adminforth.createResourceRecord({
|
|
76
|
+
resource,
|
|
77
|
+
record: cleanRecord,
|
|
78
|
+
adminUser
|
|
79
|
+
});
|
|
80
|
+
if (result.error) {
|
|
81
|
+
return { error: result.error };
|
|
82
|
+
}
|
|
83
|
+
return { record: result.createdRecord };
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AdminForthPlugin } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthResourcePages, AdminForthResourceColumn, AdminForthDataTypes, AdminForthResource } from "adminforth";
|
|
3
|
+
import type { PluginOptions } from './types.js';
|
|
4
|
+
|
|
5
|
+
export default class InlineCreatePlugin extends AdminForthPlugin {
|
|
6
|
+
options: PluginOptions;
|
|
7
|
+
|
|
8
|
+
constructor(options: PluginOptions) {
|
|
9
|
+
super(options, import.meta.url);
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
14
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
15
|
+
|
|
16
|
+
if (!resourceConfig.options.pageInjections) {
|
|
17
|
+
resourceConfig.options.pageInjections = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!resourceConfig.options.pageInjections.list) {
|
|
21
|
+
resourceConfig.options.pageInjections.list = {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resourceConfig.options.pageInjections.list.tableBodyStart = [{
|
|
25
|
+
file: this.componentPath('InlineCreateForm.vue'),
|
|
26
|
+
meta: {
|
|
27
|
+
pluginInstanceId: this.pluginInstanceId
|
|
28
|
+
}
|
|
29
|
+
}];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
33
|
+
// Check each column for potential configuration issues
|
|
34
|
+
for (const column of resourceConfig.columns) {
|
|
35
|
+
if (column.backendOnly) continue;
|
|
36
|
+
|
|
37
|
+
const isRequiredForCreate = column.required?.create === true;
|
|
38
|
+
const isVisibleInList = column.showIn?.list !== false;
|
|
39
|
+
const hasFillOnCreate = column.fillOnCreate !== undefined;
|
|
40
|
+
const isVisibleInCreate = column.showIn?.create !== false;
|
|
41
|
+
|
|
42
|
+
if (isRequiredForCreate && !isVisibleInList && !hasFillOnCreate) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Column "${column.name}" in resource "${resourceConfig.resourceId}" is required for create but not visible in list view. ` +
|
|
45
|
+
'Either:\n' +
|
|
46
|
+
'1) Set showIn.list: true, or\n' +
|
|
47
|
+
'2) Set required.create: false and ensure a database default exists, or\n' +
|
|
48
|
+
'3) Add fillOnCreate property and set showIn.create: false'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hasFillOnCreate && isVisibleInCreate) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Column "${column.name}" in resource "${resourceConfig.resourceId}" has fillOnCreate but is still visible in create form. ` +
|
|
55
|
+
'When using fillOnCreate, set showIn.create: false'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
instanceUniqueRepresentation() {
|
|
62
|
+
return 'inline-create';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setupEndpoints(server: IHttpServer) {
|
|
66
|
+
server.endpoint({
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: `/plugin/${this.pluginInstanceId}/create`,
|
|
69
|
+
handler: async ({ body, adminforth, adminUser }) => {
|
|
70
|
+
const { record, resourceId } = body;
|
|
71
|
+
|
|
72
|
+
const resource = this.adminforth.config.resources.find(r => r.resourceId === resourceId);
|
|
73
|
+
|
|
74
|
+
const cleanRecord = resource.columns.reduce((acc, field) => {
|
|
75
|
+
if (record[field.name] !== undefined && record[field.name] !== null) {
|
|
76
|
+
acc[field.name] = record[field.name];
|
|
77
|
+
}
|
|
78
|
+
return acc;
|
|
79
|
+
}, {});
|
|
80
|
+
|
|
81
|
+
const result = await this.adminforth.createResourceRecord({
|
|
82
|
+
resource,
|
|
83
|
+
record: cleanRecord,
|
|
84
|
+
adminUser
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (result.error) {
|
|
88
|
+
return { error: result.error };
|
|
89
|
+
}
|
|
90
|
+
return { record: result.createdRecord };
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adminforth/inline-create",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prepare": "npm link adminforth",
|
|
9
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/devforth/adminforth-inline-create.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"adminforth",
|
|
20
|
+
"inline create"
|
|
21
|
+
],
|
|
22
|
+
"author": "devforth",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"description": "Inline create plugin for adminforth",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"adminforth": "latest"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.10.7",
|
|
30
|
+
"semantic-release": "^24.2.1",
|
|
31
|
+
"semantic-release-slack-bot": "^4.0.2",
|
|
32
|
+
"typescript": "^5.7.3"
|
|
33
|
+
},
|
|
34
|
+
"release": {
|
|
35
|
+
"plugins": [
|
|
36
|
+
"@semantic-release/commit-analyzer",
|
|
37
|
+
"@semantic-release/release-notes-generator",
|
|
38
|
+
"@semantic-release/npm",
|
|
39
|
+
"@semantic-release/github",
|
|
40
|
+
[
|
|
41
|
+
"semantic-release-slack-bot",
|
|
42
|
+
{
|
|
43
|
+
"notifyOnSuccess": true,
|
|
44
|
+
"notifyOnFail": true,
|
|
45
|
+
"slackIcon": ":package:",
|
|
46
|
+
"markdownReleaseNotes": true
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
],
|
|
50
|
+
"branches": [
|
|
51
|
+
"main",
|
|
52
|
+
{
|
|
53
|
+
"name": "next",
|
|
54
|
+
"prerelease": true
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include*/
|
|
4
|
+
"module": "node16", /* Specify what module code is generated. */
|
|
5
|
+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
|
6
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
|
|
7
|
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
8
|
+
"strict": false, /* Enable all strict type-checking options. */
|
|
9
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
10
|
+
},
|
|
11
|
+
"exclude": ["node_modules", "dist", "custom"], /* Exclude files from compilation. */
|
|
12
|
+
}
|
|
13
|
+
|
package/types.ts
ADDED