Add task drag-and-drop reordering

Order tasks by priority (ASC) by default. Enable drag-and-drop
on the TODO and DONE tabs using native HTML5 drag events,
following the compliance external URLs pattern. The "All" tab
remains read-only since priority is scoped per state.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-26 19:05:18 +01:00
parent 257cbcf826
commit 4a2d308da0
6 changed files with 296 additions and 108 deletions

View File

@@ -15,15 +15,13 @@ const tasksQuery = graphql`
... on Measure {
id
canCreateTask: permission(action: "core:task:create")
tasks(first: 100)
tasks(first: 100, orderBy: { field: PRIORITY, direction: ASC })
@connection(key: "Measure__tasks")
@required(action: THROW) {
__id
edges @required(action: THROW) {
node {
id
# eslint-disable-next-line relay/unused-fields
state
...TasksCard_task
...TaskFormDialogFragment
...TasksCard_TaskRowFragment
}

View File

@@ -1,83 +1,52 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, IconPlusLarge, PageHeader } from "@probo/ui";
import {
type PreloadedQuery,
usePreloadedQuery,
useRefetchableFragment,
} from "react-relay";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { TaskGraphQuery } from "#/__generated__/core/TaskGraphQuery.graphql";
import type { TasksPageFragment$key } from "#/__generated__/core/TasksPageFragment.graphql";
import type { TasksPageFragment_query } from "#/__generated__/core/TasksPageFragment_query.graphql";
import type { TasksCardOrganizationFragment$key } from "#/__generated__/core/TasksCardOrganizationFragment.graphql";
import type { TasksPageQuery } from "#/__generated__/core/TasksPageQuery.graphql";
import TaskFormDialog from "#/components/tasks/TaskFormDialog";
import { TasksCard } from "#/components/tasks/TasksCard";
import { tasksQuery } from "#/hooks/graph/TaskGraph";
import { OrganizationTasksCard } from "#/components/tasks/TasksCard";
const tasksFragment = graphql`
fragment TasksPageFragment on Organization
@refetchable(queryName: "TasksPageFragment_query")
@argumentDefinitions(
first: { type: "Int", defaultValue: 500 }
order: { type: "TaskOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
canCreateTask: permission(action: "core:task:create")
tasks(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "TasksPageFragment_tasks") @required(action: THROW) {
__id
edges @required(action: THROW) {
# eslint-disable-next-line relay/unused-fields
node {
# eslint-disable-next-line relay/unused-fields
id
# eslint-disable-next-line relay/unused-fields
state
...TaskFormDialogFragment
...TasksCard_TaskRowFragment
}
export const tasksPageQuery = graphql`
query TasksPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
...TasksCardOrganizationFragment
}
}
}
`;
interface Props {
queryRef: PreloadedQuery<TaskGraphQuery>;
queryRef: PreloadedQuery<TasksPageQuery>;
}
export default function TasksPage({ queryRef }: Props) {
const { __ } = useTranslate();
const query = usePreloadedQuery(tasksQuery, queryRef);
const [data] = useRefetchableFragment<TasksPageFragment_query, TasksPageFragment$key>(
tasksFragment,
query.organization as TasksPageFragment$key,
);
const connectionId = data.tasks.__id;
const query = usePreloadedQuery(tasksPageQuery, queryRef);
usePageTitle(__("Tasks"));
return (
<div className="space-y-6">
<PageHeader
title={__("Tasks")}
description={__(
"Track your assigned compliance tasks and keep progress on track.",
<OrganizationTasksCard
organizationRef={query.organization as TasksCardOrganizationFragment$key}
header={({ connectionId, canCreateTask }) => (
<PageHeader
title={__("Tasks")}
description={__(
"Track your assigned compliance tasks and keep progress on track.",
)}
>
{canCreateTask && (
<TaskFormDialog connection={connectionId}>
<Button icon={IconPlusLarge}>{__("New task")}</Button>
</TaskFormDialog>
)}
</PageHeader>
)}
>
{data.canCreateTask && (
<TaskFormDialog connection={connectionId}>
<Button icon={IconPlusLarge}>{__("New task")}</Button>
</TaskFormDialog>
)}
</PageHeader>
<TasksCard connectionId={connectionId} tasks={data.tasks.edges} />
/>
</div>
);
}