@solidxai/core-ui 0.1.4-beta.0 → 0.1.4-beta.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.
Files changed (61) hide show
  1. package/dist/components/core/common/SolidGlobalSearchElement.d.ts.map +1 -1
  2. package/dist/components/core/common/SolidGlobalSearchElement.js +6 -5
  3. package/dist/components/core/common/SolidGlobalSearchElement.js.map +1 -1
  4. package/dist/components/core/common/SolidGlobalSearchElement.tsx +21 -17
  5. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.d.ts.map +1 -1
  6. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.js +15 -30
  7. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.js.map +1 -1
  8. package/dist/components/core/extension/solid-core/roleMetadata/RolePermissionsManyToManyFieldWidget.tsx +46 -36
  9. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.d.ts +40 -0
  10. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.d.ts.map +1 -1
  11. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.js +315 -160
  12. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.js.map +1 -1
  13. package/dist/components/core/form/fields/relations/SolidRelationManyToManyField.tsx +459 -249
  14. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.d.ts.map +1 -1
  15. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.js +46 -95
  16. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.js.map +1 -1
  17. package/dist/components/core/form/fields/relations/SolidRelationOneToManyField.tsx +57 -113
  18. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.d.ts +15 -5
  19. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.d.ts.map +1 -1
  20. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.js +203 -67
  21. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.js.map +1 -1
  22. package/dist/components/core/form/fields/relations/widgets/helpers/useRelationEntityHandler.ts +147 -67
  23. package/dist/components/core/kanban/SolidKanbanView.d.ts.map +1 -1
  24. package/dist/components/core/kanban/SolidKanbanView.js +8 -7
  25. package/dist/components/core/kanban/SolidKanbanView.js.map +1 -1
  26. package/dist/components/core/kanban/SolidKanbanView.tsx +3 -2
  27. package/dist/components/core/list/SolidListView.d.ts +8 -5
  28. package/dist/components/core/list/SolidListView.d.ts.map +1 -1
  29. package/dist/components/core/list/SolidListView.js +70 -59
  30. package/dist/components/core/list/SolidListView.js.map +1 -1
  31. package/dist/components/core/list/SolidListView.tsx +51 -57
  32. package/dist/components/core/list/SolidListViewConfigure.d.ts +7 -0
  33. package/dist/components/core/list/SolidListViewConfigure.d.ts.map +1 -1
  34. package/dist/components/core/list/SolidListViewConfigure.js +6 -5
  35. package/dist/components/core/list/SolidListViewConfigure.js.map +1 -1
  36. package/dist/components/core/list/SolidListViewConfigure.tsx +21 -12
  37. package/dist/components/core/list/columns/SolidShortTextColumn.d.ts.map +1 -1
  38. package/dist/components/core/list/columns/SolidShortTextColumn.js +1 -37
  39. package/dist/components/core/list/columns/SolidShortTextColumn.js.map +1 -1
  40. package/dist/components/core/list/columns/SolidShortTextColumn.tsx +0 -41
  41. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.d.ts.map +1 -1
  42. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js +9 -5
  43. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js.map +1 -1
  44. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.tsx +14 -3
  45. package/dist/components/core/tree/SolidTreeView.d.ts.map +1 -1
  46. package/dist/components/core/tree/SolidTreeView.js +23 -14
  47. package/dist/components/core/tree/SolidTreeView.js.map +1 -1
  48. package/dist/components/core/tree/SolidTreeView.tsx +50 -16
  49. package/dist/helpers/registry.js +3 -1
  50. package/dist/helpers/registry.js.map +1 -1
  51. package/dist/helpers/registry.ts +4 -1
  52. package/dist/resources/globals.css +14 -0
  53. package/dist/routes/pages/admin/core/ListPage.js +1 -1
  54. package/dist/routes/pages/admin/core/ListPage.js.map +1 -1
  55. package/dist/routes/pages/admin/core/ListPage.tsx +1 -1
  56. package/dist/routes/pages/admin/core/ModuleHomePage.d.ts.map +1 -1
  57. package/dist/routes/pages/admin/core/ModuleHomePage.js +4 -15
  58. package/dist/routes/pages/admin/core/ModuleHomePage.js.map +1 -1
  59. package/dist/routes/pages/admin/core/ModuleHomePage.tsx +4 -3
  60. package/dist/types/solid-core.d.ts +1 -1
  61. package/package.json +1 -1
@@ -1,4 +1,3 @@
1
-
2
1
  import { Message } from "primereact/message";
3
2
  import { useEffect, useState } from "react";
4
3
  import * as Yup from 'yup';
@@ -9,16 +8,33 @@ import { Button } from "primereact/button";
9
8
  import { SolidFormFieldWidgetProps } from "../../../../../types/solid-core";
10
9
  import { useRelationEntityHandler } from "./widgets/helpers/useRelationEntityHandler";
11
10
  import { InlineRelationEntityDialog } from "./widgets/helpers/InlineRelationEntityDialog";
12
- import { capitalize } from "lodash";
13
11
  import { Checkbox } from "primereact/checkbox";
14
12
  import { Panel } from "primereact/panel";
15
13
  import { SolidFieldTooltip } from "../../../../../components/common/SolidFieldTooltip";
16
14
  import qs from 'qs';
17
- // import Handlebars from "handlebars/dist/handlebars";
18
15
  import * as Handlebars from "handlebars";
19
16
  import { ERROR_MESSAGES } from "../../../../../constants/error-messages";
