feat(app-iced): 实例页「导入已有版本」向导(M223)
页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导: | 段 | 画什么 | |---|---| | Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) | | Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 | | Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) | | Done | 实例名 + 运行目录 + 提示原文 | - 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据; - 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState; - 在等回程时主按钮禁用(连点「确认」不会写两条实例); - i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
This commit is contained in:
1 parent
5a59cb33b2
commit
41caae3571
16 files changed
+1265
-2
No files matched your search
@@ -0,0 +1,176 @@
|
||||
//! 「导入已有版本」向导的落点(M223 GUIADOPT)。
|
||||
//!
|
||||
//! ## 四段各自发什么、等什么
|
||||
//!
|
||||
//! | 用户动作 | 发什么 | 等什么回程 |
|
||||
//! |---|---|---|
|
||||
//! | 页头「导入」/ [`AdoptMessage::Open`] | `AdoptCommand::Scan` | `AdoptEvent::Scan` |
|
||||
//! | 选目标 + 点「预检」/ [`AdoptMessage::Check`] | `AdoptCommand::Preview` | `Planned` / `Failed` |
|
||||
//! | 点「确认导入」/ [`AdoptMessage::Confirm`] | `AdoptCommand::Confirm` | `Done` / `Failed` |
|
||||
//! | 「返回」/ [`AdoptMessage::Back`] | `AdoptCommand::Scan`(重扫一次,顺手清掉预检/失败) | `Scan` |
|
||||
//! | 「关闭」/ [`AdoptMessage::Close`] | `AdoptCommand::Dismiss` | `Cleared` |
|
||||
//!
|
||||
//! ## 三段纪律
|
||||
//!
|
||||
//! 1. **不在本地判「能不能导入」**:候选可不可用、落点在哪、有哪些问题全部来自 presenter 的回程
|
||||
//! (`AdoptState`)。这里只把「用户点了什么」翻成命令;
|
||||
//! 2. **确认只发用户填过的那份请求**(版本 + 名字 + 目标档),预检与确认**同一份**——
|
||||
//! 于是「预检 A、写 B」这种错位不可能发生;
|
||||
//! 3. **在等回程时把主按钮并掉**(`Form::busy`):连点两次「确认」不会写两条实例记录。
|
||||
|
||||
use iced::Task;
|
||||
use unlucky_presenter::dto::{AdoptCommand, AdoptRequestView, AppCommand};
|
||||
|
||||
use crate::app::adopt::{AdoptMessage, Busy};
|
||||
|
||||
use super::super::message::Message;
|
||||
use super::super::state::App;
|
||||
|
||||
impl App {
|
||||
/// 向导的一条消息。
|
||||
pub(super) fn on_adopt(&mut self, message: AdoptMessage) -> Task<Message> {
|
||||
match message {
|
||||
AdoptMessage::Open => {
|
||||
self.adopt_form.open();
|
||||
self.dispatch_adopt(AdoptCommand::Scan);
|
||||
}
|
||||
AdoptMessage::Close => {
|
||||
self.adopt_form.close();
|
||||
self.dispatch_adopt(AdoptCommand::Dismiss);
|
||||
}
|
||||
AdoptMessage::Select(version_id) => {
|
||||
self.adopt_form.selected = Some(version_id);
|
||||
}
|
||||
AdoptMessage::Name(name) => self.adopt_form.name = name,
|
||||
AdoptMessage::Target(target) => self.adopt_form.target = target,
|
||||
AdoptMessage::Check => {
|
||||
if let Some(request) = self.adopt_request() {
|
||||
self.adopt_form.busy = Busy::Checking;
|
||||
self.dispatch_adopt(AdoptCommand::Preview(Box::new(request)));
|
||||
}
|
||||
}
|
||||
AdoptMessage::Confirm => {
|
||||
if let Some(request) = self.adopt_request() {
|
||||
self.adopt_form.busy = Busy::Importing;
|
||||
self.dispatch_adopt(AdoptCommand::Confirm(Box::new(request)));
|
||||
}
|
||||
}
|
||||
AdoptMessage::Back => {
|
||||
// 退回候选列表 = 重新扫一次:`scan` 会把上一轮的预检/失败一起作废,
|
||||
// 于是界面回到 Pick 段,不需要再给 core 加一个「清失败」的命令。
|
||||
self.adopt_form.busy = Busy::Scanning;
|
||||
self.dispatch_adopt(AdoptCommand::Scan);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// 回程到了:这一条命令已经交卷(`pump` 收到 `CoreEvent::Adopt` 时调,见 `update/clock.rs`)。
|
||||
pub(super) fn settle_adopt(&mut self) {
|
||||
self.adopt_form.settled();
|
||||
}
|
||||
|
||||
/// 当前表单能拼出的请求(没选版本就没有请求——按钮在那边也是禁用的)。
|
||||
fn adopt_request(&self) -> Option<AdoptRequestView> {
|
||||
let version_id = self.adopt_form.selected.clone()?;
|
||||
let name = self.adopt_form.name.trim();
|
||||
Some(AdoptRequestView {
|
||||
version_id,
|
||||
name: (!name.is_empty()).then(|| name.to_string()),
|
||||
target: self.adopt_form.target,
|
||||
})
|
||||
}
|
||||
|
||||
fn dispatch_adopt(&mut self, command: AdoptCommand) {
|
||||
self.dispatch(AppCommand::Adopt(Box::new(command)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::dto::{AdoptEvent, AdoptTargetView};
|
||||
use unlucky_presenter::mock::MockBackend;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use crate::app::adopt::{AdoptMessage, Busy};
|
||||
use crate::app::state::App;
|
||||
use crate::cli::Options;
|
||||
use crate::theme::Families;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
fn headless() -> App {
|
||||
App::with_presenter(
|
||||
Options::default(),
|
||||
FAMILIES,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(MockBackend::new()),
|
||||
SettingsBridge::memory_only(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 打开向导 = 表单进入「在等扫描」+ 派一条 `Scan`。
|
||||
///
|
||||
/// 抓得住:`Open` 没派命令(面板永远停在「正在扫描」)、或忘了置 `Busy::Scanning`(没有等待态)。
|
||||
/// 抓不住:扫描真的返回了什么(那在 presenter 的 `host/adopt` 用例里)。
|
||||
#[test]
|
||||
fn opening_the_wizard_asks_for_a_scan() {
|
||||
let mut app = headless();
|
||||
let _ = app.on_adopt(AdoptMessage::Open);
|
||||
assert!(app.adopt_form.open);
|
||||
assert_eq!(app.adopt_form.busy, Busy::Scanning);
|
||||
assert_eq!(
|
||||
app.adopt_form.target,
|
||||
AdoptTargetView::InstanceDir,
|
||||
"缺省不是 isolated"
|
||||
);
|
||||
}
|
||||
|
||||
/// 没选版本时「预检 / 确认」不派命令(否则会发出一条 `version_id` 为空的请求)。
|
||||
///
|
||||
/// 抓得住:`adopt_request()` 在没有选中项时仍拼出请求(空版本 id 打到 core 换一句
|
||||
/// `adopt_version_not_found`,用户看到莫名其妙的失败)。
|
||||
#[test]
|
||||
fn no_request_without_a_selected_version() {
|
||||
let mut app = headless();
|
||||
let _ = app.on_adopt(AdoptMessage::Open);
|
||||
let _ = app.on_adopt(AdoptMessage::Check);
|
||||
assert_eq!(app.adopt_form.busy, Busy::Scanning, "Check 不该改等待态");
|
||||
let _ = app.on_adopt(AdoptMessage::Confirm);
|
||||
assert_eq!(app.adopt_form.busy, Busy::Scanning);
|
||||
}
|
||||
|
||||
/// 选中 + 预检 = 一条 Preview 命令,且请求里的名字是**去过空白**的用户输入。
|
||||
#[test]
|
||||
fn check_sends_the_form_as_a_preview_request() {
|
||||
let mut app = headless();
|
||||
let _ = app.on_adopt(AdoptMessage::Open);
|
||||
let _ = app.on_adopt(AdoptMessage::Select("1.20.1".to_string()));
|
||||
let _ = app.on_adopt(AdoptMessage::Name(" 生存 ".to_string()));
|
||||
let _ = app.on_adopt(AdoptMessage::Target(AdoptTargetView::Shared));
|
||||
let _ = app.on_adopt(AdoptMessage::Check);
|
||||
assert_eq!(app.adopt_form.busy, Busy::Checking);
|
||||
// 请求本体不在 App 上留副本:能证明意图的只有等待态与目标档
|
||||
assert_eq!(app.adopt_form.target, AdoptTargetView::Shared);
|
||||
}
|
||||
|
||||
/// 回程到了就解除等待(`settle_adopt` 由 `pump` 调)。
|
||||
#[test]
|
||||
fn an_incoming_event_settles_the_wait() {
|
||||
let mut app = headless();
|
||||
let _ = app.on_adopt(AdoptMessage::Open);
|
||||
assert!(app.adopt_form.busy.waiting());
|
||||
app.settle_adopt();
|
||||
assert_eq!(app.adopt_form.busy, Busy::Idle);
|
||||
// 事件真相进了 presenter 的状态(界面从它取词)
|
||||
app.state.apply(unlucky_presenter::dto::AppEvent::Core(
|
||||
unlucky_presenter::dto::CoreEvent::Adopt(Box::new(AdoptEvent::Cleared)),
|
||||
));
|
||||
assert!(app.state.adopt.scan.is_none());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user