Skip to content
Open
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
51 changes: 51 additions & 0 deletions apps/console/src/components/tasks/TaskFormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
DurationPicker,
Input,
Label,
Option,
PriorityLevel,
PropertyRow,
Select,
Textarea,
useDialogRef,
} from "@probo/ui";
Expand All @@ -33,6 +36,7 @@ const taskFragment = graphql`
id
description
name
priority
timeEstimate
deadline
assignedTo {
Expand Down Expand Up @@ -73,9 +77,12 @@ export const taskUpdateMutation = graphql`
}
`;

const taskPriorities = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const;

const createTaskSchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
priority: z.enum(taskPriorities),
timeEstimate: z.string().optional().nullable(),
assignedToId: z.string().optional().nullable(),
measureId: z.preprocess(
Expand All @@ -88,6 +95,7 @@ const createTaskSchema = z.object({
const updateTaskSchema = z.object({
name: z.string().min(1),
description: z.string().optional().nullable(),
priority: z.enum(taskPriorities),
timeEstimate: z.string().optional().nullable(),
assignedToId: z.preprocess(
val => (val === "" || val == null ? null : val),
Expand Down Expand Up @@ -131,6 +139,7 @@ export default function TaskFormDialog(props: Props) {
defaultValues: {
name: task?.name ?? "",
description: task?.description ?? "",
priority: task?.priority ?? "MEDIUM",
timeEstimate: task?.timeEstimate ?? "",
assignedToId: task?.assignedTo?.id ?? "",
measureId: task?.measure?.id ?? measureId ?? "",
Expand All @@ -146,6 +155,7 @@ export default function TaskFormDialog(props: Props) {
taskId: task.id,
name: data.name,
description: data.description || null,
priority: data.priority,
timeEstimate: data.timeEstimate || null,
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId ?? null,
Expand All @@ -160,6 +170,7 @@ export default function TaskFormDialog(props: Props) {
organizationId,
name: data.name,
description: data.description || null,
priority: data.priority,
timeEstimate: data.timeEstimate || null,
deadline: formatDatetime(data.deadline) ?? null,
assignedToId: data.assignedToId || null,
Expand Down Expand Up @@ -210,6 +221,46 @@ export default function TaskFormDialog(props: Props) {
{/* Properties form */}
<div className="py-5 px-6 bg-subtle">
<Label>{__("Properties")}</Label>
<PropertyRow
label={__("Priority")}
error={formState.errors.priority?.message}
>
<Controller
name="priority"
control={control}
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<Option value="URGENT">
<span className="flex items-center gap-2">
<PriorityLevel level="URGENT" />
{__("Urgent")}
</span>
</Option>
<Option value="HIGH">
<span className="flex items-center gap-2">
<PriorityLevel level="HIGH" />
{__("High")}
</span>
</Option>
<Option value="MEDIUM">
<span className="flex items-center gap-2">
<PriorityLevel level="MEDIUM" />
{__("Medium")}
</span>
</Option>
<Option value="LOW">
<span className="flex items-center gap-2">
<PriorityLevel level="LOW" />
{__("Low")}
</span>
</Option>
</Select>
)}
/>
</PropertyRow>
<PropertyRow
label={__("Assigned to")}
error={formState.errors.assignedToId?.message}
Expand Down
20 changes: 11 additions & 9 deletions apps/console/src/components/tasks/TasksCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const taskInlineFragment = graphql`
id
state
priority
rank
}
`;

Expand All @@ -68,7 +69,7 @@ const organizationTasksFragment = graphql`
@refetchable(queryName: "TasksCardOrganizationQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 500 }
order: { type: "TaskOrder", defaultValue: { field: PRIORITY, direction: ASC } }
order: { type: "TaskOrder", defaultValue: { field: RANK, direction: ASC } }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
Expand Down Expand Up @@ -118,12 +119,12 @@ export function OrganizationTasksCard({ organizationRef, header }: OrganizationT
);
}

const updatePriorityMutation = graphql`
mutation TasksCardUpdatePriorityMutation($input: UpdateTaskInput!) {
const updateRankMutation = graphql`
mutation TasksCardUpdateRankMutation($input: UpdateTaskInput!) {
updateTask(input: $input) {
task {
id
priority
rank
}
}
}
Expand All @@ -137,7 +138,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
const { toast } = useToast();
const [draggedId, setDraggedId] = useState<string | null>(null);
const [previewOrder, setPreviewOrder] = useState<string[] | null>(null);
const [updatePriority] = useMutation<TaskFormDialogUpdateMutation>(updatePriorityMutation);
const [updateRank] = useMutation<TaskFormDialogUpdateMutation>(updateRankMutation);

const handleStateChange = () => {
if (refetch) {
Expand Down Expand Up @@ -196,15 +197,15 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
let targetOriginalIdx = newIdx;
if (targetOriginalIdx >= originalIdx) targetOriginalIdx++;
if (targetOriginalIdx >= filteredTasks.length) targetOriginalIdx = filteredTasks.length - 1;
const targetPriority = readTask(filteredTasks[targetOriginalIdx].node).priority;
const targetRank = readTask(filteredTasks[targetOriginalIdx].node).rank;

setDraggedId(null);

updatePriority({
updateRank({
variables: {
input: {
taskId: draggedId,
priority: targetPriority,
rank: targetRank,
},
},
onCompleted: (_, errors) => {
Expand Down Expand Up @@ -337,6 +338,7 @@ const fragment = graphql`
id
name
state
priority
description
timeEstimate
deadline
Expand Down Expand Up @@ -452,7 +454,7 @@ function TaskRow(props: TaskRowProps) {
>
<div className="flex gap-2 items-start">
<div className="flex items-center gap-2 pt-[2px]">
<PriorityLevel level={1} />
<PriorityLevel level={task.priority} />
<button
onClick={() => void onToggle()}
className="cursor-pointer -m-1 p-1 disabled:opacity-60"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tasksQuery = graphql`
... on Measure {
id
canCreateTask: permission(action: "core:task:create")
tasks(first: 100, orderBy: { field: PRIORITY, direction: ASC })
tasks(first: 100, orderBy: { field: RANK, direction: ASC })
@connection(key: "Measure__tasks")
@required(action: THROW) {
__id
Expand Down
6 changes: 3 additions & 3 deletions e2e/console/rbac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ func TestRBAC(t *testing.T) {
client: owner,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task"), "priority": "MEDIUM"}}
},
shouldAllow: true,
},
Expand All @@ -641,7 +641,7 @@ func TestRBAC(t *testing.T) {
client: admin,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task"), "priority": "MEDIUM"}}
},
shouldAllow: true,
},
Expand All @@ -651,7 +651,7 @@ func TestRBAC(t *testing.T) {
client: viewer,
query: createTaskMutation,
variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task")}}
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "measureId": measureID, "name": factory.SafeName("Task"), "priority": "MEDIUM"}}
},
shouldAllow: false,
},
Expand Down
15 changes: 14 additions & 1 deletion e2e/console/task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestTask_Create(t *testing.T) {
"measureId": measureID,
"name": "Owner Task",
"description": "Created by owner",
"priority": "MEDIUM",
},
}, &result)
require.NoError(t, err)
Expand Down Expand Up @@ -106,6 +107,7 @@ func TestTask_CreateWithoutMeasure(t *testing.T) {
"organizationId": owner.GetOrganizationID().String(),
"name": "Task without measure",
"description": "Created without a measure",
"priority": "HIGH",
},
}, &result)
require.NoError(t, err)
Expand Down Expand Up @@ -252,7 +254,8 @@ func TestTask_RequiredFields(t *testing.T) {
{
name: "missing organizationId",
input: map[string]any{
"name": "Test Task",
"name": "Test Task",
"priority": "MEDIUM",
},
skipOrganization: true,
wantErrorContains: "organizationId",
Expand All @@ -261,9 +264,18 @@ func TestTask_RequiredFields(t *testing.T) {
name: "missing name",
input: map[string]any{
"organizationId": "placeholder",
"priority": "MEDIUM",
},
wantErrorContains: "name",
},
{
name: "missing priority",
input: map[string]any{
"organizationId": "placeholder",
"name": "Test Task",
},
wantErrorContains: "priority",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -770,6 +782,7 @@ func TestTask_OmittableDeadline(t *testing.T) {
"organizationId": owner.GetOrganizationID().String(),
"measureId": measureID,
"name": "Deadline Test Task",
"priority": "MEDIUM",
"deadline": "2025-12-31T00:00:00Z",
},
}, &createResult)
Expand Down
1 change: 1 addition & 0 deletions e2e/internal/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ func CreateTask(c *testutil.Client, measureID *string, attrs ...Attrs) string {
input := map[string]any{
"organizationId": c.GetOrganizationID().String(),
"name": a.getString("name", SafeName("Task")),
"priority": a.getString("priority", "MEDIUM"),
}
if measureID != nil {
input["measureId"] = *measureID
Expand Down
22 changes: 20 additions & 2 deletions packages/ui/src/Atoms/PriorityLevel/PriorityLevel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,26 @@ export default {

type Story = StoryObj<typeof PriorityLevel>;

export const Default: Story = {
export const Low: Story = {
args: {
level: 1,
level: "LOW",
},
};

export const Medium: Story = {
args: {
level: "MEDIUM",
},
};

export const High: Story = {
args: {
level: "HIGH",
},
};

export const Urgent: Story = {
args: {
level: "URGENT",
},
};
32 changes: 25 additions & 7 deletions packages/ui/src/Atoms/PriorityLevel/PriorityLevel.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import { clsx } from "clsx";

type Props = {
level: number;
level: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
};

export function PriorityLevel({ level }: Props) {
if (level === "URGENT") {
return (
<div className="w-max flex items-center justify-center text-txt-danger">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 1.75v5.25M7 10.5h.005"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
);
}

const bars = level === "HIGH" ? 3 : level === "MEDIUM" ? 2 : 1;

return (
<div className="w-max p-[2px] flex gap-[2px] items-end">
<div
className={clsx(
"h-1 w-[3px] bg-txt-quaternary rounded",
level >= 1 ? "bg-txt-secondary" : "bg-txt-quaternary",
"h-1 w-[3px] rounded",
bars >= 1 ? "bg-txt-secondary" : "bg-txt-quaternary",
)}
/>
<div
className={clsx(
"h-2 w-[3px] bg-txt-quaternary rounded",
level >= 2 ? "bg-txt-secondary" : "bg-txt-quaternary",
"h-2 w-[3px] rounded",
bars >= 2 ? "bg-txt-secondary" : "bg-txt-quaternary",
)}
/>
<div
className={clsx(
"h-3 w-[3px] bg-txt-quaternary rounded",
level >= 3 ? "bg-txt-secondary" : "bg-txt-quaternary",
"h-3 w-[3px] rounded",
bars >= 3 ? "bg-txt-secondary" : "bg-txt-quaternary",
)}
/>
</div>
Expand Down
16 changes: 16 additions & 0 deletions pkg/coredata/migrations/20260330T120000Z.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Rename priority to rank
ALTER TABLE tasks RENAME COLUMN priority TO rank;

ALTER TABLE tasks DROP CONSTRAINT tasks_organization_id_state_priority_key;

ALTER TABLE tasks
ADD CONSTRAINT tasks_organization_id_state_rank_key
UNIQUE (organization_id, state, rank)
DEFERRABLE INITIALLY DEFERRED;

-- Add task priority enum
CREATE TYPE task_priority AS ENUM ('URGENT', 'HIGH', 'MEDIUM', 'LOW');

ALTER TABLE tasks ADD COLUMN priority task_priority NOT NULL DEFAULT 'MEDIUM'::task_priority;

ALTER TABLE tasks ALTER COLUMN priority DROP DEFAULT;
Loading
Loading