save docs

This commit is contained in:
Michael Zhang 2024-05-26 23:07:02 -05:00
parent bcd2e9086b
commit ccebb53879
12 changed files with 2062 additions and 2414 deletions

1
Cargo.lock generated
View file

@ -2778,6 +2778,7 @@ dependencies = [
"axum",
"chrono",
"cozo",
"csv",
"dirs",
"futures",
"miette",

View file

@ -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": {

View file

@ -1,3 +1,10 @@
.container {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.mdContent {
flex-grow: 1;
display: flex;

View file

@ -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>
);
}

View file

@ -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) {
}
}

View file

@ -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"

View file

@ -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();
writer
.write_record(
row.iter().map(|col| serde_json::to_string(&col).unwrap()),
)
.unwrap();
// file.write(b"\n");
}
let serialized = serde_json::to_string(&object).unwrap();
file.write(serialized.as_bytes());
file.write(b"\n");
}
writer.flush().unwrap();
}
Ok(())

View file

@ -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 }
}
{

View file

@ -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

View file

@ -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')],

View file

@ -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,8 +92,48 @@ 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()?;

File diff suppressed because it is too large Load diff