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