@nocobase/client-v2 2.2.0-alpha.7 → 2.2.0-alpha.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.2.0-alpha.7",
3
+ "version": "2.2.0-alpha.8",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.2.0-alpha.7",
31
- "@nocobase/flow-engine": "2.2.0-alpha.7",
32
- "@nocobase/sdk": "2.2.0-alpha.7",
33
- "@nocobase/shared": "2.2.0-alpha.7",
34
- "@nocobase/utils": "2.2.0-alpha.7",
30
+ "@nocobase/evaluators": "2.2.0-alpha.8",
31
+ "@nocobase/flow-engine": "2.2.0-alpha.8",
32
+ "@nocobase/sdk": "2.2.0-alpha.8",
33
+ "@nocobase/shared": "2.2.0-alpha.8",
34
+ "@nocobase/utils": "2.2.0-alpha.8",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -47,5 +47,5 @@
47
47
  "react-i18next": "^11.15.1",
48
48
  "react-router-dom": "^6.30.1"
49
49
  },
50
- "gitHead": "34ba02960fca9ab0b6881d3db7040a8a64067bba"
50
+ "gitHead": "810c81e5963966bda7ec03b6453a3a80fe60e027"
51
51
  }
@@ -35,6 +35,7 @@ import { SystemSettingsSource } from './flow/system-settings';
35
35
  import { LayoutManager } from './layout-manager/LayoutManager';
36
36
  import type { PluginClass, PluginManager, PluginType } from './PluginManager';
37
37
  import { RouteRepository } from './RouteRepository';
38
+ import { stripModernClientPrefix } from './authRedirect';
38
39
  import type {
39
40
  ComponentTypeAndString,
40
41
  RenderableComponentType,
@@ -402,7 +403,7 @@ export abstract class BaseApplication<
402
403
  this.favicon = favicon || '';
403
404
  }
404
405
 
405
- const iconHref = this.favicon || '/favicon/favicon.ico';
406
+ const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
406
407
 
407
408
  if (faviconLinkElement) {
408
409
  faviconLinkElement.href = iconHref;
@@ -438,7 +439,7 @@ export abstract class BaseApplication<
438
439
  }
439
440
 
440
441
  getCdnUrl() {
441
- return ensureTrailingSlash(window['__webpack_public_path__'] || this.getPublicPath());
442
+ return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
442
443
  }
443
444
 
