save docs
This commit is contained in:
parent
bcd2e9086b
commit
ccebb53879
12 changed files with 2062 additions and 2414 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2778,6 +2778,7 @@ dependencies = [
|
|||
"axum",
|
||||
"chrono",
|
||||
"cozo",
|
||||
"csv",
|
||||
"dirs",
|
||||
"futures",
|
||||
"miette",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@mui/material": "^5.15.18",
|
||||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tauri-apps/api": "^1",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"classnames": "^2.5.1",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
|
@ -31,6 +32,7 @@
|
|||
"react-time-ago": "^7.3.3",
|
||||
"remark": "^15.0.1",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"use-debounce": "^10.0.1",
|
||||
"vfile": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
.container {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mdContent {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
|
|
@ -1,139 +1,59 @@
|
|||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
||||
import styles from "./JournalPage.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import Markdown from "react-markdown";
|
||||
import { toMdast } from "hast-util-to-mdast";
|
||||
import { Node as MdastNode } from "mdast";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import { toMarkdown } from "mdast-util-to-markdown";
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { VFile } from "vfile";
|
||||
import { common } from "@mui/material/colors";
|
||||
import classNames from "classnames";
|
||||
import { usePrevious, useDebounce } from "@uidotdev/usehooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import styles from "./JournalPage.module.scss";
|
||||
|
||||
interface MDContextValue {
|
||||
isEditing: boolean;
|
||||
export interface JournalPageProps {
|
||||
id: string;
|
||||
data: {
|
||||
content: string;
|
||||
};
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
const MDContext = createContext<MDContextValue>(null!);
|
||||
|
||||
const emptyContent = { type: "root", children: [] };
|
||||
|
||||
export default function JournalPage({ id, data }) {
|
||||
const [content, setContent] = useState(() => data.content);
|
||||
const [isEditing, setIsEditing] = useState(() => false);
|
||||
const [currentlyFocused, setCurrentlyFocused] = useState<string | undefined>(
|
||||
() => undefined,
|
||||
);
|
||||
export default function JournalPage({ id, data }: JournalPageProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [value, setValue] = useState(() => data.content);
|
||||
const valueToSave = useDebounce(value, 1000);
|
||||
const previous = usePrevious(valueToSave);
|
||||
const changed = valueToSave !== previous;
|
||||
|
||||
useEffect(() => {
|
||||
if (content === null) {
|
||||
setContent(() => ({
|
||||
type: "root",
|
||||
children: [
|
||||
{ type: "paragraph", children: [{ type: "text", value: "" }] },
|
||||
],
|
||||
}));
|
||||
setCurrentlyFocused(".children[0]");
|
||||
setIsEditing(true);
|
||||
if (changed) {
|
||||
(async () => {
|
||||
console.log("Saving...");
|
||||
const resp = await fetch(`http://localhost:5195/node/${id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
extra_data: {
|
||||
"panorama/journal/page/content": valueToSave,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const data = await resp.text();
|
||||
console.log("result", data);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["fetchNode", id] });
|
||||
})();
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const contextValue = { content, setContent, isEditing, setIsEditing };
|
||||
|
||||
const jsxContent = convertToJsx(content, { currentlyFocused });
|
||||
}, [id, changed, valueToSave, queryClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-color-mode="light" className={styles.container}>
|
||||
<details>
|
||||
<summary>JSON</summary>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
|
||||
<div className={styles.mdContent} data-color-mode="light">
|
||||
<MDContext.Provider value={contextValue}>
|
||||
{jsxContent}
|
||||
</MDContext.Provider>
|
||||
|
||||
<pre>{JSON.stringify(content, null, 2)}</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConvertToJsxOpts {
|
||||
currentlyFocused?: string | undefined;
|
||||
parent?: MdastNode | undefined;
|
||||
}
|
||||
|
||||
function convertToJsx(
|
||||
tree: MdastNode,
|
||||
opts?: ConvertToJsxOpts | undefined,
|
||||
): ReactNode {
|
||||
console.log("tree", tree);
|
||||
|
||||
if (tree === null) return;
|
||||
|
||||
const commonProps = {
|
||||
node: tree,
|
||||
parent: opts?.parent,
|
||||
};
|
||||
|
||||
switch (tree.type) {
|
||||
case "root":
|
||||
return tree.children.map((child) =>
|
||||
convertToJsx(child, { parent: tree }),
|
||||
);
|
||||
|
||||
case "paragraph":
|
||||
return <Paragraph {...commonProps} />;
|
||||
|
||||
default:
|
||||
throw new Error(`unhandled ${tree.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function Paragraph({ ...args }) {
|
||||
// const { isEditing } = useContext(MDContext);
|
||||
const [isEditing, setIsEditing] = useState(() => false);
|
||||
const [localValue, setLocalValue] = useState(null);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const save = useCallback(() => {
|
||||
console.log("saving!", localValue);
|
||||
});
|
||||
|
||||
const onPaste = useCallback((evt) => {
|
||||
console.log("pasted");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={classNames(styles.block, isEditing && styles.isEditing)}
|
||||
contentEditable={isEditing}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onPaste={onPaste}
|
||||
onBlur={save}
|
||||
>
|
||||
<br />
|
||||
</div>
|
||||
<MDEditor
|
||||
value={value}
|
||||
className={styles.mdEditor}
|
||||
onChange={(newValue) => newValue && setValue(newValue)}
|
||||
preview="preview"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { ReactNode } from "react";
|
||||
import { Nodes as MdastNodes } from "mdast";
|
||||
|
||||
export function convertToJsx(tree: MdastNodes): ReactNode {
|
||||
console.log("tree", tree);
|
||||
|
||||
switch (tree.type) {
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ anyhow = "1.0.86"
|
|||
axum = "0.7.5"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
|
||||
csv = "1.3.0"
|
||||
dirs = "5.0.1"
|
||||
futures = "0.3.30"
|
||||
miette = "5.5.0"
|
||||
|
|
|
@ -7,9 +7,12 @@ use std::{
|
|||
|
||||
use axum::extract::State;
|
||||
use cozo::ScriptMutability;
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use crate::{error::AppResult, AppState};
|
||||
|
||||
// This code is really bad but gives me a quick way to look at all of the data
|
||||
// in the data at once. Rip this out once there's any Real Security Mechanism.
|
||||
pub async fn export(State(state): State<AppState>) -> AppResult<()> {
|
||||
let result = state.db.run_script(
|
||||
"::relations",
|
||||
|
@ -52,8 +55,12 @@ pub async fn export(State(state): State<AppState>) -> AppResult<()> {
|
|||
let tx = state.db.multi_transaction(false);
|
||||
|
||||
for relation_name in relation_names.iter() {
|
||||
let relation_path = base_dir.join(format!("{relation_name}.ndjson"));
|
||||
let mut file = File::create(&relation_path).unwrap();
|
||||
let relation_path = base_dir.join(format!("{relation_name}.csv"));
|
||||
let mut writer = WriterBuilder::new()
|
||||
.has_headers(true)
|
||||
.from_path(relation_path)
|
||||
.unwrap();
|
||||
// let mut file = File::create(&relation_path).unwrap();
|
||||
|
||||
let columns = relation_columns
|
||||
.get(relation_name.as_str())
|
||||
|
@ -64,18 +71,19 @@ pub async fn export(State(state): State<AppState>) -> AppResult<()> {
|
|||
println!("Query: {query}");
|
||||
let result = tx.run_script(&query, Default::default())?;
|
||||
|
||||
writer.write_record(result.headers).unwrap();
|
||||
|
||||
for row in result.rows.into_iter() {
|
||||
let mut object = HashMap::new();
|
||||
|
||||
for (idx, col) in row.into_iter().enumerate() {
|
||||
let row_name = result.headers[idx].clone();
|
||||
object.insert(row_name, col);
|
||||
}
|
||||
|
||||
let serialized = serde_json::to_string(&object).unwrap();
|
||||
file.write(serialized.as_bytes());
|
||||
file.write(b"\n");
|
||||
// let serialized = serde_json::to_string(&object).unwrap();
|
||||
writer
|
||||
.write_record(
|
||||
row.iter().map(|col| serde_json::to_string(&col).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
// file.write(b"\n");
|
||||
}
|
||||
|
||||
writer.flush().unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -24,6 +24,7 @@ pub async fn get_todays_journal_id(
|
|||
|
||||
println!("Result: {:?}", result);
|
||||
|
||||
// TODO: Do this check on the server side
|
||||
if result.rows.len() == 0 {
|
||||
// Insert a new one
|
||||
let uuid = Uuid::now_v7();
|
||||
|
@ -36,7 +37,7 @@ pub async fn get_todays_journal_id(
|
|||
:put node { id, title, type }
|
||||
}
|
||||
{
|
||||
?[node_id, content] <- [[$node_id, {}]]
|
||||
?[node_id, content] <- [[$node_id, '']]
|
||||
:put journal { node_id => content }
|
||||
}
|
||||
{
|
||||
|
|
|
@ -59,9 +59,8 @@ async fn main() -> Result<()> {
|
|||
let state = AppState { db };
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_headers(cors::Any)
|
||||
.allow_origin(cors::Any);
|
||||
|
||||
// build our application with a single route
|
||||
|
|
|
@ -126,13 +126,19 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
|||
type: String,
|
||||
}
|
||||
}
|
||||
{
|
||||
?[key, relation, field_name, type] <- [
|
||||
['panorama/journal/page/content', 'journal', 'content', 'string']
|
||||
]
|
||||
:put fqkey_to_dbkey { key, relation, field_name, type }
|
||||
}
|
||||
|
||||
# Create journal type
|
||||
{ :create journal { node_id: String => content: Json } }
|
||||
{ :create journal { node_id: String => content: String } }
|
||||
{ :create journal_day { day: String => node_id: String } }
|
||||
{
|
||||
::fts create journal:text_index {
|
||||
extractor: dump_json(content),
|
||||
extractor: content,
|
||||
extract_filter: !is_null(content),
|
||||
tokenizer: Simple,
|
||||
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
||||
|
|
|
@ -5,7 +5,7 @@ use axum::{
|
|||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use cozo::{DataValue, ScriptMutability};
|
||||
use cozo::{DataValue, ScriptMutability, Vector};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{error::AppResult, AppState};
|
||||
|
@ -69,18 +69,21 @@ pub async fn update_node(
|
|||
Json(update_data): Json<UpdateData>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
println!("Update data: {:?}", update_data);
|
||||
let node_id_data = DataValue::from(node_id.clone());
|
||||
|
||||
// TODO: Combine these into the same script
|
||||
|
||||
let tx = state.db.multi_transaction(true);
|
||||
|
||||
if let Some(extra_data) = update_data.extra_data {
|
||||
let result = tx.run_script(
|
||||
"
|
||||
?[relation, field_name, type] :=
|
||||
?[key, relation, field_name, type] :=
|
||||
*fqkey_to_dbkey{key, relation, field_name, type},
|
||||
key = $key
|
||||
is_in(key, $keys)
|
||||
",
|
||||
btmap! {
|
||||
"key".to_owned() => DataValue::List(
|
||||
"keys".to_owned() => DataValue::List(
|
||||
extra_data
|
||||
.keys()
|
||||
.map(|s| DataValue::from(s.as_str()))
|
||||
|
@ -89,9 +92,49 @@ pub async fn update_node(
|
|||
},
|
||||
)?;
|
||||
|
||||
println!("Result: {result:?}");
|
||||
let s = |s: &DataValue| s.get_str().unwrap().to_owned();
|
||||
let result = result
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(|row| (s(&row[0]), (s(&row[1]), s(&row[2]), s(&row[3]))))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
for (key, (relation, field_name, ty)) in result.iter() {
|
||||
let new_value = extra_data.get(key).unwrap();
|
||||
|
||||
// TODO: Make this more generic
|
||||
let new_value = DataValue::from(new_value.as_str().unwrap());
|
||||
|
||||
let query = format!(
|
||||
"
|
||||
?[ node_id, {field_name} ] <- [[$node_id, $input_data]]
|
||||
:update {relation} {{ node_id, {field_name} }}
|
||||
"
|
||||
);
|
||||
println!("QUERY: {query:?}");
|
||||
let result = tx.run_script(
|
||||
&query,
|
||||
btmap! {
|
||||
"node_id".to_owned() => node_id_data.clone(),
|
||||
"input_data".to_owned() => new_value,
|
||||
},
|
||||
)?;
|
||||
|
||||
println!("RESULT: {result:?}");
|
||||
}
|
||||
}
|
||||
|
||||
tx.run_script(
|
||||
"
|
||||
# Always update the time
|
||||
?[ id, updated_at ] <- [[ $node_id, now() ]]
|
||||
:update node { id, updated_at }
|
||||
",
|
||||
btmap! {
|
||||
"node_id".to_owned() => node_id_data,
|
||||
},
|
||||
);
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
Ok(Json(json!({})))
|
||||
|
|
4191
pnpm-lock.yaml
4191
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue