Skip to content

Commit 20cdde6

Browse files
committed
first commit
0 parents  commit 20cdde6

File tree

8 files changed

+3794
-0
lines changed

8 files changed

+3794
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
custom/node_modules
3+
dist

custom/InlineCreateForm.vue

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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-4 py-2">
5+
<div v-if="isEditableColumn(column)" class="flex gap-2">
6+
<ColumnValueInputWrapper
7+
ref="input"
8+
class=""
9+
:source="'create'"
10+
:column="column"
11+
:currentValues="formData"
12+
:mode="'create'"
13+
:columnOptions="columnOptions"
14+
:unmasked="unmasked"
15+
:setCurrentValue="setCurrentValue"
16+
/>
17+
</div>
18+
<div v-else></div>
19+
</td>
20+
<td class="px-4 pt-4 flex gap-2 items-center">
21+
<button
22+
@click="handleSave"
23+
class="text-green-600 hover:text-green-800 disabled:opacity-50"
24+
:disabled="saving || !isValid"
25+
>
26+
<IconCheckOutline v-if="!saving" class="w-5 h-5"/>
27+
</button>
28+
<button
29+
@click="cancelCreate"
30+
class="text-red-600 hover:text-red-800"
31+
>
32+
<IconXOutline class="w-5 h-5"/>
33+
</button>
34+
</td>
35+
</template>
36+
<template v-else>
37+
<td :colspan="visibleColumns.length + 1" class="px-4 py-2">
38+
<button
39+
@click="startCreate"
40+
class="w-full text-left text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
41+
>
42+
<div class="flex items-center">
43+
<IconPlusOutline class="w-4 h-4 mr-2"/>
44+
{{ t('Add new record') }}
45+
</div>
46+
</button>
47+
</td>
48+
</template>
49+
</template>
50+
51+
<script setup>
52+
import { ref, computed } from 'vue';
53+
import { useCoreStore } from '@/stores/core';
54+
import { callAdminForthApi } from '@/utils';
55+
import { useI18n } from 'vue-i18n';
56+
import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
57+
import { IconCheckOutline, IconXOutline, IconPlusOutline, IconExclamationCircleSolid} from '@iconify-prerendered/vue-flowbite';
58+
import { computedAsync } from '@vueuse/core';
59+
import { useRouter } from 'vue-router';
60+
import { Tooltip } from '@/afcl';
61+
62+
const props = defineProps(['meta']);
63+
const emit = defineEmits(['update:records']);
64+
const { t } = useI18n();
65+
const router = useRouter();
66+
67+
const coreStore = useCoreStore();
68+
const isCreating = ref(false);
69+
const saving = ref(false);
70+
const formData = ref({});
71+
const unmasked = ref({});
72+
const invalidFields = ref({});
73+
const emptyFields = ref({});
74+
75+
console.log("visibleColumns", coreStore.resource.columns);
76+
77+
78+
// Determine which columns should be editable
79+
const visibleColumns = computed(() =>
80+
coreStore.resource.columns.filter(c => !c.backendOnly && c.showIn?.create !== false && !c.primaryKey)
81+
);
82+
83+
console.log("visibleColumns", visibleColumns.value);
84+
85+
const allVisibleColumns = computed(() => {
86+
// Create a Map using column name as key to remove duplicates
87+
const columnsMap = new Map();
88+
89+
// Add all visible columns
90+
coreStore.resource.columns.filter(c => c.showIn?.list).forEach(column => {
91+
columnsMap.set(column.label, column);
92+
});
93+
94+
// Add all editable columns
95+
visibleColumns.value.forEach(column => {
96+
columnsMap.set(column.label, column);
97+
});
98+
99+
// Convert Map values back to array
100+
return Array.from(columnsMap.values());
101+
});
102+
103+
// Function to check if a column should be editable
104+
function isEditableColumn(column) {
105+
return !column.backendOnly && column.showIn?.create !== false && !column.primaryKey;
106+
}
107+
108+
const columnOptions = computedAsync(async () => {
109+
return (await Promise.all(
110+
Object.values(coreStore.resource.columns).map(async (column) => {
111+
if (column.foreignResource) {
112+
const list = await callAdminForthApi({
113+
method: 'POST',
114+
path: `/get_resource_foreign_data`,
115+
body: {
116+
resourceId: coreStore.resource.resourceId,
117+
column: column.name,
118+
limit: 1000,
119+
offset: 0,
120+
},
121+
});
122+
123+
if (!column.required?.create) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
124+
125+
return { [column.name]: list.items };
126+
}
127+
})
128+
)).reduce((acc, val) => Object.assign(acc, val), {})
129+
}, {});
130+
131+
const isValid = computed(() => {
132+
return !Object.values(invalidFields.value).some(invalid => invalid);
133+
});
134+
135+
function initializeFormData() {
136+
const newFormData = {};
137+
visibleColumns.value.forEach(column => {
138+
if (column.isArray?.enabled) {
139+
newFormData[column.name] = []; // Initialize as empty array
140+
} else if (column.type === 'json') {
141+
newFormData[column.name] = null;
142+
} else if (column.suggestOnCreate !== undefined) {
143+
newFormData[column.name] = column.suggestOnCreate;
144+
} else {
145+
newFormData[column.name] = null;
146+
}
147+
});
148+
formData.value = newFormData;
149+
invalidFields.value = {};
150+
emptyFields.value = {};
151+
}
152+
153+
function startCreate() {
154+
isCreating.value = true;
155+
initializeFormData();
156+
}
157+
158+
function cancelCreate() {
159+
isCreating.value = false;
160+
formData.value = {};
161+
invalidFields.value = {};
162+
emptyFields.value = {};
163+
}
164+
165+
function setCurrentValue(field, value, arrayIndex = undefined) {
166+
if (arrayIndex !== undefined) {
167+
// Handle array updates
168+
if (!Array.isArray(formData.value[field])) {
169+
formData.value[field] = [];
170+
}
171+
172+
const column = coreStore.resource.columns.find(c => c.name === field);
173+
const newArray = [...formData.value[field]];
174+
175+
if (arrayIndex >= newArray.length) {
176+
// When adding a new item, always add null
177+
newArray.push(null);
178+
} else {
179+
// For existing items, handle type conversion
180+
if (column?.isArray?.itemType && ['integer', 'float', 'decimal'].includes(column.isArray.itemType)) {
181+
newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
182+
} else {
183+
newArray[arrayIndex] = value;
184+
}
185+
}
186+
187+
// Assign the new array
188+
formData.value[field] = newArray;
189+
} else {
190+
// Handle non-array updates
191+
formData.value[field] = value;
192+
}
193+
}
194+
195+
async function handleSave() {
196+
if (!isValid.value) return;
197+
198+
saving.value = true;
199+
try {
200+
const response = await callAdminForthApi({
201+
method: 'POST',
202+
path: `/plugin/${props.meta.pluginInstanceId}/create`,
203+
body: {
204+
resourceId: coreStore.resource.resourceId,
205+
record: formData.value
206+
}
207+
});
208+
209+
if (response.error) {
210+
adminforth.alert({
211+
message: response.error,
212+
variant: 'error'
213+
});
214+
return;
215+
}
216+
cancelCreate();
217+
218+
adminforth.alert({
219+
message: t('Record created successfully!'),
220+
variant: 'success'
221+
});
222+
await adminforth.list.refresh();
223+
} finally {
224+
saving.value = false;
225+
}
226+
}
227+
</script>

custom/tsconfig.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

index.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
// Add custom component injection for inline create form
17+
if (!resourceConfig.options.pageInjections) {
18+
resourceConfig.options.pageInjections = {};
19+
}
20+
21+
if (!resourceConfig.options.pageInjections.list) {
22+
resourceConfig.options.pageInjections.list = {};
23+
}
24+
25+
// Set as array of component declarations
26+
resourceConfig.options.pageInjections.list.tableBodyStart = [{
27+
file: this.componentPath('InlineCreateForm.vue'),
28+
meta: {
29+
pluginInstanceId: this.pluginInstanceId
30+
}
31+
}];
32+
}
33+
34+
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
35+
// Check each column for potential configuration issues
36+
for (const column of resourceConfig.columns) {
37+
if (column.backendOnly) continue;
38+
39+
const isRequiredForCreate = column.required?.create === true;
40+
const isVisibleInList = column.showIn?.list !== false;
41+
const hasFillOnCreate = column.fillOnCreate !== undefined;
42+
const isVisibleInCreate = column.showIn?.create !== false;
43+
44+
if (isRequiredForCreate && !isVisibleInList && !hasFillOnCreate) {
45+
throw new Error(
46+
`Column "${column.name}" in resource "${resourceConfig.resourceId}" is required for create but not visible in list view. ` +
47+
'Either:\n' +
48+
'1) Set showIn.list: true, or\n' +
49+
'2) Set required.create: false and ensure a database default exists, or\n' +
50+
'3) Add fillOnCreate property and set showIn.create: false'
51+
);
52+
}
53+
54+
if (hasFillOnCreate && isVisibleInCreate) {
55+
throw new Error(
56+
`Column "${column.name}" in resource "${resourceConfig.resourceId}" has fillOnCreate but is still visible in create form. ` +
57+
'When using fillOnCreate, set showIn.create: false'
58+
);
59+
}
60+
}
61+
}
62+
63+
instanceUniqueRepresentation() {
64+
return 'inline-create';
65+
}
66+
67+
setupEndpoints(server: IHttpServer) {
68+
server.endpoint({
69+
method: 'POST',
70+
path: `/plugin/${this.pluginInstanceId}/create`,
71+
handler: async ({ body, adminforth, adminUser }) => {
72+
const { record, resourceId } = body;
73+
74+
const resource = this.adminforth.config.resources.find(r => r.resourceId === resourceId);
75+
76+
// Create a new record object with only valid database columns
77+
const cleanRecord = {};
78+
79+
for (const field of resource.columns) {
80+
81+
if (record[field.name] !== undefined && record[field.name] !== null) {
82+
cleanRecord[field.name] = record[field.name];
83+
}
84+
}
85+
const result = await this.adminforth.createResourceRecord({
86+
resource,
87+
record: cleanRecord,
88+
adminUser
89+
});
90+
91+
if (result.error) {
92+
return { error: result.error };
93+
}
94+
return { record: result.createdRecord };
95+
}
96+
});
97+
}
98+
}

0 commit comments

Comments
 (0)