@comet/admin-generator 9.0.0-canary-20250915134805 → 9.0.0-canary-20251002064922

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.
@@ -15,18 +15,6 @@ type Icon = IconName | IconObject | ComponentType;
15
15
  export type Adornment = string | {
16
16
  icon: Icon;
17
17
  };
18
- type SingleFileFormFieldConfig = {
19
- type: "fileUpload";
20
- multiple?: false;
21
- maxFiles?: 1;
22
- download?: boolean;
23
- } & Pick<Partial<FinalFormFileUploadProps<false>>, "maxFileSize" | "readOnly" | "layout" | "accept">;
24
- type MultiFileFormFieldConfig = {
25
- type: "fileUpload";
26
- multiple: true;
27
- maxFiles?: number;
28
- download?: boolean;
29
- } & Pick<Partial<FinalFormFileUploadProps<true>>, "maxFileSize" | "readOnly" | "layout" | "accept">;
30
18
  type InputBaseFieldConfig = {
31
19
  startAdornment?: Adornment;
32
20
  endAdornment?: Adornment;
@@ -39,73 +27,103 @@ export type StaticSelectValue = {
39
27
  value: string;
40
28
  label: string;
41
29
  } | string;
30
+ type AsyncSelectFilter = {
31
+ /**
32
+ * Filter by value of field in current form
33
+ */
34
+ type: "field";
35
+ /**
36
+ * Name of the field in current form, that will be used to filter the query
37
+ */
38
+ formFieldName: string;
39
+ /**
40
+ * Name of the graphql argument the prop will be applied to. Defaults to propdName.
41
+ *
42
+ * Root Argument or filter argument are supported.
43
+ */
44
+ rootQueryArg?: string;
45
+ } | {
46
+ /**
47
+ * Filter by a prop passed into the form, this prop will be generated
48
+ */
49
+ type: "formProp";
50
+ /**
51
+ * Name of the prop generated for this form
52
+ */
53
+ propName: string;
54
+ /**
55
+ * Name of the graphql argument the prop will be applied to. Defaults to propdName.
56
+ *
57
+ * Root Argument or filter argument are supported.
58
+ */
59
+ rootQueryArg?: string;
60
+ };
42
61
  export type FormFieldConfig<T> = (({
43
62
  type: "text";
63
+ name: keyof T;
44
64
  multiline?: boolean;
45
65
  } & InputBaseFieldConfig) | ({
46
66
  type: "number";
67
+ name: keyof T;
47
68
  decimals?: number;
48
69
  } & InputBaseFieldConfig) | ({
49
70
  type: "numberRange";
71
+ name: keyof T;
50
72
  minValue: number;
51
73
  maxValue: number;
52
74
  disableSlider?: boolean;
53
75
  } & InputBaseFieldConfig) | {
54
76
  type: "boolean";
77
+ name: keyof T;
55
78
  } | ({
56
79
  type: "date";
80
+ name: keyof T;
57
81
  } & InputBaseFieldConfig) | ({
58
82
  type: "dateTime";
83
+ name: keyof T;
59
84
  } & InputBaseFieldConfig) | ({
60
85
  type: "staticSelect";
86
+ name: keyof T;
61
87
  values?: StaticSelectValue[];
62
88
  inputType?: "select" | "radio";
63
89
  } & Omit<InputBaseFieldConfig, "endAdornment">) | ({
64
90
  type: "asyncSelect";
91
+ name: keyof T;
65
92
  rootQuery: string;
66
93
  labelField?: string;
67
94
  /**
68
95
  * filter for query, passed as variable to graphql query
69
96
  */
70
- filter?: {
71
- /**
72
- * Filter by value of field in current form
73
- */
74
- type: "field";
75
- /**
76
- * Name of the field in current form, that will be used to filter the query
77
- */
78
- formFieldName: string;
79
- /**
80
- * Name of the graphql argument the prop will be applied to. Defaults to propdName.
81
- *
82
- * Root Argument or filter argument are supported.
83
- */
84
- rootQueryArg?: string;
85
- } | {
86
- /**
87
- * Filter by a prop passed into the form, this prop will be generated
88
- */
89
- type: "formProp";
90
- /**
91
- * Name of the prop generated for this form
92
- */
93
- propName: string;
94
- /**
95
- * Name of the graphql argument the prop will be applied to. Defaults to propdName.
96
- *
97
- * Root Argument or filter argument are supported.
98
- */
99
- rootQueryArg?: string;
100
- };
97
+ filter?: AsyncSelectFilter;
98
+ } & Omit<InputBaseFieldConfig, "endAdornment">) | ({
99
+ type: "asyncSelectFilter";
100
+ name: string;
101
+ loadValueQueryField: string;
102
+ rootQuery: string;
103
+ labelField?: string;
104
+ /**
105
+ * filter for query, passed as variable to graphql query
106
+ */
107
+ filter?: AsyncSelectFilter;
101
108
  } & Omit<InputBaseFieldConfig, "endAdornment">) | {
102
109
  type: "block";
110
+ name: keyof T;
103
111
  block: BlockInterface;
104
- } | SingleFileFormFieldConfig | MultiFileFormFieldConfig) & {
112
+ } | ({
113
+ type: "fileUpload";
114
+ multiple?: false;
115
+ name: keyof T;
116
+ maxFiles?: 1;
117
+ download?: boolean;
118
+ } & Pick<Partial<FinalFormFileUploadProps<false>>, "maxFileSize" | "readOnly" | "layout" | "accept">) | ({
119
+ type: "fileUpload";
120
+ multiple: true;
105
121
  name: keyof T;
122
+ maxFiles?: number;
123
+ download?: boolean;
124
+ } & Pick<Partial<FinalFormFileUploadProps<true>>, "maxFileSize" | "readOnly" | "layout" | "accept">)) & {
106
125
  label?: string;
107
126
  required?: boolean;
108
- virtual?: boolean;
109
127
  validate?: FieldValidator<unknown>;
110
128
  helperText?: string;
111
129
  readOnly?: boolean;
@@ -1,11 +1,24 @@
1
- import { type IntrospectionQuery } from "graphql";
1
+ import { type IntrospectionObjectType, type IntrospectionQuery } from "graphql";
2
2
  import { type FormConfig, type FormFieldConfig } from "../../generate-command";
3
3
  import { type GenerateFieldsReturn } from "../generateFields";
4
+ /**
5
+ * Helper that returns the introspection object type for a given form field config, supporting the special case for asyncSelectFilter
6
+ */
7
+ export declare function findIntrospectionObjectType({ config, gqlIntrospection, gqlType, }: {
8
+ config: FormFieldConfig<any>;
9
+ gqlIntrospection: IntrospectionQuery;
10
+ gqlType: string;
11
+ }): {
12
+ multiple: boolean;
13
+ objectType: IntrospectionObjectType;
14
+ };
4
15
  export declare function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, formConfig, gqlType, namePrefix, }: {
5
16
  gqlIntrospection: IntrospectionQuery;
6
17
  baseOutputFilename: string;
7
18
  config: Extract<FormFieldConfig<any>, {
8
19
  type: "asyncSelect";
20
+ } | {
21
+ type: "asyncSelectFilter";
9
22
  }>;
10
23
  formConfig: FormConfig<any>;
11
24
  gqlType: string;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findIntrospectionObjectType = findIntrospectionObjectType;
3
4
  exports.generateAsyncSelect = generateAsyncSelect;
4
5
  const generate_command_1 = require("../../generate-command");
5
6
  const findQueryType_1 = require("../../utils/findQueryType");
@@ -56,28 +57,69 @@ function buildTypeInfo(arg, gqlIntrospection) {
56
57
  inputType,
57
58
  };
58
59
  }
