@contentful/field-editor-reference 6.19.0 → 6.19.1-canary.18

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 (42) hide show
  1. package/dist/cjs/__fixtures__/FakeSdk.js +13 -20
  2. package/dist/cjs/assets/WrappedAssetCard/AssetCardActions.js +11 -1
  3. package/dist/cjs/assets/WrappedAssetCard/FetchingWrappedAssetCard.js +10 -1
  4. package/dist/cjs/assets/WrappedAssetCard/WrappedAssetCard.js +4 -2
  5. package/dist/cjs/common/EntityStore.js +45 -26
  6. package/dist/cjs/common/MultipleReferenceEditor.js +6 -1
  7. package/dist/cjs/common/SingleReferenceEditor.js +8 -2
  8. package/dist/cjs/common/queryClient.js +4 -91
  9. package/dist/cjs/common/useContentTypePermissions.js +6 -9
  10. package/dist/cjs/common/useEditorPermissions.js +5 -1
  11. package/dist/cjs/components/LinkActions/LinkEntityActions.js +2 -0
  12. package/dist/cjs/entries/MultipleEntryReferenceEditor.js +2 -1
  13. package/dist/cjs/entries/SingleEntryReferenceEditor.js +1 -0
  14. package/dist/cjs/entries/WrappedEntryCard/FetchingWrappedEntryCard.js +10 -1
  15. package/dist/cjs/entries/WrappedEntryCard/WrappedEntryCard.js +13 -2
  16. package/dist/cjs/resources/Cards/ResourceCard.spec.js +28 -2
  17. package/dist/esm/__fixtures__/FakeSdk.js +13 -20
  18. package/dist/esm/assets/WrappedAssetCard/AssetCardActions.js +11 -1
  19. package/dist/esm/assets/WrappedAssetCard/FetchingWrappedAssetCard.js +10 -1
  20. package/dist/esm/assets/WrappedAssetCard/WrappedAssetCard.js +4 -2
  21. package/dist/esm/common/EntityStore.js +45 -26
  22. package/dist/esm/common/MultipleReferenceEditor.js +6 -1
  23. package/dist/esm/common/SingleReferenceEditor.js +8 -2
  24. package/dist/esm/common/queryClient.js +1 -47
  25. package/dist/esm/common/useContentTypePermissions.js +6 -9
  26. package/dist/esm/common/useEditorPermissions.js +5 -1
  27. package/dist/esm/components/LinkActions/LinkEntityActions.js +2 -0
  28. package/dist/esm/entries/MultipleEntryReferenceEditor.js +2 -1
  29. package/dist/esm/entries/SingleEntryReferenceEditor.js +1 -0
  30. package/dist/esm/entries/WrappedEntryCard/FetchingWrappedEntryCard.js +10 -1
  31. package/dist/esm/entries/WrappedEntryCard/WrappedEntryCard.js +13 -2
  32. package/dist/esm/resources/Cards/ResourceCard.spec.js +28 -2
  33. package/dist/types/assets/WrappedAssetCard/AssetCardActions.d.ts +1 -0
  34. package/dist/types/assets/WrappedAssetCard/FetchingWrappedAssetCard.d.ts +5 -1
  35. package/dist/types/assets/WrappedAssetCard/WrappedAssetCard.d.ts +2 -1
  36. package/dist/types/common/EntityStore.d.ts +4 -2
  37. package/dist/types/common/ReferenceEditor.d.ts +5 -1
  38. package/dist/types/common/customCardTypes.d.ts +1 -0
  39. package/dist/types/common/queryClient.d.ts +3 -7
  40. package/dist/types/entries/WrappedEntryCard/FetchingWrappedEntryCard.d.ts +5 -1
  41. package/dist/types/entries/WrappedEntryCard/WrappedEntryCard.d.ts +2 -1
  42. package/package.json +4 -4
@@ -35,15 +35,6 @@ export function newReferenceEditorFakeSdk(props) {
35
35
  const delay = (ms)=>{
36
36
  return new Promise((resolve)=>setTimeout(resolve, ms));
37
37
  };
38
- const localizeContentTypes = (contentTypes)=>{
39
- return contentTypes.map((contentType)=>({
40
- ...contentType,
41
- fields: contentType.fields.map((field)=>({
42
- ...field,
43
- localized: true
44
- }))
45
- }));
46
- };
47
38
  const sdk = {
48
39
  field,
49
40
  locales,
@@ -96,6 +87,19 @@ export function newReferenceEditorFakeSdk(props) {
96
87
  return contentTypes.published;
97
88
  }
98
89
  return Promise.reject({});
90
+ },
91
+ getMany: async ()=>{
92
+ return Promise.resolve({
93
+ items: [
94
+ contentTypes.published
95
+ ],
96
+ total: 1,
97
+ skip: 0,
98
+ limit: 1000,
99
+ sys: {
100
+ type: 'Array'
101
+ }
102
+ });
99
103
  }
100
104
  },
