119 lines
3.5 KiB
Svelte
119 lines
3.5 KiB
Svelte
<!-- [DEF:FrontendComponentShot:Component] -->
|
|
<!--
|
|
/**
|
|
* @TIER: CRITICAL
|
|
* @SEMANTICS: Task, Button, Action, UX
|
|
* @PURPOSE: Action button to spawn a new task with full UX feedback cycle.
|
|
* @LAYER: UI (Presentation)
|
|
* @RELATION: CALLS -> postApi
|
|
*
|
|
* @INVARIANT: Must prevent double-submission while loading.
|
|
* @INVARIANT: Loading state must always terminate (no infinite spinner).
|
|
* @INVARIANT: User must receive feedback on both success and failure.
|
|
*
|
|
* @TEST_CONTRACT: ComponentState ->
|
|
* {
|
|
* required_fields: {
|
|
* isLoading: bool
|
|
* },
|
|
* invariants: [
|
|
* "isLoading=true implies button.disabled=true",
|
|
* "isLoading=true implies aria-busy=true",
|
|
* "isLoading=true implies spinner visible"
|
|
* ]
|
|
* }
|
|
*
|
|
* @TEST_CONTRACT: ApiResponse ->
|
|
* {
|
|
* required_fields: {},
|
|
* optional_fields: {
|
|
* task_id: str
|
|
* }
|
|
* }
|
|
|
|
* @TEST_FIXTURE: idle_state ->
|
|
* {
|
|
* isLoading: false
|
|
* }
|
|
*
|
|
* @TEST_FIXTURE: successful_response ->
|
|
* {
|
|
* task_id: "task_123"
|
|
* }
|
|
|
|
* @TEST_EDGE: api_failure -> raises Error("Network")
|
|
* @TEST_EDGE: empty_response -> {}
|
|
* @TEST_EDGE: rapid_double_click -> special: concurrent_click
|
|
* @TEST_EDGE: unresolved_promise -> special: pending_state
|
|
|
|
* @TEST_INVARIANT: prevent_double_submission -> verifies: [rapid_double_click]
|
|
* @TEST_INVARIANT: loading_state_consistency -> verifies: [idle_state, pending_state]
|
|
* @TEST_INVARIANT: feedback_always_emitted -> verifies: [successful_response, api_failure]
|
|
|
|
* @UX_STATE: Idle -> Button enabled, primary color, no spinner.
|
|
* @UX_STATE: Loading -> Button disabled, spinner visible, aria-busy=true.
|
|
* @UX_STATE: Success -> Toast success displayed.
|
|
* @UX_STATE: Error -> Toast error displayed.
|
|
*
|
|
* @UX_FEEDBACK: toast.success, toast.error
|
|
*
|
|
* @UX_TEST: Idle -> {click: spawnTask, expected: isLoading=true}
|
|
* @UX_TEST: Loading -> {double_click: ignored, expected: single_api_call}
|
|
* @UX_TEST: Success -> {api_resolve: task_id, expected: toast.success called}
|
|
* @UX_TEST: Error -> {api_reject: error, expected: toast.error called}
|
|
-->
|
|
<script>
|
|
import { postApi } from "$lib/api.js";
|
|
import { t } from "$lib/i18n";
|
|
import { toast } from "$lib/stores/toast";
|
|
|
|
export let plugin_id = "";
|
|
export let params = {};
|
|
|
|
let isLoading = false;
|
|
|
|
// [DEF:spawnTask:Function]
|
|
/**
|
|
* @purpose Execute task creation request and emit user feedback.
|
|
* @pre plugin_id is resolved and request params are serializable.
|
|
* @post isLoading is reset and user receives success/error feedback.
|
|
*/
|
|
async function spawnTask() {
|
|
isLoading = true;
|
|
console.log("[FrontendComponentShot][Loading] Spawning task...");
|
|
|
|
try {
|
|
// 1. Action: API Call
|
|
const response = await postApi("/api/tasks", {
|
|
plugin_id,
|
|
params
|
|
});
|
|
|
|
// 2. Feedback: Success
|
|
if (response.task_id) {
|
|
console.log("[FrontendComponentShot][Success] Task created.");
|
|
toast.success($t.tasks.spawned_success);
|
|
}
|
|
} catch (error) {
|
|
// 3. Recovery: User notification
|
|
console.log("[FrontendComponentShot][Error] Failed:", error);
|
|
toast.error(`${$t.errors.task_failed}: ${error.message}`);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
// [/DEF:spawnTask:Function]
|
|
</script>
|
|
|
|
<button
|
|
on:click={spawnTask}
|
|
disabled={isLoading}
|
|
class="btn-primary flex items-center gap-2"
|
|
aria-busy={isLoading}
|
|
>
|
|
{#if isLoading}
|
|
<span class="animate-spin" aria-label="Loading">🌀</span>
|
|
{/if}
|
|
<span>{$t.actions.start_task}</span>
|
|
</button>
|
|
<!-- [/DEF:FrontendComponentShot:Component] --> |