Create project and documents #1

Merged
nicole merged 9 commits from projects-and-documents into main 2024-05-21 12:59:05 +00:00
9 changed files with 213 additions and 54 deletions
Showing only changes of commit fbfc3d2970 - Show all commits

1
Cargo.lock generated
View File

@ -1858,6 +1858,7 @@ dependencies = [
"memo-map", "memo-map",
"self_cell", "self_cell",
"serde", "serde",
"serde_json",
"v_htmlescape", "v_htmlescape",
] ]

View File

@ -19,7 +19,7 @@ dotenvy = "0.15.7"
env_logger = "0.11.3" env_logger = "0.11.3"
fjall = "0.6.5" fjall = "0.6.5"
free-icons = "0.7.0" free-icons = "0.7.0"
minijinja = { version = "1.0.14", features = ["loader"] } minijinja = { version = "1.0.14", features = ["loader", "json", "builtins"] }
minijinja-autoreload = "1.0.14" minijinja-autoreload = "1.0.14"
model_derive = { path = "./model_derive" } model_derive = { path = "./model_derive" }
rand = "0.8.5" rand = "0.8.5"

View File

@ -7,18 +7,6 @@ import { nord } from '@milkdown/theme-nord'
import { listener, listenerCtx } from '@milkdown/plugin-listener'; import { listener, listenerCtx } from '@milkdown/plugin-listener';
import '@milkdown/theme-nord/style.css' import '@milkdown/theme-nord/style.css'
const markdown =
`# Milkdown Vanilla Commonmark
> You're scared of a world where you're needed.
This is a demo for using Milkdown with **Vanilla Typescript**.
- A list
- [ ] Something to do
- [x] Something done`
function configureListItem(ctx: Ctx) { function configureListItem(ctx: Ctx) {
ctx.set(listItemBlockConfig.key, { ctx.set(listItemBlockConfig.key, {
renderLabel: (label: string, listType, checked?: boolean) => { renderLabel: (label: string, listType, checked?: boolean) => {
@ -35,16 +23,20 @@ function configureListItem(ctx: Ctx) {
}) })
} }
Editor function createEditor(rootId, fieldId, content) {
Editor
.make() .make()
.config(ctx => { .config(ctx => {
ctx.set(rootCtx, "#editor") ctx.set(rootCtx, rootId)
ctx.set(defaultValueCtx, markdown) ctx.set(defaultValueCtx, content)
const listener = ctx.get(listenerCtx); const listener = ctx.get(listenerCtx);
listener.markdownUpdated((ctx, markdown, prevMarkdown) => { listener.markdownUpdated((ctx, markdown, prevMarkdown) => {
if (markdown !== prevMarkdown) { if (markdown !== prevMarkdown) {
console.log(markdown); console.log(markdown);
console.log(fieldId);
document.getElementById(fieldId).value = markdown;
console.log("updated");
} }
}) })
}) })
@ -55,3 +47,6 @@ Editor
.use(listener) .use(listener)
.use(listItemBlockComponent) .use(listItemBlockComponent)
.create(); .create();
}
window.createEditor = createEditor;

View File

@ -45,8 +45,8 @@ pub async fn create_document_page(
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct CreateDocumentSubmission { pub struct CreateDocumentSubmission {
project_id: Uuid, pub project_id: Uuid,
title: String, pub title: String,
} }
pub async fn create_document_submit( pub async fn create_document_submit(
@ -117,6 +117,7 @@ pub async fn edit_document_page(
} }
}; };
dbg!(&document);
let values = context! { let values = context! {
user => user, user => user,
document => document, document => document,
@ -124,3 +125,46 @@ pub async fn edit_document_page(
ctx.render_resp("documents/edit_document.html", values) ctx.render_resp("documents/edit_document.html", values)
} }
#[derive(Debug, Deserialize)]
pub struct EditDocumentSubmission {
pub title: String,
pub content: String,
}
pub async fn edit_document_submit(
State(ctx): State<Context>,
auth_session: AuthSession<Context>,
Path((document_id,)): Path<(Uuid,)>,
form: Form<EditDocumentSubmission>,
) -> Response {
let user = match auth_session.user {
Some(user) => user,
None => return Redirect::to("/login").into_response(),
};
let mut document = match ModelPermission::user_document(&ctx.kv_handles, user.id, document_id) {
Ok(Some(document)) => document,
Ok(None) => return Redirect::to("/documents").into_response(),
Err(err) => {
error!(?err, "failed to load document");
return internal_server_error();
}
};
let new_document = Document {
id: document.id,
project_id: document.id,
title: form.title.to_owned(),
content: form.content.to_owned(),
};
if let Err(err) = new_document.save(&ctx.kv_handles) {
error!(?err, "failed to save document");
return internal_server_error();
}
info!(?new_document, "document updated");
Redirect::to("/documents").into_response()
}

View File

@ -10,7 +10,7 @@ use tower_sessions::SessionManagerLayer;
use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore}; use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore};
use tracing::Level; use tracing::Level;
use crate::{config::CommandLineOptions, context::Context, handler::{documents::{create_document_page, create_document_submit, documents_page, edit_document_page}, home::home_page, login::logout, login_page, login_submit, projects::{create_project_page, create_project_submit, projects_page}}, kv::KvHandle, logging::setup_logging, templates::make_template_loader}; use crate::{config::CommandLineOptions, context::Context, handler::{documents::{create_document_page, create_document_submit, documents_page, edit_document_page, edit_document_submit}, home::home_page, login::logout, login_page, login_submit, projects::{create_project_page, create_project_submit, projects_page}}, kv::KvHandle, logging::setup_logging, templates::make_template_loader};
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
dotenvy::dotenv()?; dotenvy::dotenv()?;
@ -50,6 +50,7 @@ pub async fn run() -> Result<()> {
.route("/documents/new", get(create_document_page)) .route("/documents/new", get(create_document_page))
.route("/documents/new", post(create_document_submit)) .route("/documents/new", post(create_document_submit))
.route("/documents/edit/:id", get(edit_document_page)) .route("/documents/edit/:id", get(edit_document_page))
.route("/documents/edit/:id", post(edit_document_submit))
.layer(trace_layer) .layer(trace_layer)
.layer(session_layer) .layer(session_layer)
.layer(auth_layer) .layer(auth_layer)

View File

@ -31819,15 +31819,6 @@
}; };
// frontend/main.ts // frontend/main.ts
var markdown = `# Milkdown Vanilla Commonmark
> You're scared of a world where you're needed.
This is a demo for using Milkdown with **Vanilla Typescript**.
- A list
- [ ] Something to do
- [x] Something done`;
function configureListItem(ctx) { function configureListItem(ctx) {
ctx.set(listItemBlockConfig.key, { ctx.set(listItemBlockConfig.key, {
renderLabel: (label, listType, checked) => { renderLabel: (label, listType, checked) => {
@ -31842,14 +31833,20 @@ This is a demo for using Milkdown with **Vanilla Typescript**.
} }
}); });
} }
function createEditor(rootId, fieldId, content3) {
Oe.make().config((ctx) => { Oe.make().config((ctx) => {
ctx.set(ce, "#editor"); ctx.set(ce, rootId);
ctx.set(re, markdown); ctx.set(re, content3);
const listener = ctx.get(h4); const listener = ctx.get(h4);
listener.markdownUpdated((ctx2, markdown2, prevMarkdown) => { listener.markdownUpdated((ctx2, markdown, prevMarkdown) => {
if (markdown2 !== prevMarkdown) { if (markdown !== prevMarkdown) {
console.log(markdown2); console.log(markdown);
console.log(fieldId);
document.getElementById(fieldId).value = markdown;
console.log("updated");
} }
}); });
}).config(configureListItem).use(cr).use(wt).use(c5).use(U5).use(listItemBlockComponent).create(); }).config(configureListItem).use(cr).use(wt).use(c5).use(U5).use(listItemBlockComponent).create();
}
window.createEditor = createEditor;
})(); })();