444
445
  getPublicPath() {
@@ -35,6 +35,7 @@ describe('app', () => {
35
35
  document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
36
36
  document.documentElement.removeAttribute('lang');
37
37
  delete window['__webpack_public_path__'];
38
+ delete window['__nocobase_modern_client_prefix__'];
38
39
  vi.restoreAllMocks();
39
40
  });
40
41
 
@@ -84,6 +85,34 @@ describe('app', () => {
84
85
  expect(app.getCdnUrl()).toBe('/cdn/assets/');
85
86
  });
86
87
 
88
+ it('should remove the modern client prefix from the CDN fallback path', () => {
89
+ const app = new Application({
90
+ router,
91
+ publicPath: '/v/',
92
+ });
93
+
94
+ expect(app.getCdnUrl()).toBe('/');
95
+ });
96
+
97
+ it('should preserve APP_PUBLIC_PATH when removing the modern client prefix', () => {
98
+ const app = new Application({
99
+ router,
100
+ publicPath: '/nocobase/v/',
101
+ });
102
+
103
+ expect(app.getCdnUrl()).toBe('/nocobase/');
104
+ });
105
+
106
+ it('should support a custom modern client prefix', () => {
107
+ window['__nocobase_modern_client_prefix__'] = 'modern';
108
+ const app = new Application({
109
+ router,
110
+ publicPath: '/nocobase/modern/',
111
+ });
112
+
113
+ expect(app.getCdnUrl()).toBe('/nocobase/');
114
+ });
115
+
87
116
  it('should apply the provided favicon immediately', () => {
88
117
  const app = new Application({ router });
89
118
 
@@ -121,12 +121,15 @@ function normalizeTypes(types: TypedConstantSpec[]): NormalizedType[] {
121
121
  );
122
122
  }
123
123
 
124
- function defaultValueFor(type: TypedConstantType): unknown {
124
+ function defaultValueFor(type: TypedConstantType, typedProps: Record<string, unknown> = {}): unknown {
125
125
  switch (type) {
126
126
  case 'string':
127
127
  return '';
128
- case 'number':
128
+ case 'number': {
129
+ const min = typeof typedProps.min === 'number' ? typedProps.min : undefined;
130
+ if (min !== undefined && min > 0) return min;
129
131
  return 0;
132
+ }
130
133
  case 'boolean':
131
134
  return false;
132
135
  case 'date': {
@@ -433,7 +436,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
433
436
  return undefined;
434
437
  }
435
438
  const firstType = normalizedTypes[0];
436
- return firstType ? defaultValueFor(firstType.type) : undefined;
439
+ return firstType ? defaultValueFor(firstType.type, firstType.props) : undefined;
437
440
  }, [defaultToFirstConstantTypeWhenUndefined, normalizedTypes, value, variableOnly]);
438
441
  const effectiveValue = value === undefined && defaultedValue !== undefined ? defaultedValue : value;
439
442
  const detected = useMemo(() => detectMode(effectiveValue, parseVariablePath), [effectiveValue, parseVariablePath]);
@@ -529,7 +532,8 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
529
532
  const targetType = (path[1] as TypedConstantType | undefined) ?? normalizedTypes[0]?.type;
530
533
  if (!targetType) return;
531
534
  if (detected.mode === targetType) return;
532
- onChange?.(defaultValueFor(targetType));
535
+ const target = normalizedTypes.find(({ type }) => type === targetType);
536
+ onChange?.(defaultValueFor(targetType, target?.props));
533
537
  return;
534
538
  }
535
539
  const leaf = selectedOptions?.[selectedOptions.length - 1] as SwitcherOption | undefined;
@@ -548,7 +552,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
548
552
  }
549
553
  const first = normalizedTypes[0];
550
554
  if (first) {
551
- onChange?.(defaultValueFor(first.type));
555
+ onChange?.(defaultValueFor(first.type, first.props));
552
556
  return;
553
557
  }
554
558
  if (nullable) {
@@ -585,11 +589,11 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
585
589
  if (variableOnly) {
586
590
  return undefined;
587
591
  }
588
- if (isNull) {
592
+ if (isNull && nullable) {
589
593
  return [NULL_KEY];
590
594
  }
591
595
  return [CONST_KEY, constantTypeForRendering];
592
- }, [constantTypeForRendering, detected.variablePath, isNull, isVariable, variableOnly]);
596
+ }, [constantTypeForRendering, detected.variablePath, isNull, isVariable, nullable, variableOnly]);
593
597
 
594
598
  // Preload a saved variable's label path across lazy levels. `resolveVariableLabels` can only read already-loaded
595
599
  // `children`; when a saved reference points below a node whose children are still a lazy thunk (e.g. a relation field
@@ -734,7 +738,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
734
738
  </div>
