Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5bdf461ba | ||
|
|
cd1cb27d49 | ||
|
|
10d7cfa739 | ||
|
|
dc6f027d6b |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -59,6 +59,7 @@ jobs:
|
|||||||
- name: Type check
|
- name: Type check
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: npx vue-tsc --noEmit
|
run: npx vue-tsc --noEmit
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
49
.github/workflows/release.yml
vendored
49
.github/workflows/release.yml
vendored
@@ -10,21 +10,27 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build ${{ matrix.target }}
|
name: Build ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- name: Linux (x86_64)
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
archive: tar.gz
|
archive: tar.gz
|
||||||
- target: x86_64-apple-darwin
|
asset_name: ironpad-linux-x86_64
|
||||||
|
- name: macOS (x86_64)
|
||||||
|
target: x86_64-apple-darwin
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
archive: tar.gz
|
archive: tar.gz
|
||||||
- target: x86_64-pc-windows-msvc
|
asset_name: ironpad-macos-x86_64
|
||||||
|
- name: Windows (x86_64)
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
archive: zip
|
archive: zip
|
||||||
|
asset_name: ironpad-windows-x86_64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -35,6 +41,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Install system dependencies (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y cmake
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -60,10 +70,10 @@ jobs:
|
|||||||
if: matrix.archive == 'tar.gz'
|
if: matrix.archive == 'tar.gz'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BINARY_NAME="ironpad"
|
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
||||||
RELEASE_DIR="ironpad-${{ github.ref_name }}-${{ matrix.target }}"
|
|
||||||
mkdir -p "$RELEASE_DIR"
|
mkdir -p "$RELEASE_DIR"
|
||||||
cp "backend/target/${{ matrix.target }}/release/$BINARY_NAME" "$RELEASE_DIR/"
|
cp "backend/target/${{ matrix.target }}/release/ironpad" "$RELEASE_DIR/"
|
||||||
|
cp -r backend/static "$RELEASE_DIR/static"
|
||||||
cp README.md LICENSE "$RELEASE_DIR/"
|
cp README.md LICENSE "$RELEASE_DIR/"
|
||||||
tar czf "$RELEASE_DIR.tar.gz" "$RELEASE_DIR"
|
tar czf "$RELEASE_DIR.tar.gz" "$RELEASE_DIR"
|
||||||
echo "ASSET=$RELEASE_DIR.tar.gz" >> $GITHUB_ENV
|
echo "ASSET=$RELEASE_DIR.tar.gz" >> $GITHUB_ENV
|
||||||
@@ -72,10 +82,10 @@ jobs:
|
|||||||
if: matrix.archive == 'zip'
|
if: matrix.archive == 'zip'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BINARY_NAME="ironpad.exe"
|
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
||||||
RELEASE_DIR="ironpad-${{ github.ref_name }}-${{ matrix.target }}"
|
|
||||||
mkdir -p "$RELEASE_DIR"
|
mkdir -p "$RELEASE_DIR"
|
||||||
cp "backend/target/${{ matrix.target }}/release/$BINARY_NAME" "$RELEASE_DIR/"
|
cp "backend/target/${{ matrix.target }}/release/ironpad.exe" "$RELEASE_DIR/"
|
||||||
|
cp -r backend/static "$RELEASE_DIR/static"
|
||||||
cp README.md LICENSE "$RELEASE_DIR/"
|
cp README.md LICENSE "$RELEASE_DIR/"
|
||||||
7z a "$RELEASE_DIR.zip" "$RELEASE_DIR"
|
7z a "$RELEASE_DIR.zip" "$RELEASE_DIR"
|
||||||
echo "ASSET=$RELEASE_DIR.zip" >> $GITHUB_ENV
|
echo "ASSET=$RELEASE_DIR.zip" >> $GITHUB_ENV
|
||||||
@@ -83,7 +93,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ironpad-${{ matrix.target }}
|
name: ${{ matrix.asset_name }}
|
||||||
path: ${{ env.ASSET }}
|
path: ${{ env.ASSET }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -99,6 +109,23 @@ jobs:
|
|||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
name: Ironpad ${{ github.ref_name }}
|
||||||
|
body: |
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| Windows (x86_64) | `ironpad-windows-x86_64-${{ github.ref_name }}.zip` |
|
||||||
|
| macOS (x86_64) | `ironpad-macos-x86_64-${{ github.ref_name }}.tar.gz` |
|
||||||
|
| Linux (x86_64) | `ironpad-linux-x86_64-${{ github.ref_name }}.tar.gz` |
|
||||||
|
|
||||||
|
**Linux:** Extract the tar.gz and run `./ironpad`. Works on any distro -- it's a standalone binary with no dependencies.
|
||||||
|
|
||||||
|
**macOS:** Extract and run. You may need to allow it in System Settings > Privacy & Security on first launch.
|
||||||
|
|
||||||
|
**Windows:** Extract the zip and run `ironpad.exe`.
|
||||||
|
|
||||||
|
---
|
||||||
files: artifacts/**/*
|
files: artifacts/**/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
|
|||||||
10
backend/Cargo.lock
generated
10
backend/Cargo.lock
generated
@@ -1141,6 +1141,15 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "300.5.5+3.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.111"
|
version = "0.9.111"
|
||||||
@@ -1149,6 +1158,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ serde_yaml = "0.9"
|
|||||||
# Markdown parsing (CommonMark)
|
# Markdown parsing (CommonMark)
|
||||||
markdown = "1.0.0-alpha.22"
|
markdown = "1.0.0-alpha.22"
|
||||||
|
|
||||||
# Git operations
|
# Git operations (vendored-openssl for cross-platform CI builds)
|
||||||
git2 = "0.19"
|
git2 = { version = "0.19", features = ["vendored-openssl"] }
|
||||||
|
|
||||||
|
|
||||||
# File system watching
|
# File system watching
|
||||||
|
|||||||
@@ -5,19 +5,39 @@ use std::sync::OnceLock;
|
|||||||
/// Priority: IRONPAD_DATA_DIR env var > auto-detect (production vs development).
|
/// Priority: IRONPAD_DATA_DIR env var > auto-detect (production vs development).
|
||||||
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Directory where the executable lives.
|
||||||
|
/// Used to resolve `static/` and `data/` in production mode.
|
||||||
|
static EXE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Get the directory containing the executable.
|
||||||
|
/// Falls back to "." if detection fails.
|
||||||
|
pub fn exe_dir() -> &'static Path {
|
||||||
|
EXE_DIR.get_or_init(|| {
|
||||||
|
std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if we're in production mode (static/index.html exists next to the binary).
|
||||||
|
pub fn is_production() -> bool {
|
||||||
|
exe_dir().join("static").join("index.html").exists()
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize the data directory path. Call once at startup.
|
/// Initialize the data directory path. Call once at startup.
|
||||||
///
|
///
|
||||||
/// Resolution order:
|
/// Resolution order:
|
||||||
/// 1. `IRONPAD_DATA_DIR` environment variable (if set)
|
/// 1. `IRONPAD_DATA_DIR` environment variable (if set)
|
||||||
/// 2. `./data` if `static/index.html` exists (production mode)
|
/// 2. `{exe_dir}/data` if `{exe_dir}/static/index.html` exists (production mode)
|
||||||
/// 3. `../data` (development mode, binary runs from backend/)
|
/// 3. `../data` (development mode, binary runs from backend/)
|
||||||
pub fn init_data_dir() {
|
pub fn init_data_dir() {
|
||||||
let path = if let Ok(custom) = std::env::var("IRONPAD_DATA_DIR") {
|
let path = if let Ok(custom) = std::env::var("IRONPAD_DATA_DIR") {
|
||||||
tracing::info!("Using custom data directory from IRONPAD_DATA_DIR");
|
tracing::info!("Using custom data directory from IRONPAD_DATA_DIR");
|
||||||
PathBuf::from(custom)
|
PathBuf::from(custom)
|
||||||
} else if Path::new("static/index.html").exists() {
|
} else if is_production() {
|
||||||
// Production mode: data/ is next to the binary
|
// Production mode: data/ is next to the binary
|
||||||
PathBuf::from("data")
|
exe_dir().join("data")
|
||||||
} else {
|
} else {
|
||||||
// Development mode: binary runs from backend/, data/ is one level up
|
// Development mode: binary runs from backend/, data/ is one level up
|
||||||
PathBuf::from("../data")
|
PathBuf::from("../data")
|
||||||
@@ -31,10 +51,14 @@ pub fn init_data_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("Data directory: {}", path.display());
|
tracing::info!("Data directory: {}", path.display());
|
||||||
DATA_DIR.set(path).expect("Data directory already initialized");
|
DATA_DIR
|
||||||
|
.set(path)
|
||||||
|
.expect("Data directory already initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the resolved data directory path.
|
/// Get the resolved data directory path.
|
||||||
pub fn data_dir() -> &'static Path {
|
pub fn data_dir() -> &'static Path {
|
||||||
DATA_DIR.get().expect("Data directory not initialized. Call config::init_data_dir() first.")
|
DATA_DIR
|
||||||
|
.get()
|
||||||
|
.expect("Data directory not initialized. Call config::init_data_dir() first.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
@@ -95,14 +94,19 @@ async fn main() {
|
|||||||
.layer(cors);
|
.layer(cors);
|
||||||
|
|
||||||
// Check for embedded frontend (production mode)
|
// Check for embedded frontend (production mode)
|
||||||
let static_dir = Path::new("static");
|
// Resolve relative to the executable's directory, not the working directory
|
||||||
let has_frontend = static_dir.join("index.html").exists();
|
let has_frontend = config::is_production();
|
||||||
|
|
||||||
if has_frontend {
|
if has_frontend {
|
||||||
// Production mode: serve frontend from static/ and use SPA fallback
|
// Production mode: serve frontend from static/ next to the exe
|
||||||
info!("Production mode: serving frontend from static/");
|
let static_path = config::exe_dir().join("static");
|
||||||
let serve_dir = ServeDir::new("static")
|
let index_path = static_path.join("index.html");
|
||||||
.fallback(tower_http::services::ServeFile::new("static/index.html"));
|
info!(
|
||||||
|
"Production mode: serving frontend from {}",
|
||||||
|
static_path.display()
|
||||||
|
);
|
||||||
|
let serve_dir =
|
||||||
|
ServeDir::new(&static_path).fallback(tower_http::services::ServeFile::new(index_path));
|
||||||
app = app.fallback_service(serve_dir);
|
app = app.fallback_service(serve_dir);
|
||||||
} else {
|
} else {
|
||||||
// Development mode: API-only
|
// Development mode: API-only
|
||||||
@@ -121,7 +125,11 @@ async fn main() {
|
|||||||
// Small delay to ensure server is ready
|
// Small delay to ensure server is ready
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||||
if let Err(e) = webbrowser::open(&url) {
|
if let Err(e) = webbrowser::open(&url) {
|
||||||
tracing::warn!("Failed to open browser: {}. Open http://localhost:{} manually.", e, port);
|
tracing::warn!(
|
||||||
|
"Failed to open browser: {}. Open http://localhost:{} manually.",
|
||||||
|
e,
|
||||||
|
port
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,10 @@ async fn upload_asset(
|
|||||||
if !is_allowed_content_type(&content_type) {
|
if !is_allowed_content_type(&content_type) {
|
||||||
return (
|
return (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("Unsupported file type: {}. Only images are allowed.", content_type),
|
format!(
|
||||||
|
"Unsupported file type: {}. Only images are allowed.",
|
||||||
|
content_type
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,10 @@ async fn upload_asset(
|
|||||||
if data.len() > MAX_FILE_SIZE {
|
if data.len() > MAX_FILE_SIZE {
|
||||||
return (
|
return (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("File too large. Maximum size is {} MB.", MAX_FILE_SIZE / 1024 / 1024),
|
format!(
|
||||||
|
"File too large. Maximum size is {} MB.",
|
||||||
|
MAX_FILE_SIZE / 1024 / 1024
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
@@ -149,7 +155,11 @@ async fn upload_asset(
|
|||||||
|
|
||||||
/// Validate that a path component doesn't contain directory traversal
|
/// Validate that a path component doesn't contain directory traversal
|
||||||
fn validate_path_component(component: &str) -> Result<(), String> {
|
fn validate_path_component(component: &str) -> Result<(), String> {
|
||||||
if component.contains("..") || component.contains('/') || component.contains('\\') || component.is_empty() {
|
if component.contains("..")
|
||||||
|
|| component.contains('/')
|
||||||
|
|| component.contains('\\')
|
||||||
|
|| component.is_empty()
|
||||||
|
{
|
||||||
return Err("Invalid path component".to_string());
|
return Err("Invalid path component".to_string());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -199,12 +209,7 @@ async fn get_asset(Path((project, filename)): Path<(String, String)>) -> impl In
|
|||||||
let stream = ReaderStream::new(file);
|
let stream = ReaderStream::new(file);
|
||||||
let body = Body::from_stream(stream);
|
let body = Body::from_stream(stream);
|
||||||
|
|
||||||
(
|
(StatusCode::OK, [(header::CONTENT_TYPE, content_type)], body).into_response()
|
||||||
StatusCode::OK,
|
|
||||||
[(header::CONTENT_TYPE, content_type)],
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_allowed_content_type(content_type: &str) -> bool {
|
fn is_allowed_content_type(content_type: &str) -> bool {
|
||||||
@@ -220,11 +225,7 @@ fn is_allowed_content_type(content_type: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_content_type(filename: &str) -> &'static str {
|
fn get_content_type(filename: &str) -> &'static str {
|
||||||
let ext = filename
|
let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase();
|
||||||
.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"jpg" | "jpeg" => "image/jpeg",
|
"jpg" | "jpeg" => "image/jpeg",
|
||||||
@@ -248,7 +249,13 @@ fn generate_unique_filename(dir: &StdPath, original: &str) -> String {
|
|||||||
// Sanitize filename
|
// Sanitize filename
|
||||||
let sanitized_name: String = name
|
let sanitized_name: String = name
|
||||||
.chars()
|
.chars()
|
||||||
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
|
.map(|c| {
|
||||||
|
if c.is_alphanumeric() || c == '-' || c == '_' {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let base_filename = format!("{}{}", sanitized_name, ext);
|
let base_filename = format!("{}{}", sanitized_name, ext);
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes, extract::Path, http::StatusCode, response::IntoResponse, routing::get, Json,
|
||||||
extract::Path,
|
Router,
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Json, Router,
|
|
||||||
};
|
};
|
||||||
use chrono::{NaiveDate, Utc};
|
use chrono::{NaiveDate, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use crate::services::filesystem;
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
use crate::services::filesystem;
|
||||||
use crate::services::frontmatter;
|
use crate::services::frontmatter;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -35,7 +31,12 @@ pub fn router() -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_daily_notes))
|
.route("/", get(list_daily_notes))
|
||||||
.route("/today", get(get_or_create_today))
|
.route("/today", get(get_or_create_today))
|
||||||
.route("/{date}", get(get_daily_note).post(create_daily_note).put(update_daily_note))
|
.route(
|
||||||
|
"/{date}",
|
||||||
|
get(get_daily_note)
|
||||||
|
.post(create_daily_note)
|
||||||
|
.put(update_daily_note),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all daily notes
|
/// List all daily notes
|
||||||
@@ -123,14 +124,16 @@ async fn get_or_create_today() -> impl IntoResponse {
|
|||||||
async fn get_daily_note(Path(date): Path<String>) -> impl IntoResponse {
|
async fn get_daily_note(Path(date): Path<String>) -> impl IntoResponse {
|
||||||
// Validate date format
|
// Validate date format
|
||||||
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
||||||
return (StatusCode::BAD_REQUEST, "Invalid date format. Use YYYY-MM-DD").into_response();
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Invalid date format. Use YYYY-MM-DD",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
match get_daily_note_impl(&date) {
|
match get_daily_note_impl(&date) {
|
||||||
Ok(note) => Json(note).into_response(),
|
Ok(note) => Json(note).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to get daily note: {}", err),
|
format!("Failed to get daily note: {}", err),
|
||||||
@@ -171,16 +174,18 @@ async fn create_daily_note(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Validate date format
|
// Validate date format
|
||||||
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
||||||
return (StatusCode::BAD_REQUEST, "Invalid date format. Use YYYY-MM-DD").into_response();
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Invalid date format. Use YYYY-MM-DD",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = body.and_then(|b| b.content.clone());
|
let content = body.and_then(|b| b.content.clone());
|
||||||
|
|
||||||
match create_daily_note_impl(&date, content.as_deref()) {
|
match create_daily_note_impl(&date, content.as_deref()) {
|
||||||
Ok(note) => (StatusCode::CREATED, Json(note)).into_response(),
|
Ok(note) => (StatusCode::CREATED, Json(note)).into_response(),
|
||||||
Err(err) if err.contains("already exists") => {
|
Err(err) if err.contains("already exists") => (StatusCode::CONFLICT, err).into_response(),
|
||||||
(StatusCode::CONFLICT, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to create daily note: {}", err),
|
format!("Failed to create daily note: {}", err),
|
||||||
@@ -206,8 +211,7 @@ fn create_daily_note_impl(date: &str, initial_content: Option<&str>) -> Result<D
|
|||||||
let now = Utc::now().to_rfc3339();
|
let now = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
// Parse date for display
|
// Parse date for display
|
||||||
let parsed_date = NaiveDate::parse_from_str(date, "%Y-%m-%d")
|
let parsed_date = NaiveDate::parse_from_str(date, "%Y-%m-%d").map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let display_date = parsed_date.format("%A, %B %d, %Y").to_string();
|
let display_date = parsed_date.format("%A, %B %d, %Y").to_string();
|
||||||
|
|
||||||
// Create frontmatter
|
// Create frontmatter
|
||||||
@@ -238,14 +242,12 @@ fn create_daily_note_impl(date: &str, initial_content: Option<&str>) -> Result<D
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use provided content or default template
|
// Use provided content or default template
|
||||||
let body = initial_content
|
let body = initial_content.map(|c| c.to_string()).unwrap_or_else(|| {
|
||||||
.map(|c| c.to_string())
|
format!(
|
||||||
.unwrap_or_else(|| {
|
"# {}\n\n## Today's Focus\n\n- \n\n## Notes\n\n\n\n## Tasks\n\n- [ ] \n",
|
||||||
format!(
|
display_date
|
||||||
"# {}\n\n## Today's Focus\n\n- \n\n## Notes\n\n\n\n## Tasks\n\n- [ ] \n",
|
)
|
||||||
display_date
|
});
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let content = frontmatter::serialize_frontmatter(&fm, &body)?;
|
let content = frontmatter::serialize_frontmatter(&fm, &body)?;
|
||||||
|
|
||||||
@@ -261,22 +263,21 @@ fn create_daily_note_impl(date: &str, initial_content: Option<&str>) -> Result<D
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update a daily note's content
|
/// Update a daily note's content
|
||||||
async fn update_daily_note(
|
async fn update_daily_note(Path(date): Path<String>, body: Bytes) -> impl IntoResponse {
|
||||||
Path(date): Path<String>,
|
|
||||||
body: Bytes,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
// Validate date format
|
// Validate date format
|
||||||
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
if NaiveDate::parse_from_str(&date, "%Y-%m-%d").is_err() {
|
||||||
return (StatusCode::BAD_REQUEST, "Invalid date format. Use YYYY-MM-DD").into_response();
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"Invalid date format. Use YYYY-MM-DD",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = String::from_utf8_lossy(&body).to_string();
|
let content = String::from_utf8_lossy(&body).to_string();
|
||||||
|
|
||||||
match update_daily_note_impl(&date, &content) {
|
match update_daily_note_impl(&date, &content) {
|
||||||
Ok(note) => Json(note).into_response(),
|
Ok(note) => Json(note).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to update daily note: {}", err),
|
format!("Failed to update daily note: {}", err),
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
extract::Path,
|
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Json, Router,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::models::note::{Note, NoteSummary};
|
use crate::models::note::{Note, NoteSummary};
|
||||||
use crate::services::filesystem;
|
use crate::services::filesystem;
|
||||||
|
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
Router::new()
|
Router::new().route("/{id}", get(get_note).put(update_note).delete(delete_note))
|
||||||
.route("/{id}", get(get_note).put(update_note).delete(delete_note))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_notes() -> impl IntoResponse {
|
pub async fn list_notes() -> impl IntoResponse {
|
||||||
@@ -50,10 +43,7 @@ pub async fn create_note() -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_note(
|
async fn update_note(Path(id): Path<String>, body: String) -> impl IntoResponse {
|
||||||
Path(id): Path<String>,
|
|
||||||
body: String,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
match filesystem::update_note(&id, &body) {
|
match filesystem::update_note(&id, &body) {
|
||||||
Ok(note) => Json::<Note>(note).into_response(),
|
Ok(note) => Json::<Note>(note).into_response(),
|
||||||
Err(err) if err.starts_with("Note not found") => {
|
Err(err) if err.starts_with("Note not found") => {
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ use axum::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
use crate::routes::tasks::{
|
use crate::routes::tasks::{
|
||||||
CreateTaskRequest, UpdateTaskMetaRequest,
|
create_task_handler, delete_task_handler, get_task_handler, list_project_tasks_handler,
|
||||||
list_project_tasks_handler, create_task_handler, get_task_handler,
|
toggle_task_handler, update_task_content_handler, update_task_meta_handler, CreateTaskRequest,
|
||||||
update_task_content_handler, toggle_task_handler, update_task_meta_handler,
|
UpdateTaskMetaRequest,
|
||||||
delete_task_handler,
|
|
||||||
};
|
};
|
||||||
use crate::services::filesystem;
|
use crate::services::filesystem;
|
||||||
use crate::config;
|
|
||||||
use crate::services::frontmatter;
|
use crate::services::frontmatter;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -75,15 +74,34 @@ pub fn router() -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_projects).post(create_project))
|
.route("/", get(list_projects).post(create_project))
|
||||||
.route("/{id}", get(get_project))
|
.route("/{id}", get(get_project))
|
||||||
.route("/{id}/content", get(get_project_content).put(update_project_content))
|
.route(
|
||||||
|
"/{id}/content",
|
||||||
|
get(get_project_content).put(update_project_content),
|
||||||
|
)
|
||||||
// Task routes (file-based)
|
// Task routes (file-based)
|
||||||
.route("/{id}/tasks", get(get_project_tasks).post(create_project_task))
|
.route(
|
||||||
.route("/{id}/tasks/{task_id}", get(get_project_task).put(update_project_task).delete(delete_project_task))
|
"/{id}/tasks",
|
||||||
|
get(get_project_tasks).post(create_project_task),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/tasks/{task_id}",
|
||||||
|
get(get_project_task)
|
||||||
|
.put(update_project_task)
|
||||||
|
.delete(delete_project_task),
|
||||||
|
)
|
||||||
.route("/{id}/tasks/{task_id}/toggle", put(toggle_project_task))
|
.route("/{id}/tasks/{task_id}/toggle", put(toggle_project_task))
|
||||||
.route("/{id}/tasks/{task_id}/meta", put(update_project_task_meta))
|
.route("/{id}/tasks/{task_id}/meta", put(update_project_task_meta))
|
||||||
// Note routes
|
// Note routes
|
||||||
.route("/{id}/notes", get(list_project_notes).post(create_project_note))
|
.route(
|
||||||
.route("/{id}/notes/{note_id}", get(get_project_note).put(update_project_note).delete(delete_project_note))
|
"/{id}/notes",
|
||||||
|
get(list_project_notes).post(create_project_note),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/notes/{note_id}",
|
||||||
|
get(get_project_note)
|
||||||
|
.put(update_project_note)
|
||||||
|
.delete(delete_project_note),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Task Handlers ============
|
// ============ Task Handlers ============
|
||||||
@@ -355,10 +373,7 @@ async fn get_project_content(Path(id): Path<String>) -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_project_content(
|
async fn update_project_content(Path(id): Path<String>, body: String) -> impl IntoResponse {
|
||||||
Path(id): Path<String>,
|
|
||||||
body: String,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
let index_path = config::data_dir()
|
let index_path = config::data_dir()
|
||||||
.join("projects")
|
.join("projects")
|
||||||
.join(&id)
|
.join(&id)
|
||||||
@@ -618,7 +633,9 @@ async fn create_project_note(
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_project_note(Path((project_id, note_id)): Path<(String, String)>) -> impl IntoResponse {
|
async fn get_project_note(
|
||||||
|
Path((project_id, note_id)): Path<(String, String)>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
let notes_dir = config::data_dir()
|
let notes_dir = config::data_dir()
|
||||||
.join("projects")
|
.join("projects")
|
||||||
.join(&project_id)
|
.join(&project_id)
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
use axum::{
|
use axum::{extract::Query, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
extract::Query,
|
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Json, Router,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::services::search;
|
use crate::services::search;
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
use axum::{
|
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Json, Router,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path as StdPath;
|
use std::path::Path as StdPath;
|
||||||
|
|
||||||
use crate::services::filesystem;
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
|
use crate::services::filesystem;
|
||||||
use crate::services::frontmatter;
|
use crate::services::frontmatter;
|
||||||
|
|
||||||
/// Task summary for list views
|
/// Task summary for list views
|
||||||
@@ -73,8 +68,7 @@ pub struct UpdateTaskMetaRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router {
|
pub fn router() -> Router {
|
||||||
Router::new()
|
Router::new().route("/", get(list_all_tasks_handler))
|
||||||
.route("/", get(list_all_tasks_handler))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Handler Functions (called from projects.rs) ============
|
// ============ Handler Functions (called from projects.rs) ============
|
||||||
@@ -96,7 +90,12 @@ pub async fn create_task_handler(
|
|||||||
project_id: String,
|
project_id: String,
|
||||||
payload: CreateTaskRequest,
|
payload: CreateTaskRequest,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match create_task_impl(&project_id, &payload.title, payload.section.as_deref(), payload.parent_id.as_deref()) {
|
match create_task_impl(
|
||||||
|
&project_id,
|
||||||
|
&payload.title,
|
||||||
|
payload.section.as_deref(),
|
||||||
|
payload.parent_id.as_deref(),
|
||||||
|
) {
|
||||||
Ok(task) => (StatusCode::CREATED, Json(task)).into_response(),
|
Ok(task) => (StatusCode::CREATED, Json(task)).into_response(),
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@@ -110,9 +109,7 @@ pub async fn create_task_handler(
|
|||||||
pub async fn get_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
pub async fn get_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
||||||
match get_task_impl(&project_id, &task_id) {
|
match get_task_impl(&project_id, &task_id) {
|
||||||
Ok(task) => Json(task).into_response(),
|
Ok(task) => Json(task).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to get task: {}", err),
|
format!("Failed to get task: {}", err),
|
||||||
@@ -129,9 +126,7 @@ pub async fn update_task_content_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match update_task_content_impl(&project_id, &task_id, &body) {
|
match update_task_content_impl(&project_id, &task_id, &body) {
|
||||||
Ok(task) => Json(task).into_response(),
|
Ok(task) => Json(task).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to update task: {}", err),
|
format!("Failed to update task: {}", err),
|
||||||
@@ -144,9 +139,7 @@ pub async fn update_task_content_handler(
|
|||||||
pub async fn toggle_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
pub async fn toggle_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
||||||
match toggle_task_impl(&project_id, &task_id) {
|
match toggle_task_impl(&project_id, &task_id) {
|
||||||
Ok(task) => Json(task).into_response(),
|
Ok(task) => Json(task).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to toggle task: {}", err),
|
format!("Failed to toggle task: {}", err),
|
||||||
@@ -163,9 +156,7 @@ pub async fn update_task_meta_handler(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match update_task_meta_impl(&project_id, &task_id, payload) {
|
match update_task_meta_impl(&project_id, &task_id, payload) {
|
||||||
Ok(task) => Json(task).into_response(),
|
Ok(task) => Json(task).into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to update task metadata: {}", err),
|
format!("Failed to update task metadata: {}", err),
|
||||||
@@ -178,9 +169,7 @@ pub async fn update_task_meta_handler(
|
|||||||
pub async fn delete_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
pub async fn delete_task_handler(project_id: String, task_id: String) -> impl IntoResponse {
|
||||||
match delete_task_impl(&project_id, &task_id) {
|
match delete_task_impl(&project_id, &task_id) {
|
||||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||||
Err(err) if err.contains("not found") => {
|
Err(err) if err.contains("not found") => (StatusCode::NOT_FOUND, err).into_response(),
|
||||||
(StatusCode::NOT_FOUND, err).into_response()
|
|
||||||
}
|
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("Failed to delete task: {}", err),
|
format!("Failed to delete task: {}", err),
|
||||||
@@ -483,11 +472,7 @@ fn toggle_task_impl(project_id: &str, task_id: &str) -> Result<Task, String> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update section based on completion status
|
// Update section based on completion status
|
||||||
let new_section = if new_completed {
|
let new_section = if new_completed { "Completed" } else { "Active" };
|
||||||
"Completed"
|
|
||||||
} else {
|
|
||||||
"Active"
|
|
||||||
};
|
|
||||||
fm.insert(
|
fm.insert(
|
||||||
serde_yaml::Value::from("section"),
|
serde_yaml::Value::from("section"),
|
||||||
serde_yaml::Value::from(new_section),
|
serde_yaml::Value::from(new_section),
|
||||||
@@ -554,14 +539,22 @@ fn toggle_task_impl(project_id: &str, task_id: &str) -> Result<Task, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return updated task
|
// Return updated task
|
||||||
let task = parse_task_file(&fs::read_to_string(&task_path).unwrap(), &task_path, project_id)
|
let task = parse_task_file(
|
||||||
.ok_or_else(|| "Failed to parse updated task".to_string())?;
|
&fs::read_to_string(&task_path).unwrap(),
|
||||||
|
&task_path,
|
||||||
|
project_id,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| "Failed to parse updated task".to_string())?;
|
||||||
|
|
||||||
Ok(task)
|
Ok(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_next_due_date(current_due: Option<&str>, recurrence: &str, interval: i64) -> Option<String> {
|
fn calculate_next_due_date(
|
||||||
use chrono::{NaiveDate, Duration, Utc, Months};
|
current_due: Option<&str>,
|
||||||
|
recurrence: &str,
|
||||||
|
interval: i64,
|
||||||
|
) -> Option<String> {
|
||||||
|
use chrono::{Duration, Months, NaiveDate, Utc};
|
||||||
|
|
||||||
let base_date = if let Some(due_str) = current_due {
|
let base_date = if let Some(due_str) = current_due {
|
||||||
NaiveDate::parse_from_str(due_str, "%Y-%m-%d").unwrap_or_else(|_| Utc::now().date_naive())
|
NaiveDate::parse_from_str(due_str, "%Y-%m-%d").unwrap_or_else(|_| Utc::now().date_naive())
|
||||||
@@ -600,28 +593,73 @@ fn create_recurring_task_impl(
|
|||||||
let id = format!("{}-{}", project_id, filename);
|
let id = format!("{}-{}", project_id, filename);
|
||||||
|
|
||||||
let mut fm = serde_yaml::Mapping::new();
|
let mut fm = serde_yaml::Mapping::new();
|
||||||
fm.insert(serde_yaml::Value::from("id"), serde_yaml::Value::from(id.clone()));
|
fm.insert(
|
||||||
fm.insert(serde_yaml::Value::from("type"), serde_yaml::Value::from("task"));
|
serde_yaml::Value::from("id"),
|
||||||
fm.insert(serde_yaml::Value::from("title"), serde_yaml::Value::from(title));
|
serde_yaml::Value::from(id.clone()),
|
||||||
fm.insert(serde_yaml::Value::from("completed"), serde_yaml::Value::from(false));
|
);
|
||||||
fm.insert(serde_yaml::Value::from("section"), serde_yaml::Value::from("Active"));
|
fm.insert(
|
||||||
fm.insert(serde_yaml::Value::from("priority"), serde_yaml::Value::from("normal"));
|
serde_yaml::Value::from("type"),
|
||||||
fm.insert(serde_yaml::Value::from("is_active"), serde_yaml::Value::from(true));
|
serde_yaml::Value::from("task"),
|
||||||
fm.insert(serde_yaml::Value::from("project_id"), serde_yaml::Value::from(project_id));
|
);
|
||||||
fm.insert(serde_yaml::Value::from("recurrence"), serde_yaml::Value::from(recurrence));
|
fm.insert(
|
||||||
fm.insert(serde_yaml::Value::from("recurrence_interval"), serde_yaml::Value::from(interval as u64));
|
serde_yaml::Value::from("title"),
|
||||||
|
serde_yaml::Value::from(title),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("completed"),
|
||||||
|
serde_yaml::Value::from(false),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("section"),
|
||||||
|
serde_yaml::Value::from("Active"),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("priority"),
|
||||||
|
serde_yaml::Value::from("normal"),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("is_active"),
|
||||||
|
serde_yaml::Value::from(true),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("project_id"),
|
||||||
|
serde_yaml::Value::from(project_id),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("recurrence"),
|
||||||
|
serde_yaml::Value::from(recurrence),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("recurrence_interval"),
|
||||||
|
serde_yaml::Value::from(interval as u64),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(due) = due_date {
|
if let Some(due) = due_date {
|
||||||
fm.insert(serde_yaml::Value::from("due_date"), serde_yaml::Value::from(due));
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("due_date"),
|
||||||
|
serde_yaml::Value::from(due),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
let yaml_tags: Vec<serde_yaml::Value> = tags.iter().map(|t| serde_yaml::Value::from(t.as_str())).collect();
|
let yaml_tags: Vec<serde_yaml::Value> = tags
|
||||||
fm.insert(serde_yaml::Value::from("tags"), serde_yaml::Value::Sequence(yaml_tags));
|
.iter()
|
||||||
|
.map(|t| serde_yaml::Value::from(t.as_str()))
|
||||||
|
.collect();
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("tags"),
|
||||||
|
serde_yaml::Value::Sequence(yaml_tags),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fm.insert(serde_yaml::Value::from("created"), serde_yaml::Value::from(now_str.clone()));
|
fm.insert(
|
||||||
fm.insert(serde_yaml::Value::from("updated"), serde_yaml::Value::from(now_str.clone()));
|
serde_yaml::Value::from("created"),
|
||||||
|
serde_yaml::Value::from(now_str.clone()),
|
||||||
|
);
|
||||||
|
fm.insert(
|
||||||
|
serde_yaml::Value::from("updated"),
|
||||||
|
serde_yaml::Value::from(now_str.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
let body = format!("# {}\n\n", title);
|
let body = format!("# {}\n\n", title);
|
||||||
let content = frontmatter::serialize_frontmatter(&fm, &body)?;
|
let content = frontmatter::serialize_frontmatter(&fm, &body)?;
|
||||||
@@ -728,8 +766,12 @@ fn update_task_meta_impl(
|
|||||||
filesystem::atomic_write(&task_path, new_content.as_bytes())?;
|
filesystem::atomic_write(&task_path, new_content.as_bytes())?;
|
||||||
|
|
||||||
// Return updated task
|
// Return updated task
|
||||||
let task = parse_task_file(&fs::read_to_string(&task_path).unwrap(), &task_path, project_id)
|
let task = parse_task_file(
|
||||||
.ok_or_else(|| "Failed to parse updated task".to_string())?;
|
&fs::read_to_string(&task_path).unwrap(),
|
||||||
|
&task_path,
|
||||||
|
project_id,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| "Failed to parse updated task".to_string())?;
|
||||||
|
|
||||||
Ok(task)
|
Ok(task)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ fn is_note_file(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// data/projects/*/index.md
|
// data/projects/*/index.md
|
||||||
if path_str.contains("projects") && path.file_name().and_then(|s| s.to_str()) == Some("index.md") {
|
if path_str.contains("projects")
|
||||||
|
&& path.file_name().and_then(|s| s.to_str()) == Some("index.md")
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +125,10 @@ pub fn normalize_path(path: &Path) -> String {
|
|||||||
} else {
|
} else {
|
||||||
&path_str
|
&path_str
|
||||||
};
|
};
|
||||||
stripped.replace('\\', "/").trim_start_matches('/').to_string()
|
stripped
|
||||||
|
.replace('\\', "/")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a full note by deterministic ID.
|
/// Read a full note by deterministic ID.
|
||||||
@@ -326,9 +331,7 @@ pub fn atomic_write(path: &Path, contents: &[u8]) -> Result<(), String> {
|
|||||||
let parent = path.parent().ok_or("Invalid path")?;
|
let parent = path.parent().ok_or("Invalid path")?;
|
||||||
let temp_name = format!(
|
let temp_name = format!(
|
||||||
".{}.tmp",
|
".{}.tmp",
|
||||||
path.file_name()
|
path.file_name().and_then(|s| s.to_str()).unwrap_or("file")
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("file")
|
|
||||||
);
|
);
|
||||||
let temp_path = parent.join(temp_name);
|
let temp_path = parent.join(temp_name);
|
||||||
|
|
||||||
|
|||||||
@@ -165,8 +165,14 @@ mod tests {
|
|||||||
let (fm, body, has_fm) = parse_frontmatter(content);
|
let (fm, body, has_fm) = parse_frontmatter(content);
|
||||||
|
|
||||||
assert!(has_fm);
|
assert!(has_fm);
|
||||||
assert_eq!(fm.get(&Value::from("id")).unwrap().as_str().unwrap(), "test");
|
assert_eq!(
|
||||||
assert_eq!(fm.get(&Value::from("title")).unwrap().as_str().unwrap(), "Test Note");
|
fm.get(&Value::from("id")).unwrap().as_str().unwrap(),
|
||||||
|
"test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fm.get(&Value::from("title")).unwrap().as_str().unwrap(),
|
||||||
|
"Test Note"
|
||||||
|
);
|
||||||
assert!(body.contains("Body content"));
|
assert!(body.contains("Body content"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,7 @@ pub fn get_status() -> Result<RepoStatus, String> {
|
|||||||
Some(CommitInfo {
|
Some(CommitInfo {
|
||||||
id: commit.id().to_string()[..8].to_string(),
|
id: commit.id().to_string()[..8].to_string(),
|
||||||
message: commit.message()?.trim().to_string(),
|
message: commit.message()?.trim().to_string(),
|
||||||
timestamp: chrono::DateTime::from_timestamp(commit.time().seconds(), 0)?
|
timestamp: chrono::DateTime::from_timestamp(commit.time().seconds(), 0)?.to_rfc3339(),
|
||||||
.to_rfc3339(),
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,7 +310,9 @@ pub fn push_to_remote() -> Result<(), String> {
|
|||||||
.map_err(|e| format!("Remote 'origin' not found: {}", e))?;
|
.map_err(|e| format!("Remote 'origin' not found: {}", e))?;
|
||||||
|
|
||||||
// Check if remote URL is configured
|
// Check if remote URL is configured
|
||||||
let remote_url = remote.url().ok_or_else(|| "No remote URL configured".to_string())?;
|
let remote_url = remote
|
||||||
|
.url()
|
||||||
|
.ok_or_else(|| "No remote URL configured".to_string())?;
|
||||||
if remote_url.is_empty() {
|
if remote_url.is_empty() {
|
||||||
return Err("No remote URL configured".to_string());
|
return Err("No remote URL configured".to_string());
|
||||||
}
|
}
|
||||||
@@ -402,9 +403,7 @@ pub fn get_log(limit: Option<usize>) -> Result<Vec<CommitDetail>, String> {
|
|||||||
let commit_tree = commit.tree().ok();
|
let commit_tree = commit.tree().ok();
|
||||||
|
|
||||||
if let (Some(pt), Some(ct)) = (parent_tree, commit_tree) {
|
if let (Some(pt), Some(ct)) = (parent_tree, commit_tree) {
|
||||||
let diff = repo
|
let diff = repo.diff_tree_to_tree(Some(&pt), Some(&ct), None).ok();
|
||||||
.diff_tree_to_tree(Some(&pt), Some(&ct), None)
|
|
||||||
.ok();
|
|
||||||
diff.map(|d| d.deltas().count()).unwrap_or(0)
|
diff.map(|d| d.deltas().count()).unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
@@ -418,10 +417,9 @@ pub fn get_log(limit: Option<usize>) -> Result<Vec<CommitDetail>, String> {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let timestamp =
|
let timestamp = chrono::DateTime::from_timestamp(commit.time().seconds(), 0)
|
||||||
chrono::DateTime::from_timestamp(commit.time().seconds(), 0)
|
.map(|dt| dt.to_rfc3339())
|
||||||
.map(|dt| dt.to_rfc3339())
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
.unwrap_or_else(|| "Unknown".to_string());
|
|
||||||
|
|
||||||
commits.push(CommitDetail {
|
commits.push(CommitDetail {
|
||||||
id: oid.to_string(),
|
id: oid.to_string(),
|
||||||
@@ -449,10 +447,7 @@ pub fn get_working_diff() -> Result<DiffInfo, String> {
|
|||||||
let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;
|
let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;
|
||||||
|
|
||||||
// Get HEAD tree (or empty tree if no commits)
|
// Get HEAD tree (or empty tree if no commits)
|
||||||
let head_tree = repo
|
let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
|
||||||
.head()
|
|
||||||
.ok()
|
|
||||||
.and_then(|h| h.peel_to_tree().ok());
|
|
||||||
|
|
||||||
// Diff against working directory
|
// Diff against working directory
|
||||||
let diff = repo
|
let diff = repo
|
||||||
@@ -609,10 +604,7 @@ pub fn get_remote_info() -> Result<Option<RemoteInfo>, String> {
|
|||||||
let (ahead, behind) = if let Some(ref up) = upstream {
|
let (ahead, behind) = if let Some(ref up) = upstream {
|
||||||
// Calculate ahead/behind
|
// Calculate ahead/behind
|
||||||
let local_oid = head.target().unwrap_or_else(git2::Oid::zero);
|
let local_oid = head.target().unwrap_or_else(git2::Oid::zero);
|
||||||
let upstream_oid = up
|
let upstream_oid = up.get().target().unwrap_or_else(git2::Oid::zero);
|
||||||
.get()
|
|
||||||
.target()
|
|
||||||
.unwrap_or_else(git2::Oid::zero);
|
|
||||||
|
|
||||||
repo.graph_ahead_behind(local_oid, upstream_oid)
|
repo.graph_ahead_behind(local_oid, upstream_oid)
|
||||||
.unwrap_or((0, 0))
|
.unwrap_or((0, 0))
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ pub fn search_notes(query: &str) -> Result<Vec<SearchResult>, String> {
|
|||||||
match search_with_ripgrep(query) {
|
match search_with_ripgrep(query) {
|
||||||
Ok(results) => return Ok(results),
|
Ok(results) => return Ok(results),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::debug!("ripgrep not available, falling back to manual search: {}", e);
|
tracing::debug!(
|
||||||
|
"ripgrep not available, falling back to manual search: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +49,12 @@ fn search_with_ripgrep(query: &str) -> Result<Vec<SearchResult>, String> {
|
|||||||
let data_dir_str = config::data_dir().to_string_lossy();
|
let data_dir_str = config::data_dir().to_string_lossy();
|
||||||
let output = Command::new("rg")
|
let output = Command::new("rg")
|
||||||
.args([
|
.args([
|
||||||
"--json", // JSON output for parsing
|
"--json", // JSON output for parsing
|
||||||
"--ignore-case", // Case insensitive
|
"--ignore-case", // Case insensitive
|
||||||
"--type", "md", // Only markdown files
|
"--type",
|
||||||
"--max-count", "5", // Max 5 matches per file
|
"md", // Only markdown files
|
||||||
|
"--max-count",
|
||||||
|
"5", // Max 5 matches per file
|
||||||
query,
|
query,
|
||||||
&data_dir_str,
|
&data_dir_str,
|
||||||
])
|
])
|
||||||
@@ -88,13 +93,13 @@ fn parse_ripgrep_output(output: &[u8]) -> Result<Vec<SearchResult>, String> {
|
|||||||
let normalized_path = normalize_path(path_str);
|
let normalized_path = normalize_path(path_str);
|
||||||
let title = extract_title_from_path(&normalized_path);
|
let title = extract_title_from_path(&normalized_path);
|
||||||
|
|
||||||
let result = results_map.entry(normalized_path.clone()).or_insert_with(|| {
|
let result = results_map
|
||||||
SearchResult {
|
.entry(normalized_path.clone())
|
||||||
|
.or_insert_with(|| SearchResult {
|
||||||
path: normalized_path,
|
path: normalized_path,
|
||||||
title,
|
title,
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
result.matches.push(SearchMatch {
|
result.matches.push(SearchMatch {
|
||||||
line_number,
|
line_number,
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ pub async fn start_watcher(ws_state: Arc<WsState>) -> Result<(), String> {
|
|||||||
// Watch the data directory
|
// Watch the data directory
|
||||||
let data_path = config::data_dir();
|
let data_path = config::data_dir();
|
||||||
if !data_path.exists() {
|
if !data_path.exists() {
|
||||||
return Err(format!("Data directory does not exist: {}", data_path.display()));
|
return Err(format!(
|
||||||
|
"Data directory does not exist: {}",
|
||||||
|
data_path.display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to keep the debouncer alive, so we'll store it
|
// We need to keep the debouncer alive, so we'll store it
|
||||||
@@ -36,7 +39,8 @@ pub async fn start_watcher(ws_state: Arc<WsState>) -> Result<(), String> {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut d = debouncer.lock().await;
|
let mut d = debouncer.lock().await;
|
||||||
d.watcher().watch(data_path, RecursiveMode::Recursive)
|
d.watcher()
|
||||||
|
.watch(data_path, RecursiveMode::Recursive)
|
||||||
.map_err(|e| format!("Failed to watch directory: {}", e))?;
|
.map_err(|e| format!("Failed to watch directory: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,9 +62,9 @@ pub async fn start_watcher(ws_state: Arc<WsState>) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
/// Track recent saves to avoid notifying about our own changes
|
/// Track recent saves to avoid notifying about our own changes
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
@@ -154,7 +158,10 @@ fn normalize_path(path: &Path) -> String {
|
|||||||
// Find "data" in the path and strip everything before and including it
|
// Find "data" in the path and strip everything before and including it
|
||||||
if let Some(idx) = path_str.find("data") {
|
if let Some(idx) = path_str.find("data") {
|
||||||
let stripped = &path_str[idx + 5..]; // Skip "data" + separator
|
let stripped = &path_str[idx + 5..]; // Skip "data" + separator
|
||||||
return stripped.replace('\\', "/").trim_start_matches('/').to_string();
|
return stripped
|
||||||
|
.replace('\\', "/")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
path_str.replace('\\', "/")
|
path_str.replace('\\', "/")
|
||||||
|
|||||||
@@ -147,11 +147,7 @@ async fn handle_socket(socket: WebSocket, state: Arc<WsState>) {
|
|||||||
if let Ok(client_msg) = serde_json::from_str::<ClientMessage>(&text) {
|
if let Ok(client_msg) = serde_json::from_str::<ClientMessage>(&text) {
|
||||||
handle_client_message(&state_clone, &client_id_clone, client_msg).await;
|
handle_client_message(&state_clone, &client_id_clone, client_msg).await;
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!(
|
tracing::debug!("Unknown message from {}: {}", client_id_clone, text);
|
||||||
"Unknown message from {}: {}",
|
|
||||||
client_id_clone,
|
|
||||||
text
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Close(_) => break,
|
Message::Close(_) => break,
|
||||||
@@ -195,7 +191,11 @@ async fn handle_client_message(state: &Arc<WsState>, client_id: &str, msg: Clien
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match state.lock_manager.acquire(&path, client_id, lock_type).await {
|
match state
|
||||||
|
.lock_manager
|
||||||
|
.acquire(&path, client_id, lock_type)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(lock_info) => {
|
Ok(lock_info) => {
|
||||||
let lock_type_str = match lock_info.lock_type {
|
let lock_type_str = match lock_info.lock_type {
|
||||||
LockType::Editor => "editor",
|
LockType::Editor => "editor",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vite build",
|
||||||
|
"typecheck": "vue-tsc -b",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { ref, computed } from 'vue'
|
|||||||
import { MilkdownProvider } from '@milkdown/vue'
|
import { MilkdownProvider } from '@milkdown/vue'
|
||||||
import { Crepe } from '@milkdown/crepe'
|
import { Crepe } from '@milkdown/crepe'
|
||||||
import { useThemeStore } from '../stores'
|
import { useThemeStore } from '../stores'
|
||||||
import { assetsApi } from '../api/client'
|
|
||||||
import MilkdownEditorCore from './MilkdownEditorCore.vue'
|
import MilkdownEditorCore from './MilkdownEditorCore.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function tryApplyPendingContent() {
|
|||||||
if (!crepe) return false
|
if (!crepe) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const editor = crepe.editor
|
const editor = (crepe as any).editor
|
||||||
if (!editor || typeof editor.action !== 'function') return false
|
if (!editor || typeof editor.action !== 'function') return false
|
||||||
|
|
||||||
console.log('[MilkdownEditorCore] Applying pending content, length:', pendingContent.value.length)
|
console.log('[MilkdownEditorCore] Applying pending content, length:', pendingContent.value.length)
|
||||||
@@ -111,10 +111,10 @@ const { get, loading } = useEditor((root) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add listener plugin for content changes
|
// Add listener plugin for content changes
|
||||||
crepe.editor
|
;(crepe as any).editor
|
||||||
.config((ctx) => {
|
.config((ctx: any) => {
|
||||||
const listenerHandler = ctx.get(listenerCtx)
|
const listenerHandler = ctx.get(listenerCtx)
|
||||||
listenerHandler.markdownUpdated((ctx, markdown, prevMarkdown) => {
|
listenerHandler.markdownUpdated((_ctx: any, markdown: string, prevMarkdown: string) => {
|
||||||
// CRITICAL: Only emit content changes if:
|
// CRITICAL: Only emit content changes if:
|
||||||
// 1. Content actually changed
|
// 1. Content actually changed
|
||||||
// 2. We're not in the middle of an external update
|
// 2. We're not in the middle of an external update
|
||||||
@@ -143,7 +143,7 @@ watch(loading, (isLoading) => {
|
|||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
const crepe = get()
|
const crepe = get()
|
||||||
if (crepe) {
|
if (crepe) {
|
||||||
emit('editor-ready', crepe)
|
emit('editor-ready', crepe as Crepe)
|
||||||
|
|
||||||
// Try to apply pending content - might need retries if editor not fully ready
|
// Try to apply pending content - might need retries if editor not fully ready
|
||||||
if (pendingContent.value !== null) {
|
if (pendingContent.value !== null) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useTasksStore, useProjectsStore, useWorkspaceStore } from '../stores'
|
import { useTasksStore, useProjectsStore, useWorkspaceStore } from '../stores'
|
||||||
import { dailyApi } from '../api/client'
|
import { dailyApi } from '../api/client'
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user