Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 31 additions & 29 deletions src/components/issue/Issue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,39 @@ import { useState } from "react";
import { useIntl } from "react-intl";
import { TIssue } from "../../api/redmine/types";
import { LocalIssue } from "../../hooks/useLocalIssues";
import { TimerController } from "../../hooks/useTimers";
import { Timer } from "../../hooks/useTimers";
import { useSettings } from "../../provider/SettingsProvider";
import { useTimerApi } from "../../provider/TimerApiProvider";
import { clsxm } from "../../utils/clsxm";
import HelpTooltip from "../general/HelpTooltip";
import { ToggleableCard } from "../general/ToggleableCard";
import Timer from "../timer/timer";
import { TimerComponents } from "../timer/timer";
import { IssueTitle, IssueTitleSkeleton } from "./IssueTitle";

type PropTypes = {
issue: TIssue;
localIssue: LocalIssue;
priorityType: PriorityType;
assignedToMe: boolean;
timers: TimerController[];
onAddTimer: () => void;
timers: Timer[];
};

const Issue = ({ issue, localIssue, priorityType, assignedToMe, timers, onAddTimer }: PropTypes) => {
const Issue = ({ issue, localIssue, priorityType, assignedToMe, timers }: PropTypes) => {
const { formatMessage } = useIntl();

const { settings } = useSettings();

const { hasProjectPermission } = usePermissions();
const canLogTime = hasProjectPermission(issue.project.id, "log_time");

const timerApi = useTimerApi();

const primaryTimer = timers[0]!;

const [areTimersExpanded, setAreTimersExpanded] = useState(false);

return (
<IssueContextMenu issue={issue} localIssue={localIssue} primaryTimer={primaryTimer} assignedToMe={assignedToMe} onAddTimer={onAddTimer}>
<IssueContextMenu issue={issue} localIssue={localIssue} primaryTimer={primaryTimer} assignedToMe={assignedToMe}>
<ToggleableCard
role="listitem"
data-type="issue"
Expand All @@ -50,7 +52,7 @@ const Issue = ({ issue, localIssue, priorityType, assignedToMe, timers, onAddTim
"border-priority-high-bg ring-priority-high-bg ring-1": priorityType === "high" || priorityType === "highest",
}
)}
{...(canLogTime && { onToggle: () => primaryTimer.toggleTimer() })}
{...(canLogTime && { onToggle: () => timerApi.toggleTimer(primaryTimer) })}
>
<IssueTitle
issue={issue}
Expand All @@ -70,13 +72,13 @@ const Issue = ({ issue, localIssue, priorityType, assignedToMe, timers, onAddTim
</div>
{canLogTime && !areTimersExpanded && (
<div>
<Timer.Root timer={primaryTimer} issue={issue}>
<Timer.Wrapper>
<Timer.Counter />
<Timer.ToggleButton />
<Timer.DoneButton canLogTime={canLogTime} />
</Timer.Wrapper>
</Timer.Root>
<TimerComponents.Root timer={primaryTimer} issue={issue}>
<TimerComponents.Wrapper>
<TimerComponents.Counter />
<TimerComponents.ToggleButton />
<TimerComponents.DoneButton canLogTime={canLogTime} />
</TimerComponents.Wrapper>
</TimerComponents.Root>
{timers.length > 1 && (
<button type="button" className="text-muted-foreground pl-3 text-xs" onClick={() => setAreTimersExpanded(true)}>
{formatMessage({ id: "issues.timers.more-timers" }, { count: timers.length - 1 })}
Expand All @@ -88,16 +90,16 @@ const Issue = ({ issue, localIssue, priorityType, assignedToMe, timers, onAddTim
{canLogTime && areTimersExpanded && (
<div className="mt-2 flex flex-col gap-y-1">
{timers.map((timer) => (
<Timer.Root key={timer.id} timer={timer} issue={issue}>
<Timer.ContextMenu>
<Timer.WrapperCard>
<Timer.NameField />
<Timer.Counter />
<Timer.ToggleButton />
<Timer.DoneButton canLogTime={canLogTime} />
</Timer.WrapperCard>
</Timer.ContextMenu>
</Timer.Root>
<TimerComponents.Root key={timer.id} timer={timer} issue={issue}>
<TimerComponents.ContextMenu>
<TimerComponents.WrapperCard>
<TimerComponents.NameField />
<TimerComponents.Counter />
<TimerComponents.ToggleButton />
<TimerComponents.DoneButton canLogTime={canLogTime} />
</TimerComponents.WrapperCard>
</TimerComponents.ContextMenu>
</TimerComponents.Root>
))}
</div>
)}
Expand Down Expand Up @@ -125,11 +127,11 @@ export const IssueSkeleton = () => (
<div className="mt-0.5">
<Skeleton className="h-5.5 w-20 rounded-sm" />
</div>
<Timer.Wrapper>
<Timer.Skeleton.Counter />
<Timer.Skeleton.ToggleButton />
<Timer.Skeleton.DoneButton />
</Timer.Wrapper>
<TimerComponents.Wrapper>
<TimerComponents.Skeleton.Counter />
<TimerComponents.Skeleton.ToggleButton />
<TimerComponents.Skeleton.DoneButton />
</TimerComponents.Wrapper>
</div>
</ToggleableCard>
);
Expand Down
19 changes: 10 additions & 9 deletions src/components/issue/IssueContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import { useIntl } from "react-intl";
import { toast } from "sonner";
import { TIssue } from "../../api/redmine/types";
import { LocalIssue } from "../../hooks/useLocalIssues";
import { TimerController } from "../../hooks/useTimers";
import { calculateTimerTotalElapsedTime, Timer } from "../../hooks/useTimers";
import { useSettings } from "../../provider/SettingsProvider";
import { useTimerApi } from "../../provider/TimerApiProvider";
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "../ui/context-menu";
import AddIssueNotesModal from "./AddIssueNotesModal";
import EditIssueModal from "./EditIssueModal";

type PropTypes = {
issue: TIssue;
localIssue: LocalIssue;
primaryTimer: TimerController;
primaryTimer: Timer;
assignedToMe: boolean;
onAddTimer: () => void;
};

export const IssueContextMenu = ({ children, ...props }: PropTypes & { children: ReactElement }) => {
Expand All @@ -52,9 +52,10 @@ export const IssueContextMenu = ({ children, ...props }: PropTypes & { children:
);
};

const IssueContextMenuItems = ({ issue, localIssue, primaryTimer, assignedToMe, onAddTimer, onEdit, onAddNotes }: PropTypes & { onEdit: () => void; onAddNotes: () => void }) => {
const IssueContextMenuItems = ({ issue, localIssue, primaryTimer, assignedToMe, onEdit, onAddNotes }: PropTypes & { onEdit: () => void; onAddNotes: () => void }) => {
const { formatMessage } = useIntl();
const { settings } = useSettings();
const timerApi = useTimerApi();

const { data: me } = useRedmineCurrentUser();

Expand Down Expand Up @@ -90,15 +91,15 @@ const IssueContextMenuItems = ({ issue, localIssue, primaryTimer, assignedToMe,
{formatMessage({ id: "issues.context-menu.add-notes" })}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={primaryTimer.isActive ? primaryTimer.pauseTimer : primaryTimer.startTimer} disabled={!canLogTime}>
{primaryTimer.isActive ? <TimerOffIcon /> : <TimerIcon />}
{formatMessage({ id: primaryTimer.isActive ? "timer.context-menu.pause" : "timer.context-menu.start" })}
<ContextMenuItem onClick={primaryTimer.activeSession ? () => timerApi.pauseTimer(primaryTimer) : () => timerApi.startTimer(primaryTimer)} disabled={!canLogTime}>
{primaryTimer.activeSession ? <TimerOffIcon /> : <TimerIcon />}
{formatMessage({ id: primaryTimer.activeSession ? "timer.context-menu.pause" : "timer.context-menu.start" })}
</ContextMenuItem>
<ContextMenuItem onClick={primaryTimer.resetTimer} disabled={primaryTimer.getElapsedTime() === 0 || !canLogTime}>
<ContextMenuItem onClick={() => timerApi.resetTimer(primaryTimer)} disabled={calculateTimerTotalElapsedTime(primaryTimer) === 0 || !canLogTime}>
<TimerResetIcon />
{formatMessage({ id: "timer.context-menu.reset" })}
</ContextMenuItem>
<ContextMenuItem onClick={onAddTimer} disabled={!canLogTime}>
<ContextMenuItem onClick={() => timerApi.addTimer(issue.id)} disabled={!canLogTime}>
<PlusIcon />
{formatMessage({ id: "timer.context-menu.add-timer" })}
</ContextMenuItem>
Expand Down
3 changes: 1 addition & 2 deletions src/components/issue/ProjectIssuesGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePermissions } from "@/provider/PermissionsProvider";
import { useSettings } from "@/provider/SettingsProvider";
import { clsxm } from "@/utils/clsxm";
import { ProjectIssuesGroup as ProjectIssuesGroupType } from "@/utils/groupIssues";
import { randomElement } from "@/utils/random";
import clsx from "clsx";
import { PinIcon, PlusIcon, SearchIcon, SquareChartGanttIcon, SquareMousePointerIcon, TimerIcon } from "lucide-react";
import { ComponentProps, Fragment, useState } from "react";
Expand All @@ -18,7 +19,6 @@ import { Badge } from "../ui/badge";
import CreateIssueModal from "./CreateIssueModal";
import Issue, { IssueSkeleton } from "./Issue";
import { useIssueSearch } from "./IssueSearch";
import { randomElement } from "@/utils/random";

interface ProjectIssuesGroupProps extends ComponentProps<"div"> {
projectGroup: ProjectIssuesGroupType;
Expand Down Expand Up @@ -47,7 +47,6 @@ export const ProjectIssuesGroup = ({ projectGroup, localIssues, timers, classNam
priorityType={getPriorityType(issue)}
assignedToMe={me ? me.id === issue.assigned_to?.id : true}
timers={timers.getTimersByIssue(issue.id)}
onAddTimer={() => timers.addTimer(issue.id)}
/>
))}
</Fragment>
Expand Down
4 changes: 2 additions & 2 deletions src/components/time-entry/CreateTimeEntryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRedmineProjectTimeEntryActivities } from "@/api/redmine/hooks/useRed
import { redmineIssuesQueries } from "@/api/redmine/queries/issues";
import { redmineTimeEntriesQueries } from "@/api/redmine/queries/timeEntries";
import { usePersistentComments } from "@/hooks/usePersistentComments";
import { TimerController } from "@/hooks/useTimers";
import { Timer } from "@/hooks/useTimers";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { startOfDay } from "date-fns";
import { useIntl } from "react-intl";
Expand All @@ -24,7 +24,7 @@ import UserField from "./form/fields/UserField";
import TimeEntryPreview from "./TimeEntryPreview";

type PropTypes = {
timer: TimerController;
timer: Timer;
issue: TIssue;
initialValues: Partial<TCreateTimeEntryForm>;
onClose: () => void;
Expand Down
24 changes: 12 additions & 12 deletions src/components/timer/CurrentIssueTimer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useRedmineIssue } from "@/api/redmine/hooks/useRedmineIssue";
import { usePermissions } from "@/provider/PermissionsProvider";
import useRedmineUrl from "../../hooks/useRedmineUrl";
import useTimers from "../../hooks/useTimers";
import Timer from "./timer";
import useTimers, { calculateTimerTotalElapsedTime } from "../../hooks/useTimers";
import { TimerComponents } from "./timer";

type PropTypes = {
issueId: number;
Expand All @@ -20,18 +20,18 @@ const CurrentIssueTimerInner = ({ issueId }: PropTypes) => {
const issueTimers = timers.getTimersByIssue(issue.id);
const primaryTimer = issueTimers[0]!;

if (!canLogTime && primaryTimer.getElapsedTime() === 0) return;
if (!canLogTime && calculateTimerTotalElapsedTime(primaryTimer) === 0) return;

return (
<Timer.Root timer={primaryTimer} issue={issue}>
<Timer.ContextMenu>
<Timer.WrapperCard>
<Timer.Counter />
<Timer.ToggleButton />
<Timer.DoneButton canLogTime={canLogTime} />
</Timer.WrapperCard>
</Timer.ContextMenu>
</Timer.Root>
<TimerComponents.Root timer={primaryTimer} issue={issue}>
<TimerComponents.ContextMenu>
<TimerComponents.WrapperCard>
<TimerComponents.Counter />
<TimerComponents.ToggleButton />
<TimerComponents.DoneButton canLogTime={canLogTime} />
</TimerComponents.WrapperCard>
</TimerComponents.ContextMenu>
</TimerComponents.Root>
);
};

Expand Down
41 changes: 23 additions & 18 deletions src/components/timer/ProjectTimersGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useRedmineIssuePriorities } from "@/api/redmine/hooks/useRedmineIssuePriorities";
import { ToggleableCard } from "@/components/general/ToggleableCard";
import { IssueTitle, IssueTitleSkeleton } from "@/components/issue/IssueTitle";
import Timer from "@/components/timer/timer";
import { TimerComponents } from "@/components/timer/timer";
import { Skeleton } from "@/components/ui/skeleton";
import { usePermissions } from "@/provider/PermissionsProvider";
import { useSettings } from "@/provider/SettingsProvider";
import { useTimerApi } from "@/provider/TimerApiProvider";
import { clsxm } from "@/utils/clsxm";
import { ProjectTimersGroup as ProjectTimersGroupType } from "@/utils/groupTimers";
import { randomElement } from "@/utils/random";
Expand All @@ -21,6 +22,8 @@ interface ProjectTimersGroupProps extends ComponentProps<"div"> {
export const ProjectTimersGroup = ({ projectGroup, className, ...props }: ProjectTimersGroupProps) => {
const { settings } = useSettings();

const timerApi = useTimerApi();

const { hasProjectPermission } = usePermissions();

const { getPriorityType } = useRedmineIssuePriorities({ enabled: settings.style.showIssuesPriority });
Expand All @@ -29,19 +32,20 @@ export const ProjectTimersGroup = ({ projectGroup, className, ...props }: Projec
<div {...props} className={clsxm("flex flex-col gap-y-2", className)}>
<TimerProject type={projectGroup.type} project={projectGroup.project} />
{projectGroup.items.map(({ timer, issue }) => (
<Timer.Root key={timer.id} timer={timer} issue={issue}>
<Timer.ContextMenu>
<ToggleableCard role="listitem" data-type="timer-card" className="flex flex-col gap-1" onToggle={() => timer.toggleTimer()}>
<TimerComponents.Root key={timer.id} timer={timer} issue={issue}>
<TimerComponents.ContextMenu>
<ToggleableCard role="listitem" data-type="timer-card" className="flex flex-col gap-1" onToggle={() => timerApi.toggleTimer(timer)}>
{issue ? <IssueTitle issue={issue} priorityType={getPriorityType(issue)} /> : <h1 className="truncate text-gray-500 line-through">#{timer.issueId}</h1>}
<Timer.Wrapper>
<Timer.NameField />
<Timer.Counter />
<Timer.ToggleButton />
<Timer.DoneButton canLogTime={issue ? hasProjectPermission(issue.project.id, "log_time") : false} />
</Timer.Wrapper>
<TimerComponents.Wrapper>
<TimerComponents.NameField />
<TimerComponents.Counter />
<TimerComponents.ToggleButton />
<TimerComponents.DoneButton canLogTime={issue ? hasProjectPermission(issue.project.id, "log_time") : false} />
</TimerComponents.Wrapper>
<TimerComponents.Sessions />
</ToggleableCard>
</Timer.ContextMenu>
</Timer.Root>
</TimerComponents.ContextMenu>
</TimerComponents.Root>
))}
</div>
);
Expand Down Expand Up @@ -78,12 +82,13 @@ export const ProjectTimersGroupSkeleton = ({ groups }: { groups: number[] }) =>
{groups.map((key) => (
<ToggleableCard key={key} className="flex flex-col gap-1">
<IssueTitleSkeleton />
<Timer.Wrapper>
<Timer.Skeleton.NameField />
<Timer.Skeleton.Counter />
<Timer.Skeleton.ToggleButton />
<Timer.Skeleton.DoneButton />
</Timer.Wrapper>
<TimerComponents.Wrapper>
<TimerComponents.Skeleton.NameField />
<TimerComponents.Skeleton.Counter />
<TimerComponents.Skeleton.ToggleButton />
<TimerComponents.Skeleton.DoneButton />
</TimerComponents.Wrapper>
<TimerComponents.Skeleton.Sessions />
</ToggleableCard>
))}
</div>
Expand Down
14 changes: 8 additions & 6 deletions src/components/timer/timer/TimerContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useTimerApi } from "@/provider/TimerApiProvider";
import { PencilIcon, TimerIcon, TimerOffIcon, TimerResetIcon, TrashIcon } from "lucide-react";
import { ReactElement } from "react";
import { useIntl } from "react-intl";
Expand All @@ -17,23 +18,24 @@ export const TimerContextMenu = ({ children }: { children: ReactElement }) => {

const TimerContextMenuItems = () => {
const { formatMessage } = useIntl();
const { timer, setIsEditing } = useTimerContext();
const timerApi = useTimerApi();
const { timer, totalElapsedTime, setIsEditing } = useTimerContext();

return (
<>
<ContextMenuItem onClick={timer.isActive ? timer.pauseTimer : timer.startTimer}>
{timer.isActive ? <TimerOffIcon /> : <TimerIcon />}
{formatMessage({ id: timer.isActive ? "timer.context-menu.pause" : "timer.context-menu.start" })}
<ContextMenuItem onClick={timer.activeSession ? () => timerApi.pauseTimer(timer) : () => timerApi.startTimer(timer)}>
{timer.activeSession ? <TimerOffIcon /> : <TimerIcon />}
{formatMessage({ id: timer.activeSession ? "timer.context-menu.pause" : "timer.context-menu.start" })}
</ContextMenuItem>
<ContextMenuItem onClick={() => setIsEditing(true)}>
<PencilIcon />
{formatMessage({ id: "timer.context-menu.edit" })}
</ContextMenuItem>
<ContextMenuItem onClick={timer.resetTimer} disabled={timer.getElapsedTime() === 0}>
<ContextMenuItem onClick={() => timerApi.resetTimer(timer)} disabled={totalElapsedTime === 0}>
<TimerResetIcon />
{formatMessage({ id: "timer.context-menu.reset" })}
</ContextMenuItem>
<ContextMenuItem onClick={timer.deleteTimer}>
<ContextMenuItem onClick={() => timerApi.deleteTimer(timer)}>
<TrashIcon />
{formatMessage({ id: "timer.context-menu.delete" })}
</ContextMenuItem>
Expand Down
Loading
Loading