@nascentdigital/funnel-editor-contentful 5.0.1 → 5.0.3

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 (164) hide show
  1. package/dist/EditorApp.js +11 -9
  2. package/dist/EditorPageRouter.d.ts +1 -0
  3. package/dist/EditorPageRouter.js +34 -0
  4. package/dist/EditorPageRoutes.d.ts +1 -0
  5. package/dist/EditorPageRoutes.js +16 -0
  6. package/dist/EntryEditor.js +30 -29
  7. package/dist/EntrySidebar.js +26 -5
  8. package/dist/NotFound.d.ts +1 -0
  9. package/dist/NotFound.js +18 -0
  10. package/dist/asset/index.d.ts +1 -0
  11. package/dist/{core/api → asset}/index.js +1 -3
  12. package/dist/{core → asset}/repository/AssetRepository.d.ts +11 -3
  13. package/dist/{core → asset}/repository/AssetRepository.js +6 -5
  14. package/dist/asset/repository/index.d.ts +1 -0
  15. package/dist/{core → asset}/repository/index.js +0 -4
  16. package/dist/collection/controller/{LocalCollectionController.d.ts → ContentfulCollectionController.d.ts} +17 -11
  17. package/dist/collection/controller/ContentfulCollectionController.js +278 -0
  18. package/dist/collection/controller/ContentfulCollectionControllerContainer.d.ts +5 -0
  19. package/dist/collection/controller/ContentfulCollectionControllerContainer.js +28 -0
  20. package/dist/collection/controller/index.d.ts +3 -4
  21. package/dist/collection/controller/index.js +3 -4
  22. package/dist/collection/controller/useContentfulCollectionController.d.ts +2 -0
  23. package/dist/collection/controller/useContentfulCollectionController.js +8 -0
  24. package/dist/{core → collection}/domain/Collection.d.ts +11 -2
  25. package/dist/{core → collection}/domain/Collection.js +76 -0
  26. package/dist/{core → collection}/domain/index.d.ts +0 -1
  27. package/dist/{core → collection}/domain/index.js +0 -1
  28. package/dist/collection/index.d.ts +1 -0
  29. package/dist/collection/index.js +1 -0
  30. package/dist/collection/repository/CollectionRepository.d.ts +49 -0
  31. package/dist/collection/repository/CollectionRepository.js +223 -0
  32. package/dist/collection/repository/index.d.ts +1 -0
  33. package/dist/{core/contentful → collection/repository}/index.js +1 -5
  34. package/dist/collection/screens/CollectionEditor.js +23 -6
  35. package/dist/core/bridge/AppBridge.d.ts +31 -0
  36. package/dist/core/bridge/AppBridge.js +160 -0
  37. package/dist/core/bridge/AppBridgeContainer.d.ts +3 -3
  38. package/dist/core/bridge/AppBridgeContainer.js +8 -44
  39. package/dist/core/bridge/AppBridgeContext.d.ts +7 -8
  40. package/dist/core/bridge/AppBridgeContext.js +1 -6
  41. package/dist/core/bridge/BridgeLifecycleMessage.d.ts +7 -0
  42. package/dist/core/bridge/BridgeLifecycleMessage.js +12 -0
  43. package/dist/core/bridge/IAppBridgeMessage.d.ts +16 -0
  44. package/dist/core/bridge/IAppBridgeMessage.js +12 -0
  45. package/dist/core/bridge/index.d.ts +2 -1
  46. package/dist/core/bridge/index.js +2 -1
  47. package/dist/core/bridge/useAppBridge.d.ts +2 -8
  48. package/dist/core/bridge/useAppBridge.js +24 -15
  49. package/dist/core/client/ContentfulClientContainer.d.ts +8 -0
  50. package/dist/core/client/ContentfulClientContainer.js +43 -0
  51. package/dist/core/client/ContentfulClientContext.d.ts +8 -0
  52. package/dist/core/client/ContentfulClientContext.js +8 -0
  53. package/dist/core/client/index.d.ts +3 -0
  54. package/dist/core/client/index.js +19 -0
  55. package/dist/core/client/useContentfulClient.d.ts +4 -0
  56. package/dist/core/client/useContentfulClient.js +24 -0
  57. package/dist/core/index.d.ts +1 -4
  58. package/dist/core/index.js +1 -4
  59. package/dist/explore/components/header/Header.d.ts +14 -0
  60. package/dist/explore/components/header/Header.js +33 -0
  61. package/dist/explore/components/header/HeaderActions.d.ts +8 -0
  62. package/dist/explore/components/header/HeaderActions.js +37 -0
  63. package/dist/explore/components/header/HeaderContainer.d.ts +2 -0
  64. package/dist/explore/components/header/HeaderContainer.js +15 -0
  65. package/dist/explore/components/header/HeaderContext.d.ts +7 -0
  66. package/dist/{core/api/EditorApiContext.js → explore/components/header/HeaderContext.js} +2 -2
  67. package/dist/explore/components/header/HeaderController.d.ts +39 -0
  68. package/dist/explore/components/header/HeaderController.js +100 -0
  69. package/dist/explore/components/header/HeaderOptions.d.ts +10 -0
  70. package/dist/explore/components/header/HeaderOptions.js +26 -0
  71. package/dist/explore/components/header/index.d.ts +5 -0
  72. package/dist/explore/components/header/index.js +21 -0
  73. package/dist/explore/components/header/useHeader.d.ts +2 -0
  74. package/dist/explore/components/header/useHeader.js +30 -0
  75. package/dist/explore/components/index.d.ts +2 -0
  76. package/dist/explore/components/index.js +18 -0
  77. package/dist/explore/components/pages/PagesExplorerLayout.d.ts +4 -0
  78. package/dist/explore/components/pages/PagesExplorerLayout.js +28 -0
  79. package/dist/explore/components/pages/index.d.ts +1 -0
  80. package/dist/explore/components/pages/index.js +17 -0
  81. package/dist/explore/index.d.ts +2 -0
  82. package/dist/explore/index.js +18 -0
  83. package/dist/explore/screens/Dashboard.d.ts +4 -0
  84. package/dist/explore/screens/Dashboard.js +55 -0
  85. package/dist/explore/screens/PageExplorer.d.ts +11 -0
  86. package/dist/explore/screens/PageExplorer.js +129 -0
  87. package/dist/explore/screens/PagesExplorer.d.ts +7 -0
  88. package/dist/explore/screens/PagesExplorer.js +98 -0
  89. package/dist/explore/screens/index.d.ts +3 -0
  90. package/dist/explore/screens/index.js +19 -0
  91. package/dist/page/bridge/BlockChangedMessage.d.ts +25 -0
  92. package/dist/page/bridge/BlockChangedMessage.js +18 -0
  93. package/dist/page/bridge/PageBridgeHub.d.ts +8 -0
  94. package/dist/page/bridge/PageBridgeHub.js +51 -0
  95. package/dist/page/bridge/index.d.ts +2 -0
  96. package/dist/page/bridge/index.js +18 -0
  97. package/dist/page/controller/{LocalPageController.d.ts → ContentfulPageController.d.ts} +15 -9
  98. package/dist/page/controller/ContentfulPageController.js +277 -0
  99. package/dist/page/controller/ContentfulPageControllerContainer.d.ts +5 -0
  100. package/dist/page/controller/ContentfulPageControllerContainer.js +28 -0
  101. package/dist/page/controller/index.d.ts +3 -3
  102. package/dist/page/controller/index.js +3 -3
  103. package/dist/page/controller/useContentfulPageController.d.ts +2 -0
  104. package/dist/page/controller/useContentfulPageController.js +8 -0
  105. package/dist/{core → page}/domain/Page.d.ts +23 -12
  106. package/dist/{core → page}/domain/Page.js +139 -0
  107. package/dist/page/domain/index.d.ts +1 -0
  108. package/dist/page/domain/index.js +17 -0
  109. package/dist/page/index.d.ts +3 -0
  110. package/dist/page/index.js +3 -0
  111. package/dist/page/repository/PageRepository.d.ts +45 -0
  112. package/dist/page/repository/PageRepository.js +241 -0
  113. package/dist/page/repository/index.d.ts +1 -0
  114. package/dist/page/repository/index.js +17 -0
  115. package/dist/page/screens/PageEditor.js +36 -12
  116. package/package.json +9 -5
  117. package/dist/collection/controller/LocalCollectionController.js +0 -200
  118. package/dist/collection/controller/LocalCollectionControllerContainer.d.ts +0 -8
  119. package/dist/collection/controller/LocalCollectionControllerContainer.js +0 -36
  120. package/dist/collection/controller/LocalCollectionControllerContext.d.ts +0 -7
  121. package/dist/collection/controller/LocalCollectionControllerContext.js +0 -6
  122. package/dist/collection/controller/useLocalCollectionController.d.ts +0 -2
  123. package/dist/collection/controller/useLocalCollectionController.js +0 -10
  124. package/dist/core/api/EditorApiClient.d.ts +0 -7
  125. package/dist/core/api/EditorApiClient.js +0 -29
  126. package/dist/core/api/EditorApiContainer.d.ts +0 -5
  127. package/dist/core/api/EditorApiContainer.js +0 -16
  128. package/dist/core/api/EditorApiContext.d.ts +0 -7
  129. package/dist/core/api/index.d.ts +0 -3
  130. package/dist/core/api/useEditorApi.d.ts +0 -2
  131. package/dist/core/api/useEditorApi.js +0 -12
  132. package/dist/core/bridge/AppMessage.d.ts +0 -19
  133. package/dist/core/bridge/AppMessage.js +0 -19
  134. package/dist/core/contentful/Contentful.d.ts +0 -33
  135. package/dist/core/contentful/Contentful.js +0 -67
  136. package/dist/core/contentful/ContentfulAsset.d.ts +0 -23
  137. package/dist/core/contentful/ContentfulAsset.js +0 -88
  138. package/dist/core/contentful/ContentfulCollection.d.ts +0 -29
  139. package/dist/core/contentful/ContentfulCollection.js +0 -97
  140. package/dist/core/contentful/ContentfulPage.d.ts +0 -31
  141. package/dist/core/contentful/ContentfulPage.js +0 -84
  142. package/dist/core/contentful/ContentfulRefs.d.ts +0 -16
  143. package/dist/core/contentful/ContentfulRefs.js +0 -152
  144. package/dist/core/contentful/index.d.ts +0 -5
  145. package/dist/core/repository/CollectionSerializer.d.ts +0 -9
  146. package/dist/core/repository/CollectionSerializer.js +0 -41
  147. package/dist/core/repository/LocalCollectionRepository.d.ts +0 -27
  148. package/dist/core/repository/LocalCollectionRepository.js +0 -143
  149. package/dist/core/repository/LocalPageRepository.d.ts +0 -16
  150. package/dist/core/repository/LocalPageRepository.js +0 -153
  151. package/dist/core/repository/PageSerializer.d.ts +0 -9
  152. package/dist/core/repository/PageSerializer.js +0 -47
  153. package/dist/core/repository/RemoteCollectionRepository.d.ts +0 -19
  154. package/dist/core/repository/RemoteCollectionRepository.js +0 -236
  155. package/dist/core/repository/RemotePageRepository.d.ts +0 -14
  156. package/dist/core/repository/RemotePageRepository.js +0 -113
  157. package/dist/core/repository/index.d.ts +0 -5
  158. package/dist/page/controller/LocalPageController.js +0 -190
  159. package/dist/page/controller/LocalPageControllerContainer.d.ts +0 -8
  160. package/dist/page/controller/LocalPageControllerContainer.js +0 -36
  161. package/dist/page/controller/LocalPageControllerContext.d.ts +0 -7
  162. package/dist/page/controller/LocalPageControllerContext.js +0 -6
  163. package/dist/page/controller/useLocalPageController.d.ts +0 -2
  164. package/dist/page/controller/useLocalPageController.js +0 -10
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.CollectionRepository = void 0;
15
+ const contentful_1 = require("@nascentdigital/funnel-cms/contentful");
16
+ const funnel_core_1 = require("@nascentdigital/funnel-core");
17
+ const components_1 = require("@nascentdigital/funnel-editor/components");
18
+ const domain_1 = require("../domain");
19
+ // repository
20
+ class CollectionRepository {
21
+ constructor(args) {
22
+ // initialize instance
23
+ this.sdk = args.sdk;
24
+ this.contentfulClient = args.contentfulClient;
25
+ }
26
+ async getOverview(args) {
27
+ throw new funnel_core_1.NotImplementedError();
28
+ }
29
+ async getItems(args) {
30
+ throw new funnel_core_1.NotImplementedError();
31
+ }
32
+ async get(args) {
33
+ throw new funnel_core_1.NotImplementedError();
34
+ }
35
+ async create(args) {
36
+ throw new funnel_core_1.NotImplementedError();
37
+ }
38
+ async createRaw(args) {
39
+ throw new funnel_core_1.NotImplementedError();
40
+ }
41
+ async createLanguage(args) {
42
+ throw new funnel_core_1.NotImplementedError();
43
+ }
44
+ async update(args) {
45
+ throw new funnel_core_1.NotImplementedError();
46
+ }
47
+ serializeUpdate(collection, savables) {
48
+ throw new funnel_core_1.NotImplementedError();
49
+ }
50
+ async load(sys) {
51
+ // blow up if we're not in the editor
52
+ const { sdk, contentfulClient } = this;
53
+ if (!('entry' in sdk)) {
54
+ throw new funnel_core_1.InvalidOperationError('Attempt to call load() outside of EntryEditor');
55
+ }
56
+ // throw if entry isn't a collection (it should be in this case)
57
+ const { entry } = sdk;
58
+ sys = sys !== null && sys !== void 0 ? sys : entry.getSys();
59
+ const { id: entryId, contentType: { sys: { id: entryType } } } = sys;
60
+ if (entryType !== contentful_1.ContentfulCollectionData.contentType) {
61
+ throw new funnel_core_1.ArgumentError(`Cannot load Collection from entry that isn't of "${contentful_1.ContentfulCollectionData.contentType}" type`);
62
+ }
63
+ // throw if page doesn't have a group id
64
+ const collectionId = entryId;
65
+ const type = entry.fields.type.getValue();
66
+ const groupId = entry.fields.groupId.getValue();
67
+ const brand = entry.fields.brand.getValue();
68
+ const language = funnel_core_1.Language.tryParse(entry.fields.language.getValue());
69
+ if (!groupId || !type || !brand || !language) {
70
+ throw new funnel_core_1.ArgumentError(`Collection "${collectionId}" is not configured (missing required fields)`);
71
+ }
72
+ // fetch remote data (throw if not found)
73
+ const result = await contentfulClient.collections.getGroup({ groupId, type, brand, language, version: 'draft' });
74
+ if (!result) {
75
+ throw new funnel_core_1.NotFoundError(`Unable to resolve Collection with id "${collectionId}"`);
76
+ }
77
+ const _a = result.group, { instances: collections } = _a, group = __rest(_a, ["instances"]), { collection: collectionResult } = result;
78
+ if (!collectionResult) {
79
+ throw new funnel_core_1.NotFoundError(`Unable to resolve Collection with id "${collectionId}"`);
80
+ }
81
+ // get entry data + remote references
82
+ const { data: _remote } = collectionResult, references = __rest(collectionResult, ["data"]);
83
+ const data = entryToCollectionData(entry);
84
+ // return collection model from data
85
+ const collection = domain_1.Collection.fromSchemaData({ data, references });
86
+ return {
87
+ collectionGroup: new components_1.CollectionGroup(Object.assign({ template: collection.template, collections }, group)),
88
+ collection,
89
+ sys
90
+ };
91
+ }
92
+ async save(collection) {
93
+ // guard method
94
+ const { sdk } = this;
95
+ if (!('entry' in sdk)) {
96
+ throw new funnel_core_1.InvalidOperationError('Attempt to call save() outside of EntryEditor');
97
+ }
98
+ // serialize collection updates (or skip if there are no changes)
99
+ const savables = [];
100
+ const updateArgs = domain_1.Collection.toUpdates(collection, savables);
101
+ if (updateArgs.updates.length === 0) {
102
+ return collection;
103
+ }
104
+ // start all updates
105
+ collection.onSaveStart();
106
+ savables.forEach(savable => savable.onSaveStart());
107
+ // update entry if page is local
108
+ let updatedOn;
109
+ const { entry } = sdk;
110
+ const { id: entryId, contentType: { sys: { id: entryType } } } = entry.getSys();
111
+ if (entryType === contentful_1.ContentfulCollectionData.contentType && collection.id === entryId) {
112
+ console.debug('[CollectionRepository/save] saving local entry', { entry, collection, updates: updateArgs.updates });
113
+ // save updates to entry
114
+ await saveEntryUpdates({ entry, updates: updateArgs.updates });
115
+ // capture updated timestamp
116
+ updatedOn = new Date(entry.getSys().updatedAt).getTime();
117
+ }
118
+ // or update remotely
119
+ else {
120
+ console.debug('[CollectionRepository/save] saving remote entry', collection);
121
+ // push updates remotely
122
+ const result = await this.contentfulClient.collections.update(updateArgs);
123
+ // capture update timestamp
124
+ updatedOn = result.updatedOn;
125
+ // swap to new collection (if update returned new data)
126
+ if (result.collection) {
127
+ console.debug('[PageRepository/save] switching to new page data', result.collection);
128
+ const _a = result.collection, { data } = _a, references = __rest(_a, ["data"]);
129
+ collection = domain_1.Collection.fromSchemaData({ data, references });
130
+ }
131
+ }
132
+ console.debug('[PageRepository/save] finalizing save');
133
+ // complete updates
134
+ savables.forEach(savable => savable.onSaveComplete());
135
+ collection.onSaveComplete(updatedOn);
136
+ console.debug('[PageRepository/save] save complete', collection);
137
+ // return collection
138
+ return collection;
139
+ }
140
+ async sync({ collection, sys }) {
141
+ // guard method
142
+ const { sdk } = this;
143
+ if (!('entry' in sdk)) {
144
+ throw new funnel_core_1.InvalidOperationError('Attempt to call sync() outside of EntryEditor');
145
+ }
146
+ // skip if entity version and updated sys are same
147
+ const { entry } = sdk;
148
+ const entrySys = entry.getSys();
149
+ if (sys.version === entrySys.version) {
150
+ console.debug('[ContentfulCollectionRepository/sync] ignoring unchanged entity');
151
+ return { collection, sys };
152
+ }
153
+ // force reload if entry references changed
154
+ const entryReferences = entry.fields.references.getValue();
155
+ const entryRefs = new Set(entryReferences.map(link => `${link.sys.linkType}.${link.sys.id}`));
156
+ const { references: collectionReferences } = collection;
157
+ const collectionRefs = new Set(collectionReferences.map(link => `${link.sys.linkType}.${link.sys.id}`));
158
+ if (!collectionRefs.isSupersetOf(entryRefs)) {
159
+ console.debug('[ContentfulCollectionRepository/sync] references changed - triggering reload');
160
+ return this.load(sys);
161
+ }
162
+ console.debug('[ContentfulCollectionRepository/sync] reusing stable references - merging entity');
163
+ // or load page data, but use old references in store
164
+ const data = entryToCollectionData(entry);
165
+ const references = {
166
+ files: collection.assets.reduce((files, file) => {
167
+ files[file.value.id] = file;
168
+ return files;
169
+ }, {}),
170
+ collections: collection.collections.reduce((collections, collection) => {
171
+ collections[collection.id] = collection;
172
+ return collections;
173
+ }, {}),
174
+ };
175
+ // create new page (use existing sys)
176
+ return {
177
+ collection: domain_1.Collection.fromSchemaData({ data, references }),
178
+ sys
179
+ };
180
+ }
181
+ async unpublish(collection) {
182
+ throw new funnel_core_1.NotImplementedError();
183
+ }
184
+ }
185
+ exports.CollectionRepository = CollectionRepository;
186
+ // helpers
187
+ function entryToCollectionData(entry) {
188
+ // throw if it's not a page
189
+ const entryType = entry.getSys().contentType.sys.id;
190
+ if (entryType !== contentful_1.ContentfulCollectionData.contentType) {
191
+ throw new funnel_core_1.ArgumentError(`Expected entry to be "${contentful_1.ContentfulCollectionData.contentType}", but found "${entryType}"`);
192
+ }
193
+ // serialize entry (this is the latest data)
194
+ const entryData = contentful_1.ContentfulData.fromEntryData(entry);
195
+ return contentful_1.ContentfulCollectionData.deserialize(entryData);
196
+ }
197
+ async function saveEntryUpdates({ entry, updates }) {
198
+ var _a;
199
+ // serialize entry (this is the latest data)
200
+ const entryData = contentful_1.ContentfulData.fromEntryData(entry);
201
+ const collectionData = contentful_1.ContentfulCollectionData.deserialize(entryData);
202
+ // create model and apply updates
203
+ const collectionModel = new funnel_core_1.Schema.CollectionModel(collectionData);
204
+ collectionModel.update(updates);
205
+ collectionModel.sync();
206
+ // resolve references
207
+ const references = contentful_1.ContentfulData.findReferences([collectionData.fields]);
208
+ // update fields
209
+ const { fields } = entry;
210
+ await Promise.all([
211
+ // required updates
212
+ fields.name.setValue(collectionData.name),
213
+ fields.state.setValue(collectionData.state),
214
+ fields.errors.setValue(collectionData.errors),
215
+ fields.references.setValue(references),
216
+ // TODO: save only fields that changed
217
+ // conditional updates
218
+ fields.tags.setValue((_a = collectionData.tags) !== null && _a !== void 0 ? _a : []),
219
+ fields.fields.setValue(collectionData.fields),
220
+ ]);
221
+ // save entry
222
+ await entry.save();
223
+ }
@@ -0,0 +1 @@
1
+ export * from './CollectionRepository';
@@ -14,8 +14,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./Contentful"), exports);
18
- __exportStar(require("./ContentfulAsset"), exports);
19
- __exportStar(require("./ContentfulCollection"), exports);
20
- __exportStar(require("./ContentfulPage"), exports);
21
- __exportStar(require("./ContentfulRefs"), exports);
17
+ __exportStar(require("./CollectionRepository"), exports);
@@ -2,24 +2,41 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CollectionEditor = CollectionEditor;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const funnel_core_1 = require("@nascentdigital/funnel-core");
5
6
  const components_1 = require("@nascentdigital/funnel-editor/components");
