Repository: operately/operately
Pull request #2374: feat: Introduce edit timeframe button on the goal page
Author: login: shiroyasha
Created: 2025-05-05T06:34:26Z | Merged: 2025-05-05T06:51:31Z



Diff:
diff --git a/turboui/src/GoalPage/Contributors.tsx b/turboui/src/GoalPage/Contributors.tsx
index f22e1b8a53..6f8c4c60a7 100644
--- a/turboui/src/GoalPage/Contributors.tsx
+++ b/turboui/src/GoalPage/Contributors.tsx
@@ -12,7 +12,7 @@ export function Contributors(props: GoalPage.Props) {
 
   return (
     <div>
-      <SectionHeader title="Contributors" buttons={[<Info />]} showButtons={true} />
+      <SectionHeader title="Contributors" buttons={<Info />} showButtons={true} />
 
       <div className="mt-4">
         {props.contributors!.map((c) => (
diff --git a/turboui/src/GoalPage/Sidebar.tsx b/turboui/src/GoalPage/Sidebar.tsx
index cbe77827bd..afcd6f8546 100644
--- a/turboui/src/GoalPage/Sidebar.tsx
+++ b/turboui/src/GoalPage/Sidebar.tsx
@@ -2,15 +2,18 @@ import { GoalPage } from ".";
 import { Chronometer } from "../Chronometer";
 import { IconQuestionMark } from "@tabler/icons-react";
 import { Avatar, AvatarPerson } from "../Avatar";
+import { SectionHeader } from "./SectionHeader";
+import { SecondaryButton } from "../Button";
+import { TimeframeSelectorDialog } from "../TimeframeSelectorDialog";
+import { useState } from "react";
+import { Timeframe } from "../utils/timeframes";
+import { Trigger } from "@radix-ui/react-popover";
+import { set } from "date-fns";
 
 export function Sidebar(props: GoalPage.Props) {
   return (
     <div className="sm:col-span-3 space-y-6 hidden sm:block">
-      <div>
-        <div className="font-bold mb-2">Timeline</div>
-
-        <Chronometer start={props.startDate} end={props.endDate} color="stone" />
-      </div>
+      <Timeline {...props} />
 
       <div className="space-y-3">
         <div className="font-bold">Team</div>
@@ -22,6 +25,42 @@ export function Sidebar(props: GoalPage.Props) {
   );
 }
 
+function Timeline(props: GoalPage.Props) {
+  const [open, setOpen] = useState(false);
+  const [timeframe, setTimeframe] = useState<Timeframe>(props.timeframe);
+
+  const handleTimeframeChange = (timeframe: Timeframe) => {
+    setTimeframe(timeframe);
+    props.updateTimeframe(timeframe);
+  };
+
+  const edit = (
+    <TimeframeSelectorDialog
+      open={open}
+      onOpenChange={setOpen}
+      timeframe={timeframe}
+      setTimeframe={handleTimeframeChange}
+      alignContent="center"
+      trigger={
+        <Trigger>
+          <SecondaryButton size="xxs" onClick={() => setOpen(true)}>
+            Edit
+          </SecondaryButton>
+        </Trigger>
+      }
+    />
+  );
+
+  return (
+    <div>
+      <SectionHeader title="Timeline" buttons={edit} showButtons={props.canEdit} />
+      <div className="mt-2">
+        <Chronometer start={timeframe.startDate!} end={timeframe.endDate!} color="stone" />
+      </div>
+    </div>
+  );
+}
+
 function Champion(props: GoalPage.Props) {
   if (!props.champion) {
     const message = props.canEdit ? "Assign a champion to get started" : "No champion assigned";
diff --git a/turboui/src/GoalPage/index.stories.tsx b/turboui/src/GoalPage/index.stories.tsx
index c6bb7aca8f..acd237ff50 100644
--- a/turboui/src/GoalPage/index.stories.tsx
+++ b/turboui/src/GoalPage/index.stories.tsx
@@ -3,6 +3,8 @@ import { GoalPage } from ".";
 import { genPeople } from "../utils/storybook/genPeople";
 import { genRelativeDate } from "../utils/storybook/genDates";
 import { ro } from "date-fns/locale";
+import { currentQuarter } from "../utils/timeframes";
+import { fn } from "@storybook/test";
 
 const meta: Meta<typeof GoalPage> = {
   title: "Pages/GoalPage",
@@ -257,8 +259,7 @@ const defaultArgs = {
   reviewer: reviewer,
   targets: mockTargets,
   relatedWorkItems: relatedWorkItems,
-  startDate: genRelativeDate(-15),
-  endDate: genRelativeDate(15),
+  timeframe: currentQuarter(),
   contributors: contributors,
   canEdit: true,
   description,
@@ -269,6 +270,8 @@ const defaultArgs = {
   closedOn: null,
   retrospectiveLink: null,
   privacyLevel: "internal" as const,
+
+  updateTimeframe: fn(),
 };
 
 export default meta;
diff --git a/turboui/src/GoalPage/index.tsx b/turboui/src/GoalPage/index.tsx
index c81533da72..0d23c00c3e 100644
--- a/turboui/src/GoalPage/index.tsx
+++ b/turboui/src/GoalPage/index.tsx
@@ -16,6 +16,7 @@ import { RelatedWork } from "./RelatedWork";
 import { BadgeStatus } from "../StatusBadge/types";
 import { Contributors } from "./Contributors";
 import { WarningCallout } from "../Callouts";
+import { Timeframe } from "../utils/timeframes";
 
 export namespace GoalPage {
   interface Person {
@@ -60,10 +61,9 @@ export namespace GoalPage {
     reviewer?: Person;
     contributors: Contributor[];
 
+    timeframe: Timeframe;
     targets: GoalTargetList.Target[];
     relatedWorkItems: MiniWorkMap.WorkItem[];
-    startDate: Date;
-    endDate: Date;
     checkIns: CheckIn[];
     messages: Message[];
     status: BadgeStatus;
@@ -75,6 +75,8 @@ export namespace GoalPage {
     privacyLevel: "public" | "internal" | "confidential" | "secret";
 
     neglectedGoal?: boolean;
+
+    updateTimeframe: (timeframe: Timeframe) => Promise<void>;
   }
 
   export interface CheckIn {
diff --git a/turboui/src/TimeframeSelectorDialog/index.tsx b/turboui/src/TimeframeSelectorDialog/index.tsx
index d9bd8c045a..a06faa9e1b 100644
--- a/turboui/src/TimeframeSelectorDialog/index.tsx
+++ b/turboui/src/TimeframeSelectorDialog/index.tsx
@@ -33,7 +33,7 @@ export namespace TimeframeSelectorDialog {
     trigger?: React.ReactNode;
     timeframe: Timeframe;
     setTimeframe: SetTimeframe;
-    alignContent?: "start" | "end";
+    alignContent?: "start" | "center" | "end";
 
     open: boolean;
     onOpenChange: (open: boolean) => void;


Reviews:
user: login: sourcery-ai[bot]
body: Hey @shiroyasha - I've reviewed your changes - here's some feedback:

- Consider moving the unrelated change in Contributors.tsx to a separate pull request.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Review instructions**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T06:35:47Z

Review comments:
diff_hunk: @@ -22,6 +25,42 @@ export function Sidebar(props: GoalPage.Props) {
   );
 }
 
+function Timeline(props: GoalPage.Props) {
+  const [open, setOpen] = useState(false);
+  const [timeframe, setTimeframe] = useState<Timeframe>(props.timeframe);
path: turboui/src/GoalPage/Sidebar.tsx
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Consider syncing local timeframe state with props changes

Add a useEffect to sync the local timeframe state with prop changes to prevent stale state.

Suggested implementation:

```typescript
  const [timeframe, setTimeframe] = useState<Timeframe>(props.timeframe);
  useEffect(() => {
    setTimeframe(props.timeframe);
  }, [props.timeframe]);

```

Ensure that useEffect is imported from React along with useState if it isn’t already imported at the top of the file.
created_at: 2025-05-05T06:35:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +25,42 @@ export function Sidebar(props: GoalPage.Props) {
   );
 }
 
+function Timeline(props: GoalPage.Props) {
path: turboui/src/GoalPage/Sidebar.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the Timeline component into its own file to improve modularity and reduce complexity in the Sidebar component.

```markdown
Consider extracting the Timeline component into its own file. This will isolate its internal state and nested JSX from the Sidebar, making both files easier to understand and maintain. For example:

// Timeline.tsx
```tsx
import { useState } from "react";
import { Chronometer } from "../Chronometer";
import { SecondaryButton } from "../Button";
import { TimeframeSelectorDialog } from "../TimeframeSelectorDialog";
import { SectionHeader } from "./SectionHeader";
import { Trigger } from "@radix-ui/react-popover";
import { Timeframe } from "../utils/timeframes";
import { GoalPage } from ".";

export function Timeline(props: GoalPage.Props) {
  const [open, setOpen] = useState(false);
  const [timeframe, setTimeframe] = useState<Timeframe>(props.timeframe);

  const handleTimeframeChange = (newTimeframe: Timeframe) => {
    setTimeframe(newTimeframe);
    props.updateTimeframe(newTimeframe);
  };

  return (
    <div>
      <SectionHeader
        title="Timeline"
        buttons={
          <TimeframeSelectorDialog
            open={open}
            onOpenChange={setOpen}
            timeframe={timeframe}
            setTimeframe={handleTimeframeChange}
            alignContent="center"
            trigger={
              <Trigger>
                <SecondaryButton size="xxs" onClick={() => setOpen(true)}>
                  Edit
                </SecondaryButton>
              </Trigger>
            }
          />
        }
        showButtons={props.canEdit}
      />
      <div className="mt-2">
        <Chronometer start={timeframe.startDate!} end={timeframe.endDate!} color="stone" />
      </div>
    </div>
  );
}
```

// Sidebar.tsx
```tsx
import { GoalPage } from ".";
import { Chronometer } from "../Chronometer";
import { IconQuestionMark } from "@tabler/icons-react";
import { Avatar, AvatarPerson } from "../Avatar";
import { SectionHeader } from "./SectionHeader";
import { Champion, Reviewer } from "./TeamComponents";
import { Timeline } from "./Timeline";

export function Sidebar(props: GoalPage.Props) {
  return (
    <div className="sm:col-span-3 space-y-6 hidden sm:block">
      <Timeline {...props} />
      <div className="space-y-3">
        <div className="font-bold">Team</div>
        <Champion {...props} />
        <Reviewer {...props} />
      </div>
    </div>
  );
}
```

**Actionable Steps:**

1. **Create a new file `Timeline.tsx`:** Move the Timeline component code to this new file.
2. **Update imports in Sidebar:** Remove Timeline logic from Sidebar and import the new Timeline component.
3. **Test functionality:** Verify that Timeline behavior remains intact after the refactor.

This separation keeps the Sidebar focused and reduces nested complexity within a single file.
```
created_at: 2025-05-05T06:35:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-05T06:35:13Z
body: ## Deploying operately-design with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>79e58ed</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://3863c5e8.operately-design.pages.dev'>https://3863c5e8.operately-design.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://edit-timeframe.operately-design.pages.dev'>https://edit-timeframe.operately-design.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/ae90af09f224146877d86e9d35d0a8ce/pages/view/operately-design/3863c5e8-cc48-4625-84d4-cca292afee06)

author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-05-05T06:35:17Z
body: ## Deploying storybooks with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>79e58ed</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://3e7c4ab9.storybooks-aq1.pages.dev'>https://3e7c4ab9.storybooks-aq1.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://edit-timeframe.storybooks-aq1.pages.dev'>https://edit-timeframe.storybooks-aq1.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/ae90af09f224146877d86e9d35d0a8ce/pages/view/storybooks/3e7c4ab9-1bd4-4845-a2a5-9324c965550d)

author_association: NONE