Create project and documents #1
9 changed files with 213 additions and 54 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1858,6 +1858,7 @@ dependencies = [
|
||||||
"memo-map",
|
"memo-map",
|
||||||
"self_cell",
|
"self_cell",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"v_htmlescape",
|
"v_htmlescape",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createEditor(rootId, fieldId, content) {
|
||||||
Editor
|
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;
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
48
templates/documents/edit_document.html
Normal file
48
templates/documents/edit_document.html
Normal 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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue