Skip to content
Draft
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
493 changes: 474 additions & 19 deletions frontend/package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,15 @@
"@openproject/reactivestates": "^3.0.1",
"@primer/css": "^22.0.2",
"@primer/live-region-element": "^0.8.0",
"@primer/octicons-react": "^19.21.1",
"@primer/primitives": "^11.3.2",
"@primer/react": "^38.4.0",
"@primer/view-components": "npm:@openproject/primer-view-components@^0.78.1",
"@rails/request.js": "^0.0.13",
"@stimulus-components/auto-submit": "^6.0.0",
"@stimulus-components/reveal": "^5.0.0",
"@tiptap/extensions": "^3.13.0",
"@tanstack/react-query": "^5.90.12",
"@tiptap/extensions": "^3.11.0",
"@types/hotwired__turbo": "^8.0.1",
"@types/jquery.cookie": "^1.4.36",
"@uirouter/angular": "^17.0.0",
Expand Down Expand Up @@ -167,7 +170,10 @@
"pako": "^2.0.3",
"qr-creator": "^1.0.0",
"react": "^19.2.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.2.1",
"react-is": "^19.2.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.0",
"screenfull": "^6.0.2",
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/react/backlogs/BacklogMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { KebabHorizontalIcon } from '@primer/octicons-react';
import { ActionList, ActionMenu, IconButton } from '@primer/react';

interface BacklogMenuProps {
onNewStory:() => void;
}

export function BacklogMenu({ onNewStory }:BacklogMenuProps) {
return (
<ActionMenu>
<ActionMenu.Anchor>
<IconButton icon={KebabHorizontalIcon} aria-label="Open menu" />
</ActionMenu.Anchor>
<ActionMenu.Overlay width="small">
<ActionList aria-label="Watch preference options">
<ActionList.Item onClick={onNewStory}>
New Story
</ActionList.Item>
<ActionList.Item>
Stories/Tasks
</ActionList.Item>
<ActionList.Item>
Task board
</ActionList.Item>
<ActionList.Item>
Burndown Chart
</ActionList.Item>
<ActionList.Item>
Wiki
</ActionList.Item>
<ActionList.Item>
Properties
</ActionList.Item>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
);
}


188 changes: 188 additions & 0 deletions frontend/src/react/backlogs/BacklogTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { ChevronUpIcon, ChevronDownIcon, CheckIcon, PencilIcon, UndoIcon } from '@primer/octicons-react';
import { useCallback, useEffect, useState } from 'react';
import { StoryExpanded, useProjectQueries, Backlog } from './queries';
import { StoryRow } from './StoryRow';
import { IconButton } from '@primer/react';
import { BacklogMenu } from './BacklogMenu';
import { InlineDateRangeField } from './InlineDateRangeField';
import InlineTextField from './InlineTextField';
import { BorderBoxHeading } from './BorderBox';
import { useMutation } from '@tanstack/react-query';

