update
This commit is contained in:
parent
4f31d10da4
commit
52ae6ce480
23 changed files with 530 additions and 155 deletions
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
panorama
|
||||||
|
========
|
||||||
|
|
||||||
|
Personal information manager.
|
||||||
|
|
||||||
|
Contact
|
||||||
|
-------
|
||||||
|
|
||||||
|
Author: Michael Zhang
|
||||||
|
|
||||||
|
License: GPL-3.0-only
|
|
@ -22,6 +22,7 @@
|
||||||
"@uiw/react-md-editor": "^4.0.4",
|
"@uiw/react-md-editor": "^4.0.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"formik": "^2.4.6",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||||
"hast-util-to-mdast": "^10.1.0",
|
"hast-util-to-mdast": "^10.1.0",
|
||||||
"immutable": "^4.3.6",
|
"immutable": "^4.3.6",
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn greet(name: &str) -> String {
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![greet])
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,9 @@ function App() {
|
||||||
})();
|
})();
|
||||||
}, [nodesOpened, openNode]);
|
}, [nodesOpened, openNode]);
|
||||||
|
|
||||||
const nodes = nodesOpened
|
const nodes = [...nodesOpened.reverse().values()].map((nodeId, idx) => (
|
||||||
.reverse()
|
<NodeDisplay idx={idx} key={nodeId} id={nodeId} />
|
||||||
.map((nodeId) => <NodeDisplay key={nodeId} id={nodeId} />);
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { getVersion } from "@tauri-apps/api/app";
|
||||||
import ListIcon from "@mui/icons-material/List";
|
import ListIcon from "@mui/icons-material/List";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { sidebarExpandedAtom } from "./Sidebar";
|
import { sidebarExpandedAtom } from "./Sidebar";
|
||||||
import { nodesOpenedAtom } from "../App";
|
import { nodesOpenedAtom, useOpenNode } from "../App";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
const version = await getVersion();
|
const version = await getVersion();
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const setNodesOpened = useSetAtom(nodesOpenedAtom);
|
const openNode = useOpenNode();
|
||||||
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
||||||
|
|
||||||
const createNewJournalPage = useCallback(() => {
|
const createNewJournalPage = useCallback(() => {
|
||||||
|
@ -29,9 +29,9 @@ export default function Header() {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
setNodesOpened((prev) => [data.node_id, ...prev]);
|
openNode(data.node_id);
|
||||||
})();
|
})();
|
||||||
}, [setNodesOpened]);
|
}, [openNode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,17 +14,44 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 2px 12px;
|
|
||||||
color: rgb(106, 103, 160);
|
color: rgb(106, 103, 160);
|
||||||
background: rgb(204, 201, 255);
|
background: rgb(204, 201, 255);
|
||||||
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
|
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
|
||||||
|
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 6px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding: 2px 12px;
|
padding: 2px 12px;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
// For the resize handle
|
||||||
|
// TODO: Make the entire right side resize and then remove this
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
// border: 1px solid red;
|
||||||
|
// box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
@ -34,16 +61,4 @@
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid lightgray;
|
|
||||||
outline: none;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.untitled {
|
|
||||||
color: gray;
|
|
||||||
}
|
}
|
|
@ -2,88 +2,44 @@ import { useQuery } from "@tanstack/react-query";
|
||||||
import styles from "./NodeDisplay.module.scss";
|
import styles from "./NodeDisplay.module.scss";
|
||||||
import ReactTimeAgo from "react-time-ago";
|
import ReactTimeAgo from "react-time-ago";
|
||||||
import JournalPage from "./nodes/JournalPage";
|
import JournalPage from "./nodes/JournalPage";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { getNode } from "../lib/getNode";
|
||||||
|
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||||
|
import { useOpenNode } from "../App";
|
||||||
|
|
||||||
export interface NodeDisplayProps {
|
export interface NodeDisplayProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
idx?: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NodeDisplay({ id }: NodeDisplayProps) {
|
export default function NodeDisplay({ id, idx }: NodeDisplayProps) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["fetchNode", id],
|
queryKey: ["fetchNode", id],
|
||||||
queryFn: async () => {
|
queryFn: getNode,
|
||||||
const resp = await fetch(`http://localhost:5195/node/${id}`);
|
|
||||||
const json = await resp.json();
|
|
||||||
return json;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isSuccess, status, data } = query;
|
const { isSuccess, status, data: nodeDescriptor } = query;
|
||||||
|
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
let Component = undefined;
|
||||||
const [title, setTitle] = useState(() =>
|
let data = undefined;
|
||||||
isSuccess && data ? data.title : undefined,
|
if (isSuccess) {
|
||||||
);
|
Component = nodeDescriptor.render;
|
||||||
|
data = nodeDescriptor.data;
|
||||||
useEffect(() => {
|
}
|
||||||
if (data) {
|
|
||||||
setTitle(data.title);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const saveChangedTitle = useCallback(() => {
|
|
||||||
(async () => {
|
|
||||||
const resp = await fetch(`http://localhost:5195/node/${id}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ title: title }),
|
|
||||||
});
|
|
||||||
setIsEditingTitle(false);
|
|
||||||
})();
|
|
||||||
}, [title, id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
{isSuccess ? (
|
{isSuccess ? (
|
||||||
<NodeDisplayHeaderLoaded id={id} data={data} />
|
<NodeDisplayHeaderLoaded idx={idx} id={id} data={data} />
|
||||||
) : (
|
) : (
|
||||||
<>ID {id}</>
|
<>
|
||||||
|
ID {id} ({status})
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isEditingTitle ? (
|
|
||||||
<form
|
|
||||||
onSubmit={(evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
saveChangedTitle();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
className={styles.title}
|
|
||||||
type="text"
|
|
||||||
value={title}
|
|
||||||
onChange={(evt) => setTitle(evt.target.value)}
|
|
||||||
onBlur={() => saveChangedTitle()}
|
|
||||||
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={styles.title}
|
|
||||||
onDoubleClick={() => setIsEditingTitle(true)}
|
|
||||||
>
|
|
||||||
{title ?? <span className={styles.untitled}>(untitled)</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{isSuccess ? (
|
{Component && <Component id={id} data={data} />}
|
||||||
<NodeDisplayLoaded id={id} data={data} />
|
|
||||||
) : (
|
|
||||||
<>Status: {status}</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footer}>{id}</div>
|
<div className={styles.footer}>{id}</div>
|
||||||
|
@ -91,25 +47,27 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NodeDisplayHeaderLoaded({ id, data }) {
|
function NodeDisplayHeaderLoaded({ idx, id, data }) {
|
||||||
|
const openNode = useOpenNode();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
Type {data.type} · Last updated{" "}
|
{idx === 0 || (
|
||||||
<ReactTimeAgo date={data.created_at * 1000} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openNode(id)}
|
||||||
|
title="Move node to the left"
|
||||||
|
>
|
||||||
|
<FirstPageIcon fontSize="inherit" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
Type {data.type}{" "}
|
||||||
|
{data.created_at && (
|
||||||
|
<>
|
||||||
|
· Last updated <ReactTimeAgo date={data.updated_at * 1000} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NodeDisplayLoaded({ id, data }) {
|
|
||||||
switch (data.type) {
|
|
||||||
case "panorama/journal/page":
|
|
||||||
return <JournalPage id={id} data={data} />;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
Don't know how to render node of type <code>{data.type}</code>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -93,21 +93,23 @@ function SearchMenu({ results }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.searchResults}>
|
<div className={styles.searchResults}>
|
||||||
{results.map((result) => (
|
{results.map((result) => {
|
||||||
<button
|
return (
|
||||||
type="button"
|
<button
|
||||||
key={result.node_id}
|
type="button"
|
||||||
className={styles.searchResult}
|
key={result.node_id}
|
||||||
onClick={() => {
|
className={styles.searchResult}
|
||||||
setSearchQuery("");
|
onClick={() => {
|
||||||
setShowMenu(false);
|
setSearchQuery("");
|
||||||
openNode(result.node_id);
|
setShowMenu(false);
|
||||||
}}
|
openNode(result.node_id);
|
||||||
>
|
}}
|
||||||
<div className={styles.title}>{result.title}</div>
|
>
|
||||||
<div className={styles.subtitle}>{result.content}</div>
|
<div className={styles.title}>{result.title}</div>
|
||||||
</button>
|
<div className={styles.subtitle}>{result.content}</div>
|
||||||
))}
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
|
|
@ -3,11 +3,13 @@ import styles from "./Sidebar.module.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import EmailIcon from "@mui/icons-material/Email";
|
import EmailIcon from "@mui/icons-material/Email";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
|
import { useOpenNode } from "../App";
|
||||||
|
|
||||||
export const sidebarExpandedAtom = atom(false);
|
export const sidebarExpandedAtom = atom(false);
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
|
const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
|
||||||
|
const openNode = useOpenNode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -16,10 +18,14 @@ export default function Sidebar() {
|
||||||
sidebarExpanded ? styles.expanded : styles.collapsed,
|
sidebarExpanded ? styles.expanded : styles.collapsed,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.item}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.item}
|
||||||
|
onClick={() => openNode("panorama/mail")}
|
||||||
|
>
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
<span className={styles.label}>Email</span>
|
<span className={styles.label}>Email</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,18 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid lightgray;
|
||||||
|
outline: none;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.untitled {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
.mdContent {
|
.mdContent {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import MDEditor, { PreviewType } from "@uiw/react-md-editor";
|
import MDEditor, { PreviewType } from "@uiw/react-md-editor";
|
||||||
import { usePrevious, useDebounce } from "@uidotdev/usehooks";
|
import { usePrevious } from "@uidotdev/usehooks";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import styles from "./JournalPage.module.scss";
|
import styles from "./JournalPage.module.scss";
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
import remarkEmbedder from "@remark-embedder/core";
|
|
||||||
import { parse as parseDate, format as formatDate } from "date-fns";
|
import { parse as parseDate, format as formatDate } from "date-fns";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
export interface JournalPageProps {
|
export interface JournalPageProps {
|
||||||
id: string;
|
id: string;
|
||||||
data: {
|
data: {
|
||||||
|
day?: string;
|
||||||
|
title?: string;
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,13 +21,19 @@ export default function JournalPage({ id, data }: JournalPageProps) {
|
||||||
const { day } = data;
|
const { day } = data;
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [value, setValue] = useState(() => data.content);
|
const [value, setValue] = useState(() => data.content);
|
||||||
const valueToSave = useDebounce(value, 1000);
|
const [valueToSave] = useDebounce(value, 1000, {
|
||||||
|
leading: true,
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
const previous = usePrevious(valueToSave);
|
const previous = usePrevious(valueToSave);
|
||||||
const changed = valueToSave !== previous;
|
const changed = valueToSave !== previous;
|
||||||
const [mode, setMode] = useState<PreviewType>("preview");
|
const [mode, setMode] = useState<PreviewType>("preview");
|
||||||
|
const [title, setTitle] = useState(() => data.title);
|
||||||
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
// console.log("OLD", previous, "NEW", valueToSave);
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log("Saving...");
|
console.log("Saving...");
|
||||||
const resp = await fetch(`http://localhost:5195/node/${id}`, {
|
const resp = await fetch(`http://localhost:5195/node/${id}`, {
|
||||||
|
@ -47,23 +55,63 @@ export default function JournalPage({ id, data }: JournalPageProps) {
|
||||||
}
|
}
|
||||||
}, [id, changed, valueToSave, queryClient]);
|
}, [id, changed, valueToSave, queryClient]);
|
||||||
|
|
||||||
return (
|
const saveChangedTitle = useCallback(() => {
|
||||||
<div data-color-mode="light" className={styles.container}>
|
(async () => {
|
||||||
{day && <DayIndicator day={day} />}
|
const resp = await fetch(`http://localhost:5195/node/${id}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ title: title }),
|
||||||
|
});
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
})();
|
||||||
|
}, [title, id]);
|
||||||
|
|
||||||
<MDEditor
|
return (
|
||||||
value={value}
|
<>
|
||||||
className={styles.mdEditor}
|
{isEditingTitle ? (
|
||||||
onChange={(newValue) => newValue !== undefined && setValue(newValue)}
|
<form
|
||||||
preview={mode}
|
onSubmit={(evt) => {
|
||||||
visibleDragbar={false}
|
evt.preventDefault();
|
||||||
onDoubleClick={() => setMode("live")}
|
saveChangedTitle();
|
||||||
previewOptions={{
|
}}
|
||||||
remarkPlugins: [remarkMath],
|
>
|
||||||
rehypePlugins: [rehypeKatex],
|
<input
|
||||||
}}
|
className={styles.title}
|
||||||
/>
|
type="text"
|
||||||
</div>
|
value={title}
|
||||||
|
onChange={(evt) => setTitle(evt.target.value)}
|
||||||
|
onBlur={() => saveChangedTitle()}
|
||||||
|
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles.title}
|
||||||
|
onDoubleClick={() => setIsEditingTitle(true)}
|
||||||
|
>
|
||||||
|
{title ?? <span className={styles.untitled}>(untitled)</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div data-color-mode="light" className={styles.container}>
|
||||||
|
{day && <DayIndicator day={day} />}
|
||||||
|
|
||||||
|
<MDEditor
|
||||||
|
value={value}
|
||||||
|
className={styles.mdEditor}
|
||||||
|
onChange={(newValue) => newValue !== undefined && setValue(newValue)}
|
||||||
|
preview={mode}
|
||||||
|
visibleDragbar={false}
|
||||||
|
onDoubleClick={() => setMode("live")}
|
||||||
|
previewOptions={{
|
||||||
|
remarkPlugins: [remarkMath],
|
||||||
|
rehypePlugins: [rehypeKatex],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
app/src/components/nodes/Mail.module.scss
Normal file
25
app/src/components/nodes/Mail.module.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailList {
|
||||||
|
flex-grow: 1;
|
||||||
|
border-top: 1px solid lightgray;
|
||||||
|
}
|
134
app/src/components/nodes/Mail.tsx
Normal file
134
app/src/components/nodes/Mail.tsx
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import styles from "./Mail.module.scss";
|
||||||
|
import { Formik } from "formik";
|
||||||
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export default function Mail() {
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{showSettings ? <>Settings</> : <>Mail</>}
|
||||||
|
</div>
|
||||||
|
<div className="spacer" />
|
||||||
|
<button type="button" onClick={() => setShowSettings((prev) => !prev)}>
|
||||||
|
<SettingsIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSettings && (
|
||||||
|
<div className={styles.settings}>
|
||||||
|
<MailConfig />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.mailList}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MailConfig() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useQuery({
|
||||||
|
queryKey: ["mailConfigs"],
|
||||||
|
queryFn: fetchMailConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isSuccess, data } = config;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
Add a new config
|
||||||
|
<Formik
|
||||||
|
onSubmit={(values) => {
|
||||||
|
(async () => {
|
||||||
|
const resp = await fetch("http://localhost:5195/node", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: "panorama/mail/config",
|
||||||
|
extra_data: {
|
||||||
|
"panorama/mail/config/imap_hostname": values.imapHostname,
|
||||||
|
"panorama/mail/config/imap_port": values.imapPort,
|
||||||
|
"panorama/mail/config/imap_username": values.imapUsername,
|
||||||
|
"panorama/mail/config/imap_password": values.imapPassword,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
console.log("result", data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["mailConfigs"] });
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
initialValues={{
|
||||||
|
imapHostname: "",
|
||||||
|
imapPort: 993,
|
||||||
|
imapUsername: "",
|
||||||
|
imapPassword: "",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ values, handleSubmit, handleChange, handleBlur }) => (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="imapHostname"
|
||||||
|
placeholder="IMAP Hostname"
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={values.imapHostname}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="imapPort"
|
||||||
|
placeholder="IMAP Port"
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={values.imapPort}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="imapUsername"
|
||||||
|
placeholder="IMAP Username"
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={values.imapUsername}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="imapPassword"
|
||||||
|
placeholder="IMAP Password"
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={values.imapPassword}
|
||||||
|
/>
|
||||||
|
<button type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isSuccess && (
|
||||||
|
<ul>
|
||||||
|
{data.map((config) => (
|
||||||
|
<li>{JSON.stringify(config)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMailConfig() {
|
||||||
|
const resp = await fetch("http://localhost:5195/mail/config");
|
||||||
|
const data = await resp.json();
|
||||||
|
return data.configs;
|
||||||
|
}
|
|
@ -16,4 +16,8 @@ html,
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
34
app/src/lib/getNode.ts
Normal file
34
app/src/lib/getNode.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Component, FC } from "react";
|
||||||
|
import JournalPage from "../components/nodes/JournalPage";
|
||||||
|
import Mail from "../components/nodes/Mail";
|
||||||
|
import { QueryFunctionContext } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export interface RenderProps {
|
||||||
|
id: string;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDescriptor {
|
||||||
|
render: FC<RenderProps>;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
} & any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNode({
|
||||||
|
queryKey,
|
||||||
|
}: QueryFunctionContext): Promise<NodeDescriptor> {
|
||||||
|
const [, node_id] = queryKey;
|
||||||
|
switch (node_id) {
|
||||||
|
case "panorama/mail":
|
||||||
|
return { data: { type: "panorama/mail" }, render: Mail };
|
||||||
|
default: {
|
||||||
|
const resp = await fetch(`http://localhost:5195/node/${node_id}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
return {
|
||||||
|
data: { ...data, type: "panorama/journal/page" },
|
||||||
|
render: JournalPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,8 +47,6 @@ pub async fn export(State(state): State<AppState>) -> AppResult<()> {
|
||||||
relation_columns.insert(relation_name.clone(), columns);
|
relation_columns.insert(relation_name.clone(), columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("columns: {relation_columns:?}");
|
|
||||||
|
|
||||||
let base_dir = PathBuf::from("export");
|
let base_dir = PathBuf::from("export");
|
||||||
fs::create_dir_all(&base_dir);
|
fs::create_dir_all(&base_dir);
|
||||||
|
|
||||||
|
@ -68,7 +66,6 @@ pub async fn export(State(state): State<AppState>) -> AppResult<()> {
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
let query = format!("?[{columns}] := *{relation_name} {{ {columns} }}");
|
let query = format!("?[{columns}] := *{relation_name} {{ {columns} }}");
|
||||||
println!("Query: {query}");
|
|
||||||
let result = tx.run_script(&query, Default::default())?;
|
let result = tx.run_script(&query, Default::default())?;
|
||||||
|
|
||||||
writer.write_record(result.headers).unwrap();
|
writer.write_record(result.headers).unwrap();
|
||||||
|
|
|
@ -10,7 +10,6 @@ pub async fn get_todays_journal_id(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let today = todays_date();
|
let today = todays_date();
|
||||||
println!("Getting journal id for {today}!");
|
|
||||||
|
|
||||||
let result = state.db.run_script(
|
let result = state.db.run_script(
|
||||||
"
|
"
|
||||||
|
@ -22,8 +21,6 @@ pub async fn get_todays_journal_id(
|
||||||
ScriptMutability::Immutable,
|
ScriptMutability::Immutable,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
println!("Result: {:?}", result);
|
|
||||||
|
|
||||||
// TODO: Do this check on the server side
|
// TODO: Do this check on the server side
|
||||||
if result.rows.len() == 0 {
|
if result.rows.len() == 0 {
|
||||||
// Insert a new one
|
// Insert a new one
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
use axum::{extract::State, Json};
|
||||||
|
use cozo::{DbInstance, ScriptMutability};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{error::AppResult, AppState};
|
||||||
|
|
||||||
|
pub async fn get_mail_config(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
let configs = fetch_mail_configs(&state.db)?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"configs": configs,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mail_loop(db: DbInstance) {
|
||||||
|
// Fetch the mail configs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct MailConfig {
|
||||||
|
node_id: String,
|
||||||
|
imap_hostname: String,
|
||||||
|
imap_port: u16,
|
||||||
|
imap_username: String,
|
||||||
|
imap_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_mail_configs(db: &DbInstance) -> AppResult<Vec<MailConfig>> {
|
||||||
|
let result = db.run_script(
|
||||||
|
"
|
||||||
|
?[node_id, imap_hostname, imap_port, imap_username, imap_password] :=
|
||||||
|
*node{ id: node_id },
|
||||||
|
*mail_config{ node_id, imap_hostname, imap_port, imap_username, imap_password }
|
||||||
|
",
|
||||||
|
Default::default(),
|
||||||
|
ScriptMutability::Immutable,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let result = result
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| MailConfig {
|
||||||
|
node_id: row[0].get_str().unwrap().to_owned(),
|
||||||
|
imap_hostname: row[1].get_str().unwrap().to_owned(),
|
||||||
|
imap_port: row[2].get_int().unwrap() as u16,
|
||||||
|
imap_username: row[3].get_str().unwrap().to_owned(),
|
||||||
|
imap_password: row[4].get_str().unwrap().to_owned(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ extern crate sugars;
|
||||||
mod error;
|
mod error;
|
||||||
mod export;
|
mod export;
|
||||||
mod journal;
|
mod journal;
|
||||||
|
mod mail;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
mod node;
|
mod node;
|
||||||
mod query_builder;
|
mod query_builder;
|
||||||
|
@ -31,6 +32,7 @@ use tower_http::cors::{self, CorsLayer};
|
||||||
use crate::{
|
use crate::{
|
||||||
export::export,
|
export::export,
|
||||||
journal::get_todays_journal_id,
|
journal::get_todays_journal_id,
|
||||||
|
mail::{get_mail_config, mail_loop},
|
||||||
migrations::run_migrations,
|
migrations::run_migrations,
|
||||||
node::{create_node, get_node, node_types, search_nodes, update_node},
|
node::{create_node, get_node, node_types, search_nodes, update_node},
|
||||||
};
|
};
|
||||||
|
@ -56,6 +58,8 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
run_migrations(&db).await?;
|
run_migrations(&db).await?;
|
||||||
|
|
||||||
|
tokio::spawn(mail_loop(db.clone()));
|
||||||
|
|
||||||
let state = AppState { db };
|
let state = AppState { db };
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
|
@ -73,6 +77,7 @@ async fn main() -> Result<()> {
|
||||||
.route("/node/:id", post(update_node))
|
.route("/node/:id", post(update_node))
|
||||||
.route("/node/types", get(node_types))
|
.route("/node/types", get(node_types))
|
||||||
.route("/journal/get_todays_journal_id", get(get_todays_journal_id))
|
.route("/journal/get_todays_journal_id", get(get_todays_journal_id))
|
||||||
|
.route("/mail/config", get(get_mail_config))
|
||||||
.layer(ServiceBuilder::new().layer(cors))
|
.layer(ServiceBuilder::new().layer(cors))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
@ -88,7 +93,8 @@ pub fn ensure_ok(s: &str) -> Result<()> {
|
||||||
let status = status.as_object().unwrap();
|
let status = status.as_object().unwrap();
|
||||||
let ok = status.get("ok").unwrap().as_bool().unwrap_or(false);
|
let ok = status.get("ok").unwrap().as_bool().unwrap_or(false);
|
||||||
if !ok {
|
if !ok {
|
||||||
bail!("shit (error: {s})")
|
let display = status.get("display").unwrap().as_str().unwrap();
|
||||||
|
bail!("shit (error: {display})")
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ pub async fn run_migrations(db: &DbInstance) -> Result<()> {
|
||||||
&format!("{{\"version\":{}}}", idx),
|
&format!("{{\"version\":{}}}", idx),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
ensure_ok(&result)?;
|
ensure_ok(&result)?;
|
||||||
|
|
||||||
println!("succeeded migration {idx}!");
|
println!("succeeded migration {idx}!");
|
||||||
|
@ -128,7 +129,11 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
?[key, relation, field_name, type] <- [
|
?[key, relation, field_name, type] <- [
|
||||||
['panorama/journal/page/content', 'journal', 'content', 'string']
|
['panorama/journal/page/content', 'journal', 'content', 'string'],
|
||||||
|
['panorama/mail/config/imap_hostname', 'mail_config', 'imap_hostname', 'string'],
|
||||||
|
['panorama/mail/config/imap_port', 'mail_config', 'imap_port', 'int'],
|
||||||
|
['panorama/mail/config/imap_username', 'mail_config', 'imap_username', 'string'],
|
||||||
|
['panorama/mail/config/imap_password', 'mail_config', 'imap_password', 'string'],
|
||||||
]
|
]
|
||||||
:put fqkey_to_dbkey { key, relation, field_name, type }
|
:put fqkey_to_dbkey { key, relation, field_name, type }
|
||||||
}
|
}
|
||||||
|
@ -144,6 +149,20 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Mail
|
||||||
|
{
|
||||||
|
:create mail_config {
|
||||||
|
node_id: String
|
||||||
|
=>
|
||||||
|
imap_hostname: String,
|
||||||
|
imap_port: Int,
|
||||||
|
imap_username: String,
|
||||||
|
imap_password: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calendar
|
||||||
",
|
",
|
||||||
"",
|
"",
|
||||||
false,
|
false,
|
||||||
|
|
|
@ -70,7 +70,6 @@ pub async fn update_node(
|
||||||
Path(node_id): Path<String>,
|
Path(node_id): Path<String>,
|
||||||
Json(update_data): Json<UpdateData>,
|
Json(update_data): Json<UpdateData>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
println!("Update data: {:?}", update_data);
|
|
||||||
let node_id_data = DataValue::from(node_id.clone());
|
let node_id_data = DataValue::from(node_id.clone());
|
||||||
|
|
||||||
// TODO: Combine these into the same script
|
// TODO: Combine these into the same script
|
||||||
|
@ -158,7 +157,6 @@ pub async fn create_node(
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let node_id = Uuid::now_v7();
|
let node_id = Uuid::now_v7();
|
||||||
let node_id = node_id.to_string();
|
let node_id = node_id.to_string();
|
||||||
println!("Opts: {opts:?}");
|
|
||||||
|
|
||||||
let tx = state.db.multi_transaction(true);
|
let tx = state.db.multi_transaction(true);
|
||||||
|
|
||||||
|
@ -179,15 +177,17 @@ pub async fn create_node(
|
||||||
let result_by_relation = result
|
let result_by_relation = result
|
||||||
.iter()
|
.iter()
|
||||||
.into_group_map_by(|(key, (relation, field_name, ty))| relation);
|
.into_group_map_by(|(key, (relation, field_name, ty))| relation);
|
||||||
println!("Result by relation: {result_by_relation:?}");
|
|
||||||
|
|
||||||
for (relation, fields) in result_by_relation.iter() {
|
for (relation, fields) in result_by_relation.iter() {
|
||||||
let fields_mapping = fields
|
let fields_mapping = fields
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(key, (_, field_name, _))| {
|
.map(|(key, (_, field_name, ty))| {
|
||||||
let new_value = extra_data.get(*key).unwrap();
|
let new_value = extra_data.get(*key).unwrap();
|
||||||
// TODO: Make this more generic
|
// TODO: Make this more generic
|
||||||
let new_value = DataValue::from(new_value.as_str().unwrap());
|
let new_value = match ty.as_str() {
|
||||||
|
"int" => DataValue::from(new_value.as_i64().unwrap()),
|
||||||
|
_ => DataValue::from(new_value.as_str().unwrap()),
|
||||||
|
};
|
||||||
(field_name.to_owned(), new_value)
|
(field_name.to_owned(), new_value)
|
||||||
})
|
})
|
||||||
.collect::<BTreeMap<_, _>>();
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
|
@ -47,6 +47,9 @@ importers:
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
|
formik:
|
||||||
|
specifier: ^2.4.6
|
||||||
|
version: 2.4.6(react@18.3.1)
|
||||||
hast-util-to-jsx-runtime:
|
hast-util-to-jsx-runtime:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
|
@ -1257,6 +1260,13 @@ packages:
|
||||||
'@types/unist': 3.0.2
|
'@types/unist': 3.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/hoist-non-react-statics@3.3.5:
|
||||||
|
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.3.3
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/katex@0.16.7:
|
/@types/katex@0.16.7:
|
||||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -1567,6 +1577,11 @@ packages:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/deepmerge@2.2.1:
|
||||||
|
resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dequal@2.0.3:
|
/dequal@2.0.3:
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -1674,6 +1689,22 @@ packages:
|
||||||
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/formik@2.4.6(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
'@types/hoist-non-react-statics': 3.3.5
|
||||||
|
deepmerge: 2.2.1
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
lodash: 4.17.21
|
||||||
|
lodash-es: 4.17.21
|
||||||
|
react: 18.3.1
|
||||||
|
react-fast-compare: 2.0.4
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fsevents@2.3.3:
|
/fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
@ -2101,6 +2132,14 @@ packages:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash-es@4.17.21:
|
||||||
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/longest-streak@3.1.0:
|
/longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -2713,6 +2752,10 @@ packages:
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-fast-compare@2.0.4:
|
||||||
|
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -3099,6 +3142,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tiny-warning@1.0.3:
|
||||||
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-fast-properties@2.0.0:
|
/to-fast-properties@2.0.0:
|
||||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -3122,6 +3169,10 @@ packages:
|
||||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tslib@2.6.2:
|
||||||
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/typescript@5.4.5:
|
/typescript@5.4.5:
|
||||||
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
Loading…
Reference in a new issue