Skip to content

Commit 6ca23a4

Browse files
feat(backend/frontend): split CE/EE workflows features (#16388)
Co-authored-by: Florian Delemarre <florian.delemarre@gmail.com>
1 parent fa5c04e commit 6ca23a4

10 files changed

Lines changed: 384 additions & 32 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React from 'react';
2+
import { beforeEach, describe, it, expect, vi } from 'vitest';
3+
import { screen } from '@testing-library/react';
4+
import { Formik, Form } from 'formik';
5+
import StatusForm from './StatusForm';
6+
import testRender from '../../../../../utils/tests/test-render';
7+
import { WorkflowActionType } from './utils';
8+
import type { WorkflowEditionFormValues } from './WorkflowEditionDrawer';
9+
import useEnterpriseEdition from '../../../../../utils/hooks/useEnterpriseEdition';
10+
11+
// ---------------------------------------------------------------------------
12+
// Mocks
13+
// ---------------------------------------------------------------------------
14+
vi.mock('./WorkflowFieldList', () => ({
15+
default: () => <div data-testid="workflow-field-list" />,
16+
}));
17+
18+
vi.mock('@components/common/form/StatusTemplateField', () => ({
19+
default: () => <div data-testid="status-template-field" />,
20+
}));
21+
22+
vi.mock('../../../../../utils/hooks/useEnterpriseEdition', () => ({
23+
default: vi.fn(),
24+
}));
25+
26+
vi.mock('../../../common/entreprise_edition/EEChip', () => ({
27+
default: () => <span data-testid="ee-chip" />,
28+
}));
29+
30+
// ---------------------------------------------------------------------------
31+
// Helper
32+
// ---------------------------------------------------------------------------
33+
const renderForm = (initialValues: Partial<WorkflowEditionFormValues>, onSubmit = vi.fn()) => {
34+
return testRender(
35+
<Formik initialValues={initialValues as WorkflowEditionFormValues} onSubmit={onSubmit}>
36+
<Form>
37+
<StatusForm />
38+
</Form>
39+
</Formik>,
40+
);
41+
};
42+
43+
// ---------------------------------------------------------------------------
44+
// Tests
45+
// ---------------------------------------------------------------------------
46+
describe('StatusForm – EE / CE gating', () => {
47+
const onEnterAction = [{ type: WorkflowActionType.updateAuthorizedMembers, params: { authorized_members: [] } }];
48+
const onExitAction = [{ type: WorkflowActionType.updateAuthorizedMembers, params: { authorized_members: [] } }];
49+
50+
describe('in Community Edition', () => {
51+
beforeEach(() => {
52+
vi.mocked(useEnterpriseEdition).mockReturnValue(false);
53+
});
54+
55+
it('disables the "Update authorized members on enter" switch', () => {
56+
renderForm({ onEnter: [], onExit: [] });
57+
const sw = screen.getByRole('checkbox', { name: /update authorized members on enter/i }) as HTMLInputElement;
58+
expect(sw.disabled).toBe(true);
59+
});
60+
61+
it('disables the "Update authorized members on exit" switch', () => {
62+
renderForm({ onEnter: [], onExit: [] });
63+
const sw = screen.getByRole('checkbox', { name: /update authorized members on exit/i }) as HTMLInputElement;
64+
expect(sw.disabled).toBe(true);
65+
});
66+
67+
it('renders EEChip labels on both action sections', () => {
68+
renderForm({ onEnter: [], onExit: [] });
69+
// At minimum, both section headers contain "EE" marker text
70+
expect(screen.getByText(/on enter actions/i)).toBeDefined();
71+
expect(screen.getByText(/on exit actions/i)).toBeDefined();
72+
// The switches must remain disabled
73+
expect((screen.getByRole('checkbox', { name: /on enter/i }) as HTMLInputElement).disabled).toBe(true);
74+
});
75+
});
76+
77+
describe('in Enterprise Edition', () => {
78+
beforeEach(() => {
79+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
80+
});
81+
82+
it('enables the "Update authorized members on enter" switch', () => {
83+
renderForm({ onEnter: [], onExit: [] });
84+
const sw = screen.getByRole('checkbox', { name: /update authorized members on enter/i }) as HTMLInputElement;
85+
expect(sw.disabled).toBe(false);
86+
});
87+
88+
it('enables the "Update authorized members on exit" switch', () => {
89+
renderForm({ onEnter: [], onExit: [] });
90+
const sw = screen.getByRole('checkbox', { name: /update authorized members on exit/i }) as HTMLInputElement;
91+
expect(sw.disabled).toBe(false);
92+
});
93+
94+
it('shows WorkflowFieldList for onEnter when action is toggled on', () => {
95+
renderForm({ onEnter: onEnterAction, onExit: [] });
96+
expect(screen.getAllByTestId('workflow-field-list').length).toBeGreaterThanOrEqual(1);
97+
});
98+
99+
it('shows WorkflowFieldList for onExit when action is toggled on', () => {
100+
renderForm({ onEnter: [], onExit: onExitAction });
101+
expect(screen.getAllByTestId('workflow-field-list').length).toBeGreaterThanOrEqual(1);
102+
});
103+
});
104+
105+
describe('switch checked state reflects form values', () => {
106+
beforeEach(() => {
107+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
108+
});
109+
110+
it('"on enter" switch is unchecked when onEnter is empty', () => {
111+
renderForm({ onEnter: [], onExit: [] });
112+
const sw = screen.getByRole('checkbox', { name: /update authorized members on enter/i }) as HTMLInputElement;
113+
expect(sw.checked).toBe(false);
114+
});
115+
116+
it('"on enter" switch is checked when onEnter contains updateAuthorizedMembers', () => {
117+
renderForm({ onEnter: onEnterAction, onExit: [] });
118+
const sw = screen.getByRole('checkbox', { name: /update authorized members on enter/i }) as HTMLInputElement;
119+
expect(sw.checked).toBe(true);
120+
});
121+
122+
it('"on exit" switch is unchecked when onExit is empty', () => {
123+
renderForm({ onEnter: [], onExit: [] });
124+
const sw = screen.getByRole('checkbox', { name: /update authorized members on exit/i }) as HTMLInputElement;
125+
expect(sw.checked).toBe(false);
126+
});
127+
128+
it('"on exit" switch is checked when onExit contains updateAuthorizedMembers', () => {
129+
renderForm({ onEnter: [], onExit: onExitAction });
130+
const sw = screen.getByRole('checkbox', { name: /update authorized members on exit/i }) as HTMLInputElement;
131+
expect(sw.checked).toBe(true);
132+
});
133+
});
134+
});

opencti-platform/opencti-front/src/private/components/settings/sub_types/workflow/StatusForm.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import StatusTemplateField from '@components/common/form/StatusTemplateField';
44
import { WorkflowEditionFormValues } from './WorkflowEditionDrawer';
55
import { Box, FormControlLabel, Switch, Typography } from '@mui/material';
66
import WorkflowFieldList from './WorkflowFieldList';
7-
import { WorkflowActionType, WorkflowDataType } from './utils';
7+
import { FEATURE_NAME, WorkflowActionType, WorkflowDataType } from './utils';
8+
import useEnterpriseEdition from '../../../../../utils/hooks/useEnterpriseEdition';
9+
import EEChip from '../../../common/entreprise_edition/EEChip';
810

911
const StatusForm = () => {
1012
const { t_i18n } = useFormatter();
13+
const isEnterpriseEdition = useEnterpriseEdition();
1114
const { values, setFieldValue } = useFormikContext<WorkflowEditionFormValues>();
1215
const hasUpdateAuthorizedMembersOnEnter = values.onEnter?.some((a) => a.type === WorkflowActionType.updateAuthorizedMembers);
1316
const hasUpdateAuthorizedMembersOnExit = values.onExit?.some((a) => a.type === WorkflowActionType.updateAuthorizedMembers);
@@ -30,29 +33,31 @@ const StatusForm = () => {
3033
setFieldValue={(field, { value, label, color }) => setFieldValue(field, { id: value, name: label, color })}
3134
helpertext=""
3235
/>
33-
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, marginTop: 2 }}>
36+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, marginTop: 1 }}>
3437
<Typography variant="h6">
35-
{t_i18n('On enter actions')}
38+
{t_i18n('On enter actions')} <EEChip feature={t_i18n(FEATURE_NAME)} />
3639
</Typography>
3740
<FormControlLabel
3841
control={(
3942
<Switch
4043
checked={hasUpdateAuthorizedMembersOnEnter}
44+
disabled={!isEnterpriseEdition}
4145
onChange={(e) => handleToggleUpdateAuthorizedMembers('onEnter', e.target.checked)}
4246
/>
4347
)}
4448
label={t_i18n('Update authorized members on enter')}
4549
/>
4650
{hasUpdateAuthorizedMembersOnEnter && <WorkflowFieldList name={WorkflowDataType.onEnter} />}
4751
</Box>
48-
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, marginTop: 2 }}>
52+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, marginTop: 1 }}>
4953
<Typography variant="h6">
50-
{t_i18n('On exit actions')}
54+
{t_i18n('On exit actions')} <EEChip feature={t_i18n(FEATURE_NAME)} />
5155
</Typography>
5256
<FormControlLabel
5357
control={(
5458
<Switch
5559
checked={hasUpdateAuthorizedMembersOnExit}
60+
disabled={!isEnterpriseEdition}
5661
onChange={(e) => handleToggleUpdateAuthorizedMembers('onExit', e.target.checked)}
5762
/>
5863
)}

