feat(frontend): improve Table component, add todo detail route
This commit is contained in:
parent
0fbcd7116b
commit
e33501eaa6
14 changed files with 312 additions and 156 deletions
|
@ -59,6 +59,8 @@ def read_todo(
|
||||||
if not (current_user.is_admin or current_user.id == todo_owner.id):
|
if not (current_user.is_admin or current_user.id == todo_owner.id):
|
||||||
raise HTTPException(403, "You are not authorized to view this content.")
|
raise HTTPException(403, "You are not authorized to view this content.")
|
||||||
|
|
||||||
|
return todo
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/{user_id}", response_model=list[todoschema.TodoItem])
|
@router.get("/user/{user_id}", response_model=list[todoschema.TodoItem])
|
||||||
def read_todos(
|
def read_todos(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { ItemCount, User } from './types';
|
import type { ItemCount, TodoItem, User } from './types';
|
||||||
import { getResponseBodyOrError } from '$lib/api/utils';
|
import { getResponseBodyOrError } from '$lib/api/utils';
|
||||||
import { getTokenFromLocalstorage } from '$lib/auth/session';
|
import { getTokenFromLocalstorage } from '$lib/auth/session';
|
||||||
|
|
||||||
|
@ -45,10 +45,22 @@ export async function readUser(userId: number, jwt: string = getTokenOrError()):
|
||||||
* @param {string} jwt - The JWT appended as a bearer token to authorize the request.
|
* @param {string} jwt - The JWT appended as a bearer token to authorize the request.
|
||||||
* @throws{error} - If the request fails or is not permitted.
|
* @throws{error} - If the request fails or is not permitted.
|
||||||
*/
|
*/
|
||||||
export async function readUsers(jwt: string = getTokenOrError()): Promise<User[]> {
|
export async function readUsers(
|
||||||
// TODO add pagination and sorting query params
|
skip: number,
|
||||||
// TODO refactor Table.svelte
|
limit: number,
|
||||||
const endpoint = '/api/admin/users/';
|
sortby: string,
|
||||||
|
sortorder: string,
|
||||||
|
jwt: string = getTokenOrError(),
|
||||||
|
): Promise<User[]> {
|
||||||
|
|
||||||
|
const urlParameters = new URLSearchParams({
|
||||||
|
skip: `${skip}`,
|
||||||
|
limit: `${limit}`,
|
||||||
|
sortby: `${sortby}`,
|
||||||
|
sortorder: `${sortorder}`,
|
||||||
|
});
|
||||||
|
const endpoint = `/api/admin/users/?${urlParameters}`;
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -83,3 +95,88 @@ export async function readUserCount(jwt: string = getTokenOrError()): Promise<nu
|
||||||
const itemCount = responseJson as ItemCount;
|
const itemCount = responseJson as ItemCount;
|
||||||
return itemCount.total;
|
return itemCount.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of all todo-items for the user with the specified ID from the backend API.
|
||||||
|
*
|
||||||
|
* @param {string} jwt - The JWT appended as a bearer token to authorize the request.
|
||||||
|
* @throws{error} - If the request fails or is not permitted.
|
||||||
|
*/
|
||||||
|
export async function readTodos(
|
||||||
|
userId: number,
|
||||||
|
skip: number,
|
||||||
|
limit: number,
|
||||||
|
sortby: string,
|
||||||
|
sortorder: string,
|
||||||
|
jwt: string = getTokenOrError(),
|
||||||
|
): Promise<TodoItem[]> {
|
||||||
|
|
||||||
|
const urlParameters = new URLSearchParams({
|
||||||
|
skip: `${skip}`,
|
||||||
|
limit: `${limit}`,
|
||||||
|
sortby: `${sortby}`,
|
||||||
|
sortorder: `${sortorder}`,
|
||||||
|
});
|
||||||
|
const endpoint = `/api/todos/user/${userId}?${urlParameters}`;
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await getResponseBodyOrError(response);
|
||||||
|
return responseJson as TodoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the total todo-item count for the user with the specified ID from the backend API.
|
||||||
|
*
|
||||||
|
* @param {string} jwt - The JWT appended as a bearer token to authorize the request.
|
||||||
|
* @throws{error} - If the request fails or is not permitted.
|
||||||
|
*/
|
||||||
|
export async function readTodoCount(
|
||||||
|
userId: number,
|
||||||
|
jwt: string = getTokenOrError()
|
||||||
|
): Promise<number> {
|
||||||
|
const endpoint = `/api/todos/user/${userId}/total/`;
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await getResponseBodyOrError(response);
|
||||||
|
const itemCount = responseJson as ItemCount;
|
||||||
|
return itemCount.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the todo-item with the specified ID from the backend API.
|
||||||
|
*
|
||||||
|
* @param {string} jwt - The JWT appended as a bearer token to authorize the request.
|
||||||
|
* @throws{error} - If the request fails or is not permitted.
|
||||||
|
*/
|
||||||
|
export async function readTodo(
|
||||||
|
todoId: number,
|
||||||
|
jwt: string = getTokenOrError()
|
||||||
|
): Promise<TodoItem> {
|
||||||
|
const endpoint = `/api/todos/${todoId}`;
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await getResponseBodyOrError(response);
|
||||||
|
return responseJson as TodoItem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
export type ItemCount = {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number,
|
id: number,
|
||||||
email: string,
|
email: string,
|
||||||
|
@ -8,6 +12,12 @@ export type User = {
|
||||||
is_admin: boolean,
|
is_admin: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemCount = {
|
export type TodoItem = {
|
||||||
total: number
|
id: number,
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
done: boolean,
|
||||||
|
created: Date,
|
||||||
|
updated: Date,
|
||||||
|
finished: Date,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,35 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Icon, ChevronRight, ChevronLeft, ChevronDoubleRight, ChevronDoubleLeft } from 'svelte-hero-icons';
|
import { Icon, ChevronRight, ChevronLeft, ChevronDoubleRight, ChevronDoubleLeft } from 'svelte-hero-icons';
|
||||||
import Th from '$lib/data-table/Th.svelte';
|
import Th from '$lib/components/data-table/Th.svelte';
|
||||||
import Td from '$lib/data-table/Td.svelte';
|
import Td from '$lib/components/data-table/Td.svelte';
|
||||||
|
import type { Column, Endpoint } from '$lib/components/data-table/types';
|
||||||
|
import { interpolateString as i } from '$lib/components/data-table/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The caption for the data table.
|
||||||
|
*/
|
||||||
export let caption: string | null = null;
|
export let caption: string | null = null;
|
||||||
|
/**
|
||||||
|
* The columns for the data table.
|
||||||
|
*/
|
||||||
export let columns: Column[];
|
export let columns: Column[];
|
||||||
|
|
||||||
export let itemCountEndpoint: string;
|
/**
|
||||||
export let itemsEndpoint: string;
|
* The function which fetches rows for the data table.
|
||||||
|
* Must support pagination and sorting.
|
||||||
type Column = {
|
*/
|
||||||
field: string,
|
export let getItemsEndpoint: Endpoint = {
|
||||||
heading: string,
|
callable: () => [],
|
||||||
type?: string,
|
args: [],
|
||||||
sortable?: boolean,
|
}
|
||||||
};
|
/**
|
||||||
|
* The function which fetches the total number of items. Used for pagination.
|
||||||
type TotalItemCountApiResponse = {
|
*/
|
||||||
total: number
|
export let getItemCountEndpoint: Endpoint = {
|
||||||
};
|
callable: () => 0,
|
||||||
|
args: [],
|
||||||
// API error message bodies are expected to be in one of the following formats:
|
}
|
||||||
interface ApiErrorMessage {
|
|
||||||
detail: string
|
|
||||||
};
|
|
||||||
interface ApiErrorList {
|
|
||||||
detail: { msg: string; loc?: string[]; type?: string }[]
|
|
||||||
};
|
|
||||||
|
|
||||||
type SortEvent = {
|
type SortEvent = {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -117,66 +119,19 @@
|
||||||
await updateTable();
|
await updateTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTotalItemCount(): Promise<number> {
|
|
||||||
const response = await fetch(itemCountEndpoint);
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Try to parse error body as a known format
|
|
||||||
if ('detail' in json) {
|
|
||||||
if (typeof json.detail === 'string') {
|
|
||||||
const errorBody = json as ApiErrorMessage;
|
|
||||||
errorMessages.push(errorBody.detail);
|
|
||||||
} else {
|
|
||||||
const errorBody = json as ApiErrorList;
|
|
||||||
for (let detail of errorBody.detail) {
|
|
||||||
if (typeof detail.msg === 'string') errorMessages.push(detail.msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`API request failed with status ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiResponse = json as TotalItemCountApiResponse;
|
|
||||||
return apiResponse.total;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getItems<T>(offset: number = currentItemsOffset, count: number = currentItemsPerPage): Promise<T[]> {
|
|
||||||
const urlParameters = new URLSearchParams({
|
|
||||||
skip: `${offset}`,
|
|
||||||
limit: `${count}`,
|
|
||||||
sortby: `${currentSortField}`,
|
|
||||||
sortorder: `${currentSortOrder}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`${itemsEndpoint}?${urlParameters}`);
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// Try to parse error body as a known format
|
|
||||||
if ('detail' in json) {
|
|
||||||
if (typeof json.detail === 'string') {
|
|
||||||
const errorBody = json as ApiErrorMessage;
|
|
||||||
errorMessages.push(errorBody.detail);
|
|
||||||
} else {
|
|
||||||
const errorBody = json as ApiErrorList;
|
|
||||||
for (let detail of errorBody.detail) {
|
|
||||||
if (typeof detail.msg === 'string') errorMessages.push(detail.msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`API request failed with status ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiResponse = json as T[];
|
|
||||||
return apiResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateTable() {
|
async function updateTable() {
|
||||||
try {
|
try {
|
||||||
currentState = 'loading';
|
currentState = 'loading';
|
||||||
totalItemCount = await getTotalItemCount();
|
totalItemCount = await getItemCountEndpoint.callable(
|
||||||
currentItems = await getItems();
|
...getItemCountEndpoint.args,
|
||||||
|
);
|
||||||
|
currentItems = await getItemsEndpoint.callable(
|
||||||
|
...getItemsEndpoint.args,
|
||||||
|
currentItemsOffset,
|
||||||
|
currentItemsPerPage,
|
||||||
|
currentSortField,
|
||||||
|
currentSortOrder
|
||||||
|
);
|
||||||
currentState = 'finished';
|
currentState = 'finished';
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (!errorMessages.length) {
|
if (!errorMessages.length) {
|
||||||
|
@ -229,6 +184,8 @@
|
||||||
<Td
|
<Td
|
||||||
data={item[column.field]}
|
data={item[column.field]}
|
||||||
type={column.type ?? 'string'}
|
type={column.type ?? 'string'}
|
||||||
|
isLink={column.isLink ?? false}
|
||||||
|
linkTarget={i(column.linkTarget, item) ?? ''}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
export let data: any;
|
export let data: any;
|
||||||
export let type: 'string' | 'date' | 'boolean' = 'string';
|
export let type: 'string' | 'date' | 'boolean' = 'string';
|
||||||
|
export let isLink: boolean = false;
|
||||||
|
export let linkTarget: string = "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
@ -18,7 +20,11 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Icon src={XMark} mini class="w-6 h-6" />
|
<Icon src={XMark} mini class="w-6 h-6" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#if isLink}
|
||||||
|
<a href={linkTarget}>{data}</a>
|
||||||
{:else}
|
{:else}
|
||||||
{data}
|
{data}
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
13
frontend/src/lib/components/data-table/types.ts
Normal file
13
frontend/src/lib/components/data-table/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export type Column = {
|
||||||
|
field: string,
|
||||||
|
heading: string,
|
||||||
|
type?: string,
|
||||||
|
sortable?: boolean,
|
||||||
|
isLink?: boolean,
|
||||||
|
linkTarget?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Endpoint = {
|
||||||
|
callable: Function,
|
||||||
|
args: any[],
|
||||||
|
}
|
11
frontend/src/lib/components/data-table/utils.ts
Normal file
11
frontend/src/lib/components/data-table/utils.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export function interpolateString(templateString: string, replacementItems: { [key: string]: any }): string {
|
||||||
|
if (typeof templateString !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return templateString.replace(/%(\w+)%/g, (match, key) => {
|
||||||
|
if (replacementItems.hasOwnProperty(key)) {
|
||||||
|
return replacementItems[key].toString();
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
33
frontend/src/lib/components/navbar/Navbar.svelte
Normal file
33
frontend/src/lib/components/navbar/Navbar.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { logout, storedUser } from '$lib/auth/session';
|
||||||
|
import type { StoredUser } from '$lib/auth/session';
|
||||||
|
|
||||||
|
let user: StoredUser | null = null;
|
||||||
|
storedUser.subscribe((value) => {
|
||||||
|
user = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
logout();
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li class="inline">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
{#if user}
|
||||||
|
<a href={`/users/${user.id}/todos`}>Todos</a>
|
||||||
|
<a href={`/users/${user.id}`}>My Profile</a>
|
||||||
|
{#if user.isAdmin}
|
||||||
|
<a href={`/users/all`}>All Users</a>
|
||||||
|
{/if}
|
||||||
|
<button on:click={handleLogout}>Log Out</button>
|
||||||
|
{:else}
|
||||||
|
<a href="/login">Log In</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
|
@ -23,11 +23,10 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { loadUserIntoStore, logout, storedUser } from '$lib/auth/session';
|
import { loadUserIntoStore } from '$lib/auth/session';
|
||||||
import type { StoredUser } from '$lib/auth/session';
|
import Navbar from '$lib/components/navbar/Navbar.svelte';
|
||||||
|
|
||||||
export let title = import.meta.env.PUBLIC_TITLE;
|
export let title = import.meta.env.PUBLIC_TITLE;
|
||||||
export let description = import.meta.env.PUBLIC_DESCRIPTION;
|
export let description = import.meta.env.PUBLIC_DESCRIPTION;
|
||||||
|
@ -35,32 +34,10 @@
|
||||||
|
|
||||||
const ogImageUrl = new URL('/images/common/og-image.webp', import.meta.url).href
|
const ogImageUrl = new URL('/images/common/og-image.webp', import.meta.url).href
|
||||||
|
|
||||||
let user: StoredUser | null = null;
|
|
||||||
storedUser.subscribe((value) => {
|
|
||||||
user = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleLogout() {
|
|
||||||
logout();
|
|
||||||
goto('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loadUserIntoStore();
|
loadUserIntoStore();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<Navbar />
|
||||||
<ul>
|
|
||||||
<li class="inline">
|
|
||||||
<a href="/">Home</a>
|
|
||||||
{#if user}
|
|
||||||
<a href={`/users/${user.id}`}>My Profile</a>
|
|
||||||
<button on:click={handleLogout}>Log Out</button>
|
|
||||||
{:else}
|
|
||||||
<a href="/login">Log In</a>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
7
frontend/src/routes/todos/[todo]/+page.svelte
Normal file
7
frontend/src/routes/todos/[todo]/+page.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { TodoDetailPage } from "./+page";
|
||||||
|
|
||||||
|
export let data: TodoDetailPage;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{data.todo.title}</h1>
|
18
frontend/src/routes/todos/[todo]/+page.ts
Normal file
18
frontend/src/routes/todos/[todo]/+page.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
import type { TodoItem } from '$lib/api/types';
|
||||||
|
import { readTodo } from '$lib/api/endpoints';
|
||||||
|
|
||||||
|
export const ssr = false;
|
||||||
|
export const load = (async ({ params }) => {
|
||||||
|
// check if user exists
|
||||||
|
const todoId = params.todo;
|
||||||
|
const todo = await readTodo(todoId);
|
||||||
|
return {
|
||||||
|
todo: todo
|
||||||
|
};
|
||||||
|
|
||||||
|
}) satisfies PageLoad;
|
||||||
|
|
||||||
|
export interface UserProfilePage {
|
||||||
|
todo: TodoItem
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Table from '$lib/data-table/Table.svelte'
|
import Table from '$lib/components/data-table/Table.svelte'
|
||||||
import type { Column } from '$lib/data-table/Table.svelte';
|
import type { Column } from '$lib/components/data-table/types';
|
||||||
import type { UserTodosPage } from "./+page";
|
import type { UserTodosPage } from "./+page";
|
||||||
|
import { readTodos, readTodoCount } from '$lib/api/endpoints';
|
||||||
|
|
||||||
export let data: UserTodosPage;
|
export let data: UserTodosPage;
|
||||||
|
|
||||||
const cols: Column[] = [
|
const columns: Column[] = [
|
||||||
{
|
{
|
||||||
field: "id",
|
field: "id",
|
||||||
heading: "ID",
|
heading: "ID",
|
||||||
|
@ -13,6 +14,8 @@
|
||||||
{
|
{
|
||||||
field: "title",
|
field: "title",
|
||||||
heading: "Title",
|
heading: "Title",
|
||||||
|
isLink: true,
|
||||||
|
linkTarget: '/todos/%id%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "description",
|
field: "description",
|
||||||
|
@ -43,9 +46,19 @@
|
||||||
|
|
||||||
const table = {
|
const table = {
|
||||||
caption: `List of TODOs for user ${data.user.first_name} ${data.user.last_name}`,
|
caption: `List of TODOs for user ${data.user.first_name} ${data.user.last_name}`,
|
||||||
columns: cols,
|
columns: columns,
|
||||||
itemsEndpoint: `/api/todos/user/${data.user.id}/`,
|
getItemsEndpoint: {
|
||||||
itemCountEndpoint: `/api/todos/user/${data.user.id}/total/`,
|
callable: readTodos,
|
||||||
|
args: [
|
||||||
|
data.user.id
|
||||||
|
]
|
||||||
|
},
|
||||||
|
getItemCountEndpoint: {
|
||||||
|
callable: readTodoCount,
|
||||||
|
args: [
|
||||||
|
data.user.id
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Table from '$lib/data-table/Table.svelte'
|
import Table from '$lib/components/data-table/Table.svelte';
|
||||||
|
import type { Column } from '$lib/components/data-table/types';
|
||||||
|
import { readUsers, readUserCount } from '$lib/api/endpoints';
|
||||||
|
|
||||||
const table = {
|
const columns: Column[] = [
|
||||||
caption: "List of users",
|
|
||||||
columns: [
|
|
||||||
{
|
{
|
||||||
field: "id",
|
field: "id",
|
||||||
heading: "ID",
|
heading: "ID",
|
||||||
|
@ -11,6 +11,8 @@
|
||||||
{
|
{
|
||||||
field: "email",
|
field: "email",
|
||||||
heading: "Email",
|
heading: "Email",
|
||||||
|
isLink: true,
|
||||||
|
linkTarget: '/users/%id%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "first_name",
|
field: "first_name",
|
||||||
|
@ -35,9 +37,19 @@
|
||||||
heading: "Admin",
|
heading: "Admin",
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
itemCountEndpoint: "/api/users/total/",
|
|
||||||
itemsEndpoint: "/api/users/",
|
const table = {
|
||||||
|
caption: "List of users",
|
||||||
|
columns: columns,
|
||||||
|
getItemsEndpoint: {
|
||||||
|
callable: readUsers,
|
||||||
|
args: []
|
||||||
|
},
|
||||||
|
getItemCountEndpoint: {
|
||||||
|
callable: readUserCount,
|
||||||
|
args: []
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue