Skip to content
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
115 changes: 115 additions & 0 deletions frontend/src/composables/useTaskAvailability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { computed } from "vue";
import { useWorkspaceStore } from "@/stores/workspaceStore";

/**
* Composable for accessing task availability information and formatted counters
* Uses the enhanced navigation context to provide user-friendly task status information
*/
export function useTaskAvailability() {
const workspaceStore = useWorkspaceStore();

/**
* Current navigation context with availability information
*/
const navigationContext = computed(() => workspaceStore.navigationContext);

/**
* Formatted task counter for "X of Y tasks" display
*/
const taskPositionText = computed(() => {
const context = navigationContext.value;
if (!context) return "Loading...";

return `${context.currentPosition} of ${context.totalTasks}`;
});

/**
* Detailed task availability summary
*/
const availabilitySummary = computed(() => {
const context = navigationContext.value;
if (!context) return "Loading task information...";

const parts = [];

if (context.availableTasks > 0) {
parts.push(`${context.availableTasks} available`);
}

if (context.completedTasks > 0) {
parts.push(`${context.completedTasks} completed`);
}

if (context.assignedTasks > 0) {
parts.push(`${context.assignedTasks} assigned to others`);
}

return parts.length > 0 ? parts.join(", ") : "No tasks available";
});

/**
* Status indicator for current task state
*/
const currentTaskStatus = computed(() => {
const context = navigationContext.value;
if (!context) return { text: "Loading...", color: "gray" };

if (context.availableTasks === 0) {
return {
text: "All tasks completed or assigned to others",
color: "orange",
};
}

return {
text: `${context.availableTasks} tasks available to work on`,
color: "green",
};
});

/**
* Progress percentage based on completed tasks
*/
const completionPercentage = computed(() => {
const context = navigationContext.value;
if (!context || context.totalTasks === 0) return 0;

return Math.round((context.completedTasks / context.totalTasks) * 100);
});

/**
* Whether there are tasks available for the user to work on
*/
const hasAvailableTasks = computed(() => {
const context = navigationContext.value;
return context ? context.availableTasks > 0 : false;
});

/**
* Whether all tasks are either completed or assigned to others
*/
const allTasksUnavailable = computed(() => {
const context = navigationContext.value;
if (!context) return false;

return (
context.availableTasks === 0 &&
(context.completedTasks > 0 || context.assignedTasks > 0)
);
});

return {
// Raw data
navigationContext,

// Formatted strings
taskPositionText,
availabilitySummary,
currentTaskStatus,

// Computed values
completionPercentage,
hasAvailableTasks,
allTasksUnavailable,
};
}
42 changes: 42 additions & 0 deletions frontend/src/composables/useTaskRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRouter } from "vue-router";
import { useToast } from "@/composables/useToast";

/**
* Composable for handling task loading with automatic redirect when no tasks available
* Usage example for components that need to load tasks and handle "no tasks available" gracefully
*/
export function useTaskRedirect() {
const router = useRouter();
const toast = useToast();

/**
* Handle task operation result with automatic redirect and toast for "no tasks available"
* @param result - The task operation result from TaskManager
* @param fallbackRoute - Route to redirect to when no tasks available (default: task view)
*/
const handleTaskResult = async <T>(
result: import("@/core/workspace/task/taskManager").TaskOperationResult<T>,
fallbackRoute: string = "/tasks"
) => {
if (!result.success) {
// Check if we should redirect to task view with toast
if (result.shouldRedirectToTaskView && result.toastMessage) {
// Show informative toast message
toast.showInfo("No Tasks Available", result.toastMessage);

// Redirect back to task view
await router.push(fallbackRoute);
return { redirected: true };
} else {
// Regular error - let the component handle it
return { redirected: false, error: result.error };
}
}

return { redirected: false, data: result.data };
};

return {
handleTaskResult,
};
}
26 changes: 25 additions & 1 deletion frontend/src/core/workspace/loader/workspaceLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,33 @@ export class WorkspaceLoader {
workflowStageId = currentTask.workflowStageId;
logger.info(`Loaded specific task ${initialTaskId} for stage ${workflowStageId}`);
} else {
// Check if this is an access restriction error that should redirect to alternative task
if (taskResult.shouldRedirectToTask) {
logger.warn(`Task ${initialTaskId} assigned to another user, should redirect to task ${taskResult.shouldRedirectToTask}`);
throw {
shouldRedirectToTask: taskResult.shouldRedirectToTask,
alternativeTask: taskResult.alternativeTask,
error: taskResult.error,
toastMessage: taskResult.toastMessage
};
}

// Check if this is an access restriction error that should redirect to task view
if (taskResult.shouldRedirectToTaskView) {
logger.warn(`Task ${initialTaskId} access denied, should redirect to task view`);
throw {
shouldRedirectToTaskView: true,
error: taskResult.error,
toastMessage: taskResult.toastMessage
};
}
logger.warn(`Failed to load task ${initialTaskId}, falling back to asset tasks:`, taskResult.error);
}
} catch (taskByIdError) {
} catch (taskByIdError: any) {
// Re-throw access restriction errors
if (taskByIdError?.shouldRedirectToTask || taskByIdError?.shouldRedirectToTaskView) {
throw taskByIdError;
}
logger.warn(`Failed to load task ${initialTaskId}, falling back to asset tasks:`, taskByIdError);
}
}
Expand Down
48 changes: 45 additions & 3 deletions frontend/src/core/workspace/task/taskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export type TaskOperationResult<T = Task> = {
} | {
success: false;
error: string;
shouldRedirectToTaskView?: boolean;
shouldRedirectToTask?: number;
alternativeTask?: Task;
toastMessage?: string;
};

