Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: better UX on UserTask card #20

Merged
merged 4 commits into from
Nov 19, 2024
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
68 changes: 37 additions & 31 deletions api-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
} from "./errors";

/**
* Client for interacting with the LittleHorse User Tasks API.
* Provides methods for managing user tasks, including claiming, canceling, and completing tasks,
* Client for interacting with the LittleHorse UserTasks API.
* Provides methods for managing UserTasks, including claiming, canceling, and completing tasks,
* as well as administrative functions.
*/
export class LittleHorseUserTasksApiClient {
Expand All @@ -33,7 +33,7 @@ export class LittleHorseUserTasksApiClient {
private accessToken: string;

/**
* Creates a new instance of the LittleHorse User Tasks API client
* Creates a new instance of the LittleHorse UserTasks API client
* @param config Configuration object containing baseUrl, tenantId, and accessToken
*/
constructor(config: {
Expand Down Expand Up @@ -110,12 +110,18 @@ export class LittleHorseUserTasksApiClient {
return response as T;
}

/**
* Internal method to get the error message from a response
* @param response The response to get the error message from
* @returns Promise resolving to the error message
* @private
*/
private async getErrorMessage(response: Response): Promise<string> {
try {
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const errorData = await response.json();
return errorData.detail || errorData.message || "Unknown error";
return errorData.message || "Unknown error";
}
return await response.text();
} catch {
Expand All @@ -125,8 +131,8 @@ export class LittleHorseUserTasksApiClient {

// User Methods
/**
* Claims a user task for the authenticated user
* @param userTask The user task to claim
* Claims a UserTask for the authenticated user
* @param userTask The UserTask to claim
*/
async claimUserTask(userTask: UserTask): Promise<void> {
await this.fetch(`/tasks/${userTask.wfRunId}/${userTask.id}/claim`, {
Expand All @@ -135,8 +141,8 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Cancels a user task
* @param userTask The user task to cancel
* Cancels a UserTask
* @param userTask The UserTask to cancel
*/
async cancelUserTask(userTask: UserTask): Promise<void> {
await this.fetch(`/tasks/${userTask.wfRunId}/${userTask.id}/cancel`, {
Expand All @@ -145,9 +151,9 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Lists user tasks based on search criteria
* Lists UserTasks based on search criteria
* @param search Search parameters (excluding user_task_def_name)
* @returns Promise resolving to the list of user tasks
* @returns Promise resolving to the list of UserTasks
*/
async listUserTasks(
search: Omit<ListUserTasksRequest, "type">,
Expand Down Expand Up @@ -175,17 +181,17 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Retrieves a specific user task by ID
* @param userTask The user task to retrieve
* @returns Promise resolving to the user task details
* Retrieves a specific UserTask by ID
* @param userTask The UserTask to retrieve
* @returns Promise resolving to the UserTask details
*/
async getUserTask(userTask: UserTask): Promise<GetUserTaskResponse> {
return await this.fetch(`/tasks/${userTask.wfRunId}/${userTask.id}`);
}

/**
* Completes a user task with the provided values
* @param userTask The user task to complete
* Completes a UserTask with the provided values
* @param userTask The UserTask to complete
* @param values The result values for the task
*/
async completeUserTask(
Expand All @@ -203,8 +209,8 @@ export class LittleHorseUserTasksApiClient {

// Admin Methods
/**
* Administrative method to cancel a user task
* @param userTask The user task to cancel
* Administrative method to cancel a UserTask
* @param userTask The UserTask to cancel
*/
async adminCancelUserTask(userTask: UserTask): Promise<void> {
await this.fetch(`/admin/tasks/${userTask.wfRunId}/${userTask.id}/cancel`, {
Expand All @@ -213,22 +219,22 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Administrative method to assign a user task to a specific user or group
* @param userTask The user task to assign
* @param param1 Object containing userId and/or userGroupId
* Administrative method to assign a UserTask to a specific user or group
* @param userTask The UserTask to assign
* @param assignTo Object containing userId and/or userGroupId
*/
async adminAssignUserTask(
userTask: UserTask,
{ userId, userGroupId }: { userId?: string; userGroupId?: string },
assignTo: { userId?: string; userGroupId?: string },
): Promise<void> {
await this.fetch(`/admin/tasks/${userTask.wfRunId}/${userTask.id}/assign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: userId ?? "",
userGroup: userGroupId ?? "",
userId: assignTo.userId ?? "",
userGroup: assignTo.userGroupId ?? "",
}),
});
}
Expand All @@ -250,7 +256,7 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Administrative method to list user task definition names
* Administrative method to list UserTask definition names
* @param search Search parameters for task definitions
* @returns Promise resolving to the list of task definition names
*/
Expand All @@ -271,9 +277,9 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Administrative method to get a user task
* @param userTask The user task to get the details of
* @returns Promise resolving to the user task details
* Administrative method to get a UserTask
* @param userTask The UserTask to get the details of
* @returns Promise resolving to the UserTask details
*/
async adminGetUserTask(
userTask: UserTask,
Expand All @@ -282,9 +288,9 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Administrative method to list all user tasks
* Administrative method to list all UserTasks
* @param search Search parameters for tasks
* @returns Promise resolving to the list of user tasks
* @returns Promise resolving to the list of UserTasks
*/
async adminListUserTasks(
search: ListUserTasksRequest,
Expand All @@ -304,8 +310,8 @@ export class LittleHorseUserTasksApiClient {
}

/**
* Administrative method to complete a user task
* @param userTask The user task to complete
* Administrative method to complete a UserTask
* @param userTask The UserTask to complete
* @param values The result values for the task
*/
async adminCompleteUserTask(
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.60.5",
Expand All @@ -44,11 +45,11 @@
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"prettier": "^3.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9",
"eslint-config-next": "15.0.3",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.15",
"typescript": "^5"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
adminGetUserTask,
} from "@/app/[tenantId]/actions/admin";
import { completeUserTask, getUserTask } from "@/app/[tenantId]/actions/user";
import { useTenantId } from "@/app/[tenantId]/layout";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Dialog,
Expand All @@ -23,7 +24,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
GetUserTaskResponse,
UserTask,
Expand All @@ -32,7 +32,7 @@ import {
import { useEffect, useState } from "react";
import { toast } from "sonner";
import Loading from "../../loading";
import { useTenantId } from "@/app/[tenantId]/layout";
import NotesTextArea from "../notes";

export default function CompleteUserTaskButton({
userTask,
Expand Down Expand Up @@ -73,7 +73,7 @@ export default function CompleteUserTaskButton({
{readOnly ? "View Results" : "Complete"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogContent className="gap-2">
<DialogHeader>
<DialogTitle>
{readOnly ? "View Results for" : "Complete"}{" "}
Expand All @@ -85,12 +85,11 @@ export default function CompleteUserTaskButton({
<>
<div className="space-y-2">
<Label>Notes:</Label>
<Textarea
placeholder="Notes"
value={userTaskDetails.notes ?? "N/A"}
readOnly
/>
<NotesTextArea notes={userTaskDetails.notes} />
</div>
<h1 className="text-lg font-semibold text-center">
Fill out the form
</h1>
{userTaskDetails.fields.map((field) => (
<div key={field.name} className="space-y-2">
<Label>
Expand Down
51 changes: 24 additions & 27 deletions ui/src/app/[tenantId]/components/user-task/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { UserTask } from "@littlehorse-enterprises/user-tasks-api-client";
import { useSession } from "next-auth/react";
import { ReactNode, useEffect, useRef, useState } from "react";
import AssignUserTaskButton from "./action-buttons/assign";
import CancelUserTaskButton from "./action-buttons/cancel";
import ClaimUserTaskButton from "./action-buttons/claim";
import CompleteUserTaskButton from "./action-buttons/complete";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import NotesTextArea from "./notes";

export default function UserTask({
userTask,
Expand All @@ -41,52 +43,47 @@ export default function UserTask({
<CardTitle>{userTask.userTaskDefName}</CardTitle>
</CardHeader>
<CardContent className="grid gap-2 p-0">
<Metadata label="Workflow Run ID" value={userTask.wfRunId} />
<Metadata label="UserTask Run ID" value={userTask.id} />
<Metadata label="Status" value={userTask.status} />
<Metadata
label="Scheduled Time"
value={new Date(userTask.scheduledTime).toLocaleString()}
/>
<Metadata
label="Assigned To ID"
label="Assigned To (User)"
value={
userTask.user && (
<>
{userTask.user.id}
<span className="text-red-500">
{userTask.user.valid === false && " INVALID USER"}
</span>
{userTask.user.firstName} {userTask.user.lastName}{" "}
<span className="font-medium">{userTask.user.email}</span>
{userTask.user.valid === false && (
<>
{userTask.user.id}{" "}
<span className="text-destructive">INVALID USER</span>
</>
)}
</>
)
}
/>
<Metadata label="Assigned To Email" value={userTask.user?.email} />
<Metadata
label="Assigned To Username"
value={userTask.user?.username}
/>
<Metadata
label="Assigned To Full Name"
value={
userTask.user?.firstName &&
userTask.user?.lastName &&
`${userTask.user.firstName} ${userTask.user.lastName}`
}
/>
<Metadata
label="User Group"
label="Assigned To (Group)"
value={
userTask.userGroup && (
<>
{userTask.userGroup.name ?? userTask.userGroup.id}{" "}
<span className="text-red-500">
<span className="text-destructive">
{userTask.userGroup.valid === false && "INVALID USER GROUP"}
</span>
</>
)
}
/>
<Metadata label="Workflow Run ID" value={userTask.wfRunId} />
<div>
<Label>Notes:</Label>
<NotesTextArea notes={userTask.notes} />
</div>
</CardContent>
<CardFooter className="w-full flex items-center justify-end gap-2 flex-wrap p-0">
{userTask.status !== "CANCELLED" && userTask.status !== "DONE" && (
Expand Down
Loading