@ossy/resources 1.12.2 → 3.0.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.
Files changed (89) hide show
  1. package/package.json +16 -4
  2. package/src/AudioResource.jsx +2 -2
  3. package/src/CreateDirectory.jsx +2 -10
  4. package/src/CreateDirectory.page.jsx +1 -1
  5. package/src/CreateDocument.jsx +2 -2
  6. package/src/CreateDocument.page.jsx +1 -1
  7. package/src/Definition.js +3 -2
  8. package/src/DocumentEdit.jsx +10 -38
  9. package/src/DocumentView.jsx +11 -22
  10. package/src/GenericResourceCard.jsx +25 -7
  11. package/src/GenericResourceDetail.jsx +6 -60
  12. package/src/GenericResourceForm.jsx +171 -37
  13. package/src/ImageResource.jsx +1 -1
  14. package/src/PDFResource.jsx +1 -1
  15. package/src/PlatformFileField.jsx +239 -0
  16. package/src/ResourceContentPage.jsx +5 -5
  17. package/src/ResourceDescription.jsx +1 -1
  18. package/src/ResourceDetails.jsx +3 -4
  19. package/src/ResourceDialogMove.jsx +3 -3
  20. package/src/ResourceFactory.jsx +11 -64
  21. package/src/ResourceGenericView.jsx +1 -1
  22. package/src/ResourceList.jsx +89 -106
  23. package/src/ResourcePanel.jsx +40 -57
  24. package/src/ResourceTags.jsx +1 -1
  25. package/src/ResourcesPage.jsx +26 -35
  26. package/src/SchemaDetailView.jsx +97 -0
  27. package/src/SchemaFormDefault.jsx +86 -0
  28. package/src/SchemaPresenter.jsx +207 -0
  29. package/src/Upload.jsx +6 -25
  30. package/src/Upload.page.jsx +1 -1
  31. package/src/UploadResources.jsx +2 -2
  32. package/src/VideoResource.jsx +1 -1
  33. package/src/access-filter.spec.js +1 -1
  34. package/src/create-page-view.action.js +1 -1
  35. package/src/create-page-view.task.js +10 -9
  36. package/src/create.action.js +1 -1
  37. package/src/create.task.js +27 -29
  38. package/src/delete.action.js +1 -1
  39. package/src/delete.task.js +9 -13
  40. package/src/en.translations.json +25 -24
  41. package/src/get-page-view-stats.action.js +1 -1
  42. package/src/get-page-view-stats.task.js +3 -4
  43. package/src/get-resource-alias.api.js +8 -0
  44. package/src/get-resource-info-alias.api.js +8 -0
  45. package/src/get-resource-info.api.js +36 -0
  46. package/src/get-resource-variant.api.js +8 -0
  47. package/src/get-resource.api.js +51 -0
  48. package/src/get.action.js +1 -1
  49. package/src/get.task.js +6 -6
  50. package/src/index.js +10 -0
  51. package/src/list.action.js +1 -1
  52. package/src/list.task.js +1 -1
  53. package/src/platform-file-field.component.jsx +5 -0
  54. package/src/platform-image-field.component.jsx +5 -0
  55. package/src/resource-create.page.jsx +5 -5
  56. package/src/resource-detail.page.jsx +1 -1
  57. package/src/resource-read.helpers.js +151 -0
  58. package/src/resource-stream.helpers.js +20 -0
  59. package/src/resource-stream.js +108 -0
  60. package/src/resource.helpers.js +9 -9
  61. package/src/resources.attach-media-urls.js +9 -3
  62. package/src/resources.events.js +42 -81
  63. package/src/{resources.spec.js → resources.integration.spec.js} +19 -19
  64. package/src/resources.queries.js +5 -2
  65. package/src/schema-detail.component.jsx +24 -0
  66. package/src/schema-form-default.component.jsx +14 -0
  67. package/src/schemaFormSlots.js +6 -0
  68. package/src/schemaViewSlots.js +6 -0
  69. package/src/search.action.js +1 -1
  70. package/src/search.task.js +1 -1
  71. package/src/server.js +2 -1
  72. package/src/sv.translations.json +25 -24
  73. package/src/update-access.action.js +1 -1
  74. package/src/update-access.task.js +9 -8
  75. package/src/update-content.action.js +1 -1
  76. package/src/update-content.task.js +25 -27
  77. package/src/update-location.action.js +1 -1
  78. package/src/update-location.task.js +9 -8
  79. package/src/update-name.action.js +1 -1
  80. package/src/update-name.task.js +9 -8
  81. package/src/upload-named-version.action.js +1 -1
  82. package/src/upload-named-version.task.js +16 -22
  83. package/src/useActivePath.jsx +3 -8
  84. package/src/useDocumentValidator.js +16 -0
  85. package/src/useSchemaEngine.js +9 -0
  86. package/src/useSchemas.js +29 -0
  87. package/src/useSelectedResourceId.jsx +36 -0
  88. package/src/ResourceContentPage.stories.jsx +0 -19
  89. package/src/resource.aggregate.js +0 -86
package/package.json CHANGED
@@ -1,31 +1,43 @@
1
1
  {
2
2
  "name": "@ossy/resources",
3
3
  "description": "Resource domain — aggregate and events for the Ossy resource model",
4
- "version": "1.12.2",
4
+ "version": "3.0.1",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "./src/index.js",
8
8
  "module": "./src/index.js",
9
9
  "exports": {
10
10
  ".": "./src/index.js",
11
+ "./schemaViewSlots.js": "./src/schemaViewSlots.js",
12
+ "./schemaFormSlots.js": "./src/schemaFormSlots.js",
13
+ "./resource.helpers.js": "./src/resource.helpers.js",
11
14
  "./server": "./src/server.js"
12
15
  },
13
16
  "scripts": {
14
- "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose"
17
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
18
+ "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --verbose"
15
19
  },
16
20
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
17
21
  "license": "MIT",
18
22
  "ossy": {
19
23
  "src": "./src"
20
24
  },
25
+ "dependencies": {
26
+ "@ossy/event-store": "^3.0.1",
27
+ "@ossy/fold": "^3.0.1",
28
+ "@ossy/platform": "^3.0.1",
29
+ "@ossy/schema": "^3.0.1"
30
+ },
21
31
  "peerDependencies": {
32
+ "@ossy/design-system": ">=1.0.0",
33
+ "@ossy/schema": "*",
22
34
  "@ossy/sdk-react": ">=1.0.0",
23
35
  "@ossy/workspaces": ">=1.0.0",
24
36
  "react": ">=19.0.0 <20.0.0"
25
37
  },
26
38
  "devDependencies": {
27
39
  "@jest/globals": "^30.2.0",
28
- "@ossy/platform": "^1.39.2",
40
+ "@ossy/platform": "^3.0.0",
29
41
  "casual": "^1.6.2",
30
42
  "jest": "^30.2.0"
31
43
  },
@@ -37,5 +49,5 @@
37
49
  "/src",
38
50
  "README.md"
39
51
  ],
40
- "gitHead": "2b8745d57fee8b6c08787e2755b4df489291a5cd"
52
+ "gitHead": "4a70ec216448680d2fddc57b2a8a0077489720ae"
41
53
  }
