@webbio/strapi-plugin-page-builder 0.8.5-platform → 0.9.1-platform

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.
Files changed (28) hide show
  1. package/admin/src/components/PlatformFilteredSelectField/Multi/index.tsx +19 -7
  2. package/admin/src/components/PlatformFilteredSelectField/hooks/useRelationLoad.tsx +4 -2
  3. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/components/Relations/RelationInput.tsx +689 -0
  4. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/components/Relations/RelationInputDataManager.tsx +6 -0
  5. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/components/Relations/useRelation.ts +170 -0
  6. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/components/Relations/utils/getRelationLink.ts +5 -0
  7. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/components/Relations/utils/normalizeRelations.ts +52 -0
  8. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/constants/attributes.ts +3 -0
  9. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/hooks/useDragAndDrop.ts +253 -0
  10. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/hooks/useKeyboardDragAndDrop.ts +96 -0
  11. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/hooks/usePrev.ts +11 -0
  12. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/utils/dragAndDrop.ts +8 -0
  13. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/utils/paths.ts +29 -0
  14. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/utils/refs.ts +19 -0
  15. package/admin/src/components/StrapiCore/admin/admin/src/content-manager/utils/translations.ts +3 -0
  16. package/admin/src/components/StrapiCore/content-manager/shared/contracts/collection-types.ts +300 -0
  17. package/admin/src/components/StrapiCore/content-manager/shared/contracts/components.ts +72 -0
  18. package/admin/src/components/StrapiCore/content-manager/shared/contracts/content-types.ts +116 -0
  19. package/admin/src/components/StrapiCore/content-manager/shared/contracts/index.ts +8 -0
  20. package/admin/src/components/StrapiCore/content-manager/shared/contracts/init.ts +22 -0
  21. package/admin/src/components/StrapiCore/content-manager/shared/contracts/relations.ts +80 -0
  22. package/admin/src/components/StrapiCore/content-manager/shared/contracts/review-workflows.ts +88 -0
  23. package/admin/src/components/StrapiCore/content-manager/shared/contracts/single-types.ts +112 -0
  24. package/admin/src/components/StrapiCore/content-manager/shared/contracts/uid.ts +48 -0
  25. package/admin/src/components/StrapiCore/content-manager/shared/index.ts +1 -0
  26. package/dist/package.json +1 -1
  27. package/dist/tsconfig.server.tsbuildinfo +1 -1
  28. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ Status,
5
+ Box,
6
+ Link,
7
+ Icon,
8
+ Flex,
9
+ TextButton,
10
+ Typography,
11
+ Tooltip,
12
+ VisuallyHidden,
13
+ Combobox,
14
+ IconButton,
15
+ FlexProps,
16
+ ComboboxOption,
17
+ ComboboxProps
18
+ } from '@strapi/design-system';
19
+ import { pxToRem, useFocusInputField } from '@strapi/helper-plugin';
20
+ import { Cross, Drag, Refresh } from '@strapi/icons';
21
+ import { getEmptyImage } from 'react-dnd-html5-backend';
22
+ import { useIntl } from 'react-intl';
23
+ import { FixedSizeList, FixedSizeList as List, ListChildComponentProps } from 'react-window';
24
+ import styled from 'styled-components';
25
+
26
+ import { UseDragAndDropOptions, useDragAndDrop, DROP_SENSITIVITY } from '../../hooks/useDragAndDrop';
27
+ import { usePrev } from '../../hooks/usePrev';
28
+ import { ItemTypes } from '../../utils/dragAndDrop';
29
+ import { composeRefs } from '../../utils/refs';
30
+ import { getTranslation } from '../../utils/translations';
31
+
32
+ import type { NormalizedRelation } from './utils/normalizeRelations';
33
+ import type { Contracts } from '../../../../../../content-manager/shared';
34
+ import type { Entity } from '@strapi/types';
35
+
36
+ const RELATION_ITEM_HEIGHT = 50;
37
+ const RELATION_GUTTER = 4;
38
+
39
+ /* -------------------------------------------------------------------------------------------------
40
+ * RelationInput
41
+ * -----------------------------------------------------------------------------------------------*/
42
+
43
+ interface RelationInputProps
44
+ extends Pick<
45
+ // @ts-ignore
46
+ ComboboxProps,
47
+ 'disabled' | 'error' | 'id' | 'labelAction' | 'placeholder' | 'required'
48
+ >,
49
+ Pick<RelationItemProps, 'onCancel' | 'onDropItem' | 'onGrabItem' | 'iconButtonAriaLabel'> {
50
+ canReorder: boolean;
51
+ // @ts-ignore
52
+ description: ComboboxProps['hint'];
53
+ numberOfRelationsToDisplay: number;
54
+ label: string;
55
+ labelLoadMore?: string;
56
+ labelDisconnectRelation: string;
57
+ listAriaDescription: string;
58
+ liveText: string;
59
+ loadingMessage: string;
60
+ name: string;
61
+ noRelationsMessage: string;
62
+ onRelationConnect: (relation: Contracts.Relations.RelationResult) => void;
63
+ onRelationLoadMore: () => void;
64
+ onRelationDisconnect: (relation: NormalizedRelation) => void;
65
+ onRelationReorder?: (currentIndex: number, newIndex: number) => void;
66
+ onSearchNextPage: () => void;
67
+ onSearch: (searchTerm?: string) => void;
68
+ publicationStateTranslations: {
69
+ draft: string;
70
+ published: string;
71
+ };
72
+ relations: {
73
+ data: NormalizedRelation[];
74
+ isLoading: boolean;
75
+ isFetchingNextPage: boolean;
76
+ hasNextPage?: boolean;
77
+ };
78
+ searchResults: {
79
+ data: NormalizedRelation[];
80
+ isLoading: boolean;
81
+ hasNextPage?: boolean;
82
+ };
83
+ size: number;
84
+ }
85
+
86
+ const RelationInput = ({
87
+ canReorder,
88
+ description,
89
+ disabled,
90
+ error,
91
+ iconButtonAriaLabel,
92
+ id,
93
+ name,
94
+ numberOfRelationsToDisplay,
95
+ label,
96
+ labelAction,
97
+ labelLoadMore,
98
+ labelDisconnectRelation,
99
+ listAriaDescription,
100
+ liveText,
101
+ loadingMessage,
102
+ onCancel,
103
+ onDropItem,
104
+ onGrabItem,
105
+ noRelationsMessage,
106
+ onRelationConnect,
107
+ onRelationLoadMore,
108
+ onRelationDisconnect,
109
+ onRelationReorder,
110
+ onSearchNextPage,
111
+ onSearch,
112
+ placeholder,
113
+ publicationStateTranslations,
114
+ required,
115
+ relations: paginatedRelations,
116
+ searchResults,
117
+ size
118
+ }: RelationInputProps) => {
119
+ const [textValue, setTextValue] = React.useState<string | undefined>('');
120
+ const [overflow, setOverflow] = React.useState<'top' | 'bottom' | 'top-bottom'>();
121
+
122
+ const listRef = React.useRef<FixedSizeList>(null);
123
+ const outerListRef = React.useRef<HTMLUListElement>(null);
124
+
125
+ const fieldRef = useFocusInputField(name);
126
+
127
+ const { data } = searchResults;
128
+
129
+ const relations = paginatedRelations.data;
130
+ const totalNumberOfRelations = relations.length ?? 0;
131
+
132
+ const dynamicListHeight = React.useMemo(
133
+ () =>
134
+ totalNumberOfRelations > numberOfRelationsToDisplay
135
+ ? Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * (RELATION_ITEM_HEIGHT + RELATION_GUTTER) +
136
+ RELATION_ITEM_HEIGHT / 2
137
+ : Math.min(totalNumberOfRelations, numberOfRelationsToDisplay) * (RELATION_ITEM_HEIGHT + RELATION_GUTTER),
138
+ [totalNumberOfRelations, numberOfRelationsToDisplay]
139
+ );
140
+
141
+ const shouldDisplayLoadMoreButton = !!labelLoadMore && paginatedRelations.hasNextPage;
142
+
143
+ const options = React.useMemo(
144
+ () =>
145
+ data
146
+ .flat()
147
+ .filter(Boolean)
148
+ .map((result) => ({
149
+ ...result,
150
+ value: result.id,
151
+ label: result.mainField
152
+ })),
153
+ [data]
154
+ );
155
+
156
+ React.useEffect(() => {
157
+ if (totalNumberOfRelations <= numberOfRelationsToDisplay) {
158
+ return setOverflow(undefined);
159
+ }
160
+
161
+ const handleNativeScroll = (e: Event) => {
162
+ const el = e.target as HTMLUListElement;
163
+ const parentScrollContainerHeight = (el.parentNode as HTMLDivElement).scrollHeight;
164
+ const maxScrollBottom = el.scrollHeight - el.scrollTop;
165
+
166
+ if (el.scrollTop === 0) {
167
+ return setOverflow('bottom');
168
+ }
169
+
170
+ if (maxScrollBottom === parentScrollContainerHeight) {
171
+ return setOverflow('top');
172
+ }
173
+
174
+ return setOverflow('top-bottom');
175
+ };
176
+
177
+ const outerListRefCurrent = outerListRef?.current;
178
+
179
+ if (!paginatedRelations.isLoading && relations.length > 0 && outerListRefCurrent) {
180
+ outerListRef.current.addEventListener('scroll', handleNativeScroll);
181
+ }
182
+
183
+ return () => {
184
+ if (outerListRefCurrent) {
185
+ outerListRefCurrent.removeEventListener('scroll', handleNativeScroll);
186
+ }
187
+ };
188
+ }, [paginatedRelations, relations, numberOfRelationsToDisplay, totalNumberOfRelations]);
189
+
190
+ const handleMenuOpen = (isOpen?: boolean) => {
191
+ if (isOpen) {
192
+ onSearch(textValue);
193
+ }
194
+ };
195
+
196
+ const handleUpdatePositionOfRelation = (newIndex: number, currentIndex: number) => {
197
+ if (onRelationReorder && newIndex >= 0 && newIndex < relations.length) {
198
+ onRelationReorder(currentIndex, newIndex);
199
+ }
200
+ };
201
+
202
+ const previewRelationsLength = usePrev(relations.length);
203
+ const updatedRelationsWith = React.useRef<'onChange' | 'loadMore'>();
204
+
205
+ const handleLoadMore = () => {
206
+ updatedRelationsWith.current = 'loadMore';
207
+ onRelationLoadMore();
208
+ };
209
+
210
+ React.useEffect(() => {
211
+ if (updatedRelationsWith.current === 'onChange') {
212
+ setTextValue('');
213
+ }
214
+
215
+ if (updatedRelationsWith.current === 'onChange' && relations.length !== previewRelationsLength) {
216
+ listRef.current?.scrollToItem(relations.length, 'end');
217
+ updatedRelationsWith.current = undefined;
218
+ } else if (updatedRelationsWith.current === 'loadMore' && relations.length !== previewRelationsLength) {
219
+ listRef.current?.scrollToItem(0, 'start');
220
+ updatedRelationsWith.current = undefined;
221
+ }
222
+ }, [previewRelationsLength, relations]);
223
+
224
+ const ariaDescriptionId = `${name}-item-instructions`;
225
+
226
+ return (
227
+ <Flex direction="column" gap={3} justifyContent="space-between" alignItems="stretch" wrap="wrap">
228
+ <Flex direction="row" alignItems="end" justifyContent="end" gap={2} width="100%">
229
+ <ComboboxWrapper marginRight="auto" maxWidth={size <= 6 ? '100%' : '70%'} width="100%">
230
+ <Combobox
231
+ ref={fieldRef}
232
+ autocomplete="none"
233
+ error={error}
234
+ name={name}
235
+ hint={description}
236
+ id={id}
237
+ required={required}
238
+ label={label}
239
+ labelAction={labelAction}
240
+ disabled={disabled}
241
+ placeholder={placeholder}
242
+ hasMoreItems={searchResults.hasNextPage}
243
+ loading={searchResults.isLoading}
244
+ onOpenChange={handleMenuOpen}
245
+ noOptionsMessage={() => noRelationsMessage}
246
+ loadingMessage={loadingMessage}
247
+ onLoadMore={() => {
248
+ onSearchNextPage();
249
+ }}
250
+ textValue={textValue}
251
+ // @ts-ignore
252
+ onChange={(relationId) => {
253
+ if (!relationId) {
254
+ return;
255
+ }
256
+ onRelationConnect(data.flat().find((opt) => opt.id.toString() === relationId)!);
257
+ updatedRelationsWith.current = 'onChange';
258
+ }}
259
+ // @ts-ignore
260
+ onTextValueChange={(text) => {
261
+ setTextValue(text);
262
+ }}
263
+ // @ts-ignore
264
+ onInputChange={(event) => {
265
+ onSearch(event.currentTarget.value);
266
+ }}
267
+ >
268
+ {options.map((opt) => {
269
+ return <Option key={opt.id} {...opt} />;
270
+ })}
271
+ </Combobox>
272
+ </ComboboxWrapper>
273
+
274
+ {shouldDisplayLoadMoreButton && (
275
+ <TextButton
276
+ disabled={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
277
+ onClick={handleLoadMore}
278
+ loading={paginatedRelations.isLoading || paginatedRelations.isFetchingNextPage}
279
+ startIcon={<Refresh />}
280
+ // prevent the label from line-wrapping
281
+ shrink={0}
282
+ >
283
+ {labelLoadMore}
284
+ </TextButton>
285
+ )}
286
+ </Flex>
287
+
288
+ {relations.length > 0 && (
289
+ <ShadowBox overflowDirection={overflow}>
290
+ <VisuallyHidden id={ariaDescriptionId}>{listAriaDescription}</VisuallyHidden>
291
+ <VisuallyHidden aria-live="assertive">{liveText}</VisuallyHidden>
292
+ {/* @ts-expect-error – width is expected, but we've not needed to pass it before. */}
293
+ <List
294
+ height={dynamicListHeight}
295
+ ref={listRef}
296
+ outerRef={outerListRef}
297
+ itemCount={totalNumberOfRelations}
298
+ itemSize={RELATION_ITEM_HEIGHT + RELATION_GUTTER}
299
+ itemData={{
300
+ name,
301
+ ariaDescribedBy: ariaDescriptionId,
302
+ canDrag: canReorder,
303
+ disabled,
304
+ handleCancel: onCancel,
305
+ handleDropItem: onDropItem,
306
+ handleGrabItem: onGrabItem,
307
+ iconButtonAriaLabel,
308
+ labelDisconnectRelation,
309
+ onRelationDisconnect,
310
+ publicationStateTranslations,
311
+ relations,
312
+ updatePositionOfRelation: handleUpdatePositionOfRelation
313
+ }}
314
+ itemKey={(index) => `${relations[index].mainField}_${relations[index].id}`}
315
+ innerElementType="ol"
316
+ >
317
+ {ListItem}
318
+ </List>
319
+ </ShadowBox>
320
+ )}
321
+ </Flex>
322
+ );
323
+ };
324
+
325
+ const ComboboxWrapper = styled(Box)`
326
+ align-self: flex-start;
327
+ `;
328
+
329
+ const ShadowBox = styled(Box)<{ overflowDirection?: 'top-bottom' | 'top' | 'bottom' }>`
330
+ position: relative;
331
+ overflow: hidden;
332
+ flex: 1;
333
+
334
+ &:before,
335
+ &:after {
336
+ position: absolute;
337
+ width: 100%;
338
+ height: 4px;
339
+ z-index: 1;
340
+ }
341
+
342
+ &:before {
343
+ /* TODO: as for DS Table component we would need this to be handled by the DS theme */
344
+ content: '';
345
+ background: linear-gradient(rgba(3, 3, 5, 0.2) 0%, rgba(0, 0, 0, 0) 100%);
346
+ top: 0;
347
+ opacity: ${({ overflowDirection }) => (overflowDirection === 'top-bottom' || overflowDirection === 'top' ? 1 : 0)};
348
+ transition: opacity 0.2s ease-in-out;
349
+ }
350
+
351
+ &:after {
352
+ /* TODO: as for DS Table component we would need this to be handled by the DS theme */
353
+ content: '';
354
+ background: linear-gradient(0deg, rgba(3, 3, 5, 0.2) 0%, rgba(0, 0, 0, 0) 100%);
355
+ bottom: 0;
356
+ opacity: ${({ overflowDirection }) =>
357
+ overflowDirection === 'top-bottom' || overflowDirection === 'bottom' ? 1 : 0};
358
+ transition: opacity 0.2s ease-in-out;
359
+ }
360
+ `;
361
+
362
+ /* -------------------------------------------------------------------------------------------------
363
+ * Option
364
+ * -----------------------------------------------------------------------------------------------*/
365
+
366
+ const Option = ({
367
+ publicationState,
368
+ mainField,
369
+ id
370
+ }: Pick<NormalizedRelation, 'id' | 'mainField' | 'publicationState'>) => {
371
+ const { formatMessage } = useIntl();
372
+ const stringifiedDisplayValue = (mainField ?? id).toString();
373
+
374
+ if (publicationState) {
375
+ const isDraft = publicationState === 'draft';
376
+ const draftMessage = {
377
+ id: getTranslation('components.Select.draft-info-title'),
378
+ defaultMessage: 'State: Draft'
379
+ };
380
+ const publishedMessage = {
381
+ id: getTranslation('components.Select.publish-info-title'),
382
+ defaultMessage: 'State: Published'
383
+ };
384
+ const title = isDraft ? formatMessage(draftMessage) : formatMessage(publishedMessage);
385
+
386
+ return (
387
+ <ComboboxOption value={id.toString()} textValue={stringifiedDisplayValue}>
388
+ <Flex>
389
+ <StyledBullet title={title} isDraft={isDraft} />
390
+ <Typography ellipsis>{stringifiedDisplayValue}</Typography>
391
+ </Flex>
392
+ </ComboboxOption>
393
+ );
394
+ }
395
+
396
+ return (
397
+ <ComboboxOption value={id.toString()} textValue={stringifiedDisplayValue}>
398
+ {stringifiedDisplayValue}
399
+ </ComboboxOption>
400
+ );
401
+ };
402
+
403
+ const StyledBullet = styled.div<{ isDraft?: boolean }>`
404
+ flex-shrink: 0;
405
+ width: ${pxToRem(6)};
406
+ height: ${pxToRem(6)};
407
+ margin-right: ${({ theme }) => theme.spaces[2]};
408
+ background-color: ${({ theme, isDraft }) => theme.colors[isDraft ? 'secondary600' : 'success600']};
409
+ border-radius: 50%;
410
+ `;
411
+
412
+ /* -------------------------------------------------------------------------------------------------
413
+ * ListItem
414
+ * -----------------------------------------------------------------------------------------------*/
415
+
416
+ /**
417
+ * This is in a separate component to enforce passing all the props the component requires to react-window
418
+ * to ensure drag & drop correctly works.
419
+ */
420
+
421
+ interface ListItemProps extends Pick<RelationItemProps, 'index' | 'style'> {
422
+ data: Pick<
423
+ RelationItemProps,
424
+ 'ariaDescribedBy' | 'canDrag' | 'disabled' | 'iconButtonAriaLabel' | 'name' | 'updatePositionOfRelation'
425
+ > & {
426
+ handleCancel: RelationItemProps['onCancel'];
427
+ handleDropItem: RelationItemProps['onDropItem'];
428
+ handleGrabItem: RelationItemProps['onGrabItem'];
429
+ labelDisconnectRelation: string;
430
+ onRelationDisconnect: (relation: NormalizedRelation) => void;
431
+ publicationStateTranslations: {
432
+ draft: string;
433
+ published: string;
434
+ };
435
+ relations: NormalizedRelation[];
436
+ };
437
+ }
438
+
439
+ const ListItem = ({ data, index, style }: ListItemProps) => {
440
+ const {
441
+ ariaDescribedBy,
442
+ canDrag,
443
+ disabled,
444
+ handleCancel,
445
+ handleDropItem,
446
+ handleGrabItem,
447
+ iconButtonAriaLabel,
448
+ name,
449
+ labelDisconnectRelation,
450
+ onRelationDisconnect,
451
+ publicationStateTranslations,
452
+ relations,
453
+ updatePositionOfRelation
454
+ } = data;
455
+ const { publicationState, href, mainField, id } = relations[index];
456
+ const statusColor = publicationState === 'draft' ? 'secondary' : 'success';
457
+
458
+ return (
459
+ <RelationItem
460
+ ariaDescribedBy={ariaDescribedBy}
461
+ canDrag={canDrag}
462
+ disabled={disabled}
463
+ displayValue={String(mainField ?? id)}
464
+ iconButtonAriaLabel={iconButtonAriaLabel}
465
+ id={id}
466
+ index={index}
467
+ name={name}
468
+ endAction={
469
+ <DisconnectButton
470
+ data-testid={`remove-relation-${id}`}
471
+ disabled={disabled}
472
+ type="button"
473
+ onClick={() => onRelationDisconnect(relations[index])}
474
+ aria-label={labelDisconnectRelation}
475
+ >
476
+ <Icon width="12px" as={Cross} />
477
+ </DisconnectButton>
478
+ }
479
+ onCancel={handleCancel}
480
+ onDropItem={handleDropItem}
481
+ onGrabItem={handleGrabItem}
482
+ status={publicationState || undefined}
483
+ style={{
484
+ ...style,
485
+ bottom: style.bottom ?? 0 + RELATION_GUTTER,
486
+ height: style.height ?? 0 - RELATION_GUTTER
487
+ }}
488
+ updatePositionOfRelation={updatePositionOfRelation}
489
+ >
490
+ <Box minWidth={0} paddingTop={1} paddingBottom={1} paddingRight={4}>
491
+ <Tooltip description={mainField ?? `${id}`}>
492
+ {href ? (
493
+ <LinkEllipsis to={href}>{mainField ?? id}</LinkEllipsis>
494
+ ) : (
495
+ <Typography textColor={disabled ? 'neutral600' : 'primary600'} ellipsis>
496
+ {mainField ?? id}
497
+ </Typography>
498
+ )}
499
+ </Tooltip>
500
+ </Box>
501
+
502
+ {publicationState && (
503
+ <Status variant={statusColor} showBullet={false} size="S">
504
+ <Typography fontWeight="bold" textColor={`${statusColor}700`}>
505
+ {/* @ts-ignore */}
506
+ {publicationStateTranslations[publicationState]}
507
+ </Typography>
508
+ </Status>
509
+ )}
510
+ </RelationItem>
511
+ );
512
+ };
513
+
514
+ /* -------------------------------------------------------------------------------------------------
515
+ * DisconnectButton
516
+ * -----------------------------------------------------------------------------------------------*/
517
+
518
+ const DisconnectButton = styled.button`
519
+ svg path {
520
+ fill: ${({ theme, disabled }) => (disabled ? theme.colors.neutral600 : theme.colors.neutral500)};
521
+ }
522
+
523
+ &:hover svg path,
524
+ &:focus svg path {
525
+ fill: ${({ theme, disabled }) => !disabled && theme.colors.neutral600};
526
+ }
527
+ `;
528
+
529
+ /* -------------------------------------------------------------------------------------------------
530
+ * LinkEllipsis
531
+ * -----------------------------------------------------------------------------------------------*/
532
+
533
+ const LinkEllipsis = styled(Link)`
534
+ display: block;
535
+
536
+ > span {
537
+ white-space: nowrap;
538
+ overflow: hidden;
539
+ text-overflow: ellipsis;
540
+ display: block;
541
+ }
542
+ `;
543
+
544
+ /* -------------------------------------------------------------------------------------------------
545
+ * RelationItem
546
+ * -----------------------------------------------------------------------------------------------*/
547
+
548
+ interface RelationItemProps
549
+ extends Pick<UseDragAndDropOptions, 'onCancel' | 'onDropItem' | 'onGrabItem'>,
550
+ // @ts-ignore
551
+ Omit<FlexProps, 'id' | 'style'>,
552
+ Pick<ListChildComponentProps, 'style' | 'index'> {
553
+ ariaDescribedBy: string;
554
+ canDrag: boolean;
555
+ children: React.ReactNode;
556
+ displayValue: string;
557
+ disabled: boolean;
558
+ endAction: React.ReactNode;
559
+ iconButtonAriaLabel: string;
560
+ id: Entity.ID;
561
+ name: string;
562
+ status?: NormalizedRelation['publicationState'];
563
+ updatePositionOfRelation: UseDragAndDropOptions['onMoveItem'];
564
+ }
565
+
566
+ const RelationItem = ({
567
+ ariaDescribedBy,
568
+ children,
569
+ displayValue,
570
+ canDrag,
571
+ disabled,
572
+ endAction,
573
+ iconButtonAriaLabel,
574
+ style,
575
+ id,
576
+ index,
577
+ name,
578
+ onCancel,
579
+ onDropItem,
580
+ onGrabItem,
581
+ status,
582
+ updatePositionOfRelation,
583
+ ...props
584
+ }: RelationItemProps) => {
585
+ const [{ handlerId, isDragging, handleKeyDown }, relationRef, dropRef, dragRef, dragPreviewRef] = useDragAndDrop(
586
+ canDrag && !disabled,
587
+ {
588
+ type: `${ItemTypes.RELATION}_${name}`,
589
+ index,
590
+ item: {
591
+ displayedValue: displayValue,
592
+ status,
593
+ id,
594
+ index
595
+ },
596
+ onMoveItem: updatePositionOfRelation,
597
+ onDropItem,
598
+ onGrabItem,
599
+ onCancel,
600
+ dropSensitivity: DROP_SENSITIVITY.IMMEDIATE
601
+ }
602
+ );
603
+
604
+ const composedRefs = composeRefs(relationRef, dragRef);
605
+
606
+ React.useEffect(() => {
607
+ dragPreviewRef(getEmptyImage());
608
+ }, [dragPreviewRef]);
609
+
610
+ return (
611
+ <Box
612
+ style={style}
613
+ as="li"
614
+ ref={dropRef}
615
+ aria-describedby={ariaDescribedBy}
616
+ cursor={canDrag ? 'all-scroll' : 'default'}
617
+ >
618
+ {isDragging ? (
619
+ <RelationItemPlaceholder />
620
+ ) : (
621
+ <Flex
622
+ paddingTop={2}
623
+ paddingBottom={2}
624
+ paddingLeft={canDrag ? 2 : 4}
625
+ paddingRight={4}
626
+ hasRadius
627
+ borderColor="neutral200"
628
+ background={disabled ? 'neutral150' : 'neutral0'}
629
+ justifyContent="space-between"
630
+ ref={canDrag ? composedRefs : undefined}
631
+ data-handler-id={handlerId}
632
+ {...props}
633
+ >
634
+ <FlexWrapper gap={1}>
635
+ {canDrag ? (
636
+ <IconButton
637
+ forwardedAs="div"
638
+ role="button"
639
+ tabIndex={0}
640
+ aria-label={iconButtonAriaLabel}
641
+ borderWidth={0}
642
+ onKeyDown={handleKeyDown}
643
+ disabled={disabled}
644
+ >
645
+ <Drag />
646
+ </IconButton>
647
+ ) : null}
648
+ <ChildrenWrapper justifyContent="space-between">{children}</ChildrenWrapper>
649
+ </FlexWrapper>
650
+ {endAction && <Box paddingLeft={4}>{endAction}</Box>}
651
+ </Flex>
652
+ )}
653
+ </Box>
654
+ );
655
+ };
656
+
657
+ const RelationItemPlaceholder = () => (
658
+ <Box
659
+ paddingTop={2}
660
+ paddingBottom={2}
661
+ paddingLeft={4}
662
+ paddingRight={4}
663
+ hasRadius
664
+ borderStyle="dashed"
665
+ borderColor="primary600"
666
+ borderWidth="1px"
667
+ background="primary100"
668
+ height={`calc(100% - ${RELATION_GUTTER}px)`}
669
+ />
670
+ );
671
+
672
+ const FlexWrapper = styled(Flex)`
673
+ width: 100%;
674
+ /* Used to prevent endAction to be pushed out of container */
675
+ min-width: 0;
676
+
677
+ & > div[role='button'] {
678
+ cursor: all-scroll;
679
+ }
680
+ `;
681
+
682
+ const ChildrenWrapper = styled(Flex)`
683
+ width: 100%;
684
+ /* Used to prevent endAction to be pushed out of container */
685
+ min-width: 0;
686
+ `;
687
+
688
+ export { RelationInput, FlexWrapper, ChildrenWrapper, LinkEllipsis, DisconnectButton };
689
+ export type { RelationInputProps };
@@ -0,0 +1,6 @@
1
+ const PUBLICATION_STATES = {
2
+ DRAFT: 'draft',
3
+ PUBLISHED: 'published'
4
+ } as const;
5
+
6
+ export { PUBLICATION_STATES };