use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::content::Block;
use crate::DocumentId;
use super::{Collaborator, TextRange};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChangeTracking {
pub base_version: DocumentId,
pub changes: Vec<TrackedChange>,
#[serde(default = "default_true")]
pub enabled: bool,
}
fn default_true() -> bool {
true
}
impl ChangeTracking {
#[must_use]
pub fn new(base_version: DocumentId) -> Self {
Self {
base_version,
changes: Vec::new(),
enabled: true,
}
}
pub fn add_change(&mut self, change: TrackedChange) {
self.changes.push(change);
}
#[must_use]
pub fn pending_changes(&self) -> Vec<&TrackedChange> {
self.changes
.iter()
.filter(|c| c.status == ChangeStatus::Pending)
.collect()
}
#[must_use]
pub fn changes_by_author(&self, author_name: &str) -> Vec<&TrackedChange> {
self.changes
.iter()
.filter(|c| c.author.name == author_name)
.collect()
}
pub fn accept_change(&mut self, change_id: &str) -> bool {
if let Some(change) = self.changes.iter_mut().find(|c| c.id == change_id) {
change.status = ChangeStatus::Accepted;
true
} else {
false
}
}
pub fn reject_change(&mut self, change_id: &str) -> bool {
if let Some(change) = self.changes.iter_mut().find(|c| c.id == change_id) {
change.status = ChangeStatus::Rejected;
true
} else {
false
}
}
pub fn accept_all(&mut self) {
for change in &mut self.changes {
if change.status == ChangeStatus::Pending {
change.status = ChangeStatus::Accepted;
}
}
}
pub fn reject_all(&mut self) {
for change in &mut self.changes {
if change.status == ChangeStatus::Pending {
change.status = ChangeStatus::Rejected;
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.changes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackedChange {
pub id: String,
pub change_type: ChangeType,
pub block_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<Box<Block>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<Box<Block>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<TextRange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub new_text: Option<String>,
pub author: Collaborator,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub status: ChangeStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl TrackedChange {
#[must_use]
pub fn new(
id: impl Into<String>,
change_type: ChangeType,
block_ref: impl Into<String>,
author: Collaborator,
) -> Self {
Self {
id: id.into(),
change_type,
block_ref: block_ref.into(),
before: None,
after: None,
range: None,
original_text: None,
new_text: None,
author,
timestamp: Utc::now(),
status: ChangeStatus::Pending,
note: None,
}
}
#[must_use]
pub fn insert(
id: impl Into<String>,
block_ref: impl Into<String>,
author: Collaborator,
content: Block,
) -> Self {
Self::new(id, ChangeType::Insert, block_ref, author).with_after(content)
}
#[must_use]
pub fn delete(
id: impl Into<String>,
block_ref: impl Into<String>,
author: Collaborator,
content: Block,
) -> Self {
Self::new(id, ChangeType::Delete, block_ref, author).with_before(content)
}
#[must_use]
pub fn modify(
id: impl Into<String>,
block_ref: impl Into<String>,
author: Collaborator,
before: Block,
after: Block,
) -> Self {
Self::new(id, ChangeType::Modify, block_ref, author)
.with_before(before)
.with_after(after)
}
#[must_use]
pub fn inline_text(
id: impl Into<String>,
block_ref: impl Into<String>,
author: Collaborator,
range: TextRange,
original: impl Into<String>,
replacement: impl Into<String>,
) -> Self {
Self::new(id, ChangeType::Modify, block_ref, author)
.with_range(range)
.with_text_change(original, replacement)
}
#[must_use]
pub fn with_before(mut self, block: Block) -> Self {
self.before = Some(Box::new(block));
self
}
#[must_use]
pub fn with_after(mut self, block: Block) -> Self {
self.after = Some(Box::new(block));
self
}
#[must_use]
pub fn with_range(mut self, range: TextRange) -> Self {
self.range = Some(range);
self
}
#[must_use]
pub fn with_text_change(mut self, original: impl Into<String>, new: impl Into<String>) -> Self {
self.original_text = Some(original.into());
self.new_text = Some(new.into());
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn accept(&mut self) {
self.status = ChangeStatus::Accepted;
}
pub fn reject(&mut self) {
self.status = ChangeStatus::Rejected;
}
#[must_use]
pub fn is_pending(&self) -> bool {
self.status == ChangeStatus::Pending
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ChangeType {
Insert,
Delete,
Modify,
Move,
Format,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ChangeStatus {
#[default]
Pending,
Accepted,
Rejected,
}