Check failure on line 10 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L10 <@typescript-eslint/no-unused-vars>(https://typescript-eslint.io/rules/no-unused-vars)

'useMutation' is defined but never used. Allowed unused vars must match /^_/u.
Raw output
{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'useMutation' is defined but never used. Allowed unused vars must match /^_/u.","line":10,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":10,"endColumn":21}

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import useMutation.
import { useSubmitForm2 } from './mutations';

// <pre>{JSON.stringify(backlogsQuery.data, null, 2)}</pre>
interface BacklogProps {
backlog:Backlog
}

export function BacklogTable({backlog}:BacklogProps) {
const [_, typesQuery, statusesQuery] = useProjectQueries('your-scrum-project');
const types = typesQuery.data?._embedded.elements ?? [];
const statuses = statusesQuery.data?._embedded.elements ?? [];
const [stories, setStories] = useState<StoryExpanded[]>([]);
const { mutate, error, isSuccess } = useSubmitForm2('your-scrum-project', backlog.sprint.id);

Check failure on line 23 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L23 <@typescript-eslint/no-unused-vars>(https://typescript-eslint.io/rules/no-unused-vars)

'error' is assigned a value but never used. Allowed unused vars must match /^_/u.
Raw output
{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'error' is assigned a value but never used. Allowed unused vars must match /^_/u.","line":23,"column":19,"nodeType":null,"messageId":"unusedVar","endLine":23,"endColumn":24}

Check failure on line 23 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L23 <@typescript-eslint/no-unused-vars>(https://typescript-eslint.io/rules/no-unused-vars)

'isSuccess' is assigned a value but never used. Allowed unused vars must match /^_/u.
Raw output
{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'isSuccess' is assigned a value but never used. Allowed unused vars must match /^_/u.","line":23,"column":26,"nodeType":null,"messageId":"unusedVar","endLine":23,"endColumn":35}

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable error.

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable isSuccess.

useEffect(() => {
if (!types.length || !statuses.length) return;

const expanded = backlog.stories.map((story):StoryExpanded => {
const type = types.find((t) => t.id === story.type_id)!;
const status = statuses.find((s) => s.id === story.status_id)!;
return { ...story, type, status };
});

// Sort by position before storing
setStories(expanded.sort((a, b) => a.position - b.position));
}, [backlog.stories, types, statuses]);

const totalPoints = stories
.map((story) => story.story_points ?? 0)
.reduce((accum, value) => accum + value, 0);

const moveItem = useCallback((dragIndex:number, hoverIndex:number) => {
setStories(prev => {
const updated = [...prev].sort((a, b) => a.position - b.position);
const [removed] = updated.splice(dragIndex, 1);
updated.splice(hoverIndex, 0, removed);

return updated.map((story, i) => ({ ...story, position: i }));
});
}, []);

const getCurrentPosition = (id:number | string) => stories.find(s => s.id === id)?.position ?? 0;

const updatePosition = (id:number|string, position:number) => {
mutate({ id: Number(id), position });
};

return (
<div className="position-relative Box Box--condensed" id="backlog_SPRINT_ID">
<div className="Box-header color-fg-muted">
<collapsible-header id="collapsible-header-e2b980e5-43ec-42fc-bbee-6e0ceb8c31f2" className="CollapsibleHeader">
<div
data-target="collapsible-header.triggerElement"
data-action="click:collapsible-header#toggle keydown:collapsible-header#toggleViaKeyboard"
tabIndex={0}
role="button"
aria-expanded="true"
className="CollapsibleHeader--triggerArea d-flex flex-column"
>
<div>
<BacklogHeader backlog={backlog}></BacklogHeader>
<div className="velocity">{totalPoints}</div>
<ChevronUpIcon size="small" data-target="collapsible-header.arrowUp"></ChevronUpIcon>
<ChevronDownIcon size="small" data-target="collapsible-header.arrowDown"></ChevronDownIcon>
</div>
<div>
<span data-targets="collapsible-header.collapsibleElements" className="color-fg-subtle">This backlog is unique to this one-time meeting. You can drag items in and out to add or remove them from the meeting agenda.</span>
</div>
</div>
</collapsible-header>
</div>
{stories.length > 0 && (
<ul className="stories">
{stories.sort((a, b) => a.position - b.position).map((story, index) => {
return (
<li key={story.id} className="Box-row">
<StoryRow
story={story}
projectId={backlog.sprint.project_id}
sprintId={backlog.sprint.id}
index={index}
moveItem={moveItem}
getCurrentPosition={getCurrentPosition}
updatePosition={updatePosition}
></StoryRow>
</li>
);
})}
;
</ul>
)}
{stories.length === 0 && <div className="Box-body">No content</div>}
</div>
);
}


function BacklogHeader({backlog}:BacklogProps) {
const [isEditing, setIsEditing] = useState(false);

const [formValues, setFormValues] = useState({
startDate: backlog.sprint.start_date!,
endDate: backlog.sprint.effective_date!,
name: backlog.sprint.name
});

const resetForm = () => {
setFormValues({
startDate: backlog.sprint.start_date!,
endDate: backlog.sprint.effective_date!,
name: backlog.sprint.name
});
};

useEffect(() => { resetForm(); }, [backlog]);

const handleInputChange = (field:string, value:string) => {
setFormValues((current) => ({
...current,
[field]: value,
}));
};

const handleSave = () => {

Check failure on line 134 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L134 <@typescript-eslint/no-unused-vars>(https://typescript-eslint.io/rules/no-unused-vars)

'handleSave' is assigned a value but never used. Allowed unused vars must match /^_/u.
Raw output
{"ruleId":"@typescript-eslint/no-unused-vars","severity":2,"message":"'handleSave' is assigned a value but never used. Allowed unused vars must match /^_/u.","line":134,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":134,"endColumn":19}

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable handleSave.
// mutate({
// type_id: formValues.type_id,
// subject: formValues.subject,
// status_id: formValues.status_id,
// story_points: formValues.story_points
// });
setIsEditing(false);
};

const handleCancel = () => {
resetForm();
setIsEditing(false);
};


if (isEditing) {
return (
<>

<BorderBoxHeading>
<InlineTextField
value={formValues.name}
onChange={(event) => handleInputChange('name', event.target.value)}
></InlineTextField>
</BorderBoxHeading>
<BorderBoxHeading>
<InlineDateRangeField value={[formValues.startDate, formValues.endDate]}
onChange={([newstartDate, newEndDate]) => { handleInputChange('startDate', newstartDate); handleInputChange('endDate', newEndDate); }}></InlineDateRangeField>
</BorderBoxHeading>

<div className='op-border-box-grid--row-action'>
<div className="d-flex gap-2">
<IconButton icon={CheckIcon} onClick={() => {}} variant="primary" aria-label="Save" />

Check failure on line 167 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L167 <@typescript-eslint/no-empty-function>(https://typescript-eslint.io/rules/no-empty-function)

Unexpected empty arrow function.
Raw output
{"ruleId":"@typescript-eslint/no-empty-function","severity":2,"message":"Unexpected empty arrow function.","line":167,"column":58,"nodeType":"ArrowFunctionExpression","messageId":"unexpected","endLine":167,"endColumn":60,"suggestions":[{"messageId":"suggestComment","data":{"name":"arrow function"},"fix":{"range":[6151,6151],"text":" /* empty */ "},"desc":"Add comment inside empty arrow function."}]}
<IconButton icon={UndoIcon} aria-label="Cancel" onClick={handleCancel} />
</div>
</div>
</>
);
}

return (
<>
<BorderBoxHeading>
{backlog.sprint.name}
</BorderBoxHeading>
<div className='d-flex w-full'>


<IconButton variant="invisible" icon={PencilIcon} aria-label="Edit" onClick={() => setIsEditing(true)} />
<BacklogMenu onNewStory={() => {}}></BacklogMenu>

Check failure on line 184 in frontend/src/react/backlogs/BacklogTable.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogTable.tsx#L184 <@typescript-eslint/no-empty-function>(https://typescript-eslint.io/rules/no-empty-function)

Unexpected empty arrow function.
Raw output
{"ruleId":"@typescript-eslint/no-empty-function","severity":2,"message":"Unexpected empty arrow function.","line":184,"column":38,"nodeType":"ArrowFunctionExpression","messageId":"unexpected","endLine":184,"endColumn":40,"suggestions":[{"messageId":"suggestComment","data":{"name":"arrow function"},"fix":{"range":[6631,6631],"text":" /* empty */ "},"desc":"Add comment inside empty arrow function."}]}
</div>
</>
);
}
119 changes: 119 additions & 0 deletions frontend/src/react/backlogs/BacklogsContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import '@primer/primitives/dist/css/functional/themes/light.css';
import {BaseStyles, ThemeProvider} from '@primer/react';
import React, { } from 'react';

import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { useProjectQueries } from './queries';

import { Backlog } from './queries';
import { BacklogTable } from './BacklogTable';

declare module 'react/jsx-runtime' {
namespace JSX {
interface IntrinsicElements {
'collapsible-header':React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
start?:string;
end?:string;
};
}
}
}

const queryClient = new QueryClient();

export default function BacklogsContainer() {
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<DndProvider backend={HTML5Backend}>
<BaseStyles>
<Backlogs></Backlogs>
</BaseStyles>
</DndProvider>
</ThemeProvider>
</QueryClientProvider>
);
}



function Backlogs() {
const [backlogsQuery, typesQuery, statusesQuery] = useProjectQueries('your-scrum-project');

const isLoading =
backlogsQuery.isPending ||
typesQuery.isPending ||
statusesQuery.isPending;

const isError =
backlogsQuery.error ||

Check failure on line 58 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L58 <@typescript-eslint/prefer-nullish-coalescing>(https://typescript-eslint.io/rules/prefer-nullish-coalescing)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
Raw output
{"ruleId":"@typescript-eslint/prefer-nullish-coalescing","severity":2,"message":"Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.","line":58,"column":25,"nodeType":"Punctuator","messageId":"preferNullishOverOr","endLine":58,"endColumn":27,"suggestions":[{"messageId":"suggestNullish","data":{"equals":""},"fix":{"range":[1351,1394],"text":"(backlogsQuery.error ??\n    typesQuery.error)"},"desc":"Fix to nullish coalescing operator (`??`)."}]}
typesQuery.error ||

Check failure on line 59 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L59 <@typescript-eslint/prefer-nullish-coalescing>(https://typescript-eslint.io/rules/prefer-nullish-coalescing)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
Raw output
{"ruleId":"@typescript-eslint/prefer-nullish-coalescing","severity":2,"message":"Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.","line":59,"column":22,"nodeType":"Punctuator","messageId":"preferNullishOverOr","endLine":59,"endColumn":24,"suggestions":[{"messageId":"suggestNullish","data":{"equals":""},"fix":{"range":[1395,1397],"text":"??"},"desc":"Fix to nullish coalescing operator (`??`)."}]}
statusesQuery.error;

if (isLoading) return 'Loading...';

if (isError) {
return (
'Error: ' +
(backlogsQuery.error?.message ||

Check failure on line 67 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L67 <@typescript-eslint/prefer-nullish-coalescing>(https://typescript-eslint.io/rules/prefer-nullish-coalescing)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
Raw output
{"ruleId":"@typescript-eslint/prefer-nullish-coalescing","severity":2,"message":"Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.","line":67,"column":37,"nodeType":"Punctuator","messageId":"preferNullishOverOr","endLine":67,"endColumn":39,"suggestions":[{"messageId":"suggestNullish","data":{"equals":""},"fix":{"range":[1518,1583],"text":"(backlogsQuery.error?.message ??\n        typesQuery.error?.message)"},"desc":"Fix to nullish coalescing operator (`??`)."}]}
typesQuery.error?.message ||

Check failure on line 68 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L68 <@typescript-eslint/prefer-nullish-coalescing>(https://typescript-eslint.io/rules/prefer-nullish-coalescing)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
Raw output
{"ruleId":"@typescript-eslint/prefer-nullish-coalescing","severity":2,"message":"Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.","line":68,"column":35,"nodeType":"Punctuator","messageId":"preferNullishOverOr","endLine":68,"endColumn":37,"suggestions":[{"messageId":"suggestNullish","data":{"equals":""},"fix":{"range":[1584,1586],"text":"??"},"desc":"Fix to nullish coalescing operator (`??`)."}]}
statusesQuery.error?.message)
);
}



return (
<>

<div className="d-flex flex-wrap gap-4" >
{
backlogsQuery.data.owner_backlogs.map((backlog:Backlog) => {

Check failure on line 80 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L80 <@typescript-eslint/no-unsafe-call>(https://typescript-eslint.io/rules/no-unsafe-call)

Unsafe call of a(n) `any` typed value.
Raw output
{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a(n) `any` typed value.","line":80,"column":10,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":80,"endColumn":47}

Check failure on line 80 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L80 <@typescript-eslint/no-unsafe-member-access>(https://typescript-eslint.io/rules/no-unsafe-member-access)

Unsafe member access .owner_backlogs on an `any` value.
Raw output
{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .owner_backlogs on an `any` value.","line":80,"column":29,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":80,"endColumn":43}
return (
<div key={backlog.sprint.id} style={ {flex: '0 0 calc(50% - var(--base-size-24, 24px))'}}>
<BacklogTable backlog={backlog}></BacklogTable>
</div>
);
})
}

{
backlogsQuery.data.sprint_backlogs.map((backlog:Backlog) => {

Check failure on line 90 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L90 <@typescript-eslint/no-unsafe-call>(https://typescript-eslint.io/rules/no-unsafe-call)

Unsafe call of a(n) `any` typed value.
Raw output
{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a(n) `any` typed value.","line":90,"column":10,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":90,"endColumn":48}

Check failure on line 90 in frontend/src/react/backlogs/BacklogsContainer.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] frontend/src/react/backlogs/BacklogsContainer.tsx#L90 <@typescript-eslint/no-unsafe-member-access>(https://typescript-eslint.io/rules/no-unsafe-member-access)

Unsafe member access .sprint_backlogs on an `any` value.
Raw output
{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .sprint_backlogs on an `any` value.","line":90,"column":29,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":90,"endColumn":44}
return (
<div key={backlog.sprint.id} style={ {flex: '0 0 calc(50% - var(--base-size-24, 24px))'}}>
<BacklogTable backlog={backlog}></BacklogTable>
</div>
);
})
}
</div>
</>
);


}

//op-border-box-grid
// <BorderBoxHeading>Type</BorderBoxHeading>
// <BorderBoxHeading>Subject</BorderBoxHeading>
// <BorderBoxHeading>Status</BorderBoxHeading>
// <BorderBoxHeading>Story points</BorderBoxHeading>

// <!--
// <Blankslate>
// <Blankslate.Visual>
// <BookIcon size="medium" />
// </Blankslate.Visual>
// <Blankslate.Heading>Blankslate heading</Blankslate.Heading>
// <Blankslate.Description>Use it to provide information when no dynamic content exists.</Blankslate.Description>
// </Blankslate>
// -->
Loading
Loading