6
7
  const react_1 = require("react");
7
8
  const controller_1 = require("../controller");
8
9
  // component
9
10
  function CollectionEditor() {
10
11
  // hooks
11
- const controllerContext = (0, controller_1.useLocalCollectionController)();
12
- const { controller } = controllerContext;
13
- const { collection } = controller;
12
+ const splash = (0, components_1.useSplash)();
13
+ const controller = (0, controller_1.useContentfulCollectionController)();
14
+ const { ready, collection } = controller;
14
15
  const collectionContext = (0, react_1.useMemo)(() => ({ collection }), [collection]);
15
16
  // sync with controller
16
17
  (0, components_1.useCollectionController)(controller);
17
18
  // load
18
19
  (0, react_1.useEffect)(() => {
19
- controller.load().catch(console.error);
20
- }, [controller]);
20
+ controller.load().catch(e => {
21
+ // transform error
22
+ const error = funnel_core_1.IsometricErrorUtils.coerceError(e);
23
+ // TODO: come up with an appropriate message
24
+ const message = error.message;
25
+ // show error via splash
26
+ splash.showError({ message, onRetry: () => controller.reload() });
27
+ });
28
+ }, [controller, splash]);
29
+ // dismiss splash when ready
30
+ (0, react_1.useEffect)(() => {
31
+ if (ready) {
32
+ splash.dismiss();
33
+ }
34
+ else {
35
+ splash.showLoading('Fetching content');
36
+ }
37
+ }, [splash, ready]);
21
38
  // render
