333 lines
8.8 KiB
TypeScript
333 lines
8.8 KiB
TypeScript
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<Array<TaskDefinitionSummary>>([]);
|
|
/**
|
|
* Pagination state for task definitions list.
|
|
*/
|
|
const pagination = ref<Pagination>({
|
|
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<boolean>(false);
|
|
/**
|
|
* Stores the last error message, if any.
|
|
*/
|
|
const lastError = ref<string | null>(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<TaskDefinitionSummary>): Array<TaskDefinitionSummary> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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<TaskDefinitionSummary>(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<TaskDefinitionDetailed | null> => {
|
|
try {
|
|
const response = await request(`/api/tasks/definitions/${id}`);
|
|
await ensureSuccess(response);
|
|
|
|
const payload = await response.json();
|
|
const detailed = await parse_api_response<TaskDefinitionDetailed>(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<TaskDefinitionDetailed | null> => {
|
|
try {
|
|
const response = await request('/api/tasks/definitions/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(definition),
|
|
});
|
|
|
|
await ensureSuccess(response);
|
|
|
|
const payload = await parse_api_response<TaskDefinitionDetailed>(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<TaskDefinitionDetailed | null> => {
|
|
try {
|
|
const response = await request(`/api/tasks/definitions/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(definition),
|
|
});
|
|
|
|
await ensureSuccess(response);
|
|
|
|
const payload = await parse_api_response<TaskDefinitionDetailed>(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<boolean> => {
|
|
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<TaskDefinitionDetailed | null> => {
|
|
try {
|
|
const response = await request(`/api/tasks/definitions/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
|
|
await ensureSuccess(response);
|
|
|
|
const payload = await parse_api_response<TaskDefinitionDetailed>(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;
|