20
-
21
-
17
+ import { useRouter } from "../../../../../hooks/useRouter";
18
+ import { usePathname } from "../../../../../hooks/usePathname";
19
+ import { camelCase, capitalize } from "lodash";
20
+ import { SolidListView } from "../../../../core/list/SolidListView";
21
+ import { RenderSolidFormEmbededView } from "./SolidRelationManyToOneField";
22
+ import { Dialog } from "primereact/dialog";
23
+
24
+ export type FormViewParams = {
25
+ moduleName: any;
26
+ id: any;
27
+ embeded: any;
28
+ isCustomCreate: any;
29
+ customLayout: any;
30
+ modelName: any;
31
+ parentFieldName?: any;
32
+ parentData: any;
33
+ onEmbeddedFormSave: any;
34
+ inlineCreateAutoSave: any;
35
+ customCreateHandler?: any;
36
+ handlePopupClose?: any;
37
+ };
22
38
 
23
39
 
24
40
 
@@ -31,63 +47,20 @@ export class SolidRelationManyToManyField implements ISolidField {
31
47
  }
32
48
 
33
49
  initialValue(): any {
34
-
35
- // const manyToManyFieldData = this.fieldContext.data[this.fieldContext.field.attrs.name];
36
- // const fieldMetadata = this.fieldContext.fieldMetadata;
37
- // const userKeyField = fieldMetadata?.relationModel?.userKeyField?.name;
38
- // if (manyToManyFieldData) {
39
- // return manyToManyFieldData.map((e: any) => {
40
- // const manyToManyColVal = e[userKeyField] || '';
41
- // return {
42
- // label: manyToManyColVal,
43
- // value: e?.id || '',
44
- // original: e
45
- // };
46
- // });
47
- // }
48
50
  return [];
49
51
  }
50
52
 
51
53
  updateFormData(value: any, formData: FormData): any {
52
- const fieldLayoutInfo = this.fieldContext.field;
53
- //if empty then clear the field
54
- if (value && value.length === 0) {
55
- formData.append(`${fieldLayoutInfo.attrs.name}Command`, "clear");
56
- }
57
- if (value && value.length > 0) {
58
- const shouldUseOriginal = value.every((item: any) => item.original && item.original.id);
59
-
60
- value.forEach((item: any, index: number) => {
61
- if (shouldUseOriginal) {
62
- formData.append(
63
- `${fieldLayoutInfo.attrs.name}Ids[${index}]`,
64
- item.value
65
- );
66
- } else {
67
- formData.append(
68
- `${fieldLayoutInfo.attrs.name}[${index}]`,
69
- JSON.stringify(item.original)
70
- );
71
- }
72
- });
73
- if (shouldUseOriginal) {
74
- formData.append(`${fieldLayoutInfo.attrs.name}Command`, "set")
75
- } else {
76
- formData.append(`${fieldLayoutInfo.attrs.name}Command`, "update")
77
-
78
- }
79
-
80
- }
54
+ // Link/unlink is handled per-interaction in each widget.
55
+ // No bulk update needed on form submit for many-to-many.
81
56
  }
82
57
 
