Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion apps/staged/src-tauri/src/actions/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ fn resolve_branch_repo_context(
/// Persist detected suggestions into an action context, skipping commands the
/// context already has and continuing its sort order.
///
/// A context that had no run action at all before this call gets the first
/// run-type suggestion pinned, so a freshly detected repo arrives with the play
/// button in its card header that detection has always implied. The gate is
/// "had no run actions", not "has nothing pinned": a user who deliberately
/// unpins their run action would otherwise have it pinned right back by the
/// next re-detect. Contexts that predate pinning are covered by the 0028
/// migration instead.
///
/// Persistence belongs inside the detection window: every surface treats the
/// `detecting: false` half of the `repo-actions-detection` broadcast as "this
/// context's action list is final", so a caller that detects here and persists
Expand All @@ -165,20 +173,26 @@ pub(crate) fn persist_suggested_actions(
.max()
.unwrap_or(-1)
+ 1;
let mut pin_next_run_action = !existing_actions
.iter()
.any(|a| a.action_type == ActionType::Run);

for suggestion in suggestions {
if existing_commands.contains(&suggestion.command) {
continue;
}
existing_commands.insert(suggestion.command.clone());
let pinned = pin_next_run_action && suggestion.action_type == ActionType::Run;
pin_next_run_action &= !pinned;
let action = crate::store::RepoAction::new(
context_id.to_string(),
suggestion.name,
suggestion.command,
suggestion.action_type,
next_sort_order,
)
.with_auto_commit(suggestion.auto_commit);
.with_auto_commit(suggestion.auto_commit)
.with_pinned(pinned);
store
.create_repo_action(&action)
.map_err(|e| format!("Failed to create detected action: {e}"))?;
Expand Down Expand Up @@ -1615,6 +1629,67 @@ mod tests {
.collect::<Vec<_>>(),
vec![("Dev", 0), ("Test", 1), ("Build", 2)]
);

// The context started with no run action, so the first run suggestion
// is what the card header ends up showing — and only that one.
assert_eq!(
actions
.iter()
.filter(|a| a.pinned)
.map(|a| a.name.as_str())
.collect::<Vec<_>>(),
vec!["Dev"]
);
// Detection never picks an icon; NULL means the action type's default.
assert!(actions.iter().all(|a| a.icon.is_none()));
}

#[test]
fn persist_suggested_actions_pins_only_the_first_run_action_of_a_fresh_context() {
let store = Store::in_memory().unwrap();
let context = store
.get_or_create_action_context("block/builderbot", Some("apps/staged"))
.unwrap();

persist_suggested_actions(
&store,
&context.id,
vec![
suggestion("Build", "just build", ActionType::Build),
suggestion("Dev", "just dev", ActionType::Run),
suggestion("Storybook", "just storybook", ActionType::Run),
],
)
.unwrap();

let pinned = |store: &Store| -> Vec<String> {
store
.list_repo_actions(&context.id)
.unwrap()
.into_iter()
.filter(|a| a.pinned)
.map(|a| a.name)
.collect()
};
assert_eq!(pinned(&store), vec!["Dev".to_string()]);

// Unpinning is a deliberate choice, so a later re-detect that turns up
// another run action leaves the header empty rather than re-pinning.
let dev = store
.list_repo_actions(&context.id)
.unwrap()
.into_iter()
.find(|a| a.name == "Dev")
.unwrap();
store.update_repo_action(&dev.with_pinned(false)).unwrap();

persist_suggested_actions(
&store,
&context.id,
vec![suggestion("Preview", "just preview", ActionType::Run)],
)
.unwrap();
assert!(pinned(&store).is_empty());
}

/// A context mid-detection: the flag claimed by `pid`, nothing marked yet.
Expand Down
11 changes: 10 additions & 1 deletion apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,7 @@ fn list_project_actions(
}

#[tauri::command(rename_all = "camelCase")]
#[allow(clippy::too_many_arguments)]
fn update_project_action(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
action_id: String,
Expand All @@ -1603,6 +1604,8 @@ fn update_project_action(
action_type: String,
sort_order: i32,
auto_commit: bool,
pinned: bool,
icon: Option<String>,
) -> Result<(), String> {
let store = get_store(&store)?;
let action = store
Expand All @@ -1620,6 +1623,8 @@ fn update_project_action(
sort_order,
auto_commit,
run_detection_mode: action.run_detection_mode,
pinned,
icon,
created_at: action.created_at,
updated_at: store::now_timestamp(),
};
Expand Down Expand Up @@ -1684,6 +1689,8 @@ fn create_repo_action(
action_type: String,
sort_order: i32,
auto_commit: bool,
pinned: bool,
icon: Option<String>,
) -> Result<store::models::RepoAction, String> {
let store = get_store(&store)?;
let context = store
Expand All @@ -1692,7 +1699,9 @@ fn create_repo_action(
let parsed_type = builderbot_actions::ActionType::parse(&action_type)
.ok_or_else(|| format!("Invalid action type: {action_type}"))?;
let action = store::models::RepoAction::new(context.id, name, command, parsed_type, sort_order)
.with_auto_commit(auto_commit);
.with_auto_commit(auto_commit)
.with_pinned(pinned)
.with_icon(icon);
store
.create_repo_action(&action)
.map_err(|e| e.to_string())?;
Expand Down
27 changes: 17 additions & 10 deletions apps/staged/src-tauri/src/store/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ impl Store {
.transpose()
.map_err(|e| StoreError(format!("Failed to serialize run_detection_mode: {e}")))?;
conn.execute(
"INSERT INTO repo_actions (id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
"INSERT INTO repo_actions (id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, pinned, icon, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
action.id,
action.context_id,
Expand All @@ -251,6 +251,8 @@ impl Store {
action.sort_order,
action.auto_commit as i32,
run_detection_mode_json,
action.pinned as i32,
action.icon,
action.created_at,
action.updated_at,
],
Expand All @@ -261,7 +263,7 @@ impl Store {
pub fn get_repo_action(&self, id: &str) -> Result<Option<RepoAction>, StoreError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, created_at, updated_at
"SELECT id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, pinned, icon, created_at, updated_at
FROM repo_actions WHERE id = ?1",
params![id],
Self::row_to_repo_action,
Expand All @@ -273,7 +275,7 @@ impl Store {
pub fn list_repo_actions(&self, context_id: &str) -> Result<Vec<RepoAction>, StoreError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, created_at, updated_at
"SELECT id, context_id, name, command, action_type, sort_order, auto_commit, run_detection_mode, pinned, icon, created_at, updated_at
FROM repo_actions WHERE context_id = ?1 ORDER BY sort_order ASC",
)?;
let rows = stmt.query_map(params![context_id], Self::row_to_repo_action)?;
Expand All @@ -289,15 +291,15 @@ impl Store {
pub fn list_all_repo_actions(&self) -> Result<Vec<RepoContextActions>, StoreError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT a.id, a.context_id, a.name, a.command, a.action_type, a.sort_order, a.auto_commit, a.run_detection_mode, a.created_at, a.updated_at, c.github_repo, c.subpath
"SELECT a.id, a.context_id, a.name, a.command, a.action_type, a.sort_order, a.auto_commit, a.run_detection_mode, a.pinned, a.icon, a.created_at, a.updated_at, c.github_repo, c.subpath
FROM repo_actions a
JOIN action_contexts c ON a.context_id = c.id
ORDER BY c.id ASC, a.sort_order ASC",
)?;
let rows = stmt.query_map([], |row| {
let action = Self::row_to_repo_action(row)?;
let github_repo: String = row.get(10)?;
let subpath: Option<String> = row.get(11)?;
let github_repo: String = row.get(12)?;
let subpath: Option<String> = row.get(13)?;
Ok((github_repo, subpath, action))
})?;

Expand Down Expand Up @@ -332,14 +334,16 @@ impl Store {
.transpose()
.map_err(|e| StoreError(format!("Failed to serialize run_detection_mode: {e}")))?;
conn.execute(
"UPDATE repo_actions SET name = ?1, command = ?2, action_type = ?3, sort_order = ?4, auto_commit = ?5, run_detection_mode = ?6, updated_at = ?7 WHERE id = ?8",
"UPDATE repo_actions SET name = ?1, command = ?2, action_type = ?3, sort_order = ?4, auto_commit = ?5, run_detection_mode = ?6, pinned = ?7, icon = ?8, updated_at = ?9 WHERE id = ?10",
params![
action.name,
action.command,
action.action_type.as_str(),
action.sort_order,
action.auto_commit as i32,
run_detection_mode_json,
action.pinned as i32,
action.icon,
now_timestamp(),
action.id,
],
Expand Down Expand Up @@ -404,6 +408,7 @@ impl Store {
let run_detection_mode: Option<RunDetectionMode> = run_detection_mode_str
.as_deref()
.and_then(|s| serde_json::from_str(s).ok());
let pinned: i32 = row.get(8)?;
Ok(RepoAction {
id: row.get(0)?,
context_id: row.get(1)?,
Expand All @@ -413,8 +418,10 @@ impl Store {
sort_order: row.get(5)?,
auto_commit: auto_commit != 0,
run_detection_mode,
created_at: row.get(8)?,
updated_at: row.get(9)?,
pinned: pinned != 0,
icon: row.get(9)?,
created_at: row.get(10)?,
updated_at: row.get(11)?,
})
}
}
Loading