@simai-studio/ui 5.20261023.16 → 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 (70) hide show
  1. package/README.md +4 -4
  2. package/distr/component/accordion/css/accordion.min.css +1 -1
  3. package/distr/component/accordion/js/accordion.min.js +1 -1
  4. package/distr/component/modal/css/modal.min.css +1 -1
  5. package/distr/component/modal/js/modal.min.js +1 -1
  6. 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
  7. 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
  8. 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
  9. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-reader-is-offered-nothing-to-change.json +23 -0
  10. 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
  11. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-stale-confirmation-is-asked-again.json +39 -0
  12. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/conditions-arrive-declared.json +82 -0
  13. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-applied-answers-with-the-id.json +24 -0
  14. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-field-errors-are-refused.json +22 -0
  15. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/field-presentation-keys-are-declared.json +12 -0
  16. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-applied-is-one-revision.json +16 -0
  17. 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
  18. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-field-errors-are-refused.json +16 -0
  19. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-may-ask-for-a-confirmation.json +38 -0
  20. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-stale-revision-is-a-conflict.json +16 -0
  21. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-applied-carries-the-object.json +12 -0
  22. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-for-create-is-an-empty-object.json +24 -0
  23. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-late-answer-is-dropped.json +15 -0
  24. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-refused-is-explained.json +12 -0
  25. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-unavailable-is-honest.json +12 -0
  26. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/lookup-arrives-with-its-label.json +70 -0
  27. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/opening-a-reference-is-the-hosts-decision.json +12 -0
  28. 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
  29. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-carry-the-source-and-a-limit.json +33 -0
  30. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-page-carries-a-cursor.json +15 -0
  31. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-refused-is-explained.json +14 -0
  32. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-by-source-reaches-the-host.json +30 -0
  33. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-reaches-the-host.json +14 -0
  34. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-without-the-capability.json +14 -0
  35. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/rare-fields-arrive-declared.json +12 -0
  36. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/removing-a-file-is-a-save.json +13 -0
  37. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-applied-is-one-revision.json +26 -0
  38. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-may-ask-for-a-confirmation.json +36 -0
  39. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-stale-revision-is-a-conflict.json +23 -0
  40. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/the-address-is-the-hosts.json +12 -0
  41. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-answers-with-the-stored-file.json +14 -0
  42. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-refused-is-explained.json +14 -0
  43. 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
  44. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-create-shared-may-be-refused.json +27 -0
  45. 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
  46. 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
  47. 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
  48. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-refused-is-explained.json +25 -0
  49. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-unavailable-without-the-capability.json +23 -0
  50. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-rename-changes-the-label-only.json +26 -0
  51. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-reset-answers-with-what-now-resolves.json +30 -0
  52. 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
  53. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-is-one-layer-one-revision.json +35 -0
  54. 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
  55. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-rejects-entries-by-kind.json +36 -0
  56. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-settings-without-the-capability.json +30 -0
  57. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/manifest.json +66 -0
  58. package/distr/core/contracts/ports/simai.property-view-port/1.14.0/port.json +2484 -0
  59. package/distr/core/contracts/public-variables.v1.json +7 -0
  60. package/distr/core/css/core.css +14 -0
  61. package/distr/core/css/core.min.css +1 -1
  62. package/distr/smart/drawer/css/drawer.min.css +1 -1
  63. package/distr/smart/drawer/js/drawer.min.js +9 -8
  64. package/distr/smart/drawer/smart.manifest.json +12 -6
  65. package/distr/smart/field-renderers/js/field-renderers.min.js +128 -87
  66. package/distr/smart/modal/js/modal.min.js +1 -1
  67. package/distr/smart/modal/smart.manifest.json +3 -2
  68. package/distr/smart/modal/template/default.js +1 -1
  69. package/package-manifest.json +70 -17
  70. package/package.json +5 -4
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "id": "opening-the-record-as-a-page-is-offered-only-where-it-leads-somewhere",
3
+ "intent": "record.open",
4
+ "capability": "record-open",
5
+ "summary": "The control that opens the record away from the panel is drawn only where the host declares it can open one.",
6
+ "given": {
7
+ "capabilities": [
8
+ "record-open"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "record.open",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "pages",
16
+ "field": null,
17
+ "value": {
18
+ "value": "record-1",
19
+ "label": "Платежи",
20
+ "href": "/admin/cms/record-1"
21
+ }
22
+ }
23
+ },
24
+ "then": {
25
+ "answer": "applied",
26
+ "note": "A host without this capability is not broken: the panel is the whole of the record, and the control is not drawn at all. An address is not enough to promise a page -- a host may have one to copy and nowhere to go, which is how a control that did nothing came to stand in every panel's strip."
27
+ }
28
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "id": "options-carry-the-source-and-a-limit",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "The candidates of a lookup are asked for by its source, at most limit at a time, and answered as a page with a cursor.",
6
+ "given": {
7
+ "capabilities": [
8
+ "field-options"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "field.options",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "contracts",
16
+ "field": "country",
17
+ "cursor": null,
18
+ "search": "",
19
+ "source": "countries",
20
+ "limit": 20
21
+ }
22
+ },
23
+ "then": {
24
+ "answer": "applied",
25
+ "answer_must_include": [
26
+ "options_page"
27
+ ],
28
+ "data_must_include": [
29
+ "options_page"
30
+ ],
31
+ "note": "items are {value, label}. A page holds no more than limit items; next_cursor null or absent says it is the last, whatever its length. total is optional."
32
+ }
33
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "options-page-carries-a-cursor",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "Candidates arrive a page at a time with the cursor for the next page, never as the whole set.",
6
+ "given": {"capabilities": ["field-options"]},
7
+ "when": {"intent": "field.options", "payload": {"record_id": "record-1", "collection": "pages",
8
+ "field": "section", "cursor": null, "search": ""}},
9
+ "then": {
10
+ "answer": "applied",
11
+ "answer_must_include": ["options_page"],
12
+ "data_must_include": ["options_page"],
13
+ "note": "A host that answers with every candidate makes the component's paging meaningless and its own query unbounded. next_cursor absent or null means this was the last page."
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "options-refused-is-explained",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "A field whose candidates the host will not serve is refused with a reason, and the field falls back to its value rather than to an empty list.",
6
+ "given": {"capabilities": ["field-options"]},
7
+ "when": {"intent": "field.options", "payload": {"record_id": "record-1", "collection": "pages",
8
+ "field": "forbidden", "cursor": null, "search": ""}},
9
+ "then": {
10
+ "answer": "refused",
11
+ "answer_must_include": ["reason"],
12
+ "note": "An empty page and a refusal look identical in a list, and only one of them is something a person can act on."
13
+ }
14
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "options-search-by-source-reaches-the-host",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "A search inside a lookup is the host's, over the whole reference list, case-insensitively.",
6
+ "given": {
7
+ "capabilities": [
8
+ "field-options"
9
+ ]
10
+ },
11
+ "when": {
12
+ "intent": "field.options",
13
+ "payload": {
14
+ "record_id": "record-1",
15
+ "collection": "contracts",
16
+ "field": "country",
17
+ "cursor": null,
18
+ "search": "рос",
19
+ "source": "countries",
20
+ "limit": 20
21
+ }
22
+ },
23
+ "then": {
24
+ "answer": "applied",
25
+ "answer_must_include": [
26
+ "options_page"
27
+ ],
28
+ "note": "«рос» finds «Россия» and «Белоруссия» wherever they are in the list. The component never filters a page it holds: the page is not the whole list."
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "options-search-reaches-the-host",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "What the person typed is answered by the host, not by filtering the page the component already holds.",
6
+ "given": {"capabilities": ["field-options"]},
7
+ "when": {"intent": "field.options", "payload": {"record_id": "record-1", "collection": "pages",
8
+ "field": "section", "cursor": null, "search": "платеж"}},
9
+ "then": {
10
+ "answer": "applied",
11
+ "answer_must_include": ["options_page"],
12
+ "note": "The page is not the whole set, so filtering it locally would hide every match that is not on it. A search resets the paging: the cursor is null again."
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "options-without-the-capability",
3
+ "intent": "field.options",
4
+ "capability": "field-options",
5
+ "summary": "A host that does not announce the capability is never asked, and the field shows its value read-only.",
6
+ "given": {"capabilities": []},
7
+ "when": {"intent": "field.options", "payload": {"record_id": "record-1", "collection": "pages",
8
+ "field": "section", "cursor": null, "search": ""}},
9
+ "then": {
10
+ "answer": "unavailable",
11
+ "answer_must_include": ["reason"],
12
+ "note": "This is not a degraded state to apologise for: a reference field with no picker shows the value it has, which is strictly better than an empty control over a value that exists."
13
+ }
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "rare-fields-arrive-declared",
3
+ "intent": "record.load",
4
+ "summary": "Rare sections, groups and fields are marked by the host and sent every time; the component hides them until asked and changes no value by doing so.",
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": "A host that filters rare fields out of the answer makes «Показать редкие поля» a second read, and a person toggling a view twice would see two different objects. Marking them is the host's job; hiding them is the component's."
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "id": "removing-a-file-is-a-save",
3
+ "intent": "group.save",
4
+ "summary": "Removing a stored file is a value change like any other: it travels in group.save and nothing is deleted until the button is pressed.",
5
+ "given": {"capabilities": ["field-upload"]},
6
+ "when": {"intent": "group.save", "payload": {"record_id": "record-1", "collection": "pages",
7
+ "section": "main", "group": "files", "values": {"document": []}, "revision": "7"}},
8
+ "then": {
9
+ "answer": "applied",
10
+ "answer_must_include": ["record"],
11
+ "note": "There is deliberately no remove intent. A file that disappeared from storage the moment someone pressed a cross, before saving, would be the one edit in this component that could not be cancelled -- and cancelling is the whole promise of saving by the button."
12
+ }
13
+ }