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