83
58
  validationSchema(): Yup.Schema {
84
59
  let schema = Yup.array();
85
-
86
60
  const fieldMetadata = this.fieldContext.fieldMetadata;
87
61
  const fieldLayoutInfo = this.fieldContext.field;
88
62
  const fieldLabel = fieldLayoutInfo.attrs.label ?? fieldMetadata.displayName;
89
63
 
90
- // 1. required
91
64
  if (fieldMetadata.required) {
92
65
  schema = schema
93
66
  .min(1, ERROR_MESSAGES.SELECT_ATLEAST_ONE(fieldLabel))
@@ -98,80 +71,73 @@ export class SolidRelationManyToManyField implements ISolidField {
98
71
  }
99
72
 
100
73
  render(formik: FormikObject) {
101
- const fieldMetadata = this.fieldContext.fieldMetadata;
102
74
  const fieldLayoutInfo = this.fieldContext.field;
103
75
  const className = fieldLayoutInfo.attrs?.className || 'field col-12';
104
76
 
105
- const isFormFieldValid = (formik: any, fieldName: string) => formik.touched[fieldName] && formik.errors[fieldName];
106
- const fieldLabel = fieldLayoutInfo.attrs.label ?? fieldMetadata.displayName;
107
-
108
77
  let viewWidget = fieldLayoutInfo.attrs.viewWidget;
109
78
  let editWidget = fieldLayoutInfo.attrs.editWidget;
110
- if (!editWidget) {
111
- editWidget = 'DefaultRelationManyToManyAutoCompleteFormEditWidget';
112
- }
113
- if (!viewWidget) {
114
- viewWidget = 'DefaultRelationOneToManyFormViewWidget';
115
- }
79
+ if (!editWidget) editWidget = 'DefaultRelationManyToManyAutoCompleteFormEditWidget';
80
+ if (!viewWidget) viewWidget = 'DefaultRelationManyToManyListFormEditWidget';
81
+
116
82
  const viewMode: string = this.fieldContext.viewMode;
117
83
  return (
118
- <>
119
- <div className={className}>
120
- {viewMode === "view" &&
121
- this.renderExtensionRenderMode(viewWidget, formik)
122
- }
123
- {viewMode === "edit" && (
124
- <>
125
- {editWidget &&
126
- this.renderExtensionRenderMode(editWidget, formik)
127
- }
128
- </>
129
- )
130
- }
131
- </div>
132
- </>
84
+ <div className={className}>
85
+ {viewMode === "view" && this.renderExtensionRenderMode(viewWidget, formik)}
86
+ {viewMode === "edit" && editWidget && this.renderExtensionRenderMode(editWidget, formik)}
87
+ </div>
133
88
  );
134
89
  }
135
90
 
136
91
  renderExtensionRenderMode(widget: string, formik: FormikObject) {
137
- let DynamicWidget = getExtensionComponent(widget);
138
- const widgetProps: SolidFormFieldWidgetProps = {
139
- formik: formik,
140
- fieldContext: this.fieldContext,
141
- }
142
- return (
143
- <>
144
- {DynamicWidget && <DynamicWidget {...widgetProps} />}
145
- </>
146
- )
92
+ const DynamicWidget = getExtensionComponent(widget);
93
+ const widgetProps: SolidFormFieldWidgetProps = { formik, fieldContext: this.fieldContext };
94
+ return <>{DynamicWidget && <DynamicWidget {...widgetProps} />}</>;
147
95
  }
148
96
  }
149
97
 
150
98
 
151
99
 
100
+ /**
101
+ * AUTOCOMPLETE WIDGET
102
+ *
103
+ * State:
104
+ * currentValues — chips shown in the input (currently linked items)
105
+ * suggestions — dropdown options populated on each keystroke
106
+ *
107
+ * Flow:
108
+ * mount → fetchCurrentValues()
109
+ * user types → fetchSuggestions() via completeMethod
110
+ * user selects → linkItem() → on success, adds to currentValues
111
+ * user removes chip→ unlinkItem() → on success, removes from currentValues
112
+ */
152
113
  export const DefaultRelationManyToManyAutoCompleteFormEditWidget = ({ formik, fieldContext }: SolidFormFieldWidgetProps) => {
153
114
  const fieldMetadata = fieldContext.fieldMetadata;
154
115
  const fieldLayoutInfo = fieldContext.field;
155
- const className = fieldLayoutInfo.attrs?.className || 'field col-12';
156
116
  const fieldLabel = fieldLayoutInfo.attrs.label ?? fieldMetadata.displayName;
157
117
  const showFieldLabel = fieldLayoutInfo?.attrs?.showLabel;
158
118
  const readOnlyPermission = fieldContext.readOnly;
159
119
  const disabled = fieldLayoutInfo.attrs?.disabled;
160
120
  const readOnly = fieldLayoutInfo.attrs?.readOnly;
161
- const whereClause = fieldLayoutInfo.attrs.whereClause;
162
121
 
163
122
  const [visibleCreateDialog, setVisibleCreateDialog] = useState(false);
164
- const { autoCompleteItems, fetchRelationEntities, populateFormikWithRelatedEntities, addNewRelation } = useRelationEntityHandler({ fieldContext, formik });
165
- const isFormFieldValid = (formik: any, fieldName: string) => formik.touched[fieldName] && formik.errors[fieldName];
166
123
 
167
- // const onChange = (e: any) => {
168
- // formik.setFieldValue(fieldContext.field.attrs.name, e.value);
169
- // };
124
+ const {
125
+ currentValues,
126
+ suggestions,
127
+ fetchCurrentValues,
128
+ fetchSuggestions,
129
+ linkItem,
130
+ unlinkItem,
131
+ addNewRelation,
132
+ } = useRelationEntityHandler({ fieldContext });
170
133
 
171
- useEffect(() => {
172
- populateFormikWithRelatedEntities();
173
- }, [formik.values?.id]);
134
+ const isFormFieldValid = (formik: any, fieldName: string) =>
135
+ formik.touched[fieldName] && formik.errors[fieldName];
174
136
 
137
+ // On mount: load already-linked items into currentValues
138
+ useEffect(() => {
139
+ fetchCurrentValues();
140
+ }, [fieldContext.data?.id]);
175
141
 
176
142
  const autoCompleteSearch = async (event: AutoCompleteCompleteEvent) => {
177
143
  const queryData: any = {
@@ -181,93 +147,72 @@ export const DefaultRelationManyToManyAutoCompleteFormEditWidget = ({ formik, fi
181
147
  $and: [
182
148
  {
183
149
  [fieldMetadata?.relationModel?.userKeyField?.name]: {
184
- [fieldLayoutInfo?.attrs?.autocompleteMatchMode || '$containsi']: event.query
185
- }
186
- }
187
- ]
188
- }
150
+ [fieldLayoutInfo?.attrs?.autocompleteMatchMode || '$containsi']: event.query,
151
+ },
152
+ },
153
+ ],
154
+ },
189
155
  };
156
+
190
157
  let fixedFilterToBeApplied = false;
191
158
  let fixedFilterParsed = false;
192
159
 
