@scaleflex/template-builder 0.1.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/.claude/skills/integrate-template-builder/SKILL.md +356 -0
- package/CHANGELOG.md +66 -0
- package/LICENSE +50 -0
- package/README.md +775 -0
- package/dist/define.cjs +2 -0
- package/dist/define.cjs.map +1 -0
- package/dist/define.d.ts +2 -0
- package/dist/define.js +6 -0
- package/dist/define.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +237 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.ts +95 -0
- package/dist/react.js +59 -0
- package/dist/react.js.map +1 -0
- package/dist/template-builder-CSyPZni9.cjs +52 -0
- package/dist/template-builder-CSyPZni9.cjs.map +1 -0
- package/dist/template-builder-S33H_d5T.js +354 -0
- package/dist/template-builder-S33H_d5T.js.map +1 -0
- package/dist/template-builder.d.ts +206 -0
- package/package.json +75 -0
- package/src/define.ts +10 -0
- package/src/index.ts +9 -0
- package/src/protocol.ts +298 -0
- package/src/react.ts +223 -0
- package/src/template-builder.ts +599 -0
package/src/react.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import './define'
|
|
11
|
+
import type { SfxTemplateBuilder } from './template-builder'
|
|
12
|
+
import type { TemplateBuilderSaveDetail } from './template-builder'
|
|
13
|
+
import type { BuilderDirtyData, BuilderErrorData, BuilderTheme } from './protocol'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hub session — the full-featured credential.
|
|
17
|
+
*/
|
|
18
|
+
export interface TemplateBuilderSessionAuth {
|
|
19
|
+
sassKey: string
|
|
20
|
+
sessionUuid: string
|
|
21
|
+
companyUuid?: string
|
|
22
|
+
projectUuid?: string
|
|
23
|
+
secTemplate?: never
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Filerobot security template — a guest credential for hosts that have no Hub
|
|
28
|
+
* session to hand over. `stateless` is required rather than merely implied:
|
|
29
|
+
* the app takes a security template on the stateless embed route only, so the
|
|
30
|
+
* combination is a compile-time error instead of a runtime one.
|
|
31
|
+
*
|
|
32
|
+
* `companyUuid` / `projectUuid` are absent by design — they name a Hub project
|
|
33
|
+
* that cannot be looked up without a session.
|
|
34
|
+
*/
|
|
35
|
+
export interface TemplateBuilderSecTemplateAuth {
|
|
36
|
+
secTemplate: string
|
|
37
|
+
stateless: true
|
|
38
|
+
sassKey?: never
|
|
39
|
+
sessionUuid?: never
|
|
40
|
+
companyUuid?: never
|
|
41
|
+
projectUuid?: never
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TemplateBuilderBaseProps {
|
|
45
|
+
baseUrl: string
|
|
46
|
+
token: string
|
|
47
|
+
templateId?: string
|
|
48
|
+
mode?: 'inline' | 'modal'
|
|
49
|
+
/** Hand the template in and take it back out instead of using the DAM. */
|
|
50
|
+
stateless?: boolean
|
|
51
|
+
/** Stateless mode: the template to edit, as `.fdt` XML. */
|
|
52
|
+
content?: string
|
|
53
|
+
/** Stateless mode: display name for the editor header. */
|
|
54
|
+
templateName?: string
|
|
55
|
+
/**
|
|
56
|
+
* Stateless mode: the `template_query` to open on — the value handed back on
|
|
57
|
+
* save. Reopens the template on the same layout and variable values; empty
|
|
58
|
+
* falls back to the XML's own `default=` attributes.
|
|
59
|
+
*/
|
|
60
|
+
templateQuery?: string
|
|
61
|
+
/** Accent colour for the editor chrome, as `#rgb` / `#rrggbb`. */
|
|
62
|
+
brandColor?: string
|
|
63
|
+
/** Colour scheme for the editor chrome. */
|
|
64
|
+
theme?: BuilderTheme
|
|
65
|
+
readyTimeout?: number
|
|
66
|
+
className?: string
|
|
67
|
+
style?: CSSProperties
|
|
68
|
+
onReady?: () => void
|
|
69
|
+
onOpen?: () => void
|
|
70
|
+
onClose?: () => void
|
|
71
|
+
/**
|
|
72
|
+
* Fired on save. In stateless mode the detail carries the edited `content`
|
|
73
|
+
* for you to persist; otherwise it reports the uuid the app uploaded to.
|
|
74
|
+
*
|
|
75
|
+
* In stateless mode the outcome is reported back to the editor: return (or
|
|
76
|
+
* resolve to) `false`, or throw, and the editor restores its unsaved-changes
|
|
77
|
+
* flag and tells the user the save failed. Anything else counts as persisted.
|
|
78
|
+
*/
|
|
79
|
+
onSave?: (
|
|
80
|
+
data: TemplateBuilderSaveDetail,
|
|
81
|
+
) => void | boolean | Promise<void | boolean>
|
|
82
|
+
onError?: (data: BuilderErrorData) => void
|
|
83
|
+
/**
|
|
84
|
+
* Stateless mode: unsaved-changes flag changed. Use it to prompt before
|
|
85
|
+
* swapping `content`, which discards in-progress edits.
|
|
86
|
+
*/
|
|
87
|
+
onDirtyChange?: (data: BuilderDirtyData) => void
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type TemplateBuilderProps = TemplateBuilderBaseProps &
|
|
91
|
+
(TemplateBuilderSessionAuth | TemplateBuilderSecTemplateAuth)
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* React wrapper around `<sfx-template-builder>`. Props are assigned as
|
|
95
|
+
* element properties via ref (works on React 18 and 19 alike); callbacks
|
|
96
|
+
* subscribe to the element's CustomEvents.
|
|
97
|
+
*
|
|
98
|
+
* Forwards a ref to the underlying element, which is the only way to reach the
|
|
99
|
+
* imperative API — `open()` in particular, without which `mode="modal"` can
|
|
100
|
+
* never be shown:
|
|
101
|
+
*
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const builder = useRef<SfxTemplateBuilder>(null)
|
|
104
|
+
* <TemplateBuilder ref={builder} mode="modal" … />
|
|
105
|
+
* <button onClick={() => builder.current?.open('tpl-1')}>Edit</button>
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* `forwardRef` rather than a plain `ref` prop: React 19 accepts the latter for
|
|
109
|
+
* function components, React 18 does not, and both are supported peers.
|
|
110
|
+
*/
|
|
111
|
+
export const TemplateBuilder = forwardRef<
|
|
112
|
+
SfxTemplateBuilder,
|
|
113
|
+
TemplateBuilderProps
|
|
114
|
+
>(function TemplateBuilder(props, forwardedRef): ReactElement {
|
|
115
|
+
const {
|
|
116
|
+
className,
|
|
117
|
+
style,
|
|
118
|
+
onReady,
|
|
119
|
+
onOpen,
|
|
120
|
+
onClose,
|
|
121
|
+
onSave,
|
|
122
|
+
onError,
|
|
123
|
+
onDirtyChange,
|
|
124
|
+
...config
|
|
125
|
+
} = props
|
|
126
|
+
const ref = useRef<SfxTemplateBuilder>(null)
|
|
127
|
+
|
|
128
|
+
// Hand the same element out to the caller without giving up the internal ref
|
|
129
|
+
// the effects below rely on.
|
|
130
|
+
useImperativeHandle(forwardedRef, () => ref.current as SfxTemplateBuilder, [])
|
|
131
|
+
|
|
132
|
+
// Assign config before paint so the iframe doesn't first mount with defaults.
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
const el = ref.current
|
|
135
|
+
if (!el) return
|
|
136
|
+
el.baseUrl = config.baseUrl
|
|
137
|
+
el.token = config.token
|
|
138
|
+
el.sassKey = config.sassKey ?? ''
|
|
139
|
+
el.sessionUuid = config.sessionUuid ?? ''
|
|
140
|
+
el.secTemplate = config.secTemplate ?? ''
|
|
141
|
+
el.companyUuid = config.companyUuid ?? ''
|
|
142
|
+
el.projectUuid = config.projectUuid ?? ''
|
|
143
|
+
el.templateId = config.templateId ?? ''
|
|
144
|
+
el.mode = config.mode ?? 'inline'
|
|
145
|
+
el.stateless = config.stateless ?? false
|
|
146
|
+
el.templateName = config.templateName ?? ''
|
|
147
|
+
el.templateQuery = config.templateQuery ?? ''
|
|
148
|
+
el.brandColor = config.brandColor ?? ''
|
|
149
|
+
el.theme = config.theme ?? ''
|
|
150
|
+
// Assigned last: the element sends content to the app as soon as it has
|
|
151
|
+
// both a request and a value, so the id, name and query must already be
|
|
152
|
+
// set — all four ship as one message.
|
|
153
|
+
el.content = config.content ?? ''
|
|
154
|
+
if (config.readyTimeout !== undefined) el.readyTimeout = config.readyTimeout
|
|
155
|
+
}, [
|
|
156
|
+
config.baseUrl,
|
|
157
|
+
config.token,
|
|
158
|
+
config.sassKey,
|
|
159
|
+
config.sessionUuid,
|
|
160
|
+
config.secTemplate,
|
|
161
|
+
config.companyUuid,
|
|
162
|
+
config.projectUuid,
|
|
163
|
+
config.templateId,
|
|
164
|
+
config.mode,
|
|
165
|
+
config.stateless,
|
|
166
|
+
config.content,
|
|
167
|
+
config.templateName,
|
|
168
|
+
config.templateQuery,
|
|
169
|
+
config.brandColor,
|
|
170
|
+
config.theme,
|
|
171
|
+
config.readyTimeout,
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
// Layout effect, not passive: the element reports config errors (e.g.
|
|
175
|
+
// `invalid-base-url`) in a microtask queued during this same commit, and a
|
|
176
|
+
// passive effect would subscribe only after that microtask has fired —
|
|
177
|
+
// making a mount-time error unobservable from React. This effect is declared
|
|
178
|
+
// after the config one, so it still runs once the config is assigned.
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
const el = ref.current
|
|
181
|
+
if (!el) return
|
|
182
|
+
const subs: Array<[string, EventListener]> = []
|
|
183
|
+
const on = (name: string, handler?: (detail: never) => void) => {
|
|
184
|
+
if (!handler) return
|
|
185
|
+
const listener = ((e: CustomEvent) => handler(e.detail as never)) as EventListener
|
|
186
|
+
el.addEventListener(name, listener)
|
|
187
|
+
subs.push([name, listener])
|
|
188
|
+
}
|
|
189
|
+
on('ready', onReady)
|
|
190
|
+
on('open', onOpen)
|
|
191
|
+
on('close', onClose)
|
|
192
|
+
on('error', onError)
|
|
193
|
+
on('dirtychange', onDirtyChange)
|
|
194
|
+
|
|
195
|
+
// `save` is not just re-emitted: in stateless mode the handler's outcome
|
|
196
|
+
// is acked back, so a failed write on the host side doesn't leave the
|
|
197
|
+
// editor showing the template as saved. `confirmSave` no-ops in DAM mode.
|
|
198
|
+
if (onSave) {
|
|
199
|
+
const listener = ((e: CustomEvent<TemplateBuilderSaveDetail>) => {
|
|
200
|
+
// Wrapped in a promise so a synchronous throw is handled like a
|
|
201
|
+
// rejection, and a sync `false` like a resolved one.
|
|
202
|
+
Promise.resolve()
|
|
203
|
+
.then(() => onSave(e.detail))
|
|
204
|
+
.then((result) => el.confirmSave(result !== false))
|
|
205
|
+
.catch((err) => {
|
|
206
|
+
// No message: an internal error string is not something to put in
|
|
207
|
+
// front of the end user. The editor uses its own wording.
|
|
208
|
+
console.error('[sfx-template-builder] onSave failed:', err)
|
|
209
|
+
el.confirmSave(false)
|
|
210
|
+
})
|
|
211
|
+
}) as EventListener
|
|
212
|
+
el.addEventListener('save', listener)
|
|
213
|
+
subs.push(['save', listener])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
for (const [name, listener] of subs) el.removeEventListener(name, listener)
|
|
218
|
+
}
|
|
219
|
+
}, [onReady, onOpen, onClose, onSave, onError, onDirtyChange])
|
|
220
|
+
|
|
221
|
+
// eslint-disable-next-line react-hooks/refs -- ref is forwarded as a prop, not read during render
|
|
222
|
+
return createElement('sfx-template-builder', { ref, class: className, style })
|
|
223
|
+
})
|