cdx_core/extensions/collaboration/
change_tracking.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::content::Block;
7use crate::DocumentId;
8
9use super::{Collaborator, TextRange};
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ChangeTracking {
15 pub base_version: DocumentId,
17
18 pub changes: Vec<TrackedChange>,
20
21 #[serde(default = "default_true")]
23 pub enabled: bool,
24}
25
26fn default_true() -> bool {
27 true
28}
29
30impl ChangeTracking {
31 #[must_use]
33 pub fn new(base_version: DocumentId) -> Self {
34 Self {
35 base_version,
36 changes: Vec::new(),
37 enabled: true,
38 }
39 }
40
41 pub fn add_change(&mut self, change: TrackedChange) {
43 self.changes.push(change);
44 }
45
46 #[must_use]
48 pub fn pending_changes(&self) -> Vec<&TrackedChange> {
49 self.changes
50 .iter()
51 .filter(|c| c.status == ChangeStatus::Pending)
52 .collect()
53 }
54
55 #[must_use]
57 pub fn changes_by_author(&self, author_name: &str) -> Vec<&TrackedChange> {
58 self.changes
59 .iter()
60 .filter(|c| c.author.name == author_name)
61 .collect()
62 }
63
64 pub fn accept_change(&mut self, change_id: &str) -> bool {
68 if let Some(change) = self.changes.iter_mut().find(|c| c.id == change_id) {
69 change.status = ChangeStatus::Accepted;
70 true
71 } else {
72 false
73 }
74 }
75
76 pub fn reject_change(&mut self, change_id: &str) -> bool {
80 if let Some(change) = self.changes.iter_mut().find(|c| c.id == change_id) {
81 change.status = ChangeStatus::Rejected;
82 true
83 } else {
84 false
85 }
86 }
87
88 pub fn accept_all(&mut self) {
90 for change in &mut self.changes {
91 if change.status == ChangeStatus::Pending {
92 change.status = ChangeStatus::Accepted;
93 }
94 }
95 }
96
97 pub fn reject_all(&mut self) {
99 for change in &mut self.changes {
100 if change.status == ChangeStatus::Pending {
101 change.status = ChangeStatus::Rejected;
102 }
103 }
104 }
105
106 #[must_use]
108 pub fn len(&self) -> usize {
109 self.changes.len()
110 }
111
112 #[must_use]
114 pub fn is_empty(&self) -> bool {
115 self.changes.is_empty()
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct TrackedChange {
123 pub id: String,
125
126 pub change_type: ChangeType,
128
129 pub block_ref: String,
131
132 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub before: Option<Box<Block>>,
135
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub after: Option<Box<Block>>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub range: Option<TextRange>,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub original_text: Option<String>,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub new_text: Option<String>,
151
152 pub author: Collaborator,
154
155 pub timestamp: DateTime<Utc>,
157
158 #[serde(default)]
160 pub status: ChangeStatus,
161
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub note: Option<String>,
165}
166
167impl TrackedChange {
168 #[must_use]
170 pub fn new(
171 id: impl Into<String>,
172 change_type: ChangeType,
173 block_ref: impl Into<String>,
174 author: Collaborator,
175 ) -> Self {
176 Self {
177 id: id.into(),
178 change_type,
179 block_ref: block_ref.into(),
180 before: None,
181 after: None,
182 range: None,
183 original_text: None,
184 new_text: None,
185 author,
186 timestamp: Utc::now(),
187 status: ChangeStatus::Pending,
188 note: None,
189 }
190 }
191
192 #[must_use]
194 pub fn insert(
195 id: impl Into<String>,
196 block_ref: impl Into<String>,
197 author: Collaborator,
198 content: Block,
199 ) -> Self {
200 Self::new(id, ChangeType::Insert, block_ref, author).with_after(content)
201 }
202
203 #[must_use]
205 pub fn delete(
206 id: impl Into<String>,
207 block_ref: impl Into<String>,
208 author: Collaborator,
209 content: Block,
210 ) -> Self {
211 Self::new(id, ChangeType::Delete, block_ref, author).with_before(content)
212 }
213
214 #[must_use]
216 pub fn modify(
217 id: impl Into<String>,
218 block_ref: impl Into<String>,
219 author: Collaborator,
220 before: Block,
221 after: Block,
222 ) -> Self {
223 Self::new(id, ChangeType::Modify, block_ref, author)
224 .with_before(before)
225 .with_after(after)
226 }
227
228 #[must_use]
230 pub fn inline_text(
231 id: impl Into<String>,
232 block_ref: impl Into<String>,
233 author: Collaborator,
234 range: TextRange,
235 original: impl Into<String>,
236 replacement: impl Into<String>,
237 ) -> Self {
238 Self::new(id, ChangeType::Modify, block_ref, author)
239 .with_range(range)
240 .with_text_change(original, replacement)
241 }
242
243 #[must_use]
245 pub fn with_before(mut self, block: Block) -> Self {
246 self.before = Some(Box::new(block));
247 self
248 }
249
250 #[must_use]
252 pub fn with_after(mut self, block: Block) -> Self {
253 self.after = Some(Box::new(block));
254 self
255 }
256
257 #[must_use]
259 pub fn with_range(mut self, range: TextRange) -> Self {
260 self.range = Some(range);
261 self
262 }
263
264 #[must_use]
266 pub fn with_text_change(mut self, original: impl Into<String>, new: impl Into<String>) -> Self {
267 self.original_text = Some(original.into());
268 self.new_text = Some(new.into());
269 self
270 }
271
272 #[must_use]
274 pub fn with_note(mut self, note: impl Into<String>) -> Self {
275 self.note = Some(note.into());
276 self
277 }
278
279 pub fn accept(&mut self) {
281 self.status = ChangeStatus::Accepted;
282 }
283
284 pub fn reject(&mut self) {
286 self.status = ChangeStatus::Rejected;
287 }
288
289 #[must_use]
291 pub fn is_pending(&self) -> bool {
292 self.status == ChangeStatus::Pending
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
298#[serde(rename_all = "lowercase")]
299#[strum(serialize_all = "lowercase")]
300pub enum ChangeType {
301 Insert,
303 Delete,
305 Modify,
307 Move,
309 Format,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
315#[serde(rename_all = "lowercase")]
316#[strum(serialize_all = "lowercase")]
317pub enum ChangeStatus {
318 #[default]
320 Pending,
321 Accepted,
323 Rejected,
325}