@tai42/studio-sdk 25.0.1 → 26.0.0
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.
- package/README.md +143 -26
- package/dist/components/action-menu.d.ts +30 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +101 -0
- package/dist/components/action-menu.js.map +1 -0
- package/dist/components/agent-picker.d.ts +22 -0
- package/dist/components/agent-picker.d.ts.map +1 -0
- package/dist/components/agent-picker.js +17 -0
- package/dist/components/agent-picker.js.map +1 -0
- package/dist/components/channel-picker.d.ts +27 -0
- package/dist/components/channel-picker.d.ts.map +1 -0
- package/dist/components/channel-picker.js +26 -0
- package/dist/components/channel-picker.js.map +1 -0
- package/dist/components/components.css +226 -8
- package/dist/components/confirm-dialog.d.ts +3 -1
- package/dist/components/confirm-dialog.d.ts.map +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/execution-key-picker.d.ts +26 -10
- package/dist/components/execution-key-picker.d.ts.map +1 -1
- package/dist/components/execution-key-picker.js +134 -15
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +7 -7
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +42 -16
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +13 -11
- package/dist/components/extension-combo-parts.d.ts.map +1 -1
- package/dist/components/extension-combo-parts.js +10 -19
- package/dist/components/extension-combo-parts.js.map +1 -1
- package/dist/components/extension-config-editor.d.ts +33 -0
- package/dist/components/extension-config-editor.d.ts.map +1 -0
- package/dist/components/extension-config-editor.js +190 -0
- package/dist/components/extension-config-editor.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +4 -41
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +1 -48
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +4 -3
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +6 -6
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +8 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +12 -2
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/holder-kinds.d.ts +18 -0
- package/dist/components/holder-kinds.d.ts.map +1 -0
- package/dist/components/holder-kinds.js +41 -0
- package/dist/components/holder-kinds.js.map +1 -0
- package/dist/components/holder-list.d.ts +58 -0
- package/dist/components/holder-list.d.ts.map +1 -0
- package/dist/components/holder-list.js +77 -0
- package/dist/components/holder-list.js.map +1 -0
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/nav-map.d.ts.map +1 -1
- package/dist/components/icons/nav-map.js +1 -3
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +0 -4
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +0 -4
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/named-list-picker.d.ts +39 -0
- package/dist/components/named-list-picker.d.ts.map +1 -0
- package/dist/components/named-list-picker.js +61 -0
- package/dist/components/named-list-picker.js.map +1 -0
- package/dist/components/named-list-test-utils.d.ts +23 -0
- package/dist/components/named-list-test-utils.d.ts.map +1 -0
- package/dist/components/named-list-test-utils.js +33 -0
- package/dist/components/named-list-test-utils.js.map +1 -0
- package/dist/components/recycle-roll.d.ts +23 -0
- package/dist/components/recycle-roll.d.ts.map +1 -0
- package/dist/components/recycle-roll.js +47 -0
- package/dist/components/recycle-roll.js.map +1 -0
- package/dist/components/stepper.d.ts +28 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +33 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/subject-section.d.ts +2 -1
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +10 -5
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/tokens.css +31 -41
- package/dist/components/tokens.d.ts +3 -4
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +5 -13
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/tool-picker-parts.d.ts +5 -1
- package/dist/components/tool-picker-parts.d.ts.map +1 -1
- package/dist/components/tool-picker-parts.js +8 -6
- package/dist/components/tool-picker-parts.js.map +1 -1
- package/dist/components/tool-picker.d.ts +77 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +87 -29
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts +7 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +2 -2
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +4 -2
- package/dist/components/version-history-rows.d.ts.map +1 -1
- package/dist/components/version-history-rows.js +14 -2
- package/dist/components/version-history-rows.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/useApi.d.ts +524 -197
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +43 -10
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +39 -10
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/env-var-fields.d.ts +17 -0
- package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
- package/dist/marketplace-install/env-var-fields.js +22 -0
- package/dist/marketplace-install/env-var-fields.js.map +1 -0
- package/dist/marketplace-install/index.d.ts +18 -0
- package/dist/marketplace-install/index.d.ts.map +1 -0
- package/dist/marketplace-install/index.js +8 -0
- package/dist/marketplace-install/index.js.map +1 -0
- package/dist/marketplace-install/install-env.d.ts +21 -0
- package/dist/marketplace-install/install-env.d.ts.map +1 -0
- package/dist/marketplace-install/install-env.js +22 -0
- package/dist/marketplace-install/install-env.js.map +1 -0
- package/dist/marketplace-install/install-mount.d.ts +42 -0
- package/dist/marketplace-install/install-mount.d.ts.map +1 -0
- package/dist/marketplace-install/install-mount.js +62 -0
- package/dist/marketplace-install/install-mount.js.map +1 -0
- package/dist/marketplace-install/install-report.d.ts +14 -0
- package/dist/marketplace-install/install-report.d.ts.map +1 -0
- package/dist/marketplace-install/install-report.js +19 -0
- package/dist/marketplace-install/install-report.js.map +1 -0
- package/dist/marketplace-install/install-shell.d.ts +34 -0
- package/dist/marketplace-install/install-shell.d.ts.map +1 -0
- package/dist/marketplace-install/install-shell.js +32 -0
- package/dist/marketplace-install/install-shell.js.map +1 -0
- package/dist/marketplace-install/mount-install.d.ts +41 -0
- package/dist/marketplace-install/mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/mount-install.js +90 -0
- package/dist/marketplace-install/mount-install.js.map +1 -0
- package/dist/marketplace-install/plugin-detail.d.ts +17 -0
- package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-detail.js +41 -0
- package/dist/marketplace-install/plugin-detail.js.map +1 -0
- package/dist/marketplace-install/plugin-install.d.ts +27 -0
- package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-install.js +89 -0
- package/dist/marketplace-install/plugin-install.js.map +1 -0
- package/dist/marketplace-install/route-items.d.ts +14 -0
- package/dist/marketplace-install/route-items.d.ts.map +1 -0
- package/dist/marketplace-install/route-items.js +11 -0
- package/dist/marketplace-install/route-items.js.map +1 -0
- package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
- package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
- package/dist/marketplace-install/use-debounced-value.js +18 -0
- package/dist/marketplace-install/use-debounced-value.js.map +1 -0
- package/dist/marketplace-install/use-install-action.d.ts +26 -0
- package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-action.js +35 -0
- package/dist/marketplace-install/use-install-action.js.map +1 -0
- package/dist/marketplace-install/use-install-preview.d.ts +33 -0
- package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-preview.js +44 -0
- package/dist/marketplace-install/use-install-preview.js.map +1 -0
- package/dist/marketplace-install/use-mount-install.d.ts +80 -0
- package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/use-mount-install.js +75 -0
- package/dist/marketplace-install/use-mount-install.js.map +1 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +23 -7
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/registry.d.ts +0 -8
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +25 -1
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +36 -12
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/version.d.ts +6 -4
- package/dist/plugin/version.d.ts.map +1 -1
- package/dist/plugin/version.js +6 -4
- package/dist/plugin/version.js.map +1 -1
- package/dist/query-keys.d.ts +45 -4
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +52 -4
- package/dist/query-keys.js.map +1 -1
- package/dist/schema-form/json-schema-meta.d.ts +3 -0
- package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
- package/dist/schema-form/json-schema-meta.js +3 -0
- package/dist/schema-form/json-schema-meta.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/token-rules.d.ts +26 -0
- package/dist/token-rules.d.ts.map +1 -0
- package/dist/token-rules.js +64 -0
- package/dist/token-rules.js.map +1 -0
- package/package.json +2 -2
- package/src/components/action-menu.tsx +160 -0
- package/src/components/agent-picker.tsx +53 -0
- package/src/components/channel-picker.tsx +68 -0
- package/src/components/components.css +226 -8
- package/src/components/confirm-dialog.tsx +4 -1
- package/src/components/execution-key-picker.tsx +252 -36
- package/src/components/extension-combo-builder.tsx +61 -29
- package/src/components/extension-combo-parts.tsx +28 -38
- package/src/components/extension-config-editor.tsx +307 -0
- package/src/components/extension-grouping.ts +4 -73
- package/src/components/extension-picker.tsx +13 -13
- package/src/components/fleet-report.tsx +38 -4
- package/src/components/holder-kinds.ts +60 -0
- package/src/components/holder-list.tsx +163 -0
- package/src/components/icons/index.ts +0 -2
- package/src/components/icons/nav-map.ts +0 -4
- package/src/components/icons/nav-marks.tsx +0 -15
- package/src/components/named-list-picker.tsx +182 -0
- package/src/components/named-list-test-utils.tsx +47 -0
- package/src/components/recycle-roll.tsx +120 -0
- package/src/components/stepper.tsx +111 -0
- package/src/components/subject-section.tsx +10 -5
- package/src/components/tokens.css +31 -41
- package/src/components/tokens.ts +5 -13
- package/src/components/tool-picker-parts.tsx +17 -6
- package/src/components/tool-picker.tsx +215 -41
- package/src/components/version-history-panel.tsx +8 -0
- package/src/components/version-history-rows.tsx +14 -2
- package/src/errors.ts +10 -0
- package/src/hooks/useCapabilities.ts +59 -10
- package/src/index.ts +54 -14
- package/src/marketplace-install/env-var-fields.tsx +59 -0
- package/src/marketplace-install/index.ts +17 -0
- package/src/marketplace-install/install-env.ts +39 -0
- package/src/marketplace-install/install-mount.ts +96 -0
- package/src/marketplace-install/install-report.tsx +54 -0
- package/src/marketplace-install/install-shell.tsx +99 -0
- package/src/marketplace-install/mount-install.tsx +290 -0
- package/src/marketplace-install/plugin-detail.ts +60 -0
- package/src/marketplace-install/plugin-install.tsx +221 -0
- package/src/marketplace-install/route-items.ts +20 -0
- package/src/marketplace-install/use-debounced-value.ts +18 -0
- package/src/marketplace-install/use-install-action.ts +60 -0
- package/src/marketplace-install/use-install-preview.ts +74 -0
- package/src/marketplace-install/use-mount-install.ts +116 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/types.ts +27 -7
- package/src/plugin/registry.ts +19 -1
- package/src/plugin/types.ts +40 -12
- package/src/plugin/version.ts +6 -4
- package/src/query-keys.ts +71 -4
- package/src/schema-form/json-schema-meta.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/token-rules.ts +66 -0
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Picks the api key a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Picks the api key a stored record (a route, a hook, a trigger link, a schedule)
|
|
3
|
+
* RUNS AS. Presentational and prop-driven — the host feature owns the api-key list
|
|
4
|
+
* query and hands it in, so this component (and the SDK) stay free of a
|
|
5
|
+
* data-fetching library, exactly as {@link ToolPicker} does.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
7
|
+
* The `binding` decides what renders: nothing with access control off, a pending
|
|
8
|
+
* control while the session's projection loads, and otherwise the key list plus the
|
|
9
|
+
* session's own identity when it is an api key the list does not hold. A stored key
|
|
10
|
+
* the loaded list does not hold is flagged with what the server will make of it,
|
|
11
|
+
* compared with the session's identity. The server decides authorization; the host
|
|
12
|
+
* form renders its refusal verbatim elsewhere.
|
|
10
13
|
*/
|
|
11
14
|
import type { TokensPayload } from '@tai42/api-client';
|
|
12
|
-
import type
|
|
15
|
+
import { type ReactNode, useId } from 'react';
|
|
13
16
|
|
|
14
|
-
import { errorMessage } from '../errors';
|
|
17
|
+
import { errorMessage, isForbiddenError } from '../errors';
|
|
18
|
+
import type { ExecutionKeyBinding, OwnIdentity } from '../hooks/useCapabilities';
|
|
19
|
+
import { Badge } from './badge';
|
|
15
20
|
import { Field } from './field';
|
|
16
21
|
import { ErrorState } from './primitives';
|
|
17
|
-
import { Select } from './select';
|
|
22
|
+
import { Select, type SelectOption } from './select';
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* The subset of the api-key list query this picker reads. A caller's TanStack
|
|
@@ -61,56 +66,267 @@ function pickableKeys(keys: TokensPayload): TokensPayload {
|
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Whether the form has a key to send: always with access control off, never while the
|
|
71
|
+
* projection is pending, and otherwise once the list read has answered (loaded, or
|
|
72
|
+
* refused with a 403) and the picker holds a value, lists a key, or offers a key
|
|
73
|
+
* session's own identity. Under a checked binding the picker renders its `Select`
|
|
74
|
+
* exactly while the list read is pending or this holds.
|
|
75
|
+
*/
|
|
76
|
+
export function canHoldExecutionKey(
|
|
77
|
+
query: ExecutionKeyQuery,
|
|
78
|
+
binding: ExecutionKeyBinding,
|
|
79
|
+
value: string,
|
|
80
|
+
): boolean {
|
|
81
|
+
if (binding.mode === 'unchecked') return true;
|
|
82
|
+
if (binding.mode === 'pending') return false;
|
|
83
|
+
return (
|
|
84
|
+
(query.isSuccess || isForbiddenError(query.error)) &&
|
|
85
|
+
(value !== '' ||
|
|
86
|
+
(query.isSuccess && !isExecutionKeyListEmpty(query)) ||
|
|
87
|
+
binding.own.kind === 'key')
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The unlisted line's sentence for a stored key the key list does not hold. */
|
|
92
|
+
function unlistedKeyHint(value: string, own: OwnIdentity): string {
|
|
93
|
+
if (own.admin) {
|
|
94
|
+
return `${value} is not a listed key. As an admin you may bind another identity; the server checks it on save.`;
|
|
95
|
+
}
|
|
96
|
+
if (own.kind === 'account') {
|
|
97
|
+
return `${value} is not a listed key. The server accepts only an api key you own; any other value is refused on save.`;
|
|
98
|
+
}
|
|
99
|
+
return `${value} is not a listed key. The server accepts an unlisted key only when it is your own identity; any other value is refused on save.`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The second line: what the comparison of the value with the session's identity shows, or undefined when it shows
|
|
103
|
+
* nothing to say (a key session's own identity). */
|
|
104
|
+
function unlistedKeyNote(
|
|
105
|
+
value: string,
|
|
106
|
+
own: OwnIdentity,
|
|
107
|
+
): { readonly badge: string; readonly text: string } | undefined {
|
|
108
|
+
if (value === own.userId) {
|
|
109
|
+
return own.kind === 'account'
|
|
110
|
+
? {
|
|
111
|
+
badge: 'not an api key',
|
|
112
|
+
text: `${value} is this session's account, not an api key, so the server will refuse it on save.`,
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
badge: 'not your identity',
|
|
118
|
+
text: own.admin
|
|
119
|
+
? `${value} is not this session's identity (${own.userId}).`
|
|
120
|
+
: `${value} is not this session's identity (${own.userId}), so the server will refuse it on save.`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const DEFAULT_DESCRIPTION =
|
|
125
|
+
"The api key this fire runs AS. Every tool call it makes is authorized against that key's live grants — prefer a least-privilege service key over a human's broad one.";
|
|
126
|
+
const FORBIDDEN_NOTE =
|
|
127
|
+
'This session cannot list api keys. Picking one needs read access to GET /api/auth/tokens-payload; ask an administrator to grant it.';
|
|
128
|
+
const KEY_EMPTY_NOTE =
|
|
129
|
+
'No api keys of yours are listed. Mint one on the settings API-keys tab to run as a narrower key.';
|
|
130
|
+
const ACCOUNT_EMPTY_NOTE =
|
|
131
|
+
'No api keys of yours are listed. You are signed in as an account, not an api key, so your own identity cannot be picked here. Mint a key on the settings API-keys tab first.';
|
|
132
|
+
const MUTED_LINE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
|
|
133
|
+
|
|
134
|
+
function MutedLine({ text }: { readonly text: string }): ReactNode {
|
|
135
|
+
return (
|
|
136
|
+
<p role="status" style={MUTED_LINE}>
|
|
137
|
+
{text}
|
|
138
|
+
</p>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
64
142
|
export interface ExecutionKeyPickerProps {
|
|
65
143
|
/** The api-key list read the host feature owns and hands in. */
|
|
66
144
|
readonly query: ExecutionKeyQuery;
|
|
145
|
+
/** How the host form binds its key (`useExecutionKeyBinding()`). */
|
|
146
|
+
readonly binding: ExecutionKeyBinding;
|
|
67
147
|
readonly value: string;
|
|
68
148
|
readonly onValueChange: (value: string) => void;
|
|
69
|
-
/** The host form's required-field error, shown under the control. */
|
|
149
|
+
/** The host form's required-field error, shown under the control while a `Select` renders. */
|
|
70
150
|
readonly error?: string | undefined;
|
|
151
|
+
/** The field's description; the default speaks of a fire. */
|
|
152
|
+
readonly description?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** What the checked picker draws, derived from the list read, the value and the session's identity. */
|
|
156
|
+
interface CheckedView {
|
|
157
|
+
readonly options: readonly SelectOption[];
|
|
158
|
+
/** A `Select` renders (a pending list, or something to hold). */
|
|
159
|
+
readonly selectRenders: boolean;
|
|
160
|
+
/** The muted note beside or in place of the `Select`. */
|
|
161
|
+
readonly note: string | undefined;
|
|
162
|
+
/** The stored value no listed option carries, once the list loaded. */
|
|
163
|
+
readonly unlisted: boolean;
|
|
164
|
+
/** The session's own identity is offered as an option. */
|
|
165
|
+
readonly ownOffered: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The view while the binding is pending: a disabled `Select` and nothing else. */
|
|
169
|
+
const PENDING_VIEW: CheckedView = {
|
|
170
|
+
options: [],
|
|
171
|
+
selectRenders: true,
|
|
172
|
+
note: undefined,
|
|
173
|
+
unlisted: false,
|
|
174
|
+
ownOffered: false,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** The `Select`'s options: the stored value no listed option carries (flagged once the list
|
|
178
|
+
* loaded), the session's own identity when offered, then the listed keys. */
|
|
179
|
+
function checkedOptions(
|
|
180
|
+
stored: string | null,
|
|
181
|
+
unlisted: boolean,
|
|
182
|
+
forbidden: boolean,
|
|
183
|
+
ownOffered: string | null,
|
|
184
|
+
listed: readonly TokensPayload[number][],
|
|
185
|
+
): SelectOption[] {
|
|
186
|
+
const options: SelectOption[] = [];
|
|
187
|
+
if (stored !== null && (unlisted || forbidden)) {
|
|
188
|
+
options.push({ value: stored, label: unlisted ? `${stored} (not a listed key)` : stored });
|
|
189
|
+
}
|
|
190
|
+
if (ownOffered !== null) {
|
|
191
|
+
options.push({ value: ownOffered, label: `${ownOffered} — this session's identity` });
|
|
192
|
+
}
|
|
193
|
+
for (const key of listed) options.push({ value: key.user_id, label: keyLabel(key) });
|
|
194
|
+
return options;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The muted note: the refused read's, or the empty list's for the session's kind. */
|
|
198
|
+
function checkedNote(query: ExecutionKeyQuery, own: OwnIdentity): string | undefined {
|
|
199
|
+
if (isForbiddenError(query.error)) return FORBIDDEN_NOTE;
|
|
200
|
+
if (!isExecutionKeyListEmpty(query)) return undefined;
|
|
201
|
+
return own.kind === 'key' ? KEY_EMPTY_NOTE : ACCOUNT_EMPTY_NOTE;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function checkedView(query: ExecutionKeyQuery, own: OwnIdentity, value: string): CheckedView {
|
|
205
|
+
const forbidden = isForbiddenError(query.error);
|
|
206
|
+
const listed = query.isSuccess ? pickableKeys(query.data ?? []) : [];
|
|
207
|
+
const answered = query.isSuccess || forbidden;
|
|
208
|
+
const ownOffered =
|
|
209
|
+
own.kind === 'key' && answered && !listed.some((key) => key.user_id === own.userId);
|
|
210
|
+
const stored = value !== '' && !(ownOffered && value === own.userId) ? value : null;
|
|
211
|
+
const unlisted =
|
|
212
|
+
query.isSuccess && stored !== null && !listed.some((key) => key.user_id === stored);
|
|
213
|
+
const options = checkedOptions(
|
|
214
|
+
stored,
|
|
215
|
+
unlisted,
|
|
216
|
+
forbidden,
|
|
217
|
+
ownOffered ? own.userId : null,
|
|
218
|
+
listed,
|
|
219
|
+
);
|
|
220
|
+
const selectRenders =
|
|
221
|
+
(query.isPending && !query.isError) ||
|
|
222
|
+
canHoldExecutionKey(query, { mode: 'checked', own }, value);
|
|
223
|
+
return { options, selectRenders, note: checkedNote(query, own), unlisted, ownOffered };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The lines under the `Select`: the unlisted hint, the own-identity hint and the comparison note. */
|
|
227
|
+
function KeyHints({
|
|
228
|
+
own,
|
|
229
|
+
view,
|
|
230
|
+
value,
|
|
231
|
+
hintId,
|
|
232
|
+
noteId,
|
|
233
|
+
}: {
|
|
234
|
+
readonly own: OwnIdentity | undefined;
|
|
235
|
+
readonly view: CheckedView;
|
|
236
|
+
readonly value: string;
|
|
237
|
+
readonly hintId: string;
|
|
238
|
+
readonly noteId: string;
|
|
239
|
+
}): ReactNode {
|
|
240
|
+
if (own === undefined) return null;
|
|
241
|
+
const keyNote = view.unlisted ? unlistedKeyNote(value, own) : undefined;
|
|
242
|
+
return (
|
|
243
|
+
<>
|
|
244
|
+
{view.unlisted ? (
|
|
245
|
+
<p id={hintId} className="tai-field-hint" style={{ margin: 0 }}>
|
|
246
|
+
<Badge variant="warning">not a listed key</Badge> {unlistedKeyHint(value, own)}
|
|
247
|
+
</p>
|
|
248
|
+
) : null}
|
|
249
|
+
{view.ownOffered && value === own.userId ? (
|
|
250
|
+
<p id={hintId} className="tai-field-hint" style={{ margin: 0 }}>
|
|
251
|
+
{`This runs as this session's own identity, ${own.userId}, with its live grants.`}
|
|
252
|
+
</p>
|
|
253
|
+
) : null}
|
|
254
|
+
{keyNote !== undefined ? (
|
|
255
|
+
<p id={noteId} role="status" className="tai-field-hint" style={{ margin: 0 }}>
|
|
256
|
+
<Badge variant="warning">{keyNote.badge}</Badge> {keyNote.text}
|
|
257
|
+
</p>
|
|
258
|
+
) : null}
|
|
259
|
+
</>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The ids the `Select` is described by: the hint while one shows, the note while one shows. */
|
|
264
|
+
function describedByIds(
|
|
265
|
+
own: OwnIdentity | undefined,
|
|
266
|
+
view: CheckedView,
|
|
267
|
+
value: string,
|
|
268
|
+
hintId: string,
|
|
269
|
+
noteId: string,
|
|
270
|
+
): string | undefined {
|
|
271
|
+
if (own === undefined) return undefined;
|
|
272
|
+
const hinted = view.unlisted || (view.ownOffered && value === own.userId);
|
|
273
|
+
const noted = view.unlisted && unlistedKeyNote(value, own) !== undefined;
|
|
274
|
+
const ids = [hinted ? hintId : undefined, noted ? noteId : undefined].filter(
|
|
275
|
+
(id) => id !== undefined,
|
|
276
|
+
);
|
|
277
|
+
return ids.length === 0 ? undefined : ids.join(' ');
|
|
71
278
|
}
|
|
72
279
|
|
|
73
280
|
export function ExecutionKeyPicker({
|
|
74
281
|
query,
|
|
282
|
+
binding,
|
|
75
283
|
value,
|
|
76
284
|
onValueChange,
|
|
77
285
|
error,
|
|
286
|
+
description = DEFAULT_DESCRIPTION,
|
|
78
287
|
}: ExecutionKeyPickerProps): ReactNode {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Suppressed: the empty note / the ErrorState already say it.
|
|
86
|
-
error={listEmpty || query.isError ? undefined : error}
|
|
87
|
-
// Only the Select branch claims the field's control id; the error and
|
|
88
|
-
// empty branches render no labelable element, so `for` would dangle.
|
|
89
|
-
group={query.isError || listEmpty}
|
|
90
|
-
>
|
|
91
|
-
{query.isError ? (
|
|
288
|
+
const hintId = useId();
|
|
289
|
+
const noteId = useId();
|
|
290
|
+
if (binding.mode === 'unchecked') return null;
|
|
291
|
+
if (binding.mode === 'checked' && query.isError && !isForbiddenError(query.error)) {
|
|
292
|
+
return (
|
|
293
|
+
<Field label="Execution key" description={description} group>
|
|
92
294
|
<ErrorState
|
|
93
295
|
message={errorMessage(query.error)}
|
|
94
296
|
onRetry={() => {
|
|
95
297
|
query.refetch();
|
|
96
298
|
}}
|
|
97
299
|
/>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
300
|
+
</Field>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
// A pending binding renders the same tree as a pending list, so the control is not
|
|
304
|
+
// remounted when the binding resolves.
|
|
305
|
+
const own = binding.mode === 'checked' ? binding.own : undefined;
|
|
306
|
+
const view = own === undefined ? PENDING_VIEW : checkedView(query, own, value);
|
|
307
|
+
const loading = own === undefined || query.isPending;
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<Field
|
|
311
|
+
label="Execution key"
|
|
312
|
+
description={description}
|
|
313
|
+
error={view.selectRenders ? error : undefined}
|
|
314
|
+
// Only the Select claims the field's control id; with nothing to hold no
|
|
315
|
+
// labelable element renders, so `for` would dangle.
|
|
316
|
+
group={!view.selectRenders}
|
|
317
|
+
>
|
|
318
|
+
{view.selectRenders ? (
|
|
103
319
|
<Select
|
|
104
|
-
options={
|
|
105
|
-
value: key.user_id,
|
|
106
|
-
label: keyLabel(key),
|
|
107
|
-
}))}
|
|
320
|
+
options={view.options}
|
|
108
321
|
value={value}
|
|
109
|
-
placeholder={
|
|
110
|
-
disabled={
|
|
322
|
+
placeholder={loading ? 'Loading keys…' : 'Select an execution key'}
|
|
323
|
+
disabled={loading}
|
|
111
324
|
onValueChange={onValueChange}
|
|
325
|
+
aria-describedby={describedByIds(own, view, value, hintId, noteId)}
|
|
112
326
|
/>
|
|
113
|
-
)}
|
|
327
|
+
) : null}
|
|
328
|
+
{view.note !== undefined ? <MutedLine text={view.note} /> : null}
|
|
329
|
+
<KeyHints own={own} view={view} value={value} hintId={hintId} noteId={noteId} />
|
|
114
330
|
</Field>
|
|
115
331
|
);
|
|
116
332
|
}
|
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
* - the current combos, each an ordered row of name `Badge`s with a per-combo Edit
|
|
11
11
|
* (pull it back into the draft) and Remove;
|
|
12
12
|
* - a combo UNDER CONSTRUCTION, authored with the shared `ExtensionPicker` (grouped
|
|
13
|
-
* by kind, single-select for the non-stackable `backend` kind).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* by kind, single-select for the non-stackable `backend` kind). An extension whose
|
|
14
|
+
* catalog entry declares a `config_schema` is authored as `{ name, config }` through
|
|
15
|
+
* its settings form; any other name stays a bare element, or the edited combo's own
|
|
16
|
+
* element when it survives the edit. An "Add extension set" / "Update extension
|
|
17
|
+
* set" action composes the elements and writes the combo into the value.
|
|
18
18
|
*
|
|
19
19
|
* The builder only blocks the client-trivial invalids: an EMPTY combo (Add is
|
|
20
20
|
* disabled until the draft has ≥1 member), a DUPLICATE of a combo already in the
|
|
21
|
-
* value (by name sequence; flagged inline, Add blocked), an INVALID
|
|
22
|
-
*
|
|
21
|
+
* value (by name sequence; flagged inline, Add blocked), an INVALID or not yet
|
|
22
|
+
* reported settings form of a drafted extension (Add is blocked), and an
|
|
23
23
|
* UNKNOWN extension name in an existing combo (a name absent from `available`, which
|
|
24
24
|
* happens when a plugin is removed after a preset was authored) — flagged inline on
|
|
25
25
|
* that combo row and reported through `onValidityChange` so the caller can block
|
|
@@ -39,15 +39,17 @@ import type { Extension, PresetExtensionElement } from '@tai42/api-client';
|
|
|
39
39
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
40
40
|
|
|
41
41
|
import { comboElementNames, extensionElementName } from '../extension-combos';
|
|
42
|
-
import {
|
|
42
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
43
43
|
import {
|
|
44
44
|
ComboDraftEditor,
|
|
45
45
|
CommittedComboList,
|
|
46
|
-
|
|
47
|
-
outputSchemaConfig,
|
|
46
|
+
type ConfigurableExtension,
|
|
48
47
|
sameNames,
|
|
49
48
|
unknownNames,
|
|
50
49
|
} from './extension-combo-parts';
|
|
50
|
+
import type { ExtensionConfigChange } from './extension-config-editor';
|
|
51
|
+
|
|
52
|
+
type Seeds = Readonly<Record<string, Record<string, unknown> | undefined>>;
|
|
51
53
|
|
|
52
54
|
export interface ExtensionComboBuilderProps {
|
|
53
55
|
/** The extension catalog the combos are drawn from (caller-fetched). */
|
|
@@ -84,15 +86,29 @@ export function ExtensionComboBuilder({
|
|
|
84
86
|
availableReady = true,
|
|
85
87
|
}: ExtensionComboBuilderProps): ReactNode {
|
|
86
88
|
const [draft, setDraft] = useState<string[]>([]);
|
|
87
|
-
|
|
89
|
+
// The settings each drafted configurable extension's form last reported.
|
|
90
|
+
const [draftConfigs, setDraftConfigs] = useState<Readonly<Record<string, ExtensionConfigChange>>>(
|
|
91
|
+
{},
|
|
92
|
+
);
|
|
93
|
+
// The stored settings the forms seed from when a combo is pulled in for edit.
|
|
94
|
+
const [draftSeeds, setDraftSeeds] = useState<Seeds>({});
|
|
88
95
|
// The index of the combo being edited (pulled into the draft), or `null` for a new one.
|
|
89
96
|
const [editing, setEditing] = useState<number | null>(null);
|
|
90
97
|
|
|
91
|
-
const
|
|
98
|
+
const settingsSchemas = new Map<string, JsonSchema>();
|
|
99
|
+
for (const extension of available) {
|
|
100
|
+
if (extension.config_schema !== null) {
|
|
101
|
+
settingsSchemas.set(extension.name, extension.config_schema);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const configurable: ConfigurableExtension[] = draft.flatMap((name) => {
|
|
105
|
+
const schema = settingsSchemas.get(name);
|
|
106
|
+
return schema === undefined ? [] : [{ name, schema }];
|
|
107
|
+
});
|
|
92
108
|
const isDuplicate =
|
|
93
109
|
draft.length > 0 &&
|
|
94
110
|
value.some((combo, index) => index !== editing && sameNames(comboElementNames(combo), draft));
|
|
95
|
-
const configInvalid =
|
|
111
|
+
const configInvalid = configurable.some(({ name }) => draftConfigs[name]?.valid !== true);
|
|
96
112
|
const canAdd = !disabled && draft.length > 0 && !isDuplicate && !configInvalid;
|
|
97
113
|
|
|
98
114
|
// Unknown-name validation over the EXISTING combos. Until the catalog is ready the
|
|
@@ -108,16 +124,16 @@ export function ExtensionComboBuilder({
|
|
|
108
124
|
|
|
109
125
|
const resetDraft = (): void => {
|
|
110
126
|
setDraft([]);
|
|
111
|
-
|
|
127
|
+
setDraftConfigs({});
|
|
128
|
+
setDraftSeeds({});
|
|
112
129
|
setEditing(null);
|
|
113
130
|
};
|
|
114
131
|
|
|
115
132
|
/**
|
|
116
|
-
* Compose the draft names into
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* name becomes a bare-name element.
|
|
133
|
+
* Compose the draft names into elements. An extension whose catalog entry declares
|
|
134
|
+
* a `config_schema` is authored as `{ name, config }` through its settings form;
|
|
135
|
+
* any other name stays a bare element, or the edited combo's own element when it
|
|
136
|
+
* survives the edit.
|
|
121
137
|
*/
|
|
122
138
|
const composeCombo = (): PresetExtensionElement[] => {
|
|
123
139
|
const source = editing === null ? undefined : value[editing];
|
|
@@ -125,11 +141,10 @@ export function ExtensionComboBuilder({
|
|
|
125
141
|
if (source !== undefined) {
|
|
126
142
|
for (const element of source) byName.set(extensionElementName(element), element);
|
|
127
143
|
}
|
|
128
|
-
return draft.map((name) =>
|
|
129
|
-
name
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
144
|
+
return draft.map((name) => {
|
|
145
|
+
const report = settingsSchemas.has(name) ? draftConfigs[name] : undefined;
|
|
146
|
+
return report === undefined ? (byName.get(name) ?? name) : { name, config: report.config };
|
|
147
|
+
});
|
|
133
148
|
};
|
|
134
149
|
|
|
135
150
|
const commitCombo = (): void => {
|
|
@@ -146,11 +161,28 @@ export function ExtensionComboBuilder({
|
|
|
146
161
|
const startEdit = (index: number): void => {
|
|
147
162
|
const combo = value[index];
|
|
148
163
|
if (combo === undefined) return;
|
|
164
|
+
const seeds: Record<string, Record<string, unknown>> = {};
|
|
165
|
+
for (const element of combo) {
|
|
166
|
+
if (typeof element !== 'string') seeds[element.name] = element.config;
|
|
167
|
+
}
|
|
149
168
|
setDraft(comboElementNames(combo));
|
|
150
|
-
|
|
169
|
+
setDraftConfigs({});
|
|
170
|
+
setDraftSeeds(seeds);
|
|
151
171
|
setEditing(index);
|
|
152
172
|
};
|
|
153
173
|
|
|
174
|
+
const changeDraft = (next: string[]): void => {
|
|
175
|
+
setDraft(next);
|
|
176
|
+
// An unticked extension's settings report goes with it.
|
|
177
|
+
setDraftConfigs((current) =>
|
|
178
|
+
Object.fromEntries(Object.entries(current).filter(([name]) => next.includes(name))),
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const changeDraftConfig = (name: string, change: ExtensionConfigChange): void => {
|
|
183
|
+
setDraftConfigs((current) => ({ ...current, [name]: change }));
|
|
184
|
+
};
|
|
185
|
+
|
|
154
186
|
const removeCombo = (index: number): void => {
|
|
155
187
|
onChange(value.filter((_, i) => i !== index).map((combo) => [...combo]));
|
|
156
188
|
// Removing the row under edit (or one before it) invalidates the draft target.
|
|
@@ -172,10 +204,10 @@ export function ExtensionComboBuilder({
|
|
|
172
204
|
<ComboDraftEditor
|
|
173
205
|
available={available}
|
|
174
206
|
draft={draft}
|
|
175
|
-
onDraftChange={
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
onDraftConfigChange={
|
|
207
|
+
onDraftChange={changeDraft}
|
|
208
|
+
configurable={configurable}
|
|
209
|
+
draftConfigs={draftSeeds}
|
|
210
|
+
onDraftConfigChange={changeDraftConfig}
|
|
179
211
|
isDuplicate={isDuplicate}
|
|
180
212
|
canAdd={canAdd}
|
|
181
213
|
editing={editing}
|
|
@@ -7,35 +7,18 @@ import type { Extension, PresetExtensionElement } from '@tai42/api-client';
|
|
|
7
7
|
import { Fragment, type ReactNode } from 'react';
|
|
8
8
|
|
|
9
9
|
import { comboElementNames } from '../extension-combos';
|
|
10
|
-
import {
|
|
10
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
11
11
|
import { Badge } from './badge';
|
|
12
|
+
import { type ExtensionConfigChange, ExtensionConfigEditor } from './extension-config-editor';
|
|
12
13
|
import { ExtensionPicker } from './extension-picker';
|
|
13
14
|
import { CloseIcon, XCircleIcon } from './icons';
|
|
14
15
|
import { Button } from './primitives';
|
|
15
16
|
|
|
16
|
-
/** The one config-taking extension this builder authors inline. */
|
|
17
|
-
export const OUTPUT_SCHEMA = 'output_schema';
|
|
18
|
-
|
|
19
17
|
/** Two combos are equal when their NAME sequences match (config is not part of identity). */
|
|
20
18
|
export function sameNames(a: readonly string[], b: readonly string[]): boolean {
|
|
21
19
|
return a.length === b.length && a.every((name, index) => name === b[index]);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/** The stored `output_schema` config schema for a combo, if it carries one. */
|
|
25
|
-
export function outputSchemaConfig(
|
|
26
|
-
combo: readonly PresetExtensionElement[],
|
|
27
|
-
): Record<string, unknown> | null {
|
|
28
|
-
for (const element of combo) {
|
|
29
|
-
if (typeof element !== 'string' && element.name === OUTPUT_SCHEMA) {
|
|
30
|
-
const schema = element.config.schema;
|
|
31
|
-
return typeof schema === 'object' && schema !== null && !Array.isArray(schema)
|
|
32
|
-
? (schema as Record<string, unknown>)
|
|
33
|
-
: {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
22
|
/** The element names in a combo that are absent from the known catalog. */
|
|
40
23
|
export function unknownNames(
|
|
41
24
|
combo: readonly PresetExtensionElement[],
|
|
@@ -158,14 +141,20 @@ export function CommittedComboList({
|
|
|
158
141
|
);
|
|
159
142
|
}
|
|
160
143
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
144
|
+
/** A drafted extension whose catalog entry declares a settings schema. */
|
|
145
|
+
export interface ConfigurableExtension {
|
|
146
|
+
readonly name: string;
|
|
147
|
+
readonly schema: JsonSchema;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The combo under construction: the extension picker, one settings form per drafted
|
|
151
|
+
* configurable extension, the duplicate note, and the Add/Update + Cancel actions. */
|
|
163
152
|
export function ComboDraftEditor({
|
|
164
153
|
available,
|
|
165
154
|
draft,
|
|
166
155
|
onDraftChange,
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
configurable,
|
|
157
|
+
draftConfigs,
|
|
169
158
|
onDraftConfigChange,
|
|
170
159
|
isDuplicate,
|
|
171
160
|
canAdd,
|
|
@@ -178,9 +167,9 @@ export function ComboDraftEditor({
|
|
|
178
167
|
readonly available: readonly Extension[];
|
|
179
168
|
readonly draft: string[];
|
|
180
169
|
readonly onDraftChange: (draft: string[]) => void;
|
|
181
|
-
readonly
|
|
182
|
-
readonly
|
|
183
|
-
readonly onDraftConfigChange: (change:
|
|
170
|
+
readonly configurable: readonly ConfigurableExtension[];
|
|
171
|
+
readonly draftConfigs: Readonly<Record<string, Record<string, unknown> | undefined>>;
|
|
172
|
+
readonly onDraftConfigChange: (name: string, change: ExtensionConfigChange) => void;
|
|
184
173
|
readonly isDuplicate: boolean;
|
|
185
174
|
readonly canAdd: boolean;
|
|
186
175
|
readonly editing: number | null;
|
|
@@ -199,20 +188,21 @@ export function ComboDraftEditor({
|
|
|
199
188
|
idPrefix={`${idPrefix}-draft`}
|
|
200
189
|
/>
|
|
201
190
|
|
|
202
|
-
{
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
onChange={onDraftConfigChange}
|
|
206
|
-
requireTitle={false}
|
|
207
|
-
label="Output schema"
|
|
208
|
-
description="The JSON Schema the output_schema extension enforces on this extension set's result."
|
|
209
|
-
disabled={disabled}
|
|
210
|
-
idPrefix={`${idPrefix}-output-schema`}
|
|
211
|
-
// Re-seed the editor when the draft target changes (a new draft vs a
|
|
191
|
+
{configurable.map(({ name, schema }) => (
|
|
192
|
+
<ExtensionConfigEditor
|
|
193
|
+
// Re-seed the form when the draft target changes (a new draft vs a
|
|
212
194
|
// specific combo pulled in for edit).
|
|
213
|
-
key={editing === null ? 'new' :
|
|
195
|
+
key={`${editing === null ? 'new' : String(editing)}-${name}`}
|
|
196
|
+
name={name}
|
|
197
|
+
schema={schema}
|
|
198
|
+
value={draftConfigs[name]}
|
|
199
|
+
onChange={(change) => {
|
|
200
|
+
onDraftConfigChange(name, change);
|
|
201
|
+
}}
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
idPrefix={`${idPrefix}-settings-${name}`}
|
|
214
204
|
/>
|
|
215
|
-
)
|
|
205
|
+
))}
|
|
216
206
|
|
|
217
207
|
{isDuplicate ? (
|
|
218
208
|
<p role="alert" className="tai-field-error">
|