@contentful/field-editor-reference 5.22.6 → 5.23.0

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.
@@ -15,6 +15,9 @@ _export(exports, {
15
15
  UnsupportedError: function() {
16
16
  return UnsupportedError;
17
17
  },
18
+ isContentfulResourceInfo: function() {
19
+ return isContentfulResourceInfo;
20
+ },
18
21
  isUnsupportedError: function() {
19
22
  return isUnsupportedError;
20
23
  },
@@ -92,6 +95,9 @@ function _interop_require_wildcard(obj, nodeInterop) {
92
95
  }
93
96
  return newObj;
94
97
  }
98
+ function isContentfulResourceInfo(info) {
99
+ return info.resource.sys.type === 'Entry';
100
+ }
95
101
  const globalQueue = new _pqueue.default({
96
102
  concurrency: 50
97
103
  });
@@ -108,8 +114,7 @@ function isUnsupportedError(value) {
108
114
  const isEntityQueryKey = (queryKey)=>{
109
115
  return Array.isArray(queryKey) && (queryKey[0] === 'Entry' || queryKey[0] === 'Asset') && queryKey.length === 4;
110
116
  };
111
- async function fetchContentfulEntry(params) {
112
- const { urn, fetch, options } = params;
117
+ async function fetchContentfulEntry({ urn, fetch, options }) {
113
118
  const resourceId = urn.split(':', 6)[5];
114
119
  const ENTITY_RESOURCE_ID_REGEX = RegExp("^spaces\\/(?<spaceId>[^/]+)(?:\\/environments\\/(?<environmentId>[^/]+))?\\/entries\\/(?<entityId>[^/]+)$");
115
120
  const resourceIdMatch = resourceId.match(ENTITY_RESOURCE_ID_REGEX);
@@ -172,6 +177,37 @@ async function fetchContentfulEntry(params) {
172
177
  contentType: contentType
173
178
  };
174
179
  }
180
+ async function fetchExternalResource({ urn, fetch, options, spaceId, environmentId, resourceType }) {
181
+ const [resource, resourceTypes] = await Promise.all([
182
+ fetch([
183
+ 'resource',
184
+ spaceId,
185
+ environmentId,
186
+ resourceType,
187
+ urn
188
+ ], ({ cmaClient })=>cmaClient.raw.get(`/spaces/${spaceId}/environments/${environmentId}/resource_types/${resourceType}/resources`, {
189
+ params: {
190
+ 'sys.urn[in]': urn
191
+ }
192
+ }).then(({ items })=>items[0] ?? null), options),
193
+ fetch([
194
+ 'resource-types',
195
+ spaceId,
196
+ environmentId
197
+ ], ({ cmaClient })=>cmaClient.raw.get(`/spaces/${spaceId}/environments/${environmentId}/resource_types`).then(({ items })=>items))
198
+ ]);
199
+ const resourceTypeEntity = resourceTypes.find((rt)=>rt.sys.id === resourceType);
200
+ if (!resourceTypeEntity) {
201
+ throw new UnsupportedError('Unsupported resource type');
202
+ }
203
+ if (!resource) {
204
+ throw new Error('Missing resource');
205
+ }
206
+ return {
207
+ resource,
208
+ resourceType: resourceTypeEntity
209
+ };
210
+ }
175
211
  const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = (0, _constate.default)(function useInitServices(props) {
176
212
  const currentSpaceId = props.sdk.ids.space;
177
213
  const currentEnvironmentId = props.sdk.ids.environmentAlias ?? props.sdk.ids.environment;
@@ -288,9 +324,21 @@ const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = (0,
288
324
  options
289
325
  });
290
326
  }
291
- throw new UnsupportedError('Unsupported resource type');
327
+ if (!options?.allowExternal) {
328
+ throw new UnsupportedError('Unsupported resource type');
329
+ }
330
+ return fetchExternalResource({
331
+ fetch,
332
+ urn,
333
+ options,
334
+ resourceType,
335
+ spaceId: currentSpaceId,
336
+ environmentId: currentEnvironmentId
337
+ });
292
338
  }, options);
293
339
  }, [
340
+ currentEnvironmentId,
341
+ currentSpaceId,
294
342
  fetch
295
343
  ]);
