@simai-studio/ui 5.20261023.126 → 5.20261023.212

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 (57) hide show
  1. package/README.md +4 -4
  2. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-field-hidden-by-its-condition-is-not-saved.json +45 -0
  3. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-field-says-what-it-falls-back-to.json +12 -0
  4. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-field-taken-out-of-an-area-is-unplaced.json +40 -0
  5. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-reader-is-offered-nothing-to-change.json +23 -0
  6. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-related-group-is-a-list-not-fields.json +23 -0
  7. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-stale-confirmation-is-asked-again.json +39 -0
  8. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/conditions-arrive-declared.json +82 -0
  9. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-applied-answers-with-the-id.json +24 -0
  10. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-field-errors-are-refused.json +22 -0
  11. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/field-presentation-keys-are-declared.json +12 -0
  12. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-applied-is-one-revision.json +16 -0
  13. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-confirmed-with-the-digest-is-applied.json +35 -0
  14. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-field-errors-are-refused.json +16 -0
  15. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-may-ask-for-a-confirmation.json +38 -0
  16. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-stale-revision-is-a-conflict.json +16 -0
  17. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-applied-carries-the-object.json +12 -0
  18. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-for-create-is-an-empty-object.json +24 -0
  19. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-late-answer-is-dropped.json +15 -0
  20. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-refused-is-explained.json +12 -0
  21. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-unavailable-is-honest.json +12 -0
  22. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/lookup-arrives-with-its-label.json +70 -0
  23. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/opening-a-reference-is-the-hosts-decision.json +12 -0
  24. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/opening-the-record-as-a-page-is-offered-only-where-it-leads-somewhere.json +28 -0
  25. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-carry-the-source-and-a-limit.json +33 -0
  26. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-page-carries-a-cursor.json +15 -0
  27. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-refused-is-explained.json +14 -0
  28. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-by-source-reaches-the-host.json +30 -0
  29. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-reaches-the-host.json +14 -0
  30. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-without-the-capability.json +14 -0
  31. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/rare-fields-arrive-declared.json +12 -0
  32. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/removing-a-file-is-a-save.json +13 -0
  33. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-applied-is-one-revision.json +26 -0
  34. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-may-ask-for-a-confirmation.json +36 -0
  35. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-stale-revision-is-a-conflict.json +23 -0
  36. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/the-address-is-the-hosts.json +12 -0
  37. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-answers-with-the-stored-file.json +14 -0
  38. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-refused-is-explained.json +14 -0
  39. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-create-answers-with-the-record-through-the-new-view.json +32 -0
  40. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-create-shared-may-be-refused.json +27 -0
  41. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-delete-applied-answers-with-what-is-left.json +25 -0
  42. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-delete-the-last-shared-one-is-refused.json +25 -0
  43. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-applied-carries-the-record-through-it.json +25 -0
  44. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-refused-is-explained.json +25 -0
  45. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-unavailable-without-the-capability.json +23 -0
  46. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-rename-changes-the-label-only.json +26 -0
  47. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-reset-answers-with-what-now-resolves.json +30 -0
  48. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-against-a-stale-revision-is-a-conflict.json +36 -0
  49. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-is-one-layer-one-revision.json +35 -0
  50. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-may-name-an-area-the-record-does-not-carry.json +49 -0
  51. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-rejects-entries-by-kind.json +36 -0
  52. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-settings-without-the-capability.json +30 -0
  53. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/manifest.json +66 -0
  54. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/port.json +2484 -0
  55. package/distr/smart/field-renderers/js/field-renderers.min.js +128 -87
  56. package/package-manifest.json +57 -4
  57. package/package.json +5 -4
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @simai-studio/ui
2
2
 
