@@ -86,6 +86,25 @@ function buildFields(schemaData, labels, roFields) {
8686 const result = [];
8787
8888 for (const [name , prop ] of Object .entries (props)) {
89+ // M2M field
90+ if (prop[' x-db-m2m' ]) {
91+ result .push ({
92+ name,
93+ label: (labels && labels[name]) || prop .title || name,
94+ type: ' array' ,
95+ format: null ,
96+ dbType: null ,
97+ isPk: false ,
98+ isReadonly: roSet .has (name),
99+ isRequired: false ,
100+ default: [],
101+ maxLength: null ,
102+ fk: null ,
103+ m2m: { target: prop[' x-db-target' ], through: prop[' x-db-through' ] },
104+ });
105+ continue ;
106+ }
107+
89108 if (hasRef (prop)) continue ;
90109
91110 const fk = fkMap[name] || null ;
@@ -102,6 +121,7 @@ function buildFields(schemaData, labels, roFields) {
102121 default: prop .default ?? null ,
103122 maxLength: prop .maxLength || prop[' x-db-max-length' ] || null ,
104123 fk,
124+ m2m: null ,
105125 });
106126 }
107127
@@ -125,6 +145,7 @@ const dlgVisibleFields = computed(() => {
125145});
126146
127147function componentType (field ) {
148+ if (field .m2m ) return ' multiselect' ;
128149 if (field .fk ) return ' select' ;
129150 if (field .type === ' boolean' ) return ' boolean' ;
130151 if (field .type === ' integer' || field .type === ' number' ) return ' number' ;
@@ -138,7 +159,13 @@ function initFormData(fieldList, record) {
138159 const data = {};
139160 for (const f of fieldList) {
140161 let val = record ? (record[f .name ] ?? f .default ) : f .default ;
141- if (val && (f .format === ' date-time' || f .format === ' date' )) {
162+ if (f .m2m && Array .isArray (val)) {
163+ // M2M values come as [{value, label}] from fkOptions or as objects from API
164+ val = val .map ((item ) => {
165+ if (typeof item !== ' object' ) return item;
166+ return item .value ?? item .id ?? Object .values (item)[0 ];
167+ }).filter ((v ) => v != null );
168+ } else if (val && (f .format === ' date-time' || f .format === ' date' )) {
142169 val = new Date (val);
143170 }
144171 data[f .name ] = val;
@@ -165,6 +192,10 @@ function buildPayload() {
165192 if (f .isReadonly ) continue ;
166193 if (f .isPk && isCreate .value ) continue ;
167194 let val = formData .value [f .name ];
195+ if (f .m2m ) {
196+ payload[f .name ] = Array .isArray (val) ? val : [];
197+ continue ;
198+ }
168199 if (val instanceof Date ) {
169200 val = val .toISOString ();
170201 }
@@ -190,6 +221,7 @@ async function loadSchema() {
190221
191222async function loadFkOptions () {
192223 const fkFields = fields .value .filter ((f ) => f .fk );
224+ const m2mFields = fields .value .filter ((f ) => f .m2m );
193225 const promises = fkFields .map (async (f ) => {
194226 let url = ` /api/${ f .fk .model } /options` ;
195227 const currentVal = formData .value ? .[f .name ];
@@ -199,7 +231,17 @@ async function loadFkOptions() {
199231 const res = await api (url);
200232 fkOptions .value [f .name ] = await res .json ();
201233 });
202- await Promise .all (promises);
234+ const m2mPromises = m2mFields .map (async (f ) => {
235+ let url = ` /api/${ f .m2m .target } /options` ;
236+ const currentVals = formData .value ? .[f .name ];
237+ if (Array .isArray (currentVals) && currentVals .length > 0 ) {
238+ const params = currentVals .map ((v ) => ` include=${ encodeURIComponent (v)} ` ).join (' &' );
239+ url += ` ?${ params} ` ;
240+ }
241+ const res = await api (url);
242+ fkOptions .value [f .name ] = await res .json ();
243+ });
244+ await Promise .all ([... promises, ... m2mPromises]);
203245}
204246
205247let fkSearchTimeout = null ;
@@ -215,6 +257,19 @@ function onFkSearch(field, event) {
215257 }, 300 );
216258}
217259
260+ let m2mFilterTimeout = null ;
261+ function onM2mFilter (field , event ) {
262+ clearTimeout (m2mFilterTimeout);
263+ const query = event .value ;
264+ m2mFilterTimeout = setTimeout (async () => {
265+ const url = query
266+ ? ` /api/${ field .m2m .target } /options?search=${ encodeURIComponent (query)} `
267+ : ` /api/${ field .m2m .target } /options` ;
268+ const res = await api (url);
269+ fkOptions .value [field .name ] = await res .json ();
270+ }, 300 );
271+ }
272+
218273async function loadRecord () {
219274 loading .value = true ;
220275 try {
@@ -474,8 +529,24 @@ onMounted(async () => {
474529 <span v-if="field.isRequired" class="text-red-500">*</span>
475530 </label>
476531
532+ <!-- M2M MultiSelect -->
533+ <MultiSelect
534+ v-if="componentType(field) === 'multiselect'"
535+ :id="field.name"
536+ v-model="formData[field.name]"
537+ :options="fkOptions[field.name] || []"
538+ optionLabel="label"
539+ optionValue="value"
540+ :disabled="field.isReadonly"
541+ display="chip"
542+ filter
543+ @filter="onM2mFilter(field, $event)"
544+ placeholder="Select..."
545+ fluid
546+ />
547+
477548 <!-- FK Select -->
478- <div v-if="componentType(field) === 'select'" class="flex gap-2 items-center">
549+ <div v-else- if="componentType(field) === 'select'" class="flex gap-2 items-center">
479550 <Select
480551 :id="field.name"
481552 v-model="formData[field.name]"
0 commit comments