@beforesemicolon/site-builder 0.35.0 → 0.36.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.
- package/dist/cjs/build-templates.js +5 -5
- package/dist/esm/build-templates.js +5 -5
- package/netlify/functions/auth-middleware.js +183 -0
- package/netlify/functions/auth0-config.js +77 -0
- package/netlify/functions/build.js +214 -0
- package/netlify/functions/copy-file-local.js +88 -0
- package/netlify/functions/github.js +771 -0
- package/netlify/functions/validate-user.js +109 -0
- package/netlify/functions/widgets.js +403 -0
- package/netlify.toml +67 -0
- package/package.json +2 -2
- package/scaffolds/.env.example +18 -0
- package/scaffolds/_redirects +12 -0
- package/scaffolds/admin/app.js +244 -0
- package/scaffolds/admin/auth-manager.js +275 -0
- package/scaffolds/admin/controls/code.control.js +22 -0
- package/scaffolds/admin/controls/controls.js +249 -0
- package/scaffolds/admin/controls/file.control.js +829 -0
- package/scaffolds/admin/controls/html.control.js +43 -0
- package/scaffolds/admin/controls/markdown.control.js +31 -0
- package/scaffolds/admin/data.js +543 -0
- package/scaffolds/admin/flashbar.js +104 -0
- package/scaffolds/admin/index.html +44 -0
- package/scaffolds/admin/modal.js +123 -0
- package/scaffolds/admin/preview-widget.js +102 -0
- package/scaffolds/admin/preview.js +197 -0
- package/scaffolds/admin/repository-manager.js +329 -0
- package/scaffolds/admin/styles.css +1526 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { html } = window.BFS.MARKUP
|
|
2
|
+
|
|
3
|
+
export const renderQuillEditor = (input, path, disabled, label, onChange) => {
|
|
4
|
+
return html`
|
|
5
|
+
<div class="control-field-block quill-editor-control">
|
|
6
|
+
<span class="label">${label}</span>
|
|
7
|
+
<div class="quill-editor-wrapper">
|
|
8
|
+
<div ref="quill-editor" class="quill-editor">
|
|
9
|
+
${html([input.value])}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
`.onMount((temp) => {
|
|
14
|
+
const editorElement = temp.refs['quill-editor'][0]
|
|
15
|
+
|
|
16
|
+
if (!window.Quill) {
|
|
17
|
+
console.error('Quill library not loaded')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const quillInstance = new Quill(editorElement, {
|
|
22
|
+
theme: 'snow',
|
|
23
|
+
readOnly: disabled(),
|
|
24
|
+
modules: {
|
|
25
|
+
toolbar: disabled()
|
|
26
|
+
? false
|
|
27
|
+
: [
|
|
28
|
+
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
29
|
+
['bold', 'italic', 'underline'],
|
|
30
|
+
['link', 'blockquote'],
|
|
31
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Handle content changes (Requirements 5.2, 5.3)
|
|
37
|
+
if (!disabled()) {
|
|
38
|
+
quillInstance.on('text-change', (delta, oldDelta, source) => {
|
|
39
|
+
onChange(path, quillInstance.root.innerHTML, 'html')
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { html } = window.BFS.MARKUP
|
|
2
|
+
|
|
3
|
+
export const renderMarkdownEditor = (
|
|
4
|
+
input,
|
|
5
|
+
path,
|
|
6
|
+
disabled,
|
|
7
|
+
label,
|
|
8
|
+
onChange
|
|
9
|
+
) => {
|
|
10
|
+
return html`
|
|
11
|
+
<div class="control-field-block markdown-editor-control">
|
|
12
|
+
<span class="label">${label}</span>
|
|
13
|
+
<div class="markdown-editor-wrapper">
|
|
14
|
+
<div class="markdown-editor" ref="markdown-editor"></div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
`.onMount((temp) => {
|
|
18
|
+
const editorElement = temp.refs['markdown-editor'][0]
|
|
19
|
+
|
|
20
|
+
const editor = new TinyMDE.Editor({
|
|
21
|
+
element: editorElement,
|
|
22
|
+
content: input.value,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (!disabled()) {
|
|
26
|
+
editor.addEventListener('change', ({ content }) => {
|
|
27
|
+
onChange(path, content, 'markdown')
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { hideFlashbar, showFlashbar } from './flashbar.js'
|
|
2
|
+
import { showModal } from './modal.js'
|
|
3
|
+
import * as authManager from './auth-manager.js'
|
|
4
|
+
import * as repositoryManager from './repository-manager.js'
|
|
5
|
+
|
|
6
|
+
const { MARKUP, SITE_BUILDER } = window.BFS
|
|
7
|
+
const { state, html, effect } = MARKUP
|
|
8
|
+
const { inputDefinitionsToObject, mergeDataIntoInputDefinition } = SITE_BUILDER
|
|
9
|
+
|
|
10
|
+
// Store original widget values when first loaded
|
|
11
|
+
export const originalWidgetValues = new Map()
|
|
12
|
+
export const widgets = new Map()
|
|
13
|
+
export const widgetElements = new Map()
|
|
14
|
+
export const widgetLoadingStates = new Set()
|
|
15
|
+
export const isLocalDevelopment =
|
|
16
|
+
location.hostname === 'localhost' ||
|
|
17
|
+
location.hostname === '127.0.0.1' ||
|
|
18
|
+
location.port === '8888'
|
|
19
|
+
|
|
20
|
+
export const [loading, setLoading] = state(true)
|
|
21
|
+
export const [publishing, setPublishing] = state(false)
|
|
22
|
+
export const [panel, setPanel] = state('page')
|
|
23
|
+
export const [config, setConfig] = state(null)
|
|
24
|
+
export const [currentPage, setCurrentPage] = state(null)
|
|
25
|
+
export const [currentWidget, setCurrentWidget] = state(null)
|
|
26
|
+
export const [currentWidgetId, setCurrentWidgetId] = state('')
|
|
27
|
+
const [pendingChanges, setPendingChanges] = state([])
|
|
28
|
+
|
|
29
|
+
effect(() => {
|
|
30
|
+
const widgetId = currentWidgetId()
|
|
31
|
+
if (!widgetId) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
// Re-load widget when panel changes so page overrides don't leak into widget panel.
|
|
35
|
+
loadWidget(widgetId)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const hasPendingChanges = () => pendingChanges().length > 0
|
|
39
|
+
|
|
40
|
+
export const removePendingChange = (id) => {
|
|
41
|
+
setPendingChanges(pendingChanges().filter((change) => change.id !== id))
|
|
42
|
+
}
|
|
43
|
+
export const getPendingChange = (id) => {
|
|
44
|
+
return pendingChanges().find((change) => change.id === id) ?? null
|
|
45
|
+
}
|
|
46
|
+
export const allPendingChanges = pendingChanges
|
|
47
|
+
|
|
48
|
+
export const addPendingChange = (id, change) => {
|
|
49
|
+
removePendingChange(id)
|
|
50
|
+
setPendingChanges((prev) => [
|
|
51
|
+
...prev,
|
|
52
|
+
{
|
|
53
|
+
...change,
|
|
54
|
+
id,
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const selectWidget = (widgetId, newWidget) => {
|
|
60
|
+
setCurrentWidget(newWidget)
|
|
61
|
+
setCurrentWidgetId(widgetId)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const selectPage = (newPage) => {
|
|
65
|
+
setCurrentPage(newPage)
|
|
66
|
+
setCurrentWidget(null)
|
|
67
|
+
setCurrentWidgetId('')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const getWidgetById = (id) => widgets.get(id)
|
|
71
|
+
|
|
72
|
+
const fetchLocalWidget = async (widgetId) => {
|
|
73
|
+
return (await import(`/admin/widgets/${widgetId}.js`)).default
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetchRemoteWidget = async (widgetId) => {
|
|
77
|
+
const response = await fetch(`/api/widgets?action=get&widget=${widgetId}`)
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorData = await response.json().catch(() => ({}))
|
|
81
|
+
|
|
82
|
+
throw new Error(
|
|
83
|
+
errorData.error ||
|
|
84
|
+
`Remote fetch failed: ${response.status} ${response.statusText}`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = await response.json()
|
|
89
|
+
|
|
90
|
+
if (!data.success) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
data.error || 'Remote widget fetch failed without specific error'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!data.content || typeof data.content !== 'string') {
|
|
97
|
+
throw new Error('Remote widget content is missing or invalid')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const blob = new Blob([data.content], { type: 'text/javascript' })
|
|
101
|
+
const url = URL.createObjectURL(blob)
|
|
102
|
+
try {
|
|
103
|
+
const mod = await import(url)
|
|
104
|
+
return mod?.default ?? mod
|
|
105
|
+
} finally {
|
|
106
|
+
URL.revokeObjectURL(url)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const storeOriginalValues = (key, widget) => {
|
|
111
|
+
if (!originalWidgetValues.has(key)) {
|
|
112
|
+
const originalValues = {}
|
|
113
|
+
|
|
114
|
+
const extractOriginalValues = (inputs, pathPrefix = '') => {
|
|
115
|
+
if (!Array.isArray(inputs)) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
inputs.forEach((input, index) => {
|
|
119
|
+
const currentPath = pathPrefix
|
|
120
|
+
? `${pathPrefix}.${index}`
|
|
121
|
+
: String(index)
|
|
122
|
+
|
|
123
|
+
if (input.type === 'list' || input.type === 'group') {
|
|
124
|
+
extractOriginalValues(input.definitions, currentPath)
|
|
125
|
+
} else {
|
|
126
|
+
originalValues[currentPath] = input.value
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
extractOriginalValues(widget?.inputs)
|
|
132
|
+
originalWidgetValues.set(key, originalValues)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const cloneInputs = (inputs) => {
|
|
137
|
+
if (!Array.isArray(inputs)) {
|
|
138
|
+
return []
|
|
139
|
+
}
|
|
140
|
+
return inputs.map((input) => ({
|
|
141
|
+
...input,
|
|
142
|
+
definitions: input.definitions
|
|
143
|
+
? cloneInputs(input.definitions)
|
|
144
|
+
: undefined,
|
|
145
|
+
}))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cloneWidgetWithInputs = (widget) => ({
|
|
149
|
+
...widget,
|
|
150
|
+
inputs: cloneInputs(widget?.inputs),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const applyWidgetOverrides = (inputs, overrides) => {
|
|
154
|
+
if (!Array.isArray(inputs)) {
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
return inputs.map((input) => {
|
|
158
|
+
if (!input?.name) {
|
|
159
|
+
return input
|
|
160
|
+
}
|
|
161
|
+
const overrideValue = overrides?.[input.name]
|
|
162
|
+
if (overrideValue === undefined) {
|
|
163
|
+
return input
|
|
164
|
+
}
|
|
165
|
+
return mergeDataIntoInputDefinition(overrideValue, input)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const loadWidget = async (widgetId) => {
|
|
170
|
+
let loadingId
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (widgetLoadingStates.has(widgetId)) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if widget is already cached
|
|
178
|
+
let w = widgets.get(widgetId)
|
|
179
|
+
|
|
180
|
+
if (w) {
|
|
181
|
+
selectWidget(widgetId, w)
|
|
182
|
+
return w
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
widgetLoadingStates.add(widgetId)
|
|
186
|
+
|
|
187
|
+
loadingId = showFlashbar({
|
|
188
|
+
type: 'info',
|
|
189
|
+
title: 'Loading Widget',
|
|
190
|
+
message: `Fetching ${widgetId} widget...`,
|
|
191
|
+
loading: true,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
let data
|
|
195
|
+
|
|
196
|
+
if (isLocalDevelopment) {
|
|
197
|
+
data = await fetchLocalWidget(widgetId)
|
|
198
|
+
} else {
|
|
199
|
+
data = await fetchRemoteWidget(widgetId)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
widgets.set(widgetId, data)
|
|
203
|
+
|
|
204
|
+
const pageId = currentPage()?.id ?? ''
|
|
205
|
+
const isPagePanel = panel() === 'page'
|
|
206
|
+
const pageOverrides = isPagePanel
|
|
207
|
+
? currentPage()?.widgetsData?.[widgetId] || null
|
|
208
|
+
: null
|
|
209
|
+
|
|
210
|
+
if (pageOverrides) {
|
|
211
|
+
const widgetClone = cloneWidgetWithInputs(data)
|
|
212
|
+
widgetClone.inputs = applyWidgetOverrides(
|
|
213
|
+
widgetClone.inputs,
|
|
214
|
+
pageOverrides
|
|
215
|
+
)
|
|
216
|
+
selectWidget(widgetId, widgetClone)
|
|
217
|
+
storeOriginalValues(`${pageId}:${widgetId}`, widgetClone)
|
|
218
|
+
} else {
|
|
219
|
+
selectWidget(widgetId, data)
|
|
220
|
+
storeOriginalValues(widgetId, data)
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(error)
|
|
224
|
+
showFlashbar({
|
|
225
|
+
type: 'error',
|
|
226
|
+
title: 'Widget Load Error',
|
|
227
|
+
message: `Failed to load widget "${widgetId}"`,
|
|
228
|
+
})
|
|
229
|
+
} finally {
|
|
230
|
+
hideFlashbar(loadingId)
|
|
231
|
+
widgetLoadingStates.delete(widgetId)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const loadConfig = async () => {
|
|
236
|
+
const response = await fetch('/admin/config.json')
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
return showFlashbar({
|
|
240
|
+
type: 'error',
|
|
241
|
+
title: `Failed to load your website`,
|
|
242
|
+
message: 'Please refresh to try again',
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const cf = await response.json()
|
|
247
|
+
setConfig(cf)
|
|
248
|
+
selectPage(cf.pages.find((pg) => pg.id === 'home') ?? null)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const resolveNamePath = (inputs, path) => {
|
|
252
|
+
const segments = String(path).split('.')
|
|
253
|
+
const topIndex = Number.parseInt(segments.shift(), 10)
|
|
254
|
+
if (!Number.isFinite(topIndex)) {
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
let currentInput = inputs?.[topIndex]
|
|
258
|
+
if (!currentInput?.name) {
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
261
|
+
const namePath = [currentInput.name]
|
|
262
|
+
while (segments.length) {
|
|
263
|
+
if (!currentInput?.definitions) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
const index = Number.parseInt(segments.shift(), 10)
|
|
267
|
+
if (!Number.isFinite(index)) {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
const nextInput = currentInput.definitions?.[index]
|
|
271
|
+
if (!nextInput) {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
if (currentInput.type === 'list') {
|
|
275
|
+
namePath.push(index)
|
|
276
|
+
} else {
|
|
277
|
+
namePath.push(nextInput.name)
|
|
278
|
+
}
|
|
279
|
+
currentInput = nextInput
|
|
280
|
+
}
|
|
281
|
+
return namePath
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const setDeepValue = (target, path, value) => {
|
|
285
|
+
if (!path.length) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
let cursor = target
|
|
289
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
290
|
+
const key = path[i]
|
|
291
|
+
const nextKey = path[i + 1]
|
|
292
|
+
const shouldBeArray = typeof nextKey === 'number'
|
|
293
|
+
if (cursor[key] == null) {
|
|
294
|
+
cursor[key] = shouldBeArray ? [] : {}
|
|
295
|
+
}
|
|
296
|
+
cursor = cursor[key]
|
|
297
|
+
}
|
|
298
|
+
cursor[path[path.length - 1]] = value
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const buildWidgetInputPatch = (widget, change) => {
|
|
302
|
+
const inputValues = inputDefinitionsToObject(widget.inputs || [])
|
|
303
|
+
const patch = {}
|
|
304
|
+
for (const [path, changeEntry] of Object.entries(change.changes || {})) {
|
|
305
|
+
const namePath = resolveNamePath(widget.inputs || [], path)
|
|
306
|
+
if (!namePath?.length) {
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
setDeepValue(inputValues, namePath, changeEntry.newValue)
|
|
310
|
+
const topKey = namePath[0]
|
|
311
|
+
if (Object.prototype.hasOwnProperty.call(inputValues, topKey)) {
|
|
312
|
+
patch[topKey] = inputValues[topKey]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return patch
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export const publishChanges = async () => {
|
|
319
|
+
if (publishing()) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
setPublishing(true)
|
|
324
|
+
|
|
325
|
+
let progressId
|
|
326
|
+
let publishProgressId
|
|
327
|
+
let buildProgressId
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// Step 1: Collect unique widgets, page-widget changes, and separate file uploads
|
|
331
|
+
const changedWidgetIds = new Set()
|
|
332
|
+
const fileUploads = []
|
|
333
|
+
const pageWidgetUpdates = new Map()
|
|
334
|
+
|
|
335
|
+
for (const change of allPendingChanges()) {
|
|
336
|
+
if (change.type === 'widget') {
|
|
337
|
+
changedWidgetIds.add(change.id)
|
|
338
|
+
|
|
339
|
+
// Collect file uploads from this widget's changes
|
|
340
|
+
for (const [path, changeData] of Object.entries(
|
|
341
|
+
change.changes
|
|
342
|
+
)) {
|
|
343
|
+
if (changeData.fileData) {
|
|
344
|
+
fileUploads.push({
|
|
345
|
+
widgetId: change.id,
|
|
346
|
+
path,
|
|
347
|
+
filePath: changeData.fileData.targetPath,
|
|
348
|
+
fileData: changeData.fileData,
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} else if (change.type === 'page-widget') {
|
|
353
|
+
const pageId = currentPage()?.id
|
|
354
|
+
const widgetId = change.id
|
|
355
|
+
if (!pageId || !widgetId) {
|
|
356
|
+
console.warn(
|
|
357
|
+
'[cms:publish] Skipping page-widget change without page/widget id',
|
|
358
|
+
change
|
|
359
|
+
)
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
const widget = getWidgetById(widgetId)
|
|
363
|
+
if (!widget) {
|
|
364
|
+
console.warn(
|
|
365
|
+
'[cms:publish] Widget not found for page-widget update',
|
|
366
|
+
{ widgetId, pageId }
|
|
367
|
+
)
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
const patch = buildWidgetInputPatch(widget, change)
|
|
371
|
+
if (Object.keys(patch).length === 0) {
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
for (const [path, changeData] of Object.entries(
|
|
375
|
+
change.changes
|
|
376
|
+
)) {
|
|
377
|
+
if (changeData.fileData) {
|
|
378
|
+
fileUploads.push({
|
|
379
|
+
widgetId,
|
|
380
|
+
path,
|
|
381
|
+
filePath: changeData.fileData.targetPath,
|
|
382
|
+
fileData: changeData.fileData,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!pageWidgetUpdates.has(pageId)) {
|
|
387
|
+
pageWidgetUpdates.set(pageId, new Map())
|
|
388
|
+
}
|
|
389
|
+
const pageUpdates = pageWidgetUpdates.get(pageId)
|
|
390
|
+
pageUpdates.set(widgetId, {
|
|
391
|
+
...(pageUpdates.get(widgetId) || {}),
|
|
392
|
+
...patch,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (
|
|
398
|
+
changedWidgetIds.size === 0 &&
|
|
399
|
+
fileUploads.length === 0 &&
|
|
400
|
+
pageWidgetUpdates.size === 0
|
|
401
|
+
) {
|
|
402
|
+
return showFlashbar({
|
|
403
|
+
type: 'info',
|
|
404
|
+
title: 'No Changes',
|
|
405
|
+
message: 'There are no changes to publish.',
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Show progress message
|
|
410
|
+
progressId = showFlashbar({
|
|
411
|
+
type: 'info',
|
|
412
|
+
title: 'Saving changes...',
|
|
413
|
+
message: 'Making sure your changes are valid and ready to publish.',
|
|
414
|
+
loading: true,
|
|
415
|
+
dismissable: false,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const widgetsToCommit = []
|
|
419
|
+
for (const widgetId of changedWidgetIds) {
|
|
420
|
+
const widget = getWidgetById(widgetId)
|
|
421
|
+
if (!widget) {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
widgetsToCommit.push({
|
|
426
|
+
widgetId,
|
|
427
|
+
inputs: widget.inputs,
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const changedFiles = []
|
|
432
|
+
for (const [pageId, widgetsMap] of pageWidgetUpdates.entries()) {
|
|
433
|
+
const filePath = `templates/${pageId}.json`
|
|
434
|
+
const { content, sha } =
|
|
435
|
+
await repositoryManager.getFileContent(filePath)
|
|
436
|
+
let templateData
|
|
437
|
+
try {
|
|
438
|
+
templateData = JSON.parse(content)
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Failed to parse ${filePath} JSON: ${error.message}`
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
const widgetsData = {
|
|
445
|
+
...(templateData.widgetsData || {}),
|
|
446
|
+
}
|
|
447
|
+
for (const [widgetId, patch] of widgetsMap.entries()) {
|
|
448
|
+
widgetsData[widgetId] = {
|
|
449
|
+
...(widgetsData[widgetId] || {}),
|
|
450
|
+
...patch,
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const updatedContent = `${JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
...templateData,
|
|
456
|
+
widgetsData,
|
|
457
|
+
},
|
|
458
|
+
null,
|
|
459
|
+
2
|
|
460
|
+
)}\n`
|
|
461
|
+
changedFiles.push({
|
|
462
|
+
filePath,
|
|
463
|
+
content: updatedContent,
|
|
464
|
+
sha,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
widgetsToCommit.length === 0 &&
|
|
470
|
+
fileUploads.length === 0 &&
|
|
471
|
+
changedFiles.length === 0
|
|
472
|
+
) {
|
|
473
|
+
hideFlashbar(progressId)
|
|
474
|
+
return showFlashbar({
|
|
475
|
+
type: 'error',
|
|
476
|
+
title: 'No Valid Changes',
|
|
477
|
+
message: 'No valid changes found to publish.',
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Prepare file uploads for commit (Requirements 3.1, 3.2)
|
|
482
|
+
const uploadedFiles = fileUploads.map((upload) => ({
|
|
483
|
+
filePath: upload.filePath,
|
|
484
|
+
fileData: upload.fileData,
|
|
485
|
+
}))
|
|
486
|
+
|
|
487
|
+
const widgetLabels = widgetsToCommit.map(
|
|
488
|
+
(w) => `widgets/${w.widgetId}.js`
|
|
489
|
+
)
|
|
490
|
+
const templateLabels = changedFiles.map((file) => file.filePath)
|
|
491
|
+
const commitTargets = [...widgetLabels, ...templateLabels]
|
|
492
|
+
const commitMsg = `[${authManager.user().name}] CMS Update ${commitTargets.join(', ')}${fileUploads.length > 0 ? ` + ${fileUploads.length} files` : ''}: ${new Date().toISOString()}`
|
|
493
|
+
// Step 3: Commit all changes and file uploads at once
|
|
494
|
+
const commitTime = new Date().toISOString()
|
|
495
|
+
await repositoryManager.commitChanges(
|
|
496
|
+
commitMsg,
|
|
497
|
+
changedFiles,
|
|
498
|
+
uploadedFiles,
|
|
499
|
+
widgetsToCommit
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
hideFlashbar(progressId)
|
|
503
|
+
publishProgressId = showFlashbar({
|
|
504
|
+
type: 'info',
|
|
505
|
+
title: 'Publishing...',
|
|
506
|
+
message:
|
|
507
|
+
'We are updating the live website. This may take a minute.',
|
|
508
|
+
loading: true,
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const { buildId } = await repositoryManager.waitForBuild(commitTime)
|
|
512
|
+
|
|
513
|
+
await repositoryManager.pollBuildStatus(buildId)
|
|
514
|
+
|
|
515
|
+
hideFlashbar(publishProgressId)
|
|
516
|
+
|
|
517
|
+
showModal({
|
|
518
|
+
dismissable: false,
|
|
519
|
+
title: 'Changes Published!',
|
|
520
|
+
content: html`<p>
|
|
521
|
+
Your changes are now live.
|
|
522
|
+
<a href="${location.origin}">View website</a>.
|
|
523
|
+
</p>`,
|
|
524
|
+
action: () => {
|
|
525
|
+
// Page reload will automatically clear widget cache and fetch fresh widgets
|
|
526
|
+
window.location.reload(true) // Hard refresh
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('Error publishing changes:', error)
|
|
531
|
+
hideFlashbar(progressId)
|
|
532
|
+
hideFlashbar(publishProgressId)
|
|
533
|
+
hideFlashbar(buildProgressId)
|
|
534
|
+
showFlashbar({
|
|
535
|
+
type: 'error',
|
|
536
|
+
title: 'Publish Failed',
|
|
537
|
+
message:
|
|
538
|
+
'We could not publish or update the website with your changes. Please try again.',
|
|
539
|
+
})
|
|
540
|
+
} finally {
|
|
541
|
+
setPublishing(false)
|
|
542
|
+
}
|
|
543
|
+
}
|