saving might work idk
This commit is contained in:
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": ["Homelab", "tauri"]
|
"cSpell.words": ["Homelab", "Insertable", "tauri"]
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-5
@@ -6,6 +6,13 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import TextEditor from "@/components/text-editor";
|
import TextEditor from "@/components/text-editor";
|
||||||
import { HomelabApp } from "@/types";
|
import { HomelabApp } from "@/types";
|
||||||
|
|
||||||
|
// Create a type for the Blog returned by your Rust backend
|
||||||
|
interface Blog {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
function WritePageContent() {
|
function WritePageContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const appQuery = searchParams.get("app");
|
const appQuery = searchParams.get("app");
|
||||||
@@ -13,6 +20,9 @@ function WritePageContent() {
|
|||||||
const [initialAppIds, setInitialAppIds] = useState<string[]>([]);
|
const [initialAppIds, setInitialAppIds] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Track the document ID to allow updating instead of duplicating
|
||||||
|
const [documentId, setDocumentId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkQueryParam = async () => {
|
const checkQueryParam = async () => {
|
||||||
if (!appQuery) {
|
if (!appQuery) {
|
||||||
@@ -23,7 +33,6 @@ function WritePageContent() {
|
|||||||
try {
|
try {
|
||||||
const apps = (await invoke("get_app")) as HomelabApp[];
|
const apps = (await invoke("get_app")) as HomelabApp[];
|
||||||
|
|
||||||
// Match checking both UUID structure and sanitized string names
|
|
||||||
const matchedApp = apps.find(
|
const matchedApp = apps.find(
|
||||||
(app) =>
|
(app) =>
|
||||||
app.id === appQuery ||
|
app.id === appQuery ||
|
||||||
@@ -46,9 +55,27 @@ function WritePageContent() {
|
|||||||
checkQueryParam();
|
checkQueryParam();
|
||||||
}, [appQuery]);
|
}, [appQuery]);
|
||||||
|
|
||||||
const onSave = (content: string, linkedAppIds: string[]) => {
|
// Updated onSave signature matching what TextEditor emits
|
||||||
console.log("Document Rich Text: ", content);
|
const onSave = async (
|
||||||
console.log("Document Bound App IDs: ", linkedAppIds);
|
content: string,
|
||||||
|
linkedAppIds: string[],
|
||||||
|
title: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const savedBlog = await invoke<Blog>("save_blog", {
|
||||||
|
id: documentId,
|
||||||
|
title: title || "Untitled Document",
|
||||||
|
content: content,
|
||||||
|
linkedAppIds: linkedAppIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update state with the backend-generated/returned ID
|
||||||
|
if (savedBlog && savedBlog.id) {
|
||||||
|
setDocumentId(savedBlog.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save document:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -66,7 +93,6 @@ function WritePageContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Main Page export wrapped in a Suspense boundary
|
|
||||||
export default function WritePage() {
|
export default function WritePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-2">
|
<div className="flex flex-col p-2">
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { Markdown } from "@tiptap/markdown";
|
|||||||
import { HomelabApp } from "@/types";
|
import { HomelabApp } from "@/types";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Menubar from "./menu-bar"; // Added menubar import back
|
import Menubar from "./menu-bar";
|
||||||
|
|
||||||
interface TextEditorProps {
|
interface TextEditorProps {
|
||||||
content: string;
|
content: string;
|
||||||
onSave: (content: string, linkedAppIds: string[]) => void;
|
initialTitle?: string;
|
||||||
|
onSave: (content: string, linkedAppIds: string[], title: string) => void;
|
||||||
initialLinkedAppIds?: string[];
|
initialLinkedAppIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,14 +23,21 @@ const getApps = async (): Promise<HomelabApp[]> => {
|
|||||||
|
|
||||||
export default function TextEditor({
|
export default function TextEditor({
|
||||||
content,
|
content,
|
||||||
|
initialTitle = "",
|
||||||
onSave,
|
onSave,
|
||||||
initialLinkedAppIds = [],
|
initialLinkedAppIds = [],
|
||||||
}: TextEditorProps) {
|
}: TextEditorProps) {
|
||||||
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
|
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
|
||||||
const [linkedAppIds, setLinkedAppIds] =
|
const [linkedAppIds, setLinkedAppIds] =
|
||||||
useState<string[]>(initialLinkedAppIds);
|
useState<string[]>(initialLinkedAppIds);
|
||||||
|
// Title control state
|
||||||
|
const [title, setTitle] = useState<string>(initialTitle);
|
||||||
|
// Save control
|
||||||
|
// Will be saved on completed handleSave, error if func fails, and saving if the func is in progress
|
||||||
|
const [isSaved, setIsSaved] = useState<"saved" | "error" | "saving">(
|
||||||
|
"saving",
|
||||||
|
);
|
||||||
|
|
||||||
// 🔄 FIX: Watch for updates to initialLinkedAppIds once fetched from the URL
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialLinkedAppIds.length > 0) {
|
if (initialLinkedAppIds.length > 0) {
|
||||||
setLinkedAppIds(initialLinkedAppIds);
|
setLinkedAppIds(initialLinkedAppIds);
|
||||||
@@ -44,8 +52,6 @@ export default function TextEditor({
|
|||||||
fetchApps();
|
fetchApps();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ... rest of your TextEditor component stays the same
|
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@@ -57,7 +63,7 @@ export default function TextEditor({
|
|||||||
Markdown,
|
Markdown,
|
||||||
],
|
],
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
content: content || "<p>Start typing your documentation notes here...</p>",
|
content,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: "prose max-w-none focus:outline-none min-h-[300px]",
|
class: "prose max-w-none focus:outline-none min-h-[300px]",
|
||||||
@@ -66,32 +72,64 @@ export default function TextEditor({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggleAppLink = (appId: string) => {
|
const toggleAppLink = (appId: string) => {
|
||||||
setLinkedAppIds(
|
setLinkedAppIds((prev) =>
|
||||||
(prev) =>
|
prev.includes(appId)
|
||||||
prev.includes(appId)
|
? prev.filter((id) => id !== appId)
|
||||||
? prev.filter((id) => id !== appId) // Unlink if already added
|
: [...prev, appId],
|
||||||
: [...prev, appId], // Link if not added
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!editor) return;
|
setIsSaved("saving");
|
||||||
const htmlOutput = editor.getHTML();
|
try {
|
||||||
// Pass both the layout structure and the entire linked app set
|
if (!editor) return;
|
||||||
onSave(htmlOutput, linkedAppIds);
|
const htmlOutput = editor.getHTML();
|
||||||
|
// Dispatch text body layout, assets collection array, and document title string
|
||||||
|
onSave(htmlOutput, linkedAppIds, title);
|
||||||
|
setIsSaved("saved");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving document:", error);
|
||||||
|
setIsSaved("error");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// create debounced save, every 15 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
handleSave();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [editor, linkedAppIds, title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
{/* Top action bar */}
|
{/* Top action bar updated with live Title Input */}
|
||||||
<div className="flex justify-between items-center pb-2 border-b border-zinc-100">
|
<div className="flex justify-between items-center pb-2 border-b border-zinc-100 gap-4">
|
||||||
<span className="text-sm text-zinc-400 font-medium">Draft Session</span>
|
<input
|
||||||
<button
|
type="text"
|
||||||
onClick={handleSave}
|
value={title}
|
||||||
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors"
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
>
|
placeholder="Untitled Document..."
|
||||||
Save Changes
|
className="bg-transparent text-zinc-800 font-semibold text-base placeholder-zinc-300 focus:outline-none flex-1 py-1"
|
||||||
</button>
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isSaved === "saving" && (
|
||||||
|
<span className="text-xs text-zinc-400">Saving...</span>
|
||||||
|
)}
|
||||||
|
{isSaved === "saved" && (
|
||||||
|
<span className="text-xs text-emerald-600">Saved</span>
|
||||||
|
)}
|
||||||
|
{isSaved === "error" && (
|
||||||
|
<span className="text-xs text-red-600">Error saving</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors shrink-0"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor Controls */}
|
{/* Editor Controls */}
|
||||||
@@ -129,9 +167,8 @@ export default function TextEditor({
|
|||||||
: "bg-white border-zinc-200 hover:border-zinc-300"
|
: "bg-white border-zinc-200 hover:border-zinc-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Subtle active border indicator */}
|
|
||||||
{isLinked && (
|
{isLinked && (
|
||||||
<div className="absolute top-0 right-0 h-0 w-0 border-t-[16px] border-t-emerald-500 border-l-[16px] border-l-transparent" />
|
<div className="absolute top-0 right-0 h-0 w-0 border-t-16 border-t-emerald-500 border-l-16 border-l-transparent" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -53,3 +53,87 @@ pub fn delete_app(conn: &mut SqliteConnection, app_id: &str) -> usize {
|
|||||||
.execute(conn)
|
.execute(conn)
|
||||||
.expect("Error deleting app")
|
.expect("Error deleting app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_blog_db(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
linked_app_ids: Option<&[String]>,
|
||||||
|
) -> models::Blog {
|
||||||
|
use crate::db::schema::blog;
|
||||||
|
|
||||||
|
// json seralize the referenced app ids, store as string
|
||||||
|
if let Some(ids) = linked_app_ids {
|
||||||
|
for id in ids {
|
||||||
|
// Check if the app exists in the database
|
||||||
|
let app_exists = crate::db::schema::app::dsl::app
|
||||||
|
.filter(crate::db::schema::app::dsl::id.eq(id))
|
||||||
|
.first::<models::App>(conn)
|
||||||
|
.optional()
|
||||||
|
.expect("Error checking if app exists");
|
||||||
|
|
||||||
|
if app_exists.is_none() {
|
||||||
|
panic!("App with id {} does not exist", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are linked app ids, serialize them to json, else set to None
|
||||||
|
let reference: Option<String> = if let Some(ids) = linked_app_ids {
|
||||||
|
Some(serde_json::to_string(&ids).expect("Failed to convert to JSON"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_blog = models::Blog {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: title.to_string(),
|
||||||
|
content: content.to_string(),
|
||||||
|
reference,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(blog::table)
|
||||||
|
.values(&new_blog)
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Error saving new blog");
|
||||||
|
|
||||||
|
new_blog
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will be used to update an existing blog post OR create a new one if it doesn't exist
|
||||||
|
pub fn save_blog(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
blog_id: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
linked_app_ids: Option<&[String]>,
|
||||||
|
) -> models::Blog {
|
||||||
|
use crate::db::schema::blog;
|
||||||
|
|
||||||
|
// json seralize the referenced app ids, store as string
|
||||||
|
let reference: Option<String> = if let Some(ids) = linked_app_ids {
|
||||||
|
Some(serde_json::to_string(&ids).expect("Failed to convert to JSON"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(id) = blog_id {
|
||||||
|
// Update existing blog
|
||||||
|
diesel::update(blog::table.filter(blog::id.eq(id)))
|
||||||
|
.set((
|
||||||
|
blog::title.eq(title),
|
||||||
|
blog::content.eq(content),
|
||||||
|
blog::reference.eq(reference),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.expect("Error updating blog");
|
||||||
|
|
||||||
|
blog::table
|
||||||
|
.filter(blog::id.eq(id))
|
||||||
|
.first::<models::Blog>(conn)
|
||||||
|
.expect("Error loading updated blog")
|
||||||
|
} else {
|
||||||
|
// Create new blog
|
||||||
|
create_blog_db(conn, title, content, linked_app_ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub struct App {
|
|||||||
pub ssh_address: Option<String>, // default to normal address
|
pub ssh_address: Option<String>, // default to normal address
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)]
|
#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = crate::db::schema::blog)]
|
#[diesel(table_name = crate::db::schema::blog)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct Blog {
|
pub struct Blog {
|
||||||
@@ -22,7 +22,7 @@ pub struct Blog {
|
|||||||
pub reference: Option<String>, // uuid
|
pub reference: Option<String>, // uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)]
|
#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)]
|
||||||
#[diesel(table_name = crate::db::schema::identities)]
|
#[diesel(table_name = crate::db::schema::identities)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
pub struct Identity {
|
pub struct Identity {
|
||||||
|
|||||||
+17
-7
@@ -27,14 +27,12 @@ async fn ping_target(address: &str) -> bool {
|
|||||||
|
|
||||||
let ip_target: IpAddr = socket_addr.ip();
|
let ip_target: IpAddr = socket_addr.ip();
|
||||||
|
|
||||||
// 2. Configure unprivileged ICMP requestor
|
|
||||||
let timeout = Some(Duration::from_secs(2));
|
let timeout = Some(Duration::from_secs(2));
|
||||||
let pinger = match IcmpEchoRequestor::new(ip_target, None, None, timeout) {
|
let pinger = match IcmpEchoRequestor::new(ip_target, None, None, timeout) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_) => return false,
|
Err(_) => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Send ICMP Echo request asynchronously
|
|
||||||
match pinger.send().await {
|
match pinger.send().await {
|
||||||
Ok(reply) => {
|
Ok(reply) => {
|
||||||
// Check if the status returned is a valid success response
|
// Check if the status returned is a valid success response
|
||||||
@@ -46,7 +44,7 @@ async fn ping_target(address: &str) -> bool {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Option<&str>) -> App {
|
fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Option<&str>) -> App {
|
||||||
let mut conn = &mut establish_connection();
|
let mut conn = establish_connection();
|
||||||
let app = create_app(&mut conn, name, address, description, ssh_address);
|
let app = create_app(&mut conn, name, address, description, ssh_address);
|
||||||
|
|
||||||
println!("{:#?}", app);
|
println!("{:#?}", app);
|
||||||
@@ -55,7 +53,7 @@ fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Op
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_app() -> Vec<App> {
|
fn get_app() -> Vec<App> {
|
||||||
let mut conn = &mut establish_connection();
|
let mut conn = establish_connection();
|
||||||
let apps = get_apps(&mut conn);
|
let apps = get_apps(&mut conn);
|
||||||
|
|
||||||
println!("{:#?}", apps);
|
println!("{:#?}", apps);
|
||||||
@@ -64,8 +62,8 @@ fn get_app() -> Vec<App> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn delete_apps(id: &str) {
|
fn delete_apps(id: &str) {
|
||||||
let conn = &mut establish_connection();
|
let mut conn = establish_connection();
|
||||||
delete_app(conn, id);
|
delete_app(&mut conn, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -76,6 +74,17 @@ async fn ping(address: &str) -> Result<bool, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn save_blog(
|
||||||
|
id: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
linked_app_ids: Option<Vec<String>>,
|
||||||
|
) -> db::models::Blog {
|
||||||
|
let mut conn = establish_connection();
|
||||||
|
db::save_blog(&mut conn, id, title, content, linked_app_ids.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -84,7 +93,8 @@ pub fn run() {
|
|||||||
new_app,
|
new_app,
|
||||||
get_app,
|
get_app,
|
||||||
delete_apps,
|
delete_apps,
|
||||||
ping
|
ping,
|
||||||
|
save_blog
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
Reference in New Issue
Block a user