@crombolo/backoffice-host 0.0.0-stage → 0.9.1
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/assets/login.js +352 -0
- package/package.json +35 -4
- package/plugin/assistant/client.js +53 -0
- package/plugin/assistant/drawer.js +865 -0
- package/plugin/assistant/umbraco-package.json +16 -0
- package/plugin/branding/backdrop.svg +1 -0
- package/plugin/branding/branding.js +100 -0
- package/plugin/branding/bundle-builder-labels.js +63 -0
- package/plugin/branding/client-credentials.js +49 -0
- package/plugin/branding/components-tree.js +40 -0
- package/plugin/branding/content-type-labels.js +27 -0
- package/plugin/branding/favicon.svg +1 -0
- package/plugin/branding/help-menu.js +46 -0
- package/plugin/branding/icon-mark.js +15 -0
- package/plugin/branding/icon-registry.js +40 -0
- package/plugin/branding/localization-en.js +115 -0
- package/plugin/branding/log-viewer-menu.js +70 -0
- package/plugin/branding/logo-header.svg +1 -0
- package/plugin/branding/logo-surface.svg +1 -0
- package/plugin/branding/query-builder.js +28 -0
- package/plugin/branding/sign-in.js +80 -0
- package/plugin/branding/sysinfo.js +89 -0
- package/plugin/branding/theme.css +102 -0
- package/plugin/bundles-client.js +34 -0
- package/plugin/bundles-created-overview.js +150 -0
- package/plugin/bundles-created.js +70 -0
- package/plugin/bundles-installed.js +147 -0
- package/plugin/bundles-marketplace.js +199 -0
- package/plugin/changes-dashboard.js +132 -0
- package/plugin/element-type-hint.js +58 -0
- package/plugin/form-picker.js +116 -0
- package/plugin/forms-client.js +52 -0
- package/plugin/forms-entries.js +244 -0
- package/plugin/forms-overview.js +380 -0
- package/plugin/localizations/ar.js +97 -0
- package/plugin/localizations/bs.js +86 -0
- package/plugin/localizations/cs.js +73 -0
- package/plugin/localizations/cy.js +92 -0
- package/plugin/localizations/da.js +101 -0
- package/plugin/localizations/de.js +79 -0
- package/plugin/localizations/en.js +71 -0
- package/plugin/localizations/es.js +52 -0
- package/plugin/localizations/fr.js +78 -0
- package/plugin/localizations/he.js +24 -0
- package/plugin/localizations/hr.js +89 -0
- package/plugin/localizations/it.js +86 -0
- package/plugin/localizations/ja.js +43 -0
- package/plugin/localizations/ko.js +24 -0
- package/plugin/localizations/nb.js +44 -0
- package/plugin/localizations/nl.js +85 -0
- package/plugin/localizations/pl.js +44 -0
- package/plugin/localizations/pt-br.js +25 -0
- package/plugin/localizations/pt.js +100 -0
- package/plugin/localizations/ro.js +9 -0
- package/plugin/localizations/ru.js +46 -0
- package/plugin/localizations/sv.js +46 -0
- package/plugin/localizations/tr.js +80 -0
- package/plugin/localizations/uk.js +46 -0
- package/plugin/localizations/umbraco-package.json +279 -0
- package/plugin/localizations/vi.js +104 -0
- package/plugin/localizations/zh-tw.js +38 -0
- package/plugin/localizations/zh.js +38 -0
- package/plugin/read-only-banner.js +48 -0
- package/plugin/report-client.js +13 -0
- package/plugin/seo/client.js +35 -0
- package/plugin/seo/dashboard.js +242 -0
- package/plugin/seo/document-view.js +161 -0
- package/plugin/seo/type-hint.js +45 -0
- package/plugin/seo/umbraco-package.json +61 -0
- package/plugin/settings-dashboard.js +123 -0
- package/plugin/tsx-editors.js +337 -0
- package/plugin/tsx-snippets.js +63 -0
- package/plugin/umbraco-package.json +298 -0
- package/plugin/welcome-dashboard.js +106 -0
- package/src/branding.ts +59 -0
- package/src/extensions.ts +228 -0
- package/src/graphics.ts +87 -0
- package/src/index.ts +7 -0
- package/src/login-assets.ts +18 -0
- package/src/manifests.ts +154 -0
- package/src/paths.ts +117 -0
- package/src/shell.ts +139 -0
- package/src/static.ts +107 -0
- package/src/text-imports.d.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes Umbraco's template and partial view editors write and read TSX instead
|
|
3
|
+
* of Razor. Umbraco's client hard-codes a Razor scaffold for a new template,
|
|
4
|
+
* writes a Razor `Layout = "x.cshtml"` block when a master template is chosen,
|
|
5
|
+
* starts a new partial view empty, and opens both in an editor configured for
|
|
6
|
+
* Razor. This entry point changes the methods that do so — the template
|
|
7
|
+
* repository's scaffold, the template workspace's master-template update, the
|
|
8
|
+
* partial view repository's scaffold and the code editor's own language — to
|
|
9
|
+
* write what a crombolo view is: a default-exported component, with
|
|
10
|
+
* `export const layout = '<alias>'` for its master, and for a partial view the
|
|
11
|
+
* server's `Empty` snippet. Snippets inserted into a view are translated to TSX
|
|
12
|
+
* (`tsx-snippets.js`), and the inserts that have no TSX equivalent — a partial
|
|
13
|
+
* view, which needs an import as well as a tag, and Razor's sections — are hidden.
|
|
14
|
+
*
|
|
15
|
+
* The methods are patched on Umbraco's own classes rather than the extensions
|
|
16
|
+
* re-registered: changing the registry once the backoffice is rendering makes
|
|
17
|
+
* every open section re-evaluate its extensions, and a route set up in that
|
|
18
|
+
* window throws inside Umbraco's code.
|
|
19
|
+
*/
|
|
20
|
+
import { UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor'
|
|
21
|
+
import { PartialViewService } from '@umbraco-cms/backoffice/external/backend-api'
|
|
22
|
+
import { UmbPartialViewDetailRepository } from '@umbraco-cms/backoffice/partial-view'
|
|
23
|
+
import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template'
|
|
24
|
+
import { toTsxSnippet } from './tsx-snippets.js'
|
|
25
|
+
|
|
26
|
+
export const SCAFFOLD = `import type { PageProps } from '@crombolo/crombolo'
|
|
27
|
+
|
|
28
|
+
export default function Template({ model }: PageProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<h1>{model.name}</h1>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
const LAYOUT_EXPORT = /^export\s+const\s+layout\s*=\s*['"][^'"]*['"]\s*;?[ \t]*\n*/m
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Umbraco's own Razor layout block, the same shape its workspace writes and
|
|
41
|
+
* matches. A TSX view can never legitimately hold one, and the patch below cannot
|
|
42
|
+
* guarantee it is in place before Umbraco's `create()` runs — that method calls
|
|
43
|
+
* `setLayoutTemplate(parent, true)` itself, so on a slow load the unpatched writer
|
|
44
|
+
* can insert a block (with `undefined` for the alias it has not fetched yet)
|
|
45
|
+
* before this module has replaced the method. Stripping it here makes the result
|
|
46
|
+
* the same whichever of the two ran first.
|
|
47
|
+
*/
|
|
48
|
+
const LAYOUT_BLOCK = /@\{[\s\S]*?Layout\s*=\s*(?:"[^"]*"|null)\s*;[\s\S]*?\}[ \t]*\n*/g
|
|
49
|
+
|
|
50
|
+
/** `content` with its `export const layout` set to `alias`, or removed when null. */
|
|
51
|
+
export function withLayout(content, alias) {
|
|
52
|
+
const rest = content.replace(LAYOUT_BLOCK, '').replace(LAYOUT_EXPORT, '')
|
|
53
|
+
if (!alias) return rest
|
|
54
|
+
const line = `export const layout = '${alias}'\n\n`
|
|
55
|
+
// After the imports, where a view declares it
|
|
56
|
+
const imports = /^(?:import[^\n]*\n)+\n*/.exec(rest)
|
|
57
|
+
if (!imports) return line + rest
|
|
58
|
+
return `${imports[0].replace(/\n*$/, '\n\n')}${line}${rest.slice(imports[0].length)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The dialect a crombolo view is written in. Accepted on `umb-code-editor`'s
|
|
63
|
+
* `language` in place of one of Umbraco's own, and what Umbraco's hard-coded
|
|
64
|
+
* `razor` is read as.
|
|
65
|
+
*
|
|
66
|
+
* monaco has no `tsx` language of its own, and registering one would be a step
|
|
67
|
+
* backwards: TypeScript decides whether a file may contain JSX from its *file
|
|
68
|
+
* name*, and its worker only attaches to the `typescript` and `javascript`
|
|
69
|
+
* modes. A `tsx` language would therefore tokenise and then be ignored by the
|
|
70
|
+
* service every completion and every real error comes from. So the dialect is
|
|
71
|
+
* monaco's `typescript` mode plus a model whose URI ends in `.tsx`.
|
|
72
|
+
*/
|
|
73
|
+
export const TSX = 'tsx'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The virtual directory a view is opened in. It mirrors `Views/` on disk so that
|
|
77
|
+
* a view's `../schema/content-types.d.ts` resolves in the editor exactly as it
|
|
78
|
+
* resolves for `tsc`.
|
|
79
|
+
*/
|
|
80
|
+
const VIEWS = 'file:///Views'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `bundler` module resolution, which is what the site's tsconfig uses and what
|
|
84
|
+
* resolves the served module. monaco's editor-side `ModuleResolutionKind` names
|
|
85
|
+
* only Classic and NodeJs — the enum predates the option — but the worker's own
|
|
86
|
+
* TypeScript knows it, and monaco passes the compiler options through untouched.
|
|
87
|
+
*/
|
|
88
|
+
const BUNDLER = 100
|
|
89
|
+
|
|
90
|
+
/** Code editors showing a view, rather than a stylesheet, a script or JSON. */
|
|
91
|
+
const views = new WeakSet()
|
|
92
|
+
|
|
93
|
+
let opened = 0
|
|
94
|
+
|
|
95
|
+
function pluginApi() {
|
|
96
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
97
|
+
return `${href.replace(/\/$/, '')}/crombolo/api`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** What the server checks a view against, and in which JSX mode; no libs if it will not say. */
|
|
101
|
+
async function typeLibs() {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(`${pluginApi()}/editor-types`, {
|
|
104
|
+
credentials: 'include',
|
|
105
|
+
headers: { accept: 'application/json' },
|
|
106
|
+
})
|
|
107
|
+
if (!response.ok) throw new Error(`the server answered ${response.status}`)
|
|
108
|
+
const body = await response.json()
|
|
109
|
+
return {
|
|
110
|
+
libs: Array.isArray(body?.libs) ? body.libs : [],
|
|
111
|
+
jsxImportSource:
|
|
112
|
+
typeof body?.jsxImportSource === 'string' ? body.jsxImportSource : '@crombolo/crombolo',
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Said out loud, because the consequence is not obvious: with no types,
|
|
116
|
+
// `@crombolo/crombolo` does not resolve, so there is no `JSX.IntrinsicElements` and
|
|
117
|
+
// TypeScript underlines every tag in the view. Type checking is turned off
|
|
118
|
+
// below rather than left to report that.
|
|
119
|
+
console.error('Crombolo could not fetch the types for the view editor', error)
|
|
120
|
+
return { libs: [], jsxImportSource: '@crombolo/crombolo' }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let loaded
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Configures monaco's TypeScript service for TSX, and reports both monaco and
|
|
128
|
+
* whether the dialect is usable.
|
|
129
|
+
*
|
|
130
|
+
* Done on the first view to open rather than at boot, because monaco is several
|
|
131
|
+
* megabytes and most sessions never open one. The types are asked for every time
|
|
132
|
+
* instead of once, so a content type a colleague added is in this editor as
|
|
133
|
+
* soon as the view is reopened; the endpoint tags them, so the browser fetches
|
|
134
|
+
* them again only when they have changed.
|
|
135
|
+
*/
|
|
136
|
+
async function configureTsx() {
|
|
137
|
+
let monaco
|
|
138
|
+
try {
|
|
139
|
+
loaded ??= import('@umbraco-cms/backoffice/external/monaco-editor')
|
|
140
|
+
monaco = (await loaded).monaco
|
|
141
|
+
// `monaco.typescript`, not `monaco.languages.typescript`: monaco exports each
|
|
142
|
+
// language service as its own top-level namespace, and the nested form is
|
|
143
|
+
// where older versions kept it. Read both, because getting this wrong is what
|
|
144
|
+
// went wrong the first time — and it went wrong quietly.
|
|
145
|
+
const ts = monaco.typescript ?? monaco.languages?.typescript
|
|
146
|
+
if (!ts) throw new Error('monaco exposes no TypeScript service')
|
|
147
|
+
const { JsxEmit, ModuleKind, ScriptTarget, typescriptDefaults } = ts
|
|
148
|
+
const { libs, jsxImportSource } = await typeLibs()
|
|
149
|
+
// The site's `apps/site/tsconfig.json`, so that what the editor underlines
|
|
150
|
+
// is what `tsc` would report and nothing else — down to the JSX mode the
|
|
151
|
+
// site chose, `@crombolo/crombolo` or `@crombolo/crombolo/loose`.
|
|
152
|
+
typescriptDefaults.setCompilerOptions({
|
|
153
|
+
allowNonTsExtensions: true,
|
|
154
|
+
target: ScriptTarget.ESNext,
|
|
155
|
+
module: ModuleKind.ESNext,
|
|
156
|
+
moduleResolution: BUNDLER,
|
|
157
|
+
jsx: JsxEmit.ReactJSX,
|
|
158
|
+
jsxImportSource,
|
|
159
|
+
allowImportingTsExtensions: true,
|
|
160
|
+
noEmit: true,
|
|
161
|
+
strict: true,
|
|
162
|
+
})
|
|
163
|
+
// Replaced wholesale rather than added one at a time, so a refresh cannot
|
|
164
|
+
// leave a stale copy of the generated types behind.
|
|
165
|
+
typescriptDefaults.setExtraLibs(
|
|
166
|
+
libs.map((lib) => ({ filePath: lib.path, content: lib.content })),
|
|
167
|
+
)
|
|
168
|
+
// Without the types, every name in the view is unresolved and checking it
|
|
169
|
+
// says nothing true. Syntax is still worth checking, and the dialect —
|
|
170
|
+
// highlighting, brackets, JSX that parses — is unaffected either way.
|
|
171
|
+
typescriptDefaults.setDiagnosticsOptions({
|
|
172
|
+
noSemanticValidation: libs.length === 0,
|
|
173
|
+
noSyntaxValidation: false,
|
|
174
|
+
})
|
|
175
|
+
return { monaco, ok: true }
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Reported rather than swallowed. Configured against the wrong namespace,
|
|
178
|
+
// this failed silently and turned every tag in a view red — and a console
|
|
179
|
+
// error is what the browser suite fails a test on, so the next time this
|
|
180
|
+
// breaks a test says so instead of a person noticing.
|
|
181
|
+
console.error('Crombolo could not configure the TSX dialect for the view editor', error)
|
|
182
|
+
return { monaco, ok: false }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Teaches `umb-code-editor` the dialect: `language="tsx"`, and Umbraco's own
|
|
188
|
+
* hard-coded `razor`, become monaco's `typescript` on a `.tsx` model.
|
|
189
|
+
*
|
|
190
|
+
* `language` is a Lit property, so the accessor to wrap is the one Lit defined
|
|
191
|
+
* on the prototype. If a future version of either defines it elsewhere, the
|
|
192
|
+
* editor keeps Umbraco's Razor rather than this entry point throwing on the way
|
|
193
|
+
* in and taking the TSX scaffolds down with it.
|
|
194
|
+
*/
|
|
195
|
+
function teachDialect(editors) {
|
|
196
|
+
const language = Object.getOwnPropertyDescriptor(editors, 'language')
|
|
197
|
+
if (!language?.set) return
|
|
198
|
+
Object.defineProperty(editors, 'language', {
|
|
199
|
+
...language,
|
|
200
|
+
set(value) {
|
|
201
|
+
const view = value === TSX || value === 'razor'
|
|
202
|
+
if (view) views.add(this)
|
|
203
|
+
else views.delete(this)
|
|
204
|
+
language.set.call(this, view ? 'typescript' : value)
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const firstUpdated = editors.firstUpdated
|
|
209
|
+
editors.firstUpdated = async function (changed) {
|
|
210
|
+
if (!views.has(this)) return await firstUpdated.call(this, changed)
|
|
211
|
+
const { monaco, ok } = await configureTsx()
|
|
212
|
+
await firstUpdated.call(this, changed)
|
|
213
|
+
const editor = this.editor?.monacoEditor
|
|
214
|
+
const created = editor?.getModel()
|
|
215
|
+
if (!monaco || !editor || !created) return
|
|
216
|
+
if (!ok) {
|
|
217
|
+
// Without the dialect, TypeScript reads a view as a plain `.ts` file and
|
|
218
|
+
// underlines every tag in it. Plain text says less and lies less.
|
|
219
|
+
monaco.editor.setModelLanguage(created, 'plaintext')
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
// monaco names the model it creates `inmemory://model/1`, and a name with no
|
|
223
|
+
// `.tsx` on it is a file TypeScript refuses to read JSX in — every tag in
|
|
224
|
+
// the view would be a syntax error. So the model is replaced by one named
|
|
225
|
+
// as the file it is.
|
|
226
|
+
const model = monaco.editor.createModel(
|
|
227
|
+
created.getValue(),
|
|
228
|
+
'typescript',
|
|
229
|
+
monaco.Uri.parse(`${VIEWS}/view-${++opened}.tsx`),
|
|
230
|
+
)
|
|
231
|
+
editor.setModel(model)
|
|
232
|
+
created.dispose()
|
|
233
|
+
editor.onDidDispose(() => model.dispose())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const insert = editors.insert
|
|
237
|
+
editors.insert = function (text) {
|
|
238
|
+
return insert.call(this, views.has(this) ? toTsxSnippet(text) : text)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The template editor's inserts that cannot become TSX. A partial view is an
|
|
244
|
+
* import as well as a tag, which an insert at the cursor cannot write, and
|
|
245
|
+
* sections are Razor's way for a layout to take content from a page, where a
|
|
246
|
+
* TSX layout takes props.
|
|
247
|
+
*/
|
|
248
|
+
async function hideRazorInserts() {
|
|
249
|
+
const menu = (await customElements.whenDefined('umb-templating-insert-menu')).prototype
|
|
250
|
+
Object.defineProperty(menu, 'hidePartialViews', {
|
|
251
|
+
configurable: true,
|
|
252
|
+
get: () => true,
|
|
253
|
+
set: () => {},
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const editor = (await customElements.whenDefined('umb-template-workspace-editor')).prototype
|
|
257
|
+
const updated = editor.updated
|
|
258
|
+
editor.updated = function (changed) {
|
|
259
|
+
updated?.call(this, changed)
|
|
260
|
+
const sections = this.shadowRoot?.querySelector('#sections-button')
|
|
261
|
+
if (sections) sections.style.display = 'none'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The value builder's preview shows the snippet it will insert, so it shows the
|
|
267
|
+
* TSX one. Its output is still Umbraco's, and is translated on insert with the rest.
|
|
268
|
+
*/
|
|
269
|
+
async function previewFieldsAsTsx() {
|
|
270
|
+
const modal = (await customElements.whenDefined('umb-templating-page-field-builder-modal'))
|
|
271
|
+
.prototype
|
|
272
|
+
const render = modal.render
|
|
273
|
+
modal.render = function () {
|
|
274
|
+
const result = render.call(this)
|
|
275
|
+
if (Array.isArray(result?.values)) result.values = result.values.map(toTsxSnippet)
|
|
276
|
+
return result
|
|
277
|
+
}
|
|
278
|
+
const updated = modal.updated
|
|
279
|
+
modal.updated = function (changed) {
|
|
280
|
+
updated?.call(this, changed)
|
|
281
|
+
const block = this.shadowRoot?.querySelector('umb-code-block')
|
|
282
|
+
if (block && block.getAttribute('language') !== 'TypeScript') {
|
|
283
|
+
block.setAttribute('language', 'TypeScript')
|
|
284
|
+
block.language = 'TypeScript'
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const onInit = async () => {
|
|
290
|
+
teachDialect(UmbCodeEditorElement.prototype)
|
|
291
|
+
// Registered lazily, so these wait for the editor to be opened
|
|
292
|
+
hideRazorInserts()
|
|
293
|
+
previewFieldsAsTsx()
|
|
294
|
+
|
|
295
|
+
const repository = UmbTemplateDetailRepository.prototype
|
|
296
|
+
const createScaffold = repository.createScaffold
|
|
297
|
+
repository.createScaffold = async function (preset) {
|
|
298
|
+
const result = await createScaffold.call(this, preset)
|
|
299
|
+
if (result.data) result.data.content = SCAFFOLD
|
|
300
|
+
return result
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const partialViews = UmbPartialViewDetailRepository.prototype
|
|
304
|
+
const createPartialViewScaffold = partialViews.createScaffold
|
|
305
|
+
partialViews.createScaffold = async function (preset) {
|
|
306
|
+
const result = await createPartialViewScaffold.call(this, preset)
|
|
307
|
+
// Started from a snippet it has content already; empty, it gets the skeleton
|
|
308
|
+
if (result.data && !result.data.content) {
|
|
309
|
+
const { data } = await PartialViewService.getPartialViewSnippetById({ path: { id: 'Empty' } })
|
|
310
|
+
if (data?.content) result.data.content = data.content
|
|
311
|
+
}
|
|
312
|
+
return result
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The workspace context is not exported; it is the module beside the package's
|
|
316
|
+
// entry, which the import map resolves to the very module Umbraco loads
|
|
317
|
+
const { UmbTemplateWorkspaceContext } = await import(
|
|
318
|
+
new URL(
|
|
319
|
+
'./workspace/template-workspace.context.js',
|
|
320
|
+
import.meta.resolve('@umbraco-cms/backoffice/template'),
|
|
321
|
+
).href
|
|
322
|
+
)
|
|
323
|
+
const workspace = UmbTemplateWorkspaceContext.prototype
|
|
324
|
+
const setLayoutTemplate = workspace.setLayoutTemplate
|
|
325
|
+
workspace.setLayoutTemplate = async function (unique, updateLayoutBlock) {
|
|
326
|
+
// Umbraco's own update writes Razor; the state it keeps is what we want
|
|
327
|
+
await setLayoutTemplate.call(this, unique, false)
|
|
328
|
+
if (!updateLayoutBlock) return unique
|
|
329
|
+
const items = unique ? (await this.itemRepository.requestItems([unique])).data : []
|
|
330
|
+
const content = this._data.getCurrent()?.content ?? ''
|
|
331
|
+
this._data.updateCurrent({
|
|
332
|
+
content: withLayout(content, items?.[0]?.alias ?? null),
|
|
333
|
+
layoutTemplate: unique ? { unique } : null,
|
|
334
|
+
})
|
|
335
|
+
return unique
|
|
336
|
+
}
|
|
337
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TSX a view gets in place of the Razor Umbraco's template editor inserts.
|
|
3
|
+
*
|
|
4
|
+
* Umbraco's snippet builders (`templating/utils`) are module-private, so they
|
|
5
|
+
* cannot be replaced; what they produce is translated instead, on its way into a
|
|
6
|
+
* view's editor. Each builder has one fixed output shape, matched exactly here,
|
|
7
|
+
* and anything that does not match is returned as it came.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const quote = (text) => `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
11
|
+
|
|
12
|
+
const FALLBACKS = {
|
|
13
|
+
'Fallback.To(Fallback.Ancestors, Fallback.DefaultValue)': 'ancestors',
|
|
14
|
+
'Fallback.ToAncestors': 'ancestors',
|
|
15
|
+
'Fallback.ToDefaultValue': 'defaultValue',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FIELD =
|
|
19
|
+
/^@Model\.Value\("([^"]*)"(?:, fallback: (Fallback\.To\(Fallback\.Ancestors, Fallback\.DefaultValue\)|Fallback\.ToAncestors|Fallback\.ToDefaultValue))?(?:, defaultValue: \(object\)"([\s\S]*)")?\)$/
|
|
20
|
+
|
|
21
|
+
const DICTIONARY = /^@Umbraco\.GetDictionaryValue\("([\s\S]*)"\)$/
|
|
22
|
+
|
|
23
|
+
const QUERY =
|
|
24
|
+
/^\n@\{\n\tvar selection = ([\s\S]*);\n\}\n<ul>\n\t@foreach \(var item in selection\)[\s\S]*<\/ul>\n\n$/
|
|
25
|
+
|
|
26
|
+
/** `@Model.Value(…)` as `{model.text(…)}`, with its fallback and default. */
|
|
27
|
+
function field(match) {
|
|
28
|
+
const [, alias, fallback, defaultValue] = match
|
|
29
|
+
const options = []
|
|
30
|
+
if (fallback) options.push(`fallback: '${FALLBACKS[fallback]}'`)
|
|
31
|
+
if (defaultValue !== undefined) options.push(`default: ${quote(defaultValue)}`)
|
|
32
|
+
const args = options.length ? `${quote(alias)}, { ${options.join(', ')} }` : quote(alias)
|
|
33
|
+
return `{model.text(${args})}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The query builder's selection, listed as links the way Umbraco's snippet lists them. */
|
|
37
|
+
function query(match) {
|
|
38
|
+
const chain = match[1].replace(/^const selection = /, '').split('\n')
|
|
39
|
+
return [
|
|
40
|
+
'<ul>',
|
|
41
|
+
` {${chain[0]}`,
|
|
42
|
+
...chain.slice(1).map((line) => ` ${line}`),
|
|
43
|
+
' .map((item) => (',
|
|
44
|
+
' <li>',
|
|
45
|
+
' <a href={item.url}>{item.name}</a>',
|
|
46
|
+
' </li>',
|
|
47
|
+
' ))}',
|
|
48
|
+
'</ul>',
|
|
49
|
+
'',
|
|
50
|
+
].join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `snippet` as TSX when it is one of Umbraco's Razor snippets; otherwise unchanged. */
|
|
54
|
+
export function toTsxSnippet(snippet) {
|
|
55
|
+
if (typeof snippet !== 'string') return snippet
|
|
56
|
+
const fieldMatch = FIELD.exec(snippet)
|
|
57
|
+
if (fieldMatch) return field(fieldMatch)
|
|
58
|
+
const dictionaryMatch = DICTIONARY.exec(snippet)
|
|
59
|
+
if (dictionaryMatch) return `{dictionary(${quote(dictionaryMatch[1])})}`
|
|
60
|
+
const queryMatch = QUERY.exec(snippet)
|
|
61
|
+
if (queryMatch) return query(queryMatch)
|
|
62
|
+
return snippet
|
|
63
|
+
}
|