Skip to content

Commit 09d65c8

Browse files
authored
Merge pull request #453 from STAPLE-verse/textarea-fixes
Textarea fixes
2 parents 5faf133 + 304520d commit 09d65c8

25 files changed

Lines changed: 2342 additions & 200 deletions

File tree

package-lock.json

Lines changed: 1518 additions & 121 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,15 @@
9999
"react-i18next": "15.4.1",
100100
"react-json-editor-ajrm": "2.5.14",
101101
"react-json-view": "1.21.3",
102+
"react-markdown": "10.1.0",
102103
"react-minimal-pie-chart": "9.1.0",
103104
"react-overlays": "5.2.1",
104105
"react-tag-input": "6.10.3",
105106
"react-tooltip": "5.26.0",
106107
"reactflow": "11.9.2",
107108
"reactstrap": "9.2.3",
109+
"remark-breaks": "4.0.0",
110+
"remark-gfm": "4.0.1",
108111
"resend": "4.0.0",
109112
"secure-password": "4.0.0",
110113
"tailwindcss": "3.3.3",

src/core/components/GetWidgetDisplay.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Table from "src/core/components/Table"
22
import { projectManagersColumns } from "src/widgets/components/ColumnHelpers"
33
import { CircularProgressbar, buildStyles } from "react-circular-progressbar"
44
import DateFormat from "./DateFormat"
5+
import ReactMarkdown from "react-markdown"
6+
import remarkGfm from "remark-gfm"
7+
import remarkBreaks from "remark-breaks"
58

69
// use for all tables in widgets
710
export function GetTableDisplay({ data, columns, type }) {
@@ -69,8 +72,11 @@ export function GetIconDisplay({ number, icon: Icon }) {
6972

7073
export function GetProjectSummaryDisplay({ project, projectManagers }) {
7174
return (
72-
<div>
73-
{project.description}
75+
<div className="markdown-display">
76+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
77+
{(project.description || "").slice(0, 100) +
78+
((project.description?.length ?? 0) > 100 ? "…" : "")}
79+
</ReactMarkdown>
7480
<p className="italic">
7581
Last update: <DateFormat date={project.updatedAt}></DateFormat>
7682
</p>

src/core/components/fields/LabeledTextAreaField.tsx

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef } from "react"
1+
import { forwardRef, ComponentPropsWithoutRef, PropsWithoutRef, useState } from "react"
22
import { useField, UseFieldConfig } from "react-final-form"
33

4+
import ReactMarkdown from "react-markdown"
5+
import remarkGfm from "remark-gfm"
6+
import remarkBreaks from "remark-breaks"
7+
48
export interface LabeledTextAreaFieldProps
59
extends PropsWithoutRef<JSX.IntrinsicElements["textarea"]> {
610
/** Field name. */
@@ -27,18 +31,76 @@ export const LabeledTextAreaField = forwardRef<HTMLTextAreaElement, LabeledTextA
2731
})
2832

2933
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
34+
const [mode, setMode] = useState<"edit" | "preview">("edit")
3035

3136
return (
3237
<div {...outerProps} data-testid="labeledarea-testid">
3338
<label {...labelProps}>
3439
{label}
35-
<textarea
36-
{...input}
37-
disabled={submitting}
38-
{...props}
39-
ref={ref}
40-
data-testid="labeledtarget-testid"
41-
/>
40+
41+
{/* Toolbar */}
42+
<div className="flex items-center mt-2">
43+
<div className="join">
44+
<button
45+
type="button"
46+
className={`btn btn-sm join-item ${mode === "edit" ? "btn-primary" : "btn-ghost"}`}
47+
onClick={() => setMode("edit")}
48+
>
49+
Edit
50+
</button>
51+
<button
52+
type="button"
53+
className={`btn btn-sm join-item ${
54+
mode === "preview" ? "btn-primary" : "btn-ghost"
55+
}`}
56+
onClick={() => setMode("preview")}
57+
>
58+
Preview
59+
</button>
60+
</div>
61+
<span className="text-sm text-base-content ml-3 italic">
62+
Supports{" "}
63+
<a
64+
href="https://www.markdownguide.org/cheat-sheet/"
65+
target="_blank"
66+
rel="noopener noreferrer"
67+
className="text-primary underline"
68+
>
69+
Markdown
70+
</a>{" "}
71+
formatting.
72+
</span>
73+
</div>
74+
75+
{mode === "edit" ? (
76+
<textarea
77+
{...input}
78+
disabled={submitting}
79+
{...props}
80+
ref={ref}
81+
data-testid="labeledtarget-testid"
82+
rows={6}
83+
wrap="soft"
84+
/>
85+
) : (
86+
<div className="markdown-preview mb-4" data-testid="labeledpreview-testid">
87+
<ReactMarkdown
88+
remarkPlugins={[remarkGfm, remarkBreaks]}
89+
components={{
90+
a: ({ node, ...props }) => (
91+
<a
92+
{...props}
93+
target="_blank"
94+
rel="noopener noreferrer"
95+
className="text-primary"
96+
/>
97+
),
98+
}}
99+
>
100+
{input.value || "_Nothing to preview yet…_"}
101+
</ReactMarkdown>
102+
</div>
103+
)}
42104
</label>
43105

44106
{touched && normalizedError && (
@@ -61,6 +123,11 @@ export const LabeledTextAreaField = forwardRef<HTMLTextAreaElement, LabeledTextA
61123
border-radius: 3px;
62124
appearance: none;
63125
margin-top: 0.5rem;
126+
resize: both;
127+
overflow: auto;
128+
max-width: 100%;
129+
white-space: pre-wrap;
130+
line-height: 1.5;
64131
}
65132
66133
textarea:focus {

src/core/styles/index.css

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,3 +868,106 @@ input:focus {
868868
.checkbox p[id$="__description"] {
869869
margin-right: 0.5rem;
870870
}
871+
.markdown-preview {
872+
width: 100%;
873+
min-height: 9rem; /* roughly rows={6} */
874+
padding: 0.75rem 0.9rem;
875+
margin-top: 0.5rem;
876+
border-radius: 3px;
877+
border: 1px solid oklch(var(--bc) / 0.2);
878+
background: oklch(var(--b1));
879+
overflow: auto;
880+
}
881+
.markdown-preview h1,
882+
.markdown-preview h2,
883+
.markdown-preview h3 {
884+
margin: 0.75rem 0 0.25rem;
885+
font-weight: 700;
886+
}
887+
.markdown-preview p {
888+
margin: 0.5rem 0;
889+
}
890+
.markdown-preview ul,
891+
.markdown-preview ol {
892+
margin: 0.5rem 1.25rem;
893+
}
894+
.markdown-preview ul {
895+
list-style: disc;
896+
padding-left: 1.5rem;
897+
}
898+
.markdown-preview ol {
899+
list-style: decimal;
900+
padding-left: 1.5rem;
901+
}
902+
.markdown-preview li {
903+
margin: 0.25rem 0;
904+
}
905+
.markdown-preview code {
906+
padding: 0.1rem 0.25rem;
907+
border-radius: 3px;
908+
background: oklch(var(--n) / 0.08);
909+
}
910+
.markdown-preview a {
911+
text-decoration: underline;
912+
}
913+
.join {
914+
display: inline-flex;
915+
gap: 0;
916+
}
917+
.btn-ghost {
918+
border: 1px solid transparent;
919+
}
920+
921+
.markdown-display {
922+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
923+
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
924+
}
925+
926+
.markdown-display p {
927+
margin: 0.5rem 0;
928+
}
929+
.markdown-display > :first-child {
930+
margin-top: 0;
931+
}
932+
.markdown-display > :last-child {
933+
margin-bottom: 0;
934+
}
935+
936+
/* Tame oversized headings in compact cards/previews */
937+
.markdown-display h1 {
938+
font-size: 1.5rem;
939+
line-height: 1.25;
940+
margin: 0.75rem 0 0.25rem;
941+
}
942+
.markdown-display h2 {
943+
font-size: 1.25rem;
944+
line-height: 1.3;
945+
margin: 0.75rem 0 0.25rem;
946+
}
947+
.markdown-display h3 {
948+
font-size: 1.125rem;
949+
line-height: 1.35;
950+
margin: 0.5rem 0 0.25rem;
951+
}
952+
.markdown-display h4 {
953+
font-size: 1rem; /* compact size */
954+
line-height: 1.4;
955+
margin: 0.5rem 0 0.25rem;
956+
}
957+
.markdown-display ul,
958+
.markdown-display ol {
959+
margin: 0.5rem 1.25rem !important;
960+
}
961+
.markdown-display ul {
962+
list-style: disc !important;
963+
list-style-position: outside !important;
964+
padding-left: 1.5rem !important;
965+
}
966+
.markdown-display ol {
967+
list-style: decimal !important;
968+
list-style-position: outside !important;
969+
padding-left: 1.5rem !important;
970+
}
971+
.markdown-display li {
972+
margin: 0.25rem 0 !important;
973+
}

src/milestones/components/MilestoneForm.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,17 @@ export function MilestoneForm<S extends z.ZodType<any, any>>(props: MilestoneFor
9393
}}
9494
onKeyDown={(e) => {
9595
if (e.key === "Enter") {
96-
e.preventDefault() // Prevent form submission on Enter
96+
const el = e.target as HTMLElement
97+
const tag = el?.tagName?.toLowerCase()
98+
const isTextArea = tag === "textarea"
99+
const isContentEditable = (el as any)?.isContentEditable === true
100+
const insideReactTags = !!el?.closest?.(".react-tags-wrapper")
101+
102+
// Allow Enter for textareas, contentEditable fields, and the ReactTags input.
103+
// Prevent only when it would submit the form from other inputs/buttons.
104+
if (!isTextArea && !isContentEditable && !insideReactTags) {
105+
e.preventDefault()
106+
}
97107
}
98108
}}
99109
>

src/milestones/components/MilestoneInformation.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { MilestoneTasksData } from "../tables/processing/processMilestoneTasks"
88
import { MilestoneSummary } from "./MilestoneSummary"
99
import { ProjectTasksColumns } from "src/tasks/tables/columns/ProjectTasksColumns"
1010
import { ProjectTasksData } from "src/tasks/tables/processing/processProjectTasks"
11+
import ReactMarkdown from "react-markdown"
12+
import remarkGfm from "remark-gfm"
13+
import remarkBreaks from "remark-breaks"
1114

1215
interface MilestoneInformationProps {
1316
milestone: Milestone
@@ -33,7 +36,11 @@ export const MilestoneInformation: React.FC<MilestoneInformationProps> = ({
3336
className="z-[1099] ourtooltips"
3437
/>
3538
{/* Milestone description */}
36-
{milestone.description}
39+
<div className="markdown-display">
40+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
41+
{milestone.description || ""}
42+
</ReactMarkdown>
43+
</div>
3744

3845
{/* Milestone start and end dates */}
3946
{milestone.startDate && (

src/milestones/components/MilestoneItem.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Routes } from "@blitzjs/next"
22
import Link from "next/link"
33
import { useState } from "react"
4+
import ReactMarkdown from "react-markdown"
5+
import remarkGfm from "remark-gfm"
6+
import remarkBreaks from "remark-breaks"
47
import { MilestoneWithTasks } from "src/core/types"
58
import { Task } from "db"
69
import DateRange from "src/core/components/DateRange"
@@ -34,7 +37,11 @@ const MilestoneItem: React.FC<MilestoneItemProps> = ({
3437
<div className="collapse-title text-xl font-medium">{milestone.name}</div>
3538
<div className="collapse-content">
3639
{/* Milestone description */}
37-
<p className="mb-2">{milestone.description}</p>
40+
<div className="mb-2 markdown-display">
41+
<ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
42+
{milestone.description || ""}
43+
</ReactMarkdown>
44+
</div>
3845
{/* Milestone date range */}
3946
<p className="italic mb-2">
4047
<DateRange

0 commit comments

Comments
 (0)