101
105
  Locale: {
@@ -104,17 +108,6 @@ export function newReferenceEditorFakeSdk(props) {
104
108
  },
105
109
  space: {
106
110
  ...space,
107
- getCachedContentTypes () {
108
- return localizeContentTypes(space.getCachedContentTypes());
109
- },
110
- getContentTypes () {
111
- return Promise.resolve(space.getContentTypes().then((response)=>{
112
- return {
113
- ...response,
114
- items: localizeContentTypes(response.items)
115
- };
116
- }));
117
- },
118
111
  async getEntityScheduledActions () {
119
112
  return [];
120
113
  }
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { MenuItem, Text, MenuSectionTitle } from '@contentful/f36-components';
3
+ import { PlusIcon } from '@contentful/f36-icons';
3
4
  import tokens from '@contentful/f36-tokens';
4
5
  import { shortenStorageUnit } from '@contentful/field-editor-shared';
5
6
  import { css } from 'emotion';
@@ -70,7 +71,7 @@ export function renderAssetInfo(props) {
70
71
  ];
71
72
  }
72
73
  export function renderActions(props) {
73
- const { entityFile, isDisabled, onEdit, onRemove } = props;
74
+ const { entityFile, isDisabled, onEdit, onRemove, onAddToReleaseAction } = props;
74
75
  return [
75
76
  /*#__PURE__*/ React.createElement(MenuSectionTitle, {
76
77
  key: "section-title"
@@ -80,6 +81,15 @@ export function renderActions(props) {
80
81
  onClick: onEdit,
81
82
  testId: "card-action-edit"
82
83
  }, "Edit") : null,
84
+ onAddToReleaseAction ? /*#__PURE__*/ React.createElement(MenuItem, {
85
+ key: "add-to-release",
86
+ testId: "add-to-release",
87
+ onClick: ()=>{
88
+ onAddToReleaseAction();
89
+ }
90
+ }, /*#__PURE__*/ React.createElement(PlusIcon, {
91
+ size: "tiny"
92
+ }), "Add to release") : null,
83
93
  entityFile ? /*#__PURE__*/ React.createElement(MenuItem, {
84
94
  key: "download",
85
95
  onClick: ()=>{
@@ -21,6 +21,14 @@ export function FetchingWrappedAssetCard(props) {
21
21
  locales: props.sdk.locales,
22
22
  isReference: true
23
23
  });
24
+ const onAddToRelease = ()=>{
25
+ if (asset && props.addReferenceToRelease) {
26
+ void props.addReferenceToRelease(asset, props.sdk.field.locale, {
27
+ openModalForVersionSelection: true,
28
+ skipNestedReferencesPrompt: true
29
+ });
30
+ }
31
+ };
24
32
  React.useEffect(()=>{
25
33
  if (asset) {
26
34
  props.onAction && props.onAction({
@@ -87,7 +95,8 @@ export function FetchingWrappedAssetCard(props) {
87
95
  activeLocales,
88
96
  releaseStatusMap,
89
97
  release: props.sdk.release,
90
- releaseEntityStatus
98
+ releaseEntityStatus,
99
+ onAddToRelease
91
100
  };
92
101
  if (status === 'loading') {
93
102
  return props.viewType === 'link' ? /*#__PURE__*/ React.createElement(EntryCard, {
@@ -31,7 +31,7 @@ function getFileType(file) {
31
31
  return groupToIconMap[groupName] || 'archive';
32
32
  }
33
33
  const THUMBNAIL_SIZE = 150;
34
- export const WrappedAssetCard = ({ asset, className, size, localeCode, defaultLocaleCode, activeLocales, localesStatusMap, isDisabled, isSelected, isClickable, useLocalizedEntityStatus, renderDragHandle, getEntityScheduledActions, onEdit, getAssetUrl, onRemove, releaseEntityStatus, releaseStatusMap, release })=>{
34
+ export const WrappedAssetCard = ({ asset, className, size, localeCode, defaultLocaleCode, activeLocales, localesStatusMap, isDisabled, isSelected, isClickable, useLocalizedEntityStatus, renderDragHandle, getEntityScheduledActions, onEdit, getAssetUrl, onRemove, releaseEntityStatus, releaseStatusMap, release, onAddToRelease })=>{
35
35
  const status = entityHelpers.getEntityStatus(asset.sys, useLocalizedEntityStatus ? localeCode : undefined);
36
36
  const entityFile = asset.fields.file ? asset.fields.file[localeCode] || asset.fields.file[defaultLocaleCode] : undefined;
37
37
  const imageUrl = React.useMemo(()=>{
@@ -64,6 +64,7 @@ export const WrappedAssetCard = ({ asset, className, size, localeCode, defaultLo
64
64
  defaultTitle: 'Untitled'
65
65
  });
66
66
  const href = getAssetUrl ? getAssetUrl(asset.sys.id) : undefined;
67
+ const onAddToReleaseAction = releaseEntityStatus === 'notInRelease' && release !== undefined && onAddToRelease !== undefined && !isDisabled ? onAddToRelease : undefined;
67
68
  return /*#__PURE__*/ React.createElement(AssetCard, {
68
69
  as: isClickable && href ? 'a' : 'article',
69
70
  type: getFileType(entityFile),
@@ -102,7 +103,8 @@ export const WrappedAssetCard = ({ asset, className, size, localeCode, defaultLo
102
103
  entityFile,
103
104
  isDisabled: isDisabled,
104
105
  onEdit,
105
- onRemove
106
+ onRemove,
107
+ onAddToReleaseAction
106
108
  }),
107
109
  ...entityFile ? renderAssetInfo({
108
110
  entityFile
@@ -12,6 +12,7 @@ function _define_property(obj, key, value) {
12
12
  return obj;
13
13
  }
14
14
  import React, { useCallback, useEffect, useMemo, useRef } from 'react';
15
+ import { createGetContentTypeKey, createGetEntryKey, createGetSpaceKey } from '@contentful/field-editor-shared';
15
16
  import constate from 'constate';
16
17
  import { fetchAll } from 'contentful-management';
17
18
  import { get } from 'lodash';
@@ -61,7 +62,7 @@ function handleResourceFetchError(resourceFetchError, resourceTypeEntity) {
61
62
  throw resourceFetchError;
62
63
  }
63
64
  const isEntityQueryKey = (queryKey)=>{
64
- return Array.isArray(queryKey) && (queryKey[0] === 'Entry' || queryKey[0] === 'Asset') && queryKey.length === 4;
65
+ return Array.isArray(queryKey) && (queryKey[0] === 'Entry' || queryKey[0] === 'Asset') && (queryKey.length === 4 || queryKey.length === 5);
65
66
  };
66
67
  async function fetchContentfulEntry({ urn, fetch, options }) {
67
68
  const resourceId = urn.split(':', 6)[5];
@@ -74,18 +75,10 @@ async function fetchContentfulEntry({ urn, fetch, options }) {
74
75
  const environmentId = resourceIdMatch?.groups?.environmentId || 'master';
75
76
  const entryId = resourceIdMatch.groups.entityId;
76
77
  const [space, entry] = await Promise.all([
77
- fetch([
78
- 'space',
79
- spaceId
80
- ], ({ cmaClient })=>cmaClient.space.get({
78
+ fetch(createGetSpaceKey(spaceId), ({ cmaClient })=>cmaClient.space.get({
81
79
  spaceId
82
80
  }), options),
83
- fetch([
84
- 'entry',
85
- spaceId,
86
- environmentId,
87
- entryId
88
- ], ({ cmaClient })=>cmaClient.entry.get({
81
+ fetch(createGetEntryKey(spaceId, environmentId, entryId), ({ cmaClient })=>cmaClient.entry.get({
89
82
  spaceId,
90
83
  environmentId,
91
84
  entryId
@@ -93,12 +86,7 @@ async function fetchContentfulEntry({ urn, fetch, options }) {
93
86
  ]);
94
87
  const contentTypeId = entry.sys.contentType.sys.id;
95
88
  const [contentType, defaultLocaleCode] = await Promise.all([
96
- fetch([
97
- 'contentType',
98
- spaceId,
99
- environmentId,
100
- contentTypeId
101
- ], ({ cmaClient })=>cmaClient.contentType.get({
89
+ fetch(createGetContentTypeKey(spaceId, environmentId, contentTypeId), ({ cmaClient })=>cmaClient.contentType.get({
102
90
  contentTypeId,
103
91
  spaceId,
104
92
  environmentId
@@ -382,25 +370,53 @@ const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = cons
382
370
  const onSlideInNavigation = props.sdk.navigator.onSlideInNavigation;
383
371
  useEffect(()=>{
384
372
  function findSameSpaceQueries() {
385
- return queryCache.findAll({
386
- type: 'active',
373
+ const queries = queryCache.findAll({
387
374
  predicate: (query)=>isSameSpaceEntityQueryKey(query.queryKey)
388
375
  });
376
+ return queries;
389
377
  }
390
378
  if (typeof onEntityChanged !== 'function') {
391
- return onSlideInNavigation(({ oldSlideLevel, newSlideLevel })=>{
379
+ return onSlideInNavigation(async ({ oldSlideLevel, newSlideLevel })=>{
392
380
  if (oldSlideLevel > newSlideLevel) {
393
- findSameSpaceQueries().forEach((query)=>{
394
- void queryClient.invalidateQueries(query.queryKey);
395
- });
381
+ const queries = findSameSpaceQueries();
382
+ await Promise.all(queries.map(async (query)=>{
383
+ const [entityType, entityId, spaceId, environmentId, releaseId] = query.queryKey;
384
+ try {
385
+ let freshData;
386
+ if (entityType === 'Entry') {
387
+ freshData = await cmaClient.entry.get({
388
+ entryId: entityId,
389
+ spaceId: spaceId,
390
+ environmentId: environmentId,
391
+ releaseId: releaseId
392
+ });
393
+ } else if (entityType === 'Asset') {
394
+ freshData = await cmaClient.asset.get({
395
+ assetId: entityId,
396
+ spaceId: spaceId,
397
+ environmentId: environmentId,
398
+ releaseId: releaseId
399
+ });
400
+ } else {
401
+ await queryClient.invalidateQueries(query.queryKey);
402
+ return;
403
+ }
404
+ queryClient.setQueryData(query.queryKey, freshData);
405
+ } catch (error) {
406
+ await queryClient.invalidateQueries(query.queryKey);
407
+ }
408
+ }));
396
409
  }
397
410
  });
398
411
  }
399
412
  const subscribeQuery = ({ queryKey, queryHash })=>{
400
413
  const [entityType, entityId, , , releaseId] = queryKey;
401
414
  entityChangeUnsubscribers.current[queryHash] = onEntityChanged(entityType, entityId, (data)=>{
402
- if (get(data, 'sys.release.id') === releaseId) {
415
+ const dataReleaseId = get(data, 'sys.release.id');
416
+ if (dataReleaseId === releaseId) {
403
417
  queryClient.setQueryData(queryKey, data);
418
+ } else if (releaseId && !dataReleaseId) {
419
+ void queryClient.invalidateQueries(queryKey);
404
420
  }
405
421
  });
406
422
  };
@@ -432,7 +448,8 @@ const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = cons
432
448
  isSameSpaceEntityQueryKey,
433
449
  queryClient,
434
450
  getEntity,
435
- onSlideInNavigation
451
+ onSlideInNavigation,
452
+ cmaClient
436
453
  ]);
437
454
  const getResourceProvider = useCallback(function getResourceProvider(organizationId, appDefinitionId) {
438
455
  const queryKey = [
@@ -538,6 +555,8 @@ export function useResourceProvider(organizationId, appDefinitionId) {
538
555
  };
539
556
  }
540
557
  function EntityProvider({ children, ...props }) {
541
- return /*#__PURE__*/ React.createElement(SharedQueryClientProvider, null, /*#__PURE__*/ React.createElement(InternalServiceProvider, props, children));
558
+ return /*#__PURE__*/ React.createElement(SharedQueryClientProvider, {
559
+ client: props.queryClient
560
+ }, /*#__PURE__*/ React.createElement(InternalServiceProvider, props, children));
542
561
  }
543
562
  export { EntityProvider, useEntityLoader };
@@ -1,9 +1,11 @@
1
1
  import * as React from 'react';
2
2
  import { useCallback } from 'react';
3
+ import { useContentTypes } from '@contentful/field-editor-shared';
3
4
  import { arrayMove } from '@dnd-kit/sortable';
4
5
  import { LinkEntityActions } from '../components';
5
6
  import { useLinkActionsProps } from '../components/LinkActions/LinkEntityActions';
6
7
  import { useSortIDs } from '../utils/useSortIDs';
8
+ import { SharedQueryClientProvider } from './queryClient';
7
9
  import { ReferenceEditor } from './ReferenceEditor';
8
10
  import { useEditorPermissions } from './useEditorPermissions';
9
11
  function onLinkOrCreate(setValue, entityType, items, ids, index = items.length) {
@@ -95,7 +97,10 @@ function Editor(props) {
95
97
  }));
96
98
  }
97
99
  export function MultipleReferenceEditor(props) {
98
- const allContentTypes = props.sdk.space.getCachedContentTypes();
100
+ return /*#__PURE__*/ React.createElement(SharedQueryClientProvider, null, /*#__PURE__*/ React.createElement(MultipleReferenceEditorInner, props));
101
+ }
102
+ function MultipleReferenceEditorInner(props) {
103
+ const { contentTypes: allContentTypes } = useContentTypes(props.sdk);
99
104
  return /*#__PURE__*/ React.createElement(ReferenceEditor, props, ({ value, disabled, setValue, externalReset })=>{
100
105
  return /*#__PURE__*/ React.createElement(Editor, {
101
106
  ...props,
@@ -1,7 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { useCallback } from 'react';
3
+ import { useContentTypes } from '@contentful/field-editor-shared';
3
4
  import { LinkEntityActions } from '../components';
4
5
  import { useLinkActionsProps } from '../components/LinkActions/LinkEntityActions';
6
+ import { SharedQueryClientProvider } from './queryClient';
5
7
  import { ReferenceEditor } from './ReferenceEditor';
6
8
  import { useEditorPermissions } from './useEditorPermissions';
7
9
  function Editor(props) {
@@ -48,11 +50,15 @@ function Editor(props) {
48
50
  }
49
51
  return props.children({
50
52
  ...props,
51
- renderCustomCard: props.renderCustomCard && customCardRenderer
53
+ renderCustomCard: props.renderCustomCard && customCardRenderer,
54
+ addReferenceToRelease: props.addReferenceToRelease
52
55
  });
53
56
  }
54
57
  export function SingleReferenceEditor(props) {
55
- const allContentTypes = props.sdk.space.getCachedContentTypes();
58
+ return /*#__PURE__*/ React.createElement(SharedQueryClientProvider, null, /*#__PURE__*/ React.createElement(SingleReferenceEditorInner, props));
59
+ }
60
+ function SingleReferenceEditorInner(props) {
61
+ const { contentTypes: allContentTypes } = useContentTypes(props.sdk);
56
62
  return /*#__PURE__*/ React.createElement(ReferenceEditor, props, ({ value, setValue, disabled, externalReset })=>{
57
63
  return /*#__PURE__*/ React.createElement(Editor, {
58
64
  ...props,
@@ -1,47 +1 @@
1
- import * as React from 'react';
2
- import { QueryClient, useQuery as useRQ, useQueryClient as useHostQueryClient } from '@tanstack/react-query';
3
- const clientContext = /*#__PURE__*/ React.createContext(undefined);
4
- function useMaybeHostQueryClient() {
5
- try {
6
- return useHostQueryClient();
7
- } catch {
8
- return undefined;
9
- }
10
- }
11
- export function useQueryClient() {
12
- const client = React.useContext(clientContext);
13
- const hostClient = useMaybeHostQueryClient();
14
- return React.useMemo(()=>{
15
- if (client) {
16
- return client;
17
- }
18
- if (hostClient) return hostClient;
19
- return new QueryClient({
20
- defaultOptions: {
21
- queries: {
22
- useErrorBoundary: false,
23
- refetchOnWindowFocus: false,
24
- refetchOnReconnect: true,
25
- refetchOnMount: false,
26
- staleTime: Infinity,
27
- retry: false
28
- }
29
- }
30
- });
31
- }, [
32
- client,
33
- hostClient
34
- ]);
35
- }
36
- export const useQuery = (key, fn, opt)=>{
37
- return useRQ(key, fn, {
38
- ...opt,
39
- context: clientContext
40
- });
41
- };
42
- export function SharedQueryClientProvider({ children }) {
43
- const client = useQueryClient();
44
- return /*#__PURE__*/ React.createElement(clientContext.Provider, {
45
- value: client
46
- }, children);
47
- }
1
+ export { SharedQueryClientProvider, useQueryClient, useQuery } from '@contentful/field-editor-shared';
@@ -11,6 +11,9 @@ export function useContentTypePermissions({ entityType, validations, sdk, allCon
11
11
  if (entityType === 'Asset') {
12
12
  return [];
13
13
  }
14
+ if (validations.contentTypes && allContentTypes.length === 0) {
15
+ return [];
16
+ }
14
17
  if (validations.contentTypes) {
15
18
  return allContentTypes.filter((ct)=>validations.contentTypes?.includes(ct.sys.id));
16
19
  }
@@ -23,21 +26,15 @@ export function useContentTypePermissions({ entityType, validations, sdk, allCon
23
26
  const [creatableContentTypes, setCreatableContentTypes] = useState(availableContentTypes);
24
27
  const { canPerformActionOnEntryOfType } = useAccessApi(sdk.access);
25
28
  useEffect(()=>{
26
- function getContentTypes(action) {
27
- return filter(availableContentTypes, (ct)=>canPerformActionOnEntryOfType(action, ct.sys.id));
28
- }
29
29
  async function checkContentTypeAccess() {
30
- const creatable = await getContentTypes('create');
31
- if (!isEqual(creatable, creatableContentTypes)) {
32
- setCreatableContentTypes(creatable);
33
- }
30
+ const creatable = await filter(availableContentTypes, (ct)=>canPerformActionOnEntryOfType('create', ct.sys.id));
31
+ setCreatableContentTypes((creatableContentTypes)=>isEqual(creatable, creatableContentTypes) ? creatableContentTypes : creatable);
34
32
  }
35
33
  if (availableContentTypes.length > 0) {
36
34
  void checkContentTypeAccess();
37
35
  }
38
36
  }, [
39
- availableContentTypes,
40
- creatableContentTypes
37
+ availableContentTypes
41
38
  ]);
42
39
  return {
43
40
  creatableContentTypes,
@@ -3,8 +3,12 @@ import { fromFieldValidations } from '../utils/fromFieldValidations';
3
3
  import { useAccessApi } from './useAccessApi';
4
4
  import { useContentTypePermissions } from './useContentTypePermissions';
5
5
  export function useEditorPermissions({ sdk, entityType, parameters, allContentTypes }) {
6
+ const fieldValidations = sdk.field.validations;
7
+ const itemsValidations = sdk.field.type === 'Array' ? sdk.field.items?.validations : undefined;
6
8
  const validations = useMemo(()=>fromFieldValidations(sdk.field), [
7
- sdk.field
9
+ sdk.field,
10
+ JSON.stringify(fieldValidations),
11
+ JSON.stringify(itemsValidations)
8
12
  ]);
9
13
  const [canCreateEntity, setCanCreateEntity] = useState(true);
10
14
  const [canLinkEntity, setCanLinkEntity] = useState(true);
@@ -69,6 +69,7 @@ export function useLinkActionsProps(props) {
69
69
  }, [
70
70
  sdk,
71
71
  entityType,
72
+ editorPermissions,
72
73
  onLinkedExisting
73
74
  ]);
74
75
  const onLinkSeveralExisting = React.useCallback(async (index)=>{
@@ -84,6 +85,7 @@ export function useLinkActionsProps(props) {
84
85
  }, [
85
86
  sdk,
86
87
  entityType,
88
+ editorPermissions,
87
89
  onLinkedExisting
88
90
  ]);
89
91
  return useMemo(()=>({
@@ -34,7 +34,8 @@ export function MultipleEntryReferenceEditor(props) {
34
34
  onMoveTop: index !== 0 ? ()=>childrenProps.onMove(index, 0) : undefined,
35
35
  onMoveBottom: index !== lastIndex ? ()=>childrenProps.onMove(index, lastIndex) : undefined,
36
36
  renderDragHandle: DragHandle,
37
- isBeingDragged: index === indexToUpdate
37
+ isBeingDragged: index === indexToUpdate,
38
+ addReferenceToRelease: props.addReferenceToRelease
38
39
  });
39
40
  }));
40
41
  }
@@ -17,6 +17,7 @@ export function SingleEntryReferenceEditor(props) {
17
17
  hasCardEditActions: hasCardEditActions,
18
18
  hasCardRemoveActions: hasCardRemoveActions,
19
19
  activeLocales: activeLocales,
20
+ addReferenceToRelease: props.addReferenceToRelease,
20
21
  onRemove: ()=>{
21
22
  setValue(null);
22
23
  }
@@ -63,6 +63,14 @@ export function FetchingWrappedEntryCard(props) {
63
63
  contentTypeId: entry?.sys?.contentType?.sys?.id ?? ''
64
64
  });
65
65
  };
66
+ const onAddToRelease = ()=>{
67
+ if (entry && props.addReferenceToRelease) {
68
+ void props.addReferenceToRelease(entry, props.sdk.field.locale, {
69
+ openModalForVersionSelection: true,
70
+ skipNestedReferencesPrompt: true
71
+ });
72
+ }
73
+ };
66
74
  React.useEffect(()=>{
67
75
  if (entry) {
68
76
  props.onAction?.({
@@ -117,7 +125,8 @@ export function FetchingWrappedEntryCard(props) {
117
125
  activeLocales: props.activeLocales,
118
126
  releaseStatusMap,
119
127
  release: props.sdk.release,
120
- releaseEntityStatus
128
+ releaseEntityStatus,
129
+ onAddToRelease
121
130
  };
122
131
  const { hasCardEditActions, hasCardMoveActions, hasCardRemoveActions } = props;
123
132
  function renderDefaultCard(props) {
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { EntryCard, MenuDivider, MenuItem } from '@contentful/f36-components';
3
+ import { PlusIcon } from '@contentful/f36-icons';
3
4
  import { entityHelpers, isValidImage } from '@contentful/field-editor-shared';
4
5
  import { AssetThumbnail, MissingEntityCard, EntityStatusBadge } from '../../components';
5
6
  import { SpaceName } from '../../components/SpaceName/SpaceName';
@@ -10,7 +11,7 @@ const defaultProps = {
10
11
  hasCardMoveActions: true,
11
12
  hasCardRemoveActions: true
12
13
  };
13
- export function WrappedEntryCard({ entry, entryUrl, contentType, activeLocales, localeCode, defaultLocaleCode, localesStatusMap, useLocalizedEntityStatus, size, spaceName, isClickable, isDisabled, isSelected, hasCardMoveActions, hasCardEditActions, hasCardRemoveActions, renderDragHandle, getAsset, getEntityScheduledActions, onClick, onEdit, onRemove, onMoveTop, onMoveBottom, releaseEntityStatus, releaseStatusMap, release }) {
14
+ export function WrappedEntryCard({ entry, entryUrl, contentType, activeLocales, localeCode, defaultLocaleCode, localesStatusMap, useLocalizedEntityStatus, size, spaceName, isClickable, isDisabled, isSelected, hasCardMoveActions, hasCardEditActions, hasCardRemoveActions, renderDragHandle, getAsset, getEntityScheduledActions, onClick, onEdit, onRemove, onMoveTop, onMoveBottom, releaseEntityStatus, releaseStatusMap, release, onAddToRelease }) {
14
15
  const [file, setFile] = React.useState(null);
15
16
  React.useEffect(()=>{
16
17
  let mounted = true;
@@ -61,6 +62,7 @@ export function WrappedEntryCard({ entry, entryUrl, contentType, activeLocales,
61
62
  localeCode,
62
63
  defaultLocaleCode
63
64
  });
65
+ const showAddToReleaseAction = releaseEntityStatus === 'notInRelease' && release !== undefined && onAddToRelease !== undefined && !isDisabled;
64
66
  return /*#__PURE__*/ React.createElement(EntryCard, {
65
67
  as: isClickable && entryUrl ? 'a' : 'article',
66
68
  href: isClickable ? entryUrl : undefined,
@@ -91,7 +93,7 @@ export function WrappedEntryCard({ entry, entryUrl, contentType, activeLocales,
91
93
  dragHandleRender: renderDragHandle,
92
94
  withDragHandle: !!renderDragHandle && !isDisabled,
93
95
  draggable: !!renderDragHandle && !isDisabled,
94
- actions: onEdit || onRemove ? [
96
+ actions: onEdit || onRemove || showAddToReleaseAction ? [
95
97
  hasCardEditActions && onEdit ? /*#__PURE__*/ React.createElement(MenuItem, {
96
98
  key: "edit",
97
99
  testId: "edit",
@@ -106,6 +108,15 @@ export function WrappedEntryCard({ entry, entryUrl, contentType, activeLocales,
106
108
  onRemove && onRemove();
107
109
  }
108
110
  }, "Remove") : null,
111
+ showAddToReleaseAction ? /*#__PURE__*/ React.createElement(MenuItem, {
112
+ key: "add-to-release",
113
+ testId: "add-to-release",
114
+ onClick: ()=>{
115
+ onAddToRelease();
116
+ }
117
+ }, /*#__PURE__*/ React.createElement(PlusIcon, {
118
+ size: "tiny"
119
+ }), "Add to release") : null,
109
120
  hasCardMoveActions && (onMoveTop || onMoveBottom) && !isDisabled ? /*#__PURE__*/ React.createElement(MenuDivider, {
110
121
  key: "divider"
111
122
  }) : null,
@@ -1,5 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import '@testing-library/jest-dom';
3
+ import { SharedQueryClientProvider } from '@contentful/field-editor-shared';
4
+ import { createTestQueryClient } from '@contentful/field-editor-test-utils';
3
5
  import { configure, fireEvent, render, waitFor } from '@testing-library/react';
4
6
  import publishedCT from '../../__fixtures__/content-type/published_content_type.json';
5
7
  import publishedEntryNonMasterEnvironment from '../../__fixtures__/entry/published_entry_non_master.json';
@@ -167,9 +169,33 @@ describe('ResourceCard', ()=>{
167
169
  fireEvent.mouseEnter(getByText(space.name));
168
170
  await waitFor(()=>expect(getByText(tooltipContent)).toBeDefined());
169
171
  });
170
- it('renders skeleton when no data is provided', ()=>{
171
- const { getByTestId } = renderResourceCard();
172
+ it('renders skeleton while data is loading', async ()=>{
173
+ const queryClient = createTestQueryClient();
174
+ let resolveEntry;
175
+ const pendingPromise = new Promise((resolve)=>{
176
+ resolveEntry = resolve;
177
+ });
178
+ sdk.cma.entry.get.mockReturnValueOnce(pendingPromise);
179
+ const { getByTestId, queryByTestId } = render(/*#__PURE__*/ React.createElement(SharedQueryClientProvider, {
180
+ client: queryClient
181
+ }, /*#__PURE__*/ React.createElement(EntityProvider, {
182
+ sdk: sdk
183
+ }, /*#__PURE__*/ React.createElement(ResourceCard, {
184
+ isDisabled: false,
185
+ getEntryRouteHref: ()=>'',
186
+ resourceLink: {
187
+ sys: {
188
+ type: 'ResourceLink',
189
+ linkType: 'Contentful:Entry',
190
+ urn: resolvableEntryUrn
191
+ }
192
+ }
193
+ }))));
172
194
  expect(getByTestId('cf-ui-skeleton-form')).toBeDefined();
195
+ expect(queryByTestId('cf-ui-entry-card')).toBeNull();
196
+ resolveEntry(publishedEntry);
197
+ await waitFor(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
198
+ expect(queryByTestId('cf-ui-skeleton-form')).toBeNull();
173
199
  });
174
200
  it('renders unsupported entity card when resource type is unknown', async ()=>{
175
201
  const { getByText } = renderResourceCard({
@@ -8,4 +8,5 @@ export declare function renderActions(props: {
8
8
  onRemove?: () => void;
9
9
  isDisabled: boolean;
10
10
  entityFile?: File;
11
+ onAddToReleaseAction?: () => void;
11
12
  }): (React.JSX.Element | null)[];
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { CustomCardRenderer, RenderCustomMissingEntityCard } from '../../common/customCardTypes';
3
- import { Action, FieldAppSDK, ViewType, RenderDragFn } from '../../types';
3
+ import { Action, Asset, FieldAppSDK, ViewType, RenderDragFn } from '../../types';
4
4
  type FetchingWrappedAssetCardProps = {
5
5
  assetId: string;
6
6
  isDisabled: boolean;
@@ -12,6 +12,10 @@ type FetchingWrappedAssetCardProps = {
12
12
  renderDragHandle?: RenderDragFn;
13
13
  renderCustomCard?: CustomCardRenderer;
14
14
  renderCustomMissingEntityCard?: RenderCustomMissingEntityCard;
15
+ addReferenceToRelease?: (reference: Asset, localeCode?: string, options?: {
16
+ openModalForVersionSelection?: boolean;
17
+ skipNestedReferencesPrompt?: boolean;
18
+ }) => Promise<void>;
15
19
  };
16
20
  export declare function FetchingWrappedAssetCard(props: FetchingWrappedAssetCardProps): React.JSX.Element;
17
21
  export {};
@@ -23,9 +23,10 @@ export interface WrappedAssetCardProps {
23
23
  releaseEntityStatus?: ReleaseEntityStatus;
24
24
  releaseStatusMap?: ReleaseStatusMap;
25
25
  release?: ReleaseV2Props;
26
+ onAddToRelease?: () => void;
26
27
  }
27
28
  export declare const WrappedAssetCard: {
28
- ({ asset, className, size, localeCode, defaultLocaleCode, activeLocales, localesStatusMap, isDisabled, isSelected, isClickable, useLocalizedEntityStatus, renderDragHandle, getEntityScheduledActions, onEdit, getAssetUrl, onRemove, releaseEntityStatus, releaseStatusMap, release, }: WrappedAssetCardProps): React.JSX.Element;
29
+ ({ asset, className, size, localeCode, defaultLocaleCode, activeLocales, localesStatusMap, isDisabled, isSelected, isClickable, useLocalizedEntityStatus, renderDragHandle, getEntityScheduledActions, onEdit, getAssetUrl, onRemove, releaseEntityStatus, releaseStatusMap, release, onAddToRelease, }: WrappedAssetCardProps): React.JSX.Element;
29
30
  defaultProps: {
30
31
  isClickable: boolean;
31
32
  };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { BaseAppSDK } from '@contentful/app-sdk';
3
+ import { QueryClient } from '@tanstack/react-query';
3
4
  import { ResourceProvider } from 'contentful-management';
4
5
  import { Asset, ContentType, Entry, ExternalResource, Resource, ResourceType, ScheduledAction, Space } from '../types';
5
6
  export type ContentfulResourceInfo = {
@@ -17,6 +18,7 @@ export declare function isContentfulResourceInfo(info: ResourceInfo): info is Co
17
18
  type EntityStoreProps = {
18
19
  sdk: BaseAppSDK;
19
20
  queryConcurrency?: number;
21
+ queryClient?: QueryClient;
20
22
  };
21
23
  type GetOptions = {
22
24
  priority?: number;
@@ -88,12 +90,12 @@ declare const useEntityLoader: () => {
88
90
  };
89
91
  export declare function useEntity<E extends FetchableEntity>(entityType: FetchableEntityType, entityId: string, options?: Omit<UseEntityOptions, 'releaseId'>): UseEntityResult<E>;
90
92
  export declare function useResource<R extends Resource = Resource>(resourceType: string, urn: string, { locale, referencingEntryId, ...options }?: UseResourceOptions): {
91
- status: "error" | "success" | "loading";
93
+ status: "loading" | "error" | "success";
92
94
  data: ResourceInfo<R> | undefined;
93
95
  error: unknown;
94
96
  };
95
97
  export declare function useResourceProvider(organizationId: string, appDefinitionId: string): {
96
- status: "error" | "success" | "loading";
98
+ status: "loading" | "error" | "success";
97
99
  data: ResourceProvider | undefined;
98
100
  error: unknown;
99
101
  };