diff --git a/twenty-frontend/package-lock.json b/twenty-frontend/package-lock.json
index 1805ed7..b444173 100644
--- a/twenty-frontend/package-lock.json
+++ b/twenty-frontend/package-lock.json
@@ -14,11 +14,13 @@
"@types/luxon": "^3.3.7",
"@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6",
+ "@types/showdown": "^2.0.6",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"luxon": "^3.4.4",
"prettier": "^2.7.1",
"react-hook-form": "^7.49.2",
+ "showdown": "^2.1.0",
"ts-loader": "^9.3.1",
"ts-standard": "^12.0.1",
"typescript": "^4.8.2",
@@ -317,6 +319,12 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
+ "node_modules/@types/showdown": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz",
+ "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==",
+ "dev": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
@@ -4018,6 +4026,31 @@
"node": ">=8"
}
},
+ "node_modules/showdown": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz",
+ "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^9.0.0"
+ },
+ "bin": {
+ "showdown": "bin/showdown.js"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://www.paypal.me/tiviesantos"
+ }
+ },
+ "node_modules/showdown/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
diff --git a/twenty-frontend/package.json b/twenty-frontend/package.json
index 47f50cb..022b22f 100644
--- a/twenty-frontend/package.json
+++ b/twenty-frontend/package.json
@@ -9,11 +9,13 @@
"@types/luxon": "^3.3.7",
"@types/react": "^18.0.18",
"@types/react-dom": "^18.0.6",
+ "@types/showdown": "^2.0.6",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"luxon": "^3.4.4",
"prettier": "^2.7.1",
"react-hook-form": "^7.49.2",
+ "showdown": "^2.1.0",
"ts-loader": "^9.3.1",
"ts-standard": "^12.0.1",
"typescript": "^4.8.2",
diff --git a/twenty-frontend/src/css/_colors.scss b/twenty-frontend/src/css/_colors.scss
index 1fd97ce..12038d8 100644
--- a/twenty-frontend/src/css/_colors.scss
+++ b/twenty-frontend/src/css/_colors.scss
@@ -1 +1,3 @@
$black: lighten(#000, 20%);
+$gray1: #f4f0ec;
+$gray2: lighten($gray1, 5%);
diff --git a/twenty-frontend/src/css/_tables.scss b/twenty-frontend/src/css/_tables.scss
new file mode 100644
index 0000000..c6b00e5
--- /dev/null
+++ b/twenty-frontend/src/css/_tables.scss
@@ -0,0 +1,41 @@
+@import "colors";
+
+.table {
+ .table.div {
+ display: flex;
+ padding: 10px;
+ width: 100%;
+ color: #FFF;
+ background: $gray1;
+ color: $black;
+ border-radius: 2px;
+ border: #cfcfc4 1px solid;
+ border-bottom: none;
+ }
+
+ .table.div.tabbed {
+ padding: 5px 5px 0 5px;
+ ul.tabs {
+ list-style-type: none;
+ display: flex;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ li {
+ height: 100%;
+ border: 1px solid #cfcfc4;
+ border-bottom: none;
+ padding: 10px;
+ background: #FFF;
+ margin-right: 10px;
+ border-radius: 5px;
+ }
+ }
+ }
+
+ .table.content {
+ background: $gray2;
+ padding: 10px;
+ border: #cfcfc4 1px solid;
+ }
+}
diff --git a/twenty-frontend/src/css/main.scss b/twenty-frontend/src/css/main.scss
index bcb2b25..b176d84 100644
--- a/twenty-frontend/src/css/main.scss
+++ b/twenty-frontend/src/css/main.scss
@@ -1,6 +1,7 @@
@import "fonts";
@import "colors";
@import "forms";
+@import "tables";
*, *:before, *:after {
box-sizing: border-box;
@@ -11,60 +12,58 @@ html, html body, .root {
}
body {
- font-family: "SF Mono","Segoe UI Mono","Roboto Mono",Menlo,Courier,monospace;
font-weight: normal;
+ font-family: "Noto Sans Serif";
}
.root {
margin: 0 auto;
color: $black;
width: 75%;
+ padding: 20px 0 20px 0;
max-width: 1024px;
- .navbar {
- display: flex;
- justify-content: space-between;
- width: 100%;
- padding: 10px 0px 10px 0px;
- }
- form {
- margin: 0 auto;
- height: 100%;
- width: 80%;
+}
- .textarea {
- margin: 0 0 10px 0;
- height: 50%;
- textarea { height: 100%; }
+.root {
+ .issue {
+ .content {
+ padding: 10px;
+ }
+ .content, textarea {
+ min-height: 350px;
}
}
- ul {
- margin: 0;
- padding: 0;
- list-style-type: none;
- li {
- margin: 0 0 15px 0;
- .item.row {
- display: flex;
- background: #f4f0ec;
- border: #cfcfc4 1px solid;
- border-radius: 10px;
- padding: 15px;
- flex-wrap: wrap;
- .header {
- .item.id {
- border: 1px #cfcfc4 solid;
- border-radius: 10px;
- padding: 5px;
- margin-right: 5px;
- background: #FFF;
- }
- }
- .footer {
+
+ .react-mount.issues {
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ li {
+ margin: 0 0 15px 0;
+ .item.row {
display: flex;
- width: 100%;
- justify-content: space-between;
- margin: 20px 0 0 0;
- font-size: small;
+ background: #f4f0ec;
+ border: #cfcfc4 1px solid;
+ border-radius: 10px;
+ padding: 10px;
+ flex-wrap: wrap;
+ .header {
+ .item.id {
+ border: 1px #cfcfc4 solid;
+ border-radius: 10px;
+ padding: 5px;
+ margin-right: 5px;
+ background: #FFF;
+ }
+ }
+ .footer {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ margin: 10px 0 0 0;
+ font-size: small;
+ }
}
}
}
diff --git a/twenty-frontend/src/html/issues/index.html.erb b/twenty-frontend/src/html/issues/index.html.erb
index fd3d66f..e11aa29 100644
--- a/twenty-frontend/src/html/issues/index.html.erb
+++ b/twenty-frontend/src/html/issues/index.html.erb
@@ -1,5 +1,4 @@
diff --git a/twenty-frontend/src/html/issues/new/index.html.erb b/twenty-frontend/src/html/issues/new/index.html.erb
index b1028df..ca7eebc 100644
--- a/twenty-frontend/src/html/issues/new/index.html.erb
+++ b/twenty-frontend/src/html/issues/new/index.html.erb
@@ -1,4 +1,4 @@
diff --git a/twenty-frontend/src/html/issues/read/index.html.erb b/twenty-frontend/src/html/issues/read/index.html.erb
index a6e7e69..f07daff 100644
--- a/twenty-frontend/src/html/issues/read/index.html.erb
+++ b/twenty-frontend/src/html/issues/read/index.html.erb
@@ -1,4 +1,4 @@
diff --git a/twenty-frontend/src/js/components/Issue.tsx b/twenty-frontend/src/js/components/Issue.tsx
new file mode 100644
index 0000000..14515ed
--- /dev/null
+++ b/twenty-frontend/src/js/components/Issue.tsx
@@ -0,0 +1,95 @@
+import React, { useEffect, useState, useRef } from "react";
+import { useForm } from "react-hook-form";
+import { Select } from "/components/forms/Select";
+import { useUpsertIssue } from "/hooks/useUpsertIssue";
+import { useConnections } from "/hooks/useConnections";
+import { Issue } from "/types/schema";
+import showdown from "showdown";
+
+type Inputs = {
+ id?: number;
+ title: string;
+ content: string;
+ connectionId: number;
+};
+
+export function Issue({ issue }: { issue?: Issue }) {
+ const { register, handleSubmit, watch, setValue: set } = useForm();
+ const [isEditable, setIsEditable] = useState(!issue);
+ const selectRef = useRef(null);
+ const upsert = useUpsertIssue();
+ const [connections] = useConnections();
+ const c = new showdown.Converter();
+ const content = watch("content");
+ const onSave = (input: Inputs) => {
+ upsert({ input }).then(() => {
+ location.href = "/issues/";
+ });
+ };
+
+ useEffect(() => {
+ set("connectionId", 1);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/twenty-frontend/src/js/components/Issues.tsx b/twenty-frontend/src/js/components/Issues.tsx
index 33f027f..f294e16 100644
--- a/twenty-frontend/src/js/components/Issues.tsx
+++ b/twenty-frontend/src/js/components/Issues.tsx
@@ -10,34 +10,39 @@ export function Issues() {
setIssues(issues.filter(i => i.id !== issue.id));
};
return (
-
- {issues.map((issue: Issue, key: number) => {
- const { updated_at: updatedAt } = issue;
- const datetime = DateTime.fromISO(updatedAt);
- return (
- -
-
-
-
-
-
+
+
Issues
+
+
+ {issues.map((issue: Issue, key: number) => {
+ const { updated_at: updatedAt } = issue;
+ const datetime = DateTime.fromISO(updatedAt);
+ return (
+ -
+
+
+
+
+
+
+
+ {datetime.toFormat("dd LLL, yyyy")} at{" "}
+ {datetime.toFormat("HH:mm")}
+
+
-
- {datetime.toFormat("dd LLL, yyyy")} at{" "}
- {datetime.toFormat("HH:mm")}
-
-
-
-
- );
- })}
-
+
+ );
+ })}
+
+
+
);
}
diff --git a/twenty-frontend/src/js/components/NavBar.tsx b/twenty-frontend/src/js/components/NavBar.tsx
index b2539fd..2595753 100644
--- a/twenty-frontend/src/js/components/NavBar.tsx
+++ b/twenty-frontend/src/js/components/NavBar.tsx
@@ -2,7 +2,6 @@ import React from "react";
export function NavBar() {
return (
-
Issues
diff --git a/twenty-frontend/src/js/components/ReadIssue.tsx b/twenty-frontend/src/js/components/ReadIssue.tsx
index 66b4ad7..a5a29a4 100644
--- a/twenty-frontend/src/js/components/ReadIssue.tsx
+++ b/twenty-frontend/src/js/components/ReadIssue.tsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
-import { Issue } from "/types/schema";
import { useParams } from "/hooks/useParams";
+import { Issue } from "/types/schema";
+import { Issue as Component } from "/components/Issue";
export function ReadIssue() {
const { id } = useParams();
@@ -14,12 +15,7 @@ export function ReadIssue() {
}, [id]);
if (id?.length && issue) {
- return (
-
-
{issue.title}
-
{issue.content}
-
- );
+ return
;
} else {
return null;
}
diff --git a/twenty-frontend/src/js/components/forms/NewIssue.tsx b/twenty-frontend/src/js/components/forms/NewIssue.tsx
deleted file mode 100644
index 5b98860..0000000
--- a/twenty-frontend/src/js/components/forms/NewIssue.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { useEffect, useRef } from "react";
-import { useForm } from "react-hook-form";
-import { Select } from "/components/forms/Select";
-import { useCreateIssue } from "/hooks/useCreateIssue";
-import { useConnections } from "/hooks/useConnections";
-
-type Inputs = {
- title: string;
- content: string;
- connectionId: number;
-};
-
-export function NewIssue() {
- const { register, handleSubmit, setValue: set } = useForm
();
- const selectRef = useRef(null);
- const [create] = useCreateIssue();
- const [connections] = useConnections();
- const onSave = (input: Inputs) => {
- create({ input }).then(() => {
- location.href = "/issues/";
- });
- };
-
- useEffect(() => {
- set("connectionId", 1);
- }, []);
-
- return (
-
- );
-}
diff --git a/twenty-frontend/src/js/entry-point/issues.tsx b/twenty-frontend/src/js/entry-point/issues.tsx
index a92ac47..0c87839 100644
--- a/twenty-frontend/src/js/entry-point/issues.tsx
+++ b/twenty-frontend/src/js/entry-point/issues.tsx
@@ -1,11 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Issues } from "/components/Issues";
-import { NavBar } from "/components/NavBar";
(function () {
- const n1 = document.querySelector(".react-mount.navbar");
- const n2 = document.querySelector(".react-mount.issues")!;
- ReactDOM.createRoot(n1).render();
- ReactDOM.createRoot(n2).render();
+ const n1 = document.querySelector(".react-mount.issues")!;
+ ReactDOM.createRoot(n1).render();
})();
diff --git a/twenty-frontend/src/js/entry-point/new-issue.tsx b/twenty-frontend/src/js/entry-point/new-issue.tsx
index f15e08d..0f31b77 100644
--- a/twenty-frontend/src/js/entry-point/new-issue.tsx
+++ b/twenty-frontend/src/js/entry-point/new-issue.tsx
@@ -1,8 +1,8 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import { NewIssue } from "/components/forms/NewIssue";
+import { Issue } from "/components/Issue";
(function () {
- const root = document.getElementById("reactapp")!;
- ReactDOM.createRoot(root).render();
+ const root = document.querySelector(".react-mount.new-issue")!;
+ ReactDOM.createRoot(root).render();
})();
diff --git a/twenty-frontend/src/js/entry-point/read-issue.tsx b/twenty-frontend/src/js/entry-point/read-issue.tsx
index f8538f9..2923ceb 100644
--- a/twenty-frontend/src/js/entry-point/read-issue.tsx
+++ b/twenty-frontend/src/js/entry-point/read-issue.tsx
@@ -3,6 +3,6 @@ import ReactDOM from "react-dom/client";
import { ReadIssue } from "/components/ReadIssue";
(function () {
- const root = document.getElementById("reactapp")!;
+ const root = document.querySelector(".react-mount.edit-issue")!;
ReactDOM.createRoot(root).render();
})();
diff --git a/twenty-frontend/src/js/hooks/useCreateIssue.ts b/twenty-frontend/src/js/hooks/useCreateIssue.ts
deleted file mode 100644
index bb32858..0000000
--- a/twenty-frontend/src/js/hooks/useCreateIssue.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-type Params = {
- title: string;
- content: string;
- connectionId: number;
-};
-
-export function useCreateIssue() {
- return [
- function ({ input }: { input: Params }) {
- return new Promise((accept, reject) => {
- const { connectionId, title, content } = input;
- const req = {
- method: "POST",
- body: JSON.stringify({ connection_id: connectionId, title, content }),
- };
- return fetch("/servlet/issues", req)
- .then(res => res.json())
- .then(accept)
- .catch(reject);
- });
- },
- ];
-}
diff --git a/twenty-frontend/src/js/hooks/useIssues.ts b/twenty-frontend/src/js/hooks/useIssues.ts
index b9fd8be..74dd9c2 100644
--- a/twenty-frontend/src/js/hooks/useIssues.ts
+++ b/twenty-frontend/src/js/hooks/useIssues.ts
@@ -23,5 +23,5 @@ export function useIssues(): Result {
req();
}, []);
- return { issues, setIssues, req };
+ return { issues, setIssues: set, req };
}
diff --git a/twenty-frontend/src/js/hooks/useUpsertIssue.ts b/twenty-frontend/src/js/hooks/useUpsertIssue.ts
new file mode 100644
index 0000000..49b7a6c
--- /dev/null
+++ b/twenty-frontend/src/js/hooks/useUpsertIssue.ts
@@ -0,0 +1,25 @@
+type Params = {
+ id?: number;
+ title: string;
+ content: string;
+ connectionId: number;
+};
+
+export function useUpsertIssue() {
+ const normalize = (input: Params) => {
+ const { id, title, content, connectionId } = input;
+ return { id, title, content, connection_id: connectionId };
+ };
+ return function ({ input }: { input: Params }) {
+ return new Promise((accept, reject) => {
+ const req = {
+ method: input.id ? "PUT" : "POST",
+ body: JSON.stringify(normalize(input)),
+ };
+ return fetch("/servlet/issues", req)
+ .then(res => res.json())
+ .then(accept)
+ .catch(reject);
+ });
+ };
+}