@architecture-model/editable-metadata 0.3.0 → 0.4.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/Metadata.astro +12 -7
- package/People.astro +33 -0
- package/PeopleTable.astro +37 -0
- package/README.md +50 -2
- package/editor.js +3 -3
- package/fields.js +20 -0
- package/index.js +6 -2
- package/package.json +8 -2
- package/references.js +9 -0
package/Metadata.astro
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import config from 'virtual:editable-metadata/config'
|
|
4
2
|
import Editor from './Editor.jsx'
|
|
3
|
+
import { fieldOf, people } from './fields.js'
|
|
5
4
|
import { model } from './model.js'
|
|
6
5
|
|
|
7
6
|
interface Props {
|
|
@@ -11,18 +10,17 @@ interface Props {
|
|
|
11
10
|
|
|
12
11
|
type Option = string | { value: string; label?: string }
|
|
13
12
|
|
|
14
|
-
const types = ['text', 'number', 'date', 'url', 'boolean', 'select', 'multiselect']
|
|
13
|
+
const types = ['text', 'number', 'date', 'url', 'boolean', 'select', 'multiselect', 'people']
|
|
15
14
|
|
|
16
15
|
const { of, name } = Astro.props
|
|
17
|
-
const
|
|
18
|
-
const field: { type: string; options?: Option[] } = fields[name] ?? { type: 'text' }
|
|
16
|
+
const field: { type: string; options?: Option[] } = fieldOf(name)
|
|
19
17
|
if (!types.includes(field.type)) {
|
|
20
18
|
throw new Error(`Unknown type "${field.type}" for metadata "${name}". Known types: ${types.join(', ')}`)
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
const valuesOf = (fqn: string) => [model.element(fqn).getMetadata(name) ?? []].flat()
|
|
24
22
|
const values = valuesOf(of)
|
|
25
|
-
const listed = (field.options ?? []).map((option) =>
|
|
23
|
+
const listed = ((field.type === 'people' ? people() : field.options) ?? []).map((option: Option) =>
|
|
26
24
|
typeof option === 'string' ? { value: option, label: option } : { value: option.value, label: option.label ?? option.value },
|
|
27
25
|
)
|
|
28
26
|
const labels = new Map(listed.map((option) => [option.value, option.label]))
|
|
@@ -39,7 +37,14 @@ const text =
|
|
|
39
37
|
|
|
40
38
|
{
|
|
41
39
|
import.meta.env.DEV ? (
|
|
42
|
-
<Editor
|
|
40
|
+
<Editor
|
|
41
|
+
client:load
|
|
42
|
+
fqn={of}
|
|
43
|
+
name={name}
|
|
44
|
+
type={field.type === 'people' ? 'multiselect' : field.type}
|
|
45
|
+
values={values}
|
|
46
|
+
options={options}
|
|
47
|
+
/>
|
|
43
48
|
) : field.type === 'url' && text ? (
|
|
44
49
|
<a href={text}>{text}</a>
|
|
45
50
|
) : (
|
package/People.astro
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { peopleFields } from './fields.js'
|
|
3
|
+
import Metadata from './Metadata.astro'
|
|
4
|
+
import { model } from './model.js'
|
|
5
|
+
import { referencedBy } from './references.js'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
of: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { of } = Astro.props
|
|
12
|
+
const users = referencedBy(model.element(of))
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<table>
|
|
16
|
+
<tbody>
|
|
17
|
+
{
|
|
18
|
+
Object.entries(peopleFields).map(([key, label]) => (
|
|
19
|
+
<tr>
|
|
20
|
+
<th>{label}</th>
|
|
21
|
+
<td>
|
|
22
|
+
<Metadata of={of} name={key} />
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
))
|
|
26
|
+
}
|
|
27
|
+
<tr><th>Referenced by</th><td>{users.join(', ')}</td></tr>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Last confirmed</th>
|
|
30
|
+
<td><Metadata of={of} name="peopleConfirmed" /></td>
|
|
31
|
+
</tr>
|
|
32
|
+
</tbody>
|
|
33
|
+
</table>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { peopleFields } from './fields.js'
|
|
3
|
+
import Metadata from './Metadata.astro'
|
|
4
|
+
import { model } from './model.js'
|
|
5
|
+
import { referencedBy } from './references.js'
|
|
6
|
+
|
|
7
|
+
const systems = [...model.elements()].filter((element) => !element.parent && element.shape !== 'person')
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<table>
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>System</th>
|
|
14
|
+
{Object.values(peopleFields).map((label) => <th>{label}</th>)}
|
|
15
|
+
<th>Referenced by</th>
|
|
16
|
+
<th>Last confirmed</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
{
|
|
21
|
+
systems.map((system) => (
|
|
22
|
+
<tr>
|
|
23
|
+
<td>{system.title}</td>
|
|
24
|
+
{Object.keys(peopleFields).map((key) => (
|
|
25
|
+
<td>
|
|
26
|
+
<Metadata of={system.id} name={key} />
|
|
27
|
+
</td>
|
|
28
|
+
))}
|
|
29
|
+
<td>{referencedBy(system).join(', ')}</td>
|
|
30
|
+
<td>
|
|
31
|
+
<Metadata of={system.id} name="peopleConfirmed" />
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
))
|
|
35
|
+
}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The editors are React components built on [Radix](https://www.radix-ui.com/) and
|
|
|
53
53
|
|
|
54
54
|
## Field types
|
|
55
55
|
|
|
56
|
-
Without configuration every key is text. To give keys other types, create `model/metadata.json`:
|
|
56
|
+
Without configuration every key is text, except the [people keys](#people). To give keys other types, create `model/metadata.json`:
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
@@ -70,6 +70,7 @@ Without configuration every key is text. To give keys other types, create `model
|
|
|
70
70
|
| `boolean` | switch | `'true'` or `'false'`, shown as Yes or No |
|
|
71
71
|
| `select` | searchable list of the options | string |
|
|
72
72
|
| `multiselect` | searchable list of the options, several can be checked | list of strings |
|
|
73
|
+
| `people` | searchable list of the people in `model/people.json`, several can be checked | list of handles |
|
|
73
74
|
|
|
74
75
|
The options of `select` and `multiselect` are the ones listed plus every value already in use in the model. A `url` is a link in the built site.
|
|
75
76
|
|
|
@@ -96,6 +97,52 @@ Payments calls us back: <Derived of="payments" value={callsBack} />
|
|
|
96
97
|
|
|
97
98
|
A function returns a string, a number, a list, or a boolean, shown as Yes or No. A derived value is never editable.
|
|
98
99
|
|
|
100
|
+
## People
|
|
101
|
+
|
|
102
|
+
"Owner" rarely tells you whom to ask. Each system, internal or external, can say whom to go to for which job:
|
|
103
|
+
|
|
104
|
+
| Key | Who | Ask them when |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `integratedBy` | integrated it originally | you need to know why it works the way it does |
|
|
107
|
+
| `experts` | know best how it works and how its integration is changed | there is an incident, an API or contract change, an upgrade, or a change to review |
|
|
108
|
+
| `contributors` | have worked on it | you need reviewers or extra hands, or the experts are away |
|
|
109
|
+
| `administrators` | manage it: for an external service, the vendor account, keys, billing and support; for our own, its accounts and access | credentials need rotating, someone needs access, or a support ticket or plan change is due |
|
|
110
|
+
| `peopleConfirmed` | | the date someone last checked these; older than about six months, check again |
|
|
111
|
+
|
|
112
|
+
The four people keys are `people` fields without any configuration. Each stores handles from one shared list, `model/people.json`, so a person is added once and every field picks from it:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
[
|
|
116
|
+
{ "value": "igor", "label": "Igor Morozov" },
|
|
117
|
+
{ "value": "dana", "label": "Dana Kim" }
|
|
118
|
+
]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
stripe = external 'Stripe' {
|
|
123
|
+
metadata {
|
|
124
|
+
integratedBy 'igor'
|
|
125
|
+
experts 'dana'
|
|
126
|
+
contributors ['igor', 'dana']
|
|
127
|
+
administrators 'dana'
|
|
128
|
+
peopleConfirmed '2026-10-17'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`People` shows them for one system, with the systems and containers that reference it, worked out from the model's relationships. `PeopleTable` shows the same for every system:
|
|
134
|
+
|
|
135
|
+
```mdx
|
|
136
|
+
import People from '@architecture-model/editable-metadata/People.astro'
|
|
137
|
+
import PeopleTable from '@architecture-model/editable-metadata/PeopleTable.astro'
|
|
138
|
+
|
|
139
|
+
<People of="stripe" />
|
|
140
|
+
|
|
141
|
+
<PeopleTable />
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In the dev server the people fields are click-to-edit, picking from the list. A new person is added to `model/people.json` first. Someone who has left keeps their entry, with `"left": "2026-05-01"`, so their handle still shows as their name and you can [find the systems whose people have left](https://arch.morozov.is/recipes/people-who-left/). [Model who knows and manages each system](https://arch.morozov.is/people/) says which questions the people fields let the model answer, and why.
|
|
145
|
+
|
|
99
146
|
## Properties
|
|
100
147
|
|
|
101
148
|
`Property` shows and edits an element's own `title`, `summary`, `description`, `technology` or `links`:
|
|
@@ -120,10 +167,11 @@ A title can't be emptied. Links replace the element's own links, and a row witho
|
|
|
120
167
|
editableMetadata({
|
|
121
168
|
model: 'model', // folder with the .c4 files
|
|
122
169
|
fields: 'model/metadata.json', // field types
|
|
170
|
+
people: 'model/people.json', // the people the people fields pick from
|
|
123
171
|
})
|
|
124
172
|
```
|
|
125
173
|
|
|
126
|
-
|
|
174
|
+
The paths are relative to the Astro project root.
|
|
127
175
|
|
|
128
176
|
## How it saves
|
|
129
177
|
|
package/editor.js
CHANGED
|
@@ -49,14 +49,14 @@ export async function setProperty(dir, { fqn, property, value }) {
|
|
|
49
49
|
await update(dir, fqn, { [property]: token }, asMarkdown(token, text))
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const editor = ({ modelDir, fieldsFile }) => ({
|
|
52
|
+
export const editor = ({ modelDir, fieldsFile, peopleFile }) => ({
|
|
53
53
|
name: 'editable-metadata-editor',
|
|
54
54
|
apply: 'serve',
|
|
55
55
|
configureServer(server) {
|
|
56
56
|
// Pages load the model once per module evaluation, so a changed model has to evict what was evaluated.
|
|
57
|
-
server.watcher.add([modelDir, fieldsFile])
|
|
57
|
+
server.watcher.add([modelDir, fieldsFile, peopleFile])
|
|
58
58
|
server.watcher.on('change', (file) => {
|
|
59
|
-
if (!file.endsWith('.c4') && file !== fieldsFile) return
|
|
59
|
+
if (!file.endsWith('.c4') && file !== fieldsFile && file !== peopleFile) return
|
|
60
60
|
for (const environment of Object.values(server.environments)) environment.moduleGraph.invalidateAll()
|
|
61
61
|
server.ws.send({ type: 'full-reload' })
|
|
62
62
|
})
|
package/fields.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import config from 'virtual:editable-metadata/config'
|
|
3
|
+
|
|
4
|
+
export const peopleFields = {
|
|
5
|
+
integratedBy: 'Integrated by',
|
|
6
|
+
experts: 'Experts',
|
|
7
|
+
contributors: 'Contributors',
|
|
8
|
+
administrators: 'Administrators',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const defaults = {
|
|
12
|
+
...Object.fromEntries(Object.keys(peopleFields).map((key) => [key, { type: 'people' }])),
|
|
13
|
+
peopleConfirmed: { type: 'date' },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const read = (file, fallback) => (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : fallback)
|
|
17
|
+
|
|
18
|
+
export const fieldOf = (name) => read(config.fieldsFile, {})[name] ?? defaults[name] ?? { type: 'text' }
|
|
19
|
+
|
|
20
|
+
export const people = () => read(config.peopleFile, [])
|
package/index.js
CHANGED
|
@@ -19,13 +19,17 @@ const withoutEditor = {
|
|
|
19
19
|
},
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export default function editableMetadata({
|
|
22
|
+
export default function editableMetadata({
|
|
23
|
+
model = 'model',
|
|
24
|
+
fields = 'model/metadata.json',
|
|
25
|
+
people = 'model/people.json',
|
|
26
|
+
} = {}) {
|
|
23
27
|
return {
|
|
24
28
|
name,
|
|
25
29
|
hooks: {
|
|
26
30
|
'astro:config:setup': ({ config, command, updateConfig }) => {
|
|
27
31
|
const root = fileURLToPath(config.root)
|
|
28
|
-
const paths = { modelDir: resolve(root, model), fieldsFile: resolve(root, fields) }
|
|
32
|
+
const paths = { modelDir: resolve(root, model), fieldsFile: resolve(root, fields), peopleFile: resolve(root, people) }
|
|
29
33
|
const hasReact = config.integrations.some((integration) => integration.name === '@astrojs/react')
|
|
30
34
|
updateConfig({
|
|
31
35
|
integrations: hasReact ? [] : [react()],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@architecture-model/editable-metadata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Show LikeC4 element metadata and properties in Astro pages and edit them in place in the dev server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro-integration",
|
|
@@ -21,15 +21,21 @@
|
|
|
21
21
|
".": "./index.js",
|
|
22
22
|
"./Metadata.astro": "./Metadata.astro",
|
|
23
23
|
"./Property.astro": "./Property.astro",
|
|
24
|
-
"./Derived.astro": "./Derived.astro"
|
|
24
|
+
"./Derived.astro": "./Derived.astro",
|
|
25
|
+
"./People.astro": "./People.astro",
|
|
26
|
+
"./PeopleTable.astro": "./PeopleTable.astro"
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
27
29
|
"index.js",
|
|
28
30
|
"editor.js",
|
|
29
31
|
"model.js",
|
|
32
|
+
"fields.js",
|
|
33
|
+
"references.js",
|
|
30
34
|
"Metadata.astro",
|
|
31
35
|
"Property.astro",
|
|
32
36
|
"Derived.astro",
|
|
37
|
+
"People.astro",
|
|
38
|
+
"PeopleTable.astro",
|
|
33
39
|
"Editor.jsx",
|
|
34
40
|
"NoEditor.jsx",
|
|
35
41
|
"editor.css"
|
package/references.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const systemOf = (element) => (element.parent ? systemOf(element.parent) : element)
|
|
2
|
+
|
|
3
|
+
export function referencedBy(element) {
|
|
4
|
+
const sources = [...element.incoming('all')]
|
|
5
|
+
.map((relationship) => relationship.source)
|
|
6
|
+
.filter((source) => source.id !== element.id && !source.isDescendantOf(element) && source.shape !== 'person')
|
|
7
|
+
const titles = sources.map((source) => (source.parent ? `${source.title} (${systemOf(source).title})` : source.title))
|
|
8
|
+
return [...new Set(titles)]
|
|
9
|
+
}
|