296
344
  const isSameSpaceEntityQueryKey = (0, _react.useCallback)((queryKey)=>{
@@ -13,6 +13,7 @@ const _reactintersectionobserver = require("react-intersection-observer");
13
13
  const _f36components = require("@contentful/f36-components");
14
14
  const _EntityStore = require("../../common/EntityStore");
15
15
  const _components = require("../../components");
16
+ const _ExternalResourceCard = require("../ExternalResourceCard/ExternalResourceCard");
16
17
  const _ContentfulEntryCard = require("./ContentfulEntryCard");
17
18
  function _getRequireWildcardCache(nodeInterop) {
18
19
  if (typeof WeakMap !== "function") return null;
@@ -65,23 +66,30 @@ function ExistingResourceCard(props) {
65
66
  const { resourceLink, inView, index = 0 } = props;
66
67
  const resourceOptions = {
67
68
  priority: index * -1,
68
- enabled: inView
69
+ enabled: inView,
70
+ allowExternal: true
69
71
  };
70
- const { data, error } = (0, _EntityStore.useResource)(resourceLink.sys.linkType, resourceLink.sys.urn, resourceOptions);
71
- if (!data && !error) {
72
+ const { data: info, error } = (0, _EntityStore.useResource)(resourceLink.sys.linkType, resourceLink.sys.urn, resourceOptions);
73
+ if (!info && !error) {
72
74
  return _react.createElement(ResourceCardSkeleton, null);
73
75
  }
74
- if (data) {
76
+ if (!info) {
77
+ return _react.createElement(_components.ResourceEntityErrorCard, {
78
+ linkType: resourceLink.sys.linkType,
79
+ error: error,
80
+ isDisabled: props.isDisabled,
81
+ onRemove: props.onRemove
82
+ });
83
+ }
84
+ if ((0, _EntityStore.isContentfulResourceInfo)(info)) {
75
85
  return _react.createElement(_ContentfulEntryCard.ContentfulEntryCard, {
76
- info: data,
86
+ info: info,
77
87
  ...props
78
88
  });
79
89
  }
80
- return _react.createElement(_components.ResourceEntityErrorCard, {
81
- linkType: resourceLink.sys.linkType,
82
- error: error,
83
- isDisabled: props.isDisabled,
84
- onRemove: props.onRemove
90
+ return _react.createElement(_ExternalResourceCard.ExternalResourceCard, {
91
+ info: info,
92
+ ...props
85
93
  });
86
94
  }
87
95
  function ResourceCardWrapper(props) {
@@ -9,6 +9,8 @@ const _react1 = require("@testing-library/react");
9
9
  const _published_content_typejson = _interop_require_default(require("../../__fixtures__/content-type/published_content_type.json"));
10
10
  const _published_entry_non_masterjson = _interop_require_default(require("../../__fixtures__/entry/published_entry_non_master.json"));
11
11
  const _published_entryjson = _interop_require_default(require("../../__fixtures__/entry/published_entry.json"));
12
+ const _resourcetypejson = _interop_require_default(require("../../__fixtures__/resource-type/resource-type.json"));
13
+ const _resourcejson = _interop_require_default(require("../../__fixtures__/resource/resource.json"));
12
14
  const _indifferent_spacejson = _interop_require_default(require("../../__fixtures__/space/indifferent_space.json"));
13
15
  const _EntityStore = require("../../common/EntityStore");
14
16
  const _ResourceCard = require("./ResourceCard");
@@ -68,6 +70,8 @@ const resolvableEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/lin
68
70
  const resolvableEntryUrnWithExplicitMaster = 'crn:contentful:::content:spaces/space-id/environments/master/entries/linked-entry-urn';
69
71
  const resolvableEntryUrnWithAnotherEnvironment = 'crn:contentful:::content:spaces/space-id/environments/my-test-environment/entries/linked-entry-urn';
70
72
  const unknownEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/unknown-entry-urn';
73
+ const resolvableExternalResourceType = 'External:ResourceType';
74
+ const resolvableExternalEntityUrn = 'external:entity-urn';
71
75
  const sdk = {
72
76
  locales: {
73
77
  default: 'en-US'
@@ -87,6 +91,27 @@ const sdk = {
87
91
  return Promise.reject(new Error());
88
92
  })
89
93
  },
94
+ Http: {
95
+ get: jest.fn().mockImplementation(({ url, config })=>{
96
+ if (url === '/spaces/space-id/environments/environment-id/resource_types') {
97
+ return Promise.resolve({
98
+ items: [
99
+ _resourcetypejson.default
100
+ ]
101
+ });
102
+ }
103
+ if (url === `/spaces/space-id/environments/environment-id/resource_types/${resolvableExternalResourceType}/resources` && config.params['sys.urn[in]'] === resolvableExternalEntityUrn) {
104
+ return Promise.resolve({
105
+ items: [
106
+ _resourcejson.default
107
+ ]
108
+ });
109
+ }
110
+ return Promise.resolve({
111
+ items: []
112
+ });
113
+ })
114
+ },
90
115
  Locale: {
91
116
  getMany: jest.fn().mockResolvedValue({
92
117
  items: [
@@ -116,7 +141,7 @@ const sdk = {
116
141
  environment: 'environment-id'
117
142
  }
118
143
  };
119
- function renderResourceCard({ linkType = 'Contentful:Entry', entryUrn = resolvableEntryUrn } = {}) {
144
+ function renderResourceCard({ linkType = 'Contentful:Entry', entityUrn = resolvableEntryUrn } = {}) {
120
145
  return (0, _react1.render)(_react.createElement(_EntityStore.EntityProvider, {
121
146
  sdk: sdk
122
147
  }, _react.createElement(_ResourceCard.ResourceCard, {
@@ -126,7 +151,7 @@ function renderResourceCard({ linkType = 'Contentful:Entry', entryUrn = resolvab
126
151
  sys: {
127
152
  type: 'ResourceLink',
128
153
  linkType: linkType,
129
- urn: entryUrn
154
+ urn: entityUrn
130
155
  }
131
156
  }
132
157
  })));
@@ -143,7 +168,7 @@ describe('ResourceCard', ()=>{
143
168
  });
144
169
  it('renders entry card with explicit master crn', async ()=>{
145
170
  const { getByTestId, getByText } = renderResourceCard({
146
- entryUrn: resolvableEntryUrnWithExplicitMaster
171
+ entityUrn: resolvableEntryUrnWithExplicitMaster
147
172
  });
148
173
  const tooltipContent = `Space: ${_indifferent_spacejson.default.name} (Env.: ${_published_entryjson.default.sys.environment.sys.id})`;
149
174
  await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
@@ -154,7 +179,7 @@ describe('ResourceCard', ()=>{
154
179
  });
155
180
  it('renders entry card with a non master environment', async ()=>{
156
181
  const { getByTestId, getByText } = renderResourceCard({
157
- entryUrn: resolvableEntryUrnWithAnotherEnvironment
182
+ entityUrn: resolvableEntryUrnWithAnotherEnvironment
158
183
  });
159
184
  await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
160
185
  const tooltipContent = `Space: ${_indifferent_spacejson.default.name} (Env.: ${_published_entry_non_masterjson.default.sys.environment.sys.id})`;
@@ -167,7 +192,7 @@ describe('ResourceCard', ()=>{
167
192
  const { getByTestId } = renderResourceCard();
168
193
  expect(getByTestId('cf-ui-skeleton-form')).toBeDefined();
169
194
  });
170
- it('renders unsupported entity card when unsupported link is passed', async ()=>{
195
+ it('renders unsupported entity card when resource type is unknown', async ()=>{
171
196
  const { getByText } = renderResourceCard({
172
197
  linkType: 'Contentful:UnsupportedLink'
173
198
  });
@@ -175,14 +200,30 @@ describe('ResourceCard', ()=>{
175
200
  });
176
201
  it('renders missing entity card when unknown error is returned', async ()=>{
177
202
  const { getByTestId } = renderResourceCard({
178
- entryUrn: unknownEntryUrn
203
+ entityUrn: unknownEntryUrn
179
204
  });
180
205
  await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
181
206
  });
182
207
  it('renders missing entity card when crn is invalid', async ()=>{
183
208
  const { getByTestId } = renderResourceCard({
184
- entryUrn: ''
209
+ entityUrn: ''
210
+ });
211
+ await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
212
+ });
213
+ it('renders missing entity card when external urn is invalid', async ()=>{
214
+ const { getByTestId } = renderResourceCard({
215
+ linkType: resolvableExternalResourceType,
216
+ entityUrn: ''
185
217
  });
186
218
  await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
187
219
  });
220
+ it('renders entry card for external resource', async ()=>{
221
+ const { getByTestId, getByText } = renderResourceCard({
222
+ linkType: resolvableExternalResourceType,
223
+ entityUrn: resolvableExternalEntityUrn
224
+ });
225
+ await (0, _react1.waitFor)(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
226
+ expect(getByText(_resourcejson.default.fields.title)).toBeDefined();
227
+ expect(getByText(_resourcetypejson.default.name)).toBeDefined();
228
+ });
188
229
  });
@@ -53,40 +53,32 @@ function _interop_require_wildcard(obj, nodeInterop) {
53
53
  }
54
54
  const defaultProps = {
55
55
  isClickable: true,
56
- hasCardEditActions: true,
57
56
  hasCardMoveActions: true,
58
57
  hasCardRemoveActions: true
59
58
  };
60
- const statusMap = {
61
- active: 'positive',
62
- draft: 'warning',
63
- archived: 'negative',
64
- suspended: 'negative'
65
- };
66
- function ExternalEntityBadge(entityStatus) {
67
- const variant = statusMap[entityStatus];
68
- return _react.createElement(_f36components.Badge, {
69
- variant: variant
70
- }, entityStatus);
59
+ function ExternalEntityBadge(badge) {
60
+ return badge ? _react.createElement(_f36components.Badge, {
61
+ variant: badge.variant
62
+ }, badge.label) : null;
71
63
  }
72
- function ExternalResourceCard({ entity, resourceType, size, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick }) {
73
- const status = entity.fields.additionalData.status;
74
- const badge = status ? ExternalEntityBadge(status) : null;
64
+ function ExternalResourceCard({ info, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick }) {
65
+ const { resource: entity, resourceType } = info;
66
+ const badge = ExternalEntityBadge(entity.fields.badge);
75
67
  return _react.createElement(_f36components.EntryCard, {
76
68
  as: entity.fields.externalUrl ? 'a' : 'article',
77
69
  href: entity.fields.externalUrl,
78
70
  title: entity.fields.title,
79
71
  description: entity.fields.description,
80
- contentType: resourceType,
81
- size: size,
82
- thumbnailElement: entity.fields.image && entity.fields.image.url ? _react.createElement("img", {
83
- alt: entity.fields.image.description,
72
+ contentType: resourceType.name,
73
+ size: 'auto',
74
+ thumbnailElement: entity.fields.image?.url ? _react.createElement("img", {
75
+ alt: entity.fields.image.altText,
84
76
  src: entity.fields.image.url
85
77
  }) : undefined,
86
78
  dragHandleRender: renderDragHandle,
87
79
  withDragHandle: !!renderDragHandle,
88
80
  badge: badge,
89
- actions: onEdit || onRemove ? [
81
+ actions: [
90
82
  hasCardEditActions && onEdit ? _react.createElement(_f36components.MenuItem, {
91
83
  key: "edit",
92
84
  testId: "edit",
@@ -101,7 +93,7 @@ function ExternalResourceCard({ entity, resourceType, size, isClickable, onEdit,
101
93
  onRemove && onRemove();
102
94
  }
103
95
  }, "Remove") : null,
104
- hasCardMoveActions && (onMoveTop || onMoveBottom) ? _react.createElement(_f36components.MenuDivider, {
96
+ hasCardMoveActions && (onEdit || onRemove) && (onMoveTop || onMoveBottom) ? _react.createElement(_f36components.MenuDivider, {
105
97
  key: "divider"
106
98
  }) : null,
107
99
  hasCardMoveActions && onMoveTop ? _react.createElement(_f36components.MenuItem, {
@@ -114,7 +106,7 @@ function ExternalResourceCard({ entity, resourceType, size, isClickable, onEdit,
114
106
  onClick: ()=>onMoveBottom && onMoveBottom(),
115
107
  testId: "move-bottom"
116
108
  }, "Move to bottom") : null
117
- ].filter((item)=>item) : [],
109
+ ].filter((item)=>item),
118
110
  onClick: isClickable ? (e)=>{
119
111
  e.preventDefault();
120
112
  if (onClick) return onClick(e);
@@ -16,6 +16,9 @@ import constate from 'constate';
16
16
  import { createClient } from 'contentful-management';
17
17
  import PQueue from 'p-queue';
18
18
  import { SharedQueryClientProvider, useQuery, useQueryClient } from './queryClient';
19
+ export function isContentfulResourceInfo(info) {
20
+ return info.resource.sys.type === 'Entry';
21
+ }
19
22
  const globalQueue = new PQueue({
20
23
  concurrency: 50
21
24
  });
@@ -32,8 +35,7 @@ export function isUnsupportedError(value) {
32
35
  const isEntityQueryKey = (queryKey)=>{
33
36
  return Array.isArray(queryKey) && (queryKey[0] === 'Entry' || queryKey[0] === 'Asset') && queryKey.length === 4;
34
37
  };
35
- async function fetchContentfulEntry(params) {
36
- const { urn, fetch, options } = params;
38
+ async function fetchContentfulEntry({ urn, fetch, options }) {
37
39
  const resourceId = urn.split(':', 6)[5];
38
40
  const ENTITY_RESOURCE_ID_REGEX = RegExp("^spaces\\/(?<spaceId>[^/]+)(?:\\/environments\\/(?<environmentId>[^/]+))?\\/entries\\/(?<entityId>[^/]+)$");
39
41
  const resourceIdMatch = resourceId.match(ENTITY_RESOURCE_ID_REGEX);
@@ -96,6 +98,37 @@ async function fetchContentfulEntry(params) {
96
98
  contentType: contentType
97
99
  };
98
100
  }
101
+ async function fetchExternalResource({ urn, fetch, options, spaceId, environmentId, resourceType }) {
102
+ const [resource, resourceTypes] = await Promise.all([
103
+ fetch([
104
+ 'resource',
105
+ spaceId,
106
+ environmentId,
107
+ resourceType,
108
+ urn
109
+ ], ({ cmaClient })=>cmaClient.raw.get(`/spaces/${spaceId}/environments/${environmentId}/resource_types/${resourceType}/resources`, {
110
+ params: {
111
+ 'sys.urn[in]': urn
112
+ }
113
+ }).then(({ items })=>items[0] ?? null), options),
114
+ fetch([
115
+ 'resource-types',
116
+ spaceId,
117
+ environmentId
118
+ ], ({ cmaClient })=>cmaClient.raw.get(`/spaces/${spaceId}/environments/${environmentId}/resource_types`).then(({ items })=>items))
119
+ ]);
120
+ const resourceTypeEntity = resourceTypes.find((rt)=>rt.sys.id === resourceType);
121
+ if (!resourceTypeEntity) {
122
+ throw new UnsupportedError('Unsupported resource type');
123
+ }
124
+ if (!resource) {
125
+ throw new Error('Missing resource');
126
+ }
127
+ return {
128
+ resource,
129
+ resourceType: resourceTypeEntity
130
+ };
131
+ }
99
132
  const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = constate(function useInitServices(props) {
100
133
  const currentSpaceId = props.sdk.ids.space;
101
134
  const currentEnvironmentId = props.sdk.ids.environmentAlias ?? props.sdk.ids.environment;
@@ -212,9 +245,21 @@ const [InternalServiceProvider, useFetch, useEntityLoader, useCurrentIds] = cons
212
245
  options
213
246
  });
214
247
  }
215
- throw new UnsupportedError('Unsupported resource type');
248
+ if (!options?.allowExternal) {
249
+ throw new UnsupportedError('Unsupported resource type');
250
+ }
251
+ return fetchExternalResource({
252
+ fetch,
253
+ urn,
254
+ options,
255
+ resourceType,
256
+ spaceId: currentSpaceId,
257
+ environmentId: currentEnvironmentId
258
+ });
216
259
  }, options);
217
260
  }, [
261
+ currentEnvironmentId,
262
+ currentSpaceId,
218
263
  fetch
219
264
  ]);
220
265
  const isSameSpaceEntityQueryKey = useCallback((queryKey)=>{
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import { useInView } from 'react-intersection-observer';
3
3
  import { EntryCard } from '@contentful/f36-components';
4
- import { useResource } from '../../common/EntityStore';
4
+ import { isContentfulResourceInfo, useResource } from '../../common/EntityStore';
5
5
  import { ResourceEntityErrorCard } from '../../components';
6
+ import { ExternalResourceCard } from '../ExternalResourceCard/ExternalResourceCard';
6
7
  import { ContentfulEntryCard } from './ContentfulEntryCard';
7
8
  function ResourceCardSkeleton() {
8
9
  return React.createElement(EntryCard, {
@@ -14,23 +15,30 @@ function ExistingResourceCard(props) {
14
15
  const { resourceLink, inView, index = 0 } = props;
15
16
  const resourceOptions = {
16
17
  priority: index * -1,
17
- enabled: inView
18
+ enabled: inView,
19
+ allowExternal: true
18
20
  };
19
- const { data, error } = useResource(resourceLink.sys.linkType, resourceLink.sys.urn, resourceOptions);
20
- if (!data && !error) {
21
+ const { data: info, error } = useResource(resourceLink.sys.linkType, resourceLink.sys.urn, resourceOptions);
22
+ if (!info && !error) {
21
23
  return React.createElement(ResourceCardSkeleton, null);
22
24
  }
23
- if (data) {
25
+ if (!info) {
26
+ return React.createElement(ResourceEntityErrorCard, {
27
+ linkType: resourceLink.sys.linkType,
28
+ error: error,
29
+ isDisabled: props.isDisabled,
30
+ onRemove: props.onRemove
31
+ });
32
+ }
33
+ if (isContentfulResourceInfo(info)) {
24
34
  return React.createElement(ContentfulEntryCard, {
25
- info: data,
35
+ info: info,
26
36
  ...props
27
37
  });
28
38
  }
29
- return React.createElement(ResourceEntityErrorCard, {
30
- linkType: resourceLink.sys.linkType,
31
- error: error,
32
- isDisabled: props.isDisabled,
33
- onRemove: props.onRemove
39
+ return React.createElement(ExternalResourceCard, {
40
+ info: info,
41
+ ...props
34
42
  });
35
43
  }
36
44
  function ResourceCardWrapper(props) {
@@ -5,6 +5,8 @@ import { configure, fireEvent, render, waitFor } from '@testing-library/react';
5
5
  import publishedCT from '../../__fixtures__/content-type/published_content_type.json';
6
6
  import publishedEntryNonMasterEnvironment from '../../__fixtures__/entry/published_entry_non_master.json';
7
7
  import publishedEntry from '../../__fixtures__/entry/published_entry.json';
8
+ import resourceType from '../../__fixtures__/resource-type/resource-type.json';
9
+ import resource from '../../__fixtures__/resource/resource.json';
8
10
  import space from '../../__fixtures__/space/indifferent_space.json';
9
11
  import { EntityProvider } from '../../common/EntityStore';
10
12
  import { ResourceCard } from './ResourceCard';
@@ -18,6 +20,8 @@ const resolvableEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/lin
18
20
  const resolvableEntryUrnWithExplicitMaster = 'crn:contentful:::content:spaces/space-id/environments/master/entries/linked-entry-urn';
19
21
  const resolvableEntryUrnWithAnotherEnvironment = 'crn:contentful:::content:spaces/space-id/environments/my-test-environment/entries/linked-entry-urn';
20
22
  const unknownEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/unknown-entry-urn';
23
+ const resolvableExternalResourceType = 'External:ResourceType';
24
+ const resolvableExternalEntityUrn = 'external:entity-urn';
21
25
  const sdk = {
22
26
  locales: {
23
27
  default: 'en-US'
@@ -37,6 +41,27 @@ const sdk = {
37
41
  return Promise.reject(new Error());
38
42
  })
39
43
  },
44
+ Http: {
45
+ get: jest.fn().mockImplementation(({ url, config })=>{
46
+ if (url === '/spaces/space-id/environments/environment-id/resource_types') {
47
+ return Promise.resolve({
48
+ items: [
49
+ resourceType
50
+ ]
51
+ });
52
+ }
53
+ if (url === `/spaces/space-id/environments/environment-id/resource_types/${resolvableExternalResourceType}/resources` && config.params['sys.urn[in]'] === resolvableExternalEntityUrn) {
54
+ return Promise.resolve({
55
+ items: [
56
+ resource
57
+ ]
58
+ });
59
+ }
60
+ return Promise.resolve({
61
+ items: []
62
+ });
63
+ })
64
+ },
40
65
  Locale: {
41
66
  getMany: jest.fn().mockResolvedValue({
42
67
  items: [
@@ -66,7 +91,7 @@ const sdk = {
66
91
  environment: 'environment-id'
67
92
  }
68
93
  };
69
- function renderResourceCard({ linkType = 'Contentful:Entry', entryUrn = resolvableEntryUrn } = {}) {
94
+ function renderResourceCard({ linkType = 'Contentful:Entry', entityUrn = resolvableEntryUrn } = {}) {
70
95
  return render(React.createElement(EntityProvider, {
71
96
  sdk: sdk
72
97
  }, React.createElement(ResourceCard, {
@@ -76,7 +101,7 @@ function renderResourceCard({ linkType = 'Contentful:Entry', entryUrn = resolvab
76
101
  sys: {
77
102
  type: 'ResourceLink',
78
103
  linkType: linkType,
79
- urn: entryUrn
104
+ urn: entityUrn
80
105
  }
81
106
  }
82
107
  })));
@@ -93,7 +118,7 @@ describe('ResourceCard', ()=>{
93
118
  });
94
119
  it('renders entry card with explicit master crn', async ()=>{
95
120
  const { getByTestId, getByText } = renderResourceCard({
96
- entryUrn: resolvableEntryUrnWithExplicitMaster
121
+ entityUrn: resolvableEntryUrnWithExplicitMaster
97
122
  });
98
123
  const tooltipContent = `Space: ${space.name} (Env.: ${publishedEntry.sys.environment.sys.id})`;
99
124
  await waitFor(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
@@ -104,7 +129,7 @@ describe('ResourceCard', ()=>{
104
129
  });
105
130
  it('renders entry card with a non master environment', async ()=>{
106
131
  const { getByTestId, getByText } = renderResourceCard({
107
- entryUrn: resolvableEntryUrnWithAnotherEnvironment
132
+ entityUrn: resolvableEntryUrnWithAnotherEnvironment
108
133
  });
109
134
  await waitFor(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
110
135
  const tooltipContent = `Space: ${space.name} (Env.: ${publishedEntryNonMasterEnvironment.sys.environment.sys.id})`;
@@ -117,7 +142,7 @@ describe('ResourceCard', ()=>{
117
142
  const { getByTestId } = renderResourceCard();
118
143
  expect(getByTestId('cf-ui-skeleton-form')).toBeDefined();
119
144
  });
120
- it('renders unsupported entity card when unsupported link is passed', async ()=>{
145
+ it('renders unsupported entity card when resource type is unknown', async ()=>{
121
146
  const { getByText } = renderResourceCard({
122
147
  linkType: 'Contentful:UnsupportedLink'
123
148
  });
@@ -125,14 +150,30 @@ describe('ResourceCard', ()=>{
125
150
  });
126
151
  it('renders missing entity card when unknown error is returned', async ()=>{
127
152
  const { getByTestId } = renderResourceCard({
128
- entryUrn: unknownEntryUrn
153
+ entityUrn: unknownEntryUrn
129
154
  });
130
155
  await waitFor(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
131
156
  });
132
157
  it('renders missing entity card when crn is invalid', async ()=>{
133
158
  const { getByTestId } = renderResourceCard({
134
- entryUrn: ''
159
+ entityUrn: ''
160
+ });
161
+ await waitFor(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
162
+ });
163
+ it('renders missing entity card when external urn is invalid', async ()=>{
164
+ const { getByTestId } = renderResourceCard({
165
+ linkType: resolvableExternalResourceType,
166
+ entityUrn: ''
135
167
  });
136
168
  await waitFor(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
137
169
  });
170
+ it('renders entry card for external resource', async ()=>{
171
+ const { getByTestId, getByText } = renderResourceCard({
172
+ linkType: resolvableExternalResourceType,
173
+ entityUrn: resolvableExternalEntityUrn
174
+ });
175
+ await waitFor(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
176
+ expect(getByText(resource.fields.title)).toBeDefined();
177
+ expect(getByText(resourceType.name)).toBeDefined();
178
+ });
138
179
  });
@@ -2,40 +2,32 @@ import * as React from 'react';
2
2
  import { Badge, EntryCard, MenuItem, MenuDivider } from '@contentful/f36-components';
3
3
  const defaultProps = {
4
4
  isClickable: true,
5
- hasCardEditActions: true,
6
5
  hasCardMoveActions: true,
7
6
  hasCardRemoveActions: true
8
7
  };
9
- const statusMap = {
10
- active: 'positive',
11
- draft: 'warning',
12
- archived: 'negative',
13
- suspended: 'negative'
14
- };
15
- function ExternalEntityBadge(entityStatus) {
16
- const variant = statusMap[entityStatus];
17
- return React.createElement(Badge, {
18
- variant: variant
19
- }, entityStatus);
8
+ function ExternalEntityBadge(badge) {
9
+ return badge ? React.createElement(Badge, {
10
+ variant: badge.variant
11
+ }, badge.label) : null;
20
12
  }
21
- export function ExternalResourceCard({ entity, resourceType, size, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick }) {
22
- const status = entity.fields.additionalData.status;
23
- const badge = status ? ExternalEntityBadge(status) : null;
13
+ export function ExternalResourceCard({ info, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick }) {
14
+ const { resource: entity, resourceType } = info;
15
+ const badge = ExternalEntityBadge(entity.fields.badge);
24
16
  return React.createElement(EntryCard, {
25
17
  as: entity.fields.externalUrl ? 'a' : 'article',
26
18
  href: entity.fields.externalUrl,
27
19
  title: entity.fields.title,
28
20
  description: entity.fields.description,
29
- contentType: resourceType,
30
- size: size,
31
- thumbnailElement: entity.fields.image && entity.fields.image.url ? React.createElement("img", {
32
- alt: entity.fields.image.description,
21
+ contentType: resourceType.name,
22
+ size: 'auto',
23
+ thumbnailElement: entity.fields.image?.url ? React.createElement("img", {
24
+ alt: entity.fields.image.altText,
33
25
  src: entity.fields.image.url
34
26
  }) : undefined,
35
27
  dragHandleRender: renderDragHandle,
36
28
  withDragHandle: !!renderDragHandle,
37
29
  badge: badge,
38
- actions: onEdit || onRemove ? [
30
+ actions: [
39
31
  hasCardEditActions && onEdit ? React.createElement(MenuItem, {
40
32
  key: "edit",
41
33
  testId: "edit",
@@ -50,7 +42,7 @@ export function ExternalResourceCard({ entity, resourceType, size, isClickable,
50
42
  onRemove && onRemove();
51
43
  }
52
44
  }, "Remove") : null,
53
- hasCardMoveActions && (onMoveTop || onMoveBottom) ? React.createElement(MenuDivider, {
45
+ hasCardMoveActions && (onEdit || onRemove) && (onMoveTop || onMoveBottom) ? React.createElement(MenuDivider, {
54
46
  key: "divider"
55
47
  }) : null,
56
48
  hasCardMoveActions && onMoveTop ? React.createElement(MenuItem, {
@@ -63,7 +55,7 @@ export function ExternalResourceCard({ entity, resourceType, size, isClickable,
63
55
  onClick: ()=>onMoveBottom && onMoveBottom(),
64
56
  testId: "move-bottom"
65
57
  }, "Move to bottom") : null
66
- ].filter((item)=>item) : [],
58
+ ].filter((item)=>item),
67
59
  onClick: isClickable ? (e)=>{
68
60
  e.preventDefault();
69
61
  if (onClick) return onClick(e);
@@ -1,12 +1,18 @@
1
1
  import React from 'react';
2
2
  import { BaseAppSDK } from '@contentful/app-sdk';
3
- import { Asset, ContentType, Entry, Resource, ResourceType, ScheduledAction, Space } from '../types';
4
- export type ResourceInfo<R extends Resource = Resource> = {
5
- resource: R;
3
+ import { Asset, ContentType, Entry, ExternalResource, Resource, ResourceType, ScheduledAction, Space } from '../types';
4
+ export type ContentfulResourceInfo = {
5
+ resource: Entry;
6
6
  defaultLocaleCode: string;
7
7
  contentType: ContentType;
8
8
  space: Space;
9
9
  };
10
+ export type ExternalResourceInfo = {
11
+ resource: ExternalResource;
12
+ resourceType: ResourceType;
13
+ };
14
+ export type ResourceInfo<R extends Resource = Resource> = R extends Entry ? ContentfulResourceInfo : ExternalResourceInfo;
15
+ export declare function isContentfulResourceInfo(info: ResourceInfo): info is ContentfulResourceInfo;
10
16
  type EntityStoreProps = {
11
17
  sdk: BaseAppSDK;
12
18
  queryConcurrency?: number;
@@ -22,7 +28,9 @@ type UseEntityOptions = GetEntityOptions & {
22
28
  enabled?: boolean;
23
29
  };
24
30
  type QueryEntityResult<E> = Promise<E>;
25
- type GetResourceOptions = GetOptions;
31
+ type GetResourceOptions = GetOptions & {
32
+ allowExternal?: boolean;
33
+ };
26
34
  type QueryResourceResult<R extends Resource = Resource> = QueryEntityResult<ResourceInfo<R>>;
27
35
  type UseResourceOptions = GetResourceOptions & {
28
36
  enabled?: boolean;
@@ -48,14 +56,14 @@ export declare class UnsupportedError extends Error {
48
56
  }
49
57
  export declare function isUnsupportedError(value: unknown): value is UnsupportedError;
50
58
  declare const useEntityLoader: () => {
51
- getResource: <R extends Resource = Resource>(resourceType: ResourceType, urn: string, options?: GetResourceOptions) => QueryResourceResult<R>;
59
+ getResource: <R extends Resource = Resource>(resourceType: string, urn: string, options?: GetResourceOptions) => QueryResourceResult<R>;
52
60
  getEntity: <E extends FetchableEntity>(entityType: FetchableEntityType, entityId: string, options?: GetEntityOptions) => QueryEntityResult<E>;
53
61
  getEntityScheduledActions: (entityType: FetchableEntityType, entityId: string, options?: GetEntityOptions) => QueryEntityResult<ScheduledAction[]>;
54
62
  };
55
63
  export declare function useEntity<E extends FetchableEntity>(entityType: FetchableEntityType, entityId: string, options?: UseEntityOptions): UseEntityResult<E>;
56
- export declare function useResource(resourceType: ResourceType, urn: string, options?: UseResourceOptions): {
64
+ export declare function useResource<R extends Resource = Resource>(resourceType: string, urn: string, options?: UseResourceOptions): {
57
65
  status: "error" | "success" | "loading";
58
- data: ResourceInfo<Resource> | undefined;
66
+ data: ResourceInfo<R> | undefined;
59
67
  error: unknown;
60
68
  };
61
69
  declare function EntityProvider({ children, ...props }: React.PropsWithChildren<EntityStoreProps>): JSX.Element;
@@ -3,7 +3,7 @@ import { RenderDragFn, ResourceLink } from '../../types';
3
3
  import { CardActionsHandlers, EntryRoute } from './ContentfulEntryCard';
4
4
  type ResourceCardProps = {
5
5
  index?: number;
6
- resourceLink?: ResourceLink;
6
+ resourceLink?: ResourceLink<string>;
7
7
  isDisabled: boolean;
8
8
  renderDragHandle?: RenderDragFn;
9
9
  getEntryRouteHref: (entryRoute: EntryRoute) => string;
@@ -1,35 +1,9 @@
1
1
  import * as React from 'react';
2
+ import { ExternalResourceInfo } from '../../common/EntityStore';
2
3
  import { RenderDragFn } from '../../types';
3
- type SysExternalResource<T extends string> = {
4
- sys: {
5
- type: 'Link';
6
- linkType: T;
7
- id: string;
8
- };
9
- };
10
- interface ExternalResource {
11
- sys: {
12
- type: string;
13
- id: string;
14
- resourceProvider: SysExternalResource<'ResourceProvider'>;
15
- resourceType: SysExternalResource<'ResourceType'>;
16
- };
17
- fields: {
18
- title: string;
19
- description?: string;
20
- externalUrl?: string;
21
- image?: {
22
- url?: string;
23
- description?: string;
24
- };
25
- additionalData: any;
26
- };
27
- }
28
4
  export interface ExternalResourceCardProps {
29
- entity: ExternalResource;
30
- resourceType: string;
5
+ info: ExternalResourceInfo;
31
6
  isDisabled: boolean;
32
- size: 'small' | 'default' | 'auto';
33
7
  isSelected?: boolean;
34
8
  onRemove?: () => void;
35
9
  onEdit?: () => void;
@@ -38,17 +12,15 @@ export interface ExternalResourceCardProps {
38
12
  isClickable?: boolean;
39
13
  onMoveTop?: () => void;
40
14
  onMoveBottom?: () => void;
41
- hasCardEditActions: boolean;
15
+ hasCardEditActions?: boolean;
42
16
  hasCardMoveActions?: boolean;
43
17
  hasCardRemoveActions?: boolean;
44
18
  }
45
- export declare function ExternalResourceCard({ entity, resourceType, size, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick, }: ExternalResourceCardProps): JSX.Element;
19
+ export declare function ExternalResourceCard({ info, isClickable, onEdit, onRemove, onMoveTop, onMoveBottom, hasCardEditActions, hasCardMoveActions, hasCardRemoveActions, renderDragHandle, onClick, }: ExternalResourceCardProps): JSX.Element;
46
20
  export declare namespace ExternalResourceCard {
47
21
  var defaultProps: {
48
22
  isClickable: boolean;
49
- hasCardEditActions: boolean;
50
23
  hasCardMoveActions: boolean;
51
24
  hasCardRemoveActions: boolean;
52
25
  };
53
26
  }
54
- export {};
@@ -2,7 +2,7 @@
2
2
  import { NavigatorSlideInfo, ContentEntityType } from '@contentful/app-sdk';
3
3
  import { Entry, Asset } from '@contentful/field-editor-shared';
4
4
  export type { BaseAppSDK, FieldAppSDK, ContentType, ContentTypeField, Link, ContentEntityType, NavigatorSlideInfo, ScheduledAction, } from '@contentful/app-sdk';
5
- export type { SpaceProps as Space } from 'contentful-management';
5
+ export type { SpaceProps as Space, ResourceLink } from 'contentful-management';
6
6
  export { Entry, File, Asset } from '@contentful/field-editor-shared';
7
7
  export type { ResourceInfo } from './common/EntityStore';
8
8
  export type Entity = Entry | Asset;
@@ -21,9 +21,45 @@ export type AssetLink = {
21
21
  };
22
22
  };
23
23
  export type EntityLink = EntryLink | AssetLink;
24
- export type ResourceType = 'Contentful:Entry';
25
- export type Resource = Entry | Asset;
26
- export type EntityType = 'Entry' | 'Asset' | ResourceType;
24
+ type SysExternalResource<T extends string> = {
25
+ sys: {
26
+ type: 'Link';
27
+ linkType: T;
28
+ id: string;
29
+ };
30
+ };
31
+ export type ResourceType = {
32
+ sys: {
33
+ type: 'ResourceType';
34
+ id: string;
35
+ resourceProvider: SysExternalResource<'ResourceProvider'>;
36
+ };
37
+ name: string;
38
+ };
39
+ export interface ExternalResource {
40
+ sys: {
41
+ type: 'Resource';
42
+ id: string;
43
+ resourceProvider: SysExternalResource<'ResourceProvider'>;
44
+ resourceType: SysExternalResource<'ResourceType'>;
45
+ };
46
+ fields: {
47
+ title: string;
48
+ subtitle?: string;
49
+ description?: string;
50
+ externalUrl?: string;
51
+ badge?: {
52
+ label: string;
53
+ variant: 'negative' | 'positive' | 'primary' | 'secondary' | 'warning';
54
+ };
55
+ image?: {
56
+ url: string;
57
+ altText?: string;
58
+ };
59
+ };
60
+ }
61
+ export type Resource = Entry | ExternalResource;
62
+ export type EntityType = 'Entry' | 'Asset' | string;
27
63
  export type SysResourceLink<T extends string> = {
28
64
  sys: {
29
65
  type: 'ResourceLink';
@@ -32,7 +68,6 @@ export type SysResourceLink<T extends string> = {
32
68
  };
33
69
  };
34
70
  export type ContentfulEntryLink = SysResourceLink<'Contentful:Entry'>;
35
- export type ResourceLink = ContentfulEntryLink;
36
71
  /**
37
72
  * @deprecated use `EntityLink` type
38
73
  */
@@ -1,5 +1,5 @@
1
1
  import { ReferenceValue, ResourceLink } from '../types';
2
- type Items = (ResourceLink | ReferenceValue)[];
2
+ type Items = (ResourceLink<string> | ReferenceValue)[];
3
3
  export declare const useSortIDs: (items: Items) => {
4
4
  sortIDs: {
5
5
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/field-editor-reference",
3
- "version": "5.22.6",
3
+ "version": "5.23.0",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -67,5 +67,5 @@
67
67
  "publishConfig": {
68
68
  "registry": "https://npm.pkg.github.com/"
69
69
  },
70
- "gitHead": "f251d113bc64fc7941017981bfb24975ccaa4c18"
70
+ "gitHead": "7018fb86df27fcb2e0d4bdb475549367fe6ee87d"
71
71
  }