allow to delete bookmarks

This commit is contained in:
Nikita Galaiko 2023-05-19 16:00:51 +02:00
parent fd00c76f6f
commit 9fe324996d
10 changed files with 180 additions and 46 deletions

View File

@ -302,26 +302,26 @@ impl App {
self.files_database.list_by_project_id_session_id(project_id, session_id, paths) self.files_database.list_by_project_id_session_id(project_id, session_id, paths)
} }
pub fn create_bookmark(&self, project_id: &str, timestamp_ms: &u128, note: &str) -> Result<bookmarks::Bookmark> { pub fn upsert_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<()> {
let gb_repository = gb_repository::Repository::open( let gb_repository = gb_repository::Repository::open(
self.local_data_dir.clone(), self.local_data_dir.clone(),
project_id.to_string(), bookmark.project_id.to_string(),
self.projects_storage.clone(), self.projects_storage.clone(),
self.users_storage.clone(), self.users_storage.clone(),
) )
.context("failed to open repository")?; .context("failed to open repository")?;
let bookmark = bookmarks::Bookmark{
id: uuid::Uuid::new_v4().to_string(),
project_id: project_id.to_string(),
timestamp_ms: *timestamp_ms,
note: note.to_string(),
};
let session = gb_repository.get_or_create_current_session().context("failed to get or create current session")?; let session = gb_repository.get_or_create_current_session().context("failed to get or create current session")?;
let writer = sessions::Writer::open(&gb_repository, &session).context("failed to open session writer")?; let writer = sessions::Writer::open(&gb_repository, &session).context("failed to open session writer")?;
writer.write_bookmark(&bookmark).context("failed to write bookmark")?; writer.write_bookmark(&bookmark).context("failed to write bookmark")?;
Ok(bookmark)
self.proxy_watchers.lock().unwrap().get(&bookmark.project_id).map(|proxy_watcher| {
if let Err(e) = proxy_watcher.send(watcher::Event::Bookmark(bookmark.clone())) {
log::error!("failed to send bookmark event to proxy: {}", e);
}
});
Ok(())
} }
pub fn list_bookmarks(&self, project_id: &str, range: Option<ops::Range<u128>>) -> Result<Vec<bookmarks::Bookmark>> { pub fn list_bookmarks(&self, project_id: &str, range: Option<ops::Range<u128>>) -> Result<Vec<bookmarks::Bookmark>> {
@ -337,6 +337,7 @@ impl App {
self.deltas_database.list_by_project_id_session_id(project_id, session_id, paths) self.deltas_database.list_by_project_id_session_id(project_id, session_id, paths)
} }
pub fn git_activity( pub fn git_activity(
&self, &self,
project_id: &str, project_id: &str,

View File

@ -16,15 +16,32 @@ impl Database {
Self { database } Self { database }
} }
pub fn insert(&self, bookmark: &Bookmark) -> Result<()> { pub fn get_by_id(&self, id: &str) -> Result<Option<Bookmark>> {
self.database.transaction(|tx| {
let mut stmt = get_by_id_stmt(tx).context("Failed to prepare get_by_id statement")?;
let mut rows = stmt
.query(rusqlite::named_params! { ":id": id })
.context("Failed to execute get_by_id statement")?;
if let Some(row) = rows.next()? {
Ok(Some(parse_row(row)?))
} else {
Ok(None)
}
})
}
pub fn upsert(&self, bookmark: &Bookmark) -> Result<()> {
self.database.transaction(|tx| -> Result<()> { self.database.transaction(|tx| -> Result<()> {
let mut stmt = insert_stmt(tx).context("Failed to prepare insert statement")?; let mut stmt = insert_stmt(tx).context("Failed to prepare insert statement")?;
let timestamp_ms = bookmark.timestamp_ms.to_string(); let created_timestamp_ms = bookmark.created_timestamp_ms.to_string();
let updated_timestamp_ms = bookmark.updated_timestamp_ms.to_string();
stmt.execute(rusqlite::named_params! { stmt.execute(rusqlite::named_params! {
":id": &bookmark.id, ":id": &bookmark.id,
":project_id": &bookmark.project_id, ":project_id": &bookmark.project_id,
":timestamp_ms": &timestamp_ms, ":created_timestamp_ms": &created_timestamp_ms,
":updated_timestamp_ms": &updated_timestamp_ms,
":note": &bookmark.note, ":note": &bookmark.note,
":deleted": &bookmark.deleted,
}) })
.context("Failed to execute insert statement")?; .context("Failed to execute insert statement")?;
Ok(()) Ok(())
@ -87,8 +104,24 @@ fn insert_stmt<'conn>(
) -> Result<rusqlite::CachedStatement<'conn>> { ) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached( Ok(tx.prepare_cached(
" "
INSERT INTO `bookmarks` (`id`, `project_id`, `timestamp_ms`, `note`) INSERT INTO `bookmarks` (`id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`)
VALUES (:id, :project_id, :timestamp_ms, :note) VALUES (:id, :project_id, :created_timestamp_ms, :updated_timestamp_ms, :note, :deleted)
ON CONFLICT(`id`) DO UPDATE SET
`updated_timestamp_ms` = :updated_timestamp_ms,
`note` = :note,
`deleted` = :deleted
",
)?)
}
fn get_by_id_stmt<'conn>(
tx: &'conn rusqlite::Transaction,
) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached(
"
SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`
FROM `bookmarks`
WHERE `id` = :id
", ",
)?) )?)
} }
@ -98,12 +131,12 @@ fn list_by_project_id_range_stmt<'conn>(
) -> Result<rusqlite::CachedStatement<'conn>> { ) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached( Ok(tx.prepare_cached(
" "
SELECT `id`, `project_id`, `timestamp_ms`, `note` SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`
FROM `bookmarks` FROM `bookmarks`
WHERE `project_id` = :project_id WHERE `project_id` = :project_id
AND `timestamp_ms` >= :start AND `updated_timestamp_ms` >= :start
AND `timestamp_ms` < :end AND `updated_timestamp_ms` < :end
ORDER BY `timestamp_ms` DESC ORDER BY `created_timestamp_ms` DESC
", ",
)?) )?)
} }
@ -113,10 +146,10 @@ fn list_by_project_id_stmt<'conn>(
) -> Result<rusqlite::CachedStatement<'conn>> { ) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached( Ok(tx.prepare_cached(
" "
SELECT `id`, `project_id`, `timestamp_ms`, `note` SELECT `id`, `project_id`, `created_timestamp_ms`, `updated_timestamp_ms`, `note`, `deleted`
FROM `bookmarks` FROM `bookmarks`
WHERE `project_id` = :project_id WHERE `project_id` = :project_id
ORDER BY `timestamp_ms` DESC ORDER BY `created_timestamp_ms` DESC
", ",
)?) )?)
} }
@ -125,12 +158,18 @@ fn parse_row(row: &rusqlite::Row) -> Result<Bookmark> {
Ok(Bookmark { Ok(Bookmark {
id: row.get(0).context("Failed to get id")?, id: row.get(0).context("Failed to get id")?,
project_id: row.get(1).context("Failed to get project_id")?, project_id: row.get(1).context("Failed to get project_id")?,
timestamp_ms: row created_timestamp_ms: row
.get::<usize, String>(2) .get::<usize, String>(2)
.context("Failed to get timestamp_ms")? .context("Failed to get created_timestamp_ms")?
.parse() .parse::<u128>()
.context("Failed to parse timestamp_ms")?, .context("Failed to parse created_timestamp_ms")?,
note: row.get(3).context("Failed to get note")?, updated_timestamp_ms: row
.get::<usize, String>(3)
.context("Failed to get updated_timestamp_ms")?
.parse::<u128>()
.context("Failed to parse updated_timestamp_ms")?,
note: row.get(4).context("Failed to get note")?,
deleted: row.get(5).context("Failed to get deleted")?,
}) })
} }
@ -146,11 +185,13 @@ mod tests {
let bookmark = Bookmark { let bookmark = Bookmark {
id: "id".to_string(), id: "id".to_string(),
project_id: "project_id".to_string(), project_id: "project_id".to_string(),
timestamp_ms: 123, created_timestamp_ms: 123,
updated_timestamp_ms: 123,
note: "note".to_string(), note: "note".to_string(),
deleted: false,
}; };
database.insert(&bookmark)?; database.upsert(&bookmark)?;
let result = database.list_by_project_id_all(&bookmark.project_id)?; let result = database.list_by_project_id_all(&bookmark.project_id)?;
@ -167,18 +208,22 @@ mod tests {
let bookmark_one = Bookmark { let bookmark_one = Bookmark {
id: "id".to_string(), id: "id".to_string(),
project_id: "project_id".to_string(), project_id: "project_id".to_string(),
timestamp_ms: 123, created_timestamp_ms: 123,
updated_timestamp_ms: 123,
note: "note".to_string(), note: "note".to_string(),
deleted: false,
}; };
database.insert(&bookmark_one)?; database.upsert(&bookmark_one)?;
let bookmark_two = Bookmark { let bookmark_two = Bookmark {
id: "id2".to_string(), id: "id2".to_string(),
project_id: "project_id".to_string(), project_id: "project_id".to_string(),
timestamp_ms: 456, created_timestamp_ms: 456,
updated_timestamp_ms: 456,
note: "note".to_string(), note: "note".to_string(),
deleted: false,
}; };
database.insert(&bookmark_two)?; database.upsert(&bookmark_two)?;
let result = database.list_by_project_id_range( let result = database.list_by_project_id_range(
&bookmark_one.project_id, &bookmark_one.project_id,
@ -188,4 +233,34 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn update() -> Result<()> {
let db = database::Database::memory()?;
let database = Database::new(db);
assert_eq!(database.get_by_id("id")?, None);
let bookmark = Bookmark {
id: "id".to_string(),
project_id: "project_id".to_string(),
created_timestamp_ms: 123,
updated_timestamp_ms: 123,
note: "note".to_string(),
deleted: false,
};
database.upsert(&bookmark)?;
assert_eq!(database.get_by_id(&bookmark.id)?, Some(bookmark.clone()));
let updated = Bookmark {
note: "updated".to_string(),
updated_timestamp_ms: 456,
..bookmark.clone()
};
database.upsert(&updated)?;
assert_eq!(database.get_by_id(&bookmark.id.clone())?, Some(updated));
Ok(())
}
} }

View File

@ -3,13 +3,27 @@ mod reader;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Bookmark { pub struct Bookmark {
pub id: String, pub id: String,
pub project_id: String, pub project_id: String,
pub timestamp_ms: u128, pub created_timestamp_ms: u128,
pub updated_timestamp_ms: u128,
pub note: String, pub note: String,
pub deleted: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum Event {
Created(Bookmark),
Updated {
id: String,
note: Option<String>,
deleted: Option<bool>,
timestamp_ms: u128,
},
} }
pub use database::Database; pub use database::Database;

View File

@ -0,0 +1,16 @@
ALTER TABLE `bookmarks`
ADD `created_timestamp_ms` text NOT NULL DEFAULT 0;
UPDATE
`bookmarks`
SET
`created_timestamp_ms` = `timestamp_ms`;
ALTER TABLE `bookmarks`
DROP COLUMN `timestamp_ms`;
ALTER TABLE `bookmarks`
ADD `updated_timestamp_ms` text;
ALTER TABLE `bookmarks`
ADD `deleted` boolean NOT NULL DEFAULT FALSE;

View File

@ -486,10 +486,10 @@ async fn delete_all_data(handle: tauri::AppHandle) -> Result<(), Error> {
#[timed(duration(printer = "debug!"))] #[timed(duration(printer = "debug!"))]
#[tauri::command(async)] #[tauri::command(async)]
async fn create_bookmark(handle: tauri::AppHandle, project_id: &str, timestamp_ms: u128, note: &str) -> Result<bookmarks::Bookmark, Error> { async fn upsert_bookmark(handle: tauri::AppHandle, bookmark: bookmarks::Bookmark) -> Result<(), Error> {
let app = handle.state::<app::App>(); let app = handle.state::<app::App>();
let bookmark = app.create_bookmark(project_id, &timestamp_ms, note).context("failed to create bookmark")?; app.upsert_bookmark(&bookmark).context("failed to upsert bookmark")?;
Ok(bookmark) Ok(())
} }
#[timed(duration(printer = "debug!"))] #[timed(duration(printer = "debug!"))]
@ -638,7 +638,7 @@ fn main() {
get_logs_archive_path, get_logs_archive_path,
get_project_archive_path, get_project_archive_path,
get_project_data_archive_path, get_project_data_archive_path,
create_bookmark, upsert_bookmark,
list_bookmarks, list_bookmarks,
]) ])
.build(tauri_context) .build(tauri_context)

View File

@ -1,6 +1,6 @@
use std::{path, time}; use std::{path, time};
use crate::{deltas, sessions}; use crate::{bookmarks, deltas, sessions};
pub enum Event { pub enum Event {
Tick(time::SystemTime), Tick(time::SystemTime),
@ -17,6 +17,7 @@ pub enum Event {
ProjectFileChange(path::PathBuf), ProjectFileChange(path::PathBuf),
Session(sessions::Session), Session(sessions::Session),
Bookmark(bookmarks::Bookmark),
File((String, path::PathBuf, String)), File((String, path::PathBuf, String)),
Deltas((String, path::PathBuf, Vec<deltas::Delta>)), Deltas((String, path::PathBuf, Vec<deltas::Delta>)),
} }

View File

@ -60,7 +60,14 @@ impl<'handler> Handler<'handler> {
} }
pub fn index_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<Vec<events::Event>> { pub fn index_bookmark(&self, bookmark: &bookmarks::Bookmark) -> Result<Vec<events::Event>> {
self.bookmarks_database.insert(&bookmark)?; match self.bookmarks_database.get_by_id(&bookmark.id)? {
Some(existing) => {
if existing.updated_timestamp_ms < bookmark.updated_timestamp_ms {
self.bookmarks_database.upsert(&bookmark)?;
}
}
None => self.bookmarks_database.upsert(&bookmark)?,
}
Ok(vec![]) Ok(vec![])
} }

View File

@ -139,7 +139,8 @@ impl<'handler> Handler<'handler> {
events::Event::Fetch => self.fetch_project_handler.handle(), events::Event::Fetch => self.fetch_project_handler.handle(),
events::Event::File((session_id, file_path, contents)) => { events::Event::File((session_id, file_path, contents)) => {
self.index_handler let file_events = self
.index_handler
.index_file(&session_id, file_path.to_str().unwrap(), &contents) .index_file(&session_id, file_path.to_str().unwrap(), &contents)
.context("failed to index file")?; .context("failed to index file")?;
self.events_sender self.events_sender
@ -150,19 +151,21 @@ impl<'handler> Handler<'handler> {
&contents, &contents,
)) ))
.context("failed to send file event")?; .context("failed to send file event")?;
Ok(vec![]) Ok(file_events)
} }
events::Event::Session(session) => { events::Event::Session(session) => {
self.index_handler let session_events = self
.index_handler
.index_session(&session) .index_session(&session)
.context("failed to index session")?; .context("failed to index session")?;
self.events_sender self.events_sender
.send(app_events::Event::session(&self.project_id, &session)) .send(app_events::Event::session(&self.project_id, &session))
.context("failed to send session event")?; .context("failed to send session event")?;
Ok(vec![]) Ok(session_events)
} }
events::Event::Deltas((session_id, path, deltas)) => { events::Event::Deltas((session_id, path, deltas)) => {
self.index_handler let delta_events = self
.index_handler
.index_deltas(&session_id, path.to_str().unwrap(), &deltas) .index_deltas(&session_id, path.to_str().unwrap(), &deltas)
.context("failed to index deltas")?; .context("failed to index deltas")?;
self.events_sender self.events_sender
@ -173,8 +176,12 @@ impl<'handler> Handler<'handler> {
&path, &path,
)) ))
.context("failed to send deltas event")?; .context("failed to send deltas event")?;
Ok(vec![]) Ok(delta_events)
} }
events::Event::Bookmark(bookmark) => self
.index_handler
.index_bookmark(&bookmark)
.context("failed to index bookmark"),
} }
} }
} }

View File

@ -0,0 +1,12 @@
import { invoke } from '$lib/ipc';
export type Bookmark = {
id: string;
projectId: string;
createdTimestampMs: number;
updatedTimestampMs: number;
note: string;
deleted: boolean;
};
export const upsert = (params: { bookmark: Bookmark }) => invoke<void>('upsert_bookmark', params);

View File

@ -11,6 +11,7 @@ export * as searchResults from './search';
export { type SearchResult } from './search'; export { type SearchResult } from './search';
export * as files from './files'; export * as files from './files';
export * as zip from './zip'; export * as zip from './zip';
export * as bookmarks from './bookmarks';
import { invoke } from '$lib/ipc'; import { invoke } from '$lib/ipc';