@contentful/field-editor-reference 5.9.0 → 5.11.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.
Files changed (199) hide show
  1. package/dist/cjs/__fixtures__/FakeSdk.js +183 -0
  2. package/dist/cjs/__fixtures__/asset/index.js +37 -0
  3. package/dist/cjs/__fixtures__/content-type/index.js +16 -0
  4. package/dist/cjs/__fixtures__/entry/index.js +33 -0
  5. package/dist/cjs/__fixtures__/fixtures.js +71 -0
  6. package/dist/cjs/__fixtures__/locale/index.js +40 -0
  7. package/dist/cjs/__fixtures__/space/index.js +16 -0
  8. package/dist/cjs/assets/MultipleMediaEditor.js +86 -0
  9. package/dist/cjs/assets/SingleMediaEditor.js +69 -0
  10. package/dist/cjs/assets/WrappedAssetCard/AssetCardActions.js +125 -0
  11. package/dist/cjs/assets/WrappedAssetCard/FetchingWrappedAssetCard.js +171 -0
  12. package/dist/cjs/assets/WrappedAssetCard/WrappedAssetCard.js +159 -0
  13. package/dist/cjs/assets/WrappedAssetCard/WrappedAssetLink.js +130 -0
  14. package/dist/cjs/assets/index.js +24 -0
  15. package/dist/cjs/common/EntityStore.js +420 -0
  16. package/dist/cjs/common/MultipleReferenceEditor.js +164 -0
  17. package/dist/cjs/common/ReferenceEditor.js +74 -0
  18. package/dist/cjs/common/SingleReferenceEditor.js +118 -0
  19. package/dist/cjs/common/SortableLinkList.js +95 -0
  20. package/dist/cjs/common/customCardTypes.js +44 -0
  21. package/dist/cjs/common/useAccessApi.js +19 -0
  22. package/dist/cjs/common/useContentTypePermissions.js +54 -0
  23. package/dist/cjs/common/useEditorPermissions.js +77 -0
  24. package/dist/cjs/common/useEditorPermissions.spec.js +205 -0
  25. package/dist/cjs/components/AssetThumbnail/AssetThumbnail.js +62 -0
  26. package/dist/cjs/components/CreateEntryLinkButton/CreateEntryLinkButton.js +102 -0
  27. package/dist/cjs/components/CreateEntryLinkButton/CreateEntryLinkButton.spec.js +254 -0
  28. package/dist/cjs/components/CreateEntryLinkButton/CreateEntryMenuTrigger.js +199 -0
  29. package/dist/cjs/components/CreateEntryLinkButton/CreateEntryMenuTrigger.spec.js +190 -0
  30. package/dist/cjs/components/CreateEntryLinkButton/useGlobalMouseUp.js +19 -0
  31. package/dist/cjs/components/LinkActions/CombinedLinkActions.js +167 -0
  32. package/dist/cjs/components/LinkActions/LinkActions.js +123 -0
  33. package/dist/cjs/components/LinkActions/LinkEntityActions.js +186 -0
  34. package/dist/cjs/components/LinkActions/NoLinkPermissionsInfo.js +54 -0
  35. package/dist/cjs/components/LinkActions/helpers.js +78 -0
  36. package/dist/cjs/components/LinkActions/redesignStyles.js +44 -0
  37. package/dist/cjs/components/LinkActions/styles.js +33 -0
  38. package/dist/cjs/components/MissingEntityCard/MissingEntityCard.js +75 -0
  39. package/dist/cjs/components/MissingEntityCard/styles.js +29 -0
  40. package/dist/cjs/components/ScheduledIconWithTooltip/ScheduleTooltip.js +75 -0
  41. package/dist/cjs/components/ScheduledIconWithTooltip/ScheduledIconWithTooltip.js +81 -0
  42. package/dist/cjs/components/ScheduledIconWithTooltip/formatDateAndTime.js +45 -0
  43. package/dist/cjs/components/SpaceName/SpaceName.js +91 -0
  44. package/dist/cjs/components/index.js +44 -0
  45. package/dist/cjs/entries/MultipleEntryReferenceEditor.js +86 -0
  46. package/dist/cjs/entries/SingleEntryReferenceEditor.js +74 -0
  47. package/dist/cjs/entries/WrappedEntryCard/FetchingWrappedEntryCard.js +189 -0
  48. package/dist/cjs/entries/WrappedEntryCard/WrappedEntryCard.js +181 -0
  49. package/dist/cjs/entries/index.js +24 -0
  50. package/dist/cjs/index.js +92 -0
  51. package/dist/cjs/resources/Cards/ContentfulEntryCard.js +87 -0
  52. package/dist/cjs/resources/Cards/ResourceCard.js +111 -0
  53. package/dist/cjs/resources/Cards/UnsupportedEntityCard.js +64 -0
  54. package/dist/cjs/resources/MultipleResourceReferenceEditor.js +157 -0
  55. package/dist/cjs/resources/MultipleResourceReferenceEditor.spec.js +297 -0
  56. package/dist/cjs/resources/SingleResourceReferenceEditor.js +87 -0
  57. package/dist/cjs/resources/SingleResourceReferenceEditor.spec.js +161 -0
  58. package/dist/cjs/resources/index.js +19 -0
  59. package/dist/cjs/resources/testHelpers/resourceEditorHelpers.js +121 -0
  60. package/dist/cjs/resources/useResourceLinkActions.js +88 -0
  61. package/dist/cjs/types.js +22 -0
  62. package/dist/cjs/utils/fromFieldValidations.js +54 -0
  63. package/dist/esm/__fixtures__/FakeSdk.js +173 -0
  64. package/dist/esm/__fixtures__/asset/index.js +6 -0
  65. package/dist/esm/__fixtures__/content-type/index.js +2 -0
  66. package/dist/esm/__fixtures__/entry/index.js +5 -0
  67. package/dist/esm/__fixtures__/fixtures.js +6 -0
  68. package/dist/esm/__fixtures__/locale/index.js +15 -0
  69. package/dist/esm/__fixtures__/space/index.js +2 -0
  70. package/dist/esm/assets/MultipleMediaEditor.js +37 -0
  71. package/dist/esm/assets/SingleMediaEditor.js +20 -0
  72. package/dist/esm/assets/WrappedAssetCard/AssetCardActions.js +63 -0
  73. package/dist/esm/assets/WrappedAssetCard/FetchingWrappedAssetCard.js +122 -0
  74. package/dist/esm/assets/WrappedAssetCard/WrappedAssetCard.js +105 -0
  75. package/dist/esm/assets/WrappedAssetCard/WrappedAssetLink.js +76 -0
  76. package/dist/esm/assets/index.js +3 -0
  77. package/dist/esm/common/EntityStore.js +347 -0
  78. package/dist/esm/common/MultipleReferenceEditor.js +111 -0
  79. package/dist/esm/common/ReferenceEditor.js +20 -0
  80. package/dist/esm/common/SingleReferenceEditor.js +70 -0
  81. package/dist/esm/common/SortableLinkList.js +41 -0
  82. package/dist/esm/common/customCardTypes.js +1 -0
  83. package/dist/esm/common/useAccessApi.js +9 -0
  84. package/dist/esm/common/useContentTypePermissions.js +44 -0
  85. package/dist/esm/common/useEditorPermissions.js +67 -0
  86. package/dist/esm/common/useEditorPermissions.spec.js +201 -0
  87. package/dist/esm/components/AssetThumbnail/AssetThumbnail.js +13 -0
  88. package/dist/esm/components/CreateEntryLinkButton/CreateEntryLinkButton.js +48 -0
  89. package/dist/esm/components/CreateEntryLinkButton/CreateEntryLinkButton.spec.js +206 -0
  90. package/dist/esm/components/CreateEntryLinkButton/CreateEntryMenuTrigger.js +145 -0
  91. package/dist/esm/components/CreateEntryLinkButton/CreateEntryMenuTrigger.spec.js +142 -0
  92. package/dist/esm/components/CreateEntryLinkButton/useGlobalMouseUp.js +9 -0
  93. package/dist/esm/components/LinkActions/CombinedLinkActions.js +118 -0
  94. package/dist/esm/components/LinkActions/LinkActions.js +66 -0
  95. package/dist/esm/components/LinkActions/LinkEntityActions.js +127 -0
  96. package/dist/esm/components/LinkActions/NoLinkPermissionsInfo.js +5 -0
  97. package/dist/esm/components/LinkActions/helpers.js +57 -0
  98. package/dist/esm/components/LinkActions/redesignStyles.js +18 -0
  99. package/dist/esm/components/LinkActions/styles.js +10 -0
  100. package/dist/esm/components/MissingEntityCard/MissingEntityCard.js +26 -0
  101. package/dist/esm/components/MissingEntityCard/styles.js +11 -0
  102. package/dist/esm/components/ScheduledIconWithTooltip/ScheduleTooltip.js +18 -0
  103. package/dist/esm/components/ScheduledIconWithTooltip/ScheduledIconWithTooltip.js +32 -0
  104. package/dist/esm/components/ScheduledIconWithTooltip/formatDateAndTime.js +19 -0
  105. package/dist/esm/components/SpaceName/SpaceName.js +37 -0
  106. package/dist/esm/components/index.js +8 -0
  107. package/dist/esm/entries/MultipleEntryReferenceEditor.js +37 -0
  108. package/dist/esm/entries/SingleEntryReferenceEditor.js +25 -0
  109. package/dist/esm/entries/WrappedEntryCard/FetchingWrappedEntryCard.js +135 -0
  110. package/dist/esm/entries/WrappedEntryCard/WrappedEntryCard.js +127 -0
  111. package/dist/esm/entries/index.js +3 -0
  112. package/dist/esm/index.js +7 -0
  113. package/dist/esm/resources/Cards/ContentfulEntryCard.js +38 -0
  114. package/dist/esm/resources/Cards/ResourceCard.js +62 -0
  115. package/dist/esm/resources/Cards/UnsupportedEntityCard.js +15 -0
  116. package/dist/esm/resources/MultipleResourceReferenceEditor.js +104 -0
  117. package/dist/esm/resources/MultipleResourceReferenceEditor.spec.js +254 -0
  118. package/dist/esm/resources/SingleResourceReferenceEditor.js +33 -0
  119. package/dist/esm/resources/SingleResourceReferenceEditor.spec.js +118 -0
  120. package/dist/esm/resources/index.js +2 -0
  121. package/dist/esm/resources/testHelpers/resourceEditorHelpers.js +103 -0
  122. package/dist/esm/resources/useResourceLinkActions.js +78 -0
  123. package/dist/esm/types.js +1 -0
  124. package/dist/esm/utils/fromFieldValidations.js +39 -0
  125. package/dist/{__fixtures__ → types/__fixtures__}/FakeSdk.d.ts +8 -8
  126. package/dist/{__fixtures__ → types/__fixtures__}/asset/index.d.ts +6 -6
  127. package/dist/{__fixtures__ → types/__fixtures__}/content-type/index.d.ts +2 -2
  128. package/dist/{__fixtures__ → types/__fixtures__}/entry/index.d.ts +5 -5
  129. package/dist/{__fixtures__ → types/__fixtures__}/fixtures.d.ts +6 -6
  130. package/dist/{__fixtures__ → types/__fixtures__}/locale/index.d.ts +42 -42
  131. package/dist/{__fixtures__ → types/__fixtures__}/space/index.d.ts +2 -2
  132. package/dist/{assets → types/assets}/MultipleMediaEditor.d.ts +10 -10
  133. package/dist/types/assets/SingleMediaEditor.d.ts +10 -0
  134. package/dist/{assets → types/assets}/WrappedAssetCard/AssetCardActions.d.ts +11 -11
  135. package/dist/{assets → types/assets}/WrappedAssetCard/FetchingWrappedAssetCard.d.ts +17 -17
  136. package/dist/{assets → types/assets}/WrappedAssetCard/WrappedAssetCard.d.ts +24 -24
  137. package/dist/{assets → types/assets}/WrappedAssetCard/WrappedAssetLink.d.ts +16 -16
  138. package/dist/{assets → types/assets}/index.d.ts +3 -3
  139. package/dist/{common → types/common}/EntityStore.d.ts +62 -62
  140. package/dist/{common → types/common}/MultipleReferenceEditor.d.ts +25 -25
  141. package/dist/{common → types/common}/ReferenceEditor.d.ts +46 -46
  142. package/dist/{common → types/common}/SingleReferenceEditor.d.ts +24 -24
  143. package/dist/{common → types/common}/SortableLinkList.d.ts +19 -19
  144. package/dist/{common → types/common}/customCardTypes.d.ts +29 -29
  145. package/dist/types/common/useAccessApi.d.ts +16 -0
  146. package/dist/{common → types/common}/useContentTypePermissions.d.ts +17 -17
  147. package/dist/{common → types/common}/useEditorPermissions.d.ts +17 -17
  148. package/dist/types/common/useEditorPermissions.spec.d.ts +1 -0
  149. package/dist/{components → types/components}/AssetThumbnail/AssetThumbnail.d.ts +7 -7
  150. package/dist/{components → types/components}/CreateEntryLinkButton/CreateEntryLinkButton.d.ts +19 -19
  151. package/dist/types/components/CreateEntryLinkButton/CreateEntryLinkButton.spec.d.ts +1 -0
  152. package/dist/{components → types/components}/CreateEntryLinkButton/CreateEntryMenuTrigger.d.ts +31 -31
  153. package/dist/types/components/CreateEntryLinkButton/CreateEntryMenuTrigger.spec.d.ts +1 -0
  154. package/dist/{components → types/components}/CreateEntryLinkButton/useGlobalMouseUp.d.ts +1 -1
  155. package/dist/{components → types/components}/LinkActions/CombinedLinkActions.d.ts +10 -10
  156. package/dist/{components → types/components}/LinkActions/LinkActions.d.ts +26 -26
  157. package/dist/{components → types/components}/LinkActions/LinkEntityActions.d.ts +24 -24
  158. package/dist/types/components/LinkActions/NoLinkPermissionsInfo.d.ts +2 -0
  159. package/dist/{components → types/components}/LinkActions/helpers.d.ts +26 -26
  160. package/dist/{components → types/components}/LinkActions/redesignStyles.d.ts +3 -3
  161. package/dist/{components → types/components}/LinkActions/styles.d.ts +2 -2
  162. package/dist/{components → types/components}/MissingEntityCard/MissingEntityCard.d.ts +8 -8
  163. package/dist/{components → types/components}/MissingEntityCard/styles.d.ts +2 -2
  164. package/dist/{components → types/components}/ScheduledIconWithTooltip/ScheduleTooltip.d.ts +11 -11
  165. package/dist/{components → types/components}/ScheduledIconWithTooltip/ScheduledIconWithTooltip.d.ts +10 -10
  166. package/dist/{components → types/components}/ScheduledIconWithTooltip/formatDateAndTime.d.ts +15 -15
  167. package/dist/types/components/SpaceName/SpaceName.d.ts +6 -0
  168. package/dist/{components → types/components}/index.d.ts +9 -9
  169. package/dist/{entries → types/entries}/MultipleEntryReferenceEditor.d.ts +3 -3
  170. package/dist/{entries → types/entries}/SingleEntryReferenceEditor.d.ts +8 -8
  171. package/dist/{entries → types/entries}/WrappedEntryCard/FetchingWrappedEntryCard.d.ts +18 -18
  172. package/dist/{entries → types/entries}/WrappedEntryCard/WrappedEntryCard.d.ts +35 -35
  173. package/dist/{entries → types/entries}/index.d.ts +3 -3
  174. package/dist/{index.d.ts → types/index.d.ts} +10 -8
  175. package/dist/{resources → types/resources}/Cards/ContentfulEntryCard.d.ts +21 -21
  176. package/dist/{resources → types/resources}/Cards/ResourceCard.d.ts +12 -12
  177. package/dist/{resources → types/resources}/Cards/UnsupportedEntityCard.d.ts +4 -4
  178. package/dist/{resources → types/resources}/MultipleResourceReferenceEditor.d.ts +7 -7
  179. package/dist/types/resources/MultipleResourceReferenceEditor.spec.d.ts +1 -0
  180. package/dist/{resources → types/resources}/SingleResourceReferenceEditor.d.ts +7 -7
  181. package/dist/types/resources/SingleResourceReferenceEditor.spec.d.ts +1 -0
  182. package/dist/{resources → types/resources}/index.d.ts +2 -2
  183. package/dist/{resources → types/resources}/testHelpers/resourceEditorHelpers.d.ts +50 -50
  184. package/dist/{resources → types/resources}/useResourceLinkActions.d.ts +7 -7
  185. package/dist/{types.d.ts → types/types.d.ts} +104 -104
  186. package/dist/{utils → types/utils}/fromFieldValidations.d.ts +21 -21
  187. package/package.json +25 -11
  188. package/CHANGELOG.md +0 -860
  189. package/dist/assets/SingleMediaEditor.d.ts +0 -10
  190. package/dist/common/useAccessApi.d.ts +0 -16
  191. package/dist/components/LinkActions/NoLinkPermissionsInfo.d.ts +0 -2
  192. package/dist/components/SpaceName/SpaceName.d.ts +0 -6
  193. package/dist/field-editor-reference.cjs.development.js +0 -2753
  194. package/dist/field-editor-reference.cjs.development.js.map +0 -1
  195. package/dist/field-editor-reference.cjs.production.min.js +0 -2
  196. package/dist/field-editor-reference.cjs.production.min.js.map +0 -1
  197. package/dist/field-editor-reference.esm.js +0 -2727
  198. package/dist/field-editor-reference.esm.js.map +0 -1
  199. package/dist/index.js +0 -8