View File

@ -1166,6 +1166,23 @@ select {
height: auto; height: auto;
} }
.textarea {
min-height: 3rem;
flex-shrink: 1;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 0.875rem;
line-height: 1.25rem;
line-height: 2;
border-radius: var(--rounded-btn, 0.5rem);
border-width: 1px;
border-color: transparent;
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.btm-nav > * .label { .btm-nav > * .label {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
@ -1607,6 +1624,38 @@ select {
} }
} }
.textarea:focus {
box-shadow: none;
border-color: var(--fallback-bc,oklch(var(--bc)/0.2));
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
outline-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.textarea-disabled,
.textarea:disabled,
.textarea[disabled] {
cursor: not-allowed;
--tw-border-opacity: 1;
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
color: var(--fallback-bc,oklch(var(--bc)/0.4));
}
.textarea-disabled::-moz-placeholder, .textarea:disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder {
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
--tw-placeholder-opacity: 0.2;
}
.textarea-disabled::placeholder,
.textarea:disabled::placeholder,
.textarea[disabled]::placeholder {
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
--tw-placeholder-opacity: 0.2;
}
@keyframes toast-pop { @keyframes toast-pop {
0% { 0% {
transform: scale(0.9); transform: scale(0.9);
@ -2239,6 +2288,10 @@ select {
display: flex; display: flex;
} }
.hidden {
display: none;
}
.h-16 { .h-16 {
height: 4rem; height: 4rem;
} }
@ -2255,6 +2308,11 @@ select {
height: 100%; height: 100%;
} }
.h-max {
height: -moz-max-content;
height: max-content;
}
.min-h-full { .min-h-full {
min-height: 100%; min-height: 100%;
} }
@ -2287,6 +2345,11 @@ select {
width: 100%; width: 100%;
} }
.w-max {
width: -moz-max-content;
width: max-content;
}
.flex-1 { .flex-1 {
flex: 1 1 0%; flex: 1 1 0%;
} }
@ -2404,6 +2467,16 @@ select {
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(229 231 235 / var(--tw-border-opacity));
} }
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity));
}
.border-gray-400 {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
}
.bg-accent { .bg-accent {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity))); background-color: var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en" class="h-full bg-white">
{% include "head.html" %}
<body class="h-full">
<div class="h-full">
{% set current_page = "documents" %}
{% include "components/sidebar.html" %}
<main class="pl-72 bg-gray-50 h-full">
<form action="/documents/edit/{{ document.id }}" method="POST">
<div class="navbar bg-accent text-accent-content">
<div class="navbar-start gap-2">
<a class="btn" href="/documents">
{{ "arrow-left"|heroicon("w-6 h-6")|safe }} Back
</a>
<button class="btn" onClick="saveDocument()">Save</button>
</div>
</div>
<div class="px-8 py-8 flex flex-col gap-y-4">
<label class="input input-bordered flex items-center gap-2">
Title
<input type="text" id="title" name="title" class="grow" value="{{ document.title }}" />
</label>
<div id="editor-{{document.id}}" class="prose bg-white"></div>
<textarea id="content-{{ document.id }}" name="content" class="hidden">
</textarea>
</div>
</form>
</main>
<script type="text/javascript" src="/static/main.js"></script>
<script type="text/javascript">
console.log("hi");
window.createEditor("#editor-{{document.id}}", "content-{{document.id}}", {{document.content | tojson }});
console.log({{document.content | tojson}});
console.log("hi2");
function saveDocument() {
console.log("saving");
}
</script>
</body>
</html>

View File

@ -26,7 +26,7 @@
{% for document in documents %} {% for document in documents %}
<div class="card w-96 bg-base-100 shadow-md border-2 border-solid"> <div class="card w-96 bg-base-100 shadow-md border-2 border-solid">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ document.name }}</h2> <h2 class="card-title">{{ document.title }}</h2>
<div class="card-actions justify-end"> <div class="card-actions justify-end">
<a href="/documents/edit/{{ document.id }}" class="btn btn-primary">Open</a> <a href="/documents/edit/{{ document.id }}" class="btn btn-primary">Open</a>