Skip to content

Commit 7da740a

Browse files
committed
💩(frontend) display tag on thread details and list view
Implementation need to be review. It's a first implementation. the visual rendering is good but not necessarily the implementation :D
1 parent 0bfcef3 commit 7da740a

File tree

11 files changed

+216
-12
lines changed

11 files changed

+216
-12
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useTranslation } from "react-i18next"
2+
import { Button } from "@openfun/cunningham-react"
3+
import { Tooltip } from "@gouvfr-lasuite/ui-kit"
4+
import { useMailboxContext } from "@/features/providers/mailbox"
5+
import { ThreadAccessesWidget } from "../thread-accesses-widget"
6+
import { useRead } from "@/features/message/use-read"
7+
import { useTrash } from "@/features/message/use-trash"
8+
import { ThreadAccessRoleEnum } from "@/features/api/gen/models"
9+
import { LabelBadge } from "@/features/layouts/components/label-badge"
10+
11+
export const ActionBar = () => {
12+
const { t } = useTranslation()
13+
const { selectedThread, unselectThread } = useMailboxContext()
14+
const { markAsUnread } = useRead()
15+
const { markAsTrashed, markAsUntrashed } = useTrash()
16+
17+
if (!selectedThread) return null
18+
19+
const hasOnlyOneEditor = selectedThread.accesses.filter(
20+
(access) => access.role === ThreadAccessRoleEnum.editor
21+
).length === 1
22+
23+
return (
24+
<div className="thread-action-bar">
25+
<div className="thread-action-bar__left">
26+
{selectedThread.labels && selectedThread.labels.length > 0 && (
27+
<div className="thread-action-bar__labels">
28+
{selectedThread.labels.map((label) => (
29+
<LabelBadge key={label.id} label={label} />
30+
))}
31+
</div>
32+
)}
33+
</div>
34+
<div className="thread-action-bar__right">
35+
<ThreadAccessesWidget accesses={selectedThread.accesses} />
36+
<Tooltip content={t('actions.mark_as_unread')}>
37+
<Button
38+
color="primary-text"
39+
aria-label={t('actions.mark_as_unread')}
40+
size="small"
41+
icon={<span className="material-icons">mark_email_unread</span>}
42+
onClick={() => markAsUnread({ threadIds: [selectedThread.id], onSuccess: unselectThread })}
43+
/>
44+
</Tooltip>
45+
{
46+
selectedThread.count_trashed < selectedThread.count_messages ? (
47+
<Tooltip content={t('actions.delete')}>
48+
<Button
49+
color="primary-text"
50+
aria-label={t('actions.delete')}
51+
size="small"
52+
icon={<span className="material-icons">delete</span>}
53+
onClick={() => markAsTrashed({ threadIds: [selectedThread.id], onSuccess: unselectThread })}
54+
/>
55+
</Tooltip>
56+
) : (
57+
<Tooltip content={t('actions.undelete')}>
58+
<Button
59+
color="primary-text"
60+
aria-label={t('actions.undelete')}
61+
size="small"
62+
icon={<span className="material-icons">restore</span>}
63+
onClick={() => markAsUntrashed({ threadIds: [selectedThread.id], onSuccess: unselectThread })}
64+
/>
65+
</Tooltip>
66+
)
67+
}
68+
</div>
69+
</div>
70+
)
71+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Generated by orval v7.9.0 🍺
3+
* Do not edit manually.
4+
* messages API
5+
* This is the messages API schema.
6+
* OpenAPI spec version: 1.0.0 (v1.0)
7+
*/
8+
9+
/**
10+
* Serializer for Label model.
11+
*/
12+
export interface Label {
13+
/** primary key for the record as UUID */
14+
readonly id: string;
15+
/** Name of the label/folder (can use slashes for hierarchy, e.g. 'Work/Projects') */
16+
readonly name: string;
17+
/** URL-friendly version of the name */
18+
readonly slug: string;
19+
/** Color of the label in hex format (e.g. #FF0000) */
20+
readonly color: string;
21+
/** Mailbox that owns this label */
22+
readonly mailbox: string;
23+
/** Threads that have this label */
24+
readonly threads?: readonly string[];
25+
}

src/frontend/src/features/api/gen/models/thread.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* OpenAPI spec version: 1.0.0 (v1.0)
77
*/
88
import type { ThreadAccessDetail } from "./thread_access_detail";
9+
import type { Label } from "./label";
910

1011
/**
1112
* Serialize threads.
@@ -29,4 +30,5 @@ export interface Thread {
2930
readonly updated_at: string;
3031
readonly user_role: string;
3132
readonly accesses: readonly ThreadAccessDetail[];
33+
readonly labels: readonly Label[];
3234
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Badge } from "@/features/ui/components/badge"
2+
import { Label } from "@/features/api/gen/models/label"
3+
4+
type LabelBadgeProps = {
5+
label: Label
6+
size?: "small" | "medium"
7+
}
8+
9+
export const LabelBadge = ({ label, size = "medium" }: LabelBadgeProps) => {
10+
return (
11+
<Badge>
12+
<span
13+
style={{
14+
backgroundColor: label.color,
15+
color: getContrastColor(label.color),
16+
padding: size === "small" ? "0.125rem 0.375rem" : "0.25rem 0.5rem",
17+
borderRadius: "4px",
18+
fontSize: size === "small" ? "0.75rem" : "0.875rem",
19+
fontWeight: 600,
20+
display: "inline-block",
21+
}}
22+
>
23+
{label.name}
24+
</span>
25+
</Badge>
26+
)
27+
}
28+
29+
// Helper function to determine if text should be black or white based on background color
30+
function getContrastColor(hexColor: string): string {
31+
// Remove the # if present
32+
const hex = hexColor.replace("#", "")
33+
34+
// Convert to RGB
35+
const r = parseInt(hex.substring(0, 2), 16)
36+
const g = parseInt(hex.substring(2, 4), 16)
37+
const b = parseInt(hex.substring(4, 6), 16)
38+
39+
// Calculate relative luminance
40+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
41+
42+
// Return black for light colors, white for dark colors
43+
return luminance > 0.5 ? "#000000" : "#FFFFFF"
44+
}

src/frontend/src/features/layouts/components/thread-panel/components/thread-item/_index.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,18 @@
134134
width: 76px;
135135
text-align: right;
136136
}
137+
138+
.thread-item__metadata {
139+
display: flex;
140+
flex-direction: row;
141+
align-items: center;
142+
gap: var(--c--theme--spacings--2xs);
143+
}
144+
145+
.thread-item__labels {
146+
display: flex;
147+
flex-direction: row;
148+
flex-wrap: wrap;
149+
gap: var(--c--theme--spacings--2xs);
150+
margin-top: var(--c--theme--spacings--2xs);
151+
}

src/frontend/src/features/layouts/components/thread-panel/components/thread-item/index.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useParams, useSearchParams } from "next/navigation"
55
import { Thread } from "@/features/api/gen/models"
66
import { ThreadItemSenders } from "./thread-item-senders"
77
import ThreadHelper from "@/features/utils/thread-helper"
8+
import { LabelBadge } from "@/features/layouts/components/label-badge"
89

910
type ThreadItemProps = {
1011
thread: Thread
@@ -34,13 +35,13 @@ export const ThreadItem = ({ thread }: ThreadItemProps) => {
3435
/>
3536
)}
3637
<div className="thread-item__metadata">
37-
{/* {thread.has_attachments ? (
38-
<span className="thread-item__metadata-attachments">
39-
<Tooltip placement="bottom" content={t('tooltips.has_attachments')}>
40-
<span className="material-icons">attachment</span>
41-
</Tooltip>
42-
</span>
43-
) : null} */}
38+
{thread.labels && thread.labels.length > 0 && (
39+
<div className="thread-item__labels">
40+
{thread.labels.map((label) => (
41+
<LabelBadge key={label.id} label={label} size="small" />
42+
))}
43+
</div>
44+
)}
4445
</div>
4546
</div>
4647
<p className="thread-item__subject">{thread.subject}</p>
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
.thread-action-bar {
2+
display: flex;
3+
flex-direction: row;
24
justify-content: space-between;
5+
align-items: center;
6+
padding: var(--c--theme--spacings--xs);
7+
border-bottom: 1px solid var(--c--theme--colors--greyscale-200);
38
}
49

