This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# gitea-kanban-tui
|
# gitea-kanban
|
||||||
|
|
||||||
Keyboard-driven Kanban board for native repository Projects in custom Gitea fork.
|
Native Projects Kanban tools for custom Gitea fork. `gitea-kanban-tui` provides
|
||||||
|
the keyboard interface; `gitea-kanban` provides the command-line interface.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ accepted only for loopback development.
|
|||||||
```sh
|
```sh
|
||||||
export GITEA_URL=https://gitea.hectic-lab.com
|
export GITEA_URL=https://gitea.hectic-lab.com
|
||||||
export GITEA_TOKEN_FILE="$HOME/.config/gitea/token"
|
export GITEA_TOKEN_FILE="$HOME/.config/gitea/token"
|
||||||
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml -- \
|
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml --bin gitea-kanban-tui -- \
|
||||||
--project Kanban yukkop util.nix
|
--project Kanban yukkop util.nix
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ Alternatively:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix develop .#ratatui
|
nix develop .#ratatui
|
||||||
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml -- \
|
cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml --bin gitea-kanban-tui -- \
|
||||||
--project-id 1 owner repo
|
--project-id 1 owner repo
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -47,6 +48,21 @@ cargo run --manifest-path package/gitea-kanban-tui/Cargo.toml -- \
|
|||||||
- `?`: toggle help
|
- `?`: toggle help
|
||||||
- `q`: quit
|
- `q`: quit
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
Both binaries use same configuration and API client. CLI commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gitea-kanban --project Kanban owner repo board
|
||||||
|
gitea-kanban --project Kanban owner repo create --title "Fix issue" --body "Details"
|
||||||
|
gitea-kanban --project Kanban owner repo edit 42 --title "Updated" --body "Changed"
|
||||||
|
gitea-kanban --project Kanban owner repo move 123 --column-id 7
|
||||||
|
gitea-kanban --project Kanban owner repo delete 42 --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
`delete` requires repository admin permission. `move` takes global issue and
|
||||||
|
project-column IDs.
|
||||||
|
|
||||||
Native mode reads `/projects`, project columns, and each column's issues. Moves
|
Native mode reads `/projects`, project columns, and each column's issues. Moves
|
||||||
use the issue's global API `id` and destination column `id`; optional sorting is
|
use the issue's global API `id` and destination column `id`; optional sorting is
|
||||||
supported by the server API. Project names are exact, case-sensitive matches;
|
supported by the server API. Project names are exact, case-sensitive matches;
|
||||||
|
|||||||
@@ -14,9 +14,16 @@ in
|
|||||||
|
|
||||||
inherit nativeBuildInputs;
|
inherit nativeBuildInputs;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|
||||||
doCheck = true;
|
doCheck = true;
|
||||||
|
|
||||||
|
postBuild = ''
|
||||||
|
cargo build --release --offline --bin gitea-kanban
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
install -Dm755 target/*/release/gitea-kanban $out/bin/gitea-kanban
|
||||||
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = cargo.package.description;
|
description = cargo.package.description;
|
||||||
license = lib.licenses.mit;
|
license = lib.licenses.mit;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use serde::de::DeserializeOwned;
|
|||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload, Project, ProjectColumn,
|
CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload, Project, ProjectBoard,
|
||||||
|
ProjectColumn,
|
||||||
};
|
};
|
||||||
use crate::text::sanitize_terminal_text;
|
|
||||||
|
|
||||||
pub trait GiteaApi {
|
pub trait GiteaApi {
|
||||||
fn list_projects(&self) -> Result<Vec<Project>, ApiError>;
|
fn list_projects(&self) -> Result<Vec<Project>, ApiError>;
|
||||||
@@ -78,6 +78,24 @@ pub fn resolve_project(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_project_board(
|
||||||
|
client: &impl GiteaApi,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<ProjectBoard, ApiError> {
|
||||||
|
let projects = client.list_projects()?;
|
||||||
|
let project = resolve_project(&projects, config.project.as_deref(), config.project_id)?;
|
||||||
|
let columns = client.list_project_columns(project.id)?;
|
||||||
|
let issues_by_column = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| client.list_project_column_issues(project.id, column.id))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok(ProjectBoard {
|
||||||
|
project,
|
||||||
|
columns,
|
||||||
|
issues_by_column,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl GiteaClient {
|
impl GiteaClient {
|
||||||
pub fn new(config: &Config) -> Result<Self, ApiError> {
|
pub fn new(config: &Config) -> Result<Self, ApiError> {
|
||||||
let mut api_base = Url::parse(&config.base_url)
|
let mut api_base = Url::parse(&config.base_url)
|
||||||
@@ -123,7 +141,7 @@ impl GiteaClient {
|
|||||||
response: Response,
|
response: Response,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
) -> Result<T, ApiError> {
|
) -> Result<T, ApiError> {
|
||||||
let response = check_response(response, operation, &self.token)?;
|
let response = check_response(response, operation)?;
|
||||||
response.json().map_err(|error| {
|
response.json().map_err(|error| {
|
||||||
ApiError(format!(
|
ApiError(format!(
|
||||||
"Gitea returned invalid JSON while {operation}: {error}"
|
"Gitea returned invalid JSON while {operation}: {error}"
|
||||||
@@ -215,7 +233,7 @@ impl GiteaApi for GiteaClient {
|
|||||||
"cannot reach Gitea while moving project issue: {error}"
|
"cannot reach Gitea while moving project issue: {error}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
check_response(response, "moving project issue", &self.token)?;
|
check_response(response, "moving project issue")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,30 +276,17 @@ impl GiteaApi for GiteaClient {
|
|||||||
"cannot reach Gitea while deleting issue #{issue_number}: {error}"
|
"cannot reach Gitea while deleting issue #{issue_number}: {error}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
check_response(
|
check_response(response, &format!("deleting issue #{issue_number}"))?;
|
||||||
response,
|
|
||||||
&format!("deleting issue #{issue_number}"),
|
|
||||||
&self.token,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_response(response: Response, operation: &str, token: &str) -> Result<Response, ApiError> {
|
fn check_response(response: Response, operation: &str) -> Result<Response, ApiError> {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = response
|
|
||||||
.text()
|
|
||||||
.ok()
|
|
||||||
.and_then(|body| serde_json::from_str::<serde_json::Value>(&body).ok())
|
|
||||||
.and_then(|value| value.get("message")?.as_str().map(sanitize_terminal_text))
|
|
||||||
.map(|message| message.replace(token, "[redacted token]"));
|
|
||||||
let detail = message
|
|
||||||
.map(|message| format!(": {message}"))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let guidance = match status {
|
let guidance = match status {
|
||||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
||||||
" Check token validity and repository issue permissions."
|
" Check token validity and repository issue permissions."
|
||||||
@@ -292,7 +297,7 @@ fn check_response(response: Response, operation: &str, token: &str) -> Result<Re
|
|||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
Err(ApiError(format!(
|
Err(ApiError(format!(
|
||||||
"Gitea API failed while {operation} ({status}){detail}.{guidance}"
|
"Gitea API failed while {operation} ({status}).{guidance}"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +457,6 @@ mod tests {
|
|||||||
.list_project_columns(4)
|
.list_project_columns(4)
|
||||||
.expect_err("unsupported API must fail");
|
.expect_err("unsupported API must fail");
|
||||||
assert!(error.to_string().contains("404 Not Found"));
|
assert!(error.to_string().contains("404 Not Found"));
|
||||||
assert!(error.to_string().contains("projects unavailable"));
|
|
||||||
assert!(!error.to_string().contains("secret"));
|
assert!(!error.to_string().contains("secret"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use gitea_kanban_tui::api::{load_project_board, GiteaClient};
|
||||||
|
use gitea_kanban_tui::config::{Config, SharedArgs};
|
||||||
|
use gitea_kanban_tui::model::EditIssuePayload;
|
||||||
|
use gitea_kanban_tui::operations;
|
||||||
|
use gitea_kanban_tui::text::sanitize_terminal_text;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
version,
|
||||||
|
about = "Control native Gitea project boards from the command line"
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(flatten)]
|
||||||
|
shared: SharedArgs,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Print project columns and their issues.
|
||||||
|
Board,
|
||||||
|
/// Create an issue assigned to selected project.
|
||||||
|
Create {
|
||||||
|
#[arg(long)]
|
||||||
|
title: String,
|
||||||
|
#[arg(long, default_value = "")]
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
/// Replace issue title and body.
|
||||||
|
Edit {
|
||||||
|
issue: u64,
|
||||||
|
#[arg(long)]
|
||||||
|
title: String,
|
||||||
|
#[arg(long)]
|
||||||
|
body: String,
|
||||||
|
},
|
||||||
|
/// Delete an issue. Requires repository admin permission.
|
||||||
|
Delete {
|
||||||
|
issue: u64,
|
||||||
|
/// Confirm irreversible deletion.
|
||||||
|
#[arg(long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
/// Move an issue to a project column.
|
||||||
|
Move {
|
||||||
|
issue_id: u64,
|
||||||
|
#[arg(long)]
|
||||||
|
column_id: u64,
|
||||||
|
#[arg(long)]
|
||||||
|
sorting: Option<u64>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(error) = run() {
|
||||||
|
eprintln!("error: {}", sanitize_terminal_text(&error.to_string()));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), Box<dyn Error>> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = Config::from_shared_args_with(cli.shared, |name| std::env::var(name).ok())?;
|
||||||
|
let client = GiteaClient::new(&config)?;
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Command::Board => {
|
||||||
|
let board = load_project_board(&client, &config)?;
|
||||||
|
println!(
|
||||||
|
"{} (project {})",
|
||||||
|
sanitize_terminal_text(&board.project.title),
|
||||||
|
board.project.id
|
||||||
|
);
|
||||||
|
for (column, issues) in board.columns.iter().zip(board.issues_by_column) {
|
||||||
|
println!(
|
||||||
|
"\n{} ({})",
|
||||||
|
sanitize_terminal_text(&column.title),
|
||||||
|
column.id
|
||||||
|
);
|
||||||
|
for issue in issues {
|
||||||
|
println!(
|
||||||
|
" #{} {}",
|
||||||
|
issue.number,
|
||||||
|
sanitize_terminal_text(&issue.title)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Create { title, body } => {
|
||||||
|
let board = load_project_board(&client, &config)?;
|
||||||
|
let issue = operations::create_project_issue(&client, board.project.id, title, body)?;
|
||||||
|
println!("created issue #{}", issue.number);
|
||||||
|
}
|
||||||
|
Command::Edit { issue, title, body } => {
|
||||||
|
let board = load_project_board(&client, &config)?;
|
||||||
|
if !board
|
||||||
|
.issues_by_column
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|candidate| candidate.number == issue)
|
||||||
|
{
|
||||||
|
return Err(format!("issue #{issue} is not assigned to selected project").into());
|
||||||
|
}
|
||||||
|
operations::edit_issue(&client, issue, &EditIssuePayload { title, body })?;
|
||||||
|
println!("updated issue #{issue}");
|
||||||
|
}
|
||||||
|
Command::Delete { issue, yes } => {
|
||||||
|
if !yes {
|
||||||
|
return Err("deletion requires --yes".into());
|
||||||
|
}
|
||||||
|
let board = load_project_board(&client, &config)?;
|
||||||
|
if !board
|
||||||
|
.issues_by_column
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|candidate| candidate.number == issue)
|
||||||
|
{
|
||||||
|
return Err(format!("issue #{issue} is not assigned to selected project").into());
|
||||||
|
}
|
||||||
|
operations::delete_issue(&client, issue)?;
|
||||||
|
println!("deleted issue #{issue}");
|
||||||
|
}
|
||||||
|
Command::Move {
|
||||||
|
issue_id,
|
||||||
|
column_id,
|
||||||
|
sorting,
|
||||||
|
} => {
|
||||||
|
let board = load_project_board(&client, &config)?;
|
||||||
|
operations::move_project_issue(
|
||||||
|
&client,
|
||||||
|
board.project.id,
|
||||||
|
issue_id,
|
||||||
|
&gitea_kanban_tui::model::MoveProjectIssuePayload { column_id, sorting },
|
||||||
|
)?;
|
||||||
|
println!("moved issue {issue_id} to column {column_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -5,15 +5,10 @@ use std::fs;
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Args as ClapArgs, Parser};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Clone, Debug, ClapArgs)]
|
||||||
#[command(
|
pub struct SharedArgs {
|
||||||
name = "gitea-kanban-tui",
|
|
||||||
version,
|
|
||||||
about = "Browse and move Gitea issues using native projects"
|
|
||||||
)]
|
|
||||||
pub struct Args {
|
|
||||||
/// Gitea base URL; falls back to GITEA_URL
|
/// Gitea base URL; falls back to GITEA_URL
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
@@ -37,6 +32,17 @@ pub struct Args {
|
|||||||
pub repo: Option<String>,
|
pub repo: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "gitea-kanban-tui",
|
||||||
|
version,
|
||||||
|
about = "Browse and move Gitea issues using native projects"
|
||||||
|
)]
|
||||||
|
pub struct Args {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub shared: SharedArgs,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -64,6 +70,13 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_args_with<F>(args: Args, env_var: F) -> Result<Self, ConfigError>
|
pub fn from_args_with<F>(args: Args, env_var: F) -> Result<Self, ConfigError>
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Option<String>,
|
||||||
|
{
|
||||||
|
Self::from_shared_args_with(args.shared, env_var)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_shared_args_with<F>(args: SharedArgs, env_var: F) -> Result<Self, ConfigError>
|
||||||
where
|
where
|
||||||
F: Fn(&str) -> Option<String>,
|
F: Fn(&str) -> Option<String>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod api;
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
pub mod operations;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
|
|
||||||
use gitea_kanban_tui::api::{resolve_project, GiteaApi, GiteaClient};
|
use gitea_kanban_tui::api::{load_project_board, GiteaApi, GiteaClient};
|
||||||
use gitea_kanban_tui::app::{App, IssueAction, MoveAction};
|
use gitea_kanban_tui::app::{App, IssueAction, MoveAction};
|
||||||
use gitea_kanban_tui::config::Config;
|
use gitea_kanban_tui::config::Config;
|
||||||
use gitea_kanban_tui::model::CreateIssuePayload;
|
use gitea_kanban_tui::operations;
|
||||||
use gitea_kanban_tui::terminal::TerminalGuard;
|
use gitea_kanban_tui::terminal::TerminalGuard;
|
||||||
use gitea_kanban_tui::text::sanitize_terminal_text;
|
use gitea_kanban_tui::text::sanitize_terminal_text;
|
||||||
use gitea_kanban_tui::ui;
|
use gitea_kanban_tui::ui;
|
||||||
@@ -75,21 +75,14 @@ fn issue_action(client: &impl GiteaApi, config: &Config, app: &mut App, action:
|
|||||||
project_id,
|
project_id,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
} => client
|
} => operations::create_project_issue(client, project_id, title, body)
|
||||||
.create_issue(&CreateIssuePayload {
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
projects: vec![project_id],
|
|
||||||
})
|
|
||||||
.map(|issue| format!("Created issue #{}", issue.number)),
|
.map(|issue| format!("Created issue #{}", issue.number)),
|
||||||
IssueAction::Edit {
|
IssueAction::Edit {
|
||||||
issue_number,
|
issue_number,
|
||||||
payload,
|
payload,
|
||||||
} => client
|
} => operations::edit_issue(client, issue_number, &payload)
|
||||||
.edit_issue(issue_number, &payload)
|
|
||||||
.map(|_| format!("Updated issue #{issue_number}")),
|
.map(|_| format!("Updated issue #{issue_number}")),
|
||||||
IssueAction::Delete { issue_number } => client
|
IssueAction::Delete { issue_number } => operations::delete_issue(client, issue_number)
|
||||||
.delete_issue(issue_number)
|
|
||||||
.map(|_| format!("Deleted issue #{issue_number}")),
|
.map(|_| format!("Deleted issue #{issue_number}")),
|
||||||
};
|
};
|
||||||
match message {
|
match message {
|
||||||
@@ -108,14 +101,9 @@ fn issue_action(client: &impl GiteaApi, config: &Config, app: &mut App, action:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_board(client: &impl GiteaApi, config: &Config) -> Result<App, Box<dyn Error>> {
|
fn load_board(client: &impl GiteaApi, config: &Config) -> Result<App, Box<dyn Error>> {
|
||||||
let projects = client.list_projects()?;
|
let board = load_project_board(client, config)?;
|
||||||
let project = resolve_project(&projects, config.project.as_deref(), config.project_id)?;
|
App::new_project(board.project, board.columns, board.issues_by_column)
|
||||||
let columns = client.list_project_columns(project.id)?;
|
.map_err(|error| error.into())
|
||||||
let issues_by_column = columns
|
|
||||||
.iter()
|
|
||||||
.map(|column| client.list_project_column_issues(project.id, column.id))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
App::new_project(project, columns, issues_by_column).map_err(|error| error.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_card(client: &impl GiteaApi, app: &mut App, offset: isize) {
|
fn move_card(client: &impl GiteaApi, app: &mut App, offset: isize) {
|
||||||
@@ -128,7 +116,7 @@ fn move_card(client: &impl GiteaApi, app: &mut App, offset: isize) {
|
|||||||
issue_id,
|
issue_id,
|
||||||
project_id,
|
project_id,
|
||||||
payload,
|
payload,
|
||||||
} => client.move_project_issue(*project_id, *issue_id, payload),
|
} => operations::move_project_issue(client, *project_id, *issue_id, payload),
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => app.apply_move(request),
|
Ok(()) => app.apply_move(request),
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ pub struct ProjectColumn {
|
|||||||
pub sorting: i8,
|
pub sorting: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ProjectBoard {
|
||||||
|
pub project: Project,
|
||||||
|
pub columns: Vec<ProjectColumn>,
|
||||||
|
pub issues_by_column: Vec<Vec<Issue>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
pub struct MoveProjectIssuePayload {
|
pub struct MoveProjectIssuePayload {
|
||||||
pub column_id: u64,
|
pub column_id: u64,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::api::{ApiError, GiteaApi};
|
||||||
|
use crate::model::{CreateIssuePayload, EditIssuePayload, Issue, MoveProjectIssuePayload};
|
||||||
|
|
||||||
|
pub fn create_project_issue(
|
||||||
|
client: &impl GiteaApi,
|
||||||
|
project_id: u64,
|
||||||
|
title: String,
|
||||||
|
body: String,
|
||||||
|
) -> Result<Issue, ApiError> {
|
||||||
|
client.create_issue(&CreateIssuePayload {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
projects: vec![project_id],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit_issue(
|
||||||
|
client: &impl GiteaApi,
|
||||||
|
issue_number: u64,
|
||||||
|
payload: &EditIssuePayload,
|
||||||
|
) -> Result<Issue, ApiError> {
|
||||||
|
client.edit_issue(issue_number, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_issue(client: &impl GiteaApi, issue_number: u64) -> Result<(), ApiError> {
|
||||||
|
client.delete_issue(issue_number)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_project_issue(
|
||||||
|
client: &impl GiteaApi,
|
||||||
|
project_id: u64,
|
||||||
|
issue_id: u64,
|
||||||
|
payload: &MoveProjectIssuePayload,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
client.move_project_issue(project_id, issue_id, payload)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user