@oneuptime/common 10.2.20 → 10.2.21

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.
@@ -1,8 +1,11 @@
1
1
  import ComponentLoader from "../ComponentLoader/ComponentLoader";
2
2
  import ErrorMessage from "../ErrorMessage/ErrorMessage";
3
3
  import BasicForm, { FormProps } from "../Forms/BasicForm";
4
+ import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
5
+ import { CustomElementProps } from "../Forms/Types/Field";
4
6
  import FormValues from "../Forms/Types/FormValues";
5
7
  import ComponentValuePickerModal from "./ComponentValuePickerModal";
8
+ import ModelFieldPicker from "./ModelFieldPicker";
6
9
  import { componentInputTypeToFormFieldType } from "./Utils";
7
10
  import VariableModal from "./VariableModal";
8
11
  import Dictionary from "../../../Types/Dictionary";
@@ -190,15 +193,51 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
190
193
  const isWorkflowSelect: boolean =
191
194
  arg.type === ComponentInputType.WorkflowSelect;
192
195
 
196
+ /*
197
+ * Database Select args (the "Select Fields" / "Listen on"
198
+ * trigger inputs) get a tree-style field picker backed by
199
+ * the model's schema, instead of a raw JSON textarea. We
200
+ * need a tableName on the component metadata to fetch the
201
+ * column list; without it, fall back to the JSON editor.
202
+ */
203
+ const useFieldPicker: boolean =
204
+ arg.type === ComponentInputType.Select &&
205
+ Boolean(component.metadata.tableName);
206
+
193
207
  const baseField: {
194
208
  fieldType: import("../Forms/Types/FormFieldSchemaType").default;
195
209
  dropdownOptions?: Array<DropdownOption> | undefined;
196
- } = componentInputTypeToFormFieldType(
197
- arg.type,
198
- component.arguments && component.arguments[arg.id]
199
- ? component.arguments[arg.id]
200
- : null,
201
- );
210
+ getCustomElement?: (
211
+ values: FormValues<JSONObject>,
212
+ customProps: CustomElementProps,
213
+ ) => ReactElement | undefined;
214
+ } = useFieldPicker
215
+ ? {
216
+ fieldType: FormFieldSchemaType.CustomComponent,
217
+ getCustomElement: (
218
+ _values: FormValues<JSONObject>,
219
+ customProps: CustomElementProps,
220
+ ): ReactElement => {
221
+ return (
222
+ <ModelFieldPicker
223
+ tableName={component.metadata.tableName as string}
224
+ initialValue={customProps.initialValue}
225
+ onChange={(value: string) => {
226
+ void customProps.onChange?.(value);
227
+ }}
228
+ placeholder={customProps.placeholder}
229
+ error={customProps.error}
230
+ tabIndex={customProps.tabIndex}
231
+ />
232
+ );
233
+ },
234
+ }
235
+ : componentInputTypeToFormFieldType(
236
+ arg.type,
237
+ component.arguments && component.arguments[arg.id]
238
+ ? component.arguments[arg.id]
239
+ : null,
240
+ );
202
241
 
