@supernova-studio/client 0.47.42 → 0.47.44

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.
@@ -0,0 +1,224 @@
1
+ import {
2
+ DocumentationPageSnapshot,
3
+ DocumentationPageV2,
4
+ ElementGroup,
5
+ ElementGroupSnapshot,
6
+ } from "@supernova-studio/model";
7
+ import * as Y from "yjs";
8
+ import { ZodSchema, ZodTypeDef } from "zod";
9
+ import { DocumentationHierarchySettings } from "../design-system-content";
10
+
11
+ type YJSSerializable = { id: string };
12
+
13
+ export class VersionRoomBaseYDoc {
14
+ private readonly yDoc: Y.Doc;
15
+
16
+ constructor(yDoc: Y.Doc) {
17
+ this.yDoc = yDoc;
18
+ }
19
+
20
+ //
21
+ // Pages
22
+ //
23
+
24
+ getPages(): DocumentationPageV2[] {
25
+ return this.getObjects(this.pagesYMap, DocumentationPageV2);
26
+ }
27
+
28
+ updatePages(pages: DocumentationPageV2[]) {
29
+ pages = pages.map(page => {
30
+ // We remove blocks from the payload here because it will not get parsed anyway
31
+ return {
32
+ ...page,
33
+ data: {
34
+ configuration: page.data.configuration,
35
+ },
36
+ };
37
+ });
38
+
39
+ this.setObjects(this.pagesYMap, pages);
40
+ }
41
+
42
+ deletePages(ids: string[]) {
43
+ this.deleteObjects(this.pagesYMap, ids);
44
+ }
45
+
46
+ private get pagesYMap() {
47
+ return this.yDoc.getMap<object>("documentationPages");
48
+ }
49
+
50
+ //
51
+ // Groups
52
+ //
53
+
54
+ getGroups(): ElementGroup[] {
55
+ return this.getObjects(this.groupsYMap, ElementGroup);
56
+ }
57
+
58
+ updateGroups(groups: ElementGroup[]) {
59
+ this.setObjects(this.groupsYMap, groups);
60
+ }
61
+
62
+ deleteGroups(ids: string[]) {
63
+ this.deleteObjects(this.groupsYMap, ids);
64
+ }
65
+
66
+ private get groupsYMap() {
67
+ return this.yDoc.getMap<object>("documentationGroups");
68
+ }
69
+
70
+ //
71
+ // Documentation internal settings
72
+ //
73
+
74
+ getDocumentationInternalSettings(): DocumentationHierarchySettings {
75
+ const map = this.internalSettingsYMap;
76
+
77
+ const rawSettings: Record<keyof DocumentationHierarchySettings, any> = {
78
+ routingVersion: map.get("routingVersion"),
79
+ isDraftFeatureAdopted: map.get("isDraftFeatureAdapted") ?? false,
80
+ };
81
+
82
+ const settingsParseResult = DocumentationHierarchySettings.safeParse(rawSettings);
83
+ if (!settingsParseResult.success) {
84
+ return {
85
+ routingVersion: "2",
86
+ isDraftFeatureAdopted: false,
87
+ };
88
+ }
89
+
90
+ return settingsParseResult.data;
91
+ }
92
+
93
+ updateDocumentationInternalSettings(settings: DocumentationHierarchySettings) {
94
+ const map = this.internalSettingsYMap;
95
+
96
+ map.set("routingVersion", settings.routingVersion);
97
+ }
98
+
99
+ private get internalSettingsYMap() {
100
+ return this.yDoc.getMap<unknown>("documentationInternalSettings");
101
+ }
102
+
103
+ //
104
+ // Documentation page published snapshot
105
+ //
106
+
107
+ getPagePublishedSnapshots(): DocumentationPageSnapshot[] {
108
+ return this.getObjects(this.documentationPagePublishedStatesYMap, DocumentationPageSnapshot);
109
+ }
110
+
111
+ updatePagePublishedSnapshots(snapshots: DocumentationPageSnapshot[]) {
112
+ this.setObjects(this.documentationPagePublishedStatesYMap, snapshots);
113
+ }
114
+
115
+ deletePagePublishedSnapshots(ids: string[]) {
116
+ this.deleteObjects(this.documentationPagePublishedStatesYMap, ids);
117
+ }
118
+
119
+ private get documentationPagePublishedStatesYMap() {
120
+ return this.yDoc.getMap<object>("documentationPagePublishedSnapshots");
121
+ }
122
+
123
+ //
124
+ // Documentation page deleted snapshot
125
+ //
126
+
127
+ getPageDeletedSnapshots(): DocumentationPageSnapshot[] {
128
+ return this.getObjects(this.documentationPageDeletedStatesYMap, DocumentationPageSnapshot);
129
+ }
130
+
131
+ updatePageDeletedSnapshots(snapshots: DocumentationPageSnapshot[]) {
132
+ this.setObjects(this.documentationPageDeletedStatesYMap, snapshots);
133
+ }
134
+
135
+ deletePageDeletedSnapshots(ids: string[]) {
136
+ this.deleteObjects(this.documentationPageDeletedStatesYMap, ids);
137
+ }
138
+
139
+ private get documentationPageDeletedStatesYMap() {
140
+ return this.yDoc.getMap<object>("documentationPageDeletedSnapshots");
141
+ }
142
+
143
+ //
144
+ // Documentation group published snapshots
145
+ //
146
+
147
+ getGroupPublishedSnapshots(): ElementGroupSnapshot[] {
148
+ return this.getObjects(this.documentationGroupPublishedStatesYMap, ElementGroupSnapshot);
149
+ }
150
+
151
+ updateGroupPublishedSnapshots(snapshots: ElementGroupSnapshot[]) {
152
+ this.setObjects(this.documentationGroupPublishedStatesYMap, snapshots);
153
+ }
154
+
155
+ deleteGroupPublishedSnapshots(ids: string[]) {
156
+ this.deleteObjects(this.documentationGroupPublishedStatesYMap, ids);
157
+ }
158
+
159
+ private get documentationGroupPublishedStatesYMap() {
160
+ return this.yDoc.getMap<object>("documentationGroupPublishedSnapshots");
161
+ }
162
+
163
+ //
164
+ // Documentation group deleted snapshots
165
+ //
166
+
167
+ getGroupDeletedSnapshots(): ElementGroupSnapshot[] {
168
+ return this.getObjects(this.documentationGroupDeletedStatesYMap, ElementGroupSnapshot);
169
+ }
170
+
171
+ updateGroupDeletedSnapshots(snapshots: ElementGroupSnapshot[]) {
172
+ this.setObjects(this.documentationGroupDeletedStatesYMap, snapshots);
173
+ }
174
+
175
+ deleteGroupDeletedSnapshots(ids: string[]) {
176
+ this.deleteObjects(this.documentationGroupDeletedStatesYMap, ids);
177
+ }
178
+
179
+ private get documentationGroupDeletedStatesYMap() {
180
+ return this.yDoc.getMap<object>("documentationGroupDeletedSnapshots");
181
+ }
182
+
183
+ //
184
+ // Utils
185
+ //
186
+
187
+ private getObjects<T extends YJSSerializable, I>(map: Y.Map<object>, schema: ZodSchema<T, ZodTypeDef, I>): T[] {
188
+ const results: T[] = [];
189
+ map.forEach(object => results.push(schema.parse(object)));
190
+ return results;
191
+ }
192
+
193
+ private setObjects<T extends YJSSerializable>(map: Y.Map<object>, objects: T[]) {
194
+ objects.forEach(o => map.set(o.id, JSON.parse(JSON.stringify(o))));
195
+ }
196
+
197
+ private deleteObjects(map: Y.Map<object>, ids: string[]) {
198
+ ids.forEach(id => map.delete(id));
199
+ }
200
+
201
+ //
202
+ // Documentation page content hashes
203
+ //
204
+
205
+ getDocumentationPageContentHashes(): Record<string, string> {
206
+ const map = this.documentationPageContentHashesYMap;
207
+
208
+ const result: Record<string, string> = {};
209
+ map.forEach((hash, key) => {
210
+ result[key] = hash;
211
+ });
212
+
213
+ return result;
214
+ }
215
+
216
+ updateDocumentationPageContentHashes(hashes: Record<string, string>) {
217
+ const map = this.documentationPageContentHashesYMap;
218
+ Object.entries(hashes).forEach(([key, hash]) => map.set(key, hash));
219
+ }
220
+
221
+ private get documentationPageContentHashesYMap() {
222
+ return this.yDoc.getMap<string>("documentationPageHashes");
223
+ }
224
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ DocumentationItemConfigurationV2,
3
+ DocumentationPageV2,
4
+ ElementGroup,
5
+ mapByUnique,
6
+ } from "@supernova-studio/model";
7
+ import deepEqual from "deep-equal";
8
+ import * as Y from "yjs";
9
+ import {
10
+ DTODocumentationDraftState,
11
+ DTODocumentationDraftStateUpdated,
12
+ DTODocumentationHierarchyV2,
13
+ documentationItemConfigurationToDTOV2,
14
+ documentationPagesToDTOV2,
15
+ elementGroupsToDocumentationGroupDTOV2,
16
+ } from "../../api";
17
+ import { generateHash } from "../../utils";
18
+ import { DocumentationPageEditorModel } from "../docs-editor";
19
+ import { VersionRoomBaseYDoc } from "./base";
20
+
21
+ type ItemState = {
22
+ title: string;
23
+ configuration: DocumentationItemConfigurationV2 | undefined;
24
+ contentHash: string;
25
+ };
26
+
27
+ export class FrontendVersionRoomYDoc {
28
+ private readonly yDoc: Y.Doc;
29
+
30
+ constructor(yDoc: Y.Doc) {
31
+ this.yDoc = yDoc;
32
+ }
33
+
34
+ //
35
+ // Hierarchy
36
+ //
37
+
38
+ getDocumentationHierarchy(): DTODocumentationHierarchyV2 {
39
+ const doc = new VersionRoomBaseYDoc(this.yDoc);
40
+
41
+ // Read current room data
42
+ const pages = doc.getPages();
43
+ const groups = doc.getGroups();
44
+
45
+ const settings = doc.getDocumentationInternalSettings();
46
+
47
+ // Convert pages to DTOs with draft states
48
+ const pageDTOs = documentationPagesToDTOV2(pages, groups, settings.routingVersion);
49
+ const pageDraftStates = this.buildPageDraftStates(pages);
50
+ pageDTOs.forEach(p => {
51
+ const draftState = pageDraftStates.get(p.id);
52
+ draftState && (p.draftState = draftState);
53
+ });
54
+
55
+ // Convert groups to DTOs with draft states
56
+ const groupDTOs = elementGroupsToDocumentationGroupDTOV2(groups, pages);
57
+ const groupDraftStates = this.buildGroupDraftStates(groups);
58
+ groupDTOs.forEach(g => {
59
+ const draftState = groupDraftStates.get(g.id);
60
+ draftState && (g.draftState = draftState);
61
+ });
62
+
63
+ // Read deleted room data
64
+ const deletedGroups = doc.getGroupDeletedSnapshots().map(s => s.group);
65
+ const deletedPages = doc.getPageDeletedSnapshots().map(s => s.page);
66
+
67
+ // Convert deleted pages to DTOs with draft states
68
+ const deletedPageDTOs = documentationPagesToDTOV2(
69
+ deletedPages,
70
+ [...groups, ...deletedGroups],
71
+ settings.routingVersion
72
+ ).map(p => {
73
+ return { ...p, draftState: { changeType: "Deleted" } } as const;
74
+ });
75
+
76
+ // Convert deleted groups to DTOs with draft states
77
+ const deletedGroupDTOs = elementGroupsToDocumentationGroupDTOV2(deletedGroups, deletedPages).map(g => {
78
+ return { ...g, draftState: { changeType: "Deleted" } } as const;
79
+ });
80
+
81
+ return {
82
+ pages: pageDTOs,
83
+ groups: groupDTOs,
84
+
85
+ deletedPages: deletedPageDTOs,
86
+ deletedGroups: deletedGroupDTOs,
87
+ };
88
+ }
89
+
90
+ //
91
+ // Drafts - Pages
92
+ //
93
+
94
+ private buildPageDraftStates(pages: DocumentationPageV2[]): Map<string, DTODocumentationDraftState> {
95
+ const doc = new VersionRoomBaseYDoc(this.yDoc);
96
+
97
+ // Read room data
98
+ const pageHashes = doc.getDocumentationPageContentHashes();
99
+ const publishedSnapshots = doc.getPagePublishedSnapshots();
100
+
101
+ const publishedSnapshotsByPageId = mapByUnique(publishedSnapshots, s => s.page.id);
102
+ const publishedPagesById = mapByUnique(
103
+ publishedSnapshots.map(s => s.page),
104
+ p => p.id
105
+ );
106
+
107
+ const result = new Map<string, DTODocumentationDraftState>();
108
+
109
+ pages.forEach(page => {
110
+ // Current state
111
+ const currentPageContentHash = pageHashes[page.persistentId] ?? "";
112
+ const currentState: ItemState = this.itemStateFromPage(page, currentPageContentHash);
113
+
114
+ // Published state
115
+ const snapshot = publishedSnapshotsByPageId.get(page.id);
116
+ let publishedState: ItemState | undefined;
117
+ if (snapshot) {
118
+ const publishedPage = publishedPagesById.get(snapshot.page.id)!;
119
+ publishedState = this.itemStateFromPage(publishedPage, snapshot.pageContentHash);
120
+ }
121
+
122
+ // Calculate draft
123
+ const draftState = this.createDraftState(currentState, publishedState);
124
+ if (draftState) result.set(page.id, draftState);
125
+ });
126
+
127
+ return result;
128
+ }
129
+
130
+ private itemStateFromPage(page: DocumentationPageV2, pageContentHash: string): ItemState {
131
+ return {
132
+ title: page.meta.name,
133
+ configuration: page.data.configuration,
134
+ contentHash: pageContentHash,
135
+ };
136
+ }
137
+
138
+ //
139
+ // Drafts - Groups
140
+ //
141
+
142
+ private buildGroupDraftStates(groups: ElementGroup[]): Map<string, DTODocumentationDraftState> {
143
+ const doc = new VersionRoomBaseYDoc(this.yDoc);
144
+
145
+ // Read room data
146
+ const publishedSnapshots = doc.getGroupPublishedSnapshots();
147
+
148
+ const publishedSnapshotsByGroupId = mapByUnique(publishedSnapshots, s => s.group.id);
149
+ const publishedGroupsById = mapByUnique(
150
+ publishedSnapshots.map(s => s.group),
151
+ g => g.id
152
+ );
153
+
154
+ const result = new Map<string, DTODocumentationDraftState>();
155
+
156
+ groups.forEach(group => {
157
+ // Current state
158
+ const currentState: ItemState = this.itemStateFromGroup(group);
159
+
160
+ // Published state
161
+ const snapshot = publishedSnapshotsByGroupId.get(group.id);
162
+ let publishedState: ItemState | undefined;
163
+ if (snapshot) {
164
+ const publishedGroup = publishedGroupsById.get(snapshot.group.id)!;
165
+ publishedState = this.itemStateFromGroup(publishedGroup);
166
+ }
167
+
168
+ // Calculate draft
169
+ const draftState = this.createDraftState(currentState, publishedState);
170
+ if (draftState) result.set(group.id, draftState);
171
+ });
172
+
173
+ return result;
174
+ }
175
+
176
+ private itemStateFromGroup(group: ElementGroup): ItemState {
177
+ return {
178
+ title: group.meta.name,
179
+ configuration: group.data?.configuration,
180
+ contentHash: "-",
181
+ };
182
+ }
183
+
184
+ //
185
+ // Drafts - Shared
186
+ //
187
+
188
+ private createDraftState(
189
+ currentState: ItemState,
190
+ publishedState: ItemState | undefined
191
+ ): DTODocumentationDraftState | undefined {
192
+ if (!publishedState) {
193
+ // New item (hasn't been published yet)
194
+ return { changeType: "Created" };
195
+ }
196
+
197
+ // Compare current item state to the published state
198
+ const updatedDraftState: DTODocumentationDraftStateUpdated = {
199
+ changeType: "Updated",
200
+ changes: {},
201
+ };
202
+
203
+ if (currentState.title !== publishedState.title) {
204
+ updatedDraftState.changes.previousTitle = publishedState.title;
205
+ }
206
+
207
+ if (!deepEqual(currentState.configuration, publishedState.configuration)) {
208
+ const configurationDto = documentationItemConfigurationToDTOV2(publishedState.configuration);
209
+ updatedDraftState.changes.previousConfiguration = configurationDto;
210
+ }
211
+
212
+ if (currentState.contentHash !== publishedState.contentHash) {
213
+ updatedDraftState.changes.previousContentHash = publishedState.contentHash;
214
+ }
215
+
216
+ if (Object.keys(updatedDraftState).length) {
217
+ // Item has at least one of the draft changes
218
+ return updatedDraftState;
219
+ }
220
+
221
+ // Item has no draft changes compared to the published item
222
+ return undefined;
223
+ }
224
+
225
+ //
226
+ // Update page content hash
227
+ //
228
+
229
+ notifyDocumentationPageContentUpdated(pageId: string, content: DocumentationPageEditorModel) {
230
+ const pageContentHash = generateHash(content);
231
+
232
+ new VersionRoomBaseYDoc(this.yDoc).updateDocumentationPageContentHashes({
233
+ [pageId]: pageContentHash,
234
+ });
235
+ }
236
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./backend";
2
+ export * from "./base";
3
+ export * from "./frontend";
@@ -1,23 +0,0 @@
1
- import { DesignElementSnapshot, DocumentationPageV2, ElementGroup } from "@supernova-studio/model";
2
- import { DTODocumentationHierarchyV2 } from "../../dto";
3
- import { elementGroupsToDocumentationGroupStructureDTOV2 } from "./documentation-group-v2-to-dto";
4
- import { documentationPagesToStructureDTOV2 } from "./documentation-page-v2-to-dto";
5
-
6
- // The fact that DTO conversion is located here instead of main backend code is due to the fact
7
- // that we store page and group data in YJS documents in the same way as they are stored in the database.
8
- // Therefore, we need to expose this conversion to the client so that it can consume data from YJS documents.
9
- //
10
- // Please do not put more DTO conversion here unless you know what you're doing.
11
-
12
- export function documentationElementsToHierarchyDto(
13
- docPages: DocumentationPageV2[],
14
- docGroups: ElementGroup[],
15
- publishedPagesSnapshots: DesignElementSnapshot[],
16
- routingVersion: string
17
- ): DTODocumentationHierarchyV2 {
18
- return {
19
- pages: documentationPagesToStructureDTOV2(docPages, docGroups, routingVersion),
20
- groups: elementGroupsToDocumentationGroupStructureDTOV2(docGroups, docPages),
21
- publishedPagesSnapshots,
22
- };
23
- }