@integry/sdk 4.6.39 → 4.6.41

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.
@@ -0,0 +1,337 @@
1
+ import { html, Component } from 'htm/preact';
2
+ import { connect } from 'unistore/preact';
3
+
4
+ import { Loader } from '@/components/Loader';
5
+ import { IntegryAPI } from '@/modules/api';
6
+ import { Radio } from '@/components/RadioGroup/Radio';
7
+ import { Label } from '@/components/Label';
8
+ import { Hint } from '@/components/Tooltip';
9
+ import { StoreType } from '@/types/store';
10
+ import { actionFunctions } from '@/store';
11
+ import isBrowser from '@/utils/isBrowser';
12
+ import AppContext from '@/contexts/AppContext';
13
+
14
+ import { openPopupWindow } from '@/utils/popup';
15
+ import { AuthData, isAuthMessage } from '@/utils/isAuthMessage';
16
+
17
+ import styles from './styles.module.scss';
18
+
19
+ type Authorization = {
20
+ id: number;
21
+ display_name: string;
22
+ };
23
+
24
+ interface AuthSelectorPropsType extends StoreType {
25
+ authorizations: Authorization[];
26
+ selectedAuthId?: number | null;
27
+ onAuthSelected(authId: number | null): void;
28
+ apiHandler: IntegryAPI;
29
+ loginUrl?: string;
30
+ appName?: string;
31
+ onAuthCreated?(auth: Authorization): void;
32
+ onAuthDeleted?(authId: number): void;
33
+ }
34
+
35
+ interface AuthSelectorStateType {
36
+ isLoading: boolean;
37
+ fetched: boolean;
38
+ willDeleteAuthWithIndex: number | null;
39
+ isDeleting: boolean;
40
+ }
41
+
42
+ const Confirmation = (props: { callback(shouldDelete: boolean): void }) => {
43
+ const { callback } = props;
44
+ return html`
45
+ <div class=${styles.confirmation}>
46
+ <div class=${styles.confirmationText}>Are you sure?</div>
47
+ <button onclick=${() => callback(false)}>No</button>
48
+ <button onclick=${() => callback(true)}>Yes</button>
49
+ </div>
50
+ `;
51
+ };
52
+
53
+ const AuthorizationRow = (props: {
54
+ id: string;
55
+ value: string;
56
+ isChecked?: boolean;
57
+ isLoading: boolean;
58
+ willDelete?: boolean;
59
+ isDeleting?: boolean;
60
+ error?: string;
61
+ status: 'VERIFYING' | 'CONNECTED' | 'ERROR' | 'IDLE' | null;
62
+ handleChange?: (id: string) => void;
63
+ handleReverify?: () => void;
64
+ handleDelete?: (shouldDelete: boolean) => void;
65
+ handleWillDelete?: () => void;
66
+ isReadOnly?: boolean;
67
+ forcedLoading?: boolean;
68
+ }) => {
69
+ const {
70
+ id,
71
+ value,
72
+ isChecked,
73
+ status = 'IDLE',
74
+ isDeleting = false,
75
+ willDelete = false,
76
+ handleChange,
77
+ handleReverify,
78
+ handleDelete = () => null,
79
+ handleWillDelete = () => null,
80
+ error,
81
+ isReadOnly,
82
+ } = props;
83
+ return html`
84
+ <${Hint} dismissOnClick=${false} position="top" deltaY=${0}>
85
+ <div
86
+ class="${styles.checkboxRow} ${isChecked
87
+ ? styles.isCheckBoxSelected
88
+ : ''}"
89
+ >
90
+ ${isReadOnly &&
91
+ html`<span
92
+ class=${styles.readonlyHint}
93
+ data-hint=${isReadOnly ? `Cannot modify user selection` : ''}
94
+ ></span>`}
95
+ <div class=${`${styles.radio} ${isReadOnly ? styles.readonly : ''}`}>
96
+ <${Radio}
97
+ id=${id}
98
+ value=${value}
99
+ isChecked=${isChecked}
100
+ onChange=${handleChange}
101
+ isDisabled=${false}
102
+ className="authRadio"
103
+ />
104
+ </div>
105
+
106
+ ${willDelete &&
107
+ !isDeleting &&
108
+ html` <${Confirmation} callback=${handleDelete} /> `}
109
+ ${!(status === 'VERIFYING' || isDeleting || willDelete) &&
110
+ !isReadOnly &&
111
+ html`
112
+ <div class=${styles.deleteIcon} onclick=${handleWillDelete}>
113
+ <svg
114
+ width="15"
115
+ height="15"
116
+ viewBox="0 0 15 15"
117
+ fill="none"
118
+ xmlns="http://www.w3.org/2000/svg"
119
+ >
120
+ <path d="M1.5 4.1999H13.65" stroke="#F05C42" />
121
+ <path
122
+ d="M11.6254 5.5498V6.2248L10.9504 13.6498H4.20039L3.52539 6.2248V5.5498"
123
+ stroke="#F05C42"
124
+ stroke-linejoin="round"
125
+ />
126
+ <path
127
+ d="M5.55078 3.525V2.175C5.55078 1.80221 5.85299 1.5 6.22578 1.5H8.92578C9.29857 1.5 9.60078 1.80221 9.60078 2.175V3.525"
128
+ stroke="#F05C42"
129
+ />
130
+ </svg>
131
+ </div>
132
+ `}
133
+ ${isDeleting &&
134
+ html`
135
+ <div class=${styles.deletingIcon}>
136
+ <${Loader} size="small" />
137
+ </div>
138
+ `}
139
+ </div>
140
+ <//>
141
+ `;
142
+ };
143
+
144
+ class AuthSelectorV2 extends Component<
145
+ AuthSelectorPropsType,
146
+ AuthSelectorStateType
147
+ > {
148
+ static contextType = AppContext;
149
+
150
+ constructor(props: AuthSelectorPropsType) {
151
+ super(props);
152
+ this.state = {
153
+ isLoading: false,
154
+ fetched: false,
155
+ willDeleteAuthWithIndex: null,
156
+ isDeleting: false,
157
+ };
158
+ }
159
+
160
+ componentDidMount() {
161
+ if (isBrowser())
162
+ window.addEventListener('message', this.onAuthResponseReceived);
163
+
164
+ let { selectedAuthId } = this.props;
165
+ const { authorizations } = this.props;
166
+
167
+ if (!selectedAuthId && authorizations.length > 0) {
168
+ selectedAuthId = authorizations[0].id;
169
+ }
170
+ let authIndex = authorizations.findIndex((el) => el.id === selectedAuthId);
171
+ if (authorizations.length > 0 && authIndex === -1) {
172
+ authIndex = 0;
173
+ }
174
+ }
175
+
176
+ componentDidUpdate(prevProps: AuthSelectorPropsType) {
177
+ const { selectedAuthId, authorizations } = this.props;
178
+
179
+ const authIndex = authorizations.findIndex(
180
+ (el) => el.id === selectedAuthId,
181
+ );
182
+ }
183
+
184
+ componentWillUnmount() {
185
+ if (isBrowser())
186
+ window.removeEventListener('message', this.onAuthResponseReceived);
187
+ }
188
+
189
+ private willDelete = (authIndex: number) => {
190
+ this.setState({
191
+ willDeleteAuthWithIndex: authIndex,
192
+ });
193
+ };
194
+
195
+ private deleteAuth = (authIndex: number) => {
196
+ const { authorizations } = this.props;
197
+ const auth = authorizations[authIndex];
198
+ this.setState({ isDeleting: true });
199
+
200
+ this.props.apiHandler
201
+ .deleteAuth(auth.id)
202
+ .then((res) => {
203
+ if (res?.status === 200) {
204
+ this.context.eventEmitter.emit('did-remove-authorization', {
205
+ authorizationId: Number(auth.id),
206
+ appId: this.props.genericData.app_id || '',
207
+ });
208
+ if (this.props.onAuthDeleted) {
209
+ this.props.onAuthDeleted(auth.id);
210
+ }
211
+ }
212
+ this.setState({ isDeleting: false });
213
+ })
214
+ .catch((err) => {
215
+ console.error(err);
216
+ })
217
+ .finally(() =>
218
+ this.setState({ willDeleteAuthWithIndex: null, isDeleting: false }),
219
+ );
220
+ };
221
+
222
+ private onAuthResponseReceived = (messageEvent: MessageEvent<AuthData>) => {
223
+ if (isAuthMessage(messageEvent) && messageEvent.data.activity_name) {
224
+ if (this.props.onAuthCreated) {
225
+ this.props.onAuthCreated({
226
+ id: Number(messageEvent.data.authorization_id),
227
+ display_name: messageEvent.data.user_identity,
228
+ });
229
+ }
230
+ this.context.eventEmitter.emit('did-add-authorization', {
231
+ identity: messageEvent.data.user_identity,
232
+ authorizationId: Number(messageEvent.data.authorization_id),
233
+ flowId: this.props.genericData.templateId,
234
+ alreadyExists: messageEvent.data.already_exists,
235
+ externalId: messageEvent.data.external_id,
236
+ });
237
+ }
238
+ };
239
+
240
+ public openAuthWindow = () => {
241
+ if (this.props.loginUrl) {
242
+ const { loginUrl } = this.props;
243
+ openPopupWindow(loginUrl, 'Auth Window', window, 800, 600);
244
+ }
245
+ };
246
+
247
+ render() {
248
+ const { authorizations, appName } = this.props;
249
+
250
+ return html`
251
+ <div
252
+ class="${styles.authSelectorWrapV2} integry-container__auth-selector"
253
+ >
254
+ <div class="${styles.authSelectorHeader}">
255
+ <${Label}
256
+ title="Connect account"
257
+ description=${`Select your ${appName || ''} account`}
258
+ isRequired=${true}
259
+ />
260
+ </div>
261
+ <div class=${styles.rows}>
262
+ ${authorizations.map(
263
+ (el, index) => html`<${AuthorizationRow}
264
+ id=${el.id}
265
+ value=${el.display_name}
266
+ status=${`CONNECTED`}
267
+ isChecked=${el.id === this.props.selectedAuthId}
268
+ willDelete=${this.state.willDeleteAuthWithIndex === index}
269
+ handleChange=${(id: string) => {
270
+ this.props.onAuthSelected(Number(id));
271
+ }}
272
+ handleDelete=${(shouldDelete: boolean) =>
273
+ shouldDelete
274
+ ? this.deleteAuth(index)
275
+ : this.setState({ willDeleteAuthWithIndex: null })}
276
+ handleWillDelete=${() => this.willDelete(index)}
277
+ isDeleting=${this.state.isDeleting}
278
+ />`,
279
+ )}
280
+ </div>
281
+ ${true &&
282
+ html`
283
+ <div>
284
+ <${Hint}
285
+ dismissOnClick=${false}
286
+ position="bottom-right"
287
+ deltaY=${0}
288
+ >
289
+ ${true &&
290
+ html`<div
291
+ data-hint=${this.props.genericData.isPreviewMode &&
292
+ 'New accounts cannot be added in preview mode'}
293
+ class=${styles.addAccountCTA}
294
+ onClick=${() => {
295
+ if (
296
+ !this.props.genericData.isPreviewMode &&
297
+ !(
298
+ this.state.isLoading ||
299
+ this.props.genericData.isPreviewMode
300
+ )
301
+ ) {
302
+ this.openAuthWindow();
303
+ }
304
+ }}
305
+ >
306
+ <svg
307
+ xmlns="http://www.w3.org/2000/svg"
308
+ width="16"
309
+ height="16"
310
+ viewBox="0 0 16 16"
311
+ fill="none"
312
+ >
313
+ <path
314
+ d="M7.94453 7.94417V8.44417H8.44453V7.94417H7.94453ZM7.94453 8.44417H8.44453V7.94417H7.94453V8.44417ZM8.44453 8.44417V7.94417H7.94453V8.44417H8.44453ZM8.44453 7.94417H7.94453V8.44417H8.44453V7.94417ZM8.19453 3.64417C8.05646 3.64417 7.94453 3.53224 7.94453 3.39417H8.94453C8.94453 2.97995 8.60874 2.64417 8.19453 2.64417V3.64417ZM8.44453 3.39417C8.44453 3.53224 8.3326 3.64417 8.19453 3.64417V2.64417C7.78032 2.64417 7.44453 2.97995 7.44453 3.39417H8.44453ZM8.44453 7.94417V3.39417H7.44453V7.94417H8.44453ZM3.39453 8.44417H7.94453V7.44417H3.39453V8.44417ZM3.64453 8.19417C3.64453 8.33224 3.5326 8.44417 3.39453 8.44417V7.44417C2.98032 7.44417 2.64453 7.77995 2.64453 8.19417H3.64453ZM3.39453 7.94417C3.5326 7.94417 3.64453 8.05609 3.64453 8.19417H2.64453C2.64453 8.60838 2.98032 8.94417 3.39453 8.94417V7.94417ZM7.94453 7.94417H3.39453V8.94417H7.94453V7.94417ZM8.44453 12.9942V8.44417H7.44453V12.9942H8.44453ZM8.19453 12.7442C8.3326 12.7442 8.44453 12.8561 8.44453 12.9942H7.44453C7.44453 13.4084 7.78032 13.7442 8.19453 13.7442V12.7442ZM7.94453 12.9942C7.94453 12.8561 8.05646 12.7442 8.19453 12.7442V13.7442C8.60874 13.7442 8.94453 13.4084 8.94453 12.9942H7.94453ZM7.94453 8.44417V12.9942H8.94453V8.44417H7.94453ZM12.9945 7.94417H8.44453V8.94417H12.9945V7.94417ZM12.7445 8.19417C12.7445 8.05609 12.8565 7.94417 12.9945 7.94417V8.94417C13.4087 8.94417 13.7445 8.60838 13.7445 8.19417H12.7445ZM12.9945 8.44417C12.8565 8.44417 12.7445 8.33224 12.7445 8.19417H13.7445C13.7445 7.77995 13.4087 7.44417 12.9945 7.44417V8.44417ZM8.44453 8.44417H12.9945V7.44417H8.44453V8.44417ZM7.94453 3.39417V7.94417H8.94453V3.39417H7.94453Z"
315
+ fill="#333333"
316
+ />
317
+ </svg>
318
+
319
+ <span>${`Add account`}</span>
320
+ </div>`}
321
+ <//>
322
+ </div>
323
+ `}
324
+ </div>
325
+ `;
326
+ }
327
+ }
328
+
329
+ export default connect<
330
+ AuthSelectorPropsType,
331
+ AuthSelectorStateType,
332
+ StoreType,
333
+ unknown
334
+ >(
335
+ ['stepMapping', 'genericData'],
336
+ actionFunctions,
337
+ )(AuthSelectorV2);
@@ -0,0 +1,232 @@
1
+ .authSelectorWrapV2 {
2
+ .label {
3
+ font-weight: 800;
4
+ font-size: 14px;
5
+ color: #333333;
6
+ line-height: 20px;
7
+ margin-bottom: 5px;
8
+
9
+ span {
10
+ color: #ff4d34;
11
+ margin-left: 2px;
12
+ font-size: 16px;
13
+ }
14
+ }
15
+
16
+ .confirmation {
17
+ display: flex;
18
+ align-items: center;
19
+ .confirmationText {
20
+ font-size: 12px;
21
+ }
22
+ // width: 35%;
23
+ button {
24
+ cursor: pointer;
25
+ -webkit-appearance: button;
26
+ font-weight: 500;
27
+ font-size: 12px;
28
+ line-height: 15px;
29
+ /* identical to box height */
30
+ padding: 4px 12px;
31
+
32
+ text-align: center;
33
+ border-radius: 12px;
34
+ border: 0;
35
+ outline: none;
36
+
37
+ color: #7e7a8f;
38
+ background-color: #f1f1f1;
39
+
40
+ &:nth-child(2) {
41
+ color: #7e7a8f;
42
+ margin-left: 10px;
43
+ margin-right: 10px;
44
+ }
45
+ &:nth-child(3) {
46
+ color: #ffffff;
47
+ background-color: #f05c42;
48
+ }
49
+ }
50
+ }
51
+ .rows {
52
+ margin-top: 10px;
53
+ margin-bottom: 20px;
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 8px;
57
+ }
58
+
59
+ .error {
60
+ color: #f05c42;
61
+ button {
62
+ color: #f05c42;
63
+ border-color: #f05c42;
64
+ }
65
+ }
66
+
67
+ .errorMessage {
68
+ margin-top: 4px;
69
+ font-size: 12px;
70
+ line-height: 15px;
71
+ letter-spacing: -0.197647px;
72
+ color: #f05c42;
73
+ }
74
+
75
+ .description {
76
+ font-size: 14px;
77
+ line-height: 20px;
78
+ color: #8c8c8c;
79
+ margin-bottom: 10px;
80
+ }
81
+
82
+ .checkboxRow {
83
+ display: flex;
84
+ align-items: center;
85
+ color: #333;
86
+ font-size: 14px;
87
+ border-radius: 4px;
88
+ border: 1px solid var(--Gray-5, #e0e0e0);
89
+ background: #fff;
90
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.08);
91
+ padding: 0 16px;
92
+ max-height: 48px;
93
+ min-height: 48px;
94
+
95
+ .radio {
96
+ // width: 45%;
97
+ flex: 1;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+
102
+ .authRadio {
103
+ margin: 0;
104
+ }
105
+
106
+ &.readonly {
107
+ // width: 70%;
108
+ }
109
+ }
110
+ .deleteIcon {
111
+ width: 20px;
112
+ // padding-left: 8px;
113
+ // padding-right: 8px;
114
+ display: flex;
115
+ align-items: center;
116
+ cursor: pointer;
117
+ svg {
118
+ cursor: pointer;
119
+ visibility: hidden;
120
+ user-select: none;
121
+ }
122
+ }
123
+ .statusWrap {
124
+ display: flex;
125
+ align-items: center;
126
+ font-size: 12px;
127
+ width: 20px;
128
+ &.readonly {
129
+ // width: 30%;
130
+ width: 20px;
131
+ }
132
+ .status {
133
+ position: relative;
134
+ width: 200px;
135
+ div {
136
+ display: flex;
137
+ align-items: center;
138
+ }
139
+ svg {
140
+ margin-left: 8px;
141
+ }
142
+ .statusRed,
143
+ .statusOrange,
144
+ .statusGreen {
145
+ width: 5px;
146
+ height: 5px;
147
+ border-radius: 9999px;
148
+ margin: 0 8px;
149
+ }
150
+ .statusRed {
151
+ background: #e26154;
152
+ }
153
+ .statusOrange {
154
+ background: #fcc700;
155
+ }
156
+
157
+ .statusGreen {
158
+ background: #2ecc71;
159
+ }
160
+
161
+ span {
162
+ margin-left: 10px;
163
+ }
164
+ .resend {
165
+ cursor: pointer;
166
+ }
167
+ }
168
+ }
169
+ &:hover {
170
+ .deleteIcon svg {
171
+ visibility: visible;
172
+ user-select: auto;
173
+ }
174
+ }
175
+ .readonlyHint {
176
+ position: absolute;
177
+ width: 40%;
178
+ height: -webkit-fill-available;
179
+ z-index: 99;
180
+ margin: 10px 0px;
181
+ }
182
+
183
+ &:hover {
184
+ border: 1px solid var(--theme-accent-blue-1, #4250f0);
185
+ box-shadow: 0px 0px 8px 0px rgba(66, 80, 240, 0.16);
186
+ }
187
+ [class^='styles-module_text_'] {
188
+ font-size: 12px;
189
+ }
190
+ }
191
+
192
+ .isCheckBoxSelected {
193
+ border: 1px solid var(--theme-accent-blue-1, #4250f0);
194
+ box-shadow: 0px 0px 8px 0px rgba(66, 80, 240, 0.16);
195
+ }
196
+
197
+ .addAccountCTA {
198
+ border-radius: 4px;
199
+ border: 1px solid var(--Gray-5, #e0e0e0);
200
+ background: #fff;
201
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.08);
202
+ color: var(--black-and-grey-title-and-desc, #333);
203
+ font-feature-settings: 'clig' off, 'liga' off;
204
+ font-family: Inter;
205
+ font-size: 12px;
206
+ font-style: normal;
207
+ font-weight: 400;
208
+ line-height: normal;
209
+ padding: 16px;
210
+ display: flex;
211
+ flex-direction: row;
212
+ align-items: center;
213
+ justify-content: flex-start;
214
+ gap: 8px;
215
+ cursor: pointer;
216
+
217
+ &:hover {
218
+ border: 1px solid var(--theme-accent-blue-1, #4250f0);
219
+ box-shadow: 0px 0px 8px 0px rgba(66, 80, 240, 0.16);
220
+ }
221
+ }
222
+ .authSelectorHeader {
223
+ label {
224
+ div:first-child {
225
+ font-size: 13px;
226
+ }
227
+ div:nth-child(2) {
228
+ font-size: 11px;
229
+ }
230
+ }
231
+ }
232
+ }
@@ -4,25 +4,16 @@ import { connect } from 'unistore/preact';
4
4
  import { useContext } from 'preact/hooks';
5
5
  // import DOMPurify from 'dompurify';
6
6
  import { ListBox } from '@/components/MultipurposeField/Dropdown';
7
- import AppPageLoader from '@/features/containers/AppFlowContainer/AppFlowWrap/app-page-loader';
8
7
  import { actionFunctions } from '@/store';
9
- import MappingUI from '@/features/common/MappingUI';
10
8
  import { Button } from '@/components/Button';
11
9
  import { StoreType } from '@/types/store';
12
- import { DateInput, Input, PasswordInput } from '@/components/Input';
13
- import HTMLContent from '@/components/HTMLContent';
14
- import TextContent from '@/components/TextContent';
15
10
  import { MultipurposeField } from '@/components/MultipurposeField';
16
11
  import DynamicTypedField from '@/features/common/DynamicTypedField';
17
- import SectionField from '@/features/common/SectionField';
18
- import { TimeInput } from '@/components/TimeInput';
19
- import NewMappingUI from '@/features/common/NewMappingUI';
20
- import { areParentValuesValid, getFieldLabelTags } from '@/utils/stepUtils';
21
12
  import ConfigureFieldWrapper from '@/components/ConfigureFieldWrapper';
22
- import { TextArea } from '@/components/TextArea';
23
13
  import { CheckboxGroup } from '@/components/CheckboxGroup';
24
14
  import AppContext from '@/contexts/AppContext';
25
- import { ObjectField } from '@/components/form';
15
+ import { ObjectField } from '@/components/form/ObjectField';
16
+ import { LargeLoader } from '@/components/LargeLoader';
26
17
  import {
27
18
  JSONToActivityOutputData,
28
19
  JSONToDynamicFieldData,
@@ -39,6 +30,7 @@ interface FunctionFormPropsType extends StoreType {
39
30
  autoMapVars: boolean;
40
31
  onClose: (response: any) => void;
41
32
  apiHandler: any;
33
+ customSaveCallback?: (response: any) => void; // Optional callback: Helps the implementor to implement their own save button
42
34
  }
43
35
 
44
36
  interface ActionFormStateType {
@@ -159,7 +151,7 @@ class FunctionForm extends Component<
159
151
  return arr;
160
152
  };
161
153
 
162
- private onSubmit = () => {
154
+ private onSubmit = (event: any, returnResponse = false) => {
163
155
  this.setState({ isSubmitting: true });
164
156
  const { functionName } = this.props;
165
157
  const { functionDetails } = this.state;
@@ -237,7 +229,12 @@ class FunctionForm extends Component<
237
229
  }
238
230
  });
239
231
 
232
+ if (returnResponse) {
233
+ return args;
234
+ }
235
+
240
236
  this.props.onClose(args);
237
+ return args;
241
238
  };
242
239
 
243
240
  parseJsonArray = (input: string): any[] => {
@@ -307,6 +304,9 @@ class FunctionForm extends Component<
307
304
  });
308
305
 
309
306
  const isValid = true;
307
+ let hasInvalidFields = Object.keys(this.state.dynamicFieldDataState).some(
308
+ (key) => key !== fieldId && this.state.invalidFields.includes(key),
309
+ );
310
310
 
311
311
  if (value && isValid) {
312
312
  this.setState({
@@ -315,6 +315,7 @@ class FunctionForm extends Component<
315
315
  ),
316
316
  });
317
317
  } else if (isRequired) {
318
+ hasInvalidFields = true;
318
319
  this.setState((prevState) => ({
319
320
  invalidFields: prevState.invalidFields.includes(fieldId)
320
321
  ? prevState.invalidFields
@@ -326,6 +327,13 @@ class FunctionForm extends Component<
326
327
  if (this.state.parentFields.includes(fieldId)) {
327
328
  this.setState({ parentFieldsChanged: !this.state.parentFieldsChanged });
328
329
  }
330
+
331
+ if (this.props.customSaveCallback) {
332
+ this.props.customSaveCallback({
333
+ hasInvalidFields,
334
+ data: this.onSubmit({}, true),
335
+ });
336
+ }
329
337
  };
330
338
 
331
339
  replacePlaceholders = (
@@ -793,8 +801,8 @@ class FunctionForm extends Component<
793
801
 
794
802
  return html`
795
803
  ${this.state.loading
796
- ? html`<div>
797
- <${AppPageLoader} renderMode=${`MODAL`} />
804
+ ? html`<div class="${styles.functionFormLoader}">
805
+ <${LargeLoader} />
798
806
  </div>`
799
807
  : functionDetails &&
800
808
  functionDetails.meta &&
@@ -8,6 +8,12 @@
8
8
  color: #999999;
9
9
  }
10
10
  }
11
+ .functionFormLoader {
12
+ display: flex;
13
+ justify-content: center;
14
+ min-height: 100px;
15
+ align-items: center;
16
+ }
11
17
  .functionFormWrap {
12
18
  padding: 5px 20px;
13
19
  font-family: 'Inter';