Skip to content

Commit 468c9b8

Browse files
pvillantigarashitm
authored andcommitted
feat(DataMapper): Add settings modal with XML declaration control
1 parent 57a8c9b commit 468c9b8

15 files changed

Lines changed: 1087 additions & 22 deletions

packages/ui/src/components/DataMapper/debug/ExportMappingFileModal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ interface ExportMappingFileModalProps {
1414
}
1515

1616
export const ExportMappingFileModal: FunctionComponent<ExportMappingFileModalProps> = ({ isOpen, onClose }) => {
17-
const { mappingTree, sourceParameterMap } = useDataMapper();
17+
const { mappingTree, sourceParameterMap, dataMapperSettings } = useDataMapper();
1818
const serializedMappings = useMemo(
19-
() => (isOpen ? MappingSerializerService.serialize(mappingTree, sourceParameterMap) : undefined),
20-
[isOpen, mappingTree, sourceParameterMap],
19+
() =>
20+
isOpen ? MappingSerializerService.serialize(mappingTree, sourceParameterMap, dataMapperSettings) : undefined,
21+
[isOpen, mappingTree, sourceParameterMap, dataMapperSettings],
2122
);
2223

2324
const editorHeight = useMemo(() => {

packages/ui/src/components/Document/BaseDocument.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
.document {
44
&__actions {
55
flex: 1;
6-
display: flex;
6+
display: flex !important;
7+
flex-flow: row nowrap;
78
justify-content: flex-end;
89
align-items: center;
910
padding-left: 0.5rem;
1011
padding-right: 0.5rem;
12+
gap: 0;
1113

1214
--pf-v6-c-action-list--child--spacer-base: 0.5rem;
1315
--pf-v6-c-action-list--group--spacer-base: 0.5rem;
@@ -36,6 +38,13 @@
3638
flex: 1;
3739
}
3840
}
41+
42+
/* stylelint-disable-next-line selector-class-pattern */
43+
.pf-v6-c-divider {
44+
margin: 0 var(--pf-t--global--spacer--xs);
45+
align-self: center;
46+
height: calc(var(--datamapper-header-icon-size, 0.75rem) + 0.25rem);
47+
}
3948
}
4049
}
4150

packages/ui/src/components/Document/BaseDocument.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './BaseDocument.scss';
22

33
import { Draggable } from '@carbon/icons-react';
4-
import { ActionListGroup, ActionListItem, Icon } from '@patternfly/react-core';
4+
import { ActionListGroup, ActionListItem, Divider, Icon } from '@patternfly/react-core';
55
import clsx from 'clsx';
66
import { FunctionComponent, KeyboardEvent, MouseEvent, ReactNode, useCallback, useMemo } from 'react';
77

@@ -12,6 +12,7 @@ import { DocumentNodeData } from '../../models/datamapper/visualization';
1212
import { useDocumentTreeStore } from '../../store';
1313
import { AttachSchemaButton } from './actions/AttachSchema';
1414
import { DetachSchemaButton } from './actions/DetachSchemaButton';
15+
import { DataMapperSettingsButton } from './actions/Settings/DataMapperSettingsButton';
1516
import { NodeContainer } from './NodeContainer';
1617

1718
// ============================================================================
@@ -50,6 +51,7 @@ type DocumentHeaderProps = {
5051
additionalActions?: ReactNode[];
5152
enableDnD?: boolean;
5253
nodeData?: DocumentNodeData; // Optional: use existing nodeData instead of creating new one
54+
showSettingsButton?: boolean;
5355
};
5456

5557
/**
@@ -65,6 +67,7 @@ export const DocumentHeader: FunctionComponent<DocumentHeaderProps> = ({
6567
additionalActions = [],
6668
enableDnD = false,
6769
nodeData: externalNodeData,
70+
showSettingsButton = false,
6871
}) => {
6972
const { mappingTree } = useDataMapper();
7073
const toggleSelectedNode = useDocumentTreeStore((state) => state.toggleSelectedNode);
@@ -128,6 +131,15 @@ export const DocumentHeader: FunctionComponent<DocumentHeaderProps> = ({
128131
documentReferenceId={documentReferenceId}
129132
/>
130133
</ActionListItem>
134+
135+
{showSettingsButton && (
136+
<>
137+
<Divider orientation={{ default: 'vertical' }} />
138+
<ActionListItem key="datamapper-settings">
139+
<DataMapperSettingsButton />
140+
</ActionListItem>
141+
</>
142+
)}
131143
</ActionListGroup>
132144
)}
133145
</div>

packages/ui/src/components/Document/Parameters.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
/* stylelint-disable-next-line selector-class-pattern */
2424
.pf-v6-c-divider {
2525
margin: 0 var(--pf-t--global--spacer--xs);
26+
align-self: center;
27+
height: calc(var(--datamapper-header-icon-size, 0.75rem) + 0.25rem);
2628
}
2729
}
2830
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { act, render, screen } from '@testing-library/react';
2+
import { FunctionComponent, PropsWithChildren } from 'react';
3+
4+
import { DocumentDefinitionType, DocumentInitializationModel, DocumentType } from '../../../../models/datamapper';
5+
import { DataMapperProvider } from '../../../../providers/datamapper.provider';
6+
import { DataMapperSettingsButton } from './DataMapperSettingsButton';
7+
8+
describe('DataMapperSettingsButton', () => {
9+
const createWrapper = (): FunctionComponent<PropsWithChildren> => {
10+
return ({ children }) => (
11+
<DataMapperProvider
12+
documentInitializationModel={
13+
new DocumentInitializationModel(
14+
{},
15+
{
16+
documentType: DocumentType.SOURCE_BODY,
17+
definitionType: DocumentDefinitionType.XML_SCHEMA,
18+
name: 'source',
19+
},
20+
{
21+
documentType: DocumentType.TARGET_BODY,
22+
definitionType: DocumentDefinitionType.XML_SCHEMA,
23+
name: 'target',
24+
},
25+
)
26+
}
27+
>
28+
{children}
29+
</DataMapperProvider>
30+
);
31+
};
32+
33+
describe('Button Rendering', () => {
34+
it('should render settings button', () => {
35+
const wrapper = createWrapper();
36+
render(<DataMapperSettingsButton />, { wrapper });
37+
38+
const button = screen.getByTestId('datamapper-settings-button');
39+
expect(button).toBeInTheDocument();
40+
});
41+
42+
it('should have correct aria-label', () => {
43+
const wrapper = createWrapper();
44+
render(<DataMapperSettingsButton />, { wrapper });
45+
46+
const button = screen.getByTestId('datamapper-settings-button');
47+
expect(button).toHaveAttribute('aria-label', 'Settings');
48+
});
49+
50+
it('should have correct title', () => {
51+
const wrapper = createWrapper();
52+
render(<DataMapperSettingsButton />, { wrapper });
53+
54+
const button = screen.getByTestId('datamapper-settings-button');
55+
expect(button).toHaveAttribute('title', 'DataMapper Settings');
56+
});
57+
58+
it('should render with plain variant', () => {
59+
const wrapper = createWrapper();
60+
render(<DataMapperSettingsButton />, { wrapper });
61+
62+
const button = screen.getByTestId('datamapper-settings-button');
63+
expect(button).toHaveClass('pf-m-plain');
64+
});
65+
66+
it('should render cog icon', () => {
67+
const wrapper = createWrapper();
68+
render(<DataMapperSettingsButton />, { wrapper });
69+
70+
const button = screen.getByTestId('datamapper-settings-button');
71+
const icon = button.querySelector('svg');
72+
expect(icon).toBeInTheDocument();
73+
});
74+
});
75+
76+
describe('Modal Interaction', () => {
77+
it('should not render modal initially', () => {
78+
const wrapper = createWrapper();
79+
render(<DataMapperSettingsButton />, { wrapper });
80+
81+
expect(screen.queryByTestId('datamapper-settings-modal')).not.toBeInTheDocument();
82+
});
83+
84+
it('should open modal when button is clicked', async () => {
85+
const wrapper = createWrapper();
86+
render(<DataMapperSettingsButton />, { wrapper });
87+
88+
const button = screen.getByTestId('datamapper-settings-button');
89+
90+
act(() => {
91+
button.click();
92+
});
93+
94+
await screen.findByTestId('datamapper-settings-modal');
95+
expect(screen.getByText('DataMapper Settings')).toBeInTheDocument();
96+
});
97+
98+
it('should close modal when cancel button is clicked', async () => {
99+
const wrapper = createWrapper();
100+
render(<DataMapperSettingsButton />, { wrapper });
101+
102+
const button = screen.getByTestId('datamapper-settings-button');
103+
104+
// Open modal
105+
act(() => {
106+
button.click();
107+
});
108+
109+
await screen.findByTestId('datamapper-settings-modal');
110+
111+
// Close modal
112+
const cancelButton = screen.getByTestId('datamapper-settings-cancel-btn');
113+
act(() => {
114+
cancelButton.click();
115+
});
116+
117+
expect(screen.queryByTestId('datamapper-settings-modal')).not.toBeInTheDocument();
118+
});
119+
120+
it('should close modal when save button is clicked', async () => {
121+
const wrapper = createWrapper();
122+
render(<DataMapperSettingsButton />, { wrapper });
123+
124+
const button = screen.getByTestId('datamapper-settings-button');
125+
126+
// Open modal
127+
act(() => {
128+
button.click();
129+
});
130+
131+
await screen.findByTestId('datamapper-settings-modal');
132+
133+
// Close modal
134+
const saveButton = screen.getByTestId('datamapper-settings-save-btn');
135+
act(() => {
136+
saveButton.click();
137+
});
138+
139+
expect(screen.queryByTestId('datamapper-settings-modal')).not.toBeInTheDocument();
140+
});
141+
142+
it('should close modal when close button is clicked', async () => {
143+
const wrapper = createWrapper();
144+
render(<DataMapperSettingsButton />, { wrapper });
145+
146+
const button = screen.getByTestId('datamapper-settings-button');
147+
148+
// Open modal
149+
act(() => {
150+
button.click();
151+
});
152+
153+
await screen.findByTestId('datamapper-settings-modal');
154+
155+
// Close modal
156+
const closeButton = screen.getByLabelText('Close');
157+
act(() => {
158+
closeButton.click();
159+
});
160+
161+
expect(screen.queryByTestId('datamapper-settings-modal')).not.toBeInTheDocument();
162+
});
163+
164+
it('should be able to reopen modal after closing', async () => {
165+
const wrapper = createWrapper();
166+
render(<DataMapperSettingsButton />, { wrapper });
167+
168+
const button = screen.getByTestId('datamapper-settings-button');
169+
170+
// Open modal
171+
act(() => {
172+
button.click();
173+
});
174+
await screen.findByTestId('datamapper-settings-modal');
175+
176+
// Close modal
177+
const cancelButton = screen.getByTestId('datamapper-settings-cancel-btn');
178+
act(() => {
179+
cancelButton.click();
180+
});
181+
expect(screen.queryByTestId('datamapper-settings-modal')).not.toBeInTheDocument();
182+
183+
// Reopen modal
184+
act(() => {
185+
button.click();
186+
});
187+
await screen.findByTestId('datamapper-settings-modal');
188+
expect(screen.getByText('DataMapper Settings')).toBeInTheDocument();
189+
});
190+
});
191+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Settings } from '@carbon/icons-react';
2+
import { Button } from '@patternfly/react-core';
3+
import { FunctionComponent, useCallback, useState } from 'react';
4+
5+
import { DataMapperSettingsModal } from './DataMapperSettingsModal';
6+
7+
export const DataMapperSettingsButton: FunctionComponent = () => {
8+
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
9+
10+
const onModalOpen = useCallback(() => {
11+
setIsModalOpen(true);
12+
}, []);
13+
14+
const onModalClose = useCallback(() => {
15+
setIsModalOpen(false);
16+
}, []);
17+
18+
return (
19+
<>
20+
<Button
21+
icon={<Settings />}
22+
variant="plain"
23+
title="DataMapper Settings"
24+
aria-label="Settings"
25+
data-testid="datamapper-settings-button"
26+
onClick={onModalOpen}
27+
/>
28+
29+
<DataMapperSettingsModal isModalOpen={isModalOpen} onModalClose={onModalClose} />
30+
</>
31+
);
32+
};

0 commit comments

Comments
 (0)