Skip to content

Commit aa6a06d

Browse files
committed
feat: add scrollbar hiding option and fix calendar drag-create due date
Features: - Add UI setting to hide scrollbars for cleaner appearance - Add GlobalUiApplier component to apply global UI preferences - Extend globalViewOptions with hideScrollBar property Bug fixes: - Fix calendar drag-created task overwriting existing due dates - Add createQuickAddPrefillHelpers to prevent value overwrites Refactor: - Extract initial setup logic to shared runInitialSetup utility - Remove sponsor button from about modal Tests: - Add auth test for static ID behavior - Add test for calendar context drag with existing due date - Update sidepanel integration test with hideScrollBar
1 parent e7e6b2e commit aa6a06d

26 files changed

Lines changed: 617 additions & 186 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# tasktrove
22

3+
## 0.12.3
4+
5+
### Minor Changes
6+
7+
- Add UI settings for hiding scrollbar
8+
- Hover over task item no longer shows all metadata placehoder
9+
10+
### Patch Changes
11+
12+
🐛 Bug - fix calendar week mode drag created task wrong due date
13+
14+
## 0.12.1
15+
16+
### Patch Changes
17+
18+
🐛 Bug - fix auth bug preventing login (https://github.com/dohsimpson/TaskTrovePro/issues/238)
19+
320
## 0.12.0
421

522
### Features

apps/web/app/api/initial-setup/route.ts

Lines changed: 38 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import { NextResponse } from "next/server"
22
import { User } from "@tasktrove/types/core"
33
import { InitialSetupResponse } from "@tasktrove/types/api-responses"
4-
import { InitialSetupRequestSchema } from "@tasktrove/types/api-requests"
54
import { ErrorResponse } from "@tasktrove/types/api-responses"
6-
import { ApiErrorCode } from "@tasktrove/types/api-errors"
7-
import { validateRequestBody, createErrorResponse } from "@/lib/utils/validation"
85
import { safeReadDataFile, safeWriteDataFile } from "@/lib/utils/safe-file-operations"
96
import {
107
withApiLogging,
11-
logBusinessEvent,
128
withFileOperationLogging,
139
withPerformanceLogging,
1410
type EnhancedRequest,
1511
} from "@/lib/middleware/api-logger"
1612
import { withMutexProtection } from "@/lib/utils/api-mutex"
17-
import { saltAndHashPassword } from "@tasktrove/utils"
1813
import { initializeDataFileIfNeeded } from "@/lib/utils/data-initialization"
14+
import { runInitialSetup } from "@/lib/utils/initial-setup"
1915

2016
/**
2117
* POST /api/(auth)/initial-setup
@@ -26,113 +22,44 @@ import { initializeDataFileIfNeeded } from "@/lib/utils/data-initialization"
2622
async function initialSetup(
2723
request: EnhancedRequest,
2824
): Promise<NextResponse<InitialSetupResponse | ErrorResponse>> {
29-
// Validate request body
30-
const validation = await validateRequestBody(request, InitialSetupRequestSchema)
31-
if (!validation.success) {
32-
return validation.error
33-
}
34-
35-
const { password, username } = validation.data
36-
37-
// Read current data file to check existing password
38-
let fileData = await withFileOperationLogging(
39-
() => safeReadDataFile(),
40-
"read-data-file",
41-
request.context,
42-
)
43-
44-
// If data file read failed, try to initialize it and read again
45-
if (!fileData) {
46-
const initSuccess = await initializeDataFileIfNeeded()
47-
if (!initSuccess) {
48-
return createErrorResponse(
49-
"Failed to initialize data file",
50-
"Data file initialization failed",
51-
500,
52-
)
53-
}
54-
55-
// Retry reading the data file after initialization
56-
fileData = await withFileOperationLogging(
57-
() => safeReadDataFile(),
58-
"read-data-file-retry",
59-
request.context,
60-
)
61-
62-
if (!fileData) {
63-
return createErrorResponse(
64-
"Failed to read data file after initialization",
65-
"File reading failed after initialization",
66-
500,
67-
)
68-
}
69-
}
70-
71-
// Check if password is already set - only allow initial setup if password is empty
72-
if (fileData.user.password !== "") {
73-
return createErrorResponse(
74-
"Password already set",
75-
"Initial setup is only allowed when no password is currently set",
76-
409, // Conflict
77-
)
78-
}
79-
80-
// Hash the password
81-
let hashedPassword: string
82-
try {
83-
hashedPassword = saltAndHashPassword(password)
84-
} catch {
85-
return createErrorResponse(
86-
"Failed to hash password",
87-
"Password hashing failed",
88-
500,
89-
ApiErrorCode.INTERNAL_SERVER_ERROR,
90-
)
91-
}
92-
93-
// Update user with the new password (keeping existing username and avatar unless username is provided)
94-
const updatedUser: User = {
95-
...fileData.user,
96-
...(username && { username }),
97-
password: hashedPassword,
98-
}
99-
100-
// Update the data file with new user data
101-
const updatedFileData = {
102-
...fileData,
103-
user: updatedUser,
104-
}
105-
106-
// Write updated data to file
107-
const writeSuccess = await withPerformanceLogging(
108-
() => safeWriteDataFile({ data: updatedFileData }),
109-
"write-data-file",
110-
request.context,
111-
500, // 500ms threshold for slow file writes
112-
)
113-
114-
if (!writeSuccess) {
115-
return createErrorResponse(
116-
"Failed to save data",
117-
"File writing failed",
118-
500,
119-
ApiErrorCode.DATA_FILE_WRITE_ERROR,
120-
)
121-
}
122-
123-
logBusinessEvent(
124-
"initial_setup_completed",
125-
{
126-
username: updatedUser.username,
25+
type SafeDataFile = NonNullable<Awaited<ReturnType<typeof safeReadDataFile>>>
26+
27+
return runInitialSetup<SafeDataFile>({
28+
request,
29+
readData: () =>
30+
withFileOperationLogging(
31+
async () => (await safeReadDataFile()) ?? null,
32+
"read-data-file",
33+
request.context,
34+
),
35+
initializeIfNeeded: () => initializeDataFileIfNeeded(),
36+
isPasswordSet: (fileData) => fileData.user.password !== "",
37+
buildUpdatedData: (fileData, { passwordHash, username }) => {
38+
const updatedUser: User = {
39+
...fileData.user,
40+
...(username && { username }),
41+
password: passwordHash,
42+
}
43+
44+
return {
45+
updatedData: {
46+
...fileData,
47+
user: updatedUser,
48+
},
49+
logEvent: {
50+
username: updatedUser.username,
51+
},
52+
}
12753
},
128-
request.context,
129-
)
130-
131-
const response: InitialSetupResponse = {
132-
success: true,
133-
}
134-
135-
return NextResponse.json(response)
54+
writeData: (updatedFileData) =>
55+
withPerformanceLogging(
56+
() => safeWriteDataFile({ data: updatedFileData }),
57+
"write-data-file",
58+
request.context,
59+
500,
60+
),
61+
passwordAlreadySetMessage: "Initial setup is only allowed when no password is currently set",
62+
})
13663
}
13764

13865
export const POST = withMutexProtection(

apps/web/auth.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,24 @@ describe("auth module", () => {
152152
expect(result).toEqual({ id: "1", name: "Test User" })
153153
})
154154

155+
it("always returns static id 1 for base auth", async () => {
156+
const { verifyPassword, safeReadUserFile } = await loadAuthModule("test-secret")
157+
158+
safeReadUserFile.mockResolvedValue({
159+
user: {
160+
id: createUserId("f47ac10b-58cc-4372-a567-0e02b2c3d479"),
161+
username: "Another User",
162+
password: "hashed",
163+
},
164+
})
165+
verifyPassword.mockReturnValue(true)
166+
167+
const authorize = getAuthorize()
168+
const result = await authorize({ password: "correct" })
169+
170+
expect(result).toEqual({ id: "1", name: "Another User" })
171+
})
172+
155173
it("rejects when password is invalid", async () => {
156174
const { verifyPassword, safeReadUserFile } = await loadAuthModule("test-secret")
157175

apps/web/components/client-app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect } from "react"
44
import { SessionProvider } from "next-auth/react"
55
import { JotaiProvider } from "@/providers/index"
66
import { MainLayoutWrapper } from "@/components/layout/main-layout-wrapper"
7+
import { GlobalUiApplier } from "@/components/layout/global-ui-applier"
78
import { HydrateWrapper } from "@/providers/hydrate-wrapper"
89
import { LanguageProviderWrapper } from "@/components/providers/language-provider-wrapper"
910
import type { AppLanguage } from "@/lib/i18n/config"
@@ -42,6 +43,7 @@ export function ClientApp({ children, initialLanguage }: ClientAppProps) {
4243
<LanguageProviderWrapper initialLanguage={initialLanguage}>
4344
<JotaiProvider>
4445
<HydrateWrapper>
46+
<GlobalUiApplier />
4547
<MainLayoutWrapper>{children}</MainLayoutWrapper>
4648
</HydrateWrapper>
4749
</JotaiProvider>

apps/web/components/dialogs/about-modal.test.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ describe("AboutModal", () => {
4949
render(<AboutModal {...defaultProps} />)
5050

5151
expect(screen.getByRole("button", { name: /star on github/i })).toBeInTheDocument()
52-
expect(screen.getByRole("button", { name: /sponsor/i })).toBeInTheDocument()
5352
})
5453

5554
it("opens GitHub repository when Star on GitHub button is clicked", () => {
@@ -64,18 +63,6 @@ describe("AboutModal", () => {
6463
)
6564
})
6665

67-
it("opens GitHub sponsors when Sponsor button is clicked", () => {
68-
render(<AboutModal {...defaultProps} />)
69-
70-
const sponsorButton = screen.getByRole("button", { name: /sponsor/i })
71-
fireEvent.click(sponsorButton)
72-
73-
expect(mockWindowOpen).toHaveBeenCalledWith(
74-
`https://github.com/sponsors/${GITHUB_REPO_OWNER}`,
75-
"_blank",
76-
)
77-
})
78-
7966
it("renders author button that opens link", () => {
8067
render(<AboutModal {...defaultProps} />)
8168

apps/web/components/dialogs/about-modal.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
DialogDescription,
1010
} from "@/components/ui/dialog"
1111
import { Button } from "@/components/ui/button"
12-
import { Star, Heart } from "lucide-react"
12+
import { Star } from "lucide-react"
1313
import { TaskTroveLogo } from "@/components/ui/custom/tasktrove-logo"
1414
import { getAppVersion } from "@/lib/utils/version"
1515
import { GITHUB_REPO_NAME, GITHUB_REPO_OWNER } from "@/lib/constants/default"
@@ -25,7 +25,6 @@ export function AboutModal({ open, onOpenChange, extraVersionInfo }: AboutModalP
2525
const [version, setVersion] = useState<string | null>(null)
2626
const { t } = useTranslation("dialogs")
2727
const githubRepoUrl = `https://github.com/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}`
28-
const githubSponsorsUrl = `https://github.com/sponsors/${GITHUB_REPO_OWNER}`
2928

3029
useEffect(() => {
3130
void (async () => {
@@ -78,26 +77,16 @@ export function AboutModal({ open, onOpenChange, extraVersionInfo }: AboutModalP
7877
</Button>
7978
</div>
8079

81-
<div className="flex gap-2">
80+
<div className="flex justify-center">
8281
<Button
8382
variant="outline"
8483
size="sm"
85-
className="flex-1 cursor-pointer group"
84+
className="cursor-pointer group"
8685
onClick={() => window.open(githubRepoUrl, "_blank")}
8786
>
8887
<Star className="size-4 mr-2 text-yellow-600 group-hover:animate-[breathe_3s_ease-in-out_infinite]" />
8988
{t("about.starOnGitHub", "Star on GitHub")}
9089
</Button>
91-
92-
<Button
93-
variant="outline"
94-
size="sm"
95-
className="flex-1 cursor-pointer group"
96-
onClick={() => window.open(githubSponsorsUrl, "_blank")}
97-
>
98-
<Heart className="size-4 mr-2 text-pink-600 group-hover:animate-[breathe_3s_ease-in-out_infinite]" />
99-
{t("about.sponsorMe", "Sponsor Me")}
100-
</Button>
10190
</div>
10291
</div>
10392
<PrivacyTermsNotice className="pt-2" />

0 commit comments

Comments
 (0)