@tribe-nest/forge 3.60.0 → 3.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +20 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/seo/_tests/seo.spec.ts +326 -0
- package/src/seo/applySeoToHead.ts +98 -0
- package/src/seo/index.ts +9 -0
- package/src/seo/resolve.ts +211 -0
- package/src/seo/seoHead.ts +61 -0
- package/src/seo/siteSeoHead.ts +97 -0
- package/src/seo/types.ts +99 -0
- package/src/server/_tests/siteBootstrap.spec.ts +65 -0
- package/src/server/index.ts +33 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityHarness.tsx +10 -3
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/landing.spec.ts +39 -0
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
- package/src/utils/landing.ts +36 -10
- package/src/utils/metaEvents.ts +302 -0
- package/src/utils/structuredData.ts +339 -1
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { useState, type FormEvent } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useId, useState, type FormEvent } from "react";
|
|
2
|
+
import { Send } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
4
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
5
|
import { Loading } from "../Loading";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
useWorkTaskComments,
|
|
8
|
-
type WorkTaskComment,
|
|
9
|
-
} from "../../headless/work/useWorkPortal";
|
|
6
|
+
import { useWorkTaskComments, type WorkTaskComment } from "../../headless/work/useWorkPortal";
|
|
7
|
+
import { PortalBlock, usePortal, usePortalButtons } from "../portal/shared";
|
|
10
8
|
|
|
11
9
|
export interface WorkTaskCommentsProps {
|
|
12
10
|
/** The client project id (from the route). */
|
|
@@ -16,13 +14,15 @@ export interface WorkTaskCommentsProps {
|
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
|
-
* The authenticated client's comment thread for a single task
|
|
20
|
-
* existing comments (author
|
|
21
|
-
*
|
|
22
|
-
* theme tokens only.
|
|
17
|
+
* The authenticated client's comment thread for a single task. Renders the
|
|
18
|
+
* existing comments (author, time and content, newest first) and a composer
|
|
19
|
+
* that posts a new one. Built on `useWorkTaskComments`; theme tokens only.
|
|
23
20
|
*/
|
|
24
21
|
export function WorkTaskComments({ projectId, taskId }: WorkTaskCommentsProps) {
|
|
25
22
|
const t = useThemeTokens();
|
|
23
|
+
const tr = useForgeT();
|
|
24
|
+
const buttons = usePortalButtons();
|
|
25
|
+
const inputId = useId();
|
|
26
26
|
const { comments, isLoading, error, addComment, isAdding } = useWorkTaskComments(projectId, taskId);
|
|
27
27
|
const [content, setContent] = useState("");
|
|
28
28
|
|
|
@@ -38,112 +38,85 @@ export function WorkTaskComments({ projectId, taskId }: WorkTaskCommentsProps) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
<div
|
|
43
|
-
data-testid="work-task-comments"
|
|
44
|
-
style={{ display: "flex", flexDirection: "column", gap: 16, color: t.text, fontFamily: t.fontFamily }}
|
|
45
|
-
>
|
|
46
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
47
|
-
<MessageSquare size={16} style={{ color: t.primary }} />
|
|
48
|
-
<h2
|
|
49
|
-
style={{
|
|
50
|
-
fontSize: 13,
|
|
51
|
-
fontWeight: 600,
|
|
52
|
-
textTransform: "uppercase",
|
|
53
|
-
letterSpacing: "0.05em",
|
|
54
|
-
margin: 0,
|
|
55
|
-
color: t.text,
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
Comments
|
|
59
|
-
</h2>
|
|
60
|
-
</div>
|
|
41
|
+
const empty = content.trim().length === 0;
|
|
61
42
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
43
|
+
return (
|
|
44
|
+
<div data-testid="work-task-comments" style={{ color: t.text, fontFamily: t.fontFamily }}>
|
|
45
|
+
<PortalBlock title={tr("forge.work_task_comments.title")}>
|
|
46
|
+
{isLoading ? (
|
|
47
|
+
<Loading />
|
|
48
|
+
) : error ? (
|
|
49
|
+
<p style={{ margin: 0, fontSize: 14, color: t.muted }}>{tr("forge.work_task_comments.load_error")}</p>
|
|
50
|
+
) : comments.length === 0 ? (
|
|
51
|
+
<p style={{ margin: 0, fontSize: 14, color: t.muted }}>{tr("forge.work_task_comments.empty")}</p>
|
|
52
|
+
) : (
|
|
53
|
+
<ul style={{ margin: 0, padding: 0, listStyle: "none", borderTop: `1px solid ${t.border}` }}>
|
|
54
|
+
{comments.map((c) => (
|
|
55
|
+
<CommentItem key={c.id} comment={c} />
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
)}
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
padding: "10px 12px",
|
|
89
|
-
fontFamily: t.fontFamily,
|
|
90
|
-
fontSize: 14,
|
|
91
|
-
color: t.text,
|
|
92
|
-
background: t.background,
|
|
93
|
-
border: `1px solid ${t.border}`,
|
|
94
|
-
borderRadius: t.cornerRadius,
|
|
95
|
-
boxSizing: "border-box",
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
|
99
|
-
<button
|
|
100
|
-
type="submit"
|
|
101
|
-
data-testid="work-task-comment-submit"
|
|
102
|
-
disabled={isAdding || content.trim().length === 0}
|
|
60
|
+
<form onSubmit={handleSubmit} className="fg:mt-6 fg:flex fg:flex-col fg:gap-3">
|
|
61
|
+
<label htmlFor={inputId} className="fg:sr-only">
|
|
62
|
+
{tr("forge.work_task_comments.input_label")}
|
|
63
|
+
</label>
|
|
64
|
+
<textarea
|
|
65
|
+
id={inputId}
|
|
66
|
+
data-testid="work-task-comment-input"
|
|
67
|
+
value={content}
|
|
68
|
+
onChange={(e) => setContent(e.target.value)}
|
|
69
|
+
placeholder={tr("forge.work_task_comments.placeholder")}
|
|
70
|
+
rows={3}
|
|
71
|
+
disabled={isAdding}
|
|
103
72
|
style={{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
padding: "8px 16px",
|
|
73
|
+
width: "100%",
|
|
74
|
+
resize: "vertical",
|
|
75
|
+
padding: "10px 12px",
|
|
108
76
|
fontFamily: t.fontFamily,
|
|
109
|
-
fontSize:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
border: "none",
|
|
77
|
+
fontSize: 15,
|
|
78
|
+
color: t.text,
|
|
79
|
+
background: t.background,
|
|
80
|
+
border: `1px solid ${t.border}`,
|
|
114
81
|
borderRadius: t.cornerRadius,
|
|
115
|
-
|
|
116
|
-
opacity: isAdding || content.trim().length === 0 ? 0.6 : 1,
|
|
82
|
+
boxSizing: "border-box",
|
|
117
83
|
}}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
84
|
+
/>
|
|
85
|
+
<div className="fg:flex fg:justify-end">
|
|
86
|
+
<button
|
|
87
|
+
type="submit"
|
|
88
|
+
data-testid="work-task-comment-submit"
|
|
89
|
+
disabled={isAdding || empty}
|
|
90
|
+
style={{
|
|
91
|
+
...buttons.primary,
|
|
92
|
+
cursor: isAdding || empty ? "not-allowed" : "pointer",
|
|
93
|
+
opacity: isAdding || empty ? 0.5 : 1,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<Send size={14} aria-hidden />
|
|
97
|
+
{isAdding ? tr("forge.work_task_comments.posting") : tr("forge.work_task_comments.submit")}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</form>
|
|
101
|
+
</PortalBlock>
|
|
124
102
|
</div>
|
|
125
103
|
);
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
function CommentItem({ comment
|
|
106
|
+
function CommentItem({ comment }: { comment: WorkTaskComment }) {
|
|
107
|
+
const t = useThemeTokens();
|
|
108
|
+
const { formatDate } = usePortal();
|
|
129
109
|
return (
|
|
130
110
|
<li
|
|
131
111
|
data-testid="work-task-comment-item"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
border: `1px solid ${t.border}`,
|
|
135
|
-
borderRadius: t.cornerRadius,
|
|
136
|
-
background: t.surface,
|
|
137
|
-
display: "flex",
|
|
138
|
-
flexDirection: "column",
|
|
139
|
-
gap: 6,
|
|
140
|
-
}}
|
|
112
|
+
className="fg:flex fg:flex-col fg:gap-1.5 fg:py-4"
|
|
113
|
+
style={{ borderBottom: `1px solid ${t.border}` }}
|
|
141
114
|
>
|
|
142
|
-
<div
|
|
115
|
+
<div className="fg:flex fg:flex-wrap fg:items-baseline fg:gap-x-3">
|
|
143
116
|
<span style={{ fontWeight: 600, fontSize: 14, color: t.text }}>{comment.fullName}</span>
|
|
144
|
-
<span style={{ fontSize:
|
|
117
|
+
<span style={{ fontSize: 13, color: t.muted }}>{formatDate(comment.createdAt)}</span>
|
|
145
118
|
</div>
|
|
146
|
-
<p style={{ margin: 0, fontSize:
|
|
119
|
+
<p style={{ margin: 0, fontSize: 15, lineHeight: 1.5, color: t.text, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
|
147
120
|
{comment.content}
|
|
148
121
|
</p>
|
|
149
122
|
</li>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MouseEvent } from "react";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
4
|
+
import { internalPath } from "../../../runtime/pages";
|
|
2
5
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
3
6
|
import { Loading } from "../Loading";
|
|
4
|
-
import {
|
|
7
|
+
import { useWorkProjectReport } from "../../headless/work/useWorkPortal";
|
|
8
|
+
import { PortalHeading, usePortal } from "../portal/shared";
|
|
5
9
|
import { WorkTaskComments } from "./WorkTaskComments";
|
|
6
10
|
import { WorkTaskAttachments } from "./WorkTaskAttachments";
|
|
7
11
|
|
|
@@ -10,81 +14,66 @@ export interface WorkTaskDetailProps {
|
|
|
10
14
|
projectId: string;
|
|
11
15
|
/** The task id (from the route). */
|
|
12
16
|
taskId: string;
|
|
13
|
-
/** Href back to the project report.
|
|
14
|
-
*
|
|
17
|
+
/** Href back to the project report. Defaults to
|
|
18
|
+
* `internalPath("account/projects/$projectId")`. */
|
|
15
19
|
backHref?: string;
|
|
16
|
-
/** Router-driven back handler.
|
|
17
|
-
*
|
|
20
|
+
/** Router-driven back handler. The back link stays a real `<a href>`, and a
|
|
21
|
+
* plain left click calls this instead of reloading the page. */
|
|
18
22
|
onBack?: () => void;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
|
-
* The authenticated client's single-task detail
|
|
23
|
-
* the comment thread (`WorkTaskComments`). Resolves the
|
|
24
|
-
* report so it shows a friendly title. Theme tokens only.
|
|
26
|
+
* The authenticated client's single-task detail: the task title and status,
|
|
27
|
+
* its attachments and the comment thread (`WorkTaskComments`). Resolves the
|
|
28
|
+
* task from the project report so it shows a friendly title. Theme tokens only.
|
|
25
29
|
*/
|
|
26
30
|
export function WorkTaskDetail({ projectId, taskId, backHref, onBack }: WorkTaskDetailProps) {
|
|
27
31
|
const t = useThemeTokens();
|
|
32
|
+
const tr = useForgeT();
|
|
33
|
+
const { formatDate } = usePortal();
|
|
28
34
|
const { data: report, isLoading } = useWorkProjectReport(projectId);
|
|
29
|
-
const href = backHref ??
|
|
35
|
+
const href = backHref ?? internalPath("account/projects/$projectId", { projectId });
|
|
30
36
|
|
|
31
37
|
const task = report?.tasks.find((tk) => tk.id === taskId);
|
|
32
38
|
|
|
33
|
-
const backLinkStyle: React.CSSProperties = {
|
|
34
|
-
display: "inline-flex",
|
|
35
|
-
alignItems: "center",
|
|
36
|
-
gap: 6,
|
|
37
|
-
fontSize: 13,
|
|
38
|
-
fontWeight: 600,
|
|
39
|
-
color: t.primary,
|
|
40
|
-
background: "transparent",
|
|
41
|
-
border: "none",
|
|
42
|
-
padding: 0,
|
|
43
|
-
cursor: "pointer",
|
|
44
|
-
textDecoration: "none",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const backLink = onBack ? (
|
|
48
|
-
<button type="button" data-testid="work-task-back" onClick={onBack} style={backLinkStyle}>
|
|
49
|
-
<ArrowLeft size={14} /> Back to project
|
|
50
|
-
</button>
|
|
51
|
-
) : (
|
|
52
|
-
<a data-testid="work-task-back" href={href} style={backLinkStyle}>
|
|
53
|
-
<ArrowLeft size={14} /> Back to project
|
|
54
|
-
</a>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
39
|
return (
|
|
58
|
-
<div
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
<div data-testid="work-task-detail" style={{ color: t.text, fontFamily: t.fontFamily }}>
|
|
41
|
+
<a
|
|
42
|
+
data-testid="work-task-back"
|
|
43
|
+
href={href}
|
|
44
|
+
className="fg:mb-4 fg:inline-flex fg:items-center fg:gap-1.5"
|
|
45
|
+
style={{ fontSize: 14, color: t.muted, textDecoration: "none" }}
|
|
46
|
+
onClick={
|
|
47
|
+
onBack
|
|
48
|
+
? (event: MouseEvent<HTMLAnchorElement>) => {
|
|
49
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
onBack();
|
|
52
|
+
}
|
|
53
|
+
: undefined
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<ArrowLeft size={14} aria-hidden /> {tr("forge.work_task_detail.back_to_project")}
|
|
57
|
+
</a>
|
|
63
58
|
|
|
64
59
|
{isLoading ? (
|
|
65
|
-
<Loading
|
|
60
|
+
<Loading />
|
|
66
61
|
) : (
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
) : null}
|
|
79
|
-
{task ? task.title : "Task"}
|
|
80
|
-
</h1>
|
|
81
|
-
{task?.dueDate && <span style={{ fontSize: 13, color: t.muted }}>Due {formatWorkDate(task.dueDate)}</span>}
|
|
82
|
-
</div>
|
|
62
|
+
<PortalHeading
|
|
63
|
+
title={<span data-testid="work-task-title">{task ? task.title : tr("forge.work_task_detail.fallback_title")}</span>}
|
|
64
|
+
lead={task?.dueDate ? tr("forge.work_task_detail.due_on", { date: formatDate(task.dueDate) }) : undefined}
|
|
65
|
+
actions={
|
|
66
|
+
task ? (
|
|
67
|
+
<span data-testid="work-task-status" style={{ fontSize: 14, color: t.muted }}>
|
|
68
|
+
{task.done ? tr("forge.work_task_detail.status_done") : tr("forge.work_task_detail.status_open")}
|
|
69
|
+
</span>
|
|
70
|
+
) : undefined
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
83
73
|
)}
|
|
84
74
|
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
<div style={{ padding: 20, border: `1px solid ${t.border}`, borderRadius: t.cornerRadius, background: t.surface }}>
|
|
75
|
+
<div className="fg:flex fg:flex-col fg:gap-10">
|
|
76
|
+
<WorkTaskAttachments projectId={projectId} taskId={taskId} />
|
|
88
77
|
<WorkTaskComments projectId={projectId} taskId={taskId} />
|
|
89
78
|
</div>
|
|
90
79
|
</div>
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
+
import { useForgeT } from "../../../i18n";
|
|
1
2
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
2
3
|
import { Loading } from "../Loading";
|
|
3
4
|
import { useWorkTokenReport } from "../../headless/work/useWorkPortal";
|
|
5
|
+
import { PortalEmpty } from "../portal/shared";
|
|
4
6
|
import { WorkReportBody } from "./WorkReportBody";
|
|
5
7
|
|
|
6
8
|
export interface WorkTokenReportProps {
|
|
7
|
-
/** Signed share token from the URL (`/i/
|
|
9
|
+
/** Signed share token from the URL (`/i/account/time-report/:token`). */
|
|
8
10
|
token: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Public, login-free tokenized share of a project's time report. The signed
|
|
13
|
-
* token IS the authorization
|
|
14
|
-
* a friendly not-found notice (the API returns 404). Built on
|
|
15
|
+
* token IS the authorization, so there is no auth guard. Invalid or expired
|
|
16
|
+
* tokens resolve to a friendly not-found notice (the API returns 404). Built on
|
|
15
17
|
* `useWorkTokenReport`; wraps `WorkReportBody`. Theme tokens only.
|
|
16
18
|
*/
|
|
17
19
|
export function WorkTokenReport({ token }: WorkTokenReportProps) {
|
|
18
20
|
const t = useThemeTokens();
|
|
21
|
+
const tr = useForgeT();
|
|
19
22
|
const { data: report, isLoading, isError } = useWorkTokenReport(token);
|
|
20
23
|
|
|
21
24
|
if (isLoading) {
|
|
@@ -24,19 +27,8 @@ export function WorkTokenReport({ token }: WorkTokenReportProps) {
|
|
|
24
27
|
|
|
25
28
|
if (isError || !report) {
|
|
26
29
|
return (
|
|
27
|
-
<div
|
|
28
|
-
|
|
29
|
-
style={{
|
|
30
|
-
border: `1px solid ${t.border}`,
|
|
31
|
-
borderRadius: t.cornerRadius,
|
|
32
|
-
background: t.surface,
|
|
33
|
-
padding: 32,
|
|
34
|
-
textAlign: "center",
|
|
35
|
-
color: t.text,
|
|
36
|
-
fontFamily: t.fontFamily,
|
|
37
|
-
}}
|
|
38
|
-
>
|
|
39
|
-
<p style={{ margin: 0, color: t.muted }}>This report link is invalid or has expired.</p>
|
|
30
|
+
<div data-testid="work-token-report-error" style={{ fontFamily: t.fontFamily }}>
|
|
31
|
+
<PortalEmpty title={tr("forge.work_token_report.invalid_link")} />
|
|
40
32
|
</div>
|
|
41
33
|
);
|
|
42
34
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { WorkPortalProjects, type WorkPortalProjectsProps } from "./WorkPortalProjects";
|
|
3
3
|
export { WorkProjectReport, type WorkProjectReportProps } from "./WorkProjectReport";
|
|
4
4
|
export { WorkTokenReport, type WorkTokenReportProps } from "./WorkTokenReport";
|
|
5
|
-
export { WorkReportBody, type WorkReportBodyProps } from "./WorkReportBody";
|
|
5
|
+
export { WorkReportBody, type WorkReportBodyProps, type WorkReportSection } from "./WorkReportBody";
|
|
6
6
|
export { WorkTaskComments, type WorkTaskCommentsProps } from "./WorkTaskComments";
|
|
7
7
|
export { WorkTaskAttachments, type WorkTaskAttachmentsProps } from "./WorkTaskAttachments";
|
|
8
8
|
export { WorkTaskDetail, type WorkTaskDetailProps } from "./WorkTaskDetail";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { readLanding } from "../landing";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SEO step 6: checkout credits a sale to the page the buyer first landed on.
|
|
7
|
+
* `readLanding` is spread into every Forge checkout body, so `landingPath` riding
|
|
8
|
+
* on it is what puts the pathname on the purchase row.
|
|
9
|
+
*/
|
|
10
|
+
function store(snapshot: Record<string, unknown>) {
|
|
11
|
+
window.localStorage.setItem("tn:landing", JSON.stringify(snapshot));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("readLanding", () => {
|
|
15
|
+
beforeEach(() => window.localStorage.clear());
|
|
16
|
+
|
|
17
|
+
it("returns the pathname of the first landing URL, without its query", () => {
|
|
18
|
+
store({
|
|
19
|
+
landingUtm: { utm_source: "tribenest" },
|
|
20
|
+
referrer: "https://instagram.com/",
|
|
21
|
+
landingUrl: "https://clayroom.example/i/blog/first-class?utm_source=tribenest#top",
|
|
22
|
+
at: Date.now(),
|
|
23
|
+
});
|
|
24
|
+
expect(readLanding()).toEqual({
|
|
25
|
+
landingUtm: { utm_source: "tribenest" },
|
|
26
|
+
referrer: "https://instagram.com/",
|
|
27
|
+
landingPath: "/i/blog/first-class",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("leaves landingPath undefined when the stored URL cannot be parsed", () => {
|
|
32
|
+
store({ landingUtm: {}, referrer: "", landingUrl: "not a url", at: Date.now() });
|
|
33
|
+
expect(readLanding()).toEqual({ landingUtm: undefined, referrer: undefined, landingPath: undefined });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns null when nothing is stored", () => {
|
|
37
|
+
expect(readLanding()).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|