feat: subtask indicators, drag & drop fixes, font layout, someday scroll & Google Tasks sync
- Move subtask indicator badge outside text span for visibility - Prevent parent task from hijacking subtask drag events - Restructure font settings to 2-row layout (font dropdown + size/weight) - Fix someday horizontal scroll with callback ref wheel handler - Sync new tasks in Google-linked someday lists to Google Tasks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> v1.14.0
This commit is contained in:
parent
3ddfc6af79
commit
3d75d7b39b
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.13.1",
|
"version": "1.14.0",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -252,6 +252,49 @@ export async function POST(request: NextRequest) {
|
|||||||
isRolling = user.autoRolling || false;
|
isRolling = user.autoRolling || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if someday list is linked to an external provider
|
||||||
|
let externalId: string | null = null;
|
||||||
|
let externalProvider: string | null = null;
|
||||||
|
let externalListId: string | null = null;
|
||||||
|
|
||||||
|
if (somedayListId) {
|
||||||
|
const somedayList = await prisma.somedayList.findUnique({
|
||||||
|
where: { id: somedayListId },
|
||||||
|
select: { externalId: true, externalProvider: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (somedayList?.externalId && somedayList?.externalProvider) {
|
||||||
|
try {
|
||||||
|
if (somedayList.externalProvider === 'google') {
|
||||||
|
const { createGoogleClient, createGoogleTask } = await import('@/lib/google-tasks');
|
||||||
|
const account = await prisma.account.findFirst({
|
||||||
|
where: { userId, provider: { in: ['google-calendar', 'google'] } },
|
||||||
|
});
|
||||||
|
if (account?.access_token) {
|
||||||
|
const client = createGoogleClient(account.access_token, account.refresh_token || undefined);
|
||||||
|
const gTask = await createGoogleTask(client, somedayList.externalId, { title });
|
||||||
|
externalId = gTask.id;
|
||||||
|
externalProvider = 'google';
|
||||||
|
externalListId = somedayList.externalId;
|
||||||
|
}
|
||||||
|
} else if (somedayList.externalProvider === 'outlook') {
|
||||||
|
const { createMsTodoTask } = await import('@/lib/microsoft-todo');
|
||||||
|
const { getOutlookAccessToken } = await import('@/lib/outlook-token');
|
||||||
|
const accessToken = await getOutlookAccessToken(userId);
|
||||||
|
if (accessToken) {
|
||||||
|
const msTask = await createMsTodoTask(accessToken, somedayList.externalId, { title });
|
||||||
|
externalId = msTask.id;
|
||||||
|
externalProvider = 'outlook';
|
||||||
|
externalListId = somedayList.externalId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (syncError) {
|
||||||
|
console.error('Failed to sync new task to external provider:', syncError);
|
||||||
|
// Continue creating locally even if sync fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const task = await prisma.task.create({
|
const task = await prisma.task.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
@ -273,6 +316,7 @@ export async function POST(request: NextRequest) {
|
|||||||
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
|
somedaySlotIndex: somedaySlotIndex !== undefined ? parseInt(somedaySlotIndex) : null,
|
||||||
parentTaskId: parentTaskId || null,
|
parentTaskId: parentTaskId || null,
|
||||||
...(projectId !== undefined && { projectId: projectId || null }),
|
...(projectId !== undefined && { projectId: projectId || null }),
|
||||||
|
...(externalId && { externalId, externalProvider, externalListId }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1453,7 +1453,6 @@ h3 {
|
|||||||
.weekly-someday-list-title-input {
|
.weekly-someday-list-title-input {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
color: var(--weekly-text, #222);
|
color: var(--weekly-text, #222);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@ -1012,6 +1012,7 @@ export default function WeeklyView() {
|
|||||||
const isScrollSyncing = useRef(false);
|
const isScrollSyncing = useRef(false);
|
||||||
const dayHeaderRef = useRef<HTMLElement>(null);
|
const dayHeaderRef = useRef<HTMLElement>(null);
|
||||||
const somedayGridRef = useRef<HTMLDivElement>(null);
|
const somedayGridRef = useRef<HTMLDivElement>(null);
|
||||||
|
const somedaySectionRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
// Scroll sync handler
|
// Scroll sync handler
|
||||||
const handleTimeColumnScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleTimeColumnScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
@ -1477,40 +1478,42 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
// Horizontal scroll: convert vertical wheel to horizontal in someday area
|
||||||
const el = somedayGridRef.current;
|
// Callback ref ensures handler is attached as soon as element mounts
|
||||||
if (!el) return;
|
const somedayWheelCleanup = useRef<(() => void) | null>(null);
|
||||||
|
const somedaySectionRefCb = useCallback((node: HTMLElement | null) => {
|
||||||
|
// Cleanup previous
|
||||||
|
if (somedayWheelCleanup.current) {
|
||||||
|
somedayWheelCleanup.current();
|
||||||
|
somedayWheelCleanup.current = null;
|
||||||
|
}
|
||||||
|
somedaySectionRef.current = node;
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
const handler = (e: WheelEvent) => {
|
const handler = (e: WheelEvent) => {
|
||||||
// Ignore if scrolling horizontally natively (trackpad)
|
const grid = somedayGridRef.current;
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
// Let native horizontal scroll (trackpad) pass through
|
||||||
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
|
||||||
|
if (e.deltaY === 0) return;
|
||||||
|
|
||||||
// Check if hovering over a vertically scrollable list that isn't at its boundary
|
// Only convert if grid has horizontal overflow
|
||||||
let target = e.target as HTMLElement | null;
|
if (grid.scrollWidth <= grid.clientWidth + 1) return;
|
||||||
let canScrollVertically = false;
|
|
||||||
|
|
||||||
while (target && target !== el) {
|
// Check boundaries - allow page scroll when at edges
|
||||||
if (target.scrollHeight > target.clientHeight) {
|
const atLeft = grid.scrollLeft <= 0;
|
||||||
const style = window.getComputedStyle(target);
|
const atRight = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 1;
|
||||||
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
|
if (e.deltaY < 0 && atLeft) return;
|
||||||
const atTop = target.scrollTop <= 0;
|
if (e.deltaY > 0 && atRight) return;
|
||||||
const atBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 1;
|
|
||||||
if (!(e.deltaY < 0 && atTop) && !(e.deltaY > 0 && atBottom)) {
|
|
||||||
canScrollVertically = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target = target.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canScrollVertically && e.deltaY !== 0) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
grid.scrollLeft += e.deltaY;
|
||||||
el.scrollLeft += e.deltaY;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
el.addEventListener("wheel", handler, { passive: false });
|
|
||||||
return () => el.removeEventListener("wheel", handler);
|
node.addEventListener("wheel", handler, { passive: false });
|
||||||
}, [showSomeday, somedayExpanded]);
|
somedayWheelCleanup.current = () => node.removeEventListener("wheel", handler);
|
||||||
|
}, []);
|
||||||
const saveSetting = async (key: string, value: any) => {
|
const saveSetting = async (key: string, value: any) => {
|
||||||
// Per-device settings: save to cookie ONLY (not DB) so each device keeps its own value
|
// Per-device settings: save to cookie ONLY (not DB) so each device keeps its own value
|
||||||
if (DEVICE_SETTINGS_KEYS.includes(key)) {
|
if (DEVICE_SETTINGS_KEYS.includes(key)) {
|
||||||
@ -3511,31 +3514,6 @@ export default function WeeklyView() {
|
|||||||
setDropPreview(null);
|
setDropPreview(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Added: Horizontal scroll with mouse wheel for someday area
|
|
||||||
useEffect(() => {
|
|
||||||
const somedayGrid = somedayGridRef.current;
|
|
||||||
if (!somedayGrid) return;
|
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
|
||||||
// Only handle if there is horizontal overflow
|
|
||||||
if (somedayGrid.scrollWidth > somedayGrid.clientWidth) {
|
|
||||||
// Dominant vertical scroll -> horizontal scroll
|
|
||||||
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
||||||
// Check if we can scroll further in the requested direction
|
|
||||||
const isAtLeft = somedayGrid.scrollLeft === 0;
|
|
||||||
const isAtRight = Math.abs(somedayGrid.scrollWidth - somedayGrid.clientWidth - somedayGrid.scrollLeft) < 1;
|
|
||||||
|
|
||||||
if ((e.deltaY < 0 && !isAtLeft) || (e.deltaY > 0 && !isAtRight)) {
|
|
||||||
e.preventDefault();
|
|
||||||
somedayGrid.scrollLeft += e.deltaY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
somedayGrid.addEventListener("wheel", handleWheel, { passive: false });
|
|
||||||
return () => somedayGrid.removeEventListener("wheel", handleWheel);
|
|
||||||
}, [somedayExpanded]);
|
|
||||||
|
|
||||||
const handleSomedayDragOver = (e: React.DragEvent, listId: string, slotIdx: number) => {
|
const handleSomedayDragOver = (e: React.DragEvent, listId: string, slotIdx: number) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -5345,6 +5323,7 @@ export default function WeeklyView() {
|
|||||||
{/* Someday Section */}
|
{/* Someday Section */}
|
||||||
{showSomeday && (
|
{showSomeday && (
|
||||||
<section
|
<section
|
||||||
|
ref={somedaySectionRefCb}
|
||||||
className={`weekly-someday ${somedayExpanded ? "expanded" : "collapsed"} transition-colors duration-200`}
|
className={`weekly-someday ${somedayExpanded ? "expanded" : "collapsed"} transition-colors duration-200`}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", flexDirection: "row", maxWidth: "100%", width: "100%" }}>
|
<div style={{ display: "flex", flexDirection: "row", maxWidth: "100%", width: "100%" }}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user