Commit 7ce1c3061f5680c2a3758ee746b2dff3dec438ec
Merge branch 'feature/markdown-content-storage' into 'master'
Feature/markdown content storage The markdown storage is feature complete... When I am a bit more experienced with mogwai the code should be cleaned up. See merge request !1
Showing
43 changed files
with
1550 additions
and
210 deletions
common/Cargo.toml
0 → 100644
1 | +# This is from the webpage: | |
2 | +# https://www.developer.com/languages/creating-an-api-with-rust-and-sqlite/ | |
3 | +# I have fixed some things manually as the code on that page did not compile | |
4 | +# without. | |
5 | + | |
6 | +# Additional informations at https://actix.rs/docs/databases/ assume one | |
7 | +# should use web::block to access the database... | |
8 | +# TODO check what is the difference to this approach. | |
9 | +# - well, we use web::block already for write actions. | |
10 | + | |
11 | +# Introduction to rust async: | |
12 | +# https://gruberbastian.com/posts/rust_async/ | |
13 | +# https://blog.logrocket.com/a-practical-guide-to-async-in-rust/ | |
14 | +# https://os.phil-opp.com/async-await/ | |
15 | + | |
16 | +# Simple explanation on technical terms synchronous, asynchronous, concurrent | |
17 | +# and parallel. | |
18 | +# https://medium.com/plain-and-simple/synchronous-vs-asynchronous-vs-concurrent-vs-parallel-4342bfb8b9f2 | |
19 | + | |
20 | +[package] | |
21 | +name = "artshop-common" | |
22 | +version = "0.1.0" | |
23 | +workspace = ".." | |
24 | +edition = "2018" | |
25 | + | |
26 | +[dependencies] | |
27 | +serde = "^1.0" | ... | ... |
common/src/lib.rs
0 → 100644
1 | +pub mod types; | ... | ... |
common/src/types.rs
0 → 100644
1 | +use serde::{Deserialize, Serialize}; | |
2 | + | |
3 | +#[derive(Debug, Clone, Deserialize, Serialize)] | |
4 | +pub enum Either<L, R> { | |
5 | + Left(L), | |
6 | + Right(R) | |
7 | +} | |
8 | + | |
9 | +#[derive(Clone, Debug, Serialize, Deserialize)] | |
10 | +pub struct MarkdownJson { | |
11 | + pub name: String, | |
12 | + pub content: String, | |
13 | + pub number_of_versions: i32, | |
14 | + pub date_created: String, | |
15 | + pub date_updated: String, | |
16 | +} | |
17 | + | |
18 | +#[derive(Clone, Debug, Serialize, Deserialize)] | |
19 | +pub struct MarkdownDiffJson { | |
20 | + pub id: i32, | |
21 | + pub date_created: String, | |
22 | +} | ... | ... |
No preview for this file type
1 | -# Ein sehr schöner Titel | |
1 | +# Markdown Cheatsheat | |
2 | 2 | |
3 | -## Ein sinnloser Text | |
4 | - | |
5 | -Hier kommt ganz viel Text der irgendwie auch was machen soll, aber Zeilen | |
6 | -sollen auch im <pre> Eingabefeld automatisch umbrechen. | |
7 | - | |
8 | -Ein neuner Paragraph beginnt nach einer Leerzeile. | |
9 | -Ein Umbruch entsteht wie gewohnt durch 2 spaces am Ende einer | |
10 | -Zeile. | |
11 | - | |
12 | -## Fußnoten | |
3 | +## Überschriften | |
4 | +--- | |
13 | 5 | |
14 | -Vllt. kann man sogar so was wie Fussnoten[^1] in den Markdown Text | |
15 | -einbinden... diese kann man dann irgendwo einbauen... | |
6 | +# # <H1> | |
7 | +## ## <H2> | |
8 | +### ### <H3> | |
9 | +#### #### <H4> | |
10 | +##### ##### <H5> | |
11 | +###### ###### <H6> | |
16 | 12 | |
13 | +## Absätze und Umbrüche | |
17 | 14 | --- |
18 | 15 | |
19 | -[^1]: Zum Beispiel so... | |
20 | - | |
21 | -[^2]: Oder so... | |
16 | +Ein einfacher Zeilenumbuch | |
17 | +verändert den Textfluß nicht. Spaces haben auch keinen Einfluß auf den Textfluß. Es ist selten eine gute Idee große Abstände innerhalb eines Textes zu habe, sollte man diese aber wirklich brauchen kann man auf inline html zurückgreifen. | |
22 | 18 | |
23 | -## inline html ist im Moment auch ok. | |
19 | +Leerzeilen erzeugen neue Paragraphen. Um im formatierten Text einen Zeilenumbruch zu erzeugen verwendet man zwei Spaces vor einem Zeilenumbruch im Eingabetext. | |
20 | +Dies führt nicht zu einem Paragraphen. | |
24 | 21 | |
25 | -<pre>Lustigerweise geht auch inline html</pre> | |
22 | +## Hervorhebungen | |
23 | +--- | |
26 | 24 | |
27 | -## Listen for fun | |
25 | +- *kursive (schwache) Hervorhebung* | |
26 | +- **fette (starke) Hervorhebung** | |
27 | +- ***kursiv und fette (sehr starke) Hervorhebung*** | |
28 | +- *schwache mit **eingebetteter starker** Hervorhebung* | |
29 | +- **starke mit *eingebetteter schwacher* Hervorhebung** | |
30 | +- ~~durchgestrichen~~ | |
31 | +- <u>untersteichen nur mit HTML</u> | |
32 | +- ~~*durchgestrichen kursiv*~~ | |
33 | +- **~~fett durchgestrichen~~** | |
34 | +- *<u>kursiv unterstrichen</u>* | |
35 | +- <u>**unterstrichen fett**</u> | |
36 | + | |
37 | +## Listen | |
38 | +--- | |
28 | 39 | |
29 | -- ein Liste | |
30 | - - mehr Liste | |
31 | - - diesmal als Subliste. | |
32 | -- und was auch immer... | |
33 | - 1. und nun Verschachtelt. | |
34 | - 1. Numeriert. | |
35 | - 2. huhuhu | |
36 | - 3. wie bitte. | |
37 | - 2. juhu | |
38 | -- noch mehr Liste | |
40 | +- erster Listeneintrag | |
41 | + - erster Unterlisteneintrag | |
42 | + - zweiter Unterlisteneintrag | |
43 | +- zweiter Listeneintrag | |
44 | + 1. erster numerierter Listeneintrag | |
45 | + 1. erster numerierter Unterlisteneintrag | |
46 | + 2. zeiter numerierter Unterlisteneintrag | |
47 | + 3. dritter numerierter Unterlisteneintrag | |
48 | + 2. zweiter numerierter Listeneintrag | |
49 | +- dritter Listeneintrag | |
50 | + - [x] erster Auswahllisteneintrag | |
51 | + - [ ] zweiter Auswahllisteneintrag | |
52 | + - [ ] dritter Auswahllisteneintrag | |
53 | +- vierter Listeneintrag | |
54 | + 1. [ ] erster numerierter Auswahllisteneintrag | |
55 | + 2. [x] zweiter numerierter Auswahllisteneintrag | |
56 | + | |
57 | +## Code Blöcke | |
58 | +--- | |
39 | 59 | |
40 | -## Preformated Text | |
60 | + Dies ist ein codeblock durch Einrückung. | |
61 | + In diesem werden keine Formatierungen | |
62 | + vorgenommen. | |
41 | 63 | |
42 | -```Hier kommt der code``` | |
64 | +Mit backticks lassen sich Codeblöcke mit Sprachinformation | |
65 | +erstellen. Theoretisch koennte fuer solche Böcke dann Syntax-Highliting eingebaut werden. | |
43 | 66 | |
44 | -Und hier der Paragraph mit `inline code` der auch sehr schön aussehen kann. | |
67 | +```shell | |
68 | +#!/bin/env sh | |
45 | 69 | |
46 | -## Hervorhebungen | |
70 | +FOO="foo" | |
47 | 71 | |
48 | -Man kann Text auch sehr schön formatieren. So ist es z.B. möglich | |
49 | -*Worte kursiv zu stellen* oder man kann **sie auch fett schreiben**. | |
50 | -Als spezielles feature kann der von mir verwendete Parser auch | |
51 | -~~Texte durchstreichen~~. | |
72 | +function func() { | |
73 | + local BAR=bar | |
74 | +} | |
75 | +``` | |
52 | 76 | |
53 | -Nur wenn man Text <u>unterstreichen</u> will muss man auf inline html | |
54 | -zurückgreifen. | |
77 | +Auch in den Fließtext lassen sich `inline code` Elemente einfügen um z.B. einzelne Kommandos hervorzuheben. | |
55 | 78 | |
56 | -## Blockquotes und horizontale Linie | |
79 | +## Zitate und horizontale Linie | |
80 | +--- | |
57 | 81 | |
58 | -> Dies sollte jetzt als quote erkennbar sein. | |
82 | +> Dies ist ein Zitat. | |
59 | 83 | > |
60 | ->> Auch diese sind schachtelbar | |
84 | +>> Zitate können verschachtelt sein. | |
61 | 85 | > |
62 | -> Und weiter gehts. | |
86 | +> Wir können also zitieren was jemand zitiert hat. | |
87 | +> Solange die Zeilen ohne Unterbrechung mit einem > | |
88 | +> beginnen bleibt es ein Zitat | |
63 | 89 | |
64 | ---- | |
65 | 90 | |
66 | -> Aber dies ist ein neuer quote. | |
91 | +> Sobald eine Zeile on führendes > auftaucht endet ein | |
92 | +> Zitat. | |
67 | 93 | |
68 | 94 | ## Links |
95 | +--- | |
69 | 96 | |
70 | 97 | Ein link kann inline geschrieben werden, so wie diese zu |
71 | 98 | [Heise.de](https://heise.de/ 'Heise.de') oder als Referenz am Ende des Textes |
72 | 99 | wie diese nach [Telepolis][lnk1]. |
73 | 100 | |
74 | -## Bilder koennte man auch einbinden. | |
101 | +## Bilder | |
102 | +--- | |
75 | 103 | |
76 | 104 | Wie Links lassen sich auch Bilder wie mein |
77 | 105 | ![Gravatar](https://www.gravatar.com/avatar/fd016c954ec4ed3a4315eeed6c8b97b8) |
... | ... | @@ -85,7 +113,8 @@ Paragraphen zu plazieren. |
85 | 113 | |
86 | 114 | Etwas so wie hier. |
87 | 115 | |
88 | -## Tabellen sollten auch gehen... | |
116 | +## Tabellen | |
117 | +--- | |
89 | 118 | |
90 | 119 | Die folgenden Beispiele kommen von [markdown.land][lnk2]: |
91 | 120 | |
... | ... | @@ -103,12 +132,31 @@ Bananas | 1.89 | 6 |
103 | 132 | |
104 | 133 | und die Spaltenausrichtung kann man auch einstellen: |
105 | 134 | |
106 | - | |
107 | 135 | | Item | Price | # In stock | |
108 | 136 | |--------------|:-----:|-----------:| |
109 | 137 | | Juicy Apples | 1.99 | 739 | |
110 | 138 | | Bananas | 1.8900 | 6 | |
111 | 139 | |
140 | +## Fußnoten | |
141 | +--- | |
142 | + | |
143 | +Man kann auch verlinkte Fußnoten[^1] in den Text | |
144 | +einbinden. Die Fußnote selber kann dann an beliebige stelle im Text stehen. | |
145 | + | |
146 | +--- | |
147 | +[^1]: Zum Beispiel so. | |
148 | + | |
149 | +[^2]: Diese Fußnote hat keine Verlinkung im Text. | |
150 | + | |
151 | +## HTML einbetten. | |
152 | +--- | |
153 | + | |
154 | +<ul> | |
155 | +<li> | |
156 | +<pre>Man kann auch direkt HTML tags einbetten, | |
157 | +wie hier.</pre> | |
158 | +</li> | |
159 | +</ul> | |
112 | 160 | |
113 | 161 | [lnk1]: https://heise.de/tp/ 'Telepolis' |
114 | 162 | [lnk2]: https://markdown.land/markdown-table 'markdown.land' | ... | ... |
docs/notes.md
0 → 100644
1 | +# Markdown Historie. | |
2 | + | |
3 | +Jedes gespeicherte Markdown erhaelt eine historie ueber die jede vergangene | |
4 | +Version eingesehen werden kann. Gespeichert ist diese in Form eines | |
5 | +komprimierten diffs. | |
6 | + | |
7 | +Die Frontend Komponente kann jede beliebige existierende Patchversion | |
8 | +darstellen. Auf der Datenseite ist aber kein **undo** möglich. Genau wie man | |
9 | +in der Realität nicht in der Zeit zurück gehen kann kann man das auch nicht | |
10 | +mit den Inhalten. Würde man das zulassen, so würde entweder ein Teil der | |
11 | +Historie des Markdowns wegfallen müssen oder aber es würde ein beliebig | |
12 | +komplexer Baum von Histoien entstehen. | |
13 | + | |
14 | +Statt also ein **undo** auf eine frühere Version zuzulassen kann man in der | |
15 | +aktuellen Version auf frühere Stände zurück gehen und diese gegebenenfalls | |
16 | +noch anpassen. Dieser Stand wird wieder ein neuer Patch gegen die gegenwärtige | |
17 | +Version. | ... | ... |
... | ... | @@ -15,3 +15,10 @@ |
15 | 15 | ## Free Artwork |
16 | 16 | - [Designlooter](https://designlooter.com/ 'Designlooter') |
17 | 17 | - [SVGRepo](https://www.svgrepo.com/ 'SVGRepo') |
18 | + | |
19 | +## Send and async and using non Send types... | |
20 | +- [Rust Send Story](https://procmarco.netlify.app/blog/2021-05-04-a-story-about-async-rust-and-using-send-types/ 'Rust Send story') | |
21 | + | |
22 | +## Restful API | |
23 | +[ReadTheDocs apiguide](https://apiguide.readthedocs.io/en/latest/build_and_publish/use_RESTful_urls.html 'ReadTheDocs apiguide') | |
24 | +[RestfulApi](https://restfulapi.net/versioning/ 'RestfulApi') | ... | ... |
1 | -pub(crate) const MD_EXAMPLE :&str = r"# Ein sehr schöner Titel | |
1 | +CREATE TABLE "markdowns" ( | |
2 | + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, | |
3 | + name VARCHAR(256) NOT NULL, | |
4 | + content TEXT NOT NULL, | |
5 | + number_of_versions INTEGER NOT NULL DEFAULT (1), | |
6 | + date_created TEXT NOT NULL, | |
7 | + date_updated TEXT NOT NULL | |
8 | +); | |
9 | + | |
10 | +-- This holds compressed reverse patches to markdown.content. | |
11 | +-- The markdown.content field always holds the latest version. | |
12 | +-- This way we can always restore every version we had in the past. | |
13 | +-- The date_created here should be set to the value of | |
14 | +-- markdown.date_updated when the patch was created. This diff_id | |
15 | +-- is always current last max diff_id for given markdown_id plus 1. | |
16 | +CREATE TABLE "markdown_diffs" ( | |
17 | + markdown_id INTEGER NOT NULL, | |
18 | + diff_id INTEGER NOT NULL, | |
19 | + diff BLOB NOT NULL, | |
20 | + date_created TEXT NOT NULL, | |
21 | + PRIMARY KEY (markdown_id, diff_id) | |
22 | +); | |
23 | + | |
24 | +INSERT INTO | |
25 | + "markdowns"(name, content, date_created, date_updated) | |
26 | +VALUES | |
27 | + ( "md-example" | |
28 | + , "# Ein sehr schöner Titel | |
2 | 29 | |
3 | 30 | ## Ein sinnloser Text |
4 | 31 | |
... | ... | @@ -103,30 +130,12 @@ Bananas | 1.89 | 6 |
103 | 130 | |
104 | 131 | und die Spaltenausrichtung kann man auch einstellen: |
105 | 132 | |
106 | - | |
107 | 133 | | Item | Price | # In stock | |
108 | 134 | |--------------|:-----:|-----------:| |
109 | 135 | | Juicy Apples | 1.99 | 739 | |
110 | 136 | | Bananas | 1.8900 | 6 | |
111 | 137 | |
112 | - | |
113 | 138 | [lnk1]: https://heise.de/tp/ 'Telepolis' |
114 | -[lnk2]: https://markdown.land/markdown-table 'markdown.land'"; | |
115 | - | |
116 | -/* | |
117 | -pub(crate) const PEN_ICON :&str = r#"<?xml version="1.0" encoding="iso-8859-1"?> | |
118 | -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" | |
119 | - "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |
120 | -<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" | |
121 | - xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
122 | - viewBox="0 0 220.001 220.001" | |
123 | - style="enable-background:new 0 0 220.001 220.001;" xml:space="preserve"> | |
124 | - <g> | |
125 | - <polygon points="0,220 59.34,213.86 6.143,160.661"/> | |
126 | - <path d="M132.018,34.787l53.197,53.197L69.568,203.631L16.37, | |
127 | - 150.434L132.018,34.787z M212.696,60.502c9.738-9.738,9.742-25.527, | |
128 | - 0-35.268l-17.93-17.93c-9.738-9.74-25.529-9.738-35.268,0l-17.346, | |
129 | - 17.347l53.199,53.196L212.696,60.502z"/> | |
130 | - </g> | |
131 | -</svg>"#; | |
132 | -*/ | |
139 | +[lnk2]: https://markdown.land/markdown-table 'markdown.land'" | |
140 | + , "Today" | |
141 | + , "Today" ); | ... | ... |
mogwai-list/Cargo.toml
0 → 100644
1 | +[package] | |
2 | +name = "mogwai-list" | |
3 | +version = "0.0.0" | |
4 | +authors = ["Georg Hopp <georg@steffers.org>"] | |
5 | +workspace = ".." | |
6 | +edition = "2018" | |
7 | + | |
8 | +[lib] | |
9 | +crate-type = ["cdylib", "rlib"] | |
10 | + | |
11 | +[features] | |
12 | +default = ["console_error_panic_hook"] | |
13 | + | |
14 | +[dependencies] | |
15 | +artshop-common = { path = "../common" } | |
16 | +katex = { version = "0.4", default-features = false, features = ["wasm-js"] } | |
17 | +pulldown-cmark = "0.9" | |
18 | +console_log = "^0.1" | |
19 | +log = "^0.4" | |
20 | +serde = { version = "^1.0", features = ["derive"] } | |
21 | +serde_json = "^1.0" | |
22 | +wasm-bindgen = "^0.2" | |
23 | +wasm-bindgen-futures = "^0.4" | |
24 | + | |
25 | +# The `console_error_panic_hook` crate provides better debugging of panics by | |
26 | +# logging them with `console.error`. This is great for development, but requires | |
27 | +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for | |
28 | +# code size when deploying. | |
29 | +console_error_panic_hook = { version = "0.1.6", optional = true } | |
30 | +wee_alloc = { version = "0.4.2", optional = true } | |
31 | +js-sys = "^0.3" | |
32 | + | |
33 | +[dependencies.mogwai] | |
34 | +version = "^0.5" | |
35 | + | |
36 | +[dependencies.web-sys] | |
37 | +version = "^0.3" | |
38 | +features = [ | |
39 | + "Document", | |
40 | + "DomParser", | |
41 | + "Headers", | |
42 | + "HtmlElement", | |
43 | + "HtmlInputElement", | |
44 | + "MouseEvent", | |
45 | + "Node", | |
46 | + "Request", | |
47 | + "RequestInit", | |
48 | + "RequestMode", | |
49 | + "Response", | |
50 | + "SupportedType", | |
51 | + "Window", | |
52 | +] | |
53 | + | |
54 | +[dev-dependencies] | |
55 | +wasm-bindgen-test = "0.2" | ... | ... |
mogwai-list/src/lib.rs
0 → 100644
1 | +#![allow(unused_braces)] | |
2 | +use log::Level; | |
3 | +use mogwai::{futures, prelude::*}; | |
4 | +use std::panic; | |
5 | +use wasm_bindgen::prelude::*; | |
6 | + | |
7 | +/// An item widget. | |
8 | +/// Keeps track of clicks. | |
9 | +#[derive(Clone, Debug)] | |
10 | +struct Item { | |
11 | + id: usize, | |
12 | + clicks: Model<u32>, | |
13 | +} | |
14 | + | |
15 | +/// An item's update messages. | |
16 | +#[derive(Clone)] | |
17 | +enum ItemMsg { | |
18 | + /// The user clicked | |
19 | + Click, | |
20 | + /// The user requested this item be removed | |
21 | + Remove, | |
22 | +} | |
23 | + | |
24 | +/// One item's logic loop. | |
25 | +async fn item_logic( | |
26 | + id: usize, | |
27 | + clicks: Model<u32>, | |
28 | + mut from_view: broadcast::Receiver<ItemMsg>, | |
29 | + to_list: broadcast::Sender<ListMsg>, | |
30 | +) { | |
31 | + loop { | |
32 | + match from_view.recv().await { | |
33 | + Ok(ItemMsg::Click) => { | |
34 | + clicks.visit_mut(|c| *c += 1).await; | |
35 | + } | |
36 | + Ok(ItemMsg::Remove) => { | |
37 | + to_list.broadcast(ListMsg::RemoveItem(id)).await.unwrap(); | |
38 | + break; | |
39 | + } | |
40 | + Err(_) => break, | |
41 | + } | |
42 | + } | |
43 | + log::info!("item {} logic loop is done", id); | |
44 | +} | |
45 | + | |
46 | +// ANCHOR: item_view | |
47 | +fn item_view( | |
48 | + clicks: impl Stream<Item = u32> + Sendable, | |
49 | + to_logic: broadcast::Sender<ItemMsg>, | |
50 | +) -> ViewBuilder<Dom> { | |
51 | + builder! { | |
52 | + <li> | |
53 | + <button | |
54 | + style:cursor="pointer" | |
55 | + on:click=to_logic.sink().contra_map(|_| ItemMsg::Click)> | |
56 | + "Increment" | |
57 | + </button> | |
58 | + <button | |
59 | + style:cursor="pointer" | |
60 | + on:click=to_logic.sink().contra_map(|_| ItemMsg::Remove)> | |
61 | + "Remove" | |
62 | + </button> | |
63 | + " " | |
64 | + <span> | |
65 | + { | |
66 | + ("", clicks.map(|clicks| match clicks { | |
67 | + 1 => "1 click".to_string(), | |
68 | + n => format!("{} clicks", n), | |
69 | + })) | |
70 | + } | |
71 | + </span> | |
72 | + </li> | |
73 | + } | |
74 | +} | |
75 | +// ANCHOR_END: item_view | |
76 | + | |
77 | +/// Create a new item component. | |
78 | +fn item(id: usize, clicks: Model<u32>, to_list: broadcast::Sender<ListMsg>) -> Component<Dom> { | |
79 | + let (tx, rx) = broadcast::bounded(1); | |
80 | + Component::from(item_view(clicks.stream(), tx)).with_logic(item_logic(id, clicks, rx, to_list)) | |
81 | +} | |
82 | + | |
83 | +#[derive(Clone)] | |
84 | +enum ListMsg { | |
85 | + /// Create a new item | |
86 | + NewItem, | |
87 | + /// Remove the item with the given id | |
88 | + RemoveItem(usize), | |
89 | +} | |
90 | + | |
91 | +// ANCHOR: list_logic_coms | |
92 | +/// Launch the logic loop of our list of items. | |
93 | +async fn list_logic( | |
94 | + input: broadcast::Receiver<ListMsg>, | |
95 | + tx_patch_children: mpmc::Sender<ListPatch<ViewBuilder<Dom>>>, | |
96 | +) { | |
97 | + // Set up our communication from items to this logic loop by | |
98 | + // * creating a list patch model | |
99 | + // * creating a channel to go from item to list logic (aka here) | |
100 | + // * creating a side-effect stream (for_each) that runs for each item patch | |
101 | + // * map patches of Item to patches of builders and send that to our view | |
102 | + // through tx_patch_children | |
103 | + let mut items: ListPatchModel<Item> = ListPatchModel::new(); | |
104 | + let (to_list, from_items) = broadcast::bounded::<ListMsg>(1); | |
105 | + let to_list = to_list.clone(); | |
106 | + let all_item_patches = items.stream().map(move |patch| { | |
107 | + log::info!("mapping patch for item: {:?}", patch); | |
108 | + let to_list = to_list.clone(); | |
109 | + patch.map(move |Item { id, clicks }: Item| { | |
110 | + let to_list = to_list.clone(); | |
111 | + let component = item(id, clicks, to_list); | |
112 | + let builder: ViewBuilder<Dom> = component.into(); | |
113 | + builder | |
114 | + }) | |
115 | + }).for_each(move |patch| { | |
116 | + let tx_patch_children = tx_patch_children.clone(); | |
117 | + async move { | |
118 | + tx_patch_children.send(patch).await.unwrap(); | |
119 | + } | |
120 | + }); | |
121 | + mogwai::spawn(all_item_patches); | |
122 | + // ANCHOR_END: list_logic_coms | |
123 | + // ANCHOR: list_logic_loop | |
124 | + // Combine the input from our view with the input from our items | |
125 | + let mut input = futures::stream::select_all(vec![input, from_items]); | |
126 | + let mut next_id = 0; | |
127 | + loop { | |
128 | + match input.next().await { | |
129 | + Some(ListMsg::NewItem) => { | |
130 | + log::info!("creating a new item"); | |
131 | + let item: Item = Item { | |
132 | + id: next_id, | |
133 | + clicks: Model::new(0), | |
134 | + }; | |
135 | + next_id += 1; | |
136 | + // patch our items easily and _item_patch_stream's for_each runs automatically, | |
137 | + // keeping the list of item views in sync | |
138 | + items.list_patch_push(item); | |
139 | + } | |
140 | + Some(ListMsg::RemoveItem(id)) => { | |
141 | + log::info!("removing item: {}", id); | |
142 | + let mut may_index = None; | |
143 | + 'find_item_by_id: for (item, index) in items.read().await.iter().zip(0..) { | |
144 | + if item.id == id { | |
145 | + may_index = Some(index); | |
146 | + break 'find_item_by_id; | |
147 | + } | |
148 | + } | |
149 | + | |
150 | + if let Some(index) = may_index { | |
151 | + // patch our items to remove the item at the index | |
152 | + let _ = items.list_patch_remove(index); | |
153 | + } | |
154 | + } | |
155 | + _ => { | |
156 | + log::error!("Leaving list logic loop - this shouldn't happen"); | |
157 | + break; | |
158 | + }, | |
159 | + } | |
160 | + } | |
161 | + // ANCHOR_END: list_logic_loop | |
162 | +} | |
163 | + | |
164 | +// ANCHOR: list_view | |
165 | +fn list_view<T>(to_logic: broadcast::Sender<ListMsg>, children: T) -> ViewBuilder<Dom> | |
166 | +where | |
167 | + T: Stream<Item = ListPatch<ViewBuilder<Dom>>> + Sendable, | |
168 | +{ | |
169 | + builder! { | |
170 | + <fieldset> | |
171 | + <legend>"A List of Gizmos"</legend> | |
172 | + <button style:cursor="pointer" on:click=to_logic.sink().contra_map(|_| ListMsg::NewItem)> | |
173 | + "Create a new item" | |
174 | + </button> | |
175 | + <fieldset> | |
176 | + <legend>"Items"</legend> | |
177 | + <ol patch:children=children> | |
178 | + </ol> | |
179 | + </fieldset> | |
180 | + </fieldset> | |
181 | + } | |
182 | +} | |
183 | +// ANCHOR_END: list_view | |
184 | + | |
185 | +/// Create our list component. | |
186 | +fn list() -> Component<Dom> { | |
187 | + let (logic_tx, logic_rx) = broadcast::bounded(1); | |
188 | + let (item_patch_tx, item_patch_rx) = mpmc::bounded(1); | |
189 | + Component::from(list_view(logic_tx, item_patch_rx)) | |
190 | + .with_logic(list_logic(logic_rx, item_patch_tx)) | |
191 | +} | |
192 | + | |
193 | +#[wasm_bindgen] | |
194 | +pub fn main(parent_id: Option<String>) -> Result<(), JsValue> { | |
195 | + panic::set_hook(Box::new(console_error_panic_hook::hook)); | |
196 | + console_log::init_with_level(Level::Trace).unwrap(); | |
197 | + let component = list(); | |
198 | + let view = component.build().unwrap(); | |
199 | + | |
200 | + if let Some(id) = parent_id { | |
201 | + let parent = mogwai::utils::document().get_element_by_id(&id).unwrap(); | |
202 | + view.run_in_container(&parent) | |
203 | + } else { | |
204 | + view.run() | |
205 | + } | |
206 | +} | ... | ... |
... | ... | @@ -27,6 +27,7 @@ edition = "2018" |
27 | 27 | actix-files = "0.2" |
28 | 28 | actix-web = "2.0" |
29 | 29 | actix-rt = "1.1.1" |
30 | +artshop-common = { path = "../common" } | |
30 | 31 | diesel = { version = "1.4.7", features = ["sqlite", "r2d2"]} |
31 | 32 | r2d2 = "0.8.9" |
32 | 33 | dotenv = "0.15.0" |
... | ... | @@ -36,3 +37,5 @@ serde_json = "1.0" |
36 | 37 | anyhow = "1.0" |
37 | 38 | chrono = "0.4.15" |
38 | 39 | listenfd = "0.3" |
40 | +diffy = "0.2" | |
41 | +flate2 = "^1.0" | ... | ... |
1 | -use std::error::Error as StdError; | |
2 | -use std::fmt; | |
1 | +use std::{fmt::Display, pin::Pin}; | |
3 | 2 | |
4 | 3 | use diesel::result; |
4 | +use diffy::ParsePatchError; | |
5 | 5 | use r2d2; |
6 | 6 | |
7 | +type ParentError = Option<Pin<Box<dyn std::error::Error>>>; | |
8 | + | |
7 | 9 | #[derive(Debug)] |
8 | -pub(crate) enum Error { | |
9 | - DieselResult(result::Error), | |
10 | - DieselR2d2(r2d2::Error), | |
10 | +pub(crate) struct Error { | |
11 | + source: ParentError, | |
12 | + message: String, | |
11 | 13 | } |
12 | 14 | |
13 | -impl fmt::Display for Error { | |
14 | - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
15 | - match self { | |
16 | - Error::DieselR2d2(r) => write!(f, "{}", r), | |
17 | - Error::DieselResult(r) => write!(f, "{}", r), | |
18 | - } | |
15 | +unsafe impl Send for Error {} | |
16 | + | |
17 | +pub(crate) type Result<T> = std::result::Result<T, Error>; | |
18 | + | |
19 | +impl std::error::Error for Error { | |
20 | + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
21 | + self.source.as_deref() | |
19 | 22 | } |
20 | 23 | } |
21 | 24 | |
22 | -impl StdError for Error { | |
23 | - fn source(&self) -> Option<&(dyn StdError + 'static)> { | |
25 | +impl Display for Error { | |
26 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
24 | 27 | match self { |
25 | - Error::DieselR2d2(r) => Some(r), | |
26 | - Error::DieselResult(r) => Some(r), | |
28 | + Error { source: Some(source), message } => | |
29 | + write!(f, "{}: {}", message, source), | |
30 | + Error { source: None, message } => write!(f, "{}", message), | |
27 | 31 | } |
28 | 32 | } |
29 | 33 | } |
30 | 34 | |
35 | +impl From<&str> for Error { | |
36 | + fn from(message: &str) -> Self { | |
37 | + Self { source: None, message: String::from(message) } | |
38 | + } | |
39 | +} | |
40 | + | |
31 | 41 | impl From<result::Error> for Error { |
32 | - fn from(error: result::Error) -> Self { | |
33 | - Error::DieselResult(error) | |
42 | + fn from(source: result::Error) -> Self { | |
43 | + Self { source: Some(Box::pin(source)) | |
44 | + , message: String::from("Diesel Result Error") | |
45 | + } | |
34 | 46 | } |
35 | 47 | } |
36 | 48 | |
37 | 49 | impl From<r2d2::Error> for Error { |
38 | - fn from(error: r2d2::Error) -> Self { | |
39 | - Error::DieselR2d2(error) | |
50 | + fn from(source: r2d2::Error) -> Self { | |
51 | + Self { source: Some(Box::pin(source)) | |
52 | + , message: String::from("Diesel Result Error") | |
53 | + } | |
54 | + } | |
55 | +} | |
56 | + | |
57 | +impl From<std::io::Error> for Error { | |
58 | + fn from(source: std::io::Error) -> Self { | |
59 | + Self { source: Some(Box::pin(source)) | |
60 | + , message: String::from("IO Error") | |
61 | + } | |
62 | + } | |
63 | +} | |
64 | + | |
65 | +impl From<std::str::Utf8Error> for Error { | |
66 | + fn from(source: std::str::Utf8Error) -> Self { | |
67 | + Self { source: Some(Box::pin(source)) | |
68 | + , message: String::from("IO Error") | |
69 | + } | |
70 | + } | |
71 | +} | |
72 | + | |
73 | +impl From<ParsePatchError> for Error { | |
74 | + fn from(source: ParsePatchError) -> Self { | |
75 | + Self { source: Some(Box::pin(source)) | |
76 | + , message: String::from("IO Error") | |
77 | + } | |
40 | 78 | } |
41 | 79 | } | ... | ... |
... | ... | @@ -6,10 +6,15 @@ mod models; |
6 | 6 | mod routes; |
7 | 7 | mod schema; |
8 | 8 | |
9 | +use crate::routes::markdown::*; | |
10 | +use crate::routes::other::*; | |
11 | +use crate::routes::user::*; | |
12 | + | |
9 | 13 | use actix_web::{guard, web, App, HttpResponse, HttpServer}; |
10 | 14 | use diesel::r2d2::{self, ConnectionManager}; |
11 | 15 | use diesel::SqliteConnection; |
12 | 16 | use listenfd::ListenFd; |
17 | +use routes::markdown::get_markdown; | |
13 | 18 | |
14 | 19 | pub(crate) type Pool = r2d2::Pool<ConnectionManager<SqliteConnection>>; |
15 | 20 | |
... | ... | @@ -28,24 +33,41 @@ async fn main() -> std::io::Result<()> { |
28 | 33 | App::new() . data(database_pool.clone()) |
29 | 34 | . service(actix_files::Files::new("/static", "./static")) |
30 | 35 | . service( web::scope("/api/v0") |
36 | + . service( web::resource("/markdowns") | |
37 | + . route(web::get().to(get_markdowns)) | |
38 | + ) | |
39 | + . service( web::resource("/markdowns/{id}") | |
40 | + . route(web::get().to(get_markdown)) | |
41 | + . route(web::put().to(update_markdown)) | |
42 | + ) | |
43 | + . service( web::resource("/markdowns/{id}/patches") | |
44 | + . route(web::get().to(get_patches)) | |
45 | + ) | |
31 | 46 | . service( web::resource("/users") |
32 | - . route(web::get().to(routes::get_users)) | |
33 | - . route(web::put().to(routes::create_user))) | |
47 | + . route(web::get().to(get_users)) | |
48 | + . route(web::put().to(create_user)) | |
49 | + ) | |
34 | 50 | . service( web::resource("/users/{id}") |
35 | - . route(web::delete().to(routes::delete_user)) | |
36 | - . route(web::get().to(routes::get_user)) | |
37 | - . route(web::put().to(routes::update_user)))) | |
51 | + . route(web::delete().to(delete_user)) | |
52 | + . route(web::get().to(get_user)) | |
53 | + . route(web::put().to(update_user)) | |
54 | + ) | |
55 | + ) | |
38 | 56 | . service( web::scope("") |
39 | - . route("/", web::get().to(routes::root)) | |
40 | - . route("/index", web::get().to(routes::root)) | |
41 | - . route("/index.html", web::get().to(routes::root)) | |
42 | - . route("/favicon", web::get().to(routes::favicon)) | |
43 | - . route("/favicon.ico", web::get().to(routes::favicon))) | |
44 | - . default_service(web::resource("") | |
45 | - . route(web::get().to(routes::p404)) | |
57 | + . route("/", web::get().to(root)) | |
58 | + . route("/api.html", web::get().to(apidoc)) | |
59 | + . route("/index", web::get().to(root)) | |
60 | + . route("/index.html", web::get().to(root)) | |
61 | + . route("/favicon", web::get().to(favicon)) | |
62 | + . route("/favicon.ico", web::get().to(favicon)) | |
63 | + ) | |
64 | + . default_service( web::resource("") | |
65 | + . route( web::get().to(p404) ) | |
46 | 66 | . route( web::route() |
47 | - . guard(guard::Not(guard::Get())) | |
48 | - . to(HttpResponse::MethodNotAllowed))) | |
67 | + . guard( guard::Not(guard::Get()) ) | |
68 | + . to(HttpResponse::MethodNotAllowed) | |
69 | + ) | |
70 | + ) | |
49 | 71 | }); |
50 | 72 | |
51 | 73 | let server = match listenfd.take_tcp_listener(0).unwrap() { | ... | ... |
server/src/models/markdown.rs
0 → 100644
1 | +use std::sync::Arc; | |
2 | +use crate::schema::*; | |
3 | +use crate::error::*; | |
4 | +use crate::Pool; | |
5 | +use artshop_common::types::MarkdownDiffJson; | |
6 | +use artshop_common::types::MarkdownJson; | |
7 | +use diesel::prelude::*; | |
8 | +use diesel::{ | |
9 | + dsl::{delete, insert_into, update}, | |
10 | + RunQueryDsl | |
11 | +}; | |
12 | +use serde::{Deserialize, Serialize}; | |
13 | +use std::io::Write; | |
14 | +use diffy::{apply, create_patch, Patch}; | |
15 | +use flate2::Compression; | |
16 | +use flate2::write::{DeflateEncoder, DeflateDecoder}; | |
17 | + | |
18 | + | |
19 | +#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable)] | |
20 | +pub struct Markdown { | |
21 | + pub id: i32, | |
22 | + pub name: String, | |
23 | + pub content: String, | |
24 | + pub number_of_versions: i32, | |
25 | + pub date_created: String, | |
26 | + pub date_updated: String, | |
27 | +} | |
28 | + | |
29 | +#[derive(Debug, Serialize, Deserialize, Queryable, Identifiable)] | |
30 | +#[table_name = "markdown_diffs"] | |
31 | +#[primary_key(markdown_id, diff_id)] | |
32 | +pub struct MarkdownDiff { | |
33 | + pub markdown_id: i32, | |
34 | + pub diff_id: i32, | |
35 | + pub diff: Vec<u8>, | |
36 | + pub date_created: String, | |
37 | +} | |
38 | + | |
39 | +#[derive(Debug, Insertable)] | |
40 | +#[table_name = "markdowns"] | |
41 | +pub struct MarkdownNew<'a> { | |
42 | + pub name: &'a str, | |
43 | + pub content: &'a str, | |
44 | + pub number_of_versions: i32, | |
45 | + pub date_created: &'a str, | |
46 | + pub date_updated: &'a str, | |
47 | +} | |
48 | + | |
49 | +#[derive(Debug, Insertable)] | |
50 | +#[table_name = "markdown_diffs"] | |
51 | +pub struct MarkdownDiffNew<'a> { | |
52 | + pub markdown_id: i32, | |
53 | + pub diff_id: i32, | |
54 | + pub diff: &'a [u8], | |
55 | + pub date_created: &'a str, | |
56 | +} | |
57 | + | |
58 | +#[derive(Debug, Serialize, Deserialize, AsChangeset)] | |
59 | +#[table_name="markdowns"] | |
60 | +struct MarkdownChange<'a> { | |
61 | + pub name: &'a str, | |
62 | + pub content: &'a str, | |
63 | + pub number_of_versions: i32, | |
64 | + pub date_created: &'a str, | |
65 | + pub date_updated: &'a str, | |
66 | +} | |
67 | + | |
68 | +impl<'a> From<&'a MarkdownJson> for MarkdownChange<'a> { | |
69 | + fn from(md: &'a MarkdownJson) -> Self { | |
70 | + Self { name: md.name.as_str() | |
71 | + , content: md.content.as_str() | |
72 | + , number_of_versions: md.number_of_versions | |
73 | + , date_created: md.date_created.as_str() | |
74 | + , date_updated: md.date_updated.as_str() | |
75 | + } | |
76 | + } | |
77 | +} | |
78 | + | |
79 | +impl From<&MarkdownDiff> for MarkdownDiffJson { | |
80 | + fn from(md_diff: &MarkdownDiff) -> Self { | |
81 | + Self { id: md_diff.diff_id | |
82 | + , date_created: md_diff.date_created.to_owned() | |
83 | + } | |
84 | + } | |
85 | +} | |
86 | + | |
87 | +impl MarkdownDiff { | |
88 | + pub(crate) fn get_diff_as_string(&self) -> Result<String> { | |
89 | + let mut decoder = DeflateDecoder::new(Vec::new()); | |
90 | + | |
91 | + decoder.write_all(self.diff.as_ref())?; | |
92 | + let decomp = decoder.finish()?; | |
93 | + | |
94 | + Ok(String::from(std::str::from_utf8(decomp.as_ref())?)) | |
95 | + } | |
96 | +} | |
97 | + | |
98 | +pub(crate) enum _Action { | |
99 | + Created(Markdown), | |
100 | + Found(Markdown), | |
101 | +} | |
102 | + | |
103 | + | |
104 | +pub(crate) fn _create_markdown( pool: Arc<Pool> | |
105 | + , item: MarkdownJson ) -> Result<_Action> { | |
106 | + use crate::schema::markdowns::dsl::*; | |
107 | + let db_connection = pool.get()?; | |
108 | + | |
109 | + match markdowns . filter(name.eq(&item.name)) | |
110 | + . first::<Markdown>(&db_connection) | |
111 | + { | |
112 | + Ok(result) => Ok(_Action::Found(result)), | |
113 | + Err(_) => { | |
114 | + let now = chrono::Local::now().naive_local(); | |
115 | + let new_markdown = MarkdownNew { | |
116 | + name: &item.name, | |
117 | + content: &item.content, | |
118 | + number_of_versions: item.number_of_versions, | |
119 | + date_created: &format!("{}", now), | |
120 | + date_updated: &format!("{}", now), | |
121 | + }; | |
122 | + | |
123 | + Ok(_Action::Created(db_connection.transaction(|| { | |
124 | + insert_into(markdowns) . values(&new_markdown) | |
125 | + . execute(&db_connection)?; | |
126 | + | |
127 | + markdowns . order(id.desc()) | |
128 | + . first::<Markdown>(&db_connection) | |
129 | + })?)) | |
130 | + } | |
131 | + } | |
132 | +} | |
133 | + | |
134 | +pub(crate) fn get_markdowns(pool: Arc<Pool>) -> Result<Vec<Markdown>> | |
135 | +{ | |
136 | + use crate::schema::markdowns::dsl::*; | |
137 | + let db_connection = pool.get()?; | |
138 | + Ok(markdowns.load::<Markdown>(&db_connection)?) | |
139 | +} | |
140 | + | |
141 | +pub(crate) fn get_markdown( pool: Arc<Pool> | |
142 | + , ident: &str | |
143 | + , patch: Option<i32> ) -> Result<Markdown> | |
144 | +{ | |
145 | + use crate::schema::markdowns::dsl::*; | |
146 | + use crate::schema::markdown_diffs::dsl::*; | |
147 | + | |
148 | + let db_connection = pool.get()?; | |
149 | + | |
150 | + let mut markdown = markdowns | |
151 | + . filter(name.eq(ident)) | |
152 | + . first::<Markdown>(&db_connection)?; | |
153 | + | |
154 | + if let Some(patch) = patch { | |
155 | + let result = markdown_diffs | |
156 | + . filter(markdown_id.eq(markdown.id)) | |
157 | + . filter(diff_id.ge(patch)) | |
158 | + . order(diff_id.desc()) | |
159 | + . load::<MarkdownDiff>(&db_connection)?; | |
160 | + | |
161 | + for patch in result { | |
162 | + let patch_data = patch.get_diff_as_string()?; | |
163 | + let decomp = Patch::from_str(&patch_data)?; | |
164 | + | |
165 | + markdown.content = apply(&markdown.content, &decomp).unwrap(); | |
166 | + markdown.date_updated = patch.date_created; | |
167 | + } | |
168 | + }; | |
169 | + | |
170 | + Ok(markdown) | |
171 | +} | |
172 | + | |
173 | +pub(crate) fn get_patches( pool: Arc<Pool> | |
174 | + , ident: &str ) -> Result<Vec<MarkdownDiffJson>> { | |
175 | + use crate::schema::markdowns::dsl::*; | |
176 | + use crate::schema::markdown_diffs::dsl::*; | |
177 | + | |
178 | + let db_connection = pool.get()?; | |
179 | + | |
180 | + let markdown = markdowns | |
181 | + . filter(name.eq(ident)) | |
182 | + . first::<Markdown>(&db_connection)?; | |
183 | + | |
184 | + Ok ( markdown_diffs . filter(markdown_id.eq(markdown.id)) | |
185 | + . order(diff_id.desc()) | |
186 | + . load::<MarkdownDiff>(&db_connection)? | |
187 | + . iter() | |
188 | + . map(|d| MarkdownDiffJson::from(d)) | |
189 | + . collect() ) | |
190 | +} | |
191 | + | |
192 | +pub(crate) fn _delete_markdown( pool: Arc<Pool> | |
193 | + , ident: i32 ) -> Result<usize> | |
194 | +{ | |
195 | + use crate::schema::markdowns::dsl::*; | |
196 | + let db_connection = pool.get()?; | |
197 | + Ok(delete(markdowns.find(ident)).execute(&db_connection)?) | |
198 | +} | |
199 | + | |
200 | +pub(crate) fn update_markdown( pool: Arc<Pool> | |
201 | + , ident: String | |
202 | + , mut item: MarkdownJson ) -> Result<Markdown> | |
203 | +{ | |
204 | + use crate::schema::markdowns::dsl::*; | |
205 | + use crate::schema::markdown_diffs::dsl::*; | |
206 | + let db_connection = pool.get()?; | |
207 | + let mut markdown = markdowns | |
208 | + . filter(name.eq(ident)) | |
209 | + . first::<Markdown>(&db_connection)?; | |
210 | + | |
211 | + let patch = format!( "{}", create_patch( item.content.as_str() | |
212 | + , markdown.content.as_str() )); | |
213 | + let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best()); | |
214 | + encoder.write_all(patch.as_bytes()).unwrap(); | |
215 | + let compressed = encoder.finish().unwrap(); | |
216 | + | |
217 | + let last_diff = match markdown_diffs . filter(markdown_id.eq(markdown.id)) | |
218 | + . order(diff_id.desc()) | |
219 | + . first::<MarkdownDiff>(&db_connection) | |
220 | + { | |
221 | + Ok(result) => (result.diff_id, Some(result)), | |
222 | + Err(_) => (0, None), | |
223 | + }; | |
224 | + | |
225 | + let now = chrono::Local::now().naive_local(); | |
226 | + let new_markdown_diff = MarkdownDiffNew { | |
227 | + markdown_id: markdown.id, | |
228 | + diff_id: last_diff.0 + 1, | |
229 | + diff: compressed.as_ref(), | |
230 | + date_created: markdown.date_updated.as_str(), | |
231 | + }; | |
232 | + | |
233 | + item.date_updated = format!("{}", now); | |
234 | + item.number_of_versions = item.number_of_versions + 1; | |
235 | + | |
236 | + db_connection.transaction::<_, Error, _>(|| { | |
237 | + insert_into(markdown_diffs) . values(&new_markdown_diff) | |
238 | + . execute(&db_connection)?; | |
239 | + | |
240 | + update(&markdown).set(MarkdownChange::from(&item)).execute(&db_connection)?; | |
241 | + | |
242 | + Ok(()) | |
243 | + }).unwrap(); | |
244 | + | |
245 | + markdown.name = item.name; | |
246 | + markdown.content = item.content; | |
247 | + markdown.number_of_versions = item.number_of_versions; | |
248 | + markdown.date_updated = item.date_updated; | |
249 | + | |
250 | + Ok(markdown) | |
251 | +} | ... | ... |
server/src/models/mod.rs
0 → 100644
1 | 1 | use std::sync::Arc; |
2 | 2 | use crate::schema::*; |
3 | -use crate::error::Error; | |
3 | +use crate::error::*; | |
4 | 4 | use crate::Pool; |
5 | 5 | use diesel::prelude::*; |
6 | 6 | use diesel::{ |
... | ... | @@ -40,7 +40,7 @@ pub(crate) enum Action { |
40 | 40 | |
41 | 41 | |
42 | 42 | pub(crate) fn create_user( pool: Arc<Pool> |
43 | - , item: UserJson ) -> Result<Action, Error> { | |
43 | + , item: UserJson ) -> Result<Action> { | |
44 | 44 | use crate::schema::users::dsl::*; |
45 | 45 | let db_connection = pool.get()?; |
46 | 46 | |
... | ... | @@ -67,7 +67,7 @@ pub(crate) fn create_user( pool: Arc<Pool> |
67 | 67 | } |
68 | 68 | } |
69 | 69 | |
70 | -pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>, Error> | |
70 | +pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>> | |
71 | 71 | { |
72 | 72 | use crate::schema::users::dsl::*; |
73 | 73 | let db_connection = pool.get()?; |
... | ... | @@ -75,7 +75,7 @@ pub(crate) fn get_users(pool: Arc<Pool>) -> Result<Vec<User>, Error> |
75 | 75 | } |
76 | 76 | |
77 | 77 | pub(crate) fn get_user( pool: Arc<Pool> |
78 | - , ident: i32 ) -> Result<User, Error> | |
78 | + , ident: i32 ) -> Result<User> | |
79 | 79 | { |
80 | 80 | use crate::schema::users::dsl::*; |
81 | 81 | let db_connection = pool.get()?; |
... | ... | @@ -83,7 +83,7 @@ pub(crate) fn get_user( pool: Arc<Pool> |
83 | 83 | } |
84 | 84 | |
85 | 85 | pub(crate) fn delete_user( pool: Arc<Pool> |
86 | - , ident: i32 ) -> Result<usize, Error> | |
86 | + , ident: i32 ) -> Result<usize> | |
87 | 87 | { |
88 | 88 | use crate::schema::users::dsl::*; |
89 | 89 | let db_connection = pool.get()?; |
... | ... | @@ -92,7 +92,7 @@ pub(crate) fn delete_user( pool: Arc<Pool> |
92 | 92 | |
93 | 93 | pub(crate) fn update_user( pool: Arc<Pool> |
94 | 94 | , ident: i32 |
95 | - , item: UserJson ) -> Result<User, Error> | |
95 | + , item: UserJson ) -> Result<User> | |
96 | 96 | { |
97 | 97 | use crate::schema::users::dsl::*; |
98 | 98 | let db_connection = pool.get()?; | ... | ... |
server/src/routes/markdown.rs
0 → 100644
1 | +use crate::models::markdown; | |
2 | +use crate::Pool; | |
3 | + | |
4 | +use actix_web::{Error, HttpResponse, web}; | |
5 | +use anyhow::Result; | |
6 | +use artshop_common::types::MarkdownJson; | |
7 | +use serde::Deserialize; | |
8 | + | |
9 | +#[derive(Debug, Deserialize)] | |
10 | +pub struct Patchset { | |
11 | + patch: Option<i32>, | |
12 | +} | |
13 | + | |
14 | +pub async fn get_markdowns(pool: web::Data<Pool>) | |
15 | + -> Result<HttpResponse, Error> | |
16 | +{ | |
17 | + Ok( web::block(move || markdown::get_markdowns(pool.into_inner())) | |
18 | + . await | |
19 | + . map(|markdowns| HttpResponse::Ok().json(markdowns)) | |
20 | + . map_err(|_| HttpResponse::InternalServerError())? | |
21 | + ) | |
22 | +} | |
23 | + | |
24 | +pub async fn get_markdown( pool: web::Data<Pool> | |
25 | + , name: web::Path<String> | |
26 | + , patch: web::Query<Patchset> | |
27 | + ) -> Result<HttpResponse, Error> | |
28 | +{ | |
29 | + let pool = pool.into_inner(); | |
30 | + let name = name.into_inner(); | |
31 | + let patch = patch.into_inner(); | |
32 | + | |
33 | + Ok( web::block(move || markdown::get_markdown(pool, name.as_str(), patch.patch)) | |
34 | + . await | |
35 | + . map(|markdowns| HttpResponse::Ok().json(markdowns)) | |
36 | + . map_err(|_| HttpResponse::InternalServerError())? | |
37 | + ) | |
38 | +} | |
39 | + | |
40 | +pub async fn get_patches( pool: web::Data<Pool> | |
41 | + , name: web::Path<String> | |
42 | + ) -> Result<HttpResponse, Error> { | |
43 | + let pool = pool.into_inner(); | |
44 | + let name = name.into_inner(); | |
45 | + | |
46 | + Ok( web::block(move || markdown::get_patches(pool, name.as_str())) | |
47 | + . await | |
48 | + . map(|patches| HttpResponse::Ok().json(patches)) | |
49 | + . map_err(|_| HttpResponse::InternalServerError())? | |
50 | + ) | |
51 | +} | |
52 | + | |
53 | +pub async fn update_markdown( pool: web::Data<Pool> | |
54 | + , name: web::Path<String> | |
55 | + , item: web::Json<MarkdownJson> ) | |
56 | + -> Result<HttpResponse, Error> | |
57 | +{ | |
58 | + let pool = pool.into_inner(); | |
59 | + let name = name.into_inner(); | |
60 | + let item = item.into_inner(); | |
61 | + | |
62 | + Ok(web::block(move || markdown::update_markdown(pool, name, item)) | |
63 | + . await | |
64 | + . map(|markdown| HttpResponse::Ok().json(markdown)) | |
65 | + . map_err(|_| HttpResponse::InternalServerError())?) | |
66 | +} | ... | ... |
server/src/routes/mod.rs
0 → 100644
server/src/routes/other.rs
0 → 100644
1 | +use actix_web::Error; | |
2 | +use anyhow::Result; | |
3 | + | |
4 | +pub async fn root() -> Result<actix_files::NamedFile, Error> { | |
5 | + Ok(actix_files::NamedFile::open("static/index.html")?) | |
6 | +} | |
7 | + | |
8 | +pub async fn apidoc() -> Result<actix_files::NamedFile, Error> { | |
9 | + Ok(actix_files::NamedFile::open("static/api.html")?) | |
10 | +} | |
11 | + | |
12 | +pub async fn p404() -> Result<actix_files::NamedFile, Error> { | |
13 | + Ok(actix_files::NamedFile::open("static/404.html")?) | |
14 | +} | |
15 | + | |
16 | +pub async fn favicon() -> Result<actix_files::NamedFile, Error> { | |
17 | + Ok(actix_files::NamedFile::open("static/favicon.ico")?) | |
18 | +} | ... | ... |
1 | -use crate::models::{self, Action}; | |
1 | +use crate::models::user::{self, Action}; | |
2 | 2 | use crate::Pool; |
3 | 3 | |
4 | 4 | use actix_web::{Error, HttpResponse, web}; |
5 | 5 | use anyhow::Result; |
6 | 6 | |
7 | -pub async fn root() -> Result<actix_files::NamedFile, Error> { | |
8 | - Ok(actix_files::NamedFile::open("static/index.html")?) | |
9 | -} | |
10 | - | |
11 | -pub async fn p404() -> Result<actix_files::NamedFile, Error> { | |
12 | - Ok(actix_files::NamedFile::open("static/404.html")?) | |
13 | -} | |
14 | - | |
15 | -pub async fn favicon() -> Result<actix_files::NamedFile, Error> { | |
16 | - Ok(actix_files::NamedFile::open("static/favicon.ico")?) | |
17 | -} | |
18 | - | |
19 | 7 | pub async fn create_user( pool: web::Data<Pool> |
20 | - , item: web::Json<models::UserJson> ) | |
8 | + , item: web::Json<user::UserJson> ) | |
21 | 9 | -> Result<HttpResponse, Error> |
22 | 10 | { |
23 | 11 | let pool = pool.into_inner(); |
24 | 12 | let item = item.into_inner(); |
25 | 13 | |
26 | - Ok(web::block(move || models::create_user(pool, item)) | |
14 | + Ok(web::block(move || user::create_user(pool, item)) | |
27 | 15 | . await |
28 | 16 | . map(|action| { |
29 | 17 | match action { |
... | ... | @@ -36,7 +24,7 @@ pub async fn create_user( pool: web::Data<Pool> |
36 | 24 | pub async fn get_users(pool: web::Data<Pool>) |
37 | 25 | -> Result<HttpResponse, Error> |
38 | 26 | { |
39 | - Ok(web::block(move || models::get_users(pool.into_inner())) | |
27 | + Ok(web::block(move || user::get_users(pool.into_inner())) | |
40 | 28 | . await |
41 | 29 | . map(|users| HttpResponse::Ok().json(users)) |
42 | 30 | . map_err(|_| HttpResponse::InternalServerError())?) |
... | ... | @@ -48,7 +36,7 @@ pub async fn get_user(pool: web::Data<Pool>, id: web::Path<i32>) |
48 | 36 | let pool = pool.into_inner(); |
49 | 37 | let id = id.into_inner(); |
50 | 38 | |
51 | - Ok(web::block(move || models::get_user(pool, id)) | |
39 | + Ok(web::block(move || user::get_user(pool, id)) | |
52 | 40 | . await |
53 | 41 | . map(|user| HttpResponse::Ok().json(user)) |
54 | 42 | . map_err(|_| HttpResponse::InternalServerError())?) |
... | ... | @@ -60,7 +48,7 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>) |
60 | 48 | let pool = pool.into_inner(); |
61 | 49 | let id = id.into_inner(); |
62 | 50 | |
63 | - Ok(web::block(move || models::delete_user(pool, id)) | |
51 | + Ok(web::block(move || user::delete_user(pool, id)) | |
64 | 52 | . await |
65 | 53 | . map(|_| HttpResponse::NoContent().finish()) |
66 | 54 | . map_err(|_| HttpResponse::InternalServerError())?) |
... | ... | @@ -68,14 +56,14 @@ pub async fn delete_user(pool: web::Data<Pool>, id: web::Path<i32>) |
68 | 56 | |
69 | 57 | pub async fn update_user( pool: web::Data<Pool> |
70 | 58 | , id: web::Path<i32> |
71 | - , item: web::Json<models::UserJson> ) | |
59 | + , item: web::Json<user::UserJson> ) | |
72 | 60 | -> Result<HttpResponse, Error> |
73 | 61 | { |
74 | 62 | let pool = pool.into_inner(); |
75 | 63 | let id = id.into_inner(); |
76 | 64 | let item = item.into_inner(); |
77 | 65 | |
78 | - Ok(web::block(move || models::update_user(pool, id, item)) | |
66 | + Ok(web::block(move || user::update_user(pool, id, item)) | |
79 | 67 | . await |
80 | 68 | . map(|user| HttpResponse::Ok().json(user)) |
81 | 69 | . map_err(|_| HttpResponse::InternalServerError())?) | ... | ... |
1 | 1 | table! { |
2 | + markdown_diffs (markdown_id, diff_id) { | |
3 | + markdown_id -> Integer, | |
4 | + diff_id -> Integer, | |
5 | + diff -> Binary, | |
6 | + date_created -> Text, | |
7 | + } | |
8 | +} | |
9 | + | |
10 | +table! { | |
11 | + markdowns (id) { | |
12 | + id -> Integer, | |
13 | + name -> Text, | |
14 | + content -> Text, | |
15 | + number_of_versions -> Integer, | |
16 | + date_created -> Text, | |
17 | + date_updated -> Text, | |
18 | + } | |
19 | +} | |
20 | + | |
21 | +table! { | |
2 | 22 | users (id) { |
3 | 23 | id -> Integer, |
4 | 24 | name -> Text, |
... | ... | @@ -6,3 +26,9 @@ table! { |
6 | 26 | date_created -> Text, |
7 | 27 | } |
8 | 28 | } |
29 | + | |
30 | +allow_tables_to_appear_in_same_query!( | |
31 | + markdown_diffs, | |
32 | + markdowns, | |
33 | + users, | |
34 | +); | ... | ... |
static/api.html
0 → 100644
1 | +<!doctype html> | |
2 | +<html lang="en"> | |
3 | + <head> | |
4 | + <meta charset="utf-8"> | |
5 | + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |
6 | + <title>Elements in HTML</title> | |
7 | + <!-- Embed elements Elements via Web Component --> | |
8 | + <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script> | |
9 | + <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css"> | |
10 | + </head> | |
11 | + <body> | |
12 | + | |
13 | + <elements-api | |
14 | + apiDescriptionUrl="/static/openapi.yaml" | |
15 | + router="hash" | |
16 | + layout="sidebar" | |
17 | + /> | |
18 | + | |
19 | + </body> | |
20 | +</html> | ... | ... |
static/openapi.yaml
0 → 100644
1 | +openapi: 3.0.0 | |
2 | +info: | |
3 | + version: 0.0.1 | |
4 | + title: Artshop API | |
5 | + description: A first simple try do document out api | |
6 | +paths: | |
7 | + /api/v0/markdowns: | |
8 | + get: | |
9 | + description: Returns a list of all available markdown documents. | |
10 | + responses: | |
11 | + '200': | |
12 | + description: Successful response | ... | ... |
... | ... | @@ -12,13 +12,15 @@ crate-type = ["cdylib", "rlib"] |
12 | 12 | default = ["console_error_panic_hook"] |
13 | 13 | |
14 | 14 | [dependencies] |
15 | +artshop-common = { path = "../common" } | |
16 | +katex = { version = "0.4", default-features = false, features = ["wasm-js"] } | |
15 | 17 | pulldown-cmark = "0.9" |
16 | 18 | console_log = "^0.1" |
17 | 19 | log = "^0.4" |
18 | 20 | serde = { version = "^1.0", features = ["derive"] } |
19 | 21 | serde_json = "^1.0" |
20 | 22 | wasm-bindgen = "^0.2" |
21 | -wasm-bindgen-futures = "0.4" | |
23 | +wasm-bindgen-futures = "^0.4" | |
22 | 24 | |
23 | 25 | # The `console_error_panic_hook` crate provides better debugging of panics by |
24 | 26 | # logging them with `console.error`. This is great for development, but requires |
... | ... | @@ -26,6 +28,7 @@ wasm-bindgen-futures = "0.4" |
26 | 28 | # code size when deploying. |
27 | 29 | console_error_panic_hook = { version = "0.1.6", optional = true } |
28 | 30 | wee_alloc = { version = "0.4.2", optional = true } |
31 | +js-sys = "^0.3" | |
29 | 32 | |
30 | 33 | [dependencies.mogwai] |
31 | 34 | version = "^0.5" |
... | ... | @@ -38,6 +41,7 @@ features = [ |
38 | 41 | "Headers", |
39 | 42 | "HtmlElement", |
40 | 43 | "HtmlInputElement", |
44 | + "MouseEvent", | |
41 | 45 | "Node", |
42 | 46 | "Request", |
43 | 47 | "RequestInit", | ... | ... |
ui/src/api/markdown.rs
0 → 100644
1 | +use std::fmt::Display; | |
2 | + | |
3 | +use artshop_common::types::{MarkdownJson, MarkdownDiffJson}; | |
4 | + | |
5 | +use super::super::error::*; | |
6 | +use super::super::client::Client; | |
7 | + | |
8 | +#[derive(Debug, Clone)] | |
9 | +pub struct Markdown { | |
10 | + client: Client, | |
11 | + pub json: MarkdownJson, | |
12 | +} | |
13 | + | |
14 | +impl Markdown { | |
15 | + pub(crate) async fn new(name :&str) -> Result<Markdown> { | |
16 | + let client = Client::new()?; | |
17 | + let api_uri = format!("/api/v0/markdowns/{}", name); | |
18 | + let (response, data) = client.get(&api_uri).await?; | |
19 | + | |
20 | + match response.status() { | |
21 | + 200 => Ok(Self { client | |
22 | + , json: serde_json::from_str(data.as_str())? }), | |
23 | + status => Err(Self::status_error(status)), | |
24 | + } | |
25 | + } | |
26 | + | |
27 | + pub(crate) async fn read( &mut self | |
28 | + , patch :Option<i32> | |
29 | + ) -> Result<&Markdown> | |
30 | + { | |
31 | + let api_uri = match patch { | |
32 | + Some(i) => format!( "/api/v0/markdowns/{}?patch={}" | |
33 | + , self.json.name, i ), | |
34 | + None => format!("/api/v0/markdowns/{}", self.json.name), | |
35 | + }; | |
36 | + let (response, data) = self.client.get(&api_uri).await?; | |
37 | + | |
38 | + match response.status() { | |
39 | + 200 => { | |
40 | + self.json = serde_json::from_str(data.as_str())?; | |
41 | + Ok(self) | |
42 | + }, | |
43 | + status => Err(Self::status_error(status)), | |
44 | + } | |
45 | + } | |
46 | + | |
47 | + pub(crate) async fn save(&self) -> Result<&Markdown> { | |
48 | + let url = format!("/api/v0/markdowns/{}", self.json.name); | |
49 | + let data = serde_json::to_string(&self.json)?; | |
50 | + | |
51 | + let response = self.client.put(url.as_str(), data.as_str()).await?; | |
52 | + | |
53 | + match response.status() { | |
54 | + 200 => Ok(self), | |
55 | + status => Err(Self::status_error(status)), | |
56 | + } | |
57 | + } | |
58 | + | |
59 | + pub(crate) async fn patches(&self) -> Result<Vec<MarkdownDiffJson>> { | |
60 | + let url = format!("/api/v0/markdowns/{}/patches", self.json.name); | |
61 | + let (response, data) = self.client.get(url.as_str()).await?; | |
62 | + | |
63 | + match response.status() { | |
64 | + 200 => Ok(serde_json::from_str(&data)?), | |
65 | + status => Err(Self::status_error(status)), | |
66 | + } | |
67 | + } | |
68 | + | |
69 | + pub(crate) fn _to_html_string(&self) -> String { | |
70 | + use pulldown_cmark::{Parser, Options, html}; | |
71 | + | |
72 | + let mut html_out = String::new(); | |
73 | + let parser = Parser::new_ext(&self.json.content, Options::all()); | |
74 | + | |
75 | + html::push_html(&mut html_out, parser); | |
76 | + html_out | |
77 | + } | |
78 | + | |
79 | + | |
80 | + fn status_error<I: Display>(status :I) -> Error { | |
81 | + let err_str = format!("Invalid response status: {}", status); | |
82 | + Error::from(err_str.as_str()) | |
83 | + } | |
84 | +} | ... | ... |
ui/src/api/mod.rs
0 → 100644
1 | +pub(crate) mod markdown; | ... | ... |
ui/src/client.rs
0 → 100644
1 | +use js_sys::JsString; | |
2 | +use mogwai::prelude::*; | |
3 | +use wasm_bindgen::prelude::*; | |
4 | +use web_sys::{Window, window, Response, Request, RequestInit, RequestMode}; | |
5 | +use super::error::*; | |
6 | + | |
7 | +use std::result::Result as StdResult; | |
8 | + | |
9 | +#[derive(Debug, Clone)] | |
10 | +pub(crate) struct Client { | |
11 | + window :Window, | |
12 | +} | |
13 | + | |
14 | +type ReqGetter = fn(&str, &RequestInit) -> StdResult<Request, JsValue>; | |
15 | + | |
16 | +const REQUEST :ReqGetter = Request::new_with_str_and_init; | |
17 | + | |
18 | +impl Client { | |
19 | + pub fn new() -> Result<Self> { | |
20 | + const WINDOW_ERROR :&str = "Unable to get window instance"; | |
21 | + | |
22 | + Ok(Self { window: window() | |
23 | + . ok_or(Error::from(WINDOW_ERROR))? }) | |
24 | + } | |
25 | + | |
26 | + pub async fn get(&self, url :&str) -> Result<(Response, String)> { | |
27 | + let mut init = RequestInit::new(); | |
28 | + let request = REQUEST( &url | |
29 | + , init . method("GET") | |
30 | + . mode(RequestMode::Cors) )?; | |
31 | + | |
32 | + request . headers() | |
33 | + . set("Accept", "application/json")?; | |
34 | + | |
35 | + let response = JsFuture::from( self.window | |
36 | + . fetch_with_request(&request) ) | |
37 | + . await? | |
38 | + . dyn_into::<Response>()?; | |
39 | + | |
40 | + let data = JsFuture::from(response.text()?) | |
41 | + . await? | |
42 | + . dyn_into::<JsString>()?; | |
43 | + | |
44 | + Ok((response, String::from(data))) | |
45 | + } | |
46 | + | |
47 | + pub async fn put(&self, url :&str, data :&str) -> Result<Response> { | |
48 | + let mut init = RequestInit::new(); | |
49 | + let request = REQUEST( &url | |
50 | + , init . method("PUT") | |
51 | + . mode(RequestMode::Cors) | |
52 | + . body(Some(&data.into())) )?; | |
53 | + | |
54 | + request . headers() | |
55 | + . set("Content-Type", "application/json")?; | |
56 | + | |
57 | + let response = JsFuture::from( self.window | |
58 | + . fetch_with_request(&request)) | |
59 | + . await? | |
60 | + . dyn_into::<Response>()?; | |
61 | + | |
62 | + Ok(response) | |
63 | + } | |
64 | +} | ... | ... |
ui/src/data/icons/discard.rs
0 → 100644
1 | +use mogwai::prelude::*; | |
2 | +use super::NS; | |
3 | + | |
4 | +pub(crate) fn discard_icon() -> ViewBuilder<Dom> { | |
5 | + builder! { | |
6 | + <svg xmlns=NS viewBox="10.3 3.8 76.2 88.7" | |
7 | + style:width="1.5em" style:height="1.5em"> | |
8 | + <g xmlns=NS id="_x37_0"> | |
9 | + <polygon xmlns=NS | |
10 | + points="35.4,18.1 31.1,4.3 31.1,16.3 | |
11 | + 24.5,16.3 24.5,10.9 18.3,18.9 | |
12 | + 13.8,23.7 17,23.7 19.8,31.8 | |
13 | + 27.9,34.1 27.9,37.8 38.7,41.8 | |
14 | + 38.7,34.1 38.7,30.4 38.7,25.2 | |
15 | + 46.8,25.2 "/> | |
16 | + <path xmlns=NS | |
17 | + d="M75.7,39.9 h-8.3 h-40.5 h-8.3 h-3 v9.5 | |
18 | + v2 h4.7 c2.2,13.3,6.8,42.2,6.8,42.2 h40 | |
19 | + c0,0,4.6-27.6,7-42.2 h4.7 v-2 v-9.5 | |
20 | + H75.7z | |
21 | + M67.7,51.4 l-2.6,15.6 l-6.1-6.9 l8-8.7 | |
22 | + H67.7z | |
23 | + M63.7,75.6 l-1.3,7.9 l-2.9-3.2 | |
24 | + L63.7,75.6z | |
25 | + M56.6,77.2 | |
26 | + L50,70 l6.2-6.7 l6.4,7.3 | |
27 | + L56.6,77.2z | |
28 | + M52.4,51.4 h8.9 l-5.1,5.5 l-4.8-5.4 | |
29 | + C51.8,51.4,52.1,51.4,52.4,51.4z | |
30 | + M53.3,60 l-6.2,6.8 l-6.6-7.2 l6.4-6.9 | |
31 | + L53.3,60z | |
32 | + M30.5,74.9 l4.6,5 l-3.2,3.5 | |
33 | + L30.5,74.9z | |
34 | + M31.4,69.6 l6.2-6.8 l6.6,7.2 l-6.2,6.8 | |
35 | + L31.4,69.6z | |
36 | + M41.9,51.4 c0.1,0,0.2,0,0.3,0 l-4.7,5.1 | |
37 | + l-4.7-5.1 | |
38 | + H41.9z | |
39 | + M27.1,51.4 l7.6,8.3 l-5.7,6.2l-2.3-14.5 | |
40 | + H27.1z | |
41 | + M34.6,86.7 l3.3-3.6 l3.3,3.6 | |
42 | + H34.6z | |
43 | + M47.1,86.7 l-6.2-6.8 l6.2-6.8 l6.6,7.2 | |
44 | + l-5.8,6.4 | |
45 | + H47.1z | |
46 | + M53.7,86.7 l2.9-3.2 l3,3.2 | |
47 | + H53.7z"/> | |
48 | + </g> | |
49 | + </svg> | |
50 | + } | |
51 | +} | ... | ... |
ui/src/data/icons/edit.rs
0 → 100644
1 | +use mogwai::prelude::*; | |
2 | +use super::NS; | |
3 | + | |
4 | +pub(crate) fn edit_icon() -> ViewBuilder<Dom> { | |
5 | + builder! { | |
6 | + <svg xmlns=NS viewBox="0 0 220.001 220.001" | |
7 | + style:width="1.5em" style:height="1.5em"> | |
8 | + <g xmlns=NS> | |
9 | + <polygon xmlns=NS | |
10 | + points="0,220 59.34,213.86 6.143,160.661"/> | |
11 | + <path xmlns=NS | |
12 | + d="M132.018,34.787 l53.197,53.197 | |
13 | + L69.568,203.631 | |
14 | + L16.37,150.434 | |
15 | + L132.018,34.787z | |
16 | + M212.696,60.502 | |
17 | + c9.738,-9.738,9.742,-25.527,0,-35.268 | |
18 | + l-17.93,-17.93 | |
19 | + c-9.738,-9.74,-25.529,-9.738,-35.268,0 | |
20 | + l-17.346,17.347 l53.199,53.196 | |
21 | + L212.696,60.502z"/> | |
22 | + </g> | |
23 | + </svg> | |
24 | + } | |
25 | +} | ... | ... |
ui/src/data/icons/mod.rs
0 → 100644
1 | +const NS :&str = "http://www.w3.org/2000/svg"; | |
2 | + | |
3 | +pub(crate) mod discard; | |
4 | +pub(crate) mod edit; | |
5 | +pub(crate) mod save; | |
6 | +pub(crate) mod select; | |
7 | + | |
8 | +pub(crate) use discard::discard_icon; | |
9 | +pub(crate) use edit::edit_icon; | |
10 | +pub(crate) use save::save_icon; | |
11 | +pub(crate) use select::select_icon; | ... | ... |
ui/src/data/icons/save.rs
0 → 100644
1 | +use mogwai::prelude::*; | |
2 | +use super::NS; | |
3 | + | |
4 | +pub(crate) fn save_icon() -> ViewBuilder<Dom> { | |
5 | + builder! { | |
6 | + <svg xmlns=NS viewBox="0 -1 32 32" | |
7 | + style:width="1.5em" style:height="1.5em" | |
8 | + style:enable_background="new 0 0 32 32" | |
9 | + style:fill="none" | |
10 | + style:stroke="#000" | |
11 | + style:stroke_width="2" | |
12 | + style:stroke_linecap="round" | |
13 | + style:stroke_linejoin="round" | |
14 | + style:stroke_miterlimit="10"> | |
15 | + <ellipse xmlns=NS | |
16 | + cx="14" cy="8" rx="10" ry="5"/> | |
17 | + <line xmlns=NS | |
18 | + x1="24" y1="16" x2="24" y2="8"/> | |
19 | + <path xmlns=NS | |
20 | + d="M4,8 v8 c0,2.8,4.5,5,10,5 | |
21 | + c1.2,0,2.3-0.1,3.4-0.3"/> | |
22 | + <path xmlns=NS | |
23 | + d="M4,16 v8 c0,2.8,4.5,5,10,5 | |
24 | + c2,0,3.8-0.3,5.3-0.8"/> | |
25 | + <circle xmlns=NS | |
26 | + cx="24" cy="23" r="7"/> | |
27 | + <line xmlns=NS | |
28 | + x1="24" y1="16" x2="24" y2="26"/> | |
29 | + <polyline xmlns=NS | |
30 | + points="21,23 24,26 27,23"/> | |
31 | + </svg> | |
32 | + } | |
33 | +} | ... | ... |
ui/src/data/icons/select.rs
0 → 100644
1 | +use mogwai::prelude::*; | |
2 | +use super::NS; | |
3 | + | |
4 | +pub(crate) fn select_icon() -> ViewBuilder<Dom> { | |
5 | + builder! { | |
6 | + <svg xmlns=NS viewBox="0 -1 24 24" | |
7 | + style:width="1.5em" style:height="1.5em" | |
8 | + style:fill="none"> | |
9 | + <path xmlns=NS | |
10 | + style:fill="black" | |
11 | + d="M9.17154 11.508 | |
12 | + L7.75732 10.0938 | |
13 | + L12 5.85113 | |
14 | + L16.2426 10.0938 | |
15 | + L14.8284 11.508 | |
16 | + L12 8.67956 | |
17 | + L9.17154 11.508Z"/> | |
18 | + <path xmlns=NS | |
19 | + style:fill="black" | |
20 | + d="M9.17154 12.492 | |
21 | + L7.75732 13.9062 | |
22 | + L12 18.1489 | |
23 | + L16.2426 13.9062 | |
24 | + L14.8284 12.492 | |
25 | + L12 15.3204 | |
26 | + L9.17154 12.492Z"/> | |
27 | + <path xmlns=NS | |
28 | + style:fill="black" | |
29 | + style:fill_rule="evenodd" | |
30 | + style:clip_rule="evenodd" | |
31 | + d="M1 5 | |
32 | + C1 2.79086 2.79086 1 5 1 | |
33 | + H19 | |
34 | + C21.2091 1 23 2.79086 23 5 | |
35 | + V19 | |
36 | + C23 21.2091 21.2091 23 19 23 | |
37 | + H5 | |
38 | + C2.79086 23 1 21.2091 1 19 | |
39 | + V5Z | |
40 | + M5 3 | |
41 | + H19 | |
42 | + C20.1046 3 21 3.89543 21 5 | |
43 | + V19 | |
44 | + C21 20.1046 20.1046 21 19 21 | |
45 | + H5 | |
46 | + C3.89543 21 3 20.1046 3 19 | |
47 | + V5 | |
48 | + C3 3.89543 3.89543 3 5 3Z"/> | |
49 | + </svg> | |
50 | + } | |
51 | +} | ... | ... |
ui/src/data/mod.rs
0 → 100644
1 | +pub(crate) mod icons; | ... | ... |
ui/src/error.rs
0 → 100644
1 | +use std::fmt::Display; | |
2 | +use wasm_bindgen::prelude::*; | |
3 | + | |
4 | +type ParentError = Option<Box<dyn std::error::Error>>; | |
5 | + | |
6 | +#[derive(Debug)] | |
7 | +pub(crate) struct Error { | |
8 | + source: ParentError, | |
9 | + message: String, | |
10 | +} | |
11 | + | |
12 | +pub(crate) type Result<T> = std::result::Result<T, Error>; | |
13 | + | |
14 | +impl std::error::Error for Error { | |
15 | + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
16 | + self.source.as_deref() | |
17 | + } | |
18 | +} | |
19 | + | |
20 | +impl Display for Error { | |
21 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
22 | + match self { | |
23 | + Error { source: Some(source), message } => | |
24 | + write!(f, "{}: {}", message, source), | |
25 | + Error { source: None, message } => write!(f, "{}", message), | |
26 | + } | |
27 | + } | |
28 | +} | |
29 | + | |
30 | +impl From<&str> for Error { | |
31 | + fn from(message: &str) -> Self { | |
32 | + Self { source: None, message: String::from(message) } | |
33 | + } | |
34 | +} | |
35 | + | |
36 | +impl From<JsValue> for Error { | |
37 | + fn from(source: JsValue) -> Self { | |
38 | + let source = js_sys::Error::from(source); | |
39 | + | |
40 | + let message = format!("[{}] {}", source.name(), source.message()); | |
41 | + let source = Error::from(message.as_str()); | |
42 | + | |
43 | + Self { source: Some(Box::new(source)) | |
44 | + , message: String::from("WebSys Error") | |
45 | + } | |
46 | + } | |
47 | +} | |
48 | + | |
49 | +impl From<serde_json::Error> for Error { | |
50 | + fn from(source: serde_json::Error) -> Self { | |
51 | + Self { source: Some(Box::new(source)) | |
52 | + , message: String::from("Serde Error") | |
53 | + } | |
54 | + } | |
55 | +} | ... | ... |
1 | +mod api; | |
1 | 2 | mod data; |
3 | +mod error; | |
4 | +mod client; | |
2 | 5 | |
6 | +use api::markdown::Markdown; | |
7 | +use data::icons::*; | |
3 | 8 | use log::Level; |
4 | 9 | use mogwai::prelude::*; |
5 | -use web_sys::{RequestInit, RequestMode, Request, Response, console}; | |
6 | 10 | use std::panic; |
7 | 11 | use wasm_bindgen::prelude::*; |
8 | 12 | |
9 | - | |
10 | 13 | #[derive(Clone)] |
11 | 14 | enum AppLogic { |
12 | 15 | Update, |
13 | 16 | Toggle, |
14 | -} | |
15 | - | |
16 | -fn md_to_html(source: &str) -> String { | |
17 | - use pulldown_cmark::{Parser, Options, html}; | |
18 | - | |
19 | - let parser = Parser::new_ext(source, Options::all()); | |
20 | - let mut html_output = String::new(); | |
21 | - html::push_html(&mut html_output, parser); | |
22 | - | |
23 | - html_output | |
17 | + Store, | |
18 | + Select, | |
19 | + Choose(Option<i32>), | |
20 | + Discard, | |
24 | 21 | } |
25 | 22 | |
26 | 23 | async fn editor_logic( mut rx_logic: broadcast::Receiver<AppLogic> |
27 | - , tx_view: broadcast::Sender<String> | |
28 | - , mut rx_dom: broadcast::Receiver<Dom> ) { | |
29 | - let window = web_sys::window().unwrap(); | |
30 | - let mut opts = RequestInit::new(); | |
31 | - opts.method("GET").mode(RequestMode::Cors); | |
32 | - let request = Request::new_with_str_and_init("/api/v0/users", &opts).unwrap(); | |
33 | - request.headers().set("Accept", "application/json").unwrap(); | |
34 | - | |
35 | - let response = JsFuture::from(window.fetch_with_request(&request)) | |
36 | - . await.unwrap() | |
37 | - . dyn_into::<Response>().unwrap(); | |
38 | - let data = JsFuture::from(response.json().unwrap()).await.unwrap(); | |
39 | - console::log_1(&data); | |
40 | - | |
24 | + , tx_logic: broadcast::Sender<AppLogic> | |
25 | + , tx_toggle: broadcast::Sender<bool> | |
26 | + , tx_patches: mpmc::Sender<ListPatch<ViewBuilder<Dom>>> | |
27 | + , mut rx_dom: broadcast::Receiver<Dom> ) | |
28 | +{ | |
41 | 29 | let dom = rx_dom.next().await.unwrap(); |
42 | 30 | let mut show_edit = false; |
31 | + let mut md = Markdown::new("md-example").await.unwrap(); | |
32 | + | |
33 | + let container = match dom.inner_read() { | |
34 | + Either::Left(dom_js) => | |
35 | + Some( ( dom_js | |
36 | + . to_owned() | |
37 | + . dyn_into::<Node>().unwrap() | |
38 | + . first_child().unwrap() | |
39 | + . first_child().unwrap() | |
40 | + . dyn_into::<HtmlElement>().unwrap() | |
41 | + , dom_js | |
42 | + . to_owned() | |
43 | + . dyn_into::<Node>().unwrap() | |
44 | + . child_nodes().get(1).unwrap() | |
45 | + . child_nodes().get(1).unwrap() | |
46 | + . dyn_into::<HtmlElement>().unwrap() )), | |
47 | + _ => None, | |
48 | + }; | |
49 | + | |
50 | + let cont_ref = container.as_ref(); | |
51 | + | |
52 | + let get_md = move || { | |
53 | + match cont_ref { | |
54 | + Some((md_cont, _)) => md_cont.inner_text(), | |
55 | + None => String::from(""), | |
56 | + } | |
57 | + }; | |
43 | 58 | |
44 | - fn get_md(dom: &Dom) -> String { | |
45 | - match dom.inner_read() { | |
46 | - Either::Left(dom_js) => dom_js . to_owned() | |
47 | - . dyn_into::<Node>().unwrap() | |
48 | - . first_child().unwrap() | |
49 | - . dyn_into::<HtmlElement>().unwrap() | |
50 | - . inner_text(), | |
51 | - _ => String::from(""), | |
59 | + let set_md = move |md :&str| { | |
60 | + match cont_ref { | |
61 | + Some((md_cont, _)) => md_cont.set_text_content(Some(md)), | |
62 | + None => (), | |
52 | 63 | } |
53 | - } | |
64 | + }; | |
54 | 65 | |
55 | - fn update(dom: &Dom) { | |
56 | - if let Either::Left(dom_js) = dom.inner_read() { | |
57 | - dom_js . to_owned() | |
58 | - . dyn_into::<Node>().unwrap() | |
59 | - . child_nodes().get(1).unwrap() | |
60 | - . child_nodes().get(1).unwrap() | |
61 | - . dyn_into::<HtmlElement>().unwrap() | |
62 | - . set_inner_html(md_to_html(get_md(dom).as_str()).as_str()) | |
63 | - }; | |
64 | - } | |
66 | + let update = move || { | |
67 | + match cont_ref { | |
68 | + Some((_, view_cont)) => { | |
69 | + use pulldown_cmark::{Parser, Options, html}; | |
70 | + | |
71 | + let mut html_out = String::new(); | |
72 | + let md = get_md(); | |
73 | + let parser = Parser::new_ext(&md, Options::all()); | |
65 | 74 | |
66 | - update(&dom); | |
75 | + html::push_html(&mut html_out, parser); | |
76 | + view_cont.set_inner_html(&html_out) | |
77 | + }, | |
78 | + None => (), | |
79 | + } | |
80 | + }; | |
81 | + | |
82 | + set_md(md.json.content.as_str()); | |
83 | + update(); | |
84 | + | |
85 | + /* play with katex ==== */ | |
86 | + let opts = katex::Opts::builder() | |
87 | + . output_type(katex::opts::OutputType::Mathml) | |
88 | + . build().unwrap(); | |
89 | + let formula1 = katex::render_with_opts("E = mc^2", &opts).unwrap(); | |
90 | + let formula2 = katex::render_with_opts("e^{i*\\pi} +1 = 0", &opts).unwrap(); | |
91 | + | |
92 | + if let Either::Left(dom_js) = dom.inner_read() { | |
93 | + dom_js . to_owned() | |
94 | + . dyn_into::<Node>().unwrap() | |
95 | + . child_nodes().get(1).unwrap() | |
96 | + . child_nodes().get(2).unwrap() | |
97 | + . dyn_into::<HtmlElement>().unwrap() | |
98 | + . set_inner_html(formula1.as_str()) | |
99 | + }; | |
100 | + | |
101 | + if let Either::Left(dom_js) = dom.inner_read() { | |
102 | + dom_js . to_owned() | |
103 | + . dyn_into::<Node>().unwrap() | |
104 | + . child_nodes().get(1).unwrap() | |
105 | + . child_nodes().get(3).unwrap() | |
106 | + . dyn_into::<HtmlElement>().unwrap() | |
107 | + . set_inner_html(formula2.as_str()) | |
108 | + }; | |
109 | + /* =========== */ | |
67 | 110 | |
68 | 111 | while let Some(msg) = rx_logic.next().await { |
69 | 112 | match msg { |
70 | - AppLogic::Update => update(&dom), | |
113 | + AppLogic::Store => { | |
114 | + md.json.content = get_md(); | |
115 | + md.save().await.unwrap(); | |
116 | + }, | |
117 | + AppLogic::Update => update(), | |
71 | 118 | AppLogic::Toggle => { |
72 | 119 | show_edit = ! show_edit; |
73 | - match show_edit { | |
74 | - true => tx_view . broadcast(String::from("block")) | |
75 | - . await.unwrap(), | |
76 | - false => tx_view . broadcast(String::from("none")) | |
77 | - . await.unwrap(), | |
78 | - }; | |
120 | + tx_toggle.broadcast(show_edit).await.unwrap(); | |
121 | + }, | |
122 | + AppLogic::Discard => { | |
123 | + set_md(md.json.content.as_str()); | |
124 | + update(); | |
125 | + }, | |
126 | + AppLogic::Select => { | |
127 | + let patches = md | |
128 | + . patches().await.unwrap() | |
129 | + . into_iter() | |
130 | + . map(|diff| { | |
131 | + let id = Some(diff.id); | |
132 | + let choose_filter = tx_logic | |
133 | + . sink() | |
134 | + . contra_map(move |_| AppLogic::Choose(id)); | |
135 | + builder! { | |
136 | + <li><button on:click=choose_filter | |
137 | + value=format!("{}", diff.id.to_owned())> | |
138 | + {diff.date_created.to_owned()} | |
139 | + </button></li> | |
140 | + }}); | |
141 | + let all = vec![builder! { | |
142 | + <li><button on:click=tx_logic.sink().contra_map(|_| AppLogic::Choose(None))> | |
143 | + "Current" | |
144 | + </button></li> | |
145 | + }].into_iter().chain(patches); | |
146 | + | |
147 | + let list_replace = ListPatch::splice(.., all); | |
148 | + tx_patches.send(list_replace).await.unwrap(); | |
149 | + }, | |
150 | + AppLogic::Choose(id) => { | |
151 | + md.read(id).await.unwrap(); | |
152 | + set_md(md.json.content.as_str()); | |
153 | + update(); | |
79 | 154 | }, |
80 | 155 | } |
81 | 156 | } |
82 | 157 | } |
83 | 158 | |
84 | 159 | fn editor_view( tx_logic: broadcast::Sender<AppLogic> |
85 | - , rx_view: broadcast::Receiver<String> | |
160 | + , rx_toggle: broadcast::Receiver<bool> | |
161 | + , rx_patches: mpmc::Receiver<ListPatch<ViewBuilder<Dom>>> | |
86 | 162 | , tx_dom: broadcast::Sender<Dom> |
87 | -) -> ViewBuilder<Dom> { | |
88 | - let ns = "http://www.w3.org/2000/svg"; | |
163 | + ) -> ViewBuilder<Dom> | |
164 | +{ | |
165 | + let input_filter = tx_logic | |
166 | + . sink() | |
167 | + . contra_map(|_| AppLogic::Update); | |
168 | + let store_filter = tx_logic | |
169 | + . sink() | |
170 | + . contra_map(|_| AppLogic::Store); | |
171 | + /* keep as example how to handle concrete events. == | |
172 | + . contra_filter_map(|e :DomEvent| { | |
173 | + if let Either::Left(e) = e.clone_inner() { | |
174 | + let e = e.dyn_into::<MouseEvent>().unwrap(); | |
175 | + match e.alt_key() { | |
176 | + true => Some(AppLogic::Store), | |
177 | + false => None | |
178 | + } | |
179 | + } else { | |
180 | + None | |
181 | + } | |
182 | + }); | |
183 | + == */ | |
184 | + let toggle_filter = tx_logic | |
185 | + . sink() | |
186 | + . contra_map(|_| AppLogic::Toggle); | |
187 | + let select_filter = tx_logic | |
188 | + . sink() | |
189 | + . contra_map(|_e| AppLogic::Select); | |
190 | + let discard_filter = tx_logic | |
191 | + . sink() | |
192 | + . contra_map(|_| AppLogic::Discard); | |
193 | + | |
194 | + let toggle_map = rx_toggle | |
195 | + . map(|t| match t { | |
196 | + true => String::from("block"), | |
197 | + false => String::from("none") }); | |
198 | + | |
89 | 199 | builder! { |
90 | 200 | <div class="input" |
91 | 201 | style:width="33%" |
92 | - on:input=tx_logic.sink().contra_map(|_| AppLogic::Update) | |
202 | + on:input=input_filter | |
93 | 203 | capture:view=tx_dom.sink()> |
94 | 204 | <div contenteditable="true" |
95 | 205 | style:cursor="text" |
96 | - style:display=("none", rx_view)> | |
97 | - <pre>{data::MD_EXAMPLE}</pre> | |
206 | + style:display=("none", toggle_map)> | |
207 | + <pre></pre> | |
98 | 208 | </div> |
99 | 209 | <div> |
100 | - <button on:click=tx_logic . sink() | |
101 | - . contra_map(|_| AppLogic::Toggle)> | |
102 | - <svg version="1.1" id="Capa_1" xmlns=ns | |
103 | - x="0px" y="0px" viewBox="0 0 220.001 220.001" | |
104 | - style:width="1.5em" style:height="1.5em"> | |
105 | - <g xmlns=ns> | |
106 | - <polygon xmlns=ns points="0,220 59.34,213.86 6.143,160.661"></polygon> | |
107 | - <path xmlns=ns d="M132.018,34.787l53.197,53.197L69.568,203.631L16.37, | |
108 | - 150.434L132.018,34.787z M212.696,60.502c9.738-9.738,9.742-25.527, | |
109 | - 0-35.268l-17.93-17.93c-9.738-9.74-25.529-9.738-35.268,0l-17.346, | |
110 | - 17.347l53.199,53.196L212.696,60.502z"></path> | |
111 | - </g> | |
112 | - </svg> | |
113 | - </button> | |
210 | + <div> | |
211 | + <div> | |
212 | + <button on:click=select_filter>{select_icon()}</button> | |
213 | + <ul patch:children=rx_patches> | |
214 | + </ul> | |
215 | + </div> | |
216 | + <button on:click=store_filter>{save_icon()}</button> | |
217 | + <button on:click=discard_filter>{discard_icon()}</button> | |
218 | + <button on:click=toggle_filter>{edit_icon()}</button> | |
219 | + </div> | |
220 | + <div></div> | |
221 | + <div></div> | |
114 | 222 | <div></div> |
115 | 223 | </div> |
116 | 224 | </div> |
... | ... | @@ -118,15 +226,18 @@ fn editor_view( tx_logic: broadcast::Sender<AppLogic> |
118 | 226 | } |
119 | 227 | |
120 | 228 | #[wasm_bindgen(start)] |
121 | -pub fn main() -> Result<(), JsValue> { | |
229 | +pub async fn main() -> Result<(), JsValue> { | |
122 | 230 | panic::set_hook(Box::new(console_error_panic_hook::hook)); |
123 | 231 | console_log::init_with_level(Level::Trace).unwrap(); |
124 | 232 | |
125 | 233 | let (tx_dom, rx_dom) = broadcast::bounded(1); |
126 | 234 | let (tx_logic, rx_logic) = broadcast::bounded(1); |
127 | - let (tx_view, rx_view) = broadcast::bounded(1); | |
128 | - let comp = Component::from( editor_view(tx_logic, rx_view, tx_dom) ) | |
129 | - . with_logic( editor_logic(rx_logic, tx_view, rx_dom) ); | |
235 | + let (tx_toggle, rx_toggle) = broadcast::bounded(1); | |
236 | + let (tx_patches, rx_patches) = mpmc::bounded(1); | |
237 | + | |
238 | + let view = editor_view(tx_logic.clone(), rx_toggle, rx_patches, tx_dom); | |
239 | + let logic = editor_logic(rx_logic, tx_logic, tx_toggle, tx_patches, rx_dom); | |
240 | + let comp = Component::from(view).with_logic(logic); | |
130 | 241 | |
131 | 242 | let page = Component::from(builder! {{comp}}); |
132 | 243 | page.build()?.run() | ... | ... |
Please
register
or
login
to post a comment