@@ -8,7 +8,7 @@ export const AudioResource = ({
8
8
  onClose: _onClose
9
9
  }) => {
10
10
  const sdk = useSdk()
11
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
11
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
12
12
 
13
13
  return (
14
14
  <View stack bordered>
@@ -18,7 +18,7 @@ export const AudioResource = ({
18
18
  <View
19
19
  as="audio"
20
20
  controls
21
- slot="content"
21
+ data-region="content"
22
22
  src={resource.content.src}
23
23
  />
24
24
  </View>
@@ -1,15 +1,7 @@
1
1
  import React, { useState, useRef } from 'react'
2
2
  import { createDirectory } from './resource.helpers.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import {
5
- Input,
6
- Title,
7
- Button,
8
- View,
9
- useInputValue,
10
- Text,
11
- useLocale,
12
- } from '@ossy/design-system'
4
+ import { Input, Button, View, useInputValue, Text, useLocale } from '@ossy/design-system'
13
5
  import { useRouter } from '@ossy/router-react'
14
6
 
15
7
  export const CreateDirectory = () => {
@@ -51,7 +43,7 @@ export const CreateDirectory = () => {
51
43
  return (
52
44
  <View gap="s" style={{ height: '100%' }}>
53
45
 
54
- <Title variant="primary" style={{ marginBottom: 'var(--space-m)' }}>{t('resources.createDirectory.title')}</Title>
46
+ <Text variant="heading-primary" style={{ marginBottom: 'var(--space-m)' }} as="h1" text="resources.createDirectory.title" />
55
47
 
56
48
  <Text style={{ marginBottom: 'var(--space-m)' }}>
57
49
  {t('resources.createDirectory.description', { location: directoryLocation })}
@@ -14,7 +14,7 @@ export const metadata = {
14
14
  export const CreateDirectoryPage = () => {
15
15
  return (
16
16
  <View layout="off-center-s" style={{ height: '100%' }}>
17
- <View slot="content" surface="primary" roundness="m" inset="m">
17
+ <View data-region="content" surface="primary" roundness="m" inset="m">
18
18
  <CreateDirectory/>
19
19
  </View>
20
20
  </View>
@@ -8,7 +8,7 @@ import { GenericResourceForm } from './GenericResourceForm.jsx'
8
8
  export const CreateDocument = () => {
9
9
  const { t } = useLocale()
10
10
  const router = useRouter()
11
- const templateId = router.searchParams.templateId
11
+ const schemaId = router.searchParams.schemaId
12
12
  const location = router.searchParams.location
13
13
  const sdk = useSdk()
14
14
  const [error, setError] = useState()
@@ -27,7 +27,7 @@ export const CreateDocument = () => {
27
27
  return (
28
28
  <>
29
29
  <GenericResourceForm
30
- templateId={templateId}
30
+ schemaId={schemaId}
31
31
  mode="create"
32
32
  onSubmit={onSubmit}
33
33
  onCancel={onCancel}
@@ -14,7 +14,7 @@ export const metadata = {
14
14
  export const CreateDocumentPage = () => {
15
15
  return (
16
16
  <View layout="off-center-m" style={{ height: '100%' }}>
17
- <View slot="content" surface='primary' roundness='m' inset="m">
17
+ <View data-region="content" surface='primary' roundness='m' inset="m">
18
18
  <CreateDocument />
19
19
  </View>
20
20
  </View>
package/src/Definition.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const Definition = {
2
2
  id: 'resources',
3
- title: 'Free Resources',
3
+ title: 'Resources',
4
4
  description: `
5
5
  We believe in providing value at every step of your journey with us.
6
6
  That's why we've curated a collection of free resources.
@@ -9,5 +9,6 @@ export const Definition = {
9
9
  We hope you find them useful, and we're here if you have any questions!
10
10
  `,
11
11
  icon: 'image',
12
- statuses: ['beta'],
12
+ status: ['beta'],
13
+ entitlementRequired: false,
13
14
  }
@@ -1,51 +1,23 @@
1
1
  import React from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
- import { GetWorkspace } from '@ossy/workspaces'
4
3
  import { useSdk } from '@ossy/sdk-react'
5
- import {
6
- Text,
7
- View,
8
- DelayedRender,
9
- Fields,
10
- } from '@ossy/design-system'
4
+ import { GenericResourceForm } from './GenericResourceForm.jsx'
11
5
 
6
+ /** @deprecated Prefer SchemaPresenter flow="Edit" or GenericResourceForm directly. */
12
7
  export const DocumentEdit = ({
13
8
  resourceId,
14
9
  form,
15
10
  }) => {
16
11
  const sdk = useSdk()
17
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
18
- const { data: workspace } = sdk.read(GetWorkspace)
19
- const template = workspace?.resourceTemplates?.find(t => t.id === resource?.type)
20
-
21
- if (template?.fields && form.data) {
22
- return (
23
- <View style={{ padding: '16px 0' }}>
24
- <Fields data={form.data} onChange={form.onChange} fields={template.fields} />
25
- </View>
26
- )
27
- }
28
-
29
- // Loading
30
- if (template?.fields && !form.data) {
31
- return (
32
- <DelayedRender>
33
- <View style={{ padding: '16px 0' }}>
34
- Loading...
35
- </View>
36
- </DelayedRender>
37
- )
38
- }
12
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
39
13
 
40
14
  return (
41
- <View style={{ padding: '16px 0' }}>
42
- <View inset="m" >
43
- <Text>
44
- This documents template has been deleted and can therefore not be edited
45
- </Text>
46
- </View>
47
- </View>
15
+ <GenericResourceForm
16
+ schemaId={resource?.type}
17
+ mode="edit"
18
+ resourceId={resourceId}
19
+ resource={resource}
20
+ form={form}
21
+ />
48
22
  )
49
-
50
-
51
23
  }
@@ -1,36 +1,25 @@
1
1
  import React from 'react'
2
- import { pipe, keys, map } from 'ramda'
3
2
  import { metadata as GetResource } from './get.action.js'
4
- import { GetWorkspace } from '@ossy/workspaces'
5
3
  import { useSdk } from '@ossy/sdk-react'
6
- import { Text, View } from '@ossy/design-system'
4
+ import { useSchema } from './useSchemas.js'
5
+ import { View } from '@ossy/design-system'
6
+ import { SchemaDetailView } from './SchemaDetailView.jsx'
7
7
 
8
8
  export const DocumentView = ({
9
9
  resourceId
10
10
  }) => {
11
11
  const sdk = useSdk()
12
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
13
- const { data: workspace } = sdk.read(GetWorkspace)
14
- const template = workspace?.resourceTemplates?.find(t => t.id === resource?.type)
12
+ const { data: resource } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId })
13
+ const template = useSchema(resource?.type)
15
14
 
16
15
  return (
17
16
  <View style={{ padding: '16px 0' }}>
18
- <View gap="m">
19
- {
20
- !!resource?.content && (template?.fields || [])
21
- .map(field => ({ name: field.name, value: resource?.content?.[field.name] }))
22
- .map(field => (
23
- <View gap="s" key={field.name}>
24
- <Text style={{ fontWeight: 'bold' }} >{field.name}</Text>
25
- {
26
- typeof field.value === 'object'
27
- ? pipe(keys, map(key => field.value[key] && <div>{key}</div>))(field.value)
28
- : <Text style={{ maxWidth: '500px' }}>{field.value}</Text>
29
- }
30
- </View>
31
- ))
32
- }
33
- </View>
17
+ <SchemaDetailView
18
+ schemaId={resource?.type}
19
+ fields={template?.fields}
20
+ data={resource?.content ?? {}}
21
+ title={resource?.name}
22
+ />
34
23
  </View>
35
24
  )
36
25
  }
@@ -1,13 +1,14 @@
1
- import React from 'react'
1
+ import React, { useMemo } from 'react'
2
2
  import { metadata as GetResource } from './get.action.js'
3
3
  import { useSdk } from '@ossy/sdk-react'
4
- import { Slot, resourceSlot, View, Title, Text, DelayedRender } from '@ossy/design-system'
4
+ import { resolveViewComponentChain } from '@ossy/schema/resolve'
5
+ import { Slot, View, Text, DelayedRender } from '@ossy/design-system'
5
6
 
6
7
  function GenericResourceCardFallback ({ resource }) {
7
8
  if (!resource) return null
8
9
  return (
9
10
  <View surface="secondary" roundness="m" inset="m" gap="xs">
10
- <Title variant="secondary">{resource.name}</Title>
11
+ <Text variant="heading-secondary" as="h2">{resource.name}</Text>
11
12
  {resource.content?.description && (
12
13
  <Text size="s" color="secondary">{resource.content.description}</Text>
13
14
  )}
@@ -15,11 +16,15 @@ function GenericResourceCardFallback ({ resource }) {
15
16
  )
16
17
  }
17
18
 
18
- /** Card-sized resource preview `resource:{type}/card`. */
19
+ /** Card projection for a schema-backed resource — ADR 0006. */
19
20
  export function GenericResourceCard ({ resourceId, resource: resourceProp }) {
20
21
  const sdk = useSdk()
21
- const { data: loaded } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId && !resourceProp })
22
+ const { data: loaded } = sdk.read(GetResource, { resourceId }, { enabled: !!resourceId && !resourceProp })
22
23
  const resource = resourceProp ?? loaded
24
+ const chain = useMemo(
25
+ () => (resource?.type ? resolveViewComponentChain(resource.type, 'card') : []),
26
+ [resource?.type],
27
+ )
23
28
 
24
29
  if (!resource) {
25
30
  return (
@@ -29,12 +34,25 @@ export function GenericResourceCard ({ resourceId, resource: resourceProp }) {
29
34
  )
30
35
  }
31
36
 
37
+ const [primary, secondary] = chain
38
+
32
39
  return (
33
40
  <Slot
34
- name={resourceSlot(resource.type, 'card')}
41
+ view={primary}
35
42
  resourceId={resourceId}
36
43
  resource={resource}
37
- fallback={<GenericResourceCardFallback resource={resource} />}
44
+ schemaId={resource.type}
45
+ projection="card"
46
+ fallback={
47
+ <Slot
48
+ view={secondary}
49
+ resourceId={resourceId}
50
+ resource={resource}
51
+ schemaId={resource.type}
52
+ projection="card"
53
+ fallback={<GenericResourceCardFallback resource={resource} />}
54
+ />
55
+ }
38
56
  />
39
57
  )
40
58
  }
@@ -1,66 +1,12 @@
1
- import React from 'react'
2
- import { metadata as GetResource } from './get.action.js'
3
- import { GetWorkspace } from '@ossy/workspaces'
4
- import { useSdk } from '@ossy/sdk-react'
5
- import {
6
- Slot,
7
- resourceSlot,
8
- View,
9
- Title,
10
- Text,
11
- DelayedRender,
12
- Fields,
13
- } from '@ossy/design-system'
14
- import { ResourceGenericView } from './ResourceGenericView.jsx'
15
-
16
- function GenericResourceDetailFallback ({ resourceId }) {
17
- const sdk = useSdk()
18
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
19
- const { data: workspace } = sdk.read(GetWorkspace)
20
- const template = workspace?.resourceTemplates?.find(t => t.id === resource?.type)
21
-
22
- if (!resource) {
23
- return (
24
- <DelayedRender>
25
- <Text>Loading…</Text>
26
- </DelayedRender>
27
- )
28
- }
29
-
30
- if (template?.fields?.length) {
31
- return (
32
- <View stack gap="m" inset="m">
33
- <Title>{resource.name}</Title>
34
- <Fields data={resource.content || {}} fields={template.fields} />
35
- </View>
36
- )
37
- }
38
-
39
- return <ResourceGenericView resourceId={resourceId} />
40
- }
41
-
42
- /**
43
- * Generic resource detail host — resolves `resource:{type}/detail` from the slot
44
- * registry; falls back to template fields or a minimal placeholder.
45
- */
46
- export function GenericResourceDetail ({ resourceId }) {
47
- const sdk = useSdk()
48
- const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
49
-
50
- if (!resource) {
51
- return (
52
- <DelayedRender>
53
- <Text>Loading…</Text>
54
- </DelayedRender>
55
- )
56
- }
1
+ import { SchemaPresenter } from './SchemaPresenter.jsx'
57
2
 
3
+ /** Read view for a schema-backed resource — ADR 0006 component id chain. */
4
+ export function GenericResourceDetail ({ resourceId, projection }) {
58
5
  return (
59
- <Slot
60
- name={resourceSlot(resource.type, 'detail')}
6
+ <SchemaPresenter
61
7
  resourceId={resourceId}
62
- resource={resource}
63
- fallback={<GenericResourceDetailFallback resourceId={resourceId} />}
8
+ flow="View"
9
+ projection={projection}
64
10
  />
65
11
  )
66
12
  }
@@ -1,11 +1,11 @@
1
- import React, { useState } from 'react'
2
- import { GetWorkspace } from '@ossy/workspaces'
3
- import { useSdk } from '@ossy/sdk-react'
1
+ import React, { useMemo, useState } from 'react'
2
+ import { useSchema } from './useSchemas.js'
3
+ import { useSchemaEngine } from './useSchemaEngine.js'
4
+ import { validateContent } from '@ossy/schema'
5
+ import { resolveFormComponentChain } from '@ossy/schema/resolve'
4
6
  import {
5
7
  Slot,
6
- resourceSlot,
7
8
  View,
8
- Title,
9
9
  Text,
10
10
  InputTitle,
11
11
  Button,
@@ -15,10 +15,11 @@ import {
15
15
  useInputValue,
16
16
  useLocale,
17
17
  } from '@ossy/design-system'
18
+ import { SchemaFormDefault } from './SchemaFormDefault.jsx'
18
19
 
19
- function GenericResourceFormFallback ({
20
- templateId,
21
- mode = 'create',
20
+ /** CMS create-resource form — name + schema fields (resources-specific tertiary fallback). */
21
+ function ResourceCreateFormFallback ({
22
+ schemaId,
22
23
  initialName = '',
23
24
  initialContent = {},
24
25
  onSubmit,
@@ -26,9 +27,8 @@ function GenericResourceFormFallback ({
26
27
  submitLabel,
27
28
  }) {
28
29
  const { t } = useLocale()
29
- const sdk = useSdk()
30
- const { data: workspace } = sdk.read(GetWorkspace)
31
- const template = workspace?.resourceTemplates?.find(t => t.id === templateId)
30
+ const template = useSchema(schemaId)
31
+ const engine = useSchemaEngine()
32
32
  const [name, setName] = useInputValue(initialName)
33
33
  const [content, setContent] = useState(initialContent)
34
34
  const [error, setError] = useState()
@@ -38,8 +38,18 @@ function GenericResourceFormFallback ({
38
38
  setError(t('resources.form.nameRequired'))
39
39
  return
40
40
  }
41
+ if (template) {
42
+ const result = validateContent(engine, template, content)
43
+ if (!result.ok) {
44
+ setError(result.message)
45
+ return
46
+ }
47
+ setError(undefined)
48
+ onSubmit?.({ name: name.trim(), content: result.data, type: schemaId })
49
+ return
50
+ }
41
51
  setError(undefined)
42
- onSubmit?.({ name: name.trim(), content, type: templateId })
52
+ onSubmit?.({ name: name.trim(), content, type: schemaId })
43
53
  }
44
54
 
45
55
  return (
@@ -52,62 +62,186 @@ function GenericResourceFormFallback ({
52
62
  onChange={setName}
53
63
  />
54
64
  <Fields
65
+ schemaId={schemaId}
55
66
  data={content}
56
- onChange={(event) => setContent((prev) => applyFieldChange(prev, event))}
67
+ onChange={(event) => {
68
+ setError(undefined)
69
+ setContent((prev) => applyFieldChange(prev, event))
70
+ }}
57
71
  fields={template?.fields || []}
58
72
  />
59
- {error && <Alert>{error}</Alert>}
73
+ {error && <Alert variant="danger">{error}</Alert>}
60
74
  <View layout="row" gap="s" justifyContent="flex-end">
61
- {onCancel && <Button variant="link" onClick={onCancel}>{t('design-system.cancel')}</Button>}
75
+ {onCancel && <Button variant="link" onClick={onCancel} label="design-system.cancel" />}
62
76
  <Button variant="cta" onClick={handleSubmit}>
63
- {submitLabel || (mode === 'edit' ? t('design-system.save') : (template?.name ? `${t('design-system.add')} ${template.name}` : t('resources.form.createResource')))}
77
+ {submitLabel || (template?.name ? `${t('design-system.add')} ${template.name}` : t('resources.form.createResource'))}
64
78
  </Button>
65
79
  </View>
66
80
  </View>
67
81
  )
68
82
  }
69
83
 
84
+ function ResourceFormFallback ({
85
+ schemaId,
86
+ mode = 'create',
87
+ schemaFormOnly = false,
88
+ form,
89
+ resource,
90
+ initialName,
91
+ initialContent,
92
+ onSubmit,
93
+ onCancel,
94
+ submitLabel,
95
+ fields,
96
+ }) {
97
+ if (form && mode === 'edit' && !fields?.length) {
98
+ return (
99
+ <View style={{ padding: '16px 0' }}>
100
+ <View inset="m">
101
+ <Text text="resources.form.typeRemoved" />
102
+ </View>
103
+ </View>
104
+ )
105
+ }
106
+
107
+ if (mode === 'create' && !schemaFormOnly) {
108
+ return (
109
+ <ResourceCreateFormFallback
110
+ schemaId={schemaId}
111
+ initialName={initialName ?? resource?.name}
112
+ initialContent={initialContent ?? resource?.content ?? {}}
113
+ onSubmit={onSubmit}
114
+ onCancel={onCancel}
115
+ submitLabel={submitLabel}
116
+ />
117
+ )
118
+ }
119
+
120
+ return (
121
+ <View inset="m">
122
+ <SchemaFormDefault
123
+ schemaId={schemaId}
124
+ fields={fields}
125
+ mode={mode}
126
+ form={form}
127
+ resource={resource}
128
+ initialContent={initialContent ?? resource?.content ?? {}}
129
+ onSubmit={onSubmit}
130
+ onCancel={onCancel}
131
+ submitLabel={submitLabel}
132
+ />
133
+ </View>
134
+ )
135
+ }
136
+
70
137
  /**
71
- * Generic resource create/edit host — resolves `resource:{type}/form` from the
72
- * slot registry; falls back to template-driven Fields.
138
+ * Resolves `@ossy/…/form/{concept}` `@ossy/resources/form/default` (edit) → fallback.
139
+ *
140
+ * Create mode skips the design-system default and falls through to {@link ResourceCreateFormFallback}
141
+ * unless `schemaFormOnly` is set (e.g. Storybook schema preview).
73
142
  */
74
- export function GenericResourceForm ({
75
- templateId,
143
+ function SchemaFormSlotPresenter ({
144
+ schemaId,
76
145
  mode = 'create',
146
+ schemaFormOnly = false,
77
147
  resourceId,
78
148
  resource,
149
+ form,
79
150
  initialName,
80
151
  initialContent,
81
152
  onSubmit,
82
153
  onCancel,
83
154
  submitLabel,
84
155
  }) {
85
- const { t } = useLocale()
86
-
87
- if (!templateId) {
88
- return <Text>{t('resources.form.selectType')}</Text>
156
+ const template = useSchema(schemaId)
157
+ const chain = useMemo(() => {
158
+ if (!schemaId) return []
159
+ const full = resolveFormComponentChain(schemaId)
160
+ if (mode === 'create' && !schemaFormOnly) return [full[0]]
161
+ return full
162
+ }, [schemaId, mode, schemaFormOnly])
163
+ const [primary, secondary] = chain
164
+ const slotProps = {
165
+ schemaId,
166
+ mode,
167
+ resourceId,
168
+ resource,
169
+ form,
170
+ fields: template?.fields,
171
+ initialContent: initialContent ?? resource?.content ?? {},
172
+ onSubmit,
173
+ onCancel,
174
+ submitLabel,
89
175
  }
90
176
 
177
+ const tertiaryFallback = (
178
+ <ResourceFormFallback
179
+ schemaId={schemaId}
180
+ mode={mode}
181
+ schemaFormOnly={schemaFormOnly}
182
+ form={form}
183
+ resource={resource}
184
+ initialName={initialName ?? resource?.name}
185
+ initialContent={initialContent ?? resource?.content ?? {}}
186
+ onSubmit={onSubmit}
187
+ onCancel={onCancel}
188
+ submitLabel={submitLabel}
189
+ fields={template?.fields}
190
+ />
191
+ )
192
+
193
+ const fallback = secondary
194
+ ? (
195
+ <Slot
196
+ view={secondary}
197
+ {...slotProps}
198
+ fallback={tertiaryFallback}
199
+ />
200
+ )
201
+ : tertiaryFallback
202
+
91
203
  return (
92
204
  <Slot
93
- name={resourceSlot(templateId, 'form')}
94
- templateId={templateId}
205
+ view={primary}
206
+ {...slotProps}
207
+ fallback={fallback}
208
+ />
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Generic resource create/edit host — ADR 0006 form component chain with CMS create fallback.
214
+ */
215
+ export function GenericResourceForm ({
216
+ schemaId,
217
+ mode = 'create',
218
+ schemaFormOnly = false,
219
+ resourceId,
220
+ resource,
221
+ form,
222
+ initialName,
223
+ initialContent,
224
+ onSubmit,
225
+ onCancel,
226
+ submitLabel,
227
+ }) {
228
+ if (!schemaId) {
229
+ return <Text text="resources.form.selectType" />
230
+ }
231
+
232
+ return (
233
+ <SchemaFormSlotPresenter
234
+ schemaId={schemaId}
95
235
  mode={mode}
236
+ schemaFormOnly={schemaFormOnly}
96
237
  resourceId={resourceId}
97
238
  resource={resource}
239
+ form={form}
240
+ initialName={initialName}
241
+ initialContent={initialContent}
98
242
  onSubmit={onSubmit}
99
243
  onCancel={onCancel}
100
- fallback={
101
- <GenericResourceFormFallback
102
- templateId={templateId}
103
- mode={mode}
104
- initialName={initialName ?? resource?.name}
105
- initialContent={initialContent ?? resource?.content ?? {}}
106
- onSubmit={onSubmit}
107
- onCancel={onCancel}
108
- submitLabel={submitLabel}
109
- />
110
- }
244
+ submitLabel={submitLabel}
111
245
  />
112
246
  )
113
247
  }