193
160
  if (fieldMetadata?.relationFieldFixedFilter || fieldLayoutInfo?.attrs?.whereClause) {
194
- const convertedFixedFilter = fieldLayoutInfo?.attrs?.whereClause ? fieldLayoutInfo?.attrs?.whereClause : fieldMetadata?.relationFieldFixedFilter;
161
+ const rawFilter = fieldLayoutInfo?.attrs?.whereClause ?? fieldMetadata?.relationFieldFixedFilter;
195
162
  fixedFilterToBeApplied = true;
196
- const fixedFilterTemplate = Handlebars.compile(convertedFixedFilter);
197
- const renderedFilter = fixedFilterTemplate(formik.values);
163
+ const rendered = Handlebars.compile(rawFilter)(formik.values);
198
164
 
199
- let parsedFilter: any;
200
165
  try {
201
- parsedFilter = JSON.parse(renderedFilter);
202
- const isValid = (obj: any): boolean => {
203
- if (!obj || typeof obj !== 'object') return false;
204
-
205
- const hasValidValue = (val: any): boolean => {
206
- if (val === null || val === undefined || val === '') return false;
207
- if (typeof val === 'object') {
208
- return Object.values(val).some(hasValidValue);
209
- }
210
- return true;
211
- };
212
-
213
- return hasValidValue(parsedFilter);
166
+ const parsed = JSON.parse(rendered);
167
+ const hasValue = (val: any): boolean => {
168
+ if (val === null || val === undefined || val === '') return false;
169
+ if (typeof val === 'object') return Object.values(val).some(hasValue);
170
+ return true;
214
171
  };
215
-
216
- if (isValid(parsedFilter)) {
217
- queryData.filters.$and.push(parsedFilter);
172
+ if (hasValue(parsed)) {
173
+ queryData.filters.$and.push(parsed);
218
174
  fixedFilterParsed = true;
219
175
  } else {
220
- console.warn(ERROR_MESSAGES.SKIPPING_EMPTY_FIXED_FILTER, parsedFilter);
176
+ console.warn(ERROR_MESSAGES.SKIPPING_EMPTY_FIXED_FILTER, parsed);
221
177
  }
222
178
  } catch (e) {
223
- console.error(ERROR_MESSAGES.INVALID_JSON_WHERECLAUSE, renderedFilter);
224
- parsedFilter = {};
179
+ console.error(ERROR_MESSAGES.INVALID_JSON_WHERECLAUSE, rendered);
225
180
  }
226
-
227
181
  }
228
182
 
229
- let autocompleteQs = qs.stringify(queryData, {
230
- encodeValuesOnly: true,
231
- });
232
- // if (whereClause) {
233
- // autocompleteQs = `${autocompleteQs}&${whereClause}`;
234
- // }
235
-
236
183
  if (fixedFilterToBeApplied && !fixedFilterParsed) {
237
184
  console.error(ERROR_MESSAGES.FIXED_FILTER_NOT_APPLIED);
238
-
239
185
  } else {
240
- // const autocompleteQs = qs.stringify(queryData, {
241
- // encodeValuesOnly: true,
242
- // });
243
- fetchRelationEntities(autocompleteQs);
186
+ fetchSuggestions(qs.stringify(queryData, { encodeValuesOnly: true }));
244
187
  }
245
188
  };
246
189
 
247
190
  return (
248
-
249
191
  <div className="relative">
250
192
  <div className="flex flex-column gap-2 mt-1 sm:mt-2 md:mt-3 lg:mt-4">
251
- {showFieldLabel != false &&
193
+ {showFieldLabel !== false && (
252
194
  <label htmlFor={fieldLayoutInfo.attrs.name} className="form-field-label">
253
195
  {fieldLabel}
254
196
  {fieldMetadata.required && <span className="text-red-500"> *</span>}
255
197
  <SolidFieldTooltip fieldContext={fieldContext} />
256
198
  </label>
257
- }
199
+ )}
258
200
  <div className="flex align-items-center gap-3">
259
201
  <AutoComplete
260
202
  readOnly={readOnly || readOnlyPermission}
261
203
  disabled={disabled || readOnlyPermission}
262
204
  multiple
263
- {...formik.getFieldProps(fieldLayoutInfo.attrs.name)}
264
205
  id={fieldLayoutInfo.attrs.name}
265
206
  field="label"
266
- value={formik.values[fieldLayoutInfo.attrs.name] || ''}
207
+ value={currentValues}
267
208
  dropdown={!readOnlyPermission}
268
- suggestions={autoCompleteItems}
209
+ suggestions={suggestions}
269
210
  completeMethod={autoCompleteSearch}
270
- onChange={(e) => fieldContext.onChange(e, 'onFieldChange')}
211
+ onChange={() => {
212
+ // Intentionally empty — currentValues is managed via onSelect/onUnselect
213
+ }}
214
+ onSelect={(e) => linkItem(e.value)}
215
+ onUnselect={(e) => unlinkItem(e.value)}
271
216
  className="solid-standard-autocomplete w-full"
272
217
  />
273
218
  {fieldContext.field.attrs.inlineCreate && (
@@ -301,163 +246,428 @@ export const DefaultRelationManyToManyAutoCompleteFormEditWidget = ({ formik, fi
301
246
  )}
302
247
  </div>
303
248
  );
304
- }
305
-
306
-
307
-
249
+ };
250
+
251
+
252
+
253
+ /**
254
+ * CHECKBOX WIDGET
255
+ *
256
+ * State:
257
+ * allOptions — every possible item to render as a checkbox row
258
+ * currentValues — the subset that is currently linked (drives checked state)
259
+ *
260
+ * Flow:
261
+ * mount → fetchCurrentValues() + fetchAllOptions()
262
+ * user checks → linkItem() → on success, adds to currentValues
263
+ * user unchecks → unlinkItem() → on success, removes from currentValues
264
+ */
308
265
  export const DefaultRelationManyToManyCheckBoxFormEditWidget = ({ formik, fieldContext }: SolidFormFieldWidgetProps) => {
309
266
  const fieldMetadata = fieldContext.fieldMetadata;
310
267
  const fieldLayoutInfo = fieldContext.field;
311
268
  const showFieldLabel = fieldLayoutInfo?.attrs?.showLabel;
312
-
313
269
  const readOnlyPermission = fieldContext.readOnly;
270
+
314
271
  const [visibleCreateDialog, setVisibleCreateDialog] = useState(false);
315
- const { autoCompleteItems, fetchRelationEntities, populateFormikWithRelatedEntities, addNewRelation } = useRelationEntityHandler({ fieldContext, formik });
316
272
 
273
+ const {
274
+ currentValues,
275
+ allOptions,
276
+ fetchCurrentValues,
277
+ fetchAllOptions,
278
+ linkItem,
279
+ unlinkItem,
280
+ addNewRelation,
281
+ } = useRelationEntityHandler({ fieldContext });
282
+
283
+ // On mount: load already-linked items + all possible options
317
284
  useEffect(() => {
318
- populateFormikWithRelatedEntities();
319
- }, [formik.values?.id]);
320
-
285
+ fetchCurrentValues();
286
+ }, [fieldContext.data?.id]);
321
287
 
