import { ref, readonly } from 'vue'; import { useNotification } from '~/composables/useNotification'; import { request, parse_list_response, parse_api_response, parse_api_error } from '~/utils'; import type { TaskDefinitionDetailed, TaskDefinitionDocument, TaskDefinitionSummary, } from '~/types/task_definitions'; import type { Pagination } from '~/types/responses'; /** * Reactive list of all task definition summaries, sorted by priority and name. */ const definitions = ref>([]); /** * Pagination state for task definitions list. */ const pagination = ref({ page: 1, per_page: 50, total: 0, total_pages: 0, has_next: false, has_prev: false, }); /** * Indicates if a request is in progress. */ const isLoading = ref(false); /** * Stores the last error message, if any. */ const lastError = ref(null); /** * If true, methods will throw errors instead of returning null/false (for testing) */ const throwInstead = ref(false); /** * Notification composable for showing success/error messages. */ /** * Sorts task definition summaries by priority (ascending), then name (A-Z). * @param items Array of TaskDefinitionSummary * @returns Sorted array of TaskDefinitionSummary */ const sortSummaries = (items: Array): Array => { return [...items].sort((a, b) => { if (a.priority === b.priority) { return a.name.localeCompare(b.name); } return a.priority - b.priority; }); }; /** * Throws an error if the response is not OK, using API error message if available. * @param response Fetch Response object * @throws Error with message from API or status code */ const ensureSuccess = async (response: Response): Promise => { if (response.ok) { return; } const payload = await response .clone() .json() .catch(() => null); const message = await parse_api_error(payload); throw new Error(message); }; /** * Handles errors by updating lastError and showing a notification. * @param error Error object or unknown */ const handleError = (error: unknown): void => { const message = error instanceof Error ? error.message : 'Unexpected error occurred.'; lastError.value = message; useNotification().error(message); }; /** * Updates or adds a summary in the definitions list, keeping sort order. * @param summary TaskDefinitionSummary to update/add */ const updateSummaries = (summary: TaskDefinitionSummary): void => { const isNew = !definitions.value.some((item) => item.id === summary.id); definitions.value = sortSummaries([ ...definitions.value.filter((item) => item.id !== summary.id), summary, ]); if (isNew) { pagination.value.total++; } }; /** * Removes a summary from the definitions list by ID. * @param id Task definition ID */ const removeSummary = (id: number) => { const initialLength = definitions.value.length; definitions.value = definitions.value.filter((item) => item.id !== id); if (definitions.value.length < initialLength) { pagination.value.total = Math.max(0, pagination.value.total - 1); } }; /** * Loads all task definition summaries from the API. * Updates definitions and lastError. */ const loadDefinitions = async ( page: number = 1, perPage: number | undefined = undefined, ): Promise => { isLoading.value = true; try { let url = `/api/tasks/definitions/?page=${page}`; if (perPage !== undefined) { url += `&per_page=${perPage}`; } const response = await request(url); await ensureSuccess(response); const json = await response.json(); const { items, pagination: paginationData } = await parse_list_response(json); definitions.value = sortSummaries(items); pagination.value = paginationData; lastError.value = null; } catch (error) { handleError(error); if (throwInstead.value) throw error; } finally { isLoading.value = false; } }; /** * Fetches a detailed task definition by ID from the API. * @param id Task definition ID * @returns TaskDefinitionDetailed or null on error */ const getDefinition = async (id: number): Promise => { try { const response = await request(`/api/tasks/definitions/${id}`); await ensureSuccess(response); const payload = await response.json(); const detailed = await parse_api_response(payload); lastError.value = null; return detailed; } catch (error) { handleError(error); if (throwInstead.value) throw error; return null; } }; /** * Creates a new task definition via API. * @param definition TaskDefinitionDocument to create * @returns Created TaskDefinitionDetailed or null on error */ const createDefinition = async ( definition: TaskDefinitionDocument, ): Promise => { try { const response = await request('/api/tasks/definitions/', { method: 'POST', body: JSON.stringify(definition), }); await ensureSuccess(response); const payload = await parse_api_response(response.json()); updateSummaries({ id: payload.id, name: payload.name, priority: payload.priority, match_url: payload.match_url, enabled: payload.enabled, created_at: payload.created_at, updated_at: payload.updated_at, }); useNotification().success('Task definition created.'); lastError.value = null; return payload; } catch (error) { handleError(error); if (throwInstead.value) throw error; return null; } }; /** * Updates an existing task definition via API. * @param id Task definition ID * @param definition Updated TaskDefinitionDocument * @returns Updated TaskDefinitionDetailed or null on error */ const updateDefinition = async ( id: number, definition: TaskDefinitionDocument, ): Promise => { try { const response = await request(`/api/tasks/definitions/${id}`, { method: 'PUT', body: JSON.stringify(definition), }); await ensureSuccess(response); const payload = await parse_api_response(response.json()); updateSummaries({ id: payload.id, name: payload.name, priority: payload.priority, match_url: payload.match_url, enabled: payload.enabled, created_at: payload.created_at, updated_at: payload.updated_at, }); useNotification().success('Task definition updated.'); lastError.value = null; return payload; } catch (error) { handleError(error); if (throwInstead.value) throw error; return null; } }; /** * Deletes a task definition by ID via API. * @param id Task definition ID * @returns true if deleted, false on error */ const deleteDefinition = async (id: number): Promise => { try { const response = await request(`/api/tasks/definitions/${id}`, { method: 'DELETE' }); await ensureSuccess(response); removeSummary(id); useNotification().success('Task definition deleted.'); lastError.value = null; return true; } catch (error) { handleError(error); if (throwInstead.value) throw error; return false; } }; /** * Toggles the enabled status of a task definition. * @param id Task definition ID * @param enabled New enabled status * @returns Updated TaskDefinitionDetailed or null on error */ const toggleEnabled = async ( id: number, enabled: boolean, ): Promise => { try { const response = await request(`/api/tasks/definitions/${id}`, { method: 'PATCH', body: JSON.stringify({ enabled }), }); await ensureSuccess(response); const payload = await parse_api_response(response.json()); updateSummaries({ id: payload.id, name: payload.name, priority: payload.priority, match_url: payload.match_url, enabled: payload.enabled, created_at: payload.created_at, updated_at: payload.updated_at, }); useNotification().success(`Task definition ${enabled ? 'enabled' : 'disabled'}.`); lastError.value = null; return payload; } catch (error) { handleError(error); if (throwInstead.value) throw error; return null; } }; /** * Clears the last error message. */ const clearError = () => (lastError.value = null); /** * useTaskDefinitions composable * * Returns reactive state and CRUD methods for task definitions. * @returns Object with state and API methods */ export const useTaskDefinitions = () => ({ definitions: readonly(definitions), pagination: readonly(pagination), isLoading: readonly(isLoading), lastError: readonly(lastError), loadDefinitions, getDefinition, createDefinition, updateDefinition, deleteDefinition, toggleEnabled, clearError, throwInstead, }); export default useTaskDefinitions;