@cmssy/cli 0.22.4 → 9.1.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 (248) hide show
  1. package/dist/index.js +435 -0
  2. package/package.json +25 -86
  3. package/LICENSE +0 -21
  4. package/README.md +0 -692
  5. package/config.d.ts +0 -3
  6. package/config.js +0 -4
  7. package/dist/cli.d.ts +0 -3
  8. package/dist/cli.d.ts.map +0 -1
  9. package/dist/cli.js +0 -340
  10. package/dist/cli.js.map +0 -1
  11. package/dist/commands/add-source.d.ts +0 -13
  12. package/dist/commands/add-source.d.ts.map +0 -1
  13. package/dist/commands/add-source.js +0 -273
  14. package/dist/commands/add-source.js.map +0 -1
  15. package/dist/commands/build.d.ts +0 -7
  16. package/dist/commands/build.d.ts.map +0 -1
  17. package/dist/commands/build.js +0 -127
  18. package/dist/commands/build.js.map +0 -1
  19. package/dist/commands/codegen.d.ts +0 -7
  20. package/dist/commands/codegen.d.ts.map +0 -1
  21. package/dist/commands/codegen.js +0 -150
  22. package/dist/commands/codegen.js.map +0 -1
  23. package/dist/commands/configure.d.ts +0 -6
  24. package/dist/commands/configure.d.ts.map +0 -1
  25. package/dist/commands/configure.js +0 -39
  26. package/dist/commands/configure.js.map +0 -1
  27. package/dist/commands/create.d.ts +0 -18
  28. package/dist/commands/create.d.ts.map +0 -1
  29. package/dist/commands/create.js +0 -403
  30. package/dist/commands/create.js.map +0 -1
  31. package/dist/commands/dev.d.ts +0 -6
  32. package/dist/commands/dev.d.ts.map +0 -1
  33. package/dist/commands/dev.js +0 -144
  34. package/dist/commands/dev.js.map +0 -1
  35. package/dist/commands/doctor.d.ts +0 -2
  36. package/dist/commands/doctor.d.ts.map +0 -1
  37. package/dist/commands/doctor.js +0 -230
  38. package/dist/commands/doctor.js.map +0 -1
  39. package/dist/commands/init.d.ts +0 -6
  40. package/dist/commands/init.d.ts.map +0 -1
  41. package/dist/commands/init.js +0 -414
  42. package/dist/commands/init.js.map +0 -1
  43. package/dist/commands/lib.d.ts +0 -17
  44. package/dist/commands/lib.d.ts.map +0 -1
  45. package/dist/commands/lib.js +0 -132
  46. package/dist/commands/lib.js.map +0 -1
  47. package/dist/commands/link.d.ts +0 -8
  48. package/dist/commands/link.d.ts.map +0 -1
  49. package/dist/commands/link.js +0 -116
  50. package/dist/commands/link.js.map +0 -1
  51. package/dist/commands/migrate.d.ts +0 -2
  52. package/dist/commands/migrate.d.ts.map +0 -1
  53. package/dist/commands/migrate.js +0 -231
  54. package/dist/commands/migrate.js.map +0 -1
  55. package/dist/commands/package.d.ts +0 -14
  56. package/dist/commands/package.d.ts.map +0 -1
  57. package/dist/commands/package.js +0 -137
  58. package/dist/commands/package.js.map +0 -1
  59. package/dist/commands/publish-block-buildtime.d.ts +0 -8
  60. package/dist/commands/publish-block-buildtime.d.ts.map +0 -1
  61. package/dist/commands/publish-block-buildtime.js +0 -290
  62. package/dist/commands/publish-block-buildtime.js.map +0 -1
  63. package/dist/commands/publish-template.d.ts +0 -22
  64. package/dist/commands/publish-template.d.ts.map +0 -1
  65. package/dist/commands/publish-template.js +0 -345
  66. package/dist/commands/publish-template.js.map +0 -1
  67. package/dist/commands/publish.d.ts +0 -15
  68. package/dist/commands/publish.d.ts.map +0 -1
  69. package/dist/commands/publish.js +0 -1003
  70. package/dist/commands/publish.js.map +0 -1
  71. package/dist/commands/skills.d.ts +0 -11
  72. package/dist/commands/skills.d.ts.map +0 -1
  73. package/dist/commands/skills.js +0 -178
  74. package/dist/commands/skills.js.map +0 -1
  75. package/dist/commands/sync.d.ts +0 -6
  76. package/dist/commands/sync.d.ts.map +0 -1
  77. package/dist/commands/sync.js +0 -208
  78. package/dist/commands/sync.js.map +0 -1
  79. package/dist/commands/test.d.ts +0 -8
  80. package/dist/commands/test.d.ts.map +0 -1
  81. package/dist/commands/test.js +0 -60
  82. package/dist/commands/test.js.map +0 -1
  83. package/dist/commands/upload.d.ts +0 -11
  84. package/dist/commands/upload.d.ts.map +0 -1
  85. package/dist/commands/upload.js +0 -128
  86. package/dist/commands/upload.js.map +0 -1
  87. package/dist/commands/workspaces.d.ts +0 -2
  88. package/dist/commands/workspaces.d.ts.map +0 -1
  89. package/dist/commands/workspaces.js +0 -62
  90. package/dist/commands/workspaces.js.map +0 -1
  91. package/dist/dev-ui/app.js +0 -1284
  92. package/dist/dev-ui/index.html +0 -1511
  93. package/dist/dev-ui-react/App.tsx +0 -164
  94. package/dist/dev-ui-react/__tests__/previewData.test.ts +0 -193
  95. package/dist/dev-ui-react/components/BlocksList.tsx +0 -232
  96. package/dist/dev-ui-react/components/Editor.tsx +0 -469
  97. package/dist/dev-ui-react/components/Preview.tsx +0 -146
  98. package/dist/dev-ui-react/hooks/useBlocks.ts +0 -80
  99. package/dist/dev-ui-react/index.html +0 -13
  100. package/dist/dev-ui-react/main.tsx +0 -8
  101. package/dist/dev-ui-react/styles.css +0 -856
  102. package/dist/dev-ui-react/types.ts +0 -45
  103. package/dist/skills/claude/cmssy-block.md +0 -454
  104. package/dist/skills/claude/cmssy-mcp-content.md +0 -235
  105. package/dist/test-helpers/index.d.ts +0 -23
  106. package/dist/test-helpers/index.d.ts.map +0 -1
  107. package/dist/test-helpers/index.js +0 -50
  108. package/dist/test-helpers/index.js.map +0 -1
  109. package/dist/types/block-config.d.ts +0 -6
  110. package/dist/types/block-config.d.ts.map +0 -1
  111. package/dist/types/block-config.js +0 -12
  112. package/dist/types/block-config.js.map +0 -1
  113. package/dist/types/publish-job.d.ts +0 -61
  114. package/dist/types/publish-job.d.ts.map +0 -1
  115. package/dist/types/publish-job.js +0 -13
  116. package/dist/types/publish-job.js.map +0 -1
  117. package/dist/utils/api-error.d.ts +0 -10
  118. package/dist/utils/api-error.d.ts.map +0 -1
  119. package/dist/utils/api-error.js +0 -55
  120. package/dist/utils/api-error.js.map +0 -1
  121. package/dist/utils/block-config.d.ts +0 -87
  122. package/dist/utils/block-config.d.ts.map +0 -1
  123. package/dist/utils/block-config.js +0 -214
  124. package/dist/utils/block-config.js.map +0 -1
  125. package/dist/utils/blocks-meta-cache.d.ts +0 -28
  126. package/dist/utils/blocks-meta-cache.d.ts.map +0 -1
  127. package/dist/utils/blocks-meta-cache.js +0 -72
  128. package/dist/utils/blocks-meta-cache.js.map +0 -1
  129. package/dist/utils/builder.d.ts +0 -34
  130. package/dist/utils/builder.d.ts.map +0 -1
  131. package/dist/utils/builder.js +0 -140
  132. package/dist/utils/builder.js.map +0 -1
  133. package/dist/utils/cmssy-config.d.ts +0 -16
  134. package/dist/utils/cmssy-config.d.ts.map +0 -1
  135. package/dist/utils/cmssy-config.js +0 -19
  136. package/dist/utils/cmssy-config.js.map +0 -1
  137. package/dist/utils/config.d.ts +0 -9
  138. package/dist/utils/config.d.ts.map +0 -1
  139. package/dist/utils/config.js +0 -51
  140. package/dist/utils/config.js.map +0 -1
  141. package/dist/utils/dependency-check.d.ts +0 -21
  142. package/dist/utils/dependency-check.d.ts.map +0 -1
  143. package/dist/utils/dependency-check.js +0 -56
  144. package/dist/utils/dependency-check.js.map +0 -1
  145. package/dist/utils/dev-generator/api-routes/blocks.d.ts +0 -3
  146. package/dist/utils/dev-generator/api-routes/blocks.d.ts.map +0 -1
  147. package/dist/utils/dev-generator/api-routes/blocks.js +0 -194
  148. package/dist/utils/dev-generator/api-routes/blocks.js.map +0 -1
  149. package/dist/utils/dev-generator/api-routes/config.d.ts +0 -2
  150. package/dist/utils/dev-generator/api-routes/config.d.ts.map +0 -1
  151. package/dist/utils/dev-generator/api-routes/config.js +0 -107
  152. package/dist/utils/dev-generator/api-routes/config.js.map +0 -1
  153. package/dist/utils/dev-generator/api-routes/preview.d.ts +0 -2
  154. package/dist/utils/dev-generator/api-routes/preview.d.ts.map +0 -1
  155. package/dist/utils/dev-generator/api-routes/preview.js +0 -50
  156. package/dist/utils/dev-generator/api-routes/preview.js.map +0 -1
  157. package/dist/utils/dev-generator/api-routes/workspaces.d.ts +0 -2
  158. package/dist/utils/dev-generator/api-routes/workspaces.d.ts.map +0 -1
  159. package/dist/utils/dev-generator/api-routes/workspaces.js +0 -128
  160. package/dist/utils/dev-generator/api-routes/workspaces.js.map +0 -1
  161. package/dist/utils/dev-generator/helpers.d.ts +0 -23
  162. package/dist/utils/dev-generator/helpers.d.ts.map +0 -1
  163. package/dist/utils/dev-generator/helpers.js +0 -128
  164. package/dist/utils/dev-generator/helpers.js.map +0 -1
  165. package/dist/utils/dev-generator/home-page.d.ts +0 -2
  166. package/dist/utils/dev-generator/home-page.d.ts.map +0 -1
  167. package/dist/utils/dev-generator/home-page.js +0 -643
  168. package/dist/utils/dev-generator/home-page.js.map +0 -1
  169. package/dist/utils/dev-generator/index.d.ts +0 -11
  170. package/dist/utils/dev-generator/index.d.ts.map +0 -1
  171. package/dist/utils/dev-generator/index.js +0 -85
  172. package/dist/utils/dev-generator/index.js.map +0 -1
  173. package/dist/utils/dev-generator/layout.d.ts +0 -3
  174. package/dist/utils/dev-generator/layout.d.ts.map +0 -1
  175. package/dist/utils/dev-generator/layout.js +0 -63
  176. package/dist/utils/dev-generator/layout.js.map +0 -1
  177. package/dist/utils/dev-generator/next-config.d.ts +0 -3
  178. package/dist/utils/dev-generator/next-config.d.ts.map +0 -1
  179. package/dist/utils/dev-generator/next-config.js +0 -111
  180. package/dist/utils/dev-generator/next-config.js.map +0 -1
  181. package/dist/utils/dev-generator/preview-pages.d.ts +0 -3
  182. package/dist/utils/dev-generator/preview-pages.d.ts.map +0 -1
  183. package/dist/utils/dev-generator/preview-pages.js +0 -833
  184. package/dist/utils/dev-generator/preview-pages.js.map +0 -1
  185. package/dist/utils/dev-generator.d.ts +0 -12
  186. package/dist/utils/dev-generator.d.ts.map +0 -1
  187. package/dist/utils/dev-generator.js +0 -1752
  188. package/dist/utils/dev-generator.js.map +0 -1
  189. package/dist/utils/field-schema.d.ts +0 -13
  190. package/dist/utils/field-schema.d.ts.map +0 -1
  191. package/dist/utils/field-schema.js +0 -182
  192. package/dist/utils/field-schema.js.map +0 -1
  193. package/dist/utils/graphql.d.ts +0 -39
  194. package/dist/utils/graphql.d.ts.map +0 -1
  195. package/dist/utils/graphql.js +0 -138
  196. package/dist/utils/graphql.js.map +0 -1
  197. package/dist/utils/lib-manifest.d.ts +0 -7
  198. package/dist/utils/lib-manifest.d.ts.map +0 -1
  199. package/dist/utils/lib-manifest.js +0 -59
  200. package/dist/utils/lib-manifest.js.map +0 -1
  201. package/dist/utils/publish-helpers.d.ts +0 -43
  202. package/dist/utils/publish-helpers.d.ts.map +0 -1
  203. package/dist/utils/publish-helpers.js +0 -215
  204. package/dist/utils/publish-helpers.js.map +0 -1
  205. package/dist/utils/publish-helpers.test.d.ts +0 -2
  206. package/dist/utils/publish-helpers.test.d.ts.map +0 -1
  207. package/dist/utils/publish-helpers.test.js +0 -24
  208. package/dist/utils/publish-helpers.test.js.map +0 -1
  209. package/dist/utils/resolve-workspace.d.ts +0 -6
  210. package/dist/utils/resolve-workspace.d.ts.map +0 -1
  211. package/dist/utils/resolve-workspace.js +0 -32
  212. package/dist/utils/resolve-workspace.js.map +0 -1
  213. package/dist/utils/scanner.d.ts +0 -42
  214. package/dist/utils/scanner.d.ts.map +0 -1
  215. package/dist/utils/scanner.js +0 -156
  216. package/dist/utils/scanner.js.map +0 -1
  217. package/dist/utils/schema-diff.d.ts +0 -22
  218. package/dist/utils/schema-diff.d.ts.map +0 -1
  219. package/dist/utils/schema-diff.js +0 -107
  220. package/dist/utils/schema-diff.js.map +0 -1
  221. package/dist/utils/source-collector.d.ts +0 -19
  222. package/dist/utils/source-collector.d.ts.map +0 -1
  223. package/dist/utils/source-collector.js +0 -565
  224. package/dist/utils/source-collector.js.map +0 -1
  225. package/dist/utils/theme-builder.d.ts +0 -12
  226. package/dist/utils/theme-builder.d.ts.map +0 -1
  227. package/dist/utils/theme-builder.js +0 -117
  228. package/dist/utils/theme-builder.js.map +0 -1
  229. package/dist/utils/type-generator.d.ts +0 -9
  230. package/dist/utils/type-generator.d.ts.map +0 -1
  231. package/dist/utils/type-generator.js +0 -86
  232. package/dist/utils/type-generator.js.map +0 -1
  233. package/dist/utils/version.d.ts +0 -10
  234. package/dist/utils/version.d.ts.map +0 -1
  235. package/dist/utils/version.js +0 -30
  236. package/dist/utils/version.js.map +0 -1
  237. package/dist/utils/versions.d.ts.map +0 -1
  238. package/dist/utils/versions.js.map +0 -1
  239. package/src/dev-app/app/api/blocks/[name]/config/route.ts +0 -140
  240. package/src/dev-app/app/api/blocks/route.ts +0 -61
  241. package/src/dev-app/app/api/config/route.ts +0 -98
  242. package/src/dev-app/app/api/context/route.ts +0 -126
  243. package/src/dev-app/app/api/preview/[name]/route.ts +0 -137
  244. package/src/dev-app/app/api/workspaces/route.ts +0 -124
  245. package/src/dev-app/app/layout.tsx +0 -18
  246. package/src/dev-app/app/page.tsx +0 -1785
  247. package/test.d.ts +0 -1
  248. package/test.js +0 -2
@@ -1,164 +0,0 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
- import { BlocksList } from './components/BlocksList';
3
- import { Preview } from './components/Preview';
4
- import { Editor } from './components/Editor';
5
- import { useBlocks, useBlockConfig } from './hooks/useBlocks';
6
- import { Block } from './types';
7
- import './styles.css';
8
-
9
- export function App() {
10
- const { blocks, loading: blocksLoading } = useBlocks();
11
- const [selectedBlock, setSelectedBlock] = useState<Block | null>(null);
12
- const [previewData, setPreviewData] = useState<Record<string, unknown>>({});
13
- const [currentPage, setCurrentPage] = useState<string | undefined>();
14
- const [isDirty, setIsDirty] = useState(false);
15
- // Keep reference to complete config data for merging during save
16
- const configDataRef = useRef<Record<string, unknown>>({});
17
-
18
- const { config, loading: configLoading } = useBlockConfig(
19
- selectedBlock?.name || null,
20
- selectedBlock?.type || null
21
- );
22
-
23
- // Update selected block with loaded config
24
- useEffect(() => {
25
- if (selectedBlock && config) {
26
- const updatedBlock: Block = {
27
- ...selectedBlock,
28
- schema: config.schema as Block['schema'],
29
- pages: config.pages,
30
- layoutPositions: config.layoutPositions,
31
- };
32
- setSelectedBlock(updatedBlock);
33
-
34
- // Store complete config data in ref for merging during save
35
- const configData = config.previewData || {};
36
- configDataRef.current = configData;
37
-
38
- // Set previewData from config (complete data)
39
- setPreviewData(configData);
40
-
41
- // Set first page for templates
42
- if (config.pages && config.pages.length > 0) {
43
- setCurrentPage(config.pages[0].slug);
44
- }
45
- }
46
- }, [config, selectedBlock?.name]);
47
-
48
- // Handle block selection
49
- const handleSelectBlock = useCallback((block: Block) => {
50
- setSelectedBlock(block);
51
- setPreviewData({});
52
- setCurrentPage(undefined);
53
- setIsDirty(false);
54
- configDataRef.current = {};
55
-
56
- // Update URL based on type
57
- const url = new URL(window.location.href);
58
- url.searchParams.delete('block');
59
- url.searchParams.delete('template');
60
- if (block.type === 'template') {
61
- url.searchParams.set('template', block.name);
62
- } else {
63
- url.searchParams.set('block', block.name);
64
- }
65
- window.history.replaceState({}, '', url.toString());
66
- }, []);
67
-
68
- // Handle preview data change - just update state, effect handles saving
69
- const handlePreviewDataChange = useCallback(
70
- (newData: Record<string, unknown>) => {
71
- setPreviewData(newData);
72
- setIsDirty(true);
73
- },
74
- []
75
- );
76
-
77
- // Debounced save effect - saves when data changes and is dirty
78
- useEffect(() => {
79
- // Don't save if loading, not dirty, no block selected, or config not loaded yet
80
- // configDataRef must have data to prevent losing fields
81
- if (configLoading || !isDirty || !selectedBlock || Object.keys(configDataRef.current).length === 0) {
82
- return;
83
- }
84
-
85
- const controller = new AbortController();
86
- const timeoutId = setTimeout(async () => {
87
- try {
88
- // Merge user edits with complete config data to never lose fields
89
- const dataToSave = { ...configDataRef.current, ...previewData };
90
-
91
- await fetch(`/api/preview/${selectedBlock.name}`, {
92
- method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
94
- body: JSON.stringify(dataToSave),
95
- signal: controller.signal,
96
- });
97
- setIsDirty(false);
98
- } catch (error) {
99
- if (error instanceof Error && error.name !== 'AbortError') {
100
- console.error('Failed to save preview data:', error);
101
- }
102
- }
103
- }, 500);
104
-
105
- // Cleanup: cancel timeout and abort fetch on unmount or dependency change
106
- return () => {
107
- clearTimeout(timeoutId);
108
- controller.abort();
109
- };
110
- }, [previewData, selectedBlock, configLoading, isDirty]);
111
-
112
- // Handle template page navigation
113
- const handleNavigateToPage = useCallback((pageSlug: string) => {
114
- setCurrentPage(pageSlug);
115
- }, []);
116
-
117
- // Load block/template from URL on mount (runs once when blocks are loaded)
118
- const urlLoadedRef = useRef(false);
119
- useEffect(() => {
120
- if (blocks.length === 0 || urlLoadedRef.current) return;
121
-
122
- const params = new URLSearchParams(window.location.search);
123
- const blockName = params.get('block');
124
- const templateName = params.get('template');
125
-
126
- if (templateName) {
127
- const template = blocks.find((b) => b.name === templateName && b.type === 'template');
128
- if (template) {
129
- urlLoadedRef.current = true;
130
- handleSelectBlock(template);
131
- }
132
- } else if (blockName) {
133
- const block = blocks.find((b) => b.name === blockName);
134
- if (block) {
135
- urlLoadedRef.current = true;
136
- handleSelectBlock(block);
137
- }
138
- }
139
- }, [blocks, handleSelectBlock]);
140
-
141
- return (
142
- <div className="container">
143
- <BlocksList
144
- blocks={blocks}
145
- selectedBlock={selectedBlock}
146
- onSelectBlock={handleSelectBlock}
147
- loading={blocksLoading}
148
- />
149
- <Preview
150
- block={selectedBlock}
151
- previewData={previewData}
152
- currentPage={currentPage}
153
- loading={configLoading}
154
- />
155
- <Editor
156
- block={selectedBlock}
157
- loading={configLoading}
158
- previewData={previewData}
159
- onPreviewDataChange={handlePreviewDataChange}
160
- onNavigateToPage={handleNavigateToPage}
161
- />
162
- </div>
163
- );
164
- }
@@ -1,193 +0,0 @@
1
- /**
2
- * Test for preview data merge behavior
3
- * Run with: npx vitest run src/dev-ui-react/__tests__/previewData.test.ts
4
- */
5
-
6
- import { describe, it, expect } from 'vitest';
7
-
8
- // Simulating the merge logic from App.tsx
9
- function mergeForSave(
10
- configDataRef: Record<string, unknown>,
11
- previewData: Record<string, unknown>
12
- ): Record<string, unknown> {
13
- return { ...configDataRef, ...previewData };
14
- }
15
-
16
- // Simulating mergeDefaultsWithPreview from dev.ts
17
- function mergeDefaultsWithPreview(
18
- schema: Record<string, any>,
19
- previewData: Record<string, unknown>
20
- ): Record<string, unknown> {
21
- const merged: Record<string, unknown> = { ...previewData };
22
-
23
- for (const [key, field] of Object.entries(schema)) {
24
- if (merged[key] === undefined || merged[key] === null) {
25
- if (field.defaultValue !== undefined) {
26
- merged[key] = field.defaultValue;
27
- } else if (field.type === 'repeater') {
28
- merged[key] = [];
29
- }
30
- }
31
-
32
- if (field.type === 'repeater' && field.schema && Array.isArray(merged[key])) {
33
- merged[key] = (merged[key] as any[]).map((item: any) => {
34
- const mergedItem: Record<string, unknown> = { ...item };
35
- for (const [nestedKey, nestedField] of Object.entries(field.schema as Record<string, any>)) {
36
- if (mergedItem[nestedKey] === undefined && nestedField.defaultValue !== undefined) {
37
- mergedItem[nestedKey] = nestedField.defaultValue;
38
- }
39
- }
40
- return mergedItem;
41
- });
42
- }
43
-
44
- if (field.type === 'media' && typeof merged[key] === 'string') {
45
- merged[key] = { url: merged[key], alt: '' };
46
- }
47
- }
48
-
49
- return merged;
50
- }
51
-
52
- describe('Preview Data Management', () => {
53
- const mockSchema = {
54
- badge: { type: 'singleLine', label: 'Badge', defaultValue: 'About Us' },
55
- heading: { type: 'singleLine', label: 'Heading', defaultValue: 'Default Heading' },
56
- values: {
57
- type: 'repeater',
58
- label: 'Values',
59
- schema: {
60
- title: { type: 'singleLine', defaultValue: 'Title' },
61
- description: { type: 'multiLine', defaultValue: 'Description' },
62
- },
63
- defaultValue: [
64
- { title: 'Value 1', description: 'Desc 1' },
65
- { title: 'Value 2', description: 'Desc 2' },
66
- ],
67
- },
68
- imageUrl: { type: 'media', label: 'Image', defaultValue: 'https://example.com/img.jpg' },
69
- };
70
-
71
- const completePreviewData = {
72
- badge: 'About Us',
73
- heading: 'Building the future',
74
- values: [
75
- { title: 'Innovation', description: 'We innovate' },
76
- { title: 'Quality', description: 'We deliver quality' },
77
- ],
78
- imageUrl: { url: 'https://example.com/team.jpg', alt: 'Team' },
79
- };
80
-
81
- describe('mergeDefaultsWithPreview', () => {
82
- it('should merge defaultValues into empty previewData', () => {
83
- const result = mergeDefaultsWithPreview(mockSchema, {});
84
-
85
- expect(result.badge).toBe('About Us');
86
- expect(result.heading).toBe('Default Heading');
87
- expect(result.values).toEqual([
88
- { title: 'Value 1', description: 'Desc 1' },
89
- { title: 'Value 2', description: 'Desc 2' },
90
- ]);
91
- expect(result.imageUrl).toEqual({ url: 'https://example.com/img.jpg', alt: '' });
92
- });
93
-
94
- it('should preserve existing previewData values over defaults', () => {
95
- const result = mergeDefaultsWithPreview(mockSchema, { badge: 'Custom Badge' });
96
-
97
- expect(result.badge).toBe('Custom Badge');
98
- expect(result.heading).toBe('Default Heading'); // From default
99
- });
100
-
101
- it('should handle repeater arrays correctly', () => {
102
- const result = mergeDefaultsWithPreview(mockSchema, {
103
- values: [{ title: 'Custom' }], // Missing description
104
- });
105
-
106
- expect(result.values).toEqual([
107
- { title: 'Custom', description: 'Description' }, // description from default
108
- ]);
109
- });
110
-
111
- it('should convert string media to object', () => {
112
- const result = mergeDefaultsWithPreview(mockSchema, {
113
- imageUrl: 'https://custom.com/img.png',
114
- });
115
-
116
- expect(result.imageUrl).toEqual({ url: 'https://custom.com/img.png', alt: '' });
117
- });
118
- });
119
-
120
- describe('mergeForSave', () => {
121
- it('should merge configDataRef with previewData, previewData wins', () => {
122
- const configDataRef = { ...completePreviewData };
123
- const previewData = { badge: 'New Badge' };
124
-
125
- const result = mergeForSave(configDataRef, previewData);
126
-
127
- expect(result.badge).toBe('New Badge'); // User edit
128
- expect(result.heading).toBe('Building the future'); // From config
129
- expect(result.values).toEqual(completePreviewData.values); // From config
130
- expect(result.imageUrl).toEqual(completePreviewData.imageUrl); // From config
131
- });
132
-
133
- it('should preserve all fields when user edits one field', () => {
134
- const configDataRef = { ...completePreviewData };
135
- const previewData = { ...completePreviewData, badge: 'Edited Badge' };
136
-
137
- const result = mergeForSave(configDataRef, previewData);
138
-
139
- expect(Object.keys(result)).toEqual(Object.keys(completePreviewData));
140
- expect(result.badge).toBe('Edited Badge');
141
- expect(result.values).toEqual(completePreviewData.values);
142
- });
143
-
144
- it('should NOT lose data when configDataRef is empty (BUG SCENARIO)', () => {
145
- // This is the bug: if configDataRef is empty, we lose all data
146
- const configDataRef = {}; // Bug: ref was not set
147
- const previewData = { badge: 'New Badge' };
148
-
149
- const result = mergeForSave(configDataRef, previewData);
150
-
151
- // This will fail - demonstrating the bug
152
- // If configDataRef is empty, we only get previewData
153
- expect(Object.keys(result)).toEqual(['badge']);
154
- });
155
-
156
- it('should work correctly when configDataRef has complete data', () => {
157
- const configDataRef = { ...completePreviewData };
158
- const previewData = { badge: 'New Badge' };
159
-
160
- const result = mergeForSave(configDataRef, previewData);
161
-
162
- // With proper configDataRef, all data is preserved
163
- expect(Object.keys(result).sort()).toEqual(Object.keys(completePreviewData).sort());
164
- });
165
- });
166
-
167
- describe('Full flow simulation', () => {
168
- it('should simulate complete user edit flow', () => {
169
- // 1. Server loads preview.json and merges with schema defaults
170
- const previewJsonContent = { heading: 'Only heading' };
171
- const serverMerged = mergeDefaultsWithPreview(mockSchema, previewJsonContent);
172
-
173
- expect(serverMerged.badge).toBe('About Us'); // From default
174
- expect(serverMerged.heading).toBe('Only heading'); // From preview.json
175
- expect(serverMerged.values).toBeDefined(); // From default
176
-
177
- // 2. Frontend receives merged data and stores in configDataRef
178
- const configDataRef = { ...serverMerged };
179
-
180
- // 3. User edits badge
181
- const previewData = { ...serverMerged, badge: 'Edited Badge' };
182
-
183
- // 4. Save merges configDataRef with previewData
184
- const dataToSave = mergeForSave(configDataRef, previewData);
185
-
186
- // 5. All fields should be preserved
187
- expect(dataToSave.badge).toBe('Edited Badge');
188
- expect(dataToSave.heading).toBe('Only heading');
189
- expect(dataToSave.values).toEqual(serverMerged.values);
190
- expect(Object.keys(dataToSave).length).toBeGreaterThanOrEqual(4);
191
- });
192
- });
193
- });
@@ -1,232 +0,0 @@
1
- import { useState, useMemo } from 'react';
2
- import { Block, Filters } from '../types';
3
-
4
- interface BlocksListProps {
5
- blocks: Block[];
6
- selectedBlock: Block | null;
7
- onSelectBlock: (block: Block) => void;
8
- loading: boolean;
9
- }
10
-
11
- export function BlocksList({ blocks, selectedBlock, onSelectBlock, loading }: BlocksListProps) {
12
- const [filters, setFilters] = useState<Filters>({
13
- search: '',
14
- type: 'all',
15
- category: '',
16
- tags: [],
17
- });
18
- const [collapsed, setCollapsed] = useState(false);
19
-
20
- // Extract unique categories and tags
21
- const { categories, tags } = useMemo(() => {
22
- const cats = new Set<string>();
23
- const tgs = new Set<string>();
24
-
25
- blocks.forEach((block) => {
26
- if (block.category) cats.add(block.category);
27
- block.tags?.forEach((tag) => tgs.add(tag));
28
- });
29
-
30
- return {
31
- categories: Array.from(cats).sort(),
32
- tags: Array.from(tgs).sort(),
33
- };
34
- }, [blocks]);
35
-
36
- // Filter blocks
37
- const filteredBlocks = useMemo(() => {
38
- return blocks.filter((block) => {
39
- // Search filter
40
- if (filters.search) {
41
- const search = filters.search.toLowerCase();
42
- const matches =
43
- block.name.toLowerCase().includes(search) ||
44
- block.displayName.toLowerCase().includes(search) ||
45
- block.description?.toLowerCase().includes(search);
46
- if (!matches) return false;
47
- }
48
-
49
- // Type filter
50
- if (filters.type !== 'all' && block.type !== filters.type) {
51
- return false;
52
- }
53
-
54
- // Category filter
55
- if (filters.category && block.category !== filters.category) {
56
- return false;
57
- }
58
-
59
- // Tags filter
60
- if (filters.tags.length > 0) {
61
- const hasTag = filters.tags.some((tag) => block.tags?.includes(tag));
62
- if (!hasTag) return false;
63
- }
64
-
65
- return true;
66
- });
67
- }, [blocks, filters]);
68
-
69
- // Group by category
70
- const groupedBlocks = useMemo(() => {
71
- const grouped: Record<string, Block[]> = {};
72
-
73
- filteredBlocks.forEach((block) => {
74
- const cat = block.category || 'Uncategorized';
75
- if (!grouped[cat]) grouped[cat] = [];
76
- grouped[cat].push(block);
77
- });
78
-
79
- return Object.entries(grouped).sort(([a], [b]) => {
80
- if (a === 'Uncategorized') return 1;
81
- if (b === 'Uncategorized') return -1;
82
- return a.localeCompare(b);
83
- });
84
- }, [filteredBlocks]);
85
-
86
- const hasActiveFilters =
87
- filters.search || filters.type !== 'all' || filters.category || filters.tags.length > 0;
88
-
89
- const clearFilters = () => {
90
- setFilters({ search: '', type: 'all', category: '', tags: [] });
91
- };
92
-
93
- if (collapsed) {
94
- return (
95
- <div className="blocks-panel collapsed">
96
- <div className="blocks-header">
97
- <button className="panel-toggle" onClick={() => setCollapsed(false)} title="Expand">
98
- <span className="toggle-icon">☰</span>
99
- </button>
100
- </div>
101
- </div>
102
- );
103
- }
104
-
105
- return (
106
- <div className="blocks-panel">
107
- <div className="blocks-header">
108
- <div className="blocks-header-content">
109
- <h1>Blocks</h1>
110
- <p>
111
- {hasActiveFilters
112
- ? `${filteredBlocks.length} of ${blocks.length} items`
113
- : `${blocks.length} items`}
114
- </p>
115
- </div>
116
- <button className="panel-toggle" onClick={() => setCollapsed(true)} title="Collapse">
117
- <span className="toggle-icon">☰</span>
118
- </button>
119
- </div>
120
-
121
- <div className="blocks-filters">
122
- <input
123
- type="search"
124
- className="search-input"
125
- placeholder="Search blocks..."
126
- value={filters.search}
127
- onChange={(e) => setFilters((f) => ({ ...f, search: e.target.value }))}
128
- />
129
-
130
- <div className="filter-tabs">
131
- {(['all', 'block', 'template'] as const).map((type) => (
132
- <button
133
- key={type}
134
- className={`filter-tab ${filters.type === type ? 'active' : ''}`}
135
- onClick={() => setFilters((f) => ({ ...f, type }))}
136
- >
137
- {type === 'all' ? 'All' : type === 'block' ? 'Blocks' : 'Templates'}
138
- </button>
139
- ))}
140
- </div>
141
-
142
- <select
143
- className="filter-select"
144
- value={filters.category}
145
- onChange={(e) => setFilters((f) => ({ ...f, category: e.target.value }))}
146
- >
147
- <option value="">All Categories</option>
148
- {categories.map((cat) => (
149
- <option key={cat} value={cat}>
150
- {cat}
151
- </option>
152
- ))}
153
- </select>
154
-
155
- {tags.length > 0 && (
156
- <div className="tags-filter">
157
- {tags.map((tag) => (
158
- <button
159
- key={tag}
160
- className={`tag-chip ${filters.tags.includes(tag) ? 'active' : ''}`}
161
- onClick={() =>
162
- setFilters((f) => ({
163
- ...f,
164
- tags: f.tags.includes(tag)
165
- ? f.tags.filter((t) => t !== tag)
166
- : [...f.tags, tag],
167
- }))
168
- }
169
- >
170
- {tag}
171
- </button>
172
- ))}
173
- {hasActiveFilters && (
174
- <button className="clear-filters" onClick={clearFilters}>
175
- Clear all
176
- </button>
177
- )}
178
- </div>
179
- )}
180
- </div>
181
-
182
- <div className="blocks-list">
183
- {loading ? (
184
- <div className="loading">
185
- <div className="spinner" />
186
- <span>Loading blocks...</span>
187
- </div>
188
- ) : filteredBlocks.length === 0 ? (
189
- <div className="no-results">
190
- <div className="no-results-icon">🔍</div>
191
- <div>No items match your filters</div>
192
- {hasActiveFilters && (
193
- <button className="btn-clear" onClick={clearFilters}>
194
- Clear filters
195
- </button>
196
- )}
197
- </div>
198
- ) : (
199
- groupedBlocks.map(([category, categoryBlocks]) => (
200
- <div key={category} className="block-category">
201
- <div className="category-header">
202
- {category}
203
- <span className="category-count">{categoryBlocks.length}</span>
204
- </div>
205
- {categoryBlocks.map((block) => (
206
- <div
207
- key={block.name}
208
- className={`block-item ${selectedBlock?.name === block.name ? 'active' : ''}`}
209
- onClick={() => onSelectBlock(block)}
210
- >
211
- <div className="block-item-header">
212
- <div className="block-item-name">
213
- {block.displayName}
214
- {block.type === 'template' && (
215
- <span className="type-badge template">Template</span>
216
- )}
217
- </div>
218
- <span className="version-badge">v{block.version}</span>
219
- </div>
220
- <div className="block-item-footer">
221
- <span className="block-item-type">{block.category || 'Block'}</span>
222
- <span className="status-badge status-local">Local</span>
223
- </div>
224
- </div>
225
- ))}
226
- </div>
227
- ))
228
- )}
229
- </div>
230
- </div>
231
- );
232
- }