322
288
  useEffect(() => {
323
- const fieldMetadata = fieldContext.fieldMetadata;
324
- const fieldLayoutInfo = fieldContext.field;
325
289
  const queryData: any = {
326
290
  offset: 0,
327
291
  limit: 1000,
328
- filters: {
329
- $and: []
330
- }
292
+ filters: { $and: [] },
331
293
  };
332
294
 
333
295
  let fixedFilterToBeApplied = false;
334
296
  let fixedFilterParsed = false;
335
297
 
336
298
  if (fieldMetadata?.relationFieldFixedFilter || fieldLayoutInfo?.attrs?.whereClause) {
337
- const convertedFixedFilter = fieldLayoutInfo?.attrs?.whereClause ? fieldLayoutInfo?.attrs?.whereClause : fieldMetadata?.relationFieldFixedFilter;
299
+ const rawFilter = fieldLayoutInfo?.attrs?.whereClause ?? fieldMetadata?.relationFieldFixedFilter;
338
300
  fixedFilterToBeApplied = true;
339
- const fixedFilterTemplate = Handlebars.compile(convertedFixedFilter);
340
- const renderedFilter = fixedFilterTemplate(formik.values);
301
+ const rendered = Handlebars.compile(rawFilter)(formik.values);
341
302
 
342
- let parsedFilter: any;
343
303
  try {
344
- parsedFilter = JSON.parse(renderedFilter);
345
- const isValid = (obj: any): boolean => {
346
- if (!obj || typeof obj !== 'object') return false;
347
-
348
- const hasValidValue = (val: any): boolean => {
349
- if (val === null || val === undefined || val === '') return false;
350
- if (typeof val === 'object') {
351
- return Object.values(val).some(hasValidValue);
352
- }
353
- return true;
354
- };
355
-
356
- return hasValidValue(parsedFilter);
304
+ const parsed = JSON.parse(rendered);
305
+ const hasValue = (val: any): boolean => {
306
+ if (val === null || val === undefined || val === '') return false;
307
+ if (typeof val === 'object') return Object.values(val).some(hasValue);
308
+ return true;
357
309
  };
358
-
359
- if (isValid(parsedFilter)) {
360
- queryData.filters.$and.push(parsedFilter);
310
+ if (hasValue(parsed)) {
311
+ queryData.filters.$and.push(parsed);
361
312
  fixedFilterParsed = true;
362
313
  } else {
363
- console.warn(ERROR_MESSAGES.SKIPPING_EMPTY_FIXED_FILTER, parsedFilter);
314
+ console.warn(ERROR_MESSAGES.SKIPPING_EMPTY_FIXED_FILTER, parsed);
364
315
  }
365
316
  } catch (e) {
366
- console.error(ERROR_MESSAGES.INVALID_JSON_WHERECLAUSE, renderedFilter);
367
- parsedFilter = {};
317
+ console.error(ERROR_MESSAGES.INVALID_JSON_WHERECLAUSE, rendered);
368
318
  }
369
-
370
319
  }
371
320
 
372
321
  if (fixedFilterToBeApplied && !fixedFilterParsed) {
373
322
  console.error(ERROR_MESSAGES.FIXED_FILTER_NOT_APPLIED);
374
-
375
323
  } else {
376
- const autocompleteQs = qs.stringify(queryData, {
377
- encodeValuesOnly: true,
378
- });
379
- fetchRelationEntities(autocompleteQs);
324
+ fetchAllOptions(qs.stringify(queryData, { encodeValuesOnly: true }));
380
325
  }
381
-
382
326
  }, [fieldContext, formik.values]);
383
327
 
384
- const handleCheckboxChange = (e: any) => {
385
- if (formik.values[fieldLayoutInfo.attrs.name].some((item: any) => item.value === e.value)) {
386
- formik.setFieldValue(fieldLayoutInfo.attrs.name, formik.values[fieldLayoutInfo.attrs.name].filter((s: any) => s.value !== e.value));
328
+ const handleCheckboxChange = (item: any) => {
329
+ const isCurrentlyLinked = currentValues.some((s) => s.value === item.value);
330
+ if (isCurrentlyLinked) {
331
+ unlinkItem(item);
387
332
  } else {
388
- formik.setFieldValue(fieldLayoutInfo.attrs.name, [...formik.values[fieldLayoutInfo.attrs.name], e]);
333
+ linkItem(item);
389
334
  }
390
335
  };
391
336
 
392
- const headerTemplate = (options: any) => {
393
- const className = `${options.className} justify-content-space-between`;
394
-
395
- return (
396
- <div className={className}>
397
- <div className="flex align-items-center gap-3">
398
- {showFieldLabel != false &&
399
- <label className="form-field-label">
400
- {capitalize(fieldLayoutInfo.attrs.name)}
401
- {fieldMetadata.required && <span className="text-red-500"> *</span>}
402
- <SolidFieldTooltip fieldContext={fieldContext} />
403
- </label>
404
- }
405
- {fieldContext.field.attrs.inlineCreate && (
406
- <>
407
- <Button
408
- icon="pi pi-plus"
409
- rounded
410
- outlined
411
- aria-label="Filter"
412
- type="button"
413
- size="small"
414
- onClick={() => setVisibleCreateDialog(true)}
415
- className="custom-add-button"
416
- />
417
- <InlineRelationEntityDialog
418
- visible={visibleCreateDialog}
419
- setVisible={setVisibleCreateDialog}
420
- fieldContext={fieldContext}
421
- onCreate={addNewRelation}
422
- />
423
- </>
424
- )}
425
- {/* <div className="many-to-many-add" >
426
- <Button icon="pi pi-plus"
337
+ const headerTemplate = (options: any) => (
338
+ <div className={`${options.className} justify-content-space-between`}>
339
+ <div className="flex align-items-center gap-3">
340
+ {showFieldLabel !== false && (
341
+ <label className="form-field-label">
342
+ {capitalize(fieldLayoutInfo.attrs.name)}
343
+ {fieldMetadata.required && <span className="text-red-500"> *</span>}
344
+ <SolidFieldTooltip fieldContext={fieldContext} />
345
+ </label>
346
+ )}
347
+ {fieldContext.field.attrs.inlineCreate && (
348
+ <>
349
+ <Button
350
+ icon="pi pi-plus"
427
351
  rounded
428
352
  outlined
429
353
  aria-label="Filter"
430
354
  type="button"
431
- onClick={() => autoCompleteSearch()}
355
+ size="small"
356
+ onClick={() => setVisibleCreateDialog(true)}
357
+ className="custom-add-button"
432
358
  />
433
- </div> */}
434
- </div>
435
- <div>
436
- {options.togglerElement}
437
- </div>
359
+ <InlineRelationEntityDialog
360
+ visible={visibleCreateDialog}
361
+ setVisible={setVisibleCreateDialog}
362
+ fieldContext={fieldContext}
363
+ onCreate={addNewRelation}
364
+ />
365
+ </>
366
+ )}
438
367
  </div>
439
- );
440
- };
368
+ <div>{options.togglerElement}</div>
369
+ </div>
370
+ );
371
+
441
372
  return (
442
373
  <div>
443
374
  <Panel toggleable headerTemplate={headerTemplate}>
444
375
  <div className="formgrid grid">
445
- {autoCompleteItems && autoCompleteItems.map((a: any, i: number) => {
446
- return (
447
- <div key={a.label} className={`field col-6 flex gap-2 ${i >= 2 ? 'mt-3' : ''}`}>
448
- <Checkbox
449
- readOnly={readOnlyPermission}
450
- inputId={a.label}
451
- checked={formik.values[fieldLayoutInfo.attrs.name].some((item: any) => item.label === a.label)}
452
- onChange={() => handleCheckboxChange(a)}
453
- />
454
- <label htmlFor={a.label} className="form-field-label m-0"> {a.label}</label>
455
- </div>
456
- )
457
- })}
376
+ {allOptions.map((item: any, i: number) => (
377
+ <div key={item.value} className={`field col-6 flex gap-2 ${i >= 2 ? 'mt-3' : ''}`}>
378
+ <Checkbox
379
+ readOnly={readOnlyPermission}
380
+ inputId={item.label}
381
+ checked={currentValues.some((s) => s.value === item.value)}
382
+ onChange={() => handleCheckboxChange(item)}
383
+ />
384
+ <label htmlFor={item.label} className="form-field-label m-0">
385
+ {item.label}
386
+ </label>
387
+ </div>
388
+ ))}
458
389
  </div>
459
390
  </Panel>
460
391
  </div>
461
- )
392
+ );
393
+ };
462
394
 
463
- }
395
+
396
+
397
+ const buildRelationCustomFilter = ({
398
+ fieldContext,
399
+ fieldLayoutInfo,
400
+ }: {
401
+ fieldContext: any;
402
+ fieldLayoutInfo?: any;
403
+ }) => {
404
+ if (!fieldContext) return { id: { $eq: -1 } };
405
+
406
+ const relationFieldName =
407
+ fieldContext.fieldMetadata?.relationCoModelFieldName ?? fieldContext.modelName;
408
+ const parentId = fieldContext.data?.id ?? -1;
409
+ const baseFilter = { [relationFieldName]: { id: { $eq: parentId } } };
410
+ const whereClause = fieldLayoutInfo?.attrs?.whereClause;
411
+
412
+ if (!whereClause) return { $and: [baseFilter] };
413
+
414
+ try {
415
+ return { $and: [baseFilter, JSON.parse(whereClause)] };
416
+ } catch (error) {
417
+ console.error("Failed to parse whereClause:", error);
418
+ return { $and: [baseFilter] };
419
+ }
420
+ };
421
+
422
+ export const DefaultRelationManyToManyListFormEditWidget = ({ formik, fieldContext }: SolidFormFieldWidgetProps) => {
423
+ const fieldMetadata = fieldContext.fieldMetadata;
424
+ const router = useRouter();
425
+ const fieldLayoutInfo = fieldContext.field;
426
+ const fieldLabel = fieldLayoutInfo.attrs.label ?? fieldMetadata.displayName;
427
+ const solidFormViewMetaData = fieldContext.solidFormViewMetaData;
428
+ const [visibleCreateRelationEntity, setvisibleCreateRelationEntity] = useState(false);
429
+ const [listViewParams, setListViewParams] = useState<any>();
430
+ const [formViewParams, setformViewParams] = useState<FormViewParams>();
431
+ const [refreshList, setRefreshList] = useState(false);
432
+ const showFieldLabel = fieldLayoutInfo?.attrs?.showLabel;
433
+ const readOnlyPermission = fieldContext.readOnly;
434
+ const pathname = usePathname();
435
+ const lastPathSegment = pathname.split('/').pop();
436
+ const userKeyField: any = Object.entries(
437
+ fieldContext.solidFormViewMetaData.data.solidFieldsMetadata
438
+ ).find(([_, value]: any) => value.isUserKey)?.[0];
439
+ const [showSaveParentEntityConfirmationPopup, setShowSaveParentEntityConfirmationPopup] = useState(false);
440
+
441
+
442
+
443
+ const [visibleLinkDialog, setVisibleLinkDialog] = useState(false);
444
+ const [linkSearchResults, setLinkSearchResults] = useState<any[]>([]);
445
+ const [selectedLinkItem, setSelectedLinkItem] = useState<any>(null);
446
+ const [isLinking, setIsLinking] = useState(false);
447
+
448
+ const { fetchSuggestions, linkItem, unlinkItem, suggestions } = useRelationEntityHandler({ fieldContext });
449
+
450
+ const handleAddClickForEmbeddedView = () => {
451
+ if (lastPathSegment === "new") {
452
+ setShowSaveParentEntityConfirmationPopup(true);
453
+ } else {
454
+ setSelectedLinkItem(null);
455
+ setLinkSearchResults([]);
456
+ setVisibleLinkDialog(true);
457
+ }
458
+ };
459
+
460
+ const handleLinkSearch = async (event: AutoCompleteCompleteEvent) => {
461
+ const queryData: any = {
462
+ offset: 0,
463
+ limit: 1000,
464
+ filters: {
465
+ $and: [
466
+ {
467
+ [fieldMetadata?.relationModel?.userKeyField?.name]: {
468
+ [fieldLayoutInfo?.attrs?.autocompleteMatchMode || '$containsi']: event.query,
469
+ },
470
+ },
471
+ ],
472
+ },
473
+ };
474
+ await fetchSuggestions(qs.stringify(queryData, { encodeValuesOnly: true }));
475
+ };
476
+
477
+ const handleLinkConfirm = async () => {
478
+ if (!selectedLinkItem) return;
479
+ setIsLinking(true);
480
+ try {
481
+ await linkItem(selectedLinkItem);
482
+ setVisibleLinkDialog(false);
483
+ setSelectedLinkItem(null);
484
+ setRefreshList((prev) => !prev);
485
+ } finally {
486
+ setIsLinking(false);
487
+ }
488
+ };
489
+
490
+ const handleDeleteClick = async (id: any) => {
491
+ await unlinkItem({ value: id, label: '' });
492
+ setRefreshList((prev) => !prev);
493
+ };
494
+
495
+ const handleEditClickForEmbeddedView = (id: any) => {
496
+ if (id === "new") {
497
+ setShowSaveParentEntityConfirmationPopup(true);
498
+ } else {
499
+ setformViewParams({
500
+ moduleName: fieldContext.fieldMetadata.relationModelModuleName,
501
+ id,
502
+ embeded: true,
503
+ isCustomCreate: false,
504
+ customLayout: fieldLayoutInfo?.attrs?.inlineCreateLayout,
505
+ modelName: camelCase(fieldContext.fieldMetadata.relationCoModelSingularName),
506
+ parentFieldName: fieldContext.fieldMetadata.relationCoModelFieldName,
507
+ parentData: userKeyField
508
+ ? { [userKeyField]: { solidManyToOneLabel: fieldContext.data[userKeyField], solidManyToOneValue: fieldContext.data['id'] } }
509
+ : {},
510
+ onEmbeddedFormSave: fieldContext.onEmbeddedFormSave,
511
+ inlineCreateAutoSave: fieldLayoutInfo?.attrs?.inlineCreateAutoSave,
512
+ });
513
+ setvisibleCreateRelationEntity(true);
514
+ }
515
+ };
516
+
517
+ const handlePopupClose = () => {
518
+ setvisibleCreateRelationEntity(false);
519
+ const currentUrl = new URL(window.location.href);
520
+ currentUrl.searchParams.delete('childEntity');
521
+ router.push(currentUrl.toString());
522
+ setRefreshList((prev) => !prev);
523
+ setListViewParams({
524
+ moduleName: fieldContext.fieldMetadata.relationModelModuleName,
525
+ modelName: camelCase(fieldContext.fieldMetadata.relationCoModelSingularName),
526
+ inlineCreate: readOnlyPermission === false,
527
+ customLayout: fieldLayoutInfo?.attrs?.inlineListLayout,
528
+ embeded: true,
529
+ id: fieldContext.data?.id ?? 'new',
530
+ customFilter: buildRelationCustomFilter({ fieldContext, fieldLayoutInfo }),
531
+ });
532
+ };
533
+
534
+ useEffect(() => {
535
+ setListViewParams({
536
+ moduleName: fieldContext.fieldMetadata.relationModelModuleName,
537
+ modelName: camelCase(fieldContext.fieldMetadata.relationCoModelSingularName),
538
+ inlineCreate: readOnlyPermission === false,
539
+ customLayout: fieldLayoutInfo?.attrs?.inlineListLayout,
540
+ embeded: true,
541
+ id: fieldContext.data?.id ?? 'new',
542
+ customFilter: buildRelationCustomFilter({ fieldContext, fieldLayoutInfo }),
543
+ });
544
+
545
+ setformViewParams({
546
+ moduleName: fieldContext.fieldMetadata.relationModelModuleName,
547
+ modelName: camelCase(fieldContext.fieldMetadata.relationCoModelSingularName),
548
+ parentFieldName: fieldContext.fieldMetadata.relationCoModelFieldName,
549
+ id: "new",
550
+ embeded: true,
551
+ isCustomCreate: false,
552
+ customLayout: fieldLayoutInfo?.attrs?.inlineCreateLayout,
553
+ parentData: userKeyField
554
+ ? { [userKeyField]: { solidManyToOneLabel: fieldContext.data[userKeyField], solidManyToOneValue: fieldContext.data['id'] } }
555
+ : {},
556
+ onEmbeddedFormSave: fieldContext.onEmbeddedFormSave,
557
+ inlineCreateAutoSave: fieldLayoutInfo?.attrs?.inlineCreateAutoSave,
558
+ });
559
+ }, [readOnlyPermission]);
560
+
561
+ const saveParentEntity = async () => {
562
+ const currentUrl = new URL(window.location.href);
563
+ currentUrl.searchParams.set('childEntity', fieldLayoutInfo.attrs.name);
564
+ currentUrl.searchParams.set('viewMode', 'edit');
565
+ try {
566
+ router.push(currentUrl.toString());
567
+ await formik.handleSubmit();
568
+ } catch { }
569
+ setShowSaveParentEntityConfirmationPopup(false);
570
+ };
571
+
572
+ return (
573
+ <div>
574
+ {showFieldLabel !== false && (
575
+ <label htmlFor={fieldLayoutInfo.attrs.name} className="form-field-label">
576
+ {fieldLabel}
577
+ {fieldMetadata.required && <span className="text-red-500"> *</span>}
578
+ <SolidFieldTooltip fieldContext={fieldContext} />
579
+ </label>
580
+ )}
581
+ {listViewParams && (
582
+ <SolidListView key={refreshList.toString()} {...listViewParams} embededFieldRelationType="many-to-many" handleAddClickForEmbeddedView={handleAddClickForEmbeddedView} handleEditClickForEmbeddedView={handleEditClickForEmbeddedView} handleDeleteClick={handleDeleteClick} />
583
+ )}
584
+ {readOnlyPermission !== true && formViewParams && (
585
+ <RenderSolidFormEmbededView
586
+ formik={formik}
587
+ fieldContext={fieldContext}
588
+ visibleCreateRelationEntity={visibleCreateRelationEntity}
589
+ setvisibleCreateRelationEntity={setvisibleCreateRelationEntity}
590
+ formViewParams={formViewParams}
591
+ handlePopupClose={handlePopupClose}
592
+ />
593
+ )}
594
+
595
+ <Dialog
596
+ header={`Link existing ${fieldLabel}`}
597
+ visible={visibleLinkDialog}
598
+ style={{ width: '30vw', minWidth: 320 }}
599
+ onHide={() => setVisibleLinkDialog(false)}
600
+ footer={
601
+ <div className="flex gap-2 justify-content-end">
602
+ <Button
603
+ label="Link"
604
+ size="small"
605
+ disabled={!selectedLinkItem || isLinking}
606
+ loading={isLinking}
607
+ onClick={handleLinkConfirm}
608
+ />
609
+ <Button
610
+ label="Cancel"
611
+ size="small"
612
+ outlined
613
+ className="bg-primary-reverse"
614
+ onClick={() => setVisibleLinkDialog(false)}
615
+ />
616
+ </div>
617
+ }
618
+ >
619
+ <div className="flex flex-column gap-2 pt-2">
620
+ <label className="form-field-label">
621
+ Search {fieldLabel}
622
+ </label>
623
+ <AutoComplete
624
+ field="label"
625
+ value={selectedLinkItem}
626
+ suggestions={suggestions}
627
+ completeMethod={handleLinkSearch}
628
+ onChange={(e) => setSelectedLinkItem(e.value)}
629
+ onSelect={(e) => setSelectedLinkItem(e.value)}
630
+ placeholder={`Type to search...`}
631
+ className="w-full"
632
+ dropdown
633
+ />
634
+ </div>
635
+ </Dialog>
636
+
637
+
638
+ <Dialog
639
+ showHeader={false}
640
+ headerClassName="py-2"
641
+ contentClassName="px-0 pb-0"
642
+ className="solid-confirm-dialog"
643
+ contentStyle={{ borderRadius: 6 }}
644
+ visible={showSaveParentEntityConfirmationPopup}
645
+ style={{ width: '20vw' }}
646
+ onHide={() => {
647
+ if (!showSaveParentEntityConfirmationPopup) return;
648
+ setShowSaveParentEntityConfirmationPopup(false);
649
+ }}
650
+ >
651
+ <div className="p-4">
652
+ <p className="m-0 solid-primary-title" style={{ fontSize: 16 }}>
653
+ Before Creating {fieldLabel} you need to save{' '}
654
+ {solidFormViewMetaData?.data?.solidView?.model?.displayName
655
+ ? solidFormViewMetaData?.data?.solidView?.model?.displayName
656
+ : capitalize(fieldContext.modelName)}
657
+ . Please click save if you wish to proceed?
658
+ </p>
659
+ <div className="flex align-items-center justify-content-start gap-2 mt-3">
660
+ <Button label="Save" size="small" onClick={saveParentEntity} />
661
+ <Button
662
+ label="Cancel"
663
+ size="small"
664
+ onClick={() => setShowSaveParentEntityConfirmationPopup(false)}
665
+ outlined
666
+ className="bg-primary-reverse"
667
+ />
668
+ </div>
669
+ </div>
670
+ </Dialog>
671
+ </div>
672
+ );
673
+ };