/**
Expand All @@ -37,6 +41,12 @@ export interface TaskNavigationResult {
currentPosition: number;
totalTasks: number;
message?: string | null;
/** Number of available tasks (unlocked and can be worked on) */
availableTasks: number;
/** Number of completed tasks by the current user */
completedTasks: number;
/** Number of assigned tasks (being worked on by others) */
assignedTasks: number;
}

/**
Expand All @@ -62,6 +72,29 @@ export class TaskManager {
const task = await taskService.getTaskById(projectId, taskId);
return { success: true, data: task };
} catch (error) {
// Check if this is a redirect to alternative task error
if (error instanceof Error && (error as any).redirectToTask) {
logger.info(`Task assigned to another user, should redirect to alternative task ${(error as any).redirectToTask}`);
return {
success: false,
error: error.message,
shouldRedirectToTask: (error as any).redirectToTask,
alternativeTask: (error as any).alternativeTask,
toastMessage: (error as any).toastMessage
};
}

// Check if this is a redirect to task view error
if (error instanceof Error && (error as any).redirectToTaskView) {
logger.info(`No tasks available, should redirect to task view with toast`);
return {
success: false,
error: error.message,
shouldRedirectToTaskView: true,
toastMessage: (error as any).toastMessage
};
}

const errorMessage = error instanceof Error ? error.message : 'Failed to load task';
logger.error(`Failed to load task ${taskId}:`, error);
return { success: false, error: errorMessage };
Expand Down Expand Up @@ -119,7 +152,10 @@ export class TaskManager {
hasPrevious: navigationContext.hasPrevious,
currentPosition: navigationContext.currentPosition,
totalTasks: navigationContext.totalTasks,
message: navigationContext.message
message: navigationContext.message,
availableTasks: navigationContext.availableTasks,
completedTasks: navigationContext.completedTasks,
assignedTasks: navigationContext.assignedTasks
};

return { success: true, data: context };
Expand Down Expand Up @@ -149,7 +185,10 @@ export class TaskManager {
hasPrevious: navigation.hasPrevious,
currentPosition: navigation.currentPosition,
totalTasks: navigation.totalTasks,
message: navigation.message
message: navigation.message,
availableTasks: navigation.availableTasks,
completedTasks: navigation.completedTasks,
assignedTasks: navigation.assignedTasks
};

return { success: true, data: result };
Expand Down Expand Up @@ -179,7 +218,10 @@ export class TaskManager {
hasPrevious: navigation.hasPrevious,
currentPosition: navigation.currentPosition,
totalTasks: navigation.totalTasks,
message: navigation.message
message: navigation.message,
availableTasks: navigation.availableTasks,
completedTasks: navigation.completedTasks,
assignedTasks: navigation.assignedTasks
};

return { success: true, data: result };
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/services/project/task/taskNavigation.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export interface TaskNavigationResponse {

/** Optional message for user feedback */
message?: string | null;

/** Number of available tasks (unlocked and can be worked on) */
availableTasks: number;

/** Number of completed tasks by the current user */
completedTasks: number;

/** Number of assigned tasks (being worked on by others) */
assignedTasks: number;
}

/**
Expand Down
Loading
Loading