Skip to content

Commit f01e4ba

Browse files
authored
apps/ui: Add dynamic post collection widgets to the desk UI (#3419)
1 parent 008b87e commit f01e4ba

26 files changed

Lines changed: 1639 additions & 42 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { ActionButton } from './action-button';
22
export { ControlButton, IconControlButton } from './control-button';
3+
export { LoadingPlaceholder } from './loading-placeholder';
34
export { List, ListItem } from './list';
45
export { Divider, Surface } from './surface';
56
export * as Menu from './menu';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { clsx } from 'clsx';
2+
import styles from './style.module.css';
3+
import type { ComponentPropsWithoutRef } from 'react';
4+
5+
type LoadingPlaceholderProps = ComponentPropsWithoutRef< 'div' > & {
6+
text?: string;
7+
};
8+
9+
export function LoadingPlaceholder( { className, text, ...props }: LoadingPlaceholderProps ) {
10+
return (
11+
<div { ...props } className={ clsx( styles.placeholder, className ) }>
12+
{ text && <div className={ styles.title }>{ text }</div> }
13+
<div className={ styles.line } />
14+
<div className={ styles.shortLine } />
15+
</div>
16+
);
17+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.placeholder {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 8px;
5+
}
6+
7+
.title {
8+
font-size: 13px;
9+
line-height: 18px;
10+
font-weight: 500;
11+
color: var(--loading-placeholder-text-color, #50575e);
12+
}
13+
14+
.line {
15+
width: 68%;
16+
height: 8px;
17+
border-radius: 999px;
18+
background: linear-gradient(
19+
90deg,
20+
var(--loading-placeholder-line-color, #f0f0f0) 0%,
21+
var(--loading-placeholder-line-highlight-color, #fff) 45%,
22+
var(--loading-placeholder-line-color, #f0f0f0) 90%
23+
);
24+
background-size: 220% 100%;
25+
animation: shimmer 1.4s ease-in-out infinite;
26+
}
27+
28+
.shortLine {
29+
width: 44%;
30+
height: 8px;
31+
border-radius: 999px;
32+
background: var(--loading-placeholder-line-color, #f0f0f0);
33+
}
34+
35+
@keyframes shimmer {
36+
0% {
37+
background-position: 120% 0;
38+
}
39+
40+
100% {
41+
background-position: -120% 0;
42+
}
43+
}

apps/ui/src/ui-desks/desk/provider/editor-state.ts

Lines changed: 214 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
stackSelectedWidgetsInEditor as stackSelectionInEditor,
88
unstackSelectedWidgetsInEditor as unstackSelectionInEditor,
99
} from '@/ui-desks/stacks/editor-commands';
10-
import { getStackId } from '@/ui-desks/stacks/utils';
10+
import {
11+
getStackAnchorFromMember,
12+
getStackHome,
13+
getStackId,
14+
getStackOrder,
15+
getStackZIndexFromMember,
16+
} from '@/ui-desks/stacks/utils';
1117
import { createDeskWidget } from '@/ui-desks/widgets/create-widget';
1218
import { getSelectedWidgetToolbarItem } from '@/ui-desks/widgets/toolbar-selection';
1319
import {
@@ -16,6 +22,10 @@ import {
1622
canvasShapesToDeskStacks,
1723
deskConfigToCanvasShapes,
1824
deskWidgetToCanvasShape,
25+
getDerivedDeskCanvasRecordSourceId,
26+
hasOnlyDeskCanvasRecordResolutionStateChange,
27+
isDerivedDeskCanvasRecord,
28+
isPersistentDeskCanvasShape,
1929
} from '../tldraw-adapter';
2030
import { DESK_CONFIG_VERSION, type DeskConfig } from '../types';
2131
import type { SelectedWidgetToolbarItem, AddDeskWidgetOptions } from './context';
@@ -27,6 +37,12 @@ interface CanvasStoreChanges {
2737
removed: Record< string, unknown >;
2838
}
2939

40+
interface DerivedWidgetAnchor {
41+
x: number;
42+
y: number;
43+
zIndex?: string;
44+
}
45+
3046
export function hydrateEditorFromDesk( editor: Editor, desk: DeskConfig ) {
3147
const existingShapes = editor.getCurrentPageShapes();
3248
if ( existingShapes.length > 0 ) {
@@ -133,7 +149,7 @@ export function updateSelectedWidgetPropsInEditor(
133149

134150
export function removeSelectedWidgetFromEditor( editor: Editor ) {
135151
const selection = getCurrentSelectedWidgetSelection( editor );
136-
if ( ! selection ) {
152+
if ( ! selection || ! selection.item.canRemove ) {
137153
return false;
138154
}
139155

@@ -167,6 +183,37 @@ export function hasCameraChange( changes: CanvasStoreChanges ) {
167183
return records.some( isCameraRecord );
168184
}
169185

186+
export function hasPersistentDocumentChange( changes: CanvasStoreChanges ) {
187+
const addedOrRemovedRecords = [
188+
...Object.values( changes.added ),
189+
...Object.values( changes.removed ),
190+
];
191+
if ( addedOrRemovedRecords.some( isPersistentDocumentRecord ) ) {
192+
return true;
193+
}
194+
195+
return Object.values( changes.updated ).some( ( [ previousRecord, nextRecord ] ) => {
196+
if ( isShapeRecord( previousRecord ) || isShapeRecord( nextRecord ) ) {
197+
if ( hasOnlyDeskCanvasRecordResolutionStateChange( previousRecord, nextRecord ) ) {
198+
return false;
199+
}
200+
201+
if (
202+
isDerivedDeskCanvasRecord( previousRecord ) ||
203+
isDerivedDeskCanvasRecord( nextRecord )
204+
) {
205+
return hasDerivedShapePersistenceChange( previousRecord, nextRecord );
206+
}
207+
208+
return (
209+
isPersistentDocumentRecord( previousRecord ) || isPersistentDocumentRecord( nextRecord )
210+
);
211+
}
212+
213+
return true;
214+
} );
215+
}
216+
170217
function getCurrentSelectedWidgetSelection( editor: Editor ) {
171218
const selectedShapeIds = editor.getSelectedShapeIds();
172219
if ( selectedShapeIds.length === 0 ) {
@@ -178,6 +225,11 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
178225
return null;
179226
}
180227

228+
const derivedSourceSelection = getDerivedSourceWidgetSelection( editor, shapes as TLShape[] );
229+
if ( derivedSourceSelection ) {
230+
return derivedSourceSelection;
231+
}
232+
181233
const widgets = shapes.map( ( shape ) => canvasShapeToDeskWidget( shape as TLShape ) );
182234
if ( widgets.some( ( widget ) => ! widget ) ) {
183235
return null;
@@ -201,11 +253,18 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
201253
};
202254
}
203255

204-
function getCurrentDeskWidgets( editor: Editor ) {
205-
return editor
206-
.getCurrentPageShapes()
256+
export function getCurrentDeskWidgets( editor: Editor ) {
257+
const shapes = editor.getCurrentPageShapes();
258+
const derivedAnchors = getDerivedWidgetAnchorsBySourceId( shapes );
259+
260+
return shapes
261+
.filter( isPersistentDeskCanvasShape )
207262
.map( canvasShapeToDeskWidget )
208-
.filter( ( widget ) => widget !== null );
263+
.filter( ( widget ): widget is DeskWidget => widget !== null )
264+
.map( ( widget ) => {
265+
const anchor = derivedAnchors.get( widget.id );
266+
return anchor ? { ...widget, ...anchor } : widget;
267+
} );
209268
}
210269

211270
function getCurrentDeskStacks( editor: Editor ) {
@@ -220,6 +279,155 @@ function isCameraRecord( value: unknown ) {
220279
);
221280
}
222281

282+
function isShapeRecord( value: unknown ) {
283+
return (
284+
Boolean( value ) &&
285+
typeof value === 'object' &&
286+
( value as { typeName?: unknown } ).typeName === 'shape'
287+
);
288+
}
289+
290+
function isPersistentDocumentRecord( value: unknown ) {
291+
if ( isShapeRecord( value ) ) {
292+
return ! isDerivedDeskCanvasRecord( value );
293+
}
294+
295+
return true;
296+
}
297+
298+
function hasDerivedShapePersistenceChange( previousRecord: unknown, nextRecord: unknown ) {
299+
if (
300+
! isDerivedDeskCanvasRecord( previousRecord ) ||
301+
! isDerivedDeskCanvasRecord( nextRecord )
302+
) {
303+
return false;
304+
}
305+
306+
return (
307+
getDerivedShapePersistenceSignature( previousRecord ) !==
308+
getDerivedShapePersistenceSignature( nextRecord )
309+
);
310+
}
311+
312+
function getDerivedShapePersistenceSignature( value: unknown ) {
313+
const shape = value as Partial< TLShape >;
314+
const meta = ( shape.meta ?? {} ) as Record< string, unknown >;
315+
316+
return JSON.stringify( {
317+
x: shape.x,
318+
y: shape.y,
319+
rotation: shape.rotation,
320+
index: shape.index,
321+
deskStackExpanded: meta.deskStackExpanded,
322+
deskStackHomeX: meta.deskStackHomeX,
323+
deskStackHomeY: meta.deskStackHomeY,
324+
deskStackHomeZIndex: meta.deskStackHomeZIndex,
325+
} );
326+
}
327+
328+
function getDerivedSourceWidgetSelection( editor: Editor, shapes: TLShape[] ) {
329+
const sourceWidgetId = getDerivedSelectionSourceWidgetId( shapes );
330+
if ( ! sourceWidgetId ) {
331+
return null;
332+
}
333+
334+
const sourceShape = editor
335+
.getCurrentPageShapes()
336+
.find( ( shape ) => canvasShapeToDeskWidget( shape )?.id === sourceWidgetId );
337+
if ( ! sourceShape ) {
338+
return null;
339+
}
340+
341+
const sourceWidget = canvasShapeToDeskWidget( sourceShape );
342+
if ( ! sourceWidget ) {
343+
return null;
344+
}
345+
346+
const item = getSelectedWidgetToolbarItem( [ sourceWidget ], {
347+
stackIds: [],
348+
canRemove: false,
349+
} );
350+
if ( ! item ) {
351+
return null;
352+
}
353+
354+
return {
355+
item,
356+
shapes: [ sourceShape ],
357+
};
358+
}
359+
360+
function getDerivedSelectionSourceWidgetId( shapes: TLShape[] ) {
361+
let sourceWidgetId: string | null = null;
362+
for ( const shape of shapes ) {
363+
const nextSourceWidgetId = getDerivedDeskCanvasRecordSourceId( shape );
364+
if ( ! nextSourceWidgetId ) {
365+
return null;
366+
}
367+
368+
if ( sourceWidgetId === null ) {
369+
sourceWidgetId = nextSourceWidgetId;
370+
} else if ( sourceWidgetId !== nextSourceWidgetId ) {
371+
return null;
372+
}
373+
}
374+
375+
return sourceWidgetId;
376+
}
377+
378+
function getDerivedWidgetAnchorsBySourceId( shapes: TLShape[] ) {
379+
const shapesBySourceId = new Map< string, TLShape[] >();
380+
for ( const shape of shapes ) {
381+
const sourceWidgetId = getDerivedDeskCanvasRecordSourceId( shape );
382+
if ( ! sourceWidgetId ) {
383+
continue;
384+
}
385+
386+
shapesBySourceId.set( sourceWidgetId, [
387+
...( shapesBySourceId.get( sourceWidgetId ) ?? [] ),
388+
shape,
389+
] );
390+
}
391+
392+
return new Map(
393+
Array.from( shapesBySourceId, ( [ sourceWidgetId, sourceShapes ] ) => [
394+
sourceWidgetId,
395+
getDerivedWidgetAnchor( sourceShapes ),
396+
] ).filter( ( entry ): entry is [ string, DerivedWidgetAnchor ] => entry[ 1 ] !== null )
397+
);
398+
}
399+
400+
function getDerivedWidgetAnchor( shapes: TLShape[] ): DerivedWidgetAnchor | null {
401+
const firstShape = [ ...shapes ].sort( ( first, second ) => {
402+
const firstStackOrder = getStackOrder( first );
403+
const secondStackOrder = getStackOrder( second );
404+
return firstStackOrder - secondStackOrder || sortByIndex( second, first );
405+
} )[ 0 ];
406+
if ( ! firstShape ) {
407+
return null;
408+
}
409+
410+
const stackId = getStackId( firstShape );
411+
if ( ! stackId ) {
412+
return {
413+
x: firstShape.x,
414+
y: firstShape.y,
415+
zIndex: firstShape.index,
416+
};
417+
}
418+
419+
const order = getStackOrder( firstShape );
420+
const home = getStackHome( firstShape );
421+
if ( home ) {
422+
return home;
423+
}
424+
425+
return {
426+
...getStackAnchorFromMember( firstShape, order ),
427+
zIndex: getStackZIndexFromMember( firstShape.index, order ),
428+
};
429+
}
430+
223431
function ensureContentVisible( editor: Editor ) {
224432
const shapes = editor.getCurrentPageShapes();
225433
if ( shapes.length === 0 ) {

apps/ui/src/ui-desks/desk/provider/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import {
1212
createDeskConfigFromEditor,
1313
getCurrentSelectedWidgetToolbarItem,
1414
hasCameraChange,
15+
hasPersistentDocumentChange,
1516
hydrateEditorFromDesk,
1617
removeSelectedWidgetFromEditor,
1718
stackSelectedWidgetsInEditor,
1819
unstackSelectedWidgetsInEditor,
1920
updateSelectedWidgetPropsInEditor,
2021
} from './editor-state';
2122
import { useDeskPersistence } from './persistence';
23+
import { useDeskWidgetResolvers } from './resolvers';
2224
import type { Editor } from 'tldraw';
2325

2426
export { useDesk, useRegisterDeskEditor } from './context';
@@ -72,8 +74,10 @@ export function DeskProvider( { siteId, children }: DeskProviderProps ) {
7274
};
7375

7476
const unsubscribeDocument = editor.store.listen(
75-
() => {
76-
queueSave();
77+
( { changes } ) => {
78+
if ( hasPersistentDocumentChange( changes ) ) {
79+
queueSave();
80+
}
7781
syncSelectedWidgetToolbarItem();
7882
},
7983
{ scope: 'document' }
@@ -205,5 +209,10 @@ export function DeskProvider( { siteId, children }: DeskProviderProps ) {
205209
]
206210
);
207211

212+
useDeskWidgetResolvers( {
213+
editor,
214+
isEnabled: Boolean( siteId && isHydrated ),
215+
} );
216+
208217
return <DeskContext.Provider value={ value }>{ children }</DeskContext.Provider>;
209218
}

0 commit comments

Comments
 (0)