22
- return ((0, jsx_runtime_1.jsx)(components_1.CollectionControllerContext.Provider, { value: controllerContext, children: (0, jsx_runtime_1.jsx)(components_1.CollectionContext.Provider, { value: collectionContext, children: (0, jsx_runtime_1.jsx)(components_1.AssetStoreContext.Provider, { value: collection, children: (0, jsx_runtime_1.jsx)(components_1.CollectionStoreContext.Provider, { value: collection, children: (0, jsx_runtime_1.jsx)("div", { className: "w-dvw h-dvh flex flex-col bg-background", children: (0, jsx_runtime_1.jsx)("div", { className: "relative flex-1", children: (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex", children: (0, jsx_runtime_1.jsx)("section", { className: "flex-1 h-full max-h-full max-w-[1000px] mx-auto", children: renderContent({ controller }) }) }) }) }) }) }) }) }));
39
+ return ((0, jsx_runtime_1.jsx)(components_1.CollectionContext.Provider, { value: collectionContext, children: (0, jsx_runtime_1.jsx)(components_1.AssetStoreContext.Provider, { value: collection, children: (0, jsx_runtime_1.jsx)(components_1.CollectionStoreContext.Provider, { value: collection, children: (0, jsx_runtime_1.jsx)("div", { className: "w-dvw h-dvh flex flex-col bg-background", children: (0, jsx_runtime_1.jsx)("div", { className: "relative flex-1", children: (0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex", children: (0, jsx_runtime_1.jsx)("section", { className: "flex-1 h-full max-h-full max-w-[1000px] mx-auto", children: renderContent({ controller }) }) }) }) }) }) }) }));
23
40
  }
24
41
  function renderContent({ controller }) {
25
42
  // render loader if not ready
@@ -0,0 +1,31 @@
1
+ import { Callback } from '@nascentdigital/funnel-core';
2
+ import { Observable } from 'rxjs';
3
+ import { Tagged } from 'type-fest';
4
+ import { IAppBridgeMessage } from './IAppBridgeMessage';
5
+ export declare class AppBridge {
6
+ readonly id: AppBridge.Id;
7
+ readonly role: AppBridge.Role;
8
+ private readonly _bridges$;
9
+ private readonly _messages$;
10
+ private readonly _state$;
11
+ private readonly _channel;
12
+ constructor(role: string);
13
+ get messages$(): Observable<IAppBridgeMessage<string, string>>;
14
+ get bridges$(): Observable<ReadonlySet<AppBridge.Id>>;
15
+ get bridges(): ReadonlySet<AppBridge.Id>;
16
+ private addBridge;
17
+ private removeBridge;
18
+ get state$(): Observable<AppBridge.State>;
19
+ get state(): AppBridge.State;
20
+ private set state(value);
21
+ connect(): AppBridge.DisconnectMethod;
22
+ send<TMessage extends IAppBridgeMessage>(args: AppBridge.SendArgs<TMessage>): void;
23
+ }
24
+ export declare namespace AppBridge {
25
+ type Id = Tagged<string, 'AppBridge.Id'>;
26
+ type Role = 'hub' | 'client';
27
+ type State = 'uninitialized' | 'initializing' | 'ready';
28
+ type DisconnectMethod = Callback.Void;
29
+ type SendArgs<TMessage extends IAppBridgeMessage> = Omit<TMessage, 'senderId' | 'sonderRole' | 'timestamp'>;
30
+ }
31
+ export declare function createBridgeChannel(): BroadcastChannel;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppBridge = void 0;
4
+ exports.createBridgeChannel = createBridgeChannel;
5
+ const funnel_core_1 = require("@nascentdigital/funnel-core");
6
+ const rxjs_1 = require("rxjs");
7
+ const BridgeLifecycleMessage_1 = require("./BridgeLifecycleMessage");
8
+ const IAppBridgeMessage_1 = require("./IAppBridgeMessage");
9
+ // class
10
+ class AppBridge {
11
+ constructor(role) {
12
+ // initialize instance variables
13
+ this.id = funnel_core_1.Value.uuid();
14
+ this.role = role;
15
+ this._bridges$ = new rxjs_1.BehaviorSubject(new Set());
16
+ this._messages$ = new rxjs_1.Subject();
17
+ this._state$ = new rxjs_1.BehaviorSubject('uninitialized');
18
+ this._channel = createBridgeChannel();
19
+ }
20
+ get messages$() {
21
+ return this._messages$.asObservable();
22
+ }
23
+ get bridges$() {
24
+ return this._bridges$.asObservable();
25
+ }
26
+ get bridges() {
27
+ return this._bridges$.value;
28
+ }
29
+ addBridge(bridge) {
30
+ const bridges = new Set(this.bridges);
31
+ bridges.add(bridge);
32
+ this._bridges$.next(bridges);
33
+ }
34
+ removeBridge(bridge) {
35
+ const bridges = new Set(this.bridges);
36
+ bridges.delete(bridge);
37
+ this._bridges$.next(bridges);
38
+ }
39
+ get state$() {
40
+ return this._state$.asObservable();
41
+ }
42
+ get state() {
43
+ return this._state$.value;
44
+ }
45
+ set state(state) {
46
+ this._state$.next(state);
47
+ }
48
+ connect() {
49
+ // throw if already connecting/connected
50
+ const { id, role, _messages$, _channel } = this;
51
+ if (this.state !== 'uninitialized') {
52
+ throw new funnel_core_1.InvalidOperationError('Cannot call connect() unless disconnected');
53
+ }
54
+ console.debug(`[${role}/${id}] initializing`);
55
+ // update state
56
+ this.state = 'initializing';
57
+ // add self to bridges
58
+ this.addBridge(id);
59
+ // create broadcast handler
60
+ const onChannelMessage = (event) => {
61
+ console.debug(`[${role}/${id}] received event`, event);
62
+ // ignore message if it's an echo
63
+ const message = event.data;
64
+ if (message.senderId === id) {
65
+ console.debug(`[${role}/${id}] ignoring echo`);
66
+ return;
67
+ }
68
+ // or ignore if it's directed to another bridge type
69
+ else if (message.recipientId && message.recipientId !== id) {
70
+ console.debug(`[${role}/${id}] ignoring message for recipient: `, message.recipientId);
71
+ return;
72
+ }
73
+ // handle bridge lifecycle events
74
+ if (IAppBridgeMessage_1.IAppBridgeMessage.filter(message, 'appBridge')) {
75
+ switch (message.type) {
76
+ case 'connect': {
77
+ console.debug(`[${role}/${id}] tracking connect for:`, message.senderId);
78
+ // track bridge connect
79
+ this.addBridge(message.senderId);
80
+ // greet if hub and sender joined
81
+ if (role === 'hub' && message.sonderRole === 'client') {
82
+ console.debug(`[${role}/${id}] greeting client: `, message.senderId);
83
+ // track client connect
84
+ this.send(BridgeLifecycleMessage_1.BridgeLifecycleMessage.create({ type: 'greet', recipientId: message.senderId }));
85
+ }
86
+ break;
87
+ }
88
+ case 'greet': {
89
+ // track bridge on greet
90
+ this.addBridge(message.senderId);
91
+ // mark client ready when greeted by hub
92
+ if (role === 'client' && message.sonderRole === 'hub') {
93
+ console.debug(`[${role}/${id}] received greeting from hub - marking ready`);
94
+ this.state = 'ready';
95
+ }
96
+ // shouldn't receive greet from client (or a sibling hub)
97
+ else {
98
+ console.warn(`[${role}/${id}] received unexpected greeting`, message);
99
+ }
100
+ break;
101
+ }
102
+ case 'disconnect': {
103
+ console.debug(`[${role}/${id}] tracking disconnect for:`, message.senderId);
104
+ // remove bridge on disconnect
105
+ this.removeBridge(message.senderId);
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ console.debug(`[${role}/${id}] message is not a lifecycle message`, message);
112
+ }
113
+ // forward locally
114
+ _messages$.next(message);
115
+ };
116
+ // listen to messages and forward all to the channel
117
+ _channel.addEventListener('message', onChannelMessage);
118
+ console.debug(`[${role}/${id}] broadcasting connect`);
119
+ // broadcast connect
120
+ this.send(BridgeLifecycleMessage_1.BridgeLifecycleMessage.create({ type: 'connect' }));
121
+ // broadcast greet (if we're the hub)
122
+ if (role === 'hub') {
123
+ console.debug(`[${role}/${id}] broadcasting greet`);
124
+ // greet
125
+ this.send(BridgeLifecycleMessage_1.BridgeLifecycleMessage.create({ type: 'greet' }));
126
+ // mark ready
127
+ this.state = 'ready';
128
+ }
129
+ // return disconnect handler
130
+ return () => {
131
+ console.debug(`[${role}/${id}] broadcasting disconnect`);
132
+ // stop listening
133
+ _channel.removeEventListener('message', onChannelMessage);
134
+ // broadcast disconnect
135
+ this.send(BridgeLifecycleMessage_1.BridgeLifecycleMessage.create({ type: 'disconnect' }));
136
+ };
137
+ }
138
+ send(args) {
139
+ // create full message
140
+ const { id, role, _messages$, _channel } = this;
141
+ const message = Object.assign({ senderId: id, sonderRole: role, timestamp: Date.now() }, args);
142
+ // post locally
143
+ _messages$.next(message);
144
+ _channel.postMessage(message);
145
+ }
146
+ }
147
+ exports.AppBridge = AppBridge;
148
+ // helpers
149
+ const CHANNEL_PREFIX = 'funnel:channel:';
150
+ const CHANNEL_ID_KEY = 'funnel:appBridge:channelId';
151
+ function createBridgeChannel() {
152
+ // fetch channel id (or create one)
153
+ let channelId = window.sessionStorage.getItem(CHANNEL_ID_KEY);
154
+ if (!channelId) {
155
+ channelId = funnel_core_1.Value.uuid();
156
+ window.sessionStorage.setItem(CHANNEL_ID_KEY, channelId);
157
+ }
158
+ // create broadcast channel for bridge
159
+ return new BroadcastChannel(`'${CHANNEL_PREFIX}${channelId}`);
160
+ }
@@ -1,9 +1,9 @@
1
1
  import { ReactNode } from 'react';
2
- export declare const CHANNEL_PREFIX = "funnel:channel:";
3
- export declare function AppBridgeContainer({ children }: AppBridgeContainer.Props): import("react/jsx-runtime").JSX.Element;
2
+ import { AppBridge } from './AppBridge';
3
+ export declare function AppBridgeContainer({ role, children }: AppBridgeContainer.Props): import("react/jsx-runtime").JSX.Element;
4
4
  export declare namespace AppBridgeContainer {
5
5
  type Props = {
6
+ role: AppBridge.Role;
6
7
  children: ReactNode;
7
8
  };
8
9
  }
9
- export declare function getUniqueId(): string;
@@ -1,55 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CHANNEL_PREFIX = void 0;
4
3
  exports.AppBridgeContainer = AppBridgeContainer;
5
- exports.getUniqueId = getUniqueId;
6
4
  const jsx_runtime_1 = require("react/jsx-runtime");
7
5
  const react_1 = require("react");
8
- const rxjs_1 = require("rxjs");
9
- const uuid_1 = require("uuid");
6
+ const AppBridge_1 = require("./AppBridge");
10
7
  const AppBridgeContext_1 = require("./AppBridgeContext");
11
- // constants
12
- exports.CHANNEL_PREFIX = 'funnel:channel:';
13
8
  // container
14
- function AppBridgeContainer({ children }) {
15
- // create context
16
- const context = (0, react_1.useMemo)(() => ({
17
- id: (0, uuid_1.v4)(),
18
- eventBus: new rxjs_1.Subject(),
19
- channel: new BroadcastChannel(exports.CHANNEL_PREFIX + getUniqueId())
20
- }), []);
21
- // forward all events to channel
9
+ function AppBridgeContainer({ role, children }) {
10
+ // bind bridge/context
11
+ const appBridge = (0, react_1.useMemo)(() => new AppBridge_1.AppBridge(role), [role]);
12
+ const context = (0, react_1.useMemo)(() => ({ appBridge }), [appBridge]);
13
+ // connect on mount (automatically disconnects b/c of result
22
14
  (0, react_1.useEffect)(() => {
23
- // create message handler
24
- const { id, eventBus, channel } = context;
25
- const onChannelMessage = (event) => {
26
- console.debug(`[${id}] received event`, event);
27
- // ignore message if sent from this container
28
- const message = event.data;
29
- if (message.source === id) {
30
- console.debug(`[${id}] ignoring echo`);
31
- return;
32
- }
33
- console.debug(`[${id}] forwarding message to local bus`);
34
- eventBus.next(message);
35
- };
36
- // listen to messages and forward all to the channel
37
- channel.addEventListener('message', onChannelMessage);
38
- // remove listener on unmount
39
- return () => channel.removeEventListener('message', onChannelMessage);
40
- }, [context]);
15
+ return appBridge.connect();
16
+ }, [appBridge]);
41
17
  // render
42
18
  return ((0, jsx_runtime_1.jsx)(AppBridgeContext_1.AppBridgeContext.Provider, { value: context, children: children }));
43
19
  }
44
- // helpers
45
- const UUID_KEY = 'funnel:uuid';
46
- function getUniqueId() {
47
- // fetch window id (or create one)
48
- let value = window.sessionStorage.getItem(UUID_KEY);
49
- if (!value) {
50
- value = (0, uuid_1.v4)();
51
- window.sessionStorage.setItem(UUID_KEY, value);
52
- }
53
- // return value
54
- return value;
55
- }
@@ -1,8 +1,7 @@
1
- import { Subject } from 'rxjs';
2
- import { AppContextId, TrackedAppMessage } from './AppMessage';
3
- export type IAppBridgeContext = {
4
- id: AppContextId;
5
- eventBus: Subject<TrackedAppMessage>;
6
- channel: BroadcastChannel;
7
- };
8
- export declare const AppBridgeContext: import("react").Context<IAppBridgeContext>;
1
+ import { AppBridge } from './AppBridge';
2
+ export declare const AppBridgeContext: import("react").Context<AppBridgeContext.Data>;
3
+ export declare namespace AppBridgeContext {
4
+ type Data = {
5
+ appBridge?: AppBridge;
6
+ };
7
+ }
@@ -2,10 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AppBridgeContext = void 0;
4
4
  const react_1 = require("react");
5
- const rxjs_1 = require("rxjs");
6
5
  // context
7
- exports.AppBridgeContext = (0, react_1.createContext)({
8
- id: '',
9
- eventBus: new rxjs_1.Subject(),
10
- channel: {}
11
- });
6
+ exports.AppBridgeContext = (0, react_1.createContext)({});
@@ -0,0 +1,7 @@
1
+ import type { IAppBridgeMessage } from './IAppBridgeMessage';
2
+ export interface BridgeLifecycleMessage extends IAppBridgeMessage<'appBridge', BridgeLifecycleMessage.Type> {
3
+ }
4
+ export declare namespace BridgeLifecycleMessage {
5
+ type Type = 'connect' | 'greet' | 'disconnect';
6
+ function create(args: IAppBridgeMessage.Create.Args<BridgeLifecycleMessage>): IAppBridgeMessage.Create.Result<BridgeLifecycleMessage>;
7
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BridgeLifecycleMessage = void 0;
4
+ // extensions
5
+ var BridgeLifecycleMessage;
6
+ (function (BridgeLifecycleMessage) {
7
+ // helpers
8
+ function create(args) {
9
+ return Object.assign(Object.assign({}, args), { domain: 'appBridge' });
10
+ }
11
+ BridgeLifecycleMessage.create = create;
12
+ })(BridgeLifecycleMessage || (exports.BridgeLifecycleMessage = BridgeLifecycleMessage = {}));
@@ -0,0 +1,16 @@
1
+ import type { AppBridge } from './AppBridge';
2
+ export interface IAppBridgeMessage<TDomain extends string = string, TType extends string = string> {
3
+ senderId: AppBridge.Id;
4
+ sonderRole: AppBridge.Role;
5
+ recipientId?: AppBridge.Id;
6
+ domain: TDomain;
7
+ type: TType;
8
+ timestamp: number;
9
+ }
10
+ export declare namespace IAppBridgeMessage {
11
+ namespace Create {
12
+ type Args<TMessage extends IAppBridgeMessage> = Omit<TMessage, 'senderId' | 'sonderRole' | 'domain' | 'timestamp'>;
13
+ type Result<TMessage extends IAppBridgeMessage> = Omit<TMessage, 'senderId' | 'senderRole' | 'timestamp'>;
14
+ }
15
+ function filter<TTMessage extends IAppBridgeMessage>(message: IAppBridgeMessage, domain: TTMessage['domain'], type?: TTMessage['type']): message is TTMessage;
16
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IAppBridgeMessage = void 0;
4
+ // extensions
5
+ var IAppBridgeMessage;
6
+ (function (IAppBridgeMessage) {
7
+ function filter(message, domain, type) {
8
+ return message.domain === domain
9
+ && (!type || message.type === type);
10
+ }
11
+ IAppBridgeMessage.filter = filter;
12
+ })(IAppBridgeMessage || (exports.IAppBridgeMessage = IAppBridgeMessage = {}));
@@ -1,4 +1,5 @@
1
+ export * from './AppBridge';
2
+ export * from './IAppBridgeMessage';
1
3
  export * from './AppBridgeContainer';
2
4
  export * from './AppBridgeContext';
3
- export * from './AppMessage';
4
5
  export * from './useAppBridge';
@@ -14,7 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./AppBridge"), exports);
18
+ __exportStar(require("./IAppBridgeMessage"), exports);
17
19
  __exportStar(require("./AppBridgeContainer"), exports);
18
20
  __exportStar(require("./AppBridgeContext"), exports);
19
- __exportStar(require("./AppMessage"), exports);
20
21
  __exportStar(require("./useAppBridge"), exports);