@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.
- package/README.md +4 -4
- 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
- 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
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-reader-is-offered-nothing-to-change.json +23 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/a-stale-confirmation-is-asked-again.json +39 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/conditions-arrive-declared.json +82 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-applied-answers-with-the-id.json +24 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/create-field-errors-are-refused.json +22 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/field-presentation-keys-are-declared.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-applied-is-one-revision.json +16 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-field-errors-are-refused.json +16 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-may-ask-for-a-confirmation.json +38 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/group-save-stale-revision-is-a-conflict.json +16 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-applied-carries-the-object.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-for-create-is-an-empty-object.json +24 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-late-answer-is-dropped.json +15 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-refused-is-explained.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/load-unavailable-is-honest.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/lookup-arrives-with-its-label.json +70 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/opening-a-reference-is-the-hosts-decision.json +12 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-carry-the-source-and-a-limit.json +33 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-page-carries-a-cursor.json +15 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-refused-is-explained.json +14 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-by-source-reaches-the-host.json +30 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-search-reaches-the-host.json +14 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/options-without-the-capability.json +14 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/rare-fields-arrive-declared.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/removing-a-file-is-a-save.json +13 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-applied-is-one-revision.json +26 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-may-ask-for-a-confirmation.json +36 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/save-stale-revision-is-a-conflict.json +23 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/the-address-is-the-hosts.json +12 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-answers-with-the-stored-file.json +14 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/upload-refused-is-explained.json +14 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-create-shared-may-be-refused.json +27 -0
- 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
- 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
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-refused-is-explained.json +25 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-profile-unavailable-without-the-capability.json +23 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-rename-changes-the-label-only.json +26 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-reset-answers-with-what-now-resolves.json +30 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-is-one-layer-one-revision.json +35 -0
- 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
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-save-rejects-entries-by-kind.json +36 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/cases/view-settings-without-the-capability.json +30 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/kit/manifest.json +66 -0
- package/distr/core/contracts/ports/simai.property-view-port/1.14.0/port.json +2484 -0
- package/distr/smart/field-renderers/js/field-renderers.min.js +128 -87
- package/package-manifest.json +57 -4
- package/package.json +5 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "save-applied-is-one-revision",
|
|
3
|
+
"intent": "record.save",
|
|
4
|
+
"summary": "A whole-object save is one revision carrying only the changed fields.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": []
|
|
7
|
+
},
|
|
8
|
+
"when": {
|
|
9
|
+
"intent": "record.save",
|
|
10
|
+
"payload": {
|
|
11
|
+
"record_id": "record-1",
|
|
12
|
+
"collection": "pages",
|
|
13
|
+
"values": {
|
|
14
|
+
"title": "Другое название",
|
|
15
|
+
"slug": "other"
|
|
16
|
+
},
|
|
17
|
+
"revision": "7"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"then": {
|
|
21
|
+
"answer": "applied",
|
|
22
|
+
"answer_must_include": [
|
|
23
|
+
"record"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "save-may-ask-for-a-confirmation",
|
|
3
|
+
"intent": "record.save",
|
|
4
|
+
"summary": "A whole-object save may be confirmed first, the same way as a group save.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": [],
|
|
7
|
+
"confirms": [
|
|
8
|
+
"record.save"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"when": {
|
|
12
|
+
"intent": "record.save",
|
|
13
|
+
"payload": {
|
|
14
|
+
"record_id": "record-1",
|
|
15
|
+
"collection": "contracts",
|
|
16
|
+
"values": {
|
|
17
|
+
"type": {
|
|
18
|
+
"value": "framework",
|
|
19
|
+
"label": "Рамочный"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"revision": "7"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"then": {
|
|
26
|
+
"answer": "refused",
|
|
27
|
+
"answer_must_include": [
|
|
28
|
+
"reason",
|
|
29
|
+
"confirm"
|
|
30
|
+
],
|
|
31
|
+
"data_must_include": [
|
|
32
|
+
"confirm"
|
|
33
|
+
],
|
|
34
|
+
"note": "reason is confirm_required. Raised again with confirm set to the digest, the same payload is applied."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "save-stale-revision-is-a-conflict",
|
|
3
|
+
"intent": "record.save",
|
|
4
|
+
"summary": "A whole-object save against a moved revision is a conflict, never an overwrite.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": []
|
|
7
|
+
},
|
|
8
|
+
"when": {
|
|
9
|
+
"intent": "record.save",
|
|
10
|
+
"payload": {
|
|
11
|
+
"record_id": "record-1",
|
|
12
|
+
"collection": "pages",
|
|
13
|
+
"values": {
|
|
14
|
+
"title": "Другое название"
|
|
15
|
+
},
|
|
16
|
+
"revision": "6"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"then": {
|
|
20
|
+
"answer": "conflict",
|
|
21
|
+
"note": "The person's values stay on screen with the conflict said."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "the-address-is-the-hosts",
|
|
3
|
+
"intent": "record.load",
|
|
4
|
+
"summary": "A window's address comes with the object, and a host that sends none gets no copy or open controls rather than an invented link.",
|
|
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.address is optional. The component copies it and opens it and constructs nothing: a composite that guessed at a URL would be guessing at the consumer's routing, and would be wrong first in whichever consumer did not match the guess."
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "upload-answers-with-the-stored-file",
|
|
3
|
+
"intent": "field.upload",
|
|
4
|
+
"capability": "field-upload",
|
|
5
|
+
"summary": "A file the person chose is stored by the host, which answers with it in the shape the field reads.",
|
|
6
|
+
"given": {"capabilities": ["field-upload"]},
|
|
7
|
+
"when": {"intent": "field.upload", "payload": {"record_id": "record-1", "collection": "pages",
|
|
8
|
+
"field": "document", "file": "<the File the person chose>"}},
|
|
9
|
+
"then": {
|
|
10
|
+
"answer": "applied",
|
|
11
|
+
"answer_must_include": ["file"],
|
|
12
|
+
"note": "The answer carries {value, label, mime?, href?} -- the same shape the field already reads -- so the view shows the stored file without a second read. The File is passed as it is: a component that encoded it to base64 would double the bytes of every upload for nothing."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "upload-refused-is-explained",
|
|
3
|
+
"intent": "field.upload",
|
|
4
|
+
"capability": "field-upload",
|
|
5
|
+
"summary": "A file the host will not take is refused with a reason the field shows, and nothing is added to the value.",
|
|
6
|
+
"given": {"capabilities": ["field-upload"]},
|
|
7
|
+
"when": {"intent": "field.upload", "payload": {"record_id": "record-1", "collection": "pages",
|
|
8
|
+
"field": "document", "file": "<a file too large, or of a kind the host rejects>"}},
|
|
9
|
+
"then": {
|
|
10
|
+
"answer": "refused",
|
|
11
|
+
"answer_must_include": ["reason"],
|
|
12
|
+
"note": "The reason is shown under the field. A silent failure leaves a person looking at a field that did not change and no way to know why."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"intent": "view.create",
|
|
3
|
+
"summary": "Making a view answers with the record through it: a person who has just made one is looking at it.",
|
|
4
|
+
"given": {
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"view-settings"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"when": {
|
|
10
|
+
"intent": "view.create",
|
|
11
|
+
"payload": {
|
|
12
|
+
"record_id": "record-1",
|
|
13
|
+
"collection": "pages",
|
|
14
|
+
"label": "Для продаж",
|
|
15
|
+
"shared": true,
|
|
16
|
+
"view": {
|
|
17
|
+
"schema_version": 1,
|
|
18
|
+
"sections": {},
|
|
19
|
+
"groups": {},
|
|
20
|
+
"fields": {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"then": {
|
|
25
|
+
"answer": "applied",
|
|
26
|
+
"answer_must_include": [
|
|
27
|
+
"record"
|
|
28
|
+
],
|
|
29
|
+
"note": "The answer carries the record and its view, so the new view's id and its place in the list come from the host rather than being guessed at here."
|
|
30
|
+
},
|
|
31
|
+
"id": "view-create-answers-with-the-record-through-the-new-view"
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"intent": "view.create",
|
|
3
|
+
"summary": "A person who may not write the shared layer may not make a shared view either.",
|
|
4
|
+
"given": {
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"view-settings"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"when": {
|
|
10
|
+
"intent": "view.create",
|
|
11
|
+
"payload": {
|
|
12
|
+
"record_id": "record-1",
|
|
13
|
+
"collection": "pages",
|
|
14
|
+
"label": "Для всех",
|
|
15
|
+
"shared": true,
|
|
16
|
+
"view": {}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"then": {
|
|
20
|
+
"answer": "refused",
|
|
21
|
+
"answer_must_include": [
|
|
22
|
+
"reason"
|
|
23
|
+
],
|
|
24
|
+
"note": "The same right that governs the shared layer governs a shared view; saying so is what stops a person making one and losing it on the next read."
|
|
25
|
+
},
|
|
26
|
+
"id": "view-create-shared-may-be-refused"
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"intent": "view.delete",
|
|
3
|
+
"summary": "Removing a view answers with the record through whichever view is now current.",
|
|
4
|
+
"given": {
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"view-settings"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"when": {
|
|
10
|
+
"intent": "view.delete",
|
|
11
|
+
"payload": {
|
|
12
|
+
"record_id": "record-1",
|
|
13
|
+
"collection": "pages",
|
|
14
|
+
"profile": "mine"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"then": {
|
|
18
|
+
"answer": "applied",
|
|
19
|
+
"answer_must_include": [
|
|
20
|
+
"record"
|
|
21
|
+
],
|
|
22
|
+
"note": "Deleting the view a person was looking at leaves them looking at something; which one is the host's to decide and is said in the answer."
|
|
23
|
+
},
|
|
24
|
+
"id": "view-delete-applied-answers-with-what-is-left"
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"intent": "view.delete",
|
|
3
|
+
"summary": "The last shared view cannot be removed: a scope with none has nothing for a person who has made none of their own.",
|
|
4
|
+
"given": {
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"view-settings"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"when": {
|
|
10
|
+
"intent": "view.delete",
|
|
11
|
+
"payload": {
|
|
12
|
+
"record_id": "record-1",
|
|
13
|
+
"collection": "pages",
|
|
14
|
+
"profile": "default"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"then": {
|
|
18
|
+
"answer": "refused",
|
|
19
|
+
"answer_must_include": [
|
|
20
|
+
"reason"
|
|
21
|
+
],
|
|
22
|
+
"note": "The component does not offer the control in that case either, so this refusal is a guard rather than the way a person finds out."
|
|
23
|
+
},
|
|
24
|
+
"id": "view-delete-the-last-shared-one-is-refused"
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-profile-applied-carries-the-record-through-it",
|
|
3
|
+
"intent": "view.profile",
|
|
4
|
+
"summary": "Choosing a view answers with the record read through it, because a view is a whole arrangement rather than a filter over the one already on screen.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": [
|
|
7
|
+
"view-settings"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"when": {
|
|
11
|
+
"intent": "view.profile",
|
|
12
|
+
"payload": {
|
|
13
|
+
"record_id": "record-1",
|
|
14
|
+
"collection": "pages",
|
|
15
|
+
"profile": "expert"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"then": {
|
|
19
|
+
"answer": "applied",
|
|
20
|
+
"answer_must_include": [
|
|
21
|
+
"record"
|
|
22
|
+
],
|
|
23
|
+
"note": "Answering applied with nothing else would leave the component holding the old arrangement under a new name."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-profile-refused-is-explained",
|
|
3
|
+
"intent": "view.profile",
|
|
4
|
+
"summary": "A view this person may not open is refused with a reason, not silently ignored.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": [
|
|
7
|
+
"view-settings"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"when": {
|
|
11
|
+
"intent": "view.profile",
|
|
12
|
+
"payload": {
|
|
13
|
+
"record_id": "record-1",
|
|
14
|
+
"collection": "pages",
|
|
15
|
+
"profile": "profile-forbidden"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"then": {
|
|
19
|
+
"answer": "refused",
|
|
20
|
+
"answer_must_include": [
|
|
21
|
+
"reason"
|
|
22
|
+
],
|
|
23
|
+
"note": "Without the reason a chooser that snaps back looks broken rather than guarded."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-profile-unavailable-without-the-capability",
|
|
3
|
+
"intent": "view.profile",
|
|
4
|
+
"summary": "A host that does not configure views cannot switch between them either.",
|
|
5
|
+
"given": {
|
|
6
|
+
"capabilities": []
|
|
7
|
+
},
|
|
8
|
+
"when": {
|
|
9
|
+
"intent": "view.profile",
|
|
10
|
+
"payload": {
|
|
11
|
+
"record_id": "record-1",
|
|
12
|
+
"collection": "pages",
|
|
13
|
+
"profile": "expert"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"then": {
|
|
17
|
+
"answer": "unavailable",
|
|
18
|
+
"answer_must_include": [
|
|
19
|
+
"reason"
|
|
20
|
+
],
|
|
21
|
+
"note": "The component draws no chooser without the capability; this is what a host answers if one is asked for anyway."
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"intent": "view.rename",
|
|
3
|
+
"summary": "A rename changes the label. The id is permanent, so anything pointing at the view keeps pointing at it.",
|
|
4
|
+
"given": {
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"view-settings"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"when": {
|
|
10
|
+
"intent": "view.rename",
|
|
11
|
+
"payload": {
|
|
12
|
+
"record_id": "record-1",
|
|
13
|
+
"collection": "pages",
|
|
14
|
+
"profile": "expert",
|
|
15
|
+
"label": "Экспертный режим"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"then": {
|
|
19
|
+
"answer": "applied",
|
|
20
|
+
"answer_must_include": [
|
|
21
|
+
"record"
|
|
22
|
+
],
|
|
23
|
+
"note": "Answering with the record is how the list of views comes back renamed without a second read."
|
|
24
|
+
},
|
|
25
|
+
"id": "view-rename-changes-the-label-only"
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-reset-answers-with-what-now-resolves",
|
|
3
|
+
"intent": "view.reset",
|
|
4
|
+
"capability": "view-settings",
|
|
5
|
+
"summary": "Clearing a layer answers with the arrangement that resolves after it is gone, so the panel redraws without reading the record again.",
|
|
6
|
+
"given": {
|
|
7
|
+
"capabilities": [
|
|
8
|
+
"view-settings"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"when": {
|
|
12
|
+
"intent": "view.reset",
|
|
13
|
+
"payload": {
|
|
14
|
+
"record_id": null,
|
|
15
|
+
"collection": "pages",
|
|
16
|
+
"target": {
|
|
17
|
+
"kind": "personal",
|
|
18
|
+
"revision": "v7"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"then": {
|
|
23
|
+
"answer": "applied",
|
|
24
|
+
"answer_must_include": [
|
|
25
|
+
"revision",
|
|
26
|
+
"view"
|
|
27
|
+
],
|
|
28
|
+
"note": "A reset is a write against the revision, not a deletion, and the component never sends its own idea of the defaults -- the host owns those, and writing them would overwrite them with a guess. The answer carries what now resolves because a second read would show the person a reload for a change they already made."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-save-against-a-stale-revision-is-a-conflict",
|
|
3
|
+
"intent": "view.save",
|
|
4
|
+
"capability": "view-settings",
|
|
5
|
+
"summary": "A save against a revision someone else has moved past is a conflict carrying the layer as it now stands.",
|
|
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": "shared",
|
|
18
|
+
"revision": "v2"
|
|
19
|
+
},
|
|
20
|
+
"view": {
|
|
21
|
+
"schema_version": 1,
|
|
22
|
+
"sections": {},
|
|
23
|
+
"groups": {},
|
|
24
|
+
"fields": {}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"then": {
|
|
29
|
+
"answer": "conflict",
|
|
30
|
+
"answer_must_include": [
|
|
31
|
+
"revision",
|
|
32
|
+
"view"
|
|
33
|
+
],
|
|
34
|
+
"note": "The conflict carries the stored layer, not only the revision. Without it the component would be left drawing an arrangement nobody stored, and the person would go on dragging fields in a view that no longer exists."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "view-save-is-one-layer-one-revision",
|
|
3
|
+
"intent": "view.save",
|
|
4
|
+
"capability": "view-settings",
|
|
5
|
+
"summary": "A change to the arrangement writes the whole layer against its revision and answers with the revision the layer now has.",
|
|
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
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"then": {
|
|
29
|
+
"answer": "applied",
|
|
30
|
+
"answer_must_include": [
|
|
31
|
+
"revision"
|
|
32
|
+
],
|
|
33
|
+
"note": "The layer is sent whole, never as a patch of the entry that changed. Two people reordering the same view with per-entry writes would interleave into an arrangement neither of them chose; whole-layer compare-and-set makes them collide, which is the same reason a group saves as one revision."
|
|
34
|
+
}
|
|
35
|
+
}
|