203
242
  /*
204
243
  * For WorkflowSelect, inject the dynamically fetched list
@@ -208,13 +247,17 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
208
247
  baseField.dropdownOptions = workflowDropdownOptions;
209
248
  }
210
249
 
250
+ /*
251
+ * The "pick from component / variable" footer doesn't
252
+ * apply to the field picker (it edits a structured object,
253
+ * not a free-text expression) or to WorkflowSelect.
254
+ */
255
+ const showVariableFooter: boolean =
256
+ !isWorkflowSelect && !useFieldPicker;
257
+
211
258
  return {
212
259
  title: `${arg.name}`,
213
- /*
214
- * WorkflowSelect has no "pick from component/variable"
215
- * footer — it's a bound dropdown, not a free-text field.
216
- */
217
- footerElement: isWorkflowSelect ? undefined : (
260
+ footerElement: showVariableFooter ? (
218
261
  <div className="text-gray-500">
219
262
  <p className="text-sm">
220
263
  Pick this value from other{" "}
@@ -239,7 +282,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
239
282
  </button>
240
283
  </p>
241
284
  </div>
242
- ),
285
+ ) : undefined,
243
286
  description: `${
244
287
  arg.required ? "Required" : "Optional"
245
288
  }. ${arg.description}`,
@@ -3,7 +3,7 @@ import BasicForm from "../Forms/BasicForm";
3
3
  import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
4
4
  import FormValues from "../Forms/Types/FormValues";
5
5
  import ConfirmModal from "../Modal/ConfirmModal";
6
- import SideOver from "../SideOver/SideOver";
6
+ import Modal, { ModalWidth } from "../Modal/Modal";
7
7
  import ArgumentsForm from "./ArgumentsForm";
8
8
  import ComponentPortViewer from "./ComponentPortViewer";
9
9
  import ComponentReturnValueViewer from "./ComponentReturnValueViewer";
@@ -28,6 +28,36 @@ export interface ComponentProps {
28
28
  webhookSecretKey?: string | undefined;
29
29
  }
30
30
 
31
+ interface SectionCardProps {
32
+ icon: IconProp;
33
+ title: string;
34
+ children: ReactElement | Array<ReactElement>;
35
+ tone?: "default" | "info" | undefined;
36
+ }
37
+
38
+ const SectionCard: FunctionComponent<SectionCardProps> = (
39
+ props: SectionCardProps,
40
+ ): ReactElement => {
41
+ const isInfo: boolean = props.tone === "info";
42
+ const containerClass: string = isInfo
43
+ ? "rounded-lg border border-blue-100 bg-blue-50/40 p-4"
44
+ : "rounded-lg border border-gray-200 bg-white p-4";
45
+ const iconClass: string = isInfo ? "text-blue-500" : "text-gray-400";
46
+ const titleClass: string = isInfo
47
+ ? "text-[11px] font-semibold uppercase tracking-wider text-blue-700"
48
+ : "text-[11px] font-semibold uppercase tracking-wider text-gray-500";
49
+
50
+ return (
51
+ <div className={containerClass}>
52
+ <div className="flex items-center gap-1.5 mb-3">
53
+ <Icon icon={props.icon} className={`h-3.5 w-3.5 ${iconClass}`} />
54
+ <span className={titleClass}>{props.title}</span>
55
+ </div>
56
+ {props.children}
57
+ </div>
58
+ );
59
+ };
60
+
31
61
  const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
32
62
  props: ComponentProps,
33
63
  ): ReactElement => {
@@ -38,17 +68,113 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
38
68
  const [showDeleteConfirmation, setShowDeleteConfirmation] =
39
69
  useState<boolean>(false);
40
70
 
71
+ const argumentsSection: ReactElement = (
72
+ <SectionCard icon={IconProp.Settings} title="Configuration">
73
+ <ArgumentsForm
74
+ graphComponents={props.graphComponents}
75
+ workflowId={props.workflowId}
76
+ component={component}
77
+ onFormChange={(c: NodeDataProp) => {
78
+ setComponent({ ...c });
79
+ }}
80
+ onHasFormValidationErrors={(value: Dictionary<boolean>) => {
81
+ setHasFormValidationErrors({
82
+ ...hasFormValidationErrors,
83
+ ...value,
84
+ });
85
+ }}
86
+ />
87
+ </SectionCard>
88
+ );
89
+
90
+ const identitySection: ReactElement = (
91
+ <SectionCard
92
+ icon={IconProp.Label}
93
+ title={`${component.metadata.componentType} ID`}
94
+ >
95
+ <BasicForm
96
+ hideSubmitButton={true}
97
+ initialValues={{ id: component?.id }}
98
+ onChange={(values: FormValues<JSONObject>) => {
99
+ setComponent({ ...component, ...values });
100
+ }}
101
+ onFormValidationErrorChanged={(hasError: boolean) => {
102
+ setHasFormValidationErrors({
103
+ ...hasFormValidationErrors,
104
+ id: hasError,
105
+ });
106
+ }}
107
+ fields={[
108
+ {
109
+ title: "Identifier",
110
+ description: `Used to reference this ${component.metadata.componentType.toLowerCase()} from other components.`,
111
+ field: { id: true },
112
+ required: true,
113
+ fieldType: FormFieldSchemaType.Text,
114
+ },
115
+ ]}
116
+ />
117
+ </SectionCard>
118
+ );
119
+
120
+ const documentationSection: ReactElement | null = component.metadata
121
+ .documentationLink ? (
122
+ <SectionCard icon={IconProp.Book} title="Documentation" tone="info">
123
+ <DocumentationViewer
124
+ documentationLink={component.metadata.documentationLink}
125
+ workflowId={props.workflowId}
126
+ webhookSecretKey={props.webhookSecretKey}
127
+ />
128
+ </SectionCard>
129
+ ) : null;
130
+
131
+ const connectionsSection: ReactElement = (
132
+ <SectionCard icon={IconProp.Link} title="Connections">
133
+ <>
134
+ <ComponentPortViewer
135
+ name="In Ports"
136
+ description="Input connections for this component"
137
+ ports={component.metadata.inPorts}
138
+ />
139
+ <ComponentPortViewer
140
+ name="Out Ports"
141
+ description="Output connections from this component"
142
+ ports={component.metadata.outPorts}
143
+ />
144
+ </>
145
+ </SectionCard>
146
+ );
147
+
148
+ const outputSection: ReactElement = (
149
+ <SectionCard icon={IconProp.ArrowCircleRight} title="Output">
150
+ <ComponentReturnValueViewer
151
+ name="Return Values"
152
+ description="Values this component produces for downstream use"
153
+ returnValues={component.metadata.returnValues}
154
+ />
155
+ </SectionCard>
156
+ );
157
+
158
+ const hasErrors: boolean = Object.values(hasFormValidationErrors).some(
159
+ (v: boolean) => {
160
+ return v;
161
+ },
162
+ );
163
+
41
164
  return (
42
- <SideOver
165
+ <Modal
43
166
  title={props.title}
44
167
  description={props.description}
45
168
  onClose={props.onClose}
46
169
  onSubmit={() => {
47
170
  return component && props.onSave(component);
48
171
  }}
172
+ submitButtonText="Save"
173
+ modalWidth={ModalWidth.Large}
174
+ disableSubmitButton={hasErrors}
49
175
  leftFooterElement={
50
176
  <Button
51
- title={`Delete`}
177
+ title="Delete"
52
178
  icon={IconProp.Trash}
53
179
  buttonStyle={ButtonStyleType.DANGER_OUTLINE}
54
180
  onClick={() => {
@@ -65,7 +191,7 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
65
191
  onClose={() => {
66
192
  setShowDeleteConfirmation(false);
67
193
  }}
68
- submitButtonText={"Delete"}
194
+ submitButtonText="Delete"
69
195
  onSubmit={() => {
70
196
  props.onDelete(component);
71
197
  setShowDeleteConfirmation(false);
@@ -75,260 +201,22 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
75
201
  />
76
202
  )}
77
203
 
78
- {/* Component ID Section */}
79
- <div
80
- style={{
81
- backgroundColor: "#f8fafc",
82
- borderRadius: "10px",
83
- border: "1px solid #e2e8f0",
84
- padding: "1rem",
85
- marginTop: "0.75rem",
86
- marginBottom: "1rem",
87
- }}
88
- >
89
- <div
90
- style={{
91
- display: "flex",
92
- alignItems: "center",
93
- gap: "0.5rem",
94
- marginBottom: "0.5rem",
95
- }}
96
- >
97
- <Icon
98
- icon={IconProp.Label}
99
- style={{
100
- color: "#64748b",
101
- width: "0.875rem",
102
- height: "0.875rem",
103
- }}
104
- />
105
- <span
106
- style={{
107
- fontSize: "0.8125rem",
108
- fontWeight: 600,
109
- color: "#334155",
110
- }}
111
- >
112
- Identity
113
- </span>
114
- </div>
115
- <BasicForm
116
- hideSubmitButton={true}
117
- initialValues={{
118
- id: component?.id,
119
- }}
120
- onChange={(values: FormValues<JSONObject>) => {
121
- setComponent({ ...component, ...values });
122
- }}
123
- onFormValidationErrorChanged={(hasError: boolean) => {
124
- setHasFormValidationErrors({
125
- ...hasFormValidationErrors,
126
- id: hasError,
127
- });
128
- }}
129
- fields={[
130
- {
131
- title: `${component.metadata.componentType} ID`,
132
- description: `Unique identifier used to reference this ${component.metadata.componentType.toLowerCase()} from other components.`,
133
- field: {
134
- id: true,
135
- },
136
- required: true,
137
- fieldType: FormFieldSchemaType.Text,
138
- },
139
- ]}
140
- />
141
- </div>
142
-
143
- {/* Documentation Section */}
144
- {component.metadata.documentationLink && (
145
- <div
146
- style={{
147
- backgroundColor: "#eff6ff",
148
- borderRadius: "10px",
149
- border: "1px solid #bfdbfe",
150
- padding: "1rem",
151
- marginBottom: "1rem",
152
- }}
153
- >
154
- <div
155
- style={{
156
- display: "flex",
157
- alignItems: "center",
158
- gap: "0.5rem",
159
- marginBottom: "0.5rem",
160
- }}
161
- >
162
- <Icon
163
- icon={IconProp.Book}
164
- style={{
165
- color: "#3b82f6",
166
- width: "0.875rem",
167
- height: "0.875rem",
168
- }}
169
- />
170
- <span
171
- style={{
172
- fontSize: "0.8125rem",
173
- fontWeight: 600,
174
- color: "#1e40af",
175
- }}
176
- >
177
- Documentation
178
- </span>
179
- </div>
180
- <DocumentationViewer
181
- documentationLink={component.metadata.documentationLink}
182
- workflowId={props.workflowId}
183
- webhookSecretKey={props.webhookSecretKey}
184
- />
185
- </div>
186
- )}
187
-
188
- {/* Arguments Section */}
189
- <div
190
- style={{
191
- backgroundColor: "#ffffff",
192
- borderRadius: "10px",
193
- border: "1px solid #e2e8f0",
194
- padding: "1rem",
195
- marginBottom: "1rem",
196
- }}
197
- >
198
- <div
199
- style={{
200
- display: "flex",
201
- alignItems: "center",
202
- gap: "0.5rem",
203
- marginBottom: "0.75rem",
204
- }}
205
- >
206
- <Icon
207
- icon={IconProp.Settings}
208
- style={{
209
- color: "#64748b",
210
- width: "0.875rem",
211
- height: "0.875rem",
212
- }}
213
- />
214
- <span
215
- style={{
216
- fontSize: "0.8125rem",
217
- fontWeight: 600,
218
- color: "#334155",
219
- }}
220
- >
221
- Configuration
222
- </span>
223
- </div>
224
- <ArgumentsForm
225
- graphComponents={props.graphComponents}
226
- workflowId={props.workflowId}
227
- component={component}
228
- onFormChange={(component: NodeDataProp) => {
229
- setComponent({ ...component });
230
- }}
231
- onHasFormValidationErrors={(value: Dictionary<boolean>) => {
232
- setHasFormValidationErrors({
233
- ...hasFormValidationErrors,
234
- ...value,
235
- });
236
- }}
237
- />
238
- </div>
239
-
240
- {/* Ports Section */}
241
- <div
242
- style={{
243
- backgroundColor: "#ffffff",
244
- borderRadius: "10px",
245
- border: "1px solid #e2e8f0",
246
- padding: "1rem",
247
- marginBottom: "1rem",
248
- }}
249
- >
250
- <div
251
- style={{
252
- display: "flex",
253
- alignItems: "center",
254
- gap: "0.5rem",
255
- marginBottom: "0.25rem",
256
- }}
257
- >
258
- <Icon
259
- icon={IconProp.Link}
260
- style={{
261
- color: "#64748b",
262
- width: "0.875rem",
263
- height: "0.875rem",
264
- }}
265
- />
266
- <span
267
- style={{
268
- fontSize: "0.8125rem",
269
- fontWeight: 600,
270
- color: "#334155",
271
- }}
272
- >
273
- Connections
274
- </span>
275
- </div>
276
- <ComponentPortViewer
277
- name="In Ports"
278
- description="Input connections for this component"
279
- ports={component.metadata.inPorts}
280
- />
281
- <ComponentPortViewer
282
- name="Out Ports"
283
- description="Output connections from this component"
284
- ports={component.metadata.outPorts}
285
- />
286
- </div>
287
-
288
- {/* Return Values Section */}
289
- <div
290
- style={{
291
- backgroundColor: "#ffffff",
292
- borderRadius: "10px",
293
- border: "1px solid #e2e8f0",
294
- padding: "1rem",
295
- marginBottom: "1rem",
296
- }}
297
- >
298
- <div
299
- style={{
300
- display: "flex",
301
- alignItems: "center",
302
- gap: "0.5rem",
303
- marginBottom: "0.25rem",
304
- }}
305
- >
306
- <Icon
307
- icon={IconProp.ArrowCircleRight}
308
- style={{
309
- color: "#64748b",
310
- width: "0.875rem",
311
- height: "0.875rem",
312
- }}
313
- />
314
- <span
315
- style={{
316
- fontSize: "0.8125rem",
317
- fontWeight: 600,
318
- color: "#334155",
319
- }}
320
- >
321
- Output
322
- </span>
204
+ {/*
205
+ * Two-column layout: arguments take the main column (2/3 width on
206
+ * md+), metadata sits in a narrower sidebar. Collapses to one
207
+ * column below md.
208
+ */}
209
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
210
+ <div className="md:col-span-2 space-y-4">{argumentsSection}</div>
211
+ <div className="md:col-span-1 space-y-4">
212
+ {identitySection}
213
+ {documentationSection}
214
+ {connectionsSection}
215
+ {outputSection}
323
216
  </div>
324
- <ComponentReturnValueViewer
325
- name="Return Values"
326
- description="Values this component produces for downstream use"
327
- returnValues={component.metadata.returnValues}
328
- />
329
217
  </div>
330
218
  </>
331
- </SideOver>
219
+ </Modal>
332
220
  );
333
221
  };
334
222