Skip to content

Commit 9f16041

Browse files
authored
feat: Deleted/Added level diff in sync modal [FC-0097] (#2549)
- Adds the Deleted and the Added level diff in sync container modal. - Fix the icons in the sync container modal
1 parent 82a3c28 commit 9f16041

File tree

12 files changed

+145
-46
lines changed

12 files changed

+145
-46
lines changed

src/container-comparison/CompareContainersWidget.test.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ describe('CompareContainersWidget', () => {
7171
expect(await screen.findByRole('button', { name: 'subsection block 00' })).toBeInTheDocument();
7272
expect(await screen.findByRole('button', { name: 'subsection block 0' })).toBeInTheDocument();
7373

74-
const removedRows = await screen.findAllByText('This unit was removed');
75-
// clicking on removed or added rows does not updated the page.
76-
await user.click(removedRows[0]);
77-
// Still in same page
78-
expect(await screen.findByRole('button', { name: 'subsection block 00' })).toBeInTheDocument();
79-
expect(await screen.findByRole('button', { name: 'subsection block 0' })).toBeInTheDocument();
80-
8174
// Back breadcrumb
8275
const backbtns = await screen.findAllByRole('button', { name: 'Back' });
8376
expect(backbtns.length).toEqual(2);
@@ -94,6 +87,47 @@ describe('CompareContainersWidget', () => {
9487
expect(await screen.findByRole('button', { name: 'subsection block 0' })).toBeInTheDocument();
9588
});
9689

90+
test('should show removed container diff state', async () => {
91+
// mocks title
92+
axiosMock.onGet(getLibraryContainerApiUrl(mockGetContainerMetadata.sectionId)).reply(200, { publishedDisplayName: 'Test Title' });
93+
axiosMock.onGet(
94+
getLibraryContainerApiUrl('lct:org1:Demo_course_generated:subsection:subsection-0'),
95+
).reply(200, { publishedDisplayName: 'subsection block 0' });
96+
97+
const user = userEvent.setup();
98+
render(<CompareContainersWidget
99+
upstreamBlockId={mockGetContainerMetadata.sectionId}
100+
downstreamBlockId={mockGetCourseContainerChildren.sectionId}
101+
/>);
102+
expect((await screen.findAllByText('Test Title')).length).toEqual(2);
103+
// left i.e. before side block
104+
const block = await screen.findByText('subsection block 00');
105+
await user.click(block);
106+
107+
const removedRows = await screen.findAllByText('This unit was removed');
108+
await user.click(removedRows[0]);
109+
110+
expect(await screen.findByText('This unit has been removed')).toBeInTheDocument();
111+
});
112+
113+
test('should show new added container diff state', async () => {
114+
// mocks title
115+
axiosMock.onGet(getLibraryContainerApiUrl(mockGetContainerMetadata.sectionId)).reply(200, { publishedDisplayName: 'Test Title' });
116+
axiosMock.onGet(
117+
getLibraryContainerApiUrl('lct:org1:Demo_course_generated:subsection:subsection-0'),
118+
).reply(200, { publishedDisplayName: 'subsection block 0' });
119+
120+
const user = userEvent.setup();
121+
render(<CompareContainersWidget
122+
upstreamBlockId={mockGetContainerMetadata.sectionId}
123+
downstreamBlockId="block-v1:UNIX+UX1+2025_T3+type@section+block@0-new"
124+
/>);
125+
const blocks = await screen.findAllByText('This subsection will be added in the new version');
126+
await user.click(blocks[0]);
127+
128+
expect(await screen.findByText(/this subsection is new/i)).toBeInTheDocument();
129+
});
130+
97131
test('should show alert if the only change is a single text component with local overrides', async () => {
98132
const url = getLibraryContainerApiUrl(mockGetContainerMetadata.sectionId);
99133
axiosMock.onGet(url).reply(200, { publishedDisplayName: 'Test Title' });

src/container-comparison/CompareContainersWidget.tsx

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import {
44
Alert,
55
Breadcrumb, Button, Card, Icon, Stack,
66
} from '@openedx/paragon';
7-
import { ArrowBack } from '@openedx/paragon/icons';
7+
import { ArrowBack, Add, Delete } from '@openedx/paragon/icons';
88
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
99

10-
import { ContainerType } from '@src/generic/key-utils';
10+
import { ContainerType, getBlockType } from '@src/generic/key-utils';
1111
import ErrorAlert from '@src/generic/alert-error';
1212
import { LoadingSpinner } from '@src/generic/Loading';
1313
import { useContainer, useContainerChildren } from '@src/library-authoring/data/apiHooks';
@@ -16,7 +16,9 @@ import { BoldText } from '@src/utils';
1616
import ChildrenPreview from './ChildrenPreview';
1717
import ContainerRow from './ContainerRow';
1818
import { useCourseContainerChildren } from './data/apiHooks';
19-
import { ContainerChild, ContainerChildBase, WithState } from './types';
19+
import {
20+
ContainerChild, ContainerChildBase, ContainerState, WithState,
21+
} from './types';
2022
import { diffPreviewContainerChildren, isRowClickable } from './utils';
2123
import messages from './messages';
2224

@@ -30,6 +32,7 @@ interface Props extends ContainerInfoProps {
3032
parent: ContainerInfoProps[];
3133
onRowClick: (row: WithState<ContainerChild>) => void;
3234
onBackBtnClick: () => void;
35+
state?: ContainerState;
3336
// This two props are used to show an alert for the changes to text components with local overrides.
3437
// They may be removed in the future.
3538
localUpdateAlertCount: number;
@@ -43,36 +46,54 @@ const CompareContainersWidgetInner = ({
4346
upstreamBlockId,
4447
downstreamBlockId,
4548
parent,
49+
state,
4650
onRowClick,
4751
onBackBtnClick,
4852
localUpdateAlertCount,
4953
localUpdateAlertBlockName,
5054
}: Props) => {
5155
const intl = useIntl();
5256
const { data, isError, error } = useCourseContainerChildren(downstreamBlockId, parent.length === 0);
57+
// There is the case in which the item is removed, but it still exists
58+
// in the library, for that case, we avoid bringing the children.
5359
const {
5460
data: libData,
5561
isError: isLibError,
5662
error: libError,
57-
} = useContainerChildren(upstreamBlockId, true);
63+
} = useContainerChildren(state === 'removed' ? undefined : upstreamBlockId, true);
5864
const {
5965
data: containerData,
6066
isError: isContainerTitleError,
6167
error: containerTitleError,
6268
} = useContainer(upstreamBlockId);
6369

6470
const result = useMemo(() => {
65-
if (!data || !libData) {
71+
if ((!data || !libData) && !['added', 'removed'].includes(state || '')) {
6672
return [undefined, undefined];
6773
}
68-
return diffPreviewContainerChildren(data.children, libData as ContainerChildBase[]);
74+
75+
return diffPreviewContainerChildren(data?.children || [], libData as ContainerChildBase[] || []);
6976
}, [data, libData]);
7077

7178
const renderBeforeChildren = useCallback(() => {
72-
if (!result[0]) {
79+
if (!result[0] && state !== 'added') {
7380
return <div className="m-auto"><LoadingSpinner /></div>;
7481
}
7582

83+
if (state === 'added') {
84+
return (
85+
<Stack className="align-items-center justify-content-center bg-light-200 text-gray-800">
86+
<Icon src={Add} className="big-icon" />
87+
<FormattedMessage
88+
{...messages.newContainer}
89+
values={{
90+
containerType: getBlockType(upstreamBlockId),
91+
}}
92+
/>
93+
</Stack>
94+
);
95+
}
96+
7697
return result[0]?.map((child) => (
7798
<ContainerRow
7899
key={child.id}
@@ -87,10 +108,24 @@ const CompareContainersWidgetInner = ({
87108
}, [result]);
88109

89110
const renderAfterChildren = useCallback(() => {
90-
if (!result[1]) {
111+
if (!result[1] && state !== 'removed') {
91112
return <div className="m-auto"><LoadingSpinner /></div>;
92113
}
93114

115+
if (state === 'removed') {
116+
return (
117+
<Stack className="align-items-center justify-content-center bg-light-200 text-gray-800">
118+
<Icon src={Delete} className="big-icon" />
119+
<FormattedMessage
120+
{...messages.deletedContainer}
121+
values={{
122+
containerType: getBlockType(upstreamBlockId),
123+
}}
124+
/>
125+
</Stack>
126+
);
127+
}
128+
94129
return result[1]?.map((child) => (
95130
<ContainerRow
96131
key={child.id}
@@ -134,12 +169,21 @@ const CompareContainersWidgetInner = ({
134169
);
135170
}, [parent]);
136171

137-
if (isError || isLibError || isContainerTitleError) {
172+
let beforeTitle: string | undefined | null = data?.displayName;
173+
let afterTitle = containerData?.publishedDisplayName;
174+
if (!data && state === 'added') {
175+
beforeTitle = containerData?.publishedDisplayName;
176+
}
177+
if (!containerData && state === 'removed') {
178+
afterTitle = data?.displayName;
179+
}
180+
181+
if (isError || (isLibError && state !== 'removed') || (isContainerTitleError && state !== 'removed')) {
138182
return <ErrorAlert error={error || libError || containerTitleError} />;
139183
}
140184

141185
return (
142-
<div className="row justify-content-center">
186+
<div className="compare-changes-widget row justify-content-center">
143187
{localUpdateAlertCount > 0 && (
144188
<Alert variant="info">
145189
<FormattedMessage
@@ -153,15 +197,15 @@ const CompareContainersWidgetInner = ({
153197
</Alert>
154198
)}
155199
<div className="col col-6 p-1">
156-
<Card className="p-4">
157-
<ChildrenPreview title={getTitleComponent(data?.displayName)} side="Before">
200+
<Card className="compare-card p-4">
201+
<ChildrenPreview title={getTitleComponent(beforeTitle)} side="Before">
158202
{renderBeforeChildren()}
159203
</ChildrenPreview>
160204
</Card>
161205
</div>
162206
<div className="col col-6 p-1">
163-
<Card className="p-4">
164-
<ChildrenPreview title={getTitleComponent(containerData?.publishedDisplayName)} side="After">
207+
<Card className="compare-card p-4">
208+
<ChildrenPreview title={getTitleComponent(afterTitle)} side="After">
165209
{renderAfterChildren()}
166210
</ChildrenPreview>
167211
</Card>
@@ -181,12 +225,14 @@ export const CompareContainersWidget = ({
181225
isReadyToSyncIndividually = false,
182226
}: ContainerInfoProps) => {
183227
const [currentContainerState, setCurrentContainerState] = useState<ContainerInfoProps & {
184-
parent: ContainerInfoProps[];
228+
state?: ContainerState;
229+
parent:(ContainerInfoProps & { state?: ContainerState })[];
185230
}>({
186-
upstreamBlockId,
187-
downstreamBlockId,
188-
parent: [],
189-
});
231+
upstreamBlockId,
232+
downstreamBlockId,
233+
parent: [],
234+
state: 'modified',
235+
});
190236

191237
const { data } = useCourseContainerChildren(downstreamBlockId, true);
192238
let localUpdateAlertBlockName = '';
@@ -213,9 +259,11 @@ export const CompareContainersWidget = ({
213259
setCurrentContainerState((prev) => ({
214260
upstreamBlockId: row.id!,
215261
downstreamBlockId: row.downstreamId!,
262+
state: row.state,
216263
parent: [...prev.parent, {
217264
upstreamBlockId: prev.upstreamBlockId,
218265
downstreamBlockId: prev.downstreamBlockId,
266+
state: prev.state,
219267
}],
220268
}));
221269
};
@@ -230,6 +278,7 @@ export const CompareContainersWidget = ({
230278
return {
231279
upstreamBlockId: prevParent!.upstreamBlockId,
232280
downstreamBlockId: prevParent!.downstreamBlockId,
281+
state: prevParent!.state,
233282
parent: prev.parent.slice(0, -1),
234283
};
235284
});
@@ -240,6 +289,7 @@ export const CompareContainersWidget = ({
240289
upstreamBlockId={currentContainerState.upstreamBlockId}
241290
downstreamBlockId={currentContainerState.downstreamBlockId}
242291
parent={currentContainerState.parent}
292+
state={currentContainerState.state}
243293
onRowClick={onRowClick}
244294
onBackBtnClick={onBackBtnClick}
245295
localUpdateAlertCount={localUpdateAlertCount}

src/container-comparison/ContainerRow.test.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,6 @@ describe('<ContainerRow />', () => {
2929
)).toBeInTheDocument();
3030
});
3131

32-
test('is not clickable when state !== modified', async () => {
33-
const onClick = jest.fn();
34-
render(<ContainerRow
35-
title="Test title"
36-
containerType="subsection"
37-
side="Before"
38-
state="removed"
39-
onClick={onClick}
40-
/>);
41-
const titleDiv = await screen.findByText('Test title');
42-
const card = titleDiv.closest('.clickable');
43-
expect(card).toBe(null);
44-
});
45-
4632
test('calls onClick when clicked', async () => {
4733
const onClick = jest.fn();
4834
const user = userEvent.setup();

src/container-comparison/data/api.mock.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as unitApi from '@src/course-unit/data/api';
88
* This mock returns a fixed response for the given container ID.
99
*/
1010
export async function mockGetCourseContainerChildren(containerId: string): Promise<CourseContainerChildrenData> {
11-
const numChildren: number = 3;
11+
let numChildren: number = 3;
1212
let blockType: string;
1313
let displayName: string;
1414
let upstreamReadyToSyncChildrenInfo: UpstreamReadyToSyncChildrenInfo[] = [];
@@ -61,8 +61,9 @@ export async function mockGetCourseContainerChildren(containerId: string): Promi
6161
case mockGetCourseContainerChildren.subsectionIdLoading:
6262
return new Promise(() => { });
6363
default:
64-
blockType = 'unit';
65-
displayName = 'subsection block 00';
64+
blockType = 'section';
65+
displayName = 'section block 00';
66+
numChildren = 0;
6667
break;
6768
}
6869
const children = Array(numChildren).fill(mockGetCourseContainerChildren.childTemplate).map((child, idx) => (

src/container-comparison/data/apiHooks.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ export const containerComparisonQueryKeys = {
1111
/**
1212
* Key for a single container
1313
*/
14-
container: (usageKey: string, getUpstreamInfo: boolean) => {
14+
container: (getUpstreamInfo: boolean, usageKey?: string) => {
15+
if (usageKey === undefined) {
16+
return [undefined, undefined, getUpstreamInfo.toString()];
17+
}
1518
const courseKey = getCourseKey(usageKey);
1619
return [...containerComparisonQueryKeys.course(courseKey), usageKey, getUpstreamInfo.toString()];
1720
},
@@ -21,6 +24,7 @@ export const useCourseContainerChildren = (usageKey?: string, getUpstreamInfo?:
2124
useQuery({
2225
enabled: !!usageKey,
2326
queryFn: () => getCourseContainerChildren(usageKey!, getUpstreamInfo),
24-
queryKey: containerComparisonQueryKeys.container(usageKey!, getUpstreamInfo || false),
27+
// If we first get data with a valid `usageKey` and then the `usageKey` changes to undefined, an error occurs.
28+
queryKey: containerComparisonQueryKeys.container(getUpstreamInfo || false, usageKey),
2529
})
2630
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.compare-changes-widget {
2+
.compare-card {
3+
min-height: 350px;
4+
}
5+
6+
.big-icon {
7+
height: 68px;
8+
width: 68px;
9+
}
10+
}

src/container-comparison/messages.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ const messages = defineMessages({
8686
defaultMessage: 'The only change is to {count, plural, one {text block <b>{blockName}</b> which has been edited} other {<b>{count} text blocks</b> which have been edited}} in this course. Accepting will not remove local edits.',
8787
description: 'Alert to show if the only change is on text components with local overrides.',
8888
},
89+
newContainer: {
90+
id: 'course-authoring.container-comparison.new-container.text',
91+
defaultMessage: 'This {containerType} is new',
92+
description: 'Text to show in the comparison when a container is new.',
93+
},
94+
deletedContainer: {
95+
id: 'course-authoring.container-comparison.deleted-container.text',
96+
defaultMessage: 'This {containerType} has been removed',
97+
description: 'Text to show in the comparison when a container is removed.',
98+
},
8999
});
90100

91101
export default messages;

src/container-comparison/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export function diffPreviewContainerChildren<A extends CourseContainerChildBase,
132132
}
133133

134134
export function isRowClickable(state?: ContainerState, blockType?: ContainerType) {
135-
return state === 'modified' && blockType && [
135+
return state && blockType && ['modified', 'added', 'removed'].includes(state) && [
136136
ContainerType.Section,
137137
ContainerType.Subsection,
138138
ContainerType.Unit,

src/course-outline/section-card/SectionCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const SectionCard = ({
146146
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
147147
isReadyToSyncIndividually: upstreamInfo.isReadyToSyncIndividually,
148148
isContainer: true,
149+
blockType: 'section',
149150
};
150151
}, [upstreamInfo]);
151152

src/course-outline/subsection-card/SubsectionCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const SubsectionCard = ({
128128
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
129129
isReadyToSyncIndividually: upstreamInfo.isReadyToSyncIndividually,
130130
isContainer: true,
131+
blockType: 'subsection',
131132
};
132133
}, [upstreamInfo]);
133134

0 commit comments

Comments
 (0)