3
- SIMAI Framework, release `ui-22d6367046c5` (commit 22d6367046c5240c5b4ffbd7f7853dffc3207396 of [simai/ui](https://github.com/simai/ui)).
3
+ SIMAI Framework, release `ui-720203c5b5e5` (commit 720203c5b5e5186e2d44f6e1f8d9c23a0e410cc1 of [simai/ui](https://github.com/simai/ui)).
4
4
 
5
5
  ```html
6
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.126/distr/core/css/core.css">
7
- <script>window.sfPath = 'https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.126/distr/';</script>
8
- <script src="https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.126/distr/core/js/core.js"></script>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.212/distr/core/css/core.css">
7
+ <script>window.sfPath = 'https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.212/distr/';</script>
8
+ <script src="https://cdn.jsdelivr.net/npm/@simai-studio/ui@5.20261023.212/distr/core/js/core.js"></script>
9
9
  ```
10
10
 
11
11
  Components, utilities and Smart Components ship in their minified form (the Loader asks for it); Core in both forms. Documentation: https://ui.simai.io/
@@ -0,0 +1,45 @@
1
+ {
2
+ "id": "a-field-hidden-by-its-condition-is-not-saved",
3
+ "intent": "group.save",
4
+ "summary": "A field its condition hides is absent from the save, and the host keeps what it holds.",
5
+ "given": {
6
+ "capabilities": [],
7
+ "fields": [
8
+ {
9
+ "key": "type",
10
+ "value": "framework"
11
+ },
12
+ {
13
+ "key": "valid_until",
14
+ "value": "2027-01-01",
15
+ "when": {
16
+ "visible": {
17
+ "field": "type",
18
+ "op": "eq",
19
+ "value": "framework"
20
+ }
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ "when": {
26
+ "intent": "group.save",
27
+ "payload": {
28
+ "record_id": "record-1",
29
+ "collection": "contracts",
30
+ "section": "main",
31
+ "group": "terms",
32
+ "values": {
33
+ "type": "single"
34
+ },
35
+ "revision": "7"
36
+ }
37
+ },
38
+ "then": {
39
+ "answer": "applied",
40
+ "answer_must_include": [
41
+ "record"
42
+ ],
43
+ "note": "valid_until is hidden once the type is single, so it is not in values -- and it is not emptied: the record answered still holds 2027-01-01. Hidden by a condition is not the same as cleared."
44
+ }
45
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "a-field-says-what-it-falls-back-to",
3
+ "intent": "record.load",
4
+ "summary": "A field that holds nothing and falls back to something says what it falls back to, so a person can tell «not set» from «set to the same value».",
5
+ "given": {"capabilities": []},
6
+ "when": {"intent": "record.load", "payload": {"record_id": "record-1", "collection": "pages", "reason": "connected"}},
7
+ "then": {
8
+ "answer": "applied",
9
+ "answer_must_include": ["record"],
10
+ "note": "`inherited` says that a value comes from somewhere else; `inheritedValue` says what it is. Both are needed: a colour inherited from the theme and a colour chosen to match the theme look identical on screen, and only the first follows the theme when the theme moves. A running Larena site inherits fourteen colours this way, which is why the pair exists."
11
+ }
12
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "id": "a-field-taken-out-of-an-area-is-unplaced",
3
+ "intent": "view.save",
4
+ "capability": "view-settings",
5
+ "summary": "A field removed from an area is written with a null group and comes back in `unplaced`, not as a hidden field.",
6
+ "given": {
7
+ "capabilities": [
8
+ "view-settings"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "view.save",
13
+ "payload": {
14
+ "record_id": null,
15
+ "collection": "pages",
16
+ "target": {
17
+ "kind": "personal",
18
+ "revision": "v7"
19
+ },
20
+ "view": {
21
+ "schema_version": 1,
22
+ "sections": {},
23
+ "groups": {},
24
+ "fields": {
25
+ "code": {
26
+ "group": null,
27
+ "order": 1
28
+ }
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "then": {
34
+ "answer": "applied",
35
+ "answer_must_include": [
36
+ "revision"
37
+ ],
38
+ "note": "Hiding and taking out are different things and the port has both. A hidden field is one the layer above may have hidden for everyone; an unplaced field belongs to no area and is offered back where areas take fields. Answering a null group with a hidden field would make removal look permanent."
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "a-reader-is-offered-nothing-to-change",
3
+ "intent": "record.load",
4
+ "summary": "A person who may only read gets the record with no record.edit and no lifecycle act among its actions.",
5
+ "given": {
6
+ "capabilities": []
7
+ },
8
+ "when": {
9
+ "intent": "record.load",
10
+ "payload": {
11
+ "record_id": "record-1",
12
+ "collection": "pages",
13
+ "reason": "connected"
14
+ }
15
+ },
16
+ "then": {
17
+ "answer": "applied",
18
+ "answer_must_include": [
19
+ "record"
20
+ ],
21
+ "note": "actions is [] or names only what this person may do; the component draws no edit, no footer and no delete for it."
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "a-related-group-is-a-list-not-fields",
3
+ "intent": "record.load",
4
+ "summary": "A group marked «related» holds other records rather than fields. The component composes a data view in it, and that view asks the host for its own port.",
5
+ "given": {
6
+ "capabilities": []
7
+ },
8
+ "when": {
9
+ "intent": "record.load",
10
+ "payload": {
11
+ "record_id": "record-1",
12
+ "collection": "pages",
13
+ "reason": "connected"
14
+ }
15
+ },
16
+ "then": {
17
+ "answer": "applied",
18
+ "answer_must_include": [
19
+ "record"
20
+ ],
21
+ "note": "The records themselves never travel through this port. A related list asks for a data-view port by raising sf-host-port-request with the owner, the record and the group, and the host answers with the port it already implements for collections. Routing rows, columns, sorting and paging through the property-view seam instead would duplicate a whole vocabulary and make the property view a relay for one it does not speak."
22
+ }
23
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "id": "a-stale-confirmation-is-asked-again",
3
+ "intent": "group.save",
4
+ "summary": "A digest that no longer matches the save -- the values or the object moved since it was issued -- is answered with a new confirmation, not applied.",
5
+ "given": {
6
+ "capabilities": [],
7
+ "confirms": [
8
+ "group.save"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "group.save",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "contracts",
16
+ "section": "main",
17
+ "group": "terms",
18
+ "values": {
19
+ "type": {
20
+ "value": "single",
21
+ "label": "Разовый"
22
+ }
23
+ },
24
+ "revision": "7",
25
+ "confirm": "c0ffee"
26
+ }
27
+ },
28
+ "then": {
29
+ "answer": "refused",
30
+ "answer_must_include": [
31
+ "reason",
32
+ "confirm"
33
+ ],
34
+ "data_must_include": [
35
+ "confirm"
36
+ ],
37
+ "note": "reason is confirm_required again, with a new summary and digest. The component shows the new summary; a person confirms what will actually happen."
38
+ }
39
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "id": "conditions-arrive-declared",
3
+ "intent": "record.load",
4
+ "summary": "A field may carry when: the conditions under which it is shown, required and enabled, over the other fields' values.",
5
+ "given": {
6
+ "capabilities": [],
7
+ "fields": [
8
+ {
9
+ "key": "type",
10
+ "propertyType": "choice",
11
+ "options": [
12
+ {
13
+ "value": "single",
14
+ "label": "Разовый"
15
+ },
16
+ {
17
+ "value": "framework",
18
+ "label": "Рамочный"
19
+ }
20
+ ],
21
+ "value": "single"
22
+ },
23
+ {
24
+ "key": "valid_until",
25
+ "propertyType": "date",
26
+ "value": "2027-01-01",
27
+ "when": {
28
+ "visible": {
29
+ "field": "type",
30
+ "op": "eq",
31
+ "value": "framework"
32
+ },
33
+ "required": {
34
+ "all": [
35
+ {
36
+ "field": "type",
37
+ "op": "eq",
38
+ "value": "framework"
39
+ },
40
+ {
41
+ "field": "valid_until",
42
+ "op": "empty"
43
+ }
44
+ ]
45
+ }
46
+ }
47
+ },
48
+ {
49
+ "key": "amount",
50
+ "propertyType": "number",
51
+ "value": 1000,
52
+ "when": {
53
+ "enabled": {
54
+ "not": {
55
+ "field": "status",
56
+ "op": "in",
57
+ "value": [
58
+ "signed",
59
+ "closed"
60
+ ]
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ "when": {
68
+ "intent": "record.load",
69
+ "payload": {
70
+ "record_id": "record-1",
71
+ "collection": "contracts",
72
+ "reason": "connected"
73
+ }
74
+ },
75
+ "then": {
76
+ "answer": "applied",
77
+ "answer_must_include": [
78
+ "record"
79
+ ],
80
+ "note": "The component evaluates the conditions as the person edits. valid_until is hidden while the type is single, and keeps 2027-01-01."
81
+ }
82
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "create-applied-answers-with-the-id",
3
+ "intent": "record.create",
4
+ "summary": "Pressing create sends the filled fields once; the host answers with the new object's id.",
5
+ "given": {
6
+ "capabilities": []
7
+ },
8
+ "when": {
9
+ "intent": "record.create",
10
+ "payload": {
11
+ "collection": "pages",
12
+ "values": {
13
+ "title": "Новая страница"
14
+ }
15
+ }
16
+ },
17
+ "then": {
18
+ "answer": "applied",
19
+ "answer_must_include": [
20
+ "record_id"
21
+ ],
22
+ "note": "With the record too, when the host has it at hand; the component reads it otherwise."
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "create-field-errors-are-refused",
3
+ "intent": "record.create",
4
+ "summary": "A create the host will not make comes back refused with errors keyed by field.",
5
+ "given": {
6
+ "capabilities": []
7
+ },
8
+ "when": {
9
+ "intent": "record.create",
10
+ "payload": {
11
+ "collection": "pages",
12
+ "values": {}
13
+ }
14
+ },
15
+ "then": {
16
+ "answer": "refused",
17
+ "answer_must_include": [
18
+ "errors"
19
+ ],
20
+ "note": "Each error stands under its own field; what the person typed stays."
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "field-presentation-keys-are-declared",
3
+ "intent": "record.load",
4
+ "summary": "A field carries the words and limits its control needs -- the call to action and the readable formats of a file field, the accept list its dialog filters by, a placeholder, a search placeholder, a badge appearance, a readonly mark. All of them are the host's, and an answer carrying them is applied.",
5
+ "given": {"capabilities": ["field-upload"]},
6
+ "when": {"intent": "record.load", "payload": {"record_id": "record-1", "collection": "pages", "reason": "connected"}},
7
+ "then": {
8
+ "answer": "applied",
9
+ "answer_must_include": ["record"],
10
+ "note": "This case exists because the field schema is closed. A host that validates its own answers against this port was refused for sending uploadText, formats and accept -- keys the renderer set has always read but the port never declared, which is the port lying about the seam rather than the host getting it wrong. A field control with no words of its own falls back to the component's default, and a file control's default names image formats, which is wrong for a field that takes any file."
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "group-save-applied-is-one-revision",
3
+ "intent": "group.save",
4
+ "summary": "Pressing save sends one intent carrying only the changed fields, and the host answers with the object at its new revision.",
5
+ "given": {"capabilities": []},
6
+ "when": {
7
+ "intent": "group.save",
8
+ "payload": {"record_id": "record-1", "collection": "pages", "section": "main", "group": "about",
9
+ "values": {"title": "Новое название"}, "revision": "7"}
10
+ },
11
+ "then": {
12
+ "answer": "applied",
13
+ "answer_must_include": ["record"],
14
+ "note": "One press, one intent, one revision. A field the person did not touch is absent from values, so a save cannot overwrite what another save changed meanwhile."
15
+ }
16
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "id": "group-save-confirmed-with-the-digest-is-applied",
3
+ "intent": "group.save",
4
+ "summary": "The same save raised again with confirm set to the digest is applied as one revision.",
5
+ "given": {
6
+ "capabilities": [],
7
+ "confirms": [
8
+ "group.save"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "group.save",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "contracts",
16
+ "section": "main",
17
+ "group": "terms",
18
+ "values": {
19
+ "type": {
20
+ "value": "framework",
21
+ "label": "Рамочный"
22
+ }
23
+ },
24
+ "revision": "7",
25
+ "confirm": "c0ffee"
26
+ }
27
+ },
28
+ "then": {
29
+ "answer": "applied",
30
+ "answer_must_include": [
31
+ "record"
32
+ ],
33
+ "note": "c0ffee stands for the digest the host answered the first raise with. The values are the same ones; a digest the host did not issue for this save is not applied."
34
+ }
35
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "group-save-field-errors-are-refused",
3
+ "intent": "group.save",
4
+ "summary": "Values the host will not accept come back refused with errors keyed by field, and each error is shown under its own field.",
5
+ "given": {"capabilities": []},
6
+ "when": {
7
+ "intent": "group.save",
8
+ "payload": {"record_id": "record-1", "collection": "pages", "section": "main", "group": "about",
9
+ "values": {"title": "", "count": "many"}, "revision": "7"}
10
+ },
11
+ "then": {
12
+ "answer": "refused",
13
+ "answer_must_include": ["errors"],
14
+ "note": "errors is keyed by field key, so the component can place each message under the field it belongs to. A single sentence for the whole group leaves a person hunting for which field is wrong."
15
+ }
16
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "group-save-may-ask-for-a-confirmation",
3
+ "intent": "group.save",
4
+ "summary": "A save the host will make only once confirmed is refused with confirm_required and says what it will do, with a digest.",
5
+ "given": {
6
+ "capabilities": [],
7
+ "confirms": [
8
+ "group.save"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "group.save",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "contracts",
16
+ "section": "main",
17
+ "group": "terms",
18
+ "values": {
19
+ "type": {
20
+ "value": "framework",
21
+ "label": "Рамочный"
22
+ }
23
+ },
24
+ "revision": "7"
25
+ }
26
+ },
27
+ "then": {
28
+ "answer": "refused",
29
+ "answer_must_include": [
30
+ "reason",
31
+ "confirm"
32
+ ],
33
+ "data_must_include": [
34
+ "confirm"
35
+ ],
36
+ "note": "reason is confirm_required; confirm is {summary, digest}. Nothing is written yet. The component shows the summary and keeps what the person entered."
37
+ }
38
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "group-save-stale-revision-is-a-conflict",
3
+ "intent": "group.save",
4
+ "summary": "A save against a revision the host has moved past is a conflict, never a silent overwrite.",
5
+ "given": {"capabilities": []},
6
+ "when": {
7
+ "intent": "group.save",
8
+ "payload": {"record_id": "record-1", "collection": "pages", "section": "main", "group": "about",
9
+ "values": {"title": "Новое название"}, "revision": "6"}
10
+ },
11
+ "then": {
12
+ "answer": "conflict",
13
+ "answer_must_include": ["reason"],
14
+ "note": "The component keeps what the person typed and says the object moved. Answering applied here would lose somebody else's save without anyone being told."
15
+ }
16
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "load-applied-carries-the-object",
3
+ "intent": "record.load",
4
+ "summary": "An applied read carries the object with its sections, groups and fields, and the revision a later write will be compared against.",
5
+ "given": {"capabilities": []},
6
+ "when": {"intent": "record.load", "payload": {"record_id": "record-1", "collection": "pages", "reason": "connected"}},
7
+ "then": {
8
+ "answer": "applied",
9
+ "answer_must_include": ["record"],
10
+ "note": "record.sections is required even when there is one section: the arrangement is the host's, and a flat list of values would make the component invent one."
11
+ }
12
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "load-for-create-is-an-empty-object",
3
+ "intent": "record.load",
4
+ "summary": "A create surface reads an empty object of the collection: no record_id, no revision, the collection's arrangement, defaults filled.",
5
+ "given": {
6
+ "capabilities": []
7
+ },
8
+ "when": {
9
+ "intent": "record.load",
10
+ "payload": {
11
+ "record_id": null,
12
+ "collection": "pages",
13
+ "reason": "connected",
14
+ "mode": "create"
15
+ }
16
+ },
17
+ "then": {
18
+ "answer": "applied",
19
+ "answer_must_include": [
20
+ "record"
21
+ ],
22
+ "note": "The same sections and groups a record of the collection is shown in, so arranging the view once arranges creating too."
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "load-late-answer-is-dropped",
3
+ "intent": "record.load",
4
+ "summary": "Two reads in flight: the answer to the earlier one is dropped and never drawn over the later object.",
5
+ "given": {"capabilities": []},
6
+ "when": {
7
+ "intent": "record.load",
8
+ "payload": {"record_id": "record-2", "collection": "pages", "reason": "record-id-changed"},
9
+ "after": {"intent": "record.load", "payload": {"record_id": "record-1", "collection": "pages", "reason": "connected"}}
10
+ },
11
+ "then": {
12
+ "answer": "applied",
13
+ "note": "The host may answer in either order. The component counts its own asks and renders only the newest, so a slow first answer cannot replace the object a person is now looking at."
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "load-refused-is-explained",
3
+ "intent": "record.load",
4
+ "summary": "A read the host will not serve is refused with a reason, and the component shows the refusal rather than an empty object.",
5
+ "given": {"capabilities": []},
6
+ "when": {"intent": "record.load", "payload": {"record_id": "record-forbidden", "collection": "pages", "reason": "requested"}},
7
+ "then": {
8
+ "answer": "refused",
9
+ "answer_must_include": ["reason"],
10
+ "note": "An empty arrangement and a refused read look identical on screen unless the refusal is said out loud, and the second is the one a person can act on."
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "load-unavailable-is-honest",
3
+ "intent": "record.load",
4
+ "summary": "A host that cannot answer right now says unavailable; it never answers applied with an empty object.",
5
+ "given": {"capabilities": []},
6
+ "when": {"intent": "record.load", "payload": {"record_id": "record-1", "collection": "pages", "reason": "requested"}},
7
+ "then": {
8
+ "answer": "unavailable",
9
+ "answer_must_include": ["reason"],
10
+ "note": "applied with no sections would be read as an object that genuinely has no fields."
11
+ }
12
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "id": "lookup-arrives-with-its-label",
3
+ "intent": "record.load",
4
+ "summary": "A lookup field carries its value as {value, label} -- a list of them when it is multiple -- and names the reference list it is chosen from.",
5
+ "given": {
6
+ "capabilities": [
7
+ "field-options"
8
+ ],
9
+ "fields": [
10
+ {
11
+ "key": "country",
12
+ "propertyType": "lookup",
13
+ "source": "countries",
14
+ "value": {
15
+ "value": "RU",
16
+ "label": "Россия"
17
+ }
18
+ },
19
+ {
20
+ "key": "regions",
21
+ "propertyType": "lookup",
22
+ "source": "regions",
23
+ "multiple": true,
24
+ "value": [
25
+ {
26
+ "value": 77,
27
+ "label": "Москва"
28
+ },
29
+ {
30
+ "value": 78,
31
+ "label": "Санкт-Петербург"
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "key": "status",
37
+ "propertyType": "lookup",
38
+ "options": [
39
+ {
40
+ "value": "draft",
41
+ "label": "Черновик"
42
+ },
43
+ {
44
+ "value": "signed",
45
+ "label": "Подписан"
46
+ }
47
+ ],
48
+ "value": {
49
+ "value": "draft",
50
+ "label": "Черновик"
51
+ }
52
+ }
53
+ ]
54
+ },
55
+ "when": {
56
+ "intent": "record.load",
57
+ "payload": {
58
+ "record_id": "record-1",
59
+ "collection": "contracts",
60
+ "reason": "connected"
61
+ }
62
+ },
63
+ "then": {
64
+ "answer": "applied",
65
+ "answer_must_include": [
66
+ "record"
67
+ ],
68
+ "note": "The label travels with the value, so the view shows «Россия» without a second read. A short list arrives as options and is chosen from in place; a long one is named by source and paged by field.options."
69
+ }
70
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "opening-a-reference-is-the-hosts-decision",
3
+ "intent": "record.open",
4
+ "summary": "Activating a reference value asks the host what to open; the component follows no link and opens no window by itself.",
5
+ "given": {"capabilities": []},
6
+ "when": {"intent": "record.open", "payload": {"record_id": "record-1", "collection": "pages",
7
+ "field": "section", "value": {"value": "record-0317", "label": "Платежи", "href": "/admin/cms/record-0317"}}},
8
+ "then": {
9
+ "answer": "applied",
10
+ "note": "The host may push another panel, route to a full page, or refuse. Two decisions it must keep: following the href would be the component routing for the consumer, and opening a window itself would be the component deciding whether a half-edited window may be covered. Each window is its own element, so one left mid-edit underneath another keeps its unsaved changes."
11
+ }
12
+ }