opencti-platform/opencti-front/src/private/components/settings/sub_types/workflow/TransitionForm.test.tsx

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
2-
import { describe, it, expect, vi } from 'vitest';
2+
import { beforeEach, describe, it, expect, vi } from 'vitest';
33
import { screen, waitFor } from '@testing-library/react';
44
import { Formik, Form } from 'formik';
55
import TransitionForm from './TransitionForm';
66
import testRender from '../../../../../utils/tests/test-render';
77
import { WorkflowActionType, CommentMode } from './utils';
88
import type { WorkflowEditionFormValues } from './WorkflowEditionDrawer';
99
import type { FilterGroup } from '../../../../../utils/filters/filtersHelpers-types';
10+
import useEnterpriseEdition from '../../../../../utils/hooks/useEnterpriseEdition';
1011

1112
// ---------------------------------------------------------------------------
1213
// Mock heavy sub-components with no relevance to the tested logic
@@ -23,6 +24,18 @@ vi.mock('../../../../../components/TextField', () => ({
2324
default: ({ field }: { field: { name: string } }) => <input data-testid={`field-${field.name}`} />,
2425
}));
2526

27+
vi.mock('../../../../../utils/hooks/useEnterpriseEdition', () => ({
28+
default: vi.fn(),
29+
}));
30+
31+
vi.mock('../../../common/entreprise_edition/EEChip', () => ({
32+
default: () => <span data-testid="ee-chip" />,
33+
}));
34+
35+
vi.mock('../../../common/form/ObjectOrganizationField', () => ({
36+
default: () => <div data-testid="object-organization-field" />,
37+
}));
38+
2639
// ---------------------------------------------------------------------------
2740
// Helper: render TransitionForm inside a Formik context
2841
// ---------------------------------------------------------------------------
@@ -40,6 +53,10 @@ const renderForm = (initialValues: Partial<WorkflowEditionFormValues>, onSubmit
4053
// Tests
4154
// ---------------------------------------------------------------------------
4255
describe('TransitionForm – comment section', () => {
56+
beforeEach(() => {
57+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
58+
});
59+
4360
it('renders "Enable comment" switch unchecked when comment is "disable"', () => {
4461
renderForm({ event: 'approve', comment: CommentMode.disabled, syncActions: [] });
4562
const enableSwitch = screen.getByRole('checkbox', { name: /enable comment/i });
@@ -146,6 +163,10 @@ describe('TransitionForm – comment section', () => {
146163
});
147164

148165
describe('TransitionForm – action toggles', () => {
166+
beforeEach(() => {
167+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
168+
});
169+
149170
it('"Update authorized members" switch is unchecked when action is absent', () => {
150171
renderForm({ event: 'approve', comment: CommentMode.disabled, syncActions: [] });
151172
const uamSwitch = screen.getByRole('checkbox', { name: /update authorized members/i });
@@ -256,6 +277,10 @@ describe('TransitionForm – action toggles', () => {
256277
});
257278

258279
describe('TransitionForm – rendering', () => {
280+
beforeEach(() => {
281+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
282+
});
283+
259284
it('renders the transition name field', () => {
260285
renderForm({ event: 'approve', comment: CommentMode.disabled, syncActions: [] });
261286
expect(screen.getByTestId('field-event')).toBeDefined();
@@ -282,3 +307,78 @@ describe('TransitionForm – rendering', () => {
282307
expect(screen.getByTestId('workflow-field-list')).toBeDefined();
283308
});
284309
});
310+
311+
// ---------------------------------------------------------------------------
312+
// EE / CE gating
313+
// ---------------------------------------------------------------------------
314+
describe('TransitionForm – EE / CE gating', () => {
315+
const emptyFilterGroup: FilterGroup = { mode: 'and', filters: [], filterGroups: [] };
316+
const eeActions = [{ type: WorkflowActionType.updateAuthorizedMembers, params: { authorized_members: [] } }];
317+
318+
it('disables EE-only switches in CE', () => {
319+
vi.mocked(useEnterpriseEdition).mockReturnValue(false);
320+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: [], asyncActions: [] });
321+
322+
expect((screen.getByRole('checkbox', { name: /update authorized members/i }) as HTMLInputElement).disabled).toBe(true);
323+
expect((screen.getByRole('checkbox', { name: /share with organizations/i }) as HTMLInputElement).disabled).toBe(true);
324+
expect((screen.getByRole('checkbox', { name: /unshare from organizations/i }) as HTMLInputElement).disabled).toBe(true);
325+
});
326+
327+
it('enables EE-only switches in EE', () => {
328+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
329+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: [], asyncActions: [] });
330+
331+
expect((screen.getByRole('checkbox', { name: /update authorized members/i }) as HTMLInputElement).disabled).toBe(false);
332+
expect((screen.getByRole('checkbox', { name: /share with organizations/i }) as HTMLInputElement).disabled).toBe(false);
333+
expect((screen.getByRole('checkbox', { name: /unshare from organizations/i }) as HTMLInputElement).disabled).toBe(false);
334+
});
335+
336+
it('"Validate draft" switch is always enabled regardless of EE status', () => {
337+
vi.mocked(useEnterpriseEdition).mockReturnValue(false);
338+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: [] });
339+
340+
expect((screen.getByRole('checkbox', { name: /validate draft/i }) as HTMLInputElement).disabled).toBe(false);
341+
});
342+
343+
it('renders the conditions block with pointer-events:none in CE', () => {
344+
vi.mocked(useEnterpriseEdition).mockReturnValue(false);
345+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: [], conditions: { filters: emptyFilterGroup } });
346+
347+
const conditionFilters = screen.getByTestId('workflow-condition-filters');
348+
const wrapper = conditionFilters.parentElement!;
349+
expect(wrapper.style.pointerEvents).toBe('none');
350+
expect(wrapper.style.opacity).toBe('0.5');
351+
});
352+
353+
it('renders the conditions block without pointer-events restriction in EE', () => {
354+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
355+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: [], conditions: { filters: emptyFilterGroup } });
356+
357+
const conditionFilters = screen.getByTestId('workflow-condition-filters');
358+
const wrapper = conditionFilters.parentElement!;
359+
// 'auto' is CSS default so it serializes as '' in jsdom; just confirm it's not 'none'
360+
expect(wrapper.style.pointerEvents).not.toBe('none');
361+
expect(wrapper.style.opacity).not.toBe('0.5');
362+
});
363+
364+
it('renders the WorkflowFieldList with pointer-events:none in CE when EE actions exist', () => {
365+
vi.mocked(useEnterpriseEdition).mockReturnValue(false);
366+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: eeActions });
367+
368+
const fieldList = screen.getByTestId('workflow-field-list');
369+
const wrapper = fieldList.parentElement!;
370+
expect(wrapper.style.pointerEvents).toBe('none');
371+
expect(wrapper.style.opacity).toBe('0.5');
372+
});
373+
374+
it('renders the WorkflowFieldList without pointer-events restriction in EE', () => {
375+
vi.mocked(useEnterpriseEdition).mockReturnValue(true);
376+
renderForm({ event: 'close', comment: CommentMode.disabled, syncActions: eeActions });
377+
378+
const fieldList = screen.getByTestId('workflow-field-list');
379+
const wrapper = fieldList.parentElement!;
380+
// 'auto' is CSS default so it serializes as '' in jsdom; just confirm it's not 'none'
381+
expect(wrapper.style.pointerEvents).not.toBe('none');
382+
expect(wrapper.style.opacity).not.toBe('0.5');
383+
});
384+
});

0 commit comments

Comments
 (0)