Rewrite PointListModal with svelte5 runes. fixes reactivity
This commit is contained in:
parent
0f79cefdac
commit
1a89d49e8a
4 changed files with 136 additions and 60 deletions
|
|
@ -18,16 +18,37 @@ export async function fetchAPI<T>(endpoint: string, options: RequestInit = {}):
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
let errorText = await response.json();
|
||||||
|
if (
|
||||||
|
errorText &&
|
||||||
|
typeof errorText === "object" &&
|
||||||
|
("detail" in errorText || "field_errors" in errorText || "non_field_errors" in errorText)
|
||||||
|
) {
|
||||||
|
// Handle structured error responses
|
||||||
|
if ("detail" in errorText) {
|
||||||
|
errorText = errorText.detail;
|
||||||
|
} else if ("field_errors" in errorText) {
|
||||||
|
errorText = Object.values(errorText.field_errors).join(", ");
|
||||||
|
} else if ("non_field_errors" in errorText) {
|
||||||
|
errorText = errorText.non_field_errors.join(", ");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorText = `Unexpected error: ${response.statusText}`;
|
||||||
|
}
|
||||||
|
throw new Error(`${errorText}`);
|
||||||
}
|
}
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
// No content response
|
// No content response
|
||||||
return {} as T; // Return an empty object for 204 responses
|
return {} as T; // Return an empty object for 204 responses
|
||||||
}
|
}
|
||||||
return await response.json() as T;
|
return (await response.json()) as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching ${url}:`, error);
|
console.error(`Error fetching ${url}:`, error);
|
||||||
throw error;
|
if (error instanceof Error) {
|
||||||
|
// If the error is an instance of Error, rethrow it
|
||||||
|
return Promise.reject(new Error(`${error.message}`));
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`${error}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,4 +82,4 @@ export function deleteAPI<T>(endpoint: string): Promise<T> {
|
||||||
return fetchAPI<T>(endpoint, {
|
return fetchAPI<T>(endpoint, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@
|
||||||
|
|
||||||
$: $SavedPointsStore, setCoordinatesFromSavedPoint();
|
$: $SavedPointsStore, setCoordinatesFromSavedPoint();
|
||||||
|
|
||||||
$: inputLat, inputLng, setToCustomOnChange();
|
|
||||||
|
|
||||||
function setCoordinatesFromSavedPoint() {
|
function setCoordinatesFromSavedPoint() {
|
||||||
console.log("Start point changed:", startPoint);
|
console.log("Start point changed:", startPoint);
|
||||||
|
|
||||||
|
|
@ -135,6 +133,7 @@
|
||||||
console.log("Launch position updated:", lat, lng);
|
console.log("Launch position updated:", lat, lng);
|
||||||
inputLat = lat.toFixed(6).toString();
|
inputLat = lat.toFixed(6).toString();
|
||||||
inputLng = lng.toFixed(6).toString();
|
inputLng = lng.toFixed(6).toString();
|
||||||
|
setToCustomOnChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElement = () => {
|
export const getElement = () => {
|
||||||
|
|
@ -253,9 +252,9 @@
|
||||||
<FormGroup spacing="mb-2">
|
<FormGroup spacing="mb-2">
|
||||||
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
|
<Label for="latitude" class="form-label">Широта/Долгота:</Label>
|
||||||
<InputGroup size="sm">
|
<InputGroup size="sm">
|
||||||
<Input id="latitude" type="text" bind:value={inputLat} placeholder="Latitude" />
|
<Input id="latitude" type="text" bind:value={inputLat} placeholder="Latitude" on:change={setToCustomOnChange} />
|
||||||
<InputGroupText>/</InputGroupText>
|
<InputGroupText>/</InputGroupText>
|
||||||
<Input id="longitude" type="text" bind:value={inputLng} placeholder="Longitude" />
|
<Input id="longitude" type="text" bind:value={inputLng} placeholder="Longitude" on:change={setToCustomOnChange} />
|
||||||
<Button color="success" size="sm" on:click={applyCoordinatesFromInput} title="Apply Coordinates"
|
<Button color="success" size="sm" on:click={applyCoordinatesFromInput} title="Apply Coordinates"
|
||||||
>✓</Button
|
>✓</Button
|
||||||
>
|
>
|
||||||
|
|
@ -278,6 +277,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
id="startHeight"
|
id="startHeight"
|
||||||
class="form-control-sm"
|
class="form-control-sm"
|
||||||
|
on:change={setToCustomOnChange}
|
||||||
bind:value={$FlightParametersStore.launch_altitude}
|
bind:value={$FlightParametersStore.launch_altitude}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,57 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TableHandler, Datatable } from '@vincjo/datatables'
|
import { TableHandler } from '@vincjo/datatables';
|
||||||
import { Modal,
|
import { Modal,
|
||||||
Button,
|
Button,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
InputGroup,
|
Alert,
|
||||||
InputGroupText,
|
|
||||||
Icon,
|
Icon,
|
||||||
Pagination,
|
Pagination,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
PaginationLink,
|
PaginationLink,
|
||||||
} from '@sveltestrap/sveltestrap';
|
} from '@sveltestrap/sveltestrap';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { addToast } from '$lib/components/Toast.svelte';
|
||||||
import type { SavedPoint } from '$lib/types';
|
import type { SavedPoint } from '$lib/types';
|
||||||
import { SavedPointsStore } from '$lib/stores';
|
import { SavedPointsStore } from '$lib/stores';
|
||||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points';
|
import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points';
|
||||||
|
|
||||||
export let isOpen: boolean = false;
|
// Props
|
||||||
export let onClose: () => void = () => {};
|
let { isOpen = $bindable(false), onClose = () => {} } = $props();
|
||||||
|
|
||||||
let points: SavedPoint[] = [];
|
// Runes
|
||||||
const table = new TableHandler($SavedPointsStore, { rowsPerPage: 10 })
|
let points = $state<SavedPoint[]>([]);
|
||||||
|
let selectedPoint = $state<SavedPoint | null>(null);
|
||||||
|
let newPoint = $state<SavedPoint>({ id: 0, name: '', lat: 0, lon: 0, alt: 0 });
|
||||||
|
let isEditing = $state(false);
|
||||||
|
let isAlertVisible = $state(false);
|
||||||
|
let alertText = $state('');
|
||||||
|
|
||||||
let selectedPoint: SavedPoint | null = null;
|
// Derived state
|
||||||
let newPoint: SavedPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
let modalTitle = $derived(isEditing ? 'Редактирование точки' : 'Сохраненные точки');
|
||||||
let isEditing: boolean = false;
|
|
||||||
let modalTitle: string = 'Сохраненные точки';
|
|
||||||
|
|
||||||
|
// Table handler
|
||||||
|
let table = $derived(new TableHandler(points, { rowsPerPage: 10 }));
|
||||||
|
|
||||||
|
// Sync with store
|
||||||
|
$effect(() => {
|
||||||
|
points = $SavedPointsStore || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// On mount, fetch points
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
points = await getSavedPoints();
|
const pts = await getSavedPoints();
|
||||||
|
points = pts;
|
||||||
SavedPointsStore.set(points);
|
SavedPointsStore.set(points);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modal controls
|
||||||
export function openModal() {
|
export function openModal() {
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
modalTitle = 'Сохраненные точки';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeModal() {
|
function closeModal() {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
@ -48,13 +60,20 @@
|
||||||
selectedPoint = point;
|
selectedPoint = point;
|
||||||
newPoint = { ...point };
|
newPoint = { ...point };
|
||||||
isEditing = true;
|
isEditing = true;
|
||||||
modalTitle = 'Редактирование точки';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeletePoint(point: SavedPoint) {
|
function handleDeletePoint(point: SavedPoint) {
|
||||||
deletePoint(point.id).then(() => {
|
deletePoint(point.id).then(() => {
|
||||||
points = points.filter(p => p.id !== point.id);
|
points = points.filter(p => p.id !== point.id);
|
||||||
SavedPointsStore.set(points);
|
SavedPointsStore.set(points);
|
||||||
|
addToast({
|
||||||
|
header: 'Точка удалена',
|
||||||
|
body: `Точка "${point.name}" успешно удалена.`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||||
|
console.error('Ошибка при удалении точки:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,39 +82,57 @@
|
||||||
updatePoint(newPoint).then(updatedPoint => {
|
updatePoint(newPoint).then(updatedPoint => {
|
||||||
points = points.map(p => (p.id === updatedPoint.id ? updatedPoint : p));
|
points = points.map(p => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||||
SavedPointsStore.set(points);
|
SavedPointsStore.set(points);
|
||||||
table.setRows(points);
|
|
||||||
resetForm();
|
resetForm();
|
||||||
|
addToast({
|
||||||
|
header: 'Точка обновлена',
|
||||||
|
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
savePoint(newPoint).then(savedPoint => {
|
savePoint(newPoint).then(savedPoint => {
|
||||||
points.push(savedPoint);
|
points = [...points, savedPoint];
|
||||||
points = [...points]; // Trigger reactivity
|
|
||||||
SavedPointsStore.set(points);
|
SavedPointsStore.set(points);
|
||||||
table.setRows(points);
|
|
||||||
resetForm();
|
resetForm();
|
||||||
|
addToast({
|
||||||
|
header: 'Точка сохранена',
|
||||||
|
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||||
|
color: 'success',
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||||
|
console.error('Ошибка при сохранении точки:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showAlert(message: string) {
|
||||||
|
isAlertVisible = true;
|
||||||
|
alertText = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideAlert() {
|
||||||
|
isAlertVisible = false;
|
||||||
|
alertText = '';
|
||||||
|
}
|
||||||
|
|
||||||
export function resetForm() {
|
export function resetForm() {
|
||||||
selectedPoint = null;
|
selectedPoint = null;
|
||||||
newPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
newPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
modalTitle = 'Сохраненные точки';
|
hideAlert();
|
||||||
}
|
|
||||||
|
|
||||||
$: if (isOpen) {
|
|
||||||
SavedPointsStore.set(points);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal isOpen={isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
<Modal {isOpen} toggle={closeModal} size="lg" fade={false} backdrop={true} scrollable>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">{modalTitle}</h5>
|
<h5 class="modal-title">{modalTitle}</h5>
|
||||||
<button type="button" class="btn-close" on:click={closeModal} aria-label="Close"></button>
|
<button type="button" class="btn-close" onclick={closeModal} aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" >
|
<div class="modal-body">
|
||||||
<Datatable {table}>
|
<div bind:this={table.element}>
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -107,17 +144,17 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each points as point (point.id)}
|
{#each table.rows as row}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{point.name}</td>
|
<td>{row.name}</td>
|
||||||
<td>{point.lat}</td>
|
<td>{row.lat}</td>
|
||||||
<td>{point.lon}</td>
|
<td>{row.lon}</td>
|
||||||
<td>{point.alt}</td>
|
<td>{row.alt}</td>
|
||||||
<td class="fit">
|
<td class="fit">
|
||||||
<Button color="primary" size="sm" on:click={() => handleEditPoint(point)}>
|
<Button color="primary" size="sm" onclick={() => handleEditPoint(row)}>
|
||||||
<Icon name="pencil" />
|
<Icon name="pencil" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="danger" size="sm" on:click={() => handleDeletePoint(point)}>
|
<Button color="danger" size="sm" onclick={() => handleDeletePoint(row)}>
|
||||||
<Icon name="trash" />
|
<Icon name="trash" />
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -125,25 +162,32 @@
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Datatable>
|
</div>
|
||||||
<Pagination aria-label="Page navigation">
|
<Pagination aria-label="Page navigation" size="sm">
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationLink previous on:click={() => table.setPage("previous")} />
|
<PaginationLink previous onclick={() => table.setPage("previous")} />
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{#each table.pagesWithEllipsis as page}
|
{#each table.pagesWithEllipsis as page}
|
||||||
<PaginationItem active={table.currentPage === page}>
|
<PaginationItem active={table.currentPage === page}>
|
||||||
<PaginationLink on:click={() => table.setPage(page)}>{page}</PaginationLink>
|
<PaginationLink onclick={() => table.setPage(page)}>{page}</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{/each}
|
{/each}
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationLink next on:click={() => table.setPage("next")} />
|
<PaginationLink next onclick={() => table.setPage("next")} />
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</Pagination>
|
</Pagination>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<!-- Form for adding/editing points -->
|
<!-- Form for adding/editing points -->
|
||||||
<div class="mt-2">
|
<div>
|
||||||
<h5>{isEditing ? 'Редактирование точки' : 'Добавить новую точку'}</h5>
|
<h5>{isEditing ? 'Редактирование точки' : 'Добавить новую точку'}</h5>
|
||||||
<form on:submit|preventDefault={handleSavePoint}>
|
<Alert color="danger" isOpen={isAlertVisible} toggle={() => (isAlertVisible = false)} fade={false}
|
||||||
|
class="mb-2">
|
||||||
|
<Icon name="exclamation-triangle" class="me-2" />
|
||||||
|
{alertText}
|
||||||
|
</Alert>
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleSavePoint(); }}>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<Label for="name" class="small">Имя:</Label>
|
<Label for="name" class="small">Имя:</Label>
|
||||||
<Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required />
|
<Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required />
|
||||||
|
|
@ -151,31 +195,30 @@
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<FormGroup class="flex-grow-1">
|
<FormGroup class="flex-grow-1">
|
||||||
<Label for="lat" class="small">Широта:</Label>
|
<Label for="lat" class="small">Широта:</Label>
|
||||||
<Input class="form-control-sm" type="number" id="lat" bind:value={newPoint.lat} required />
|
<Input class="form-control-sm" type="number" step="any" id="lat" bind:value={newPoint.lat} required />
|
||||||
<span class="form-text">Градусы</span>
|
<span class="form-text">Градусы</span>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="flex-grow-1">
|
<FormGroup class="flex-grow-1">
|
||||||
<Label for="lon" class="small">Долгота:</Label>
|
<Label for="lon" class="small">Долгота:</Label>
|
||||||
<Input class="form-control-sm" type="number" id="lon" bind:value={newPoint.lon} required />
|
<Input class="form-control-sm" type="number" step="any" id="lon" bind:value={newPoint.lon} required />
|
||||||
<span class="form-text">Градусы</span>
|
<span class="form-text">Градусы</span>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="flex-grow-1">
|
<FormGroup class="flex-grow-1">
|
||||||
<Label for="alt" class="small">Высота:</Label>
|
<Label for="alt" class="small">Высота:</Label>
|
||||||
<Input class="form-control-sm" type="number" id="alt" bind:value={newPoint.alt} required />
|
<Input class="form-control-sm" type="number" step="any" id="alt" bind:value={newPoint.alt} required />
|
||||||
<span class="form-text">Метры над ур. моря</span>
|
<span class="form-text">Метры над ур. моря</span>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" color="success">
|
<Button type="submit" color="success">
|
||||||
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
||||||
</Button>
|
</Button>
|
||||||
{#if isEditing}
|
{#if isEditing}
|
||||||
<Button type="button" color="secondary" on:click={resetForm} class="ms-2">Отмена</Button>
|
<Button type="button" color="secondary" onclick={resetForm} class="ms-2">Отмена</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<Button color="secondary" on:click={closeModal}>Закрыть</Button>
|
<Button color="secondary" onclick={closeModal}>Закрыть</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,17 @@
|
||||||
/** @type {import('svelte/store').Writable<ToastMessage[]>} */
|
/** @type {import('svelte/store').Writable<ToastMessage[]>} */
|
||||||
export const toasts = writable([]);
|
export const toasts = writable([]);
|
||||||
|
|
||||||
|
const TOAST_ICONS = {
|
||||||
|
primary: 'info-circle-fill',
|
||||||
|
secondary: 'info-circle-fill',
|
||||||
|
success: 'check-circle-fill',
|
||||||
|
danger: 'exclamation-triangle-fill',
|
||||||
|
warning: 'exclamation-circle-fill',
|
||||||
|
info: 'info-circle-fill',
|
||||||
|
light: 'lightbulb',
|
||||||
|
dark: 'question'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new toast to the list.
|
* Adds a new toast to the list.
|
||||||
* @param {Omit<ToastMessage, 'id'>} toast
|
* @param {Omit<ToastMessage, 'id'>} toast
|
||||||
|
|
@ -48,7 +59,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Toast, ToastBody, ToastHeader } from '@sveltestrap/sveltestrap';
|
import { Toast, ToastBody, ToastHeader, Icon } from '@sveltestrap/sveltestrap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a toast from the list by its ID.
|
* Removes a toast from the list by its ID.
|
||||||
|
|
@ -80,7 +91,8 @@
|
||||||
color={toast.color || 'info'}
|
color={toast.color || 'info'}
|
||||||
on:close={() => removeToast(toast.id)}
|
on:close={() => removeToast(toast.id)}
|
||||||
>
|
>
|
||||||
<ToastHeader toggle={() => removeToast(toast.id)}>
|
<ToastHeader toggle={() => removeToast(toast.id)} class={`text-${toast.color || 'text-info'}`}>
|
||||||
|
<Icon slot="icon" name={TOAST_ICONS[toast.color ? toast.color : 'info']} class="me-2" color={toast.color || 'info'} />
|
||||||
{toast.header}
|
{toast.header}
|
||||||
</ToastHeader>
|
</ToastHeader>
|
||||||
<ToastBody>
|
<ToastBody>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue