@dfosco/storyboard 0.6.8 → 0.6.9
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/package.json
CHANGED
|
@@ -11,11 +11,27 @@
|
|
|
11
11
|
* Schemas may declare `field.dynamic = 'prototypes'`. Pass
|
|
12
12
|
* `dynamicOptions={{ prototypes: ['my-app', ...] }}` to populate them.
|
|
13
13
|
*/
|
|
14
|
-
import { useState, useMemo, useEffect } from 'react'
|
|
14
|
+
import { useState, useMemo, useEffect, useRef } from 'react'
|
|
15
15
|
import { FormControl, Button, TextInput, Textarea, Flash, Text, ActionMenu, ActionList } from '@primer/react'
|
|
16
16
|
import { ARTIFACT_SCHEMAS, validateArtifact } from './artifactSchemas.js'
|
|
17
17
|
import styles from './ArtifactForm.module.css'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Slugify a freeform string into the kebab-case form the `name` field expects.
|
|
21
|
+
* Mirrors the NAME_PATTERN in artifactSchemas.js — lowercase letters, digits,
|
|
22
|
+
* and hyphens. Used to auto-derive `name` from `title` while the user types,
|
|
23
|
+
* until they manually edit `name` themselves.
|
|
24
|
+
*/
|
|
25
|
+
function slugifyName(value) {
|
|
26
|
+
return String(value || '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.normalize('NFKD')
|
|
29
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
30
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
31
|
+
.replace(/^-+|-+$/g, '')
|
|
32
|
+
.slice(0, 64)
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
function TypeSelector({ selected, onChange }) {
|
|
20
36
|
const types = Object.entries(ARTIFACT_SCHEMAS)
|
|
21
37
|
const current = ARTIFACT_SCHEMAS[selected]
|
|
@@ -150,18 +166,66 @@ export default function ArtifactForm({
|
|
|
150
166
|
const [errors, setErrors] = useState({})
|
|
151
167
|
const [submitting, setSubmitting] = useState(false)
|
|
152
168
|
|
|
169
|
+
// Tracks whether the `name` field should keep auto-following `title`.
|
|
170
|
+
// Flips to false the first time the user edits `name` directly, so we don't
|
|
171
|
+
// clobber their input. Reset on type change or unmount.
|
|
172
|
+
const nameAutoFollowRef = useRef(true)
|
|
173
|
+
|
|
153
174
|
// Reset form when fixed type changes (parent-controlled)
|
|
154
175
|
useEffect(() => {
|
|
155
176
|
if (!fixedType) return
|
|
156
177
|
setValues({ ...initialValues(ARTIFACT_SCHEMAS[fixedType]), ...(initialOverride || {}) })
|
|
157
178
|
setErrors({})
|
|
158
179
|
setSubmitting(false)
|
|
180
|
+
nameAutoFollowRef.current = true
|
|
159
181
|
}, [fixedType])
|
|
160
182
|
|
|
183
|
+
// Merge late-arriving initial overrides (e.g. a gh-login fetch that resolves
|
|
184
|
+
// after mount) into empty fields. Only fills fields the user hasn't typed
|
|
185
|
+
// into yet, so we never overwrite their input. Stringify the override so the
|
|
186
|
+
// dep check fires on content changes rather than parent re-render identity.
|
|
187
|
+
const overrideKey = useMemo(() => JSON.stringify(initialOverride || {}), [initialOverride])
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!initialOverride || !schema) return
|
|
190
|
+
setValues(prev => {
|
|
191
|
+
let next = prev
|
|
192
|
+
for (const [k, v] of Object.entries(initialOverride)) {
|
|
193
|
+
if (v == null || v === '') continue
|
|
194
|
+
const fieldExists = schema.fields.some(f => f.name === k)
|
|
195
|
+
const isEmpty = prev[k] === '' || prev[k] == null
|
|
196
|
+
if (fieldExists && isEmpty) {
|
|
197
|
+
if (next === prev) next = { ...prev }
|
|
198
|
+
next[k] = v
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return next
|
|
202
|
+
})
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
}, [overrideKey, schema])
|
|
205
|
+
|
|
206
|
+
// Derive `name` (kebab-case) from `title` as the user types, until they
|
|
207
|
+
// manually edit `name`. Only applies to schemas that expose both fields
|
|
208
|
+
// (prototype, canvas, flow). Component uses PascalCase and has no title,
|
|
209
|
+
// so it's naturally excluded.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!schema || !nameAutoFollowRef.current) return
|
|
212
|
+
const hasName = schema.fields.some(f => f.name === 'name' && f.type === 'text')
|
|
213
|
+
const hasTitle = schema.fields.some(f => f.name === 'title')
|
|
214
|
+
if (!hasName || !hasTitle) return
|
|
215
|
+
const derived = slugifyName(values.title)
|
|
216
|
+
if (derived === (values.name || '')) return
|
|
217
|
+
setValues(prev => ({ ...prev, name: derived }))
|
|
218
|
+
if (errors.name) {
|
|
219
|
+
setErrors(prev => { const next = { ...prev }; delete next.name; return next })
|
|
220
|
+
}
|
|
221
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
222
|
+
}, [values.title, schema])
|
|
223
|
+
|
|
161
224
|
function handleTypeChange(newType) {
|
|
162
225
|
setSelectedType(newType)
|
|
163
226
|
setValues(initialValues(ARTIFACT_SCHEMAS[newType]))
|
|
164
227
|
setErrors({})
|
|
228
|
+
nameAutoFollowRef.current = true
|
|
165
229
|
}
|
|
166
230
|
|
|
167
231
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
@@ -205,6 +269,9 @@ export default function ArtifactForm({
|
|
|
205
269
|
}
|
|
206
270
|
|
|
207
271
|
function handleChange(fieldName, value) {
|
|
272
|
+
if (fieldName === 'name') {
|
|
273
|
+
nameAutoFollowRef.current = false
|
|
274
|
+
}
|
|
208
275
|
setValues(prev => ({ ...prev, [fieldName]: value }))
|
|
209
276
|
if (errors[fieldName]) {
|
|
210
277
|
setErrors(prev => { const next = { ...prev }; delete next[fieldName]; return next })
|
|
@@ -27,6 +27,7 @@ export default function CreateDialog({ type, basePath, onClose }) {
|
|
|
27
27
|
const schema = schemaKey ? ARTIFACT_SCHEMAS[schemaKey] : null
|
|
28
28
|
const [prototypes, setPrototypes] = useState([])
|
|
29
29
|
const [partials, setPartials] = useState([])
|
|
30
|
+
const [ghLogin, setGhLogin] = useState(null)
|
|
30
31
|
|
|
31
32
|
const needsPrototypes = useMemo(() => {
|
|
32
33
|
if (!schema) return false
|
|
@@ -38,6 +39,11 @@ export default function CreateDialog({ type, basePath, onClose }) {
|
|
|
38
39
|
return schema.fields.some(f => f.dynamic === 'partials')
|
|
39
40
|
}, [schema])
|
|
40
41
|
|
|
42
|
+
const needsAuthor = useMemo(() => {
|
|
43
|
+
if (!schema) return false
|
|
44
|
+
return schema.fields.some(f => f.name === 'author')
|
|
45
|
+
}, [schema])
|
|
46
|
+
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
if (!needsPrototypes || !schemaKey) return
|
|
43
49
|
const apiBase = (basePath || '/').replace(/\/+$/, '')
|
|
@@ -68,6 +74,25 @@ export default function CreateDialog({ type, basePath, onClose }) {
|
|
|
68
74
|
.catch(() => {})
|
|
69
75
|
}, [needsPartials, schemaKey, basePath])
|
|
70
76
|
|
|
77
|
+
// Fetch the gh CLI login once per dialog open and prefill it as the default
|
|
78
|
+
// author when the schema supports it (currently only `prototype`).
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!needsAuthor || !schemaKey) return
|
|
81
|
+
const apiBase = (basePath || '/').replace(/\/+$/, '')
|
|
82
|
+
fetch(`${apiBase}/_storyboard/git-user`)
|
|
83
|
+
.then(r => (r.ok ? r.json() : null))
|
|
84
|
+
.then(data => { if (data?.login) setGhLogin(data.login) })
|
|
85
|
+
.catch(() => {})
|
|
86
|
+
}, [needsAuthor, schemaKey, basePath])
|
|
87
|
+
|
|
88
|
+
// Build the initial values object for the form. Late-arriving values (like
|
|
89
|
+
// the gh login fetch above) get merged into empty fields inside ArtifactForm
|
|
90
|
+
// — see the override-merge effect there.
|
|
91
|
+
const initialValues = useMemo(() => {
|
|
92
|
+
if (!needsAuthor || !ghLogin) return undefined
|
|
93
|
+
return { author: ghLogin }
|
|
94
|
+
}, [needsAuthor, ghLogin])
|
|
95
|
+
|
|
71
96
|
if (!schemaKey || !schema) return null
|
|
72
97
|
|
|
73
98
|
async function handleSubmit({ type: t, values }) {
|
|
@@ -120,6 +145,7 @@ export default function CreateDialog({ type, basePath, onClose }) {
|
|
|
120
145
|
onSubmit={handleSubmit}
|
|
121
146
|
onCancel={onClose}
|
|
122
147
|
dynamicOptions={{ prototypes, partials }}
|
|
148
|
+
initialValues={initialValues}
|
|
123
149
|
hideHeader
|
|
124
150
|
/>
|
|
125
151
|
</Dialog>
|