Skip to content

Commit 2dc087f

Browse files
authored
feat: differentiate between renamed and data edit in sync preview diff [FC-0112] (#2577)
Use `downstream_customized` field from upstream link to determine whether the text component was locally renamed or content updated or both and display correct notes in preview diff. Also update libraries v2 alert wording as per #2169 (comment)
1 parent 9b77a40 commit 2dc087f

File tree

13 files changed

+168
-53
lines changed

13 files changed

+168
-53
lines changed

src/container-comparison/CompareContainersWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export const CompareContainersWidget = ({
243243
// the alert would disappear. By keeping this call in CompareContainersWidget,
244244
// the alert remains in the modal regardless of whether you navigate within the children.
245245
if (!isReadyToSyncIndividually && data?.upstreamReadyToSyncChildrenInfo
246-
&& data.upstreamReadyToSyncChildrenInfo.every(value => value.isModified && value.blockType === 'html')
246+
&& data.upstreamReadyToSyncChildrenInfo.every(value => value.downstreamCustomized.length > 0 && value.blockType === 'html')
247247
) {
248248
localUpdateAlertCount = data.upstreamReadyToSyncChildrenInfo.length;
249249
if (localUpdateAlertCount === 1) {

src/container-comparison/ContainerRow.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ describe('<ContainerRow />', () => {
7474
expect(await screen.findByText(messages.locallyContentUpdatedBeforeMessage.defaultMessage.replace('{blockType}', 'html'))).toBeInTheDocument();
7575
});
7676

77+
test('renders with rename and local content update', async () => {
78+
render(<ContainerRow title="Test title" containerType="html" side="Before" state="locallyRenamedAndContentUpdated" originalName="Modified name" />);
79+
expect(await screen.findByText(messages.renamedDiffBeforeMessage.defaultMessage.replace('{name}', 'Modified name'))).toBeInTheDocument();
80+
expect(await screen.findByText(messages.locallyContentUpdatedBeforeMessage.defaultMessage.replace('{blockType}', 'html'))).toBeInTheDocument();
81+
});
82+
7783
test('renders with moved state', async () => {
7884
render(<ContainerRow title="Test title" containerType="subsection" side="After" state="moved" />);
7985
expect(await screen.findByText(

src/container-comparison/ContainerRow.tsx

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,47 @@ export interface ContainerRowProps {
2323
onClick?: () => void;
2424
}
2525

26+
interface StateContext {
27+
className: string;
28+
icon: React.ComponentType;
29+
message?: MessageDescriptor;
30+
message2?: MessageDescriptor;
31+
}
32+
2633
const ContainerRow = ({
2734
title, containerType, state, side, originalName, onClick,
2835
}: ContainerRowProps) => {
2936
const isClickable = isRowClickable(state, containerType as ContainerType);
30-
const stateContext = useMemo(() => {
37+
const stateContext: StateContext = useMemo(() => {
3138
let message: MessageDescriptor | undefined;
39+
let message2: MessageDescriptor | undefined;
3240
switch (state) {
3341
case 'added':
3442
message = side === 'Before' ? messages.addedDiffBeforeMessage : messages.addedDiffAfterMessage;
35-
return ['text-white bg-success-500', Plus, message];
43+
return { className: 'text-white bg-success-500', icon: Plus, message };
3644
case 'modified':
3745
message = side === 'Before' ? messages.modifiedDiffBeforeMessage : messages.modifiedDiffAfterMessage;
38-
return ['text-white bg-warning-900', Cached, message];
46+
return { className: 'text-white bg-warning-900', icon: Cached, message };
3947
case 'removed':
4048
message = side === 'Before' ? messages.removedDiffBeforeMessage : messages.removedDiffAfterMessage;
41-
return ['text-white bg-danger-600', Delete, message];
49+
return { className: 'text-white bg-danger-600', icon: Delete, message };
4250
case 'locallyRenamed':
4351
message = side === 'Before' ? messages.renamedDiffBeforeMessage : messages.renamedUpdatedDiffAfterMessage;
44-
return ['bg-light-300 text-light-300 ', Done, message];
52+
return { className: 'bg-light-300 text-light-300 ', icon: Done, message };
4553
case 'locallyContentUpdated':
4654
message = side === 'Before' ? messages.locallyContentUpdatedBeforeMessage : messages.locallyContentUpdatedAfterMessage;
47-
return ['bg-light-300 text-light-300 ', Done, message];
55+
return { className: 'bg-light-300 text-light-300 ', icon: Done, message };
56+
case 'locallyRenamedAndContentUpdated':
57+
message = side === 'Before' ? messages.renamedDiffBeforeMessage : messages.renamedUpdatedDiffAfterMessage;
58+
message2 = side === 'Before' ? messages.locallyContentUpdatedBeforeMessage : messages.locallyContentUpdatedAfterMessage;
59+
return {
60+
className: 'bg-light-300 text-light-300 ', icon: Done, message, message2,
61+
};
4862
case 'moved':
4963
message = side === 'Before' ? messages.movedDiffBeforeMessage : messages.movedDiffAfterMessage;
50-
return ['bg-light-300 text-light-300', Done, message];
64+
return { className: 'bg-light-300 text-light-300', icon: Done, message };
5165
default:
52-
return ['bg-light-300 text-light-300', Done, message];
66+
return { className: 'bg-light-300 text-light-300', icon: Done, message };
5367
}
5468
}, [state, side]);
5569

@@ -66,9 +80,9 @@ const ContainerRow = ({
6680
>
6781
<Stack direction="horizontal" gap={0}>
6882
<div
69-
className={`px-1 align-self-stretch align-content-center rounded-left ${stateContext[0]}`}
83+
className={`px-1 align-self-stretch align-content-center rounded-left ${stateContext.className}`}
7084
>
71-
<Icon size="sm" src={stateContext[1]} />
85+
<Icon size="sm" src={stateContext.icon} />
7286
</div>
7387
<ActionRow className="p-2">
7488
<Stack direction="vertical" gap={2}>
@@ -80,16 +94,29 @@ const ContainerRow = ({
8094
/>
8195
<span className="small font-weight-bold">{title}</span>
8296
</Stack>
83-
{stateContext[2] ? (
84-
<span className="micro">
85-
<FormattedMessage
86-
{...stateContext[2]}
87-
values={{
88-
blockType: containerType,
89-
name: originalName,
90-
}}
91-
/>
92-
</span>
97+
{stateContext.message ? (
98+
<div className="d-flex flex-column">
99+
<span className="micro">
100+
<FormattedMessage
101+
{...stateContext.message}
102+
values={{
103+
blockType: containerType,
104+
name: originalName,
105+
}}
106+
/>
107+
</span>
108+
{stateContext.message2 && (
109+
<span className="micro">
110+
<FormattedMessage
111+
{...stateContext.message2}
112+
values={{
113+
blockType: containerType,
114+
name: originalName,
115+
}}
116+
/>
117+
</span>
118+
)}
119+
</div>
93120
) : (
94121
<span className="micro">&nbsp;</span>
95122
)}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function mockGetCourseContainerChildren(containerId: string): Promi
3232
id: 'block-v1:UNIX+UX1+2025_T3+type@html+block@1',
3333
name: 'Html block 11',
3434
blockType: 'html',
35-
isModified: true,
35+
downstreamCustomized: ['display_name'],
3636
upstream: 'upstream-id',
3737
}];
3838
break;
@@ -44,14 +44,14 @@ export async function mockGetCourseContainerChildren(containerId: string): Promi
4444
id: 'block-v1:UNIX+UX1+2025_T3+type@html+block@1',
4545
name: 'Html block 11',
4646
blockType: 'html',
47-
isModified: true,
47+
downstreamCustomized: ['display_name'],
4848
upstream: 'upstream-id',
4949
},
5050
{
5151
id: 'block-v1:UNIX+UX1+2025_T3+type@html+block@2',
5252
name: 'Html block 22',
5353
blockType: 'html',
54-
isModified: true,
54+
downstreamCustomized: ['display_name'],
5555
upstream: 'upstream-id',
5656
},
5757
];
@@ -78,7 +78,7 @@ export async function mockGetCourseContainerChildren(containerId: string): Promi
7878
versionSynced: 1,
7979
versionAvailable: 2,
8080
versionDeclined: null,
81-
isModified: false,
81+
downstreamCustomized: [],
8282
},
8383
}
8484
));
@@ -107,7 +107,7 @@ mockGetCourseContainerChildren.childTemplate = {
107107
versionSynced: 1,
108108
versionAvailable: 2,
109109
versionDeclined: null,
110-
isModified: false,
110+
downstreamCustomized: [],
111111
},
112112
};
113113
/** Apply this mock. Returns a spy object that can tell you if it's been called. */

src/container-comparison/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { UpstreamInfo } from '@src/data/types';
22

3-
export type ContainerState = 'removed' | 'added' | 'modified' | 'childrenModified' | 'locallyContentUpdated' | 'locallyRenamed' | 'moved';
3+
export type ContainerState = 'removed' | 'added' | 'modified' | 'childrenModified' | 'locallyContentUpdated' | 'locallyRenamed' | 'locallyRenamedAndContentUpdated' | 'moved';
44

55
export type WithState<T> = T & { state?: ContainerState, originalName?: string };
66
export type WithIndex<T> = T & { index: number };

src/container-comparison/utils.test.ts

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ContainerChildBase, CourseContainerChildBase } from './types';
22
import { diffPreviewContainerChildren } from './utils';
33

44
export const getMockCourseContainerData = (
5-
type: 'added|deleted' | 'moved|deleted' | 'all',
5+
type: 'added|deleted' | 'moved|deleted' | 'all' | 'locallyEdited',
66
): [CourseContainerChildBase[], ContainerChildBase[]] => {
77
switch (type) {
88
case 'moved|deleted':
@@ -17,7 +17,7 @@ export const getMockCourseContainerData = (
1717
versionSynced: 11,
1818
versionAvailable: 11,
1919
versionDeclined: null,
20-
isModified: true,
20+
downstreamCustomized: ['display_name'],
2121
},
2222
},
2323
{
@@ -29,7 +29,7 @@ export const getMockCourseContainerData = (
2929
versionSynced: 7,
3030
versionAvailable: 7,
3131
versionDeclined: null,
32-
isModified: false,
32+
downstreamCustomized: [],
3333
},
3434
},
3535
{
@@ -41,7 +41,7 @@ export const getMockCourseContainerData = (
4141
versionSynced: 2,
4242
versionAvailable: 2,
4343
versionDeclined: null,
44-
isModified: false,
44+
downstreamCustomized: [],
4545
},
4646
},
4747
{
@@ -53,7 +53,7 @@ export const getMockCourseContainerData = (
5353
versionSynced: 1,
5454
versionAvailable: 1,
5555
versionDeclined: null,
56-
isModified: false,
56+
downstreamCustomized: [],
5757
},
5858
},
5959
],
@@ -87,7 +87,7 @@ export const getMockCourseContainerData = (
8787
versionSynced: 11,
8888
versionAvailable: 11,
8989
versionDeclined: null,
90-
isModified: true,
90+
downstreamCustomized: ['display_name'],
9191
},
9292
},
9393
{
@@ -99,7 +99,7 @@ export const getMockCourseContainerData = (
9999
versionSynced: 7,
100100
versionAvailable: 7,
101101
versionDeclined: null,
102-
isModified: false,
102+
downstreamCustomized: [],
103103
},
104104
},
105105
{
@@ -111,7 +111,7 @@ export const getMockCourseContainerData = (
111111
versionSynced: 2,
112112
versionAvailable: 2,
113113
versionDeclined: null,
114-
isModified: false,
114+
downstreamCustomized: [],
115115
},
116116
},
117117
{
@@ -123,7 +123,7 @@ export const getMockCourseContainerData = (
123123
versionSynced: 1,
124124
versionAvailable: 1,
125125
versionDeclined: null,
126-
isModified: false,
126+
downstreamCustomized: [],
127127
},
128128
},
129129
],
@@ -162,7 +162,7 @@ export const getMockCourseContainerData = (
162162
versionSynced: 11,
163163
versionAvailable: 11,
164164
versionDeclined: null,
165-
isModified: true,
165+
downstreamCustomized: ['display_name'],
166166
},
167167
},
168168
{
@@ -174,7 +174,7 @@ export const getMockCourseContainerData = (
174174
versionSynced: 7,
175175
versionAvailable: 7,
176176
versionDeclined: null,
177-
isModified: false,
177+
downstreamCustomized: [],
178178
},
179179
},
180180
{
@@ -186,7 +186,7 @@ export const getMockCourseContainerData = (
186186
versionSynced: 2,
187187
versionAvailable: 2,
188188
versionDeclined: null,
189-
isModified: false,
189+
downstreamCustomized: [],
190190
},
191191
},
192192
{
@@ -198,7 +198,7 @@ export const getMockCourseContainerData = (
198198
versionSynced: 1,
199199
versionAvailable: 1,
200200
versionDeclined: null,
201-
isModified: false,
201+
downstreamCustomized: [],
202202
},
203203
},
204204
],
@@ -225,6 +225,64 @@ export const getMockCourseContainerData = (
225225
},
226226
],
227227
] as [CourseContainerChildBase[], ContainerChildBase[]];
228+
case 'locallyEdited':
229+
return [
230+
[
231+
{
232+
id: 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@1',
233+
name: 'Unit 1 remote edit - local edit',
234+
blockType: 'vertical',
235+
upstreamLink: {
236+
upstreamRef: 'lct:UNIX:CS1:unit:unit-1-2a1741',
237+
versionSynced: 11,
238+
versionAvailable: 11,
239+
versionDeclined: null,
240+
downstreamCustomized: ['display_name'],
241+
},
242+
},
243+
{
244+
id: 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@2',
245+
name: 'New unit remote edit',
246+
blockType: 'vertical',
247+
upstreamLink: {
248+
upstreamRef: 'lct:UNIX:CS1:unit:new-unit-remote-7eb9d1',
249+
versionSynced: 7,
250+
versionAvailable: 7,
251+
versionDeclined: null,
252+
downstreamCustomized: ['data'],
253+
},
254+
},
255+
{
256+
id: 'block-v1:UNIX+UX1+2025_T3+type@vertical+block@3',
257+
name: 'Unit with tags - local edit',
258+
blockType: 'vertical',
259+
upstreamLink: {
260+
upstreamRef: 'lct:UNIX:CS1:unit:unit-with-tags-bec5f9',
261+
versionSynced: 2,
262+
versionAvailable: 2,
263+
versionDeclined: null,
264+
downstreamCustomized: ['display_name', 'data'],
265+
},
266+
},
267+
],
268+
[
269+
{
270+
id: 'lct:UNIX:CS1:unit:unit-1-2a1741',
271+
displayName: 'Unit 1 remote edit - remote edit',
272+
containerType: 'unit',
273+
},
274+
{
275+
id: 'lct:UNIX:CS1:unit:new-unit-remote-7eb9d1',
276+
displayName: 'New unit remote edit',
277+
containerType: 'unit',
278+
},
279+
{
280+
id: 'lct:UNIX:CS1:unit:unit-with-tags-bec5f9',
281+
displayName: 'Unit with tags - remote edit',
282+
containerType: 'unit',
283+
},
284+
],
285+
] as [CourseContainerChildBase[], ContainerChildBase[]];
228286
default:
229287
throw new Error();
230288
}
@@ -280,4 +338,22 @@ describe('diffPreviewContainerChildren', () => {
280338
expect(result[1][2].state).toEqual('added');
281339
expect(result[1][2].id).toEqual(result[0][2].id);
282340
});
341+
342+
it('should handle locally edited content', () => {
343+
const [a, b] = getMockCourseContainerData('locallyEdited');
344+
const result = diffPreviewContainerChildren(a as CourseContainerChildBase[], b);
345+
expect(result[0].length).toEqual(result[1].length);
346+
// renamed
347+
expect(result[0][0].state).toEqual('locallyRenamed');
348+
expect(result[1][0].state).toEqual('locallyRenamed');
349+
expect(result[1][0].id).toEqual(result[0][0].id);
350+
// content updated
351+
expect(result[0][1].state).toEqual('locallyContentUpdated');
352+
expect(result[1][1].state).toEqual('locallyContentUpdated');
353+
expect(result[1][1].id).toEqual(result[0][1].id);
354+
// renamed and content updated
355+
expect(result[0][2].state).toEqual('locallyRenamedAndContentUpdated');
356+
expect(result[1][2].state).toEqual('locallyRenamedAndContentUpdated');
357+
expect(result[1][2].id).toEqual(result[0][2].id);
358+
});
283359
});

0 commit comments

Comments
 (0)