#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RemoteConfig {
pub token: Option<String>,
pub base_url: Option<String>,
pub project: Option<String>,
pub team: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ExportConfig {
pub gitlab: Option<RemoteConfig>,
pub linear: Option<RemoteConfig>,
}
impl ExportConfig {
pub fn is_empty(&self) -> bool {
self.gitlab.is_none() && self.linear.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Team {
pub members: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PlanningCycle {
pub description: Option<String>,
pub start_date: Option<String>,
pub due_date: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProjectConfiguration {
pub name: String,
pub description: Option<String>,
pub status: Option<String>,
pub lead: Option<String>,
pub start_date: Option<String>,
pub due_date: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct EpicConfiguration {
pub name: String,
pub description: Option<String>,
pub status: Option<String>,
pub owner: Option<String>,
pub start_date: Option<String>,
pub due_date: Option<String>,
pub weight: Option<i64>,
pub parent: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub projects: Vec<String>,
pub milestone: Option<String>,
pub cycle: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct InitiativeConfiguration {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<crate::model::Priority>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub projects: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub epics: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub milestones: Vec<String>,
#[serde(flatten)]
pub extensions: crate::roadmap::registry::InitiativeExtensions,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PlanningConfig {
pub cycle_number: u32,
pub default_cycle: Option<String>,
pub default_milestone: Option<String>,
pub default_team: Option<String>,
}
impl PlanningConfig {
pub fn is_default(&self) -> bool {
self == &Self::default()
}
}
impl Default for PlanningConfig {
fn default() -> Self {
Self {
cycle_number: 1,
default_cycle: None,
default_milestone: None,
default_team: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct IssueTemplate {
pub title: Option<String>,
pub body: Option<String>,
pub labels: Vec<String>,
pub status: Option<String>,
pub priority: Option<String>,
pub assignee: Option<String>,
pub estimate: Option<String>,
pub reviewer: Option<String>,
pub cycles: Vec<String>,
pub milestones: Vec<String>,
pub start_date: Option<String>,
pub due_date: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StateConfig {
#[serde(skip_serializing)]
pub next_number: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum OrderBy {
#[default]
#[serde(rename = "created_at_desc")]
CreatedAtDesc,
#[serde(rename = "created_at_asc")]
CreatedAtAsc,
#[serde(rename = "none")]
None,
}
impl OrderBy {
pub fn as_str(self) -> &'static str {
match self {
Self::CreatedAtDesc => "created_at_desc",
Self::CreatedAtAsc => "created_at_asc",
Self::None => "none",
}
}
pub fn next(self) -> Self {
match self {
Self::CreatedAtDesc => Self::CreatedAtAsc,
Self::CreatedAtAsc => Self::None,
Self::None => Self::CreatedAtDesc,
}
}
}
pub const CATEGORY_KEYS: &[&str] = &[
"backlog",
"unstarted",
"active",
"blocked",
"completed",
"canceled",
];
pub const TERMINAL_CATEGORIES: &[&str] = &["completed", "canceled"];
pub const COMPLETE_CATEGORY: &str = "completed";
pub const SAFE_DEFAULT_CATEGORY: &str = "unstarted";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CategoryConfig {
pub order: Vec<String>,
pub default: String,
}
impl Default for CategoryConfig {
fn default() -> Self {
Self {
order: CATEGORY_KEYS.iter().map(|k| (*k).to_string()).collect(),
default: SAFE_DEFAULT_CATEGORY.to_string(),
}
}
}
impl CategoryConfig {
pub fn is_default(&self) -> bool {
self == &Self::default()
}
pub fn contains(&self, key: &str) -> bool {
self.order.iter().any(|k| k == key)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct TransitionConfig {
pub terminal_locked: bool,
}
impl Default for TransitionConfig {
fn default() -> Self {
Self {
terminal_locked: true,
}
}
}
impl TransitionConfig {
pub fn is_default(&self) -> bool {
self == &Self::default()
}
}
pub const DEFAULT_TITLE_LINES: usize = 3;
pub const DEFAULT_DONE_COLUMN: &str = "done";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct HealthConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy: Option<String>,
}
impl HealthConfig {
pub fn is_empty(&self) -> bool {
self.policy.is_none()
}
pub fn policy(&self) -> std::result::Result<crate::health::HealthPolicy, TixError> {
match self.policy.as_deref() {
Some(version) => crate::health::HealthPolicy::for_version(version)
.map_err(|error| TixError::Config(error.render())),
None => Ok(crate::health::HealthPolicy::v1()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct BoardConfiguration {
pub columns: Vec<String>,
pub done_column: Option<String>,
pub category_map: IndexMap<String, String>,
pub hidden_columns: Vec<String>,
pub show_mismatch: bool,
pub wrap_titles: bool,
pub title_lines: usize,
pub card_order: Vec<String>,
pub order_by: OrderBy,
pub wip: crate::wip::WipConfig,
}
impl Default for BoardConfiguration {
fn default() -> Self {
Self {
columns: ["todo", "in progress", "in review", "done"]
.into_iter()
.map(String::from)
.collect(),
done_column: Some(DEFAULT_DONE_COLUMN.to_string()),
category_map: IndexMap::new(),
hidden_columns: Vec::new(),
show_mismatch: false,
wrap_titles: true,
title_lines: DEFAULT_TITLE_LINES,
card_order: Vec::new(),
order_by: OrderBy::default(),
wip: crate::wip::WipConfig::default(),
}
}
}
impl Serialize for BoardConfiguration {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let done = self
.done_column
.as_deref()
.filter(|done| *done != DEFAULT_DONE_COLUMN);
let order_by = (self.order_by != OrderBy::default()).then_some(self.order_by);
let mut len = 1; len += usize::from(done.is_some());
len += usize::from(!self.category_map.is_empty());
len += usize::from(!self.hidden_columns.is_empty());
len += usize::from(self.show_mismatch);
len += usize::from(!self.wrap_titles);
len += usize::from(self.title_lines != DEFAULT_TITLE_LINES);
len += usize::from(!self.card_order.is_empty());
len += usize::from(order_by.is_some());
len += usize::from(!self.wip.is_empty());
let mut board = serializer.serialize_struct("BoardConfiguration", len)?;
board.serialize_field("columns", &self.columns)?;
if let Some(done) = done {
board.serialize_field("done_column", done)?;
}
if !self.category_map.is_empty() {
board.serialize_field("category_map", &self.category_map)?;
}
if !self.hidden_columns.is_empty() {
board.serialize_field("hidden_columns", &self.hidden_columns)?;
}
if self.show_mismatch {
board.serialize_field("show_mismatch", &self.show_mismatch)?;
}
if !self.wrap_titles {
board.serialize_field("wrap_titles", &self.wrap_titles)?;
}
if self.title_lines != DEFAULT_TITLE_LINES {
board.serialize_field("title_lines", &self.title_lines)?;
}
if !self.card_order.is_empty() {
board.serialize_field("card_order", &self.card_order)?;
}
if let Some(order_by) = order_by {
board.serialize_field("order_by", &order_by)?;
}
if !self.wip.is_empty() {
board.serialize_field("wip", &self.wip)?;
}
board.end()
}
}
pub fn starter_board() -> BoardConfiguration {
BoardConfiguration {
columns: [
"triage",
"todo",
"in progress",
"in review",
"done",
"wontfix",
]
.into_iter()
.map(String::from)
.collect(),
category_map: IndexMap::from([
("done".to_string(), "completed".to_string()),
("wontfix".to_string(), "canceled".to_string()),
]),
..BoardConfiguration::default()
}
}
impl BoardConfiguration {
pub fn category_for<'c>(&'c self, column: &str, categories: &'c CategoryConfig) -> &'c str {
if let Some(mapped) = self.category_map.get(column) {
return mapped;
}
if self.category_map.is_empty() && self.effective_done_column() == Some(column) {
return COMPLETE_CATEGORY;
}
&categories.default
}
pub fn effective_done_column(&self) -> Option<&str> {
self.done_column
.as_deref()
.or_else(|| self.inferred_done_column())
}
fn inferred_done_column(&self) -> Option<&str> {
if self.columns.iter().any(|c| c == DEFAULT_DONE_COLUMN) {
Some(DEFAULT_DONE_COLUMN)
} else {
self.columns.last().map(String::as_str)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, transparent)]
pub struct KanbanSection {
pub configurations: IndexMap<String, BoardConfiguration>,
}
impl KanbanSection {
pub fn is_empty(&self) -> bool {
self.configurations.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct BeadsConfig {
pub mode: BeadsMode,
pub binary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<PathBuf>,
pub id_prefix: String,
}
impl Default for BeadsConfig {
fn default() -> Self {
BeadsConfig {
mode: BeadsMode::default(),
binary: crate::sync::beads::DEFAULT_BINARY.to_string(),
workspace: None,
id_prefix: crate::sync::beads::DEFAULT_ID_PREFIX.to_string(),
}
}
}
impl BeadsConfig {
#[must_use]
pub fn is_default(&self) -> bool {
*self == BeadsConfig::default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BeadsMode {
#[default]
Off,
Mirror,
Sync,
}
impl BeadsMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
BeadsMode::Off => "off",
BeadsMode::Mirror => "mirror",
BeadsMode::Sync => "sync",
}
}
#[must_use]
pub fn from_key(key: &str) -> Option<Self> {
match key {
"off" => Some(BeadsMode::Off),
"mirror" => Some(BeadsMode::Mirror),
"sync" => Some(BeadsMode::Sync),
_ => None,
}
}
pub const ALL: [BeadsMode; 3] = [BeadsMode::Off, BeadsMode::Mirror, BeadsMode::Sync];
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct CustomAttributes {
pub keys: Vec<String>,
}
impl CustomAttributes {
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
}
pub const DEFAULT_TYPES: &[&str] = &[
"default",
"bug",
"feature",
"task",
"improvement",
"documentation",
"question",
"duplicate",
"chore",
"open",
];
pub const BUILTIN_LABELS: &[&str] = &["decision-pending"];
pub const DEFAULT_TYPE_COLORS: &[(&str, &str)] = &[
("default", "gray"),
("bug", "red"),
("feature", "green"),
("task", "blue"),
("improvement", "cyan"),
("documentation", "yellow"),
("question", "magenta"),
("duplicate", "dark_gray"),
("chore", "dark_gray"),
("open", "white"),
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct MentionsConfig {
pub enabled: bool,
pub dir: PathBuf,
}
pub const DEFAULT_MENTIONS_DIR: &str = "docs/mentions";
impl Default for MentionsConfig {
fn default() -> Self {
MentionsConfig {
enabled: true,
dir: PathBuf::from(DEFAULT_MENTIONS_DIR),
}
}
}
impl MentionsConfig {
#[must_use]
pub fn is_default(&self) -> bool {
*self == Self::default()
}
#[must_use]
pub fn resolve_dir(&self, issues_dir: &Path) -> PathBuf {
if self.dir.is_absolute() {
return self.dir.clone();
}
match Config::project_root(issues_dir) {
Some(root) => root.join(&self.dir),
None => issues_dir.parent().unwrap_or(issues_dir).join(&self.dir),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct LayoutConfig {
pub directory: String,
pub filename: String,
pub move_on_change: bool,
}
impl Default for LayoutConfig {
fn default() -> Self {
LayoutConfig {
directory: "{assignee}/{type}/{status}".to_string(),
filename: "{date}-{id}-{slug}.md".to_string(),
move_on_change: true,
}
}
}
impl LayoutConfig {
#[must_use]
pub fn is_default(&self) -> bool {
*self == Self::default()
}
}