@@ -0,0 +1,206 @@
1
+ import * as React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { act, configure, fireEvent, render, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
4
+ import noop from 'lodash/noop';
5
+ import { CreateEntryLinkButton } from './CreateEntryLinkButton';
6
+ configure({
7
+ testIdAttribute: 'data-test-id'
8
+ });
9
+ const CONTENT_TYPE_1 = {
10
+ name: 'name-1',
11
+ sys: {
12
+ id: 'ID_1'
13
+ }
14
+ };
15
+ const CONTENT_TYPE_2 = {
16
+ name: 'name-2',
17
+ sys: {
18
+ id: 'ID_2'
19
+ }
20
+ };
21
+ const CONTENT_TYPE_3 = {
22
+ name: 'name-3',
23
+ sys: {
24
+ id: 'ID_3'
25
+ }
26
+ };
27
+ const findButton = (getByTestId)=>getByTestId('create-entry-link-button');
28
+ describe('CreateEntryLinkButton general', ()=>{
29
+ const props = {
30
+ contentTypes: [
31
+ CONTENT_TYPE_1,
32
+ CONTENT_TYPE_2,
33
+ CONTENT_TYPE_3
34
+ ],
35
+ onSelect: ()=>{
36
+ return Promise.resolve();
37
+ }
38
+ };
39
+ it('renders with multiple content types as list', ()=>{
40
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, props));
41
+ expect(getByTestId('create-entry-button-menu-trigger')).toBeDefined();
42
+ const link = findButton(getByTestId);
43
+ expect(link).toBeDefined();
44
+ expect(link.textContent).toBe('Add entry');
45
+ });
46
+ it('renders dropdown menu on click when with multiple content types', ()=>{
47
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, props));
48
+ fireEvent.click(findButton(getByTestId));
49
+ const menu = getByTestId('add-entry-menu');
50
+ expect(menu).toBeDefined();
51
+ const menuItems = menu.querySelectorAll('[data-test-id="contentType"]');
52
+ expect(menuItems).toHaveLength(props.contentTypes.length);
53
+ menuItems.forEach((item, index)=>expect(item.textContent).toBe(props.contentTypes[index].name));
54
+ });
55
+ it('renders suggestedContentType as text when given', ()=>{
56
+ const suggestedContentTypeId = 'ID_2';
57
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
58
+ ...props,
59
+ suggestedContentTypeId: suggestedContentTypeId
60
+ }));
61
+ expect(getByTestId('create-entry-button-menu-trigger')).toBeDefined();
62
+ const button = findButton(getByTestId);
63
+ expect(button).toBeDefined();
64
+ expect(button.textContent).toBe(`Add ${CONTENT_TYPE_2.name}`);
65
+ });
66
+ it('renders the name of the content type as part of the text if only 1 content type is given', ()=>{
67
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
68
+ onSelect: props.onSelect,
69
+ contentTypes: [
70
+ CONTENT_TYPE_1
71
+ ]
72
+ }));
73
+ expect(getByTestId('create-entry-button-menu-trigger')).toBeDefined();
74
+ const button = findButton(getByTestId);
75
+ expect(button).toBeDefined();
76
+ expect(button.textContent).toBe(`Add ${CONTENT_TYPE_1.name}`);
77
+ });
78
+ it('renders custom text, icon', ()=>{
79
+ const propsOverrides = {
80
+ text: 'CUSTOM_TEXT',
81
+ hasPlusIcon: true
82
+ };
83
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
84
+ ...props,
85
+ ...propsOverrides
86
+ }));
87
+ const link = findButton(getByTestId);
88
+ expect(link.textContent).toBe(propsOverrides.text);
89
+ expect(link.querySelectorAll('svg')).toHaveLength(2);
90
+ });
91
+ });
92
+ describe('CreateEntryLinkButton with multiple entries', ()=>{
93
+ const props = {
94
+ contentTypes: [
95
+ CONTENT_TYPE_1,
96
+ CONTENT_TYPE_2,
97
+ CONTENT_TYPE_3
98
+ ],
99
+ onSelect: ()=>{
100
+ return Promise.resolve();
101
+ }
102
+ };
103
+ it('should render dropdown items for each content type', ()=>{
104
+ const { getByTestId , getAllByTestId } = render(React.createElement(CreateEntryLinkButton, props));
105
+ fireEvent.click(findButton(getByTestId));
106
+ expect(getAllByTestId('contentType')).toHaveLength(props.contentTypes.length);
107
+ });
108
+ it('calls onSelect after click on menu item', ()=>{
109
+ const selectSpy = jest.fn();
110
+ const { getByTestId , getAllByTestId } = render(React.createElement(CreateEntryLinkButton, {
111
+ ...props,
112
+ onSelect: selectSpy
113
+ }));
114
+ fireEvent.click(findButton(getByTestId));
115
+ fireEvent.click(getAllByTestId('contentType')[1]);
116
+ expect(selectSpy).toHaveBeenCalledWith(CONTENT_TYPE_2.sys.id);
117
+ });
118
+ });
119
+ describe('CreateEntryLinkButton with a single entry', ()=>{
120
+ const props = {
121
+ contentTypes: [
122
+ CONTENT_TYPE_1
123
+ ],
124
+ onSelect: ()=>{
125
+ return Promise.resolve();
126
+ }
127
+ };
128
+ it('should fire the onSelect function when clicked', ()=>{
129
+ const onSelectStub = jest.fn();
130
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
131
+ ...props,
132
+ onSelect: onSelectStub
133
+ }));
134
+ fireEvent.click(findButton(getByTestId));
135
+ expect(onSelectStub).toHaveBeenCalledWith(props.contentTypes[0].sys.id);
136
+ expect(()=>getByTestId('cf-ui-spinner')).toThrow('Unable to find an element by: [data-test-id="cf-ui-spinner"]');
137
+ });
138
+ });
139
+ describe('CreateEntryLinkButton common', ()=>{
140
+ it('should render a spinner if onSelect returns a promise', async ()=>{
141
+ const onSelect = jest.fn(()=>new Promise((resolve)=>setTimeout(resolve, 1000)));
142
+ const { getByTestId , container } = render(React.createElement(CreateEntryLinkButton, {
143
+ contentTypes: [
144
+ CONTENT_TYPE_1
145
+ ],
146
+ onSelect: onSelect
147
+ }));
148
+ fireEvent.click(findButton(getByTestId));
149
+ expect(onSelect).toHaveBeenCalled();
150
+ const spinner = await waitFor(()=>getByTestId('cf-ui-spinner'), {
151
+ container
152
+ });
153
+ expect(spinner).toBeDefined();
154
+ expect(spinner.textContent).toMatch(/Loading/g);
155
+ });
156
+ it('should hide a spinner after the promise from onSelect resolves', async ()=>{
157
+ const onSelect = jest.fn(()=>new Promise((resolve)=>setTimeout(resolve, 500)));
158
+ const { getByTestId , container } = render(React.createElement(CreateEntryLinkButton, {
159
+ contentTypes: [
160
+ CONTENT_TYPE_1
161
+ ],
162
+ onSelect: onSelect
163
+ }));
164
+ fireEvent.click(findButton(getByTestId));
165
+ const getSpinner = ()=>getByTestId('cf-ui-spinner');
166
+ const spinner = await waitFor(getSpinner, {
167
+ container
168
+ });
169
+ expect(spinner).toBeDefined();
170
+ await waitForElementToBeRemoved(()=>document.querySelector('[data-test-id="cf-ui-spinner"]'));
171
+ expect(getSpinner).toThrow('Unable to find an element by: [data-test-id="cf-ui-spinner"]');
172
+ });
173
+ it('does not emit onSelect on subsequent click before the promise from onSelect resolves', async ()=>{
174
+ const onSelect = jest.fn(()=>new Promise((resolve)=>setTimeout(()=>resolve(undefined), 200)));
175
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
176
+ contentTypes: [
177
+ CONTENT_TYPE_1
178
+ ],
179
+ onSelect: onSelect
180
+ }));
181
+ fireEvent.click(findButton(getByTestId));
182
+ fireEvent.click(findButton(getByTestId));
183
+ fireEvent.click(findButton(getByTestId));
184
+ await waitFor(noop, {
185
+ timeout: 1000
186
+ });
187
+ expect(onSelect).toHaveBeenCalledTimes(1);
188
+ });
189
+ it('emits onSelect on subsequent click after the promise from onSelect resolves', async ()=>{
190
+ const onSelect = jest.fn(()=>Promise.resolve());
191
+ const { getByTestId } = render(React.createElement(CreateEntryLinkButton, {
192
+ contentTypes: [
193
+ CONTENT_TYPE_1
194
+ ],
195
+ onSelect: onSelect
196
+ }));
197
+ await act(async ()=>{
198
+ fireEvent.click(findButton(getByTestId));
199
+ await waitFor(noop, {
200
+ timeout: 100
201
+ });
202
+ fireEvent.click(findButton(getByTestId));
203
+ });
204
+ expect(onSelect).toHaveBeenCalledTimes(2);
205
+ });
206
+ });
@@ -0,0 +1,145 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { TextInput, Menu } from '@contentful/f36-components';
3
+ import { SearchIcon } from '@contentful/f36-icons';
4
+ import tokens from '@contentful/f36-tokens';
5
+ import { css } from 'emotion';
6
+ import get from 'lodash/get';
7
+ const MAX_ITEMS_WITHOUT_SEARCH = 5;
8
+ const menuPlacementMap = {
9
+ 'bottom-left': 'bottom-start',
10
+ 'bottom-right': 'bottom-end'
11
+ };
12
+ const styles = {
13
+ wrapper: css({
14
+ position: 'relative'
15
+ }),
16
+ inputWrapper: css({
17
+ position: 'relative',
18
+ padding: `0 ${tokens.spacing2Xs}`
19
+ }),
20
+ searchInput: css({
21
+ paddingRight: tokens.spacingXl,
22
+ textOverflow: 'ellipsis'
23
+ }),
24
+ searchIcon: css({
25
+ position: 'absolute',
26
+ right: tokens.spacingM,
27
+ top: tokens.spacingS,
28
+ zIndex: Number(tokens.zIndexDefault),
29
+ fill: tokens.gray600
30
+ }),
31
+ separator: css({
32
+ background: tokens.gray200,
33
+ margin: '10px 0'
34
+ }),
35
+ dropdownList: css({
36
+ borderColor: tokens.gray200
37
+ })
38
+ };
39
+ export const CreateEntryMenuTrigger = ({ contentTypes , suggestedContentTypeId , contentTypesLabel , onSelect , testId , dropdownSettings ={
40
+ position: 'bottom-left'
41
+ } , customDropdownItems , children })=>{
42
+ const [isOpen, setOpen] = useState(false);
43
+ const [isSelecting, setSelecting] = useState(false);
44
+ const [searchInput, setSearchInput] = useState('');
45
+ const wrapper = useRef(null);
46
+ const textField = useRef(null);
47
+ const menuListRef = useRef(null);
48
+ const [dropdownWidth, setDropdownWidth] = useState();
49
+ const hasDropdown = contentTypes.length > 1 || !!customDropdownItems;
50
+ const closeMenu = ()=>setOpen(false);
51
+ useEffect(()=>{
52
+ if (isOpen) {
53
+ setTimeout(()=>{
54
+ textField.current?.querySelector('input')?.focus({
55
+ preventScroll: true
56
+ });
57
+ }, 200);
58
+ }
59
+ }, [
60
+ isOpen
61
+ ]);
62
+ useEffect(()=>{
63
+ if (isOpen && !dropdownWidth) {
64
+ setDropdownWidth(menuListRef.current?.clientWidth);
65
+ }
66
+ }, [
67
+ isOpen,
68
+ dropdownWidth
69
+ ]);
70
+ const handleSelect = (item)=>{
71
+ closeMenu();
72
+ const res = onSelect(item.sys.id);
73
+ if (res && typeof res.then === 'function') {
74
+ setSelecting(true);
75
+ res.then(()=>setSelecting(false), ()=>setSelecting(false));
76
+ }
77
+ };
78
+ const handleMenuOpen = ()=>{
79
+ if (hasDropdown) {
80
+ setOpen(true);
81
+ } else {
82
+ handleSelect(contentTypes[0]);
83
+ }
84
+ };
85
+ useEffect(()=>{
86
+ if (!isOpen) {
87
+ setSearchInput('');
88
+ }
89
+ }, [
90
+ isOpen
91
+ ]);
92
+ const renderSearchResultsCount = (resultsLength)=>resultsLength ? React.createElement(Menu.SectionTitle, {
93
+ testId: "add-entru-menu-search-results"
94
+ }, resultsLength, " result", resultsLength > 1 ? 's' : '') : null;
95
+ const isSearchable = contentTypes.length > MAX_ITEMS_WITHOUT_SEARCH;
96
+ const maxDropdownHeight = suggestedContentTypeId ? 300 : 250;
97
+ const suggestedContentType = contentTypes.find((ct)=>ct.sys.id === suggestedContentTypeId);
98
+ const filteredContentTypes = contentTypes.filter((ct)=>!searchInput || get(ct, 'name', 'Untitled').toLowerCase().includes(searchInput.toLowerCase()));
99
+ return React.createElement("span", {
100
+ className: styles.wrapper,
101
+ ref: wrapper,
102
+ "data-test-id": testId
103
+ }, React.createElement(Menu, {
104
+ placement: menuPlacementMap[dropdownSettings.position],
105
+ isAutoalignmentEnabled: dropdownSettings.isAutoalignmentEnabled,
106
+ isOpen: isOpen,
107
+ onClose: closeMenu,
108
+ onOpen: handleMenuOpen
109
+ }, React.createElement(Menu.Trigger, null, children({
110
+ isOpen,
111
+ isSelecting
112
+ })), isOpen && React.createElement(Menu.List, {
113
+ className: styles.dropdownList,
114
+ style: {
115
+ width: dropdownWidth != undefined ? `${dropdownWidth}px` : undefined,
116
+ maxHeight: `${maxDropdownHeight}px`
117
+ },
118
+ ref: menuListRef,
119
+ testId: "add-entry-menu"
120
+ }, Boolean(customDropdownItems) && React.createElement(React.Fragment, null, customDropdownItems, React.createElement(Menu.Divider, null)), isSearchable && React.createElement(React.Fragment, null, React.createElement("div", {
121
+ ref: textField,
122
+ className: styles.inputWrapper
123
+ }, React.createElement(TextInput, {
124
+ className: styles.searchInput,
125
+ placeholder: "Search all content types",
126
+ testId: "add-entry-menu-search",
127
+ value: searchInput,
128
+ onChange: (e)=>setSearchInput(e.target.value)
129
+ }), React.createElement(SearchIcon, {
130
+ className: styles.searchIcon
131
+ })), React.createElement(Menu.Divider, null)), searchInput && renderSearchResultsCount(filteredContentTypes.length), suggestedContentType && !searchInput && React.createElement(React.Fragment, null, React.createElement(Menu.SectionTitle, null, "Suggested Content Type"), React.createElement(Menu.Item, {
132
+ testId: "suggested",
133
+ onClick: ()=>handleSelect(suggestedContentType)
134
+ }, get(suggestedContentType, 'name')), React.createElement(Menu.Divider, null)), !searchInput && React.createElement(Menu.SectionTitle, null, contentTypesLabel), filteredContentTypes.length ? filteredContentTypes.map((contentType, i)=>React.createElement(Menu.Item, {
135
+ testId: "contentType",
136
+ key: `${get(contentType, 'name')}-${i}`,
137
+ onClick: ()=>handleSelect(contentType)
138
+ }, get(contentType, 'name', 'Untitled'))) : React.createElement(Menu.Item, {
139
+ testId: "add-entru-menu-search-results"
140
+ }, "No results found"))));
141
+ };
142
+ CreateEntryMenuTrigger.defaultProps = {
143
+ testId: 'create-entry-button-menu-trigger',
144
+ contentTypesLabel: 'All Content Types'
145
+ };
@@ -0,0 +1,142 @@
1
+ import * as React from 'react';
2
+ import { Button } from '@contentful/f36-components';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { act, configure, fireEvent, render } from '@testing-library/react';
5
+ import noop from 'lodash/noop';
6
+ import fill from 'lodash/fill';
7
+ import { CreateEntryMenuTrigger } from './CreateEntryMenuTrigger';
8
+ configure({
9
+ testIdAttribute: 'data-test-id'
10
+ });
11
+ const CONTENT_TYPE_1 = {
12
+ name: 'name-1',
13
+ sys: {
14
+ id: 'ID_1'
15
+ }
16
+ };
17
+ const CONTENT_TYPE_2 = {
18
+ name: 'name-2',
19
+ sys: {
20
+ id: 'ID_2'
21
+ }
22
+ };
23
+ const CONTENT_TYPE_3 = {
24
+ name: 'name-3',
25
+ sys: {
26
+ id: 'ID_3'
27
+ }
28
+ };
29
+ describe('CreateEntryMenuTrigger general', ()=>{
30
+ const props = {
31
+ contentTypes: [
32
+ CONTENT_TYPE_1,
33
+ CONTENT_TYPE_2,
34
+ CONTENT_TYPE_3
35
+ ],
36
+ onSelect: ()=>{
37
+ return Promise.resolve();
38
+ }
39
+ };
40
+ let stub = jest.fn();
41
+ beforeEach(()=>{
42
+ stub = jest.fn().mockImplementation(()=>React.createElement(Button, {
43
+ testId: "menu-trigger"
44
+ }));
45
+ });
46
+ it('shares the state and functions for the menu', ()=>{
47
+ const stub = (api)=>{
48
+ expect(api.isOpen).toBe(false);
49
+ expect(api.isSelecting).toBe(false);
50
+ return React.createElement("span", null);
51
+ };
52
+ render(React.createElement(CreateEntryMenuTrigger, props, stub));
53
+ });
54
+ it('should set isSelecting to true in case onSelect returns a promise', async ()=>{
55
+ const selectStub = jest.fn(()=>new Promise((resolve)=>setTimeout(resolve, 1000)));
56
+ const { getAllByTestId , getByTestId } = render(React.createElement(CreateEntryMenuTrigger, {
57
+ ...props,
58
+ onSelect: selectStub
59
+ }, stub));
60
+ act(()=>{
61
+ fireEvent.click(getByTestId('menu-trigger'));
62
+ });
63
+ act(()=>{
64
+ fireEvent.click(getAllByTestId('contentType')[0]);
65
+ });
66
+ expect(selectStub).toHaveBeenCalled();
67
+ });
68
+ it('should not set isSelecting to true in case onSelect is sync', async ()=>{
69
+ const selectStub = jest.fn();
70
+ const { getAllByTestId , getByTestId } = render(React.createElement(CreateEntryMenuTrigger, {
71
+ ...props,
72
+ onSelect: selectStub
73
+ }, stub));
74
+ act(()=>{
75
+ fireEvent.click(getByTestId('menu-trigger'));
76
+ });
77
+ act(()=>{
78
+ fireEvent.click(getAllByTestId('contentType')[0]);
79
+ });
80
+ expect(stub).toHaveBeenLastCalledWith({
81
+ isOpen: false,
82
+ isSelecting: false
83
+ });
84
+ expect(selectStub).toHaveBeenCalled();
85
+ });
86
+ it('renders text input if contentTypes.length > 20', ()=>{
87
+ const { getByTestId } = render(React.createElement(CreateEntryMenuTrigger, {
88
+ ...props,
89
+ contentTypes: fill(Array(21), CONTENT_TYPE_3)
90
+ }, stub));
91
+ act(()=>{
92
+ fireEvent.click(getByTestId('menu-trigger'));
93
+ });
94
+ expect(getByTestId('add-entry-menu-search')).toBeDefined();
95
+ });
96
+ it('shows the search results if typed in input', ()=>{
97
+ const contentTypes = fill(fill(fill(Array(21), CONTENT_TYPE_1, 0, 10), CONTENT_TYPE_2, 10, 20), CONTENT_TYPE_3, 20);
98
+ const { getByTestId , getAllByTestId } = render(React.createElement(CreateEntryMenuTrigger, {
99
+ ...props,
100
+ contentTypes: contentTypes
101
+ }, stub));
102
+ act(()=>{
103
+ fireEvent.click(getByTestId('menu-trigger'));
104
+ });
105
+ const input = getByTestId('add-entry-menu-search');
106
+ fireEvent.change(input, {
107
+ target: {
108
+ value: '1'
109
+ },
110
+ preventDefault: noop
111
+ });
112
+ expect(getAllByTestId('contentType')).toHaveLength(10);
113
+ expect(getByTestId('add-entru-menu-search-results').textContent).toBe('10 results');
114
+ fireEvent.change(input, {
115
+ target: {
116
+ value: '3'
117
+ },
118
+ preventDefault: noop
119
+ });
120
+ expect(getAllByTestId('contentType')).toHaveLength(1);
121
+ expect(getByTestId('add-entru-menu-search-results').textContent).toBe('1 result');
122
+ fireEvent.change(input, {
123
+ target: {
124
+ value: '4'
125
+ },
126
+ preventDefault: noop
127
+ });
128
+ expect(getByTestId('add-entru-menu-search-results').textContent).toBe('No results found');
129
+ });
130
+ it('shows suggestedContentType in the list', ()=>{
131
+ const { getByTestId } = render(React.createElement(CreateEntryMenuTrigger, {
132
+ ...props,
133
+ suggestedContentTypeId: props.contentTypes[0].sys.id
134
+ }, stub));
135
+ act(()=>{
136
+ fireEvent.click(getByTestId('menu-trigger'));
137
+ });
138
+ const suggestedContentType = getByTestId('suggested');
139
+ expect(suggestedContentType).toBeDefined();
140
+ expect(suggestedContentType.textContent).toBe(props.contentTypes[0].name);
141
+ });
142
+ });
@@ -0,0 +1,9 @@
1
+ import { useEffect } from 'react';
2
+ export const useGlobalMouseUp = (handler)=>{
3
+ useEffect(()=>{
4
+ document.addEventListener('mouseup', handler);
5
+ return ()=>document.removeEventListener('mouseup', handler);
6
+ }, [
7
+ handler
8
+ ]);
9
+ };
@@ -0,0 +1,118 @@
1
+ import * as React from 'react';
2
+ import { Button, Menu } from '@contentful/f36-components';
3
+ import { LinkIcon, PlusIcon, ChevronDownIcon } from '@contentful/f36-icons';
4
+ import { CreateEntryLinkButton } from '../CreateEntryLinkButton/CreateEntryLinkButton';
5
+ import { testIds as sharedTextIds } from './LinkActions';
6
+ import { NoLinkPermissionsInfo } from './NoLinkPermissionsInfo';
7
+ import * as styles from './redesignStyles';
8
+ const testIds = {
9
+ ...sharedTextIds,
10
+ actionsWrapper: 'link-actions-menu-trigger'
11
+ };
12
+ export function CombinedLinkActions(props) {
13
+ if (props.isFull) {
14
+ return null;
15
+ }
16
+ const hideEmptyCard = props.entityType === 'Asset' && !props.isEmpty;
17
+ return React.createElement("div", {
18
+ className: hideEmptyCard ? '' : styles.container
19
+ }, !props.canCreateEntity && !props.canLinkEntity && React.createElement(NoLinkPermissionsInfo, null), props.entityType === 'Entry' && React.createElement(CombinedEntryLinkActions, props), props.entityType === 'Asset' && React.createElement(CombinedAssetLinkActions, props));
20
+ }
21
+ function CombinedEntryLinkActions(props) {
22
+ if (props.canCreateEntity) {
23
+ return React.createElement(CreateEntryLinkButton, {
24
+ testId: testIds.actionsWrapper,
25
+ disabled: props.isDisabled,
26
+ text: props.combinedActionsLabel || 'Add content',
27
+ contentTypes: props.contentTypes,
28
+ hasPlusIcon: true,
29
+ useExperimentalStyles: true,
30
+ dropdownSettings: {
31
+ position: 'bottom-left'
32
+ },
33
+ onSelect: (contentTypeId)=>{
34
+ return contentTypeId ? props.onCreate(contentTypeId) : Promise.resolve();
35
+ },
36
+ customDropdownItems: props.canLinkEntity ? React.createElement(Menu.Item, {
37
+ testId: testIds.linkExisting,
38
+ onClick: ()=>{
39
+ props.onLinkExisting();
40
+ }
41
+ }, "Add existing content") : undefined
42
+ });
43
+ } else if (props.canLinkEntity) {
44
+ return React.createElement(Button, {
45
+ isDisabled: props.isDisabled,
46
+ testId: testIds.linkExisting,
47
+ className: styles.action,
48
+ onClick: ()=>{
49
+ props.onLinkExisting();
50
+ },
51
+ variant: "secondary",
52
+ startIcon: React.createElement(LinkIcon, null),
53
+ size: "small"
54
+ }, "Add existing content");
55
+ }
56
+ return null;
57
+ }
58
+ function CombinedAssetLinkActions(props) {
59
+ const [isOpen, setOpen] = React.useState(false);
60
+ if (!props.canLinkEntity || !props.canCreateEntity) {
61
+ if (props.canLinkEntity) {
62
+ return React.createElement(Button, {
63
+ isDisabled: props.isDisabled,
64
+ testId: testIds.linkExisting,
65
+ className: styles.action,
66
+ onClick: ()=>{
67
+ props.onLinkExisting();
68
+ },
69
+ variant: "secondary",
70
+ startIcon: React.createElement(PlusIcon, null),
71
+ size: "small"
72
+ }, "Add existing media");
73
+ }
74
+ if (props.canCreateEntity) {
75
+ return React.createElement(Button, {
76
+ isDisabled: props.isDisabled,
77
+ testId: testIds.createAndLink,
78
+ className: styles.action,
79
+ onClick: ()=>{
80
+ props.onCreate();
81
+ },
82
+ variant: "secondary",
83
+ startIcon: React.createElement(PlusIcon, null),
84
+ size: "small"
85
+ }, "Add media");
86
+ }
87
+ return null;
88
+ }
89
+ return React.createElement(Menu, {
90
+ isOpen: isOpen,
91
+ onClose: ()=>{
92
+ setOpen(false);
93
+ },
94
+ onOpen: ()=>{
95
+ setOpen(true);
96
+ }
97
+ }, React.createElement(Menu.Trigger, null, React.createElement(Button, {
98
+ endIcon: React.createElement(ChevronDownIcon, null),
99
+ isDisabled: props.isDisabled,
100
+ testId: testIds.actionsWrapper,
101
+ className: styles.action,
102
+ variant: "secondary",
103
+ startIcon: React.createElement(PlusIcon, null),
104
+ size: "small"
105
+ }, "Add media")), isOpen && React.createElement(Menu.List, {
106
+ testId: testIds.dropdown
107
+ }, React.createElement(Menu.Item, {
108
+ testId: testIds.linkExisting,
109
+ onClick: ()=>{
110
+ props.onLinkExisting();
111
+ }
112
+ }, "Add existing media"), React.createElement(Menu.Item, {
113
+ testId: testIds.createAndLink,
114
+ onClick: ()=>{
115
+ props.onCreate();
116
+ }
117
+ }, "Add new media")));
118
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import { Button } from '@contentful/f36-components';
3
+ import { PlusIcon, LinkIcon } from '@contentful/f36-icons';
4
+ import { CreateEntryLinkButton } from '../CreateEntryLinkButton/CreateEntryLinkButton';
5
+ import { NoLinkPermissionsInfo } from './NoLinkPermissionsInfo';
6
+ import * as styles from './styles';
7
+ const defaultEntryLabels = {
8
+ createNew: (props)=>props?.contentType ? `Create new ${props.contentType} and link` : 'Create new entry and link',
9
+ linkExisting: (props)=>props?.canLinkMultiple ? 'Link existing entries' : 'Link existing entry'
10
+ };
11
+ const defaultAssetLabels = {
12
+ createNew: ()=>`Create new asset and link`,
13
+ linkExisting: (props)=>props?.canLinkMultiple ? 'Link existing assets' : 'Link existing asset'
14
+ };
15
+ export const testIds = {
16
+ dropdown: 'linkEditor.dropdown',
17
+ createAndLink: 'linkEditor.createAndLink',
18
+ createAndLinkWrapper: 'create-entry-button-menu-trigger',
19
+ linkExisting: 'linkEditor.linkExisting'
20
+ };
21
+ export function LinkActions(props) {
22
+ if (props.isFull) {
23
+ return null;
24
+ }
25
+ const defaultLabels = props.entityType === 'Entry' ? defaultEntryLabels : defaultAssetLabels;
26
+ const labels = {
27
+ ...defaultLabels,
28
+ ...props.actionLabels
29
+ };
30
+ return React.createElement("div", {
31
+ className: styles.container
32
+ }, props.canCreateEntity && React.createElement(React.Fragment, null, props.entityType === 'Entry' && React.createElement(CreateEntryLinkButton, {
33
+ testId: testIds.createAndLink,
34
+ disabled: props.isDisabled,
35
+ text: labels.createNew({
36
+ contentType: props.contentTypes.length === 1 ? props.contentTypes[0].name : undefined
37
+ }),
38
+ contentTypes: props.contentTypes,
39
+ hasPlusIcon: true,
40
+ onSelect: (contentTypeId)=>{
41
+ return contentTypeId ? props.onCreate(contentTypeId, props.itemsLength) : Promise.resolve();
42
+ }
43
+ }), props.entityType === 'Asset' && React.createElement(Button, {
44
+ isDisabled: props.isDisabled,
45
+ testId: testIds.createAndLink,
46
+ onClick: ()=>{
47
+ props.onCreate(undefined, props.itemsLength);
48
+ },
49
+ variant: "secondary",
50
+ startIcon: React.createElement(PlusIcon, null),
51
+ size: "small"
52
+ }, labels.createNew()), React.createElement("span", {
53
+ className: styles.separator
54
+ })), props.canLinkEntity && React.createElement(Button, {
55
+ isDisabled: props.isDisabled,
56
+ testId: testIds.linkExisting,
57
+ onClick: ()=>{
58
+ props.onLinkExisting();
59
+ },
60
+ variant: "secondary",
61
+ startIcon: React.createElement(LinkIcon, null),
62
+ size: "small"
63
+ }, labels.linkExisting({
64
+ canLinkMultiple: props.canLinkMultiple
65
+ })), !props.canCreateEntity && !props.canLinkEntity && React.createElement(NoLinkPermissionsInfo, null));
66
+ }