1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Command affordance availability (phase 30, item M17; ADR-7).
//!
//! The core derives, per open source, which command affordances the UI
//! must render disabled — computed from the driver's declared
//! capabilities, never from format matches in UI code. The map lists
//! only *restricted* affordances: absence means enabled, so capable
//! sources change nothing and existing pins stay stable.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::port::{DriverCapabilities, Support};
/// Stable key naming why an affordance is unavailable for the open
/// source. Locale-neutral on the wire (ADR-4): the UI resolves it
/// through its catalog; agents read the snake_case spelling.
///
/// `#[non_exhaustive]` (M32): every new capability affordance adds a
/// variant; clients match with a wildcard arm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DisabledReason {
/// The driver cannot create tables for this source.
CreateTables,
/// The driver cannot drop tables for this source.
DropTables,
/// The driver cannot write rows for this source (read-only session).
WriteRows,
/// The source exposes no relationships.
NoRelationships,
/// The source has no database-password surface (save-as / password
/// change are unavailable).
NoPasswords,
/// The driver cannot alter the schema in place (the design view's
/// edit controls).
AlterTables,
/// The driver cannot rename tables in place (phase 72, item M81).
RenameTables,
/// The composition cannot create new database files.
CreateDatabase,
}
/// Stable affordance ids shared by the ribbon spec, the navigation
/// pane's context menu and the datasheet's new-record row.
pub mod ids {
/// Ribbon Create ▸ Tables (the new-table flow).
pub const CREATE_TABLE: &str = "table";
/// Ribbon External Data ▸ Import File (step 58.1, item M50). The
/// chain creates the table and writes its rows.
pub const IMPORT_FILE: &str = "import_file";
/// Ribbon Database Tools ▸ Relationships (the mapper pane).
pub const RELATIONSHIPS: &str = "relationships";
/// Relationship-mapper create affordance (step 41.1, item M45).
/// Gates on `Support::Yes` — the dispatch gate refuses anything less.
pub const RELATIONSHIPS_CREATE: &str = "relationships.create";
/// Relationship-mapper per-row delete affordance (step 41.1, item M45).
pub const RELATIONSHIPS_DELETE: &str = "relationships.delete";
/// Navigation-pane context menu Delete.
pub const NAV_DELETE: &str = "nav.delete";
/// Navigation-pane context menu Rename of a table (phase 72, M81);
/// sidecar objects (queries, forms, reports) are always renamable.
pub const NAV_RENAME: &str = "nav.rename";
/// Datasheet new-record row.
pub const NEW_RECORD: &str = "datasheet.new_record";
/// Datasheet record deletion (row context menu / Del key, step 40.3,
/// item M44).
pub const DELETE_RECORD: &str = "datasheet.delete_record";
/// Datasheet in-place cell editing (step 40.4, item L23).
pub const EDIT_RECORD: &str = "datasheet.edit_record";
/// Backstage Save ▸ Save a copy (step 27.2, M14).
pub const SAVE_AS: &str = "backstage.save_as";
/// Backstage Save ▸ Change password (step 27.2, M14).
pub const CHANGE_PASSWORD: &str = "backstage.change_password";
/// Backstage New ▸ Create Database (step 42.2, item L18).
pub const CREATE_DATABASE: &str = "backstage.create_database";
/// Design view ▸ Add Field (step 42.2, item L18). The four design
/// ids share the `alter_table` capability.
pub const DESIGN_ADD_FIELD: &str = "design.add_field";
/// Design view ▸ Rename (step 42.2, item L18).
pub const DESIGN_RENAME_FIELD: &str = "design.rename_field";
/// Design view ▸ Delete field (step 42.2, item L18).
pub const DESIGN_DROP_FIELD: &str = "design.drop_field";
/// Design view ▸ Required toggle (step 42.2, item L18).
pub const DESIGN_SET_FIELD_PROPERTIES: &str = "design.set_field_properties";
}
/// The availability map for one open source: disabled affordance id →
/// reason. Only restricted affordances appear.
pub fn affordances(caps: &DriverCapabilities) -> BTreeMap<String, DisabledReason> {
let mut map = BTreeMap::new();
if !caps.create_table {
map.insert(ids::CREATE_TABLE.to_owned(), DisabledReason::CreateTables);
}
if !caps.create_table {
map.insert(ids::IMPORT_FILE.to_owned(), DisabledReason::CreateTables);
} else if !caps.write_rows {
map.insert(ids::IMPORT_FILE.to_owned(), DisabledReason::WriteRows);
}
if !caps.drop_table {
map.insert(ids::NAV_DELETE.to_owned(), DisabledReason::DropTables);
}
if !caps.rename_table {
map.insert(ids::NAV_RENAME.to_owned(), DisabledReason::RenameTables);
}
if !caps.write_rows {
map.insert(ids::NEW_RECORD.to_owned(), DisabledReason::WriteRows);
map.insert(ids::DELETE_RECORD.to_owned(), DisabledReason::WriteRows);
map.insert(ids::EDIT_RECORD.to_owned(), DisabledReason::WriteRows);
}
if caps.relationships == Support::No {
map.insert(
ids::RELATIONSHIPS.to_owned(),
DisabledReason::NoRelationships,
);
}
if caps.relationships != Support::Yes {
map.insert(
ids::RELATIONSHIPS_CREATE.to_owned(),
DisabledReason::NoRelationships,
);
map.insert(
ids::RELATIONSHIPS_DELETE.to_owned(),
DisabledReason::NoRelationships,
);
}
if !caps.passwords {
map.insert(ids::SAVE_AS.to_owned(), DisabledReason::NoPasswords);
map.insert(ids::CHANGE_PASSWORD.to_owned(), DisabledReason::NoPasswords);
}
if !caps.alter_table {
map.insert(
ids::DESIGN_ADD_FIELD.to_owned(),
DisabledReason::AlterTables,
);
map.insert(
ids::DESIGN_RENAME_FIELD.to_owned(),
DisabledReason::AlterTables,
);
map.insert(
ids::DESIGN_DROP_FIELD.to_owned(),
DisabledReason::AlterTables,
);
map.insert(
ids::DESIGN_SET_FIELD_PROPERTIES.to_owned(),
DisabledReason::AlterTables,
);
}
if !caps.create_database {
map.insert(
ids::CREATE_DATABASE.to_owned(),
DisabledReason::CreateDatabase,
);
}
map
}