735
739
  ) : variableOnly ? (
736
740
  <Input placeholder={placeholder} readOnly disabled={disabled} style={{ width: '100%' }} />
737
- ) : isNull ? (
741
+ ) : isNull && nullable ? (
738
742
  // v1 used the `placeholder` slot (not `value`) so the antd default placeholder colour applies — keeps the
739
743
  // field looking visibly empty/inactive rather than holding a real text value.
740
744
  <Input placeholder={`<${t('Null')}>`} readOnly disabled={disabled} style={{ width: '100%' }} />
@@ -81,6 +81,27 @@ describe('TypedVariableInput - constant rendering', () => {
81
81
  expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
82
82
  });
83
83
 
84
+ it('keeps the number editor usable when value=null and nullable=false', async () => {
85
+ const ctx = createContextWithEnv();
86
+ const handleChange = vi.fn();
87
+ renderWithCtx(
88
+ ctx,
89
+ <TypedVariableInput
90
+ value={null}
91
+ types={[['number', { min: 1 }]]}
92
+ namespaces={['$env']}
93
+ nullable={false}
94
+ onChange={handleChange}
95
+ />,
96
+ );
97
+
98
+ const numberInput = await screen.findByRole('spinbutton');
99
+ expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
100
+ fireEvent.change(numberInput, { target: { value: '2' } });
101
+ fireEvent.blur(numberInput);
102
+ expect(handleChange).toHaveBeenCalledWith(2);
103
+ });
104
+
84
105
  it('defaults undefined to the first constant type', async () => {
85
106
  const ctx = createContextWithEnv();
86
107
  const handleChange = vi.fn();
@@ -102,6 +123,26 @@ describe('TypedVariableInput - constant rendering', () => {
102
123
  });
103
124
  });
104
125
 
126
+ it('uses a positive numeric minimum as the default value', async () => {
127
+ const ctx = createContextWithEnv();
128
+ const handleChange = vi.fn();
129
+ renderWithCtx(
130
+ ctx,
131
+ <TypedVariableInput
132
+ value={undefined}
133
+ types={[['number', { min: 1 }]]}
134
+ namespaces={['$env']}
135
+ nullable={false}
136
+ onChange={handleChange}
137
+ />,
138
+ );
139
+
140
+ expect(await screen.findByDisplayValue('1')).toBeInTheDocument();
141
+ await waitFor(() => {
142
+ expect(handleChange).toHaveBeenCalledWith(1);
143
+ });
144
+ });
145
+
105
146
  it('can still opt out to keep the null placeholder for undefined', async () => {
106
147
  const ctx = createContextWithEnv();
107
148
  renderWithCtx(
@@ -183,14 +224,14 @@ describe('TypedVariableInput - variable rendering', () => {
183
224
  expect(handleChange).toHaveBeenCalledWith(0);
184
225
  });
185
226
 
186
- it('clears back to default-of-first-type when nullable=false', async () => {
227
+ it('clears back to the valid minimum of the first type when nullable=false', async () => {
187
228
  const ctx = createContextWithEnv();
188
229
  const handleChange = vi.fn();
189
230
  const { container } = renderWithCtx(
190
231
  ctx,
191
232
  <TypedVariableInput
192
233
  value="{{$env.SMTP_PORT}}"
193
- types={['number']}
234
+ types={[['number', { min: 1 }]]}
194
235
  namespaces={['$env']}
195
236
  nullable={false}
196
237
  onChange={handleChange}
@@ -199,7 +240,7 @@ describe('TypedVariableInput - variable rendering', () => {
199
240
  const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
200
241
  expect(clear).not.toBeNull();
201
242
  fireEvent.click(clear as HTMLButtonElement);
202
- expect(handleChange).toHaveBeenCalledWith(0);
243
+ expect(handleChange).toHaveBeenCalledWith(1);
203
244
  });
204
245
 
205
246
  it('treats types=[] as variable-only mode with a readonly placeholder before selection', async () => {
@@ -135,11 +135,15 @@ describe('linkageRulesRefresh action', () => {
135
135
  expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-mounted'] });
136
136
  });
137
137
 
138
- it('runs linkage action on master model when forks exist in design mode', async () => {
138
+ it('skips master model when forks can handle flow in design mode', async () => {
139
139
  const handler = vi.fn(async () => {});
140
140
  const model: any = {
141
141
  isFork: false,
142
- forks: new Set([{}]),
142
+ forks: new Set([
143
+ {
144
+ getFlow: vi.fn(() => ({})),
145
+ },
146
+ ]),
143
147
  getFlow: vi.fn(() => ({})),
144
148
  getStepParams: vi.fn(() => ({ value: ['master'] })),
145
149
  context: {
@@ -157,7 +161,7 @@ describe('linkageRulesRefresh action', () => {
157
161
  flowKey: 'buttonSettings',
158
162
  });
159
163
 
160
- expect(handler).toHaveBeenCalledWith(ctx, { value: ['master'] });
164
+ expect(handler).not.toHaveBeenCalled();
161
165
  });
162
166
 
163
167
  it('runs linkage action on fork model and resolves params', async () => {
@@ -35,12 +35,8 @@ export const linkageRulesRefresh = defineAction({
35
35
  // Prefer running on the current model; fallback to blockModel when the current model doesn't own the flow.
36
36
  if (!hasFlow) return;
37
37
 
38
- // In runtime, only skip master when there are mounted forks that can handle the same flow.
38
+ // Skip master when unmounted forks can handle the same flow.
39
39
  // Otherwise master is likely the rendered model and still needs refresh.
40
- // In design mode, always refresh master so the currently edited model state stays in sync.
41
- const flowSettingsEnabled = Boolean(
42
- (ctx as any)?.flowSettingsEnabled || (model as any)?.context?.flowSettingsEnabled,
43
- );
44
40
  const hasForkWithFlow =
45
41
  !model?.isFork &&
46
42
  !!model?.forks?.size &&
@@ -49,7 +45,7 @@ export const linkageRulesRefresh = defineAction({
49
45
  return !!fork?.getFlow?.(flowKey);
50
46
  });
51
47
  const isMasterMounted = Boolean((model as any)?.context?.ref?.current);
52
- if (hasForkWithFlow && !isMasterMounted && !flowSettingsEnabled) {
48
+ if (hasForkWithFlow && !isMasterMounted) {
53
49
  return;
54
50
  }
55
51
 
@@ -42,6 +42,12 @@ import { normalizeFilterValueByOperator } from '../models/blocks/filter-form/val
42
42
  import { FieldAssignExactDatePicker, type ExactDatePickerMode } from './FieldAssignExactDatePicker';
43
43
  import { limitAssociationMetaTree } from './filter/metaTreeAssociationDepth';
44
44
 
45
+ type VariableInputConverters = {
46
+ renderInputComponent?: (metaTreeNode: MetaTreeNode | null) => React.ComponentType<any> | null;
47
+ resolvePathFromValue?: (value: any) => string[] | undefined;
48
+ resolveValueFromPath?: (metaTreeNode: MetaTreeNode) => any;
49
+ };
50
+
45
51
  const DATE_FIELD_INTERFACES = new Set(['date', 'datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp']);
46
52
 
47
53
  const TZ_AWARE_DATE_INTERFACES = new Set(['datetime', 'createdAt', 'updatedAt', 'unixTimestamp']);
@@ -345,6 +351,7 @@ interface Props {
345
351
  allowRunJS?: boolean;
346
352
  maxAssociationFieldDepth?: number;
347
353
  disabled?: boolean;
354
+ variableConverters?: VariableInputConverters;
348
355
  }
349
356
 
350
357
  type ResolvedFieldContext = {
@@ -718,6 +725,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
718
725
  allowRunJS = true,
719
726
  maxAssociationFieldDepth = 2,
720
727
  disabled = false,
728
+ variableConverters,
721
729
  }) => {
722
730
  const flowCtx = useFlowContext<FlowModelContext>();
723
731
  const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
@@ -1511,7 +1519,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1511
1519
  clearValue={''}
1512
1520
  disabled={disabled}
1513
1521
  converters={{
1522
+ ...variableConverters,
1514
1523
  renderInputComponent: (meta) => {
1524
+ const external = variableConverters?.renderInputComponent?.(meta ?? null);
1525
+ if (external) {
1526
+ return external;
1527
+ }
1515
1528
  const firstPath = meta?.paths?.[0];
1516
1529
  if (firstPath === 'constant') return ConstantEditor;
1517
1530
  if (firstPath === 'null') return NullComponent;
@@ -1519,6 +1532,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1519
1532
  return null;
1520
1533
  },
1521
1534
  resolveValueFromPath: (item) => {
1535
+ const external = variableConverters?.resolveValueFromPath?.(item);
1536
+ if (external !== undefined) {
1537
+ return external;
1538
+ }
1522
1539
  const firstPath = item?.paths?.[0];
1523
1540
  if (firstPath === 'constant') {
1524
1541
  return useDateVariableConstant ? { type: 'today' } : '';
@@ -1528,6 +1545,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1528
1545
  return undefined;
1529
1546
  },
1530
1547
  resolvePathFromValue: (currentValue) => {
1548
+ const external = variableConverters?.resolvePathFromValue?.(currentValue);
1549
+ if (external !== undefined) {
1550
+ return external;
1551
+ }
1531
1552
  if (currentValue === null) return ['null'];
1532
1553
  if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
1533
1554
  if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
@@ -12,9 +12,10 @@ import { describe, expect, it, vi } from 'vitest';
12
12
  import { render, waitFor } from '@nocobase/test/client';
13
13
  import { FieldAssignValueInput } from '../FieldAssignValueInput';
14
14
 
15
- const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
15
+ const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
16
16
  mockUseFlowContext: vi.fn(),
17
17
  mockGetDefaultBindingByField: vi.fn(),
18
+ mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
18
19
  }));
19
20
 
20
21
  vi.mock('@nocobase/flow-engine', async () => {
@@ -25,7 +26,7 @@ vi.mock('@nocobase/flow-engine', async () => {
25
26
  return {
26
27
  ...actual,
27
28
  useFlowContext: () => mockUseFlowContext(),
28
- VariableInput: () => <div data-testid="variable-input" />,
29
+ VariableInput: mockVariableInput,
29
30
  FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
30
31
  EditableItemModel: MockEditableItemModel,
31
32
  };
@@ -131,4 +132,85 @@ describe('FieldAssignValueInput context', () => {
131
132
  });
132
133
  expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
133
134
  });
135
+
136
+ it('lets callers override variable path parsing for domain-specific stored formats', async () => {
137
+ mockVariableInput.mockClear();
138
+ const sourceContext = {
139
+ dataSourceManager: {
140
+ getDataSource: vi.fn(() => ({})),
141
+ },
142
+ t: (key: string) => key,
143
+ };
144
+ const fieldModel = {
145
+ props: {},
146
+ setProps: vi.fn(),
147
+ dispatchEvent: vi.fn(),
148
+ remove: vi.fn(),
149
+ };
150
+ const tempRoot = {
151
+ context: {
152
+ defineProperty: vi.fn(),
153
+ },
154
+ subModels: {
155
+ fields: [fieldModel],
156
+ },
157
+ setProps: vi.fn(),
158
+ remove: vi.fn(),
159
+ };
160
+ const engine = {
161
+ createModel: vi.fn(() => tempRoot),
162
+ };
163
+ const collectionField = {
164
+ name: 'status',
165
+ interface: 'input',
166
+ uiSchema: { 'x-component': 'Input' },
167
+ isAssociationField: () => false,
168
+ getComponentProps: () => ({}),
169
+ };
170
+ const collection = {
171
+ dataSourceKey: 'main',
172
+ name: 'tasks',
173
+ getField: (name: string) => (name === 'status' ? collectionField : null),
174
+ getFields: () => [collectionField],
175
+ };
176
+ const formModel = {
177
+ context: { ...sourceContext, engine, collection, blockModel: null as any },
178
+ collection,
179
+ subModels: {},
180
+ };
181
+ formModel.context.blockModel = formModel;
182
+
183
+ mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
184
+ mockUseFlowContext.mockReturnValue({
185
+ model: formModel,
186
+ t: (key: string) => key,
187
+ getPropertyMetaTree: vi.fn(async () => []),
188
+ });
189
+
190
+ const variableConverters = {
191
+ resolvePathFromValue: vi.fn((value: string) =>
192
+ value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
193
+ ),
194
+ resolveValueFromPath: vi.fn(() => undefined),
195
+ };
196
+
197
+ render(
198
+ <FieldAssignValueInput
199
+ targetPath="status"
200
+ value="{{$context.data.updatedAt}}"
201
+ onChange={vi.fn()}
202
+ variableConverters={variableConverters}
203
+ />,
204
+ );
205
+
206
+ await waitFor(() => {
207
+ expect(engine.createModel).toHaveBeenCalled();
208
+ });
209
+ const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
210
+ expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
211
+ '$context',
212
+ 'data',
213
+ 'updatedAt',
214
+ ]);
215
+ });
134
216
  });
@@ -45,6 +45,7 @@ interface FilterGroupProps {
45
45
  value: Record<string, any>;
46
46
  /** 自定义筛选项组件 */
47
47
  FilterItem?: React.FC<FilterItemProps>;
48
+ disabled?: boolean;
48
49
  closeIcon?: ReactNode;
49
50
  /** 是否显示边框 */
50
51
  showBorder?: boolean;
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
89
90
  const {
90
91
  value = { logic: '$and', items: [] },
91
92
  FilterItem,
93
+ disabled = false,
92
94
  showBorder = false,
93
95
  onRemove,
94
96
  onChange,
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
120
122
  };
121
123
 
122
124
  const handleLogicChange = (newLogic: '$and' | '$or') => {
125
+ if (disabled) {
126
+ return;
127
+ }
123
128
  value.logic = newLogic;
124
129
  onChange?.(value);
125
130
  };
126
131
 
127
132
  const handleAddCondition = () => {
133
+ if (disabled) {
134
+ return;
135
+ }
128
136
  items.push({
129
137
  path: '',
130
138
  operator: '',
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
134
142
  };
135
143
 
136
144
  const handleAddConditionGroup = () => {
145
+ if (disabled) {
146
+ return;
147
+ }
137
148
  items.push({
138
149
  logic: '$and',
139
150
  items: [],
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
142
153
  };
143
154
 
144
155
  const handleRemoveItem = (index: number) => {
156
+ if (disabled) {
157
+ return;
158
+ }
145
159
  items.splice(index, 1);
146
160
  onChange?.(value);
147
161
  };
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
160
174
  <button
161
175
  type="button"
162
176
  aria-label="icon-close"
177
+ disabled={disabled}
163
178
  style={{
164
179
  position: 'absolute',
165
180
  right: 10,
166
181
  top: 10,
167
- cursor: 'pointer',
182
+ cursor: disabled ? 'not-allowed' : 'pointer',
168
183
  background: 'transparent',
169
184
  border: 0,
170
185
  padding: 0,
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
182
197
  data-testid="filter-select-all-or-any"
183
198
  style={{ width: 'auto' }}
184
199
  value={logic}
200
+ disabled={disabled}
185
201
  onChange={handleLogicChange}
186
202
  >
187
203
  <Select.Option value="$and">{t('All')}</Select.Option>
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
200
216
  key={getFilterItemKey(item)}
201
217
  value={item}
202
218
  FilterItem={FilterItem}
219
+ disabled={disabled}
203
220
  showBorder={true}
204
221
  onRemove={() => handleRemoveItem(index)}
205
222
  onChange={(v) => {
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
221
238
  <button
222
239
  type="button"
223
240
  aria-label="icon-close"
241
+ disabled={disabled}
224
242
  style={{
225
243
  marginLeft: 8,
226
244
  marginRight: 8,
227
245
  flex: '0 0 auto',
228
- cursor: 'pointer',
246
+ cursor: disabled ? 'not-allowed' : 'pointer',
229
247
  background: 'transparent',
230
248
  border: 0,
231
249
  padding: 0,
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
251
269
  <button
252
270
  type="button"
253
271
  aria-label="icon-close"
272
+ disabled={disabled}
254
273
  style={{
255
274
  marginLeft: 8,
256
275
  marginRight: 8,
257
276
  flex: '0 0 auto',
258
- cursor: 'pointer',
277
+ cursor: disabled ? 'not-allowed' : 'pointer',
259
278
  background: 'transparent',
260
279
  border: 0,
261
280
  padding: 0,
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
277
296
  <button
278
297
  type="button"
279
298
  aria-label="icon-close"
299
+ disabled={disabled}
280
300
  style={{
281
301
  marginLeft: 8,
282
302
  marginRight: 8,
283
303
  flex: '0 0 auto',
284
- cursor: 'pointer',
304
+ cursor: disabled ? 'not-allowed' : 'pointer',
285
305
  background: 'transparent',
286
306
  border: 0,
287
307
  padding: 0,
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
298
318
  </div>
299
319
 
300
320
  <Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
301
- <Button style={{ padding: 0 }} type="link" size="small" icon={<PlusOutlined />} onClick={handleAddCondition}>
321
+ <Button
322
+ style={{ padding: 0 }}
323
+ type="link"
324
+ size="small"
325
+ icon={<PlusOutlined />}
326
+ onClick={handleAddCondition}
327
+ disabled={disabled}
328
+ >
302
329
  {t('Add condition')}
303
330
  </Button>
304
331
  <Button
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
307
334
  size="small"
308
335
  icon={<PlusOutlined />}
309
336
  onClick={handleAddConditionGroup}
337
+ disabled={disabled}
310
338
  >
311
339
  {t('Add condition group')}
312
340
  </Button>