@teambit/compositions.ui.composition-compare 0.0.259 → 0.0.261
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/composition-compare.context.tsx +6 -6
- package/composition-compare.module.scss +126 -14
- package/composition-compare.tsx +616 -169
- package/composition-dropdown.module.scss +28 -0
- package/composition-dropdown.tsx +16 -4
- package/dist/composition-compare.context.d.ts +1 -1
- package/dist/composition-compare.d.ts +1 -1
- package/dist/composition-compare.js +443 -110
- package/dist/composition-compare.js.map +1 -1
- package/dist/composition-compare.module.scss +126 -14
- package/dist/composition-dropdown.d.ts +1 -0
- package/dist/composition-dropdown.js +23 -9
- package/dist/composition-dropdown.js.map +1 -1
- package/dist/composition-dropdown.module.scss +28 -0
- package/index.ts +1 -1
- package/package.json +18 -10
- /package/dist/{preview-1739094520607.js → preview-1772573607457.js} +0 -0
package/composition-compare.tsx
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
2
3
|
import { useComponentCompare } from '@teambit/component.ui.component-compare.context';
|
|
3
|
-
import { CompositionContent, CompositionContentProps, EmptyStateSlot } from '@teambit/compositions';
|
|
4
|
-
import { CompositionContextProvider } from '@teambit/compositions.ui.hooks.use-composition';
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
CompositionContent,
|
|
6
|
+
LiveControlsDiffPanel,
|
|
7
|
+
useDefaultControlsSchemaResponder,
|
|
8
|
+
type CompositionContentProps,
|
|
9
|
+
type EmptyStateSlot,
|
|
10
|
+
} from '@teambit/compositions';
|
|
11
|
+
import { CompositionContextProvider } from '@teambit/compositions.ui.hooks.use-composition';
|
|
12
|
+
import { useCompareQueryParam } from '@teambit/component.ui.component-compare.hooks.use-component-compare-url';
|
|
9
13
|
import { CompareSplitLayoutPreset } from '@teambit/component.ui.component-compare.layouts.compare-split-layout-preset';
|
|
10
14
|
import { RoundLoader } from '@teambit/design.ui.round-loader';
|
|
15
|
+
import { Icon } from '@teambit/evangelist.elements.icon';
|
|
16
|
+
import { useLocation } from '@teambit/base-react.navigation.link';
|
|
17
|
+
import { useQuery } from '@teambit/ui-foundation.ui.react-router.use-query';
|
|
11
18
|
import queryString from 'query-string';
|
|
12
19
|
import { CompositionDropdown } from './composition-dropdown';
|
|
13
20
|
import { CompositionCompareContext } from './composition-compare.context';
|
|
14
21
|
import { uniqBy } from 'lodash';
|
|
22
|
+
import * as semver from 'semver';
|
|
15
23
|
|
|
16
24
|
import styles from './composition-compare.module.scss';
|
|
17
25
|
|
|
26
|
+
const noop = () => {};
|
|
27
|
+
const LOCAL_VERSION = 'workspace';
|
|
28
|
+
|
|
18
29
|
export type CompositionCompareProps = {
|
|
19
30
|
emptyState?: EmptyStateSlot;
|
|
20
31
|
Widgets?: {
|
|
@@ -25,180 +36,591 @@ export type CompositionCompareProps = {
|
|
|
25
36
|
PreviewView?: React.ComponentType<CompositionContentProps>;
|
|
26
37
|
};
|
|
27
38
|
|
|
39
|
+
type ControlsStatus = 'loading' | 'available' | 'empty';
|
|
40
|
+
|
|
41
|
+
function MissingCompositionTemplate({ message, title }: { message?: string; title?: string } = {}) {
|
|
42
|
+
return (
|
|
43
|
+
<div className={styles.subView}>
|
|
44
|
+
<div className={styles.missingComposition}>
|
|
45
|
+
<div className={styles.missingCompositionTitle}>{title || 'Composition not available'}</div>
|
|
46
|
+
{message && <div className={styles.missingCompositionSubtitle}>{message}</div>}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function MissingComposition({ compositionId, version }: { compositionId?: string; version: string }) {
|
|
53
|
+
const message = compositionId
|
|
54
|
+
? `The selected composition "${compositionId}" does not exist for the ${version} version.`
|
|
55
|
+
: `The selected composition does not exist for the ${version} version.`;
|
|
56
|
+
return <MissingCompositionTemplate message={message} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getCompositionTag(hasInBase: boolean, hasInCompare: boolean): string | undefined {
|
|
60
|
+
if (hasInBase && hasInCompare) return undefined;
|
|
61
|
+
if (hasInBase) return 'Base only';
|
|
62
|
+
if (hasInCompare) return 'Compare only';
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function useResizePanel(initialHeight: number) {
|
|
67
|
+
const MIN_PANEL_HEIGHT = 80;
|
|
68
|
+
const MIN_COMPARE_HEIGHT = 150;
|
|
69
|
+
const DEFAULT_PANEL_RATIO = 0.34;
|
|
70
|
+
const [panelHeight, setPanelHeight] = useState(initialHeight);
|
|
71
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
72
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const isDragging = useRef(false);
|
|
74
|
+
const panelHeightRef = useRef(initialHeight);
|
|
75
|
+
const removeListenersRef = useRef<(() => void) | null>(null);
|
|
76
|
+
const rafRef = useRef<number | null>(null);
|
|
77
|
+
const initializedRef = useRef(false);
|
|
78
|
+
const userResizedRef = useRef(false);
|
|
79
|
+
|
|
80
|
+
const getMaxPanelHeight = useCallback(() => {
|
|
81
|
+
const containerHeight = panelRef.current?.parentElement?.clientHeight;
|
|
82
|
+
if (!containerHeight) return Math.max(MIN_PANEL_HEIGHT, initialHeight);
|
|
83
|
+
return Math.max(MIN_PANEL_HEIGHT, containerHeight - MIN_COMPARE_HEIGHT);
|
|
84
|
+
}, [MIN_COMPARE_HEIGHT, MIN_PANEL_HEIGHT, initialHeight]);
|
|
85
|
+
|
|
86
|
+
const getDefaultPanelHeight = useCallback(() => {
|
|
87
|
+
const containerHeight = panelRef.current?.parentElement?.clientHeight;
|
|
88
|
+
if (!containerHeight || containerHeight <= 0) return initialHeight;
|
|
89
|
+
return Math.max(initialHeight, Math.round(containerHeight * DEFAULT_PANEL_RATIO));
|
|
90
|
+
}, [initialHeight, DEFAULT_PANEL_RATIO]);
|
|
91
|
+
|
|
92
|
+
const clampPanelHeight = useCallback(
|
|
93
|
+
(height: number) => {
|
|
94
|
+
const maxHeight = getMaxPanelHeight();
|
|
95
|
+
return Math.max(MIN_PANEL_HEIGHT, Math.min(maxHeight, height));
|
|
96
|
+
},
|
|
97
|
+
[MIN_PANEL_HEIGHT, getMaxPanelHeight]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const applyPanelHeight = useCallback(
|
|
101
|
+
(height: number, commitToState = false) => {
|
|
102
|
+
const clamped = clampPanelHeight(height);
|
|
103
|
+
panelHeightRef.current = clamped;
|
|
104
|
+
if (panelRef.current) panelRef.current.style.height = `${clamped}px`;
|
|
105
|
+
if (commitToState) setPanelHeight(clamped);
|
|
106
|
+
return clamped;
|
|
107
|
+
},
|
|
108
|
+
[clampPanelHeight]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const syncPanelHeight = useCallback(() => {
|
|
112
|
+
if (isDragging.current) return;
|
|
113
|
+
const nextHeight = userResizedRef.current ? panelHeightRef.current : getDefaultPanelHeight();
|
|
114
|
+
applyPanelHeight(nextHeight, true);
|
|
115
|
+
}, [applyPanelHeight, getDefaultPanelHeight]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (initializedRef.current) return;
|
|
119
|
+
initializedRef.current = true;
|
|
120
|
+
syncPanelHeight();
|
|
121
|
+
}, [syncPanelHeight]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const handleWindowResize = () => {
|
|
125
|
+
syncPanelHeight();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
handleWindowResize();
|
|
129
|
+
window.addEventListener('resize', handleWindowResize);
|
|
130
|
+
return () => window.removeEventListener('resize', handleWindowResize);
|
|
131
|
+
}, [syncPanelHeight]);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
135
|
+
const parent = panelRef.current?.parentElement;
|
|
136
|
+
if (!parent) return;
|
|
137
|
+
|
|
138
|
+
const observer = new ResizeObserver(() => {
|
|
139
|
+
syncPanelHeight();
|
|
140
|
+
});
|
|
141
|
+
observer.observe(parent);
|
|
142
|
+
|
|
143
|
+
return () => observer.disconnect();
|
|
144
|
+
}, [syncPanelHeight]);
|
|
145
|
+
|
|
146
|
+
const handleResizeStart = useCallback(
|
|
147
|
+
(e: React.MouseEvent) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
isDragging.current = true;
|
|
151
|
+
setIsResizing(true);
|
|
152
|
+
|
|
153
|
+
const startY = e.clientY;
|
|
154
|
+
const startHeight = panelHeightRef.current;
|
|
155
|
+
|
|
156
|
+
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
157
|
+
if (!isDragging.current) return;
|
|
158
|
+
moveEvent.preventDefault();
|
|
159
|
+
const delta = startY - moveEvent.clientY;
|
|
160
|
+
const targetHeight = startHeight + delta;
|
|
161
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
162
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
163
|
+
applyPanelHeight(targetHeight);
|
|
164
|
+
rafRef.current = null;
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleMouseUp = () => {
|
|
169
|
+
if (rafRef.current !== null) {
|
|
170
|
+
cancelAnimationFrame(rafRef.current);
|
|
171
|
+
rafRef.current = null;
|
|
172
|
+
}
|
|
173
|
+
userResizedRef.current = true;
|
|
174
|
+
isDragging.current = false;
|
|
175
|
+
applyPanelHeight(panelHeightRef.current, true);
|
|
176
|
+
setIsResizing(false);
|
|
177
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
178
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
179
|
+
window.removeEventListener('blur', handleMouseUp);
|
|
180
|
+
document.body.style.cursor = '';
|
|
181
|
+
document.body.style.userSelect = '';
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
removeListenersRef.current?.();
|
|
185
|
+
document.body.style.cursor = 'ns-resize';
|
|
186
|
+
document.body.style.userSelect = 'none';
|
|
187
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
188
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
189
|
+
window.addEventListener('blur', handleMouseUp);
|
|
190
|
+
removeListenersRef.current = () => {
|
|
191
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
192
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
193
|
+
window.removeEventListener('blur', handleMouseUp);
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
[applyPanelHeight]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
return () => {
|
|
201
|
+
isDragging.current = false;
|
|
202
|
+
if (rafRef.current !== null) {
|
|
203
|
+
cancelAnimationFrame(rafRef.current);
|
|
204
|
+
rafRef.current = null;
|
|
205
|
+
}
|
|
206
|
+
removeListenersRef.current?.();
|
|
207
|
+
removeListenersRef.current = null;
|
|
208
|
+
document.body.style.cursor = '';
|
|
209
|
+
document.body.style.userSelect = '';
|
|
210
|
+
};
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
return { panelRef, panelHeight, isResizing, handleResizeStart };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findComposition(compositions: any[] | undefined, id: string | undefined) {
|
|
217
|
+
if (!id || !compositions) return compositions?.[0];
|
|
218
|
+
return compositions.find((c) => c.identifier === id) || undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function useCompositionSelection() {
|
|
222
|
+
const selectedCompositionBaseFile = useCompareQueryParam('compositionBaseFile');
|
|
223
|
+
const selectedCompositionCompareFile = useCompareQueryParam('compositionCompareFile');
|
|
224
|
+
return { selectedCompositionBaseFile, selectedCompositionCompareFile };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildChannelKey(prefix: string, idStr: string | undefined, compId: string | undefined): string | undefined {
|
|
228
|
+
if (!idStr || !compId) return undefined;
|
|
229
|
+
return `${prefix}:${idStr}:${compId}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildQueryParams(channelKey: string | undefined) {
|
|
233
|
+
const params = { livecontrols: true, ...(channelKey ? { lcchannel: channelKey } : {}) };
|
|
234
|
+
return { params, queryString: queryString.stringify(params) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildUpdatedUrlFromQuery(
|
|
238
|
+
query: URLSearchParams,
|
|
239
|
+
pathname: string,
|
|
240
|
+
queryParams: {
|
|
241
|
+
compositionBaseFile?: string;
|
|
242
|
+
compositionCompareFile?: string;
|
|
243
|
+
}
|
|
244
|
+
) {
|
|
245
|
+
const queryObj = Object.fromEntries(query.entries());
|
|
246
|
+
const updatedObj = { ...queryObj, ...queryParams };
|
|
247
|
+
const updatedQueryString = new URLSearchParams(updatedObj).toString();
|
|
248
|
+
return `${pathname}?${updatedQueryString}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveVersion(model: any): string | undefined {
|
|
252
|
+
const id = model?.id;
|
|
253
|
+
const versionFromField = id?.version?.toString?.();
|
|
254
|
+
const versionFromToString = typeof id?.toString === 'function' ? id.toString().split('@')[1] : undefined;
|
|
255
|
+
return versionFromField || versionFromToString;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasStableCompareData(contextLoading: boolean | undefined, base: any, compare: any) {
|
|
259
|
+
return !contextLoading && base !== undefined && compare !== undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function resolveRequestedCompositionId({
|
|
263
|
+
selectedCompositionCompareFile,
|
|
264
|
+
selectedCompositionBaseFile,
|
|
265
|
+
compareStateId,
|
|
266
|
+
baseStateId,
|
|
267
|
+
compareCompositions,
|
|
268
|
+
baseCompositions,
|
|
269
|
+
}: {
|
|
270
|
+
selectedCompositionCompareFile?: string;
|
|
271
|
+
selectedCompositionBaseFile?: string;
|
|
272
|
+
compareStateId?: string;
|
|
273
|
+
baseStateId?: string;
|
|
274
|
+
compareCompositions?: any[];
|
|
275
|
+
baseCompositions?: any[];
|
|
276
|
+
}) {
|
|
277
|
+
const explicitId = selectedCompositionCompareFile || selectedCompositionBaseFile;
|
|
278
|
+
const stateId = compareStateId || baseStateId;
|
|
279
|
+
const defaultId = compareCompositions?.[0]?.identifier || baseCompositions?.[0]?.identifier;
|
|
280
|
+
return explicitId || stateId || defaultId;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolveCompositionId(selectedComposition: any, requestedCompositionId?: string) {
|
|
284
|
+
return selectedComposition?.identifier || requestedCompositionId;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasMissingComposition(requestedCompositionId: string | undefined, selectedComposition: any) {
|
|
288
|
+
return Boolean(!requestedCompositionId || !selectedComposition);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildControlsResetKey(baseChannelKey: string | undefined, compareChannelKey: string | undefined) {
|
|
292
|
+
return `${baseChannelKey || ''}-${compareChannelKey || ''}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function formatVersionForLabel(version: string | undefined, opts?: { forceWorkspace?: boolean }) {
|
|
296
|
+
if (opts?.forceWorkspace) return LOCAL_VERSION;
|
|
297
|
+
if (!version) return undefined;
|
|
298
|
+
if (version === LOCAL_VERSION) return LOCAL_VERSION;
|
|
299
|
+
return semver.valid(version) ? version : version.slice(0, 6);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function useStableControlLabels(baseModel: any, compareModel: any, compareHasLocalChanges?: boolean) {
|
|
303
|
+
const baseVersion = useMemo(() => formatVersionForLabel(resolveVersion(baseModel)), [baseModel]);
|
|
304
|
+
const compareVersion = useMemo(
|
|
305
|
+
() => formatVersionForLabel(resolveVersion(compareModel), { forceWorkspace: compareHasLocalChanges }),
|
|
306
|
+
[compareModel, compareHasLocalChanges]
|
|
307
|
+
);
|
|
308
|
+
const [stableVersions, setStableVersions] = useState<{
|
|
309
|
+
base?: string;
|
|
310
|
+
compare?: string;
|
|
311
|
+
}>({
|
|
312
|
+
base: baseVersion,
|
|
313
|
+
compare: compareVersion,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
setStableVersions((prev) => {
|
|
318
|
+
const nextBase = baseVersion ?? prev.base;
|
|
319
|
+
const nextCompare = compareVersion ?? prev.compare;
|
|
320
|
+
if (nextBase === prev.base && nextCompare === prev.compare) return prev;
|
|
321
|
+
return { base: nextBase, compare: nextCompare };
|
|
322
|
+
});
|
|
323
|
+
}, [baseVersion, compareVersion]);
|
|
324
|
+
|
|
325
|
+
const effectiveBaseVersion = baseVersion ?? stableVersions.base;
|
|
326
|
+
const effectiveCompareVersion = compareVersion ?? stableVersions.compare;
|
|
327
|
+
|
|
328
|
+
return useMemo(
|
|
329
|
+
() => ({
|
|
330
|
+
common: 'Common',
|
|
331
|
+
base: effectiveBaseVersion ? `Base version: ${effectiveBaseVersion}` : 'Base version',
|
|
332
|
+
compare: effectiveCompareVersion ? `Compare version: ${effectiveCompareVersion}` : 'Compare version',
|
|
333
|
+
}),
|
|
334
|
+
[effectiveBaseVersion, effectiveCompareVersion]
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
type CompositionLayoutProps = {
|
|
339
|
+
model: any;
|
|
340
|
+
selected: any;
|
|
341
|
+
queryParams: string;
|
|
342
|
+
compositionParams: Record<string, any>;
|
|
343
|
+
previewViewProps: CompositionContentProps;
|
|
344
|
+
emptyState?: EmptyStateSlot;
|
|
345
|
+
PreviewView: React.ComponentType<CompositionContentProps>;
|
|
346
|
+
contextKey: string;
|
|
347
|
+
isBase?: boolean;
|
|
348
|
+
isCompare?: boolean;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
function CompositionLayout({
|
|
352
|
+
model,
|
|
353
|
+
selected,
|
|
354
|
+
queryParams,
|
|
355
|
+
compositionParams,
|
|
356
|
+
previewViewProps,
|
|
357
|
+
emptyState,
|
|
358
|
+
PreviewView,
|
|
359
|
+
contextKey,
|
|
360
|
+
isBase,
|
|
361
|
+
isCompare,
|
|
362
|
+
}: CompositionLayoutProps) {
|
|
363
|
+
return (
|
|
364
|
+
<div className={styles.subView}>
|
|
365
|
+
<CompositionCompareContext.Provider
|
|
366
|
+
value={{
|
|
367
|
+
compositionProps: {
|
|
368
|
+
forceHeight: undefined,
|
|
369
|
+
innerBottomPadding: 50,
|
|
370
|
+
...previewViewProps,
|
|
371
|
+
emptyState,
|
|
372
|
+
component: model,
|
|
373
|
+
queryParams,
|
|
374
|
+
selected,
|
|
375
|
+
},
|
|
376
|
+
isBase,
|
|
377
|
+
isCompare,
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<CompositionContextProvider queryParams={compositionParams} setQueryParams={noop}>
|
|
381
|
+
<PreviewView
|
|
382
|
+
key={contextKey}
|
|
383
|
+
forceHeight={undefined}
|
|
384
|
+
innerBottomPadding={50}
|
|
385
|
+
{...previewViewProps}
|
|
386
|
+
emptyState={emptyState}
|
|
387
|
+
component={model}
|
|
388
|
+
selected={selected}
|
|
389
|
+
queryParams={queryParams}
|
|
390
|
+
/>
|
|
391
|
+
</CompositionContextProvider>
|
|
392
|
+
</CompositionCompareContext.Provider>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
28
397
|
export function CompositionCompare(props: CompositionCompareProps) {
|
|
29
|
-
const {
|
|
398
|
+
const {
|
|
399
|
+
emptyState,
|
|
400
|
+
PreviewView = CompositionContent,
|
|
401
|
+
Widgets,
|
|
402
|
+
previewViewProps = {} as CompositionContentProps,
|
|
403
|
+
} = props;
|
|
30
404
|
|
|
31
405
|
const componentCompareContext = useComponentCompare();
|
|
406
|
+
const query = useQuery();
|
|
407
|
+
const location = useLocation() || { pathname: '/' };
|
|
408
|
+
const { base, compare, baseContext, compareContext, loading: contextLoading } = componentCompareContext || {};
|
|
32
409
|
|
|
33
|
-
const
|
|
410
|
+
const [isControlsOpen, setControlsOpen] = useState(true);
|
|
411
|
+
const [controlsStatus, setControlsStatus] = useState<ControlsStatus>('loading');
|
|
412
|
+
const [everHadControls, setEverHadControls] = useState(false);
|
|
413
|
+
const { panelRef, panelHeight, isResizing, handleResizeStart } = useResizePanel(240);
|
|
34
414
|
|
|
415
|
+
const isStableData = hasStableCompareData(contextLoading, base, compare);
|
|
35
416
|
const baseCompositions = base?.model.compositions;
|
|
36
417
|
const compareCompositions = compare?.model.compositions;
|
|
37
418
|
|
|
38
|
-
const selectedCompositionBaseFile =
|
|
39
|
-
const selectedCompositionCompareFile = useCompareQueryParam('compositionCompareFile');
|
|
419
|
+
const { selectedCompositionBaseFile, selectedCompositionCompareFile } = useCompositionSelection();
|
|
40
420
|
|
|
41
|
-
const baseState = baseContext?.state?.preview;
|
|
42
421
|
const compareState = compareContext?.state?.preview;
|
|
43
422
|
const baseHooks = baseContext?.hooks?.preview;
|
|
44
423
|
const compareHooks = compareContext?.hooks?.preview;
|
|
45
|
-
const selectedBaseFromState = baseState?.id;
|
|
46
|
-
const selectedCompareFromState = compareState?.id;
|
|
47
|
-
|
|
48
|
-
const selectedBaseCompositionId = selectedBaseFromState || selectedCompositionBaseFile;
|
|
49
|
-
const selectedCompositionCompareId = selectedCompareFromState || selectedCompositionCompareFile;
|
|
50
|
-
|
|
51
|
-
const selectedBaseComp =
|
|
52
|
-
(selectedBaseCompositionId &&
|
|
53
|
-
baseCompositions &&
|
|
54
|
-
baseCompositions.find((c) => {
|
|
55
|
-
return c.identifier === selectedBaseCompositionId;
|
|
56
|
-
})) ||
|
|
57
|
-
(baseCompositions && baseCompositions[0]);
|
|
58
|
-
|
|
59
|
-
const selectedCompareComp =
|
|
60
|
-
(selectedCompositionCompareId &&
|
|
61
|
-
compareCompositions &&
|
|
62
|
-
compareCompositions.find((c) => {
|
|
63
|
-
return c.identifier === selectedCompositionCompareId;
|
|
64
|
-
})) ||
|
|
65
|
-
(compareCompositions && compareCompositions[0]);
|
|
66
|
-
|
|
67
|
-
// const baseCompositionDropdownSource =
|
|
68
|
-
// baseCompositions?.map((c) => {
|
|
69
|
-
// const href = !baseState?.controlled
|
|
70
|
-
// ? useUpdatedUrlFromQuery({
|
|
71
|
-
// compositionBaseFile: c.identifier,
|
|
72
|
-
// compositionCompareFile: selectedCompareComp?.identifier,
|
|
73
|
-
// })
|
|
74
|
-
// : useUpdatedUrlFromQuery({});
|
|
75
|
-
|
|
76
|
-
// const onClick = baseState?.controlled ? baseHooks?.onClick : undefined;
|
|
77
|
-
|
|
78
|
-
// return { id: c.identifier, label: c.displayName, href, onClick };
|
|
79
|
-
// }) || [];
|
|
80
|
-
|
|
81
|
-
// const compareCompositionDropdownSource =
|
|
82
|
-
// compareCompositions?.map((c) => {
|
|
83
|
-
// const href = !compareState?.controlled
|
|
84
|
-
// ? useUpdatedUrlFromQuery({
|
|
85
|
-
// compositionBaseFile: selectedBaseComp?.identifier,
|
|
86
|
-
// compositionCompareFile: c.identifier,
|
|
87
|
-
// })
|
|
88
|
-
// : useUpdatedUrlFromQuery({});
|
|
89
|
-
|
|
90
|
-
// const onClick = compareState?.controlled ? () => compareHooks?.onClick : undefined;
|
|
91
|
-
|
|
92
|
-
// return { id: c.identifier, label: c.displayName, href, onClick };
|
|
93
|
-
// }) || [];
|
|
94
|
-
|
|
95
|
-
const compositionsDropdownSource = uniqBy((baseCompositions || []).concat(compareCompositions || []), 'identifier')?.map((c) => {
|
|
96
|
-
const href = !compareState?.controlled
|
|
97
|
-
? useUpdatedUrlFromQuery({
|
|
98
|
-
compositionBaseFile: selectedBaseComp?.identifier,
|
|
99
|
-
compositionCompareFile: c.identifier,
|
|
100
|
-
})
|
|
101
|
-
: useUpdatedUrlFromQuery({});
|
|
102
|
-
|
|
103
|
-
const onClick = compareState?.controlled ? (_, __) => {
|
|
104
|
-
compareHooks?.onClick?.(_, __);
|
|
105
|
-
baseHooks?.onClick?.(_, __);
|
|
106
|
-
} : undefined;
|
|
107
|
-
return { id: c.identifier, label: c.displayName, href, onClick };
|
|
108
|
-
});
|
|
109
424
|
|
|
110
|
-
const
|
|
111
|
-
|
|
425
|
+
const requestedCompositionId = useMemo(
|
|
426
|
+
() =>
|
|
427
|
+
resolveRequestedCompositionId({
|
|
428
|
+
selectedCompositionCompareFile,
|
|
429
|
+
selectedCompositionBaseFile,
|
|
430
|
+
compareStateId: compareState?.id,
|
|
431
|
+
baseStateId: baseContext?.state?.preview?.id,
|
|
432
|
+
compareCompositions,
|
|
433
|
+
baseCompositions,
|
|
434
|
+
}),
|
|
435
|
+
[
|
|
436
|
+
selectedCompositionCompareFile,
|
|
437
|
+
selectedCompositionBaseFile,
|
|
438
|
+
compareState?.id,
|
|
439
|
+
baseContext?.state?.preview?.id,
|
|
440
|
+
compareCompositions,
|
|
441
|
+
baseCompositions,
|
|
442
|
+
]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const selectedBaseComp = findComposition(baseCompositions, requestedCompositionId);
|
|
446
|
+
const selectedCompareComp = findComposition(compareCompositions, requestedCompositionId);
|
|
447
|
+
|
|
448
|
+
const baseMissingSelectedCompoisition = hasMissingComposition(requestedCompositionId, selectedBaseComp);
|
|
449
|
+
const compareMissingSelectedComposition = hasMissingComposition(requestedCompositionId, selectedCompareComp);
|
|
112
450
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
451
|
+
const baseCompositionIds = useMemo(
|
|
452
|
+
() => new Set((baseCompositions || []).map((c) => c.identifier)),
|
|
453
|
+
[baseCompositions]
|
|
454
|
+
);
|
|
455
|
+
const compareCompositionIds = useMemo(
|
|
456
|
+
() => new Set((compareCompositions || []).map((c) => c.identifier)),
|
|
457
|
+
[compareCompositions]
|
|
117
458
|
);
|
|
118
459
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
460
|
+
const compositionsDropdownSource = useMemo(() => {
|
|
461
|
+
return uniqBy((baseCompositions || []).concat(compareCompositions || []), 'identifier')?.map((c) => {
|
|
462
|
+
const hasInBase = baseCompositionIds.has(c.identifier);
|
|
463
|
+
const hasInCompare = compareCompositionIds.has(c.identifier);
|
|
464
|
+
const tag = getCompositionTag(hasInBase, hasInCompare);
|
|
465
|
+
const href = !compareState?.controlled
|
|
466
|
+
? buildUpdatedUrlFromQuery(query, location.pathname, {
|
|
467
|
+
compositionBaseFile: c.identifier,
|
|
468
|
+
compositionCompareFile: c.identifier,
|
|
469
|
+
})
|
|
470
|
+
: buildUpdatedUrlFromQuery(query, location.pathname, {});
|
|
471
|
+
const onClick = compareState?.controlled
|
|
472
|
+
? (id, e) => {
|
|
473
|
+
compareHooks?.onClick?.(id, e);
|
|
474
|
+
baseHooks?.onClick?.(id, e);
|
|
475
|
+
}
|
|
476
|
+
: undefined;
|
|
477
|
+
return { id: c.identifier, label: c.displayName, href, onClick, tag };
|
|
478
|
+
});
|
|
479
|
+
}, [
|
|
480
|
+
baseCompositions,
|
|
481
|
+
compareCompositions,
|
|
482
|
+
baseCompositionIds,
|
|
483
|
+
compareCompositionIds,
|
|
484
|
+
compareState?.controlled,
|
|
485
|
+
compareHooks,
|
|
486
|
+
baseHooks,
|
|
487
|
+
query,
|
|
488
|
+
location.pathname,
|
|
489
|
+
]);
|
|
123
490
|
|
|
124
|
-
const selectedCompareDropdown =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
491
|
+
const selectedCompareDropdown = useMemo(() => {
|
|
492
|
+
const found =
|
|
493
|
+
compositionsDropdownSource.find((item) => item.id === selectedCompareComp?.identifier) ||
|
|
494
|
+
compositionsDropdownSource.find((item) => item.id === selectedBaseComp?.identifier);
|
|
495
|
+
if (found) return found;
|
|
496
|
+
if (requestedCompositionId) return { id: requestedCompositionId, label: requestedCompositionId, tag: 'Missing' };
|
|
497
|
+
return undefined;
|
|
498
|
+
}, [
|
|
499
|
+
compositionsDropdownSource,
|
|
500
|
+
selectedCompareComp?.identifier,
|
|
501
|
+
selectedBaseComp?.identifier,
|
|
502
|
+
requestedCompositionId,
|
|
503
|
+
]);
|
|
504
|
+
|
|
505
|
+
const baseIdStr = base?.model.id?.toString();
|
|
506
|
+
const compareIdStr = compare?.model.id?.toString();
|
|
507
|
+
useDefaultControlsSchemaResponder(baseIdStr, Boolean(baseIdStr));
|
|
508
|
+
useDefaultControlsSchemaResponder(compareIdStr, Boolean(compareIdStr));
|
|
509
|
+
|
|
510
|
+
const baseCompId = resolveCompositionId(selectedBaseComp, requestedCompositionId);
|
|
511
|
+
const compareCompId = resolveCompositionId(selectedCompareComp, requestedCompositionId);
|
|
512
|
+
|
|
513
|
+
const baseChannelKey = useMemo(() => buildChannelKey('base', baseIdStr, baseCompId), [baseIdStr, baseCompId]);
|
|
514
|
+
const compareChannelKey = useMemo(
|
|
515
|
+
() => buildChannelKey('compare', compareIdStr, compareCompId),
|
|
516
|
+
[compareIdStr, compareCompId]
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const baseQuery = useMemo(() => buildQueryParams(baseChannelKey), [baseChannelKey]);
|
|
520
|
+
const compareQuery = useMemo(() => buildQueryParams(compareChannelKey), [compareChannelKey]);
|
|
521
|
+
|
|
522
|
+
const controlsResetKey = buildControlsResetKey(baseChannelKey, compareChannelKey);
|
|
523
|
+
const hasControlChannels = Boolean(baseChannelKey && compareChannelKey);
|
|
524
|
+
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
setEverHadControls(false);
|
|
527
|
+
setControlsStatus('loading');
|
|
528
|
+
}, [controlsResetKey]);
|
|
529
|
+
|
|
530
|
+
const handleControlsStatusChange = useCallback((status: ControlsStatus) => {
|
|
531
|
+
setControlsStatus(status);
|
|
532
|
+
if (status === 'available') setEverHadControls(true);
|
|
533
|
+
}, []);
|
|
534
|
+
|
|
535
|
+
const showControlsPanel = controlsStatus === 'available' || everHadControls;
|
|
536
|
+
|
|
537
|
+
const baseModel = base?.model;
|
|
538
|
+
const compareModel = compare?.model;
|
|
539
|
+
const stableLabels = useStableControlLabels(baseModel, compareModel, compare?.hasLocalChanges);
|
|
128
540
|
|
|
129
541
|
const BaseLayout = useMemo(() => {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const baseCompModel = base.model;
|
|
134
|
-
const compositionProps = {
|
|
135
|
-
forceHeight: undefined,
|
|
136
|
-
innerBottomPadding: 50,
|
|
137
|
-
...previewViewProps,
|
|
138
|
-
emptyState,
|
|
139
|
-
component: baseCompModel,
|
|
140
|
-
queryParams: baseCompQueryParams,
|
|
141
|
-
selected: selectedCompareComp,
|
|
142
|
-
}
|
|
542
|
+
if (!isStableData) return null;
|
|
543
|
+
if (baseMissingSelectedCompoisition)
|
|
544
|
+
return <MissingComposition compositionId={requestedCompositionId} version="base" />;
|
|
143
545
|
return (
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
/>
|
|
156
|
-
</CompositionContextProvider>
|
|
157
|
-
</CompositionCompareContext.Provider>
|
|
158
|
-
</div>
|
|
546
|
+
<CompositionLayout
|
|
547
|
+
model={baseModel}
|
|
548
|
+
selected={selectedBaseComp}
|
|
549
|
+
queryParams={baseQuery.queryString}
|
|
550
|
+
compositionParams={baseQuery.params}
|
|
551
|
+
previewViewProps={previewViewProps}
|
|
552
|
+
emptyState={emptyState}
|
|
553
|
+
PreviewView={PreviewView}
|
|
554
|
+
contextKey={`base-${baseIdStr}-${baseCompId}`}
|
|
555
|
+
isBase
|
|
556
|
+
/>
|
|
159
557
|
);
|
|
160
|
-
}, [
|
|
558
|
+
}, [
|
|
559
|
+
isStableData,
|
|
560
|
+
baseModel,
|
|
561
|
+
baseIdStr,
|
|
562
|
+
baseCompId,
|
|
563
|
+
baseChannelKey,
|
|
564
|
+
selectedBaseComp?.identifier,
|
|
565
|
+
baseQuery,
|
|
566
|
+
previewViewProps,
|
|
567
|
+
emptyState,
|
|
568
|
+
baseMissingSelectedCompoisition,
|
|
569
|
+
requestedCompositionId,
|
|
570
|
+
]);
|
|
161
571
|
|
|
162
572
|
const CompareLayout = useMemo(() => {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const compareCompModel = compare.model;
|
|
167
|
-
const compositionProps = {
|
|
168
|
-
forceHeight: undefined,
|
|
169
|
-
innerBottomPadding: 50,
|
|
170
|
-
...previewViewProps,
|
|
171
|
-
emptyState,
|
|
172
|
-
component: compareCompModel,
|
|
173
|
-
queryParams: compareCompQueryParams,
|
|
174
|
-
selected: selectedCompareComp,
|
|
175
|
-
}
|
|
573
|
+
if (!isStableData) return null;
|
|
574
|
+
if (compareMissingSelectedComposition)
|
|
575
|
+
return <MissingComposition compositionId={requestedCompositionId} version="compare" />;
|
|
176
576
|
return (
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/>
|
|
189
|
-
</CompositionContextProvider>
|
|
190
|
-
</CompositionCompareContext.Provider>
|
|
191
|
-
|
|
192
|
-
</div>
|
|
577
|
+
<CompositionLayout
|
|
578
|
+
model={compareModel}
|
|
579
|
+
selected={selectedCompareComp}
|
|
580
|
+
queryParams={compareQuery.queryString}
|
|
581
|
+
compositionParams={compareQuery.params}
|
|
582
|
+
previewViewProps={previewViewProps}
|
|
583
|
+
emptyState={emptyState}
|
|
584
|
+
PreviewView={PreviewView}
|
|
585
|
+
contextKey={`compare-${compareIdStr}-${compareCompId}`}
|
|
586
|
+
isCompare
|
|
587
|
+
/>
|
|
193
588
|
);
|
|
194
|
-
}, [
|
|
589
|
+
}, [
|
|
590
|
+
isStableData,
|
|
591
|
+
compareModel,
|
|
592
|
+
compareIdStr,
|
|
593
|
+
compareCompId,
|
|
594
|
+
compareChannelKey,
|
|
595
|
+
selectedCompareComp?.identifier,
|
|
596
|
+
compareQuery,
|
|
597
|
+
previewViewProps,
|
|
598
|
+
emptyState,
|
|
599
|
+
compareMissingSelectedComposition,
|
|
600
|
+
requestedCompositionId,
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
const toggleControls = useCallback(() => setControlsOpen((x) => !x), []);
|
|
604
|
+
const handleHeaderKeyDown = useCallback(
|
|
605
|
+
(e: React.KeyboardEvent) => {
|
|
606
|
+
if (e.key === 'Enter' || e.key === ' ') toggleControls();
|
|
607
|
+
},
|
|
608
|
+
[toggleControls]
|
|
609
|
+
);
|
|
195
610
|
|
|
196
|
-
const
|
|
197
|
-
if (!base && !compare) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
611
|
+
const key = `${base?.model.id.toString()}-${compare?.model.id.toString()}-composition-compare`;
|
|
200
612
|
|
|
201
|
-
|
|
613
|
+
if (!contextLoading && !baseCompositions?.length && !compareCompositions?.length) {
|
|
614
|
+
return <MissingCompositionTemplate title={`No Compositions Available`} />;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<div key={key} className={classNames(styles.container, { [styles.isResizing]: isResizing })}>
|
|
619
|
+
{contextLoading && (
|
|
620
|
+
<div className={styles.loader}>
|
|
621
|
+
<RoundLoader />
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
202
624
|
<div className={styles.toolbar}>
|
|
203
625
|
<div className={styles.left}>
|
|
204
626
|
<div className={styles.dropdown}>
|
|
@@ -212,22 +634,47 @@ export function CompositionCompare(props: CompositionCompareProps) {
|
|
|
212
634
|
<div className={styles.widgets}>{Widgets?.Right}</div>
|
|
213
635
|
</div>
|
|
214
636
|
</div>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const key = `${componentCompareContext?.base?.model.id.toString()}-${componentCompareContext?.compare?.model.id.toString()}-composition-compare`;
|
|
219
|
-
|
|
220
|
-
return (
|
|
221
|
-
<React.Fragment key={key}>
|
|
222
|
-
{componentCompareContext?.loading && (
|
|
223
|
-
<div className={styles.loader}>
|
|
224
|
-
<RoundLoader />
|
|
637
|
+
<div className={styles.compareLayout}>
|
|
638
|
+
<div className={styles.compareMain}>
|
|
639
|
+
<CompareSplitLayoutPreset base={BaseLayout} compare={CompareLayout} />
|
|
225
640
|
</div>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
641
|
+
{hasControlChannels && (
|
|
642
|
+
<div
|
|
643
|
+
ref={panelRef}
|
|
644
|
+
className={styles.controlsPanel}
|
|
645
|
+
style={showControlsPanel ? (isControlsOpen ? { height: panelHeight } : undefined) : { display: 'none' }}
|
|
646
|
+
>
|
|
647
|
+
<div
|
|
648
|
+
className={styles.controlsResizeHandle}
|
|
649
|
+
onMouseDown={handleResizeStart}
|
|
650
|
+
role="separator"
|
|
651
|
+
aria-orientation="horizontal"
|
|
652
|
+
/>
|
|
653
|
+
<div
|
|
654
|
+
className={styles.controlsPanelHeader}
|
|
655
|
+
onClick={toggleControls}
|
|
656
|
+
onKeyDown={handleHeaderKeyDown}
|
|
657
|
+
role="button"
|
|
658
|
+
tabIndex={0}
|
|
659
|
+
>
|
|
660
|
+
<Icon of={isControlsOpen ? 'fat-arrow-down' : 'fat-arrow-up'} className={styles.controlsArrow} />
|
|
661
|
+
<span className={styles.controlsPanelTitle}>Live controls</span>
|
|
662
|
+
</div>
|
|
663
|
+
<div className={styles.controlsPanelContent} style={{ display: isControlsOpen ? undefined : 'none' }}>
|
|
664
|
+
<LiveControlsDiffPanel
|
|
665
|
+
resetKey={controlsResetKey}
|
|
666
|
+
baseChannel={baseChannelKey}
|
|
667
|
+
compareChannel={compareChannelKey}
|
|
668
|
+
commonLabel={stableLabels.common}
|
|
669
|
+
baseLabel={stableLabels.base}
|
|
670
|
+
compareLabel={stableLabels.compare}
|
|
671
|
+
showEmptyState={false}
|
|
672
|
+
onStatusChange={handleControlsStatusChange}
|
|
673
|
+
/>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
231
679
|
);
|
|
232
680
|
}
|
|
233
|
-
|