5-
.thread-action-bar > div {
10+
.thread-action-bar__left {
611
display: flex;
712
flex-direction: row;
813
align-items: center;
9-
gap: var(--c--theme--spacings--2xs);
14+
gap: var(--c--theme--spacings--base);
1015
}
1116

12-
.thread-action-bar__left {
13-
justify-content: flex-start;
17+
.thread-action-bar__labels {
18+
display: flex;
19+
flex-direction: row;
20+
flex-wrap: wrap;
21+
gap: var(--c--theme--spacings--2xs);
1422
}
1523

1624
.thread-action-bar__right {
17-
justify-content: flex-end;
25+
display: flex;
26+
flex-direction: row;
27+
align-items: center;
28+
gap: var(--c--theme--spacings--2xs);
1829
}

src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
color: var(--c--theme--colors--greyscale-500);
5858
}
5959

60+
.thread-message__labels {
61+
display: flex;
62+
flex-direction: row;
63+
gap: var(--c--theme--spacings--2xs);
64+
flex-wrap: wrap;
65+
}
66+
6067
.thread-message__date {
6168
text-align: right;
6269
}

src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useMailboxContext } from "@/features/providers/mailbox";
1010
import { Badge } from "@/features/ui/components/badge";
1111
import useTrash from "@/features/message/use-trash";
1212
import { AttachmentList } from "../thread-attachment-list";
13+
import { LabelBadge } from "@/features/layouts/components/label-badge";
1314
type ThreadMessageProps = {
1415
message: Message,
1516
isLatest: boolean,
@@ -61,6 +62,13 @@ export const ThreadMessage = forwardRef<HTMLElement, ThreadMessageProps>(
6162
</div>
6263
<div className="thread-message__header-column thread-message__header-column--right flex-row flex-align-center">
6364
<div className="thread-message__metadata">
65+
{selectedThread?.labels && selectedThread.labels.length > 0 && (
66+
<div className="thread-message__labels">
67+
{selectedThread.labels.map((label) => (
68+
<LabelBadge key={label.id} label={label} size="small" />
69+
))}
70+
</div>
71+
)}
6472
{message.sent_at && (
6573
<p className="thread-message__date">{
6674
new Date(message.sent_at).toLocaleString(i18n.language, {

src/frontend/src/styles/custom.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* Override badge color with thread label color */
2+
.badge {
3+
background-color: transparent !important;
4+
padding: 0 !important;
5+
}
6+
7+
.badge > span {
8+
display: inline-block !important;
9+
padding: 0.25rem 0.5rem !important;
10+
border-radius: 4px !important;
11+
font-size: 0.875rem !important;
12+
font-weight: 600 !important;
13+
}
14+
15+
/* For small badges */
16+
.badge--small > span {
17+
padding: 0.125rem 0.375rem !important;
18+
font-size: 0.75rem !important;
19+
}

0 commit comments

Comments
 (0)