60
+ /**
61
+ * Helper that returns the introspection object type for a given form field config, supporting the special case for asyncSelectFilter
62
+ */
63
+ function findIntrospectionObjectType({ config, gqlIntrospection, gqlType, }) {
64
+ const name = String(config.name);
65
+ const introspectionObject = gqlIntrospection.__schema.types.find((type) => type.kind === "OBJECT" && type.name === gqlType);
66
+ if (!introspectionObject)
67
+ throw new Error(`didn't find object ${gqlType} in gql introspection`);
68
+ function findIntrospectionField(introspectionObject, name) {
69
+ const introspectionField = introspectionObject.fields.find((field) => field.name === name);
70
+ if (!introspectionField)
71
+ throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`);
72
+ let introspectionFieldType = introspectionField.type.kind === "NON_NULL" ? introspectionField.type.ofType : introspectionField.type;
73
+ const multiple = (introspectionFieldType === null || introspectionFieldType === void 0 ? void 0 : introspectionFieldType.kind) === "LIST";
74
+ if ((introspectionFieldType === null || introspectionFieldType === void 0 ? void 0 : introspectionFieldType.kind) === "LIST") {
75
+ introspectionFieldType =
76
+ introspectionFieldType.ofType.kind === "NON_NULL" ? introspectionFieldType.ofType.ofType : introspectionFieldType.ofType;
77
+ }
78
+ if (introspectionFieldType.kind !== "OBJECT")
79
+ throw new Error(`asyncSelect only supports OBJECT types`);
80
+ const objectType = gqlIntrospection.__schema.types.find((t) => t.kind === "OBJECT" && t.name === introspectionFieldType.name);
81
+ if (!objectType)
82
+ throw new Error(`Object type ${introspectionFieldType.name} not found for field ${name}`);
83
+ return { multiple, objectType };
84
+ }
85
+ if (config.type === "asyncSelectFilter") {
86
+ //for a filter select the field is "virtual", and it's ObjectType is defined by the path in config.loadValueQueryField
87
+ return {
88
+ multiple: false,
89
+ objectType: config.loadValueQueryField.split(".").reduce((acc, fieldName) => {
90
+ const introspectionField = findIntrospectionField(acc, fieldName);
91
+ if (introspectionField.multiple)
92
+ throw new Error(`asyncSelectFilter does not support list fields in loadValueQueryField`);
93
+ return introspectionField.objectType;
94
+ }, introspectionObject),
95
+ };
96
+ }
97
+ else {
98
+ //for a standard select we just find the field directly (no nested path to follow, name can be only one level deep)
99
+ return findIntrospectionField(introspectionObject, name);
100
+ }
101
+ }
59
102
  function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, formConfig, gqlType, namePrefix, }) {
60
103
  var _a, _b;
61
104
  const imports = [];
62
105
  const formProps = [];
63
- const { name, fieldLabel, introspectionFieldType, startAdornment,
106
+ const { name, fieldLabel, startAdornment,
64
107
  //endAdornment,
65
108
  imports: optionsImports, } = (0, options_1.buildFormFieldOptions)({ config, formConfig, gqlIntrospection, gqlType });
66
109
  imports.push(...optionsImports);
67
110
  const nameWithPrefix = `${namePrefix ? `${namePrefix}.` : ``}${name}`;
68
111
  const required = !(0, isFieldOptional_1.isFieldOptional)({ config, gqlIntrospection, gqlType });
69
- const defaultFormValuesConfig = {
70
- destructFromFormValues: config.virtual ? name : undefined,
112
+ const formValueConfig = {
113
+ destructFromFormValues: config.type == "asyncSelectFilter" ? name : undefined,
71
114
  };
72
- const formValuesConfig = [defaultFormValuesConfig]; // FormFields should only contain one entry
73
115
  let finalFormConfig;
74
116
  let code = "";
75
117
  let formValueToGqlInputCode = "";
76
- if (introspectionFieldType.kind !== "OBJECT")
77
- throw new Error(`asyncSelect only supports OBJECT types`);
78
- const objectType = gqlIntrospection.__schema.types.find((t) => t.kind === "OBJECT" && t.name === introspectionFieldType.name);
79
- if (!objectType)
80
- throw new Error(`Object type ${introspectionFieldType.name} not found for field ${name}`);
118
+ const { objectType, multiple } = findIntrospectionObjectType({
119
+ config,
120
+ gqlIntrospection,
121
+ gqlType,
122
+ });
81
123
  //find labelField: 1. as configured
82
124
  let labelField = config.labelField;
83
125
  //find labelField: 2. common names (name or title)
@@ -105,7 +147,13 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
105
147
  const rootQuery = config.rootQuery; //TODO we should infer a default value from the gql schema
106
148
  const queryName = `${rootQuery[0].toUpperCase() + rootQuery.substring(1)}Select`;
107
149
  const rootQueryType = (0, findQueryType_1.findQueryTypeOrThrow)(rootQuery, gqlIntrospection);
108
- const formFragmentField = `${name} { id ${labelField} }`;
150
+ let formFragmentFields;
151
+ if (config.type == "asyncSelectFilter") {
152
+ formFragmentFields = [`${config.loadValueQueryField}.id`, `${config.loadValueQueryField}.${labelField}`];
153
+ }
154
+ else {
155
+ formFragmentFields = [`${name}.id`, `${name}.${labelField}`];
156
+ }
109
157
  const filterConfig = config.filter
110
158
  ? (() => {
111
159
  var _a;
@@ -118,9 +166,9 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
118
166
  throw new Error(`Field ${String(config.name)}: No field with name "${config.filter.formFieldName}" referenced as filter.formFieldName found in form-config.`);
119
167
  }
120
168
  if (!(0, generate_command_1.isFormFieldConfig)(filterField)) {
121
- throw new Error(`Field ${String(config.name)}: Field with name "${config.filter.formFieldName}" referenced as filter.fieldName is no FormField.`);
169
+ throw new Error(`Field ${String(config.name)}: Field with name "${config.filter.formFieldName}" referenced as filter.formFieldName is no FormField.`);
122
170
  }
123
- filterVar = `values.${filterField.type === "asyncSelect" ? `${String(filterField.name)}?.id` : String(filterField.name)}`;
171
+ filterVar = `values.${filterField.type === "asyncSelect" || filterField.type === "asyncSelectFilter" ? `${String(filterField.name)}?.id` : String(filterField.name)}`;
124
172
  if (!rootQueryArg) {
125
173
  rootQueryArg = config.filter.formFieldName;
126
174
  }
@@ -194,7 +242,7 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
194
242
  }
195
243
  }
196
244
  else {
197
- throw new Error(`Neither filter-prop nor root-prop with name: ${rootQueryArg} for asyncSelect-query not found. Consider setting filter.rootQueryArg explicitly.`);
245
+ throw new Error(`Neither filter-prop nor root-prop with name: ${rootQueryArg} for asyncSelect-query not found. Consider setting filterField.gqlVarName explicitly.`);
198
246
  }
199
247
  }
200
248
  if (config.filter.type === "formProp") {
@@ -216,12 +264,17 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
216
264
  imports.push({ name: "OnChangeField", importPath: "@comet/admin" });
217
265
  finalFormConfig = { subscription: { values: true }, renderProps: { values: true, form: true } };
218
266
  }
219
- if (!config.virtual) {
220
- if (!required) {
221
- formValueToGqlInputCode = `${name}: formValues.${name} ? formValues.${name}.id : null,`;
267
+ if (config.type != "asyncSelectFilter") {
268
+ if (!multiple) {
269
+ if (!required) {
270
+ formValueToGqlInputCode = `${name}: formValues.${name} ? formValues.${name}.id : null,`;
271
+ }
272
+ else {
273
+ formValueToGqlInputCode = `${name}: formValues.${name}?.id,`;
274
+ }
222
275
  }
223
276
  else {
224
- formValueToGqlInputCode = `${name}: formValues.${name}?.id,`;
277
+ formValueToGqlInputCode = `${name}: formValues.${name}.map((item) => item.id),`;
225
278
  }
226
279
  }
227
280
  imports.push({
@@ -232,11 +285,18 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
232
285
  name: `GQL${queryName}QueryVariables`,
233
286
  importPath: `./${baseOutputFilename}.generated`,
234
287
  });
288
+ const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1);
289
+ if (config.type == "asyncSelectFilter") {
290
+ // add (in the gql schema) non existing value for virtual filter field
291
+ formValueConfig.typeCode = `${name}?: { id: string; ${labelField}: string };`;
292
+ formValueConfig.initializationCode = `${name}: data.${instanceGqlType}.${config.loadValueQueryField.replace(/\./g, "?.")}`;
293
+ }
235
294
  code = `<AsyncSelectField
236
295
  ${required ? "required" : ""}
237
296
  variant="horizontal"
238
297
  fullWidth
239
298
  ${config.readOnly ? "readOnly disabled" : ""}
299
+ ${multiple ? "multiple" : ""}
240
300
  name="${nameWithPrefix}"
241
301
  label={${fieldLabel}}
242
302
  ${config.startAdornment ? `startAdornment={<InputAdornment position="start">${startAdornment.adornmentString}</InputAdornment>}` : ""}
@@ -270,11 +330,11 @@ function generateAsyncSelect({ gqlIntrospection, baseOutputFilename, config, for
270
330
  code,
271
331
  hooksCode: "",
272
332
  formValueToGqlInputCode,
273
- formFragmentFields: [formFragmentField],
333
+ formFragmentFields,
274
334
  gqlDocuments: {},
275
335
  imports,
276
336
  formProps,
277
- formValuesConfig,
337
+ formValuesConfig: [formValueConfig],
278
338
  finalFormConfig,
279
339
  };
280
340
  }
@@ -23,6 +23,6 @@ export declare function buildFormFieldOptions({ config, formConfig, gqlIntrospec
23
23
  startAdornment: AdornmentData;
24
24
  endAdornment: AdornmentData;
25
25
  imports: Imports;
26
- introspectionFieldType: import("graphql").IntrospectionNamedTypeRef<import("graphql").IntrospectionOutputType> | import("graphql").IntrospectionListTypeRef<import("graphql").IntrospectionOutputTypeRef>;
26
+ introspectionFieldType: import("graphql").IntrospectionNamedTypeRef<import("graphql").IntrospectionOutputType> | import("graphql").IntrospectionListTypeRef<import("graphql").IntrospectionOutputTypeRef> | undefined;
27
27
  };
28
28
  export {};
@@ -62,9 +62,11 @@ function buildFormFieldOptions({ config, formConfig, gqlIntrospection, gqlType,
62
62
  if (!introspectionObject)
63
63
  throw new Error(`didn't find object ${gqlType} in gql introspection`);
64
64
  const introspectionField = introspectionObject.fields.find((field) => field.name === name);
65
- if (!introspectionField)
66
- throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`);
67
- const introspectionFieldType = introspectionField.type.kind === "NON_NULL" ? introspectionField.type.ofType : introspectionField.type;
65
+ const introspectionFieldType = introspectionField
66
+ ? introspectionField.type.kind === "NON_NULL"
67
+ ? introspectionField.type.ofType
68
+ : introspectionField.type
69
+ : undefined;
68
70
  const imports = [];
69
71
  let startAdornment = { adornmentString: "" };
70
72
  let endAdornment = { adornmentString: "" };
@@ -18,6 +18,7 @@ const findMutationType_1 = require("../utils/findMutationType");
18
18
  const generateImportsCode_1 = require("../utils/generateImportsCode");
19
19
  const runtimeTypeGuards_1 = require("../utils/runtimeTypeGuards");
20
20
  const generateFields_1 = require("./generateFields");
21
+ const generateFragmentByFormFragmentFields_1 = require("./generateFragmentByFormFragmentFields");
21
22
  const getForwardedGqlArgs_1 = require("./getForwardedGqlArgs");
22
23
  function generateFormPropsCode(props) {
23
24
  if (!props.length)
@@ -141,10 +142,6 @@ config) {
141
142
  const fileFields = formFields.filter((field) => field.type == "fileUpload");
142
143
  if (fileFields.length > 0) {
143
144
  imports.push({ name: "GQLFinalFormFileUploadFragment", importPath: "@comet/cms-admin" });
144
- }
145
- // Unnecessary field.type == "fileUpload" check to make TypeScript happy
146
- const downloadableFileFields = fileFields.filter((field) => field.type == "fileUpload" && field.download);
147
- if (fileFields.length > 0) {
148
145
  imports.push({ name: "GQLFinalFormFileUploadDownloadableFragment", importPath: "@comet/cms-admin" });
149
146
  }
150
147
  let hooksCode = "";
@@ -173,13 +170,7 @@ config) {
173
170
  formValuesConfig.push(...generatedFields.formValuesConfig);
174
171
  const { formPropsTypeCode, formPropsParamsCode } = generateFormPropsCode(formProps);
175
172
  gqlDocuments[`${instanceGqlType}FormFragment`] = {
176
- document: `
177
- fragment ${formFragmentName} on ${gqlType} {
178
- ${formFragmentFields.join("\n")}
179
- }
180
- ${fileFields.length > 0 && fileFields.length !== downloadableFileFields.length ? "${finalFormFileUploadFragment}" : ""}
181
- ${downloadableFileFields.length > 0 ? "${finalFormFileUploadDownloadableFragment}" : ""}
182
- `,
173
+ document: (0, generateFragmentByFormFragmentFields_1.generateFragmentByFormFragmentFields)({ formFragmentName, gqlType, formFragmentFields }),
183
174
  export: editMode,
184
175
  };
185
176
  if (editMode) {
@@ -8,13 +8,14 @@ const runtimeTypeGuards_1 = require("../utils/runtimeTypeGuards");
8
8
  const generateAsyncSelect_1 = require("./asyncSelect/generateAsyncSelect");
9
9
  const options_1 = require("./formField/options");
10
10
  function generateFormField({ gqlIntrospection, baseOutputFilename, config, formConfig, gqlType, namePrefix, }) {
11
- if (config.type == "asyncSelect") {
11
+ if (config.type == "asyncSelect" || config.type == "asyncSelectFilter") {
12
12
  return (0, generateAsyncSelect_1.generateAsyncSelect)({ gqlIntrospection, baseOutputFilename, config, formConfig, gqlType, namePrefix });
13
13
  }
14
14
  const imports = [];
15
15
  const formProps = [];
16
- const { name, formattedMessageRootId, fieldLabel, introspectionFieldType, startAdornment, endAdornment, imports: optionsImports, } = (0, options_1.buildFormFieldOptions)({ config, formConfig, gqlType, gqlIntrospection });
16
+ const { name, formattedMessageRootId, fieldLabel, startAdornment, endAdornment, imports: optionsImports, } = (0, options_1.buildFormFieldOptions)({ config, formConfig, gqlType, gqlIntrospection });
17
17
  imports.push(...optionsImports);
18
+ let { introspectionFieldType } = (0, options_1.buildFormFieldOptions)({ config, formConfig, gqlType, gqlIntrospection });
18
19
  const nameWithPrefix = `${namePrefix ? `${namePrefix}.` : ``}${name}`;
19
20
  const rootGqlType = formConfig.gqlType;
20
21
  const dataRootName = rootGqlType[0].toLowerCase() + rootGqlType.substring(1); // TODO should probably be deteced via query
@@ -23,10 +24,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
23
24
  const endAdornmentWithLockIconProp = `endAdornment={<InputAdornment position="end"><Lock /></InputAdornment>}`;
24
25
  const readOnlyProps = `readOnly disabled`;
25
26
  const readOnlyPropsWithLock = `${readOnlyProps} ${endAdornmentWithLockIconProp}`;
26
- const defaultFormValuesConfig = {
27
- destructFromFormValues: config.virtual ? name : undefined,
28
- };
29
- let formValuesConfig = [defaultFormValuesConfig]; // FormFields should only contain one entry
27
+ let formValuesConfig = [{}]; // FormFields should only contain one entry
30
28
  const gqlDocuments = {};
31
29
  const hooksCode = "";
32
30
  let finalFormConfig;
@@ -43,7 +41,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
43
41
  }
44
42
  let code = "";
45
43
  let formValueToGqlInputCode = "";
46
- let formFragmentField = name;
44
+ let formFragmentFields = [name];
47
45
  if (config.type == "text") {
48
46
  const TextInputComponent = config.multiline ? "TextAreaField" : "TextField";
49
47
  code = `
@@ -63,7 +61,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
63
61
  : ""}
64
62
  ${validateCode}
65
63
  />`;
66
- if (!config.virtual && !required && !config.readOnly) {
64
+ if (!required && !config.readOnly) {
67
65
  formValueToGqlInputCode = `${name}: formValues.${name} ?? null,`;
68
66
  }
69
67
  }
@@ -91,17 +89,17 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
91
89
  if ((0, isFieldOptional_1.isFieldOptional)({ config, gqlIntrospection: gqlIntrospection, gqlType: gqlType })) {
92
90
  assignment = `formValues.${nameWithPrefix} ? ${assignment} : null`;
93
91
  }
94
- formValueToGqlInputCode = !config.virtual ? `${name}: ${assignment},` : ``;
92
+ formValueToGqlInputCode = `${name}: ${assignment},`;
95
93
  let initializationAssignment = `String(data.${dataRootName}.${nameWithPrefix})`;
96
94
  if (!required) {
97
95
  initializationAssignment = `data.${dataRootName}.${nameWithPrefix} ? ${initializationAssignment} : undefined`;
98
96
  }
99
97
  formValuesConfig = [
100
- Object.assign(Object.assign({}, defaultFormValuesConfig), {
98
+ {
101
99
  omitFromFragmentType: name,
102
100
  typeCode: `${name}${!required ? `?` : ``}: string;`,
103
101
  initializationCode: `${name}: ${initializationAssignment}`,
104
- }),
102
+ },
105
103
  ];
106
104
  }
107
105
  else if (config.type === "numberRange") {
@@ -126,7 +124,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
126
124
  : ""}
127
125
  ${validateCode}
128
126
  />`;
129
- formFragmentField = `${name} { min max }`;
127
+ formFragmentFields = [`${name}.min`, `${name}.max`];
130
128
  }
131
129
  else if (config.type == "boolean") {
132
130
  code = `<CheckboxField
@@ -143,9 +141,9 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
143
141
  ${validateCode}
144
142
  />`;
145
143
  formValuesConfig = [
146
- Object.assign(Object.assign({}, defaultFormValuesConfig), {
144
+ {
147
145
  defaultInitializationCode: `${name}: false`,
148
- }),
146
+ },
149
147
  ];
150
148
  }
151
149
  else if (config.type == "date") {
@@ -170,9 +168,16 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
170
168
  : ""}
171
169
  ${validateCode}
172
170
  />`;
171
+ if (!required && !config.readOnly) {
172
+ formValueToGqlInputCode = `${name}: formValues.${name} ?? null,`;
173
+ }
173
174
  }
174
175
  else if (config.type == "dateTime") {
175
- code = `<DateTimeField
176
+ imports.push({
177
+ name: "Future_DateTimePickerField as DateTimePickerField",
178
+ importPath: "@comet/admin",
179
+ });
180
+ code = `<DateTimePickerField
176
181
  ${required ? "required" : ""}
177
182
  ${config.readOnly ? readOnlyPropsWithLock : ""}
178
183
  variant="horizontal"
@@ -189,13 +194,13 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
189
194
  ${validateCode}
190
195
  />`;
191
196
  formValuesConfig = [
192
- Object.assign(Object.assign({}, defaultFormValuesConfig), {
197
+ {
193
198
  initializationCode: `${name}: data.${dataRootName}.${nameWithPrefix} ? new Date(data.${dataRootName}.${nameWithPrefix}) : undefined`,
194
199
  omitFromFragmentType: name,
195
200
  typeCode: `${name}${!required ? "?" : ""}: Date${!required ? " | null" : ""};`,
196
- }),
201
+ },
197
202
  ];
198
- if (!config.virtual && !config.readOnly) {
203
+ if (!config.readOnly) {
199
204
  formValueToGqlInputCode = required
200
205
  ? `${name}: formValues.${name}.toISOString(),`
201
206
  : `${name}: formValues.${name} ? formValues.${name}.toISOString() : null,`;
@@ -205,13 +210,13 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
205
210
  code = `<Field name="${nameWithPrefix}" isEqual={isEqual} label={${fieldLabel}} variant="horizontal" fullWidth>
206
211
  {createFinalFormBlock(rootBlocks.${String(config.name)})}
207
212
  </Field>`;
208
- formValueToGqlInputCode = !config.virtual ? `${name}: rootBlocks.${name}.state2Output(formValues.${nameWithPrefix}),` : ``;
213
+ formValueToGqlInputCode = `${name}: rootBlocks.${name}.state2Output(formValues.${nameWithPrefix}),`;
209
214
  formValuesConfig = [
210
- Object.assign(Object.assign({}, defaultFormValuesConfig), {
215
+ {
211
216
  typeCode: `${name}: BlockState<typeof rootBlocks.${name}>;`,
212
217
  initializationCode: `${name}: rootBlocks.${name}.input2State(data.${dataRootName}.${nameWithPrefix})`,
213
218
  defaultInitializationCode: `${name}: rootBlocks.${name}.defaultValues()`,
214
- }),
219
+ },
215
220
  ];
216
221
  }
217
222
  else if (config.type === "fileUpload") {
@@ -231,9 +236,14 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
231
236
  else {
232
237
  formValueToGqlInputCode = `${name}: formValues.${name} ? formValues.${name}.id : null,`;
233
238
  }
234
- formFragmentField = `${name} { ...${config.download ? "FinalFormFileUploadDownloadable" : "FinalFormFileUpload"} }`;
239
+ formFragmentFields = [`${name} { ...${config.download ? "FinalFormFileUploadDownloadable" : "FinalFormFileUpload"} }`];
235
240
  }
236
241
  else if (config.type == "staticSelect") {
242
+ const multiple = (introspectionFieldType === null || introspectionFieldType === void 0 ? void 0 : introspectionFieldType.kind) === "LIST";
243
+ if ((introspectionFieldType === null || introspectionFieldType === void 0 ? void 0 : introspectionFieldType.kind) === "LIST") {
244
+ introspectionFieldType =
245
+ introspectionFieldType.ofType.kind === "NON_NULL" ? introspectionFieldType.ofType.ofType : introspectionFieldType.ofType;
246
+ }
237
247
  const enumType = gqlIntrospection.__schema.types.find((t) => t.kind === "ENUM" && t.name === introspectionFieldType.name);
238
248
  if (!enumType)
239
249
  throw new Error(`Enum type ${introspectionFieldType.name} not found for field ${name}`);
@@ -248,7 +258,30 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
248
258
  return value;
249
259
  }
250
260
  });
251
- const renderAsRadio = config.inputType === "radio" || (required && values.length <= 5 && config.inputType !== "select");
261
+ let inputType = config.inputType;
262
+ if (!inputType) {
263
+ if (!required || multiple) {
264
+ // radio is not clearable, render always as select if not required
265
+ // radio doesn't support multiple
266
+ inputType = "select";
267
+ }
268
+ else {
269
+ // auto select/radio based on number of values
270
+ if (values.length <= 5) {
271
+ inputType = "radio";
272
+ }
273
+ else {
274
+ inputType = "select";
275
+ }
276
+ }
277
+ }
278
+ if (inputType === "radio" && multiple) {
279
+ throw new Error(`${name}: inputType=radio doesn't support multiple`);
280
+ }
281
+ if (inputType === "radio" && !required) {
282
+ throw new Error(`${name}: inputType=radio must be required as it doesn't support clearable`);
283
+ }
284
+ const renderAsRadio = inputType === "radio";
252
285
  if (renderAsRadio) {
253
286
  code = `<RadioGroupField
254
287
  ${required ? "required" : ""}
@@ -286,6 +319,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
286
319
  : ""}
287
320
  ${validateCode}
288
321
  ${config.readOnly ? readOnlyPropsWithLock : ""}
322
+ ${multiple ? "multiple" : ""}
289
323
  options={[${values.map((value) => {
290
324
  const id = `${formattedMessageRootId}.${name}.${value.value.charAt(0).toLowerCase() + value.value.slice(1)}`;
291
325
  return `{
@@ -303,7 +337,7 @@ function generateFormField({ gqlIntrospection, baseOutputFilename, config, formC
303
337
  code,
304
338
  hooksCode,
305
339
  formValueToGqlInputCode,
306
- formFragmentFields: [formFragmentField],
340
+ formFragmentFields,
307
341
  gqlDocuments,
308
342
  imports,
309
343
  formProps,
@@ -88,7 +88,7 @@ function generateFormLayout({ gqlIntrospection, baseOutputFilename, config, form
88
88
  namePrefix: name,
89
89
  });
90
90
  hooksCode += generatedFields.hooksCode;
91
- formFragmentFields.push(`${name} { ${generatedFields.formFragmentFields.join(" ")} }`);
91
+ formFragmentFields.push(...generatedFields.formFragmentFields.map((field) => `${name}.${field}`));
92
92
  for (const name in generatedFields.gqlDocuments) {
93
93
  gqlDocuments[name] = generatedFields.gqlDocuments[name];
94
94
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Helper function that generates a GraphQL fragment from form fragment fields (array of dot.separated.fields).
3
+ *
4
+ * - Fragments are supported as "foo { ...FragmentName }"
5
+ * - for FinalFormFileUpload and FinalFormFileUploadDownloadable the needed variable is added automatically
6
+ */
7
+ export declare function generateFragmentByFormFragmentFields({ formFragmentName, gqlType, formFragmentFields, }: {
8
+ formFragmentName: string;
9
+ gqlType: string;
10
+ formFragmentFields: string[];
11
+ }): string;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateFragmentByFormFragmentFields = generateFragmentByFormFragmentFields;
7
+ const object_path_1 = __importDefault(require("object-path"));
8
+ /**
9
+ * Helper function that generates a GraphQL fragment from form fragment fields (array of dot.separated.fields).
10
+ *
11
+ * - Fragments are supported as "foo { ...FragmentName }"
12
+ * - for FinalFormFileUpload and FinalFormFileUploadDownloadable the needed variable is added automatically
13
+ */
14
+ function generateFragmentByFormFragmentFields({ formFragmentName, gqlType, formFragmentFields, }) {
15
+ // 1. create tree out of dot separated fields
16
+ const fieldsObject = formFragmentFields.reduce((acc, field) => {
17
+ const fragmentMatch = field.match(/(.*)({.*?})/); // keep { ... } parts as they are, contains eg. fragments
18
+ if (fragmentMatch) {
19
+ object_path_1.default.set(acc, fragmentMatch[1].trim(), fragmentMatch[2]);
20
+ }
21
+ else {
22
+ object_path_1.default.set(acc, field, true);
23
+ }
24
+ return acc;
25
+ }, {});
26
+ // 2. create fragment string out of tree
27
+ const recursiveStringify = (obj) => {
28
+ let ret = "";
29
+ let prefixField = "";
30
+ for (const key in obj) {
31
+ const value = obj[key];
32
+ if (typeof value === "boolean") {
33
+ ret += `${prefixField}${key}`;
34
+ }
35
+ else if (typeof value === "string") {
36
+ ret += `${prefixField}${key} ${value}`;
37
+ }
38
+ else {
39
+ ret += `${prefixField}${key} { ${recursiveStringify(value)} }`;
40
+ }
41
+ prefixField = " ";
42
+ }
43
+ return ret;
44
+ };
45
+ let fragmentCode = `
46
+ fragment ${formFragmentName} on ${gqlType} {
47
+ ${recursiveStringify(fieldsObject)}
48
+ }
49
+ `;
50
+ // 3. add fragment instance variables when fragments are used
51
+ // this only works for hardcoded special cases, and the imports are also not handled here - if there would be more, this needs improvement
52
+ const fragments = {
53
+ "...FinalFormFileUpload": "${finalFormFileUploadFragment}",
54
+ "...FinalFormFileUploadDownloadable": "${finalFormFileUploadDownloadableFragment}",
55
+ };
56
+ for (const [fragmentName, fragmentVar] of Object.entries(fragments)) {
57
+ if (fragmentCode.match(`${fragmentName.replace(".", "\\.")}\\b`) && !fragmentCode.includes(fragmentVar)) {
58
+ fragmentCode += `\n${fragmentVar}`;
59
+ }
60
+ }
61
+ return fragmentCode;
62
+ }
@@ -308,6 +308,7 @@ function generateGrid({ exportName, baseOutputFilename, targetDirectory, gqlIntr
308
308
  let gridColumnType = undefined;
309
309
  let renderCell = undefined;
310
310
  let valueFormatter = undefined;
311
+ let valueGetter = name.includes(".") ? `(params, row) => row.${name.replace(/\./g, "?.")}` : undefined;
311
312
  let gridType;
312
313
  let filterOperators;
313
314
  if (column.type != "virtual" && column.filterOperators) {
@@ -321,9 +322,15 @@ function generateGrid({ exportName, baseOutputFilename, targetDirectory, gqlIntr
321
322
  }
322
323
  if (type == "dateTime") {
323
324
  gridColumnType = "...dataGridDateTimeColumn,";
325
+ valueGetter = name.includes(".")
326
+ ? `(params, row) => row.${name.replace(/\./g, "?.")} && new Date(row.${name.replace(/\./g, "?.")})`
327
+ : undefined;
324
328
  }
325
329
  else if (type == "date") {
326
330
  gridColumnType = "...dataGridDateColumn,";
331
+ valueGetter = name.includes(".")
332
+ ? `(params, row) => row.${name.replace(/\./g, "?.")} && new Date(row.${name.replace(/\./g, "?.")})`
333
+ : undefined;
327
334
  }
328
335
  else if (type == "number") {
329
336
  gridType = "number";
@@ -458,7 +465,7 @@ function generateGrid({ exportName, baseOutputFilename, targetDirectory, gqlIntr
458
465
  gridType,
459
466
  columnType: gridColumnType,
460
467
  renderCell,
461
- valueGetter: name.includes(".") ? `(params, row) => row.${name.replace(/\./g, "?.")}` : undefined,
468
+ valueGetter,
462
469
  filterOperators: filterOperators,
463
470
  valueFormatter,
464
471
  width: column.width,
@@ -15,7 +15,7 @@ const isFieldOptional = ({ config, gqlIntrospection, gqlType, }) => {
15
15
  throw new Error(`kind of ${gqlType} is not object, but should be.`); // this should not happen
16
16
  const fieldDef = schemaEntity.fields.find((field) => field.name === String(config.name));
17
17
  if (!fieldDef)
18
- throw new Error(`didn't find field ${String(config.name)} of ${gqlType} in introspected gql-schema.`);
18
+ return false;
19
19
  return fieldDef.type.kind !== "NON_NULL";
20
20
  };
21
21
  exports.isFieldOptional = isFieldOptional;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comet/admin-generator",
3
- "version": "9.0.0-canary-20250915134805",
3
+ "version": "9.0.0-canary-20251002064922",
4
4
  "description": "Comet Admin Generator CLI tool",
5
5
  "repository": {
6
6
  "directory": "packages/admin/admin-generator",
@@ -40,17 +40,17 @@
40
40
  "eslint": "^9.30.1",
41
41
  "final-form": "^4.20.10",
42
42
  "jest": "^29.7.0",
43
- "npm-run-all2": "^5.0.2",
43
+ "npm-run-all2": "^8.0.0",
44
44
  "prettier": "^3.6.2",
45
45
  "react": "^18.3.1",
46
46
  "react-intl": "^7.1.11",
47
47
  "rimraf": "^6.0.1",
48
48
  "ts-jest": "^29.4.0",
49
49
  "typescript": "5.8.3",
50
- "@comet/admin": "9.0.0-canary-20250915134805",
51
- "@comet/admin-icons": "9.0.0-canary-20250915134805",
52
- "@comet/cms-admin": "9.0.0-canary-20250915134805",
53
- "@comet/eslint-config": "9.0.0-canary-20250915134805"
50
+ "@comet/admin": "9.0.0-canary-20251002064922",
51
+ "@comet/admin-icons": "9.0.0-canary-20251002064922",
52
+ "@comet/cms-admin": "9.0.0-canary-20251002064922",
53
+ "@comet/eslint-config": "9.0.0-canary-20251002064922"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">=22.0.0"