Create project and documents #1
7 changed files with 260 additions and 6 deletions
|
@ -1,7 +1,7 @@
|
||||||
use axum::{response::Redirect, Form};
|
use axum::{extract::Path, response::Redirect, Form};
|
||||||
use axum_login::AuthSession;
|
use axum_login::AuthSession;
|
||||||
|
|
||||||
use crate::{models::ModelPermission, prelude::*};
|
use crate::{handler::internal_server_error, models::{Document, ModelPermission, ModelType, Permission}, prelude::*};
|
||||||
|
|
||||||
pub async fn documents_page(
|
pub async fn documents_page(
|
||||||
State(ctx): State<Context>,
|
State(ctx): State<Context>,
|
||||||
|
@ -24,3 +24,103 @@ async fn render_documents_page(ctx: Context, user: crate::entity::user::Model) -
|
||||||
|
|
||||||
ctx.render_resp("documents/list_documents.html", values)
|
ctx.render_resp("documents/list_documents.html", values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_document_page(
|
||||||
|
State(ctx): State<Context>,
|
||||||
|
auth_session: AuthSession<Context>,
|
||||||
|
) -> Response {
|
||||||
|
let user = match auth_session.user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Redirect::to("/login").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let projects = ModelPermission::user_projects(&ctx.kv_handles, user.id).unwrap_or_default();
|
||||||
|
|
||||||
|
let values = context! {
|
||||||
|
user => user,
|
||||||
|
projects => projects,
|
||||||
|
};
|
||||||
|
ctx.render_resp("documents/create_document.html", values)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateDocumentSubmission {
|
||||||
|
project_id: Uuid,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_document_submit(
|
||||||
|
State(ctx): State<Context>,
|
||||||
|
auth_session: AuthSession<Context>,
|
||||||
|
form: Form<CreateDocumentSubmission>,
|
||||||
|
) -> Response {
|
||||||
|
let user = match auth_session.user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Redirect::to("/login").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = match ModelPermission::user_project(&ctx.kv_handles, user.id, form.project_id) {
|
||||||
|
Ok(Some(project)) => project,
|
||||||
|
Ok(None) => return Redirect::to("/documents/create").into_response(),
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "failed to access kv store");
|
||||||
|
return Redirect::to("/documents/create").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let document = Document {
|
||||||
|
id: Uuid::now_v7(),
|
||||||
|
project_id: project.id,
|
||||||
|
title: form.title.to_owned(),
|
||||||
|
content: "".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = document.save(&ctx.kv_handles) {
|
||||||
|
error!(?err, "failed to save document");
|
||||||
|
return internal_server_error();
|
||||||
|
}
|
||||||
|
info!(?document, "document created");
|
||||||
|
|
||||||
|
let permission = ModelPermission {
|
||||||
|
user_id: user.id,
|
||||||
|
model_type: ModelType::Document,
|
||||||
|
role: Permission::Admin,
|
||||||
|
model_id: document.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = permission.add(&ctx.kv_handles) {
|
||||||
|
error!(?err, "failed to save new project permission");
|
||||||
|
return internal_server_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Redirect::to("/documents").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit_document_page(
|
||||||
|
State(ctx): State<Context>,
|
||||||
|
auth_session: AuthSession<Context>,
|
||||||
|
Path((id,)): Path<(Uuid,)>,
|
||||||
|
) -> Response {
|
||||||
|
let user = match auth_session.user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Redirect::to("/login").into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let document = match ModelPermission::user_document(&ctx.kv_handles, user.id, 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 values = context! {
|
||||||
|
user => user,
|
||||||
|
document => document,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.render_resp("documents/edit_document.html", values)
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ pub async fn create_project_submit(
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = Project {
|
let project = Project {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::now_v7(),
|
||||||
owner_id: user.id,
|
owner_id: user.id,
|
||||||
|
|
||||||
name: form.name.clone(),
|
name: form.name.clone(),
|
||||||
|
|
|
@ -140,6 +140,20 @@ impl ModelPermission {
|
||||||
Ok(projects)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_project(kv_handle: &KvHandle, user_id: i32, project_id: Uuid) -> Result<Option<Project>> {
|
||||||
|
let key = format!("{}:{}:{}:{}", user_id, ModelType::Project, Permission::Admin, project_id);
|
||||||
|
let value = kv_handle.permissions.get(key)?;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Some(value) => {
|
||||||
|
let permission: ModelPermission = bincode::deserialize(&value)?;
|
||||||
|
let project = Project::load(kv_handle, permission.model_id)?;
|
||||||
|
Ok(project)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user_documents(kv_handle: &KvHandle, user_id: i32) -> Result<Vec<Document>> {
|
pub fn user_documents(kv_handle: &KvHandle, user_id: i32) -> Result<Vec<Document>> {
|
||||||
let prefix = format!("{}:{}:", user_id, ModelType::Document);
|
let prefix = format!("{}:{}:", user_id, ModelType::Document);
|
||||||
|
|
||||||
|
@ -149,13 +163,28 @@ impl ModelPermission {
|
||||||
let permission: ModelPermission = bincode::deserialize(&value)?;
|
let permission: ModelPermission = bincode::deserialize(&value)?;
|
||||||
ids.push(permission.model_id);
|
ids.push(permission.model_id);
|
||||||
}
|
}
|
||||||
|
dbg!(&ids);
|
||||||
|
|
||||||
let documents: Vec<Document> = ids
|
let documents: Vec<Document> = ids
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|id| Document::load(kv_handle, id).ok().flatten())
|
.filter_map(|id| Document::load(kv_handle, id).ok().flatten())
|
||||||
.collect();
|
.collect();
|
||||||
|
dbg!(&documents);
|
||||||
|
|
||||||
Ok(documents)
|
Ok(documents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn user_document(kv_handle: &KvHandle, user_id: i32, document_id: Uuid) -> Result<Option<Document>> {
|
||||||
|
let key = format!("{}:{}:{}:{}", user_id, ModelType::Document, Permission::Admin, document_id);
|
||||||
|
let value = kv_handle.permissions.get(key)?;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Some(value) => {
|
||||||
|
let permission: ModelPermission = bincode::deserialize(&value)?;
|
||||||
|
let document = Document::load(kv_handle, permission.model_id)?;
|
||||||
|
Ok(document)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::documents_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}, 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()?;
|
||||||
|
@ -47,6 +47,9 @@ pub async fn run() -> Result<()> {
|
||||||
.route("/projects/new", get(create_project_page))
|
.route("/projects/new", get(create_project_page))
|
||||||
.route("/projects/new", post(create_project_submit))
|
.route("/projects/new", post(create_project_submit))
|
||||||
.route("/documents", get(documents_page))
|
.route("/documents", get(documents_page))
|
||||||
|
.route("/documents/new", get(create_document_page))
|
||||||
|
.route("/documents/new", post(create_document_submit))
|
||||||
|
.route("/documents/edit/:id", get(edit_document_page))
|
||||||
.layer(trace_layer)
|
.layer(trace_layer)
|
||||||
.layer(session_layer)
|
.layer(session_layer)
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
|
|
|
@ -1132,6 +1132,40 @@ select {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
display: inline-flex;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 3rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 2.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)));
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||||
|
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
||||||
|
background-position: calc(100% - 20px) calc(1px + 50%),
|
||||||
|
calc(100% - 16.1px) calc(1px + 50%);
|
||||||
|
background-size: 4px 4px,
|
||||||
|
4px 4px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select[multiple] {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.btm-nav > * .label {
|
.btm-nav > * .label {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
|
@ -1519,6 +1553,50 @@ select {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select: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));
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-disabled,
|
||||||
|
.select:disabled,
|
||||||
|
.select[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-disabled::-moz-placeholder, .select:disabled::-moz-placeholder, .select[disabled]::-moz-placeholder {
|
||||||
|
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
|
||||||
|
--tw-placeholder-opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-disabled::placeholder,
|
||||||
|
.select:disabled::placeholder,
|
||||||
|
.select[disabled]::placeholder {
|
||||||
|
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));
|
||||||
|
--tw-placeholder-opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-multiple,
|
||||||
|
.select[multiple],
|
||||||
|
.select[size].select:not([size="1"]) {
|
||||||
|
background-image: none;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .select {
|
||||||
|
background-position: calc(0% + 12px) calc(1px + 50%),
|
||||||
|
calc(0% + 16px) calc(1px + 50%);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes skeleton {
|
@keyframes skeleton {
|
||||||
from {
|
from {
|
||||||
background-position: 150%;
|
background-position: 150%;
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-white">
|
||||||
|
{% include "head.html" %}
|
||||||
|
<body class="h-full">
|
||||||
|
|
||||||
|
<div class="h-full">
|
||||||
|
{% set current_page = "projects" %}
|
||||||
|
{% include "components/sidebar.html" %}
|
||||||
|
|
||||||
|
<main class="pl-72 bg-gray-50 h-full">
|
||||||
|
<div class="navbar bg-accent text-accent-content">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="btn" href="/projects">
|
||||||
|
{{ "arrow-left"|heroicon("w-6 h-6")|safe }} Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="/documents/new" method="POST">
|
||||||
|
<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" placeholder="The title of the document is..." />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
Project
|
||||||
|
<select id="project_id" name="project_id" class="grow">
|
||||||
|
{% for project in projects %}
|
||||||
|
<option value="{{ project.id }}">{{ project.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn btn-primary">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/static/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -22,14 +22,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-8 flex flex-col gap-y-4">
|
<div class="px-8 py-8 flex flex-col gap-y-4">
|
||||||
|
|
||||||
<div id="editor" class="prose bg-white"></div>
|
|
||||||
|
|
||||||
{% 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.name }}</h2>
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<button class="btn btn-primary">Open</button>
|
<a href="/documents/edit/{{ document.id }}" class="btn btn-primary">Open</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +36,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="editor" class="prose bg-white"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue