qa: Refactor layout

This commit is contained in:
nemunaire 2023-07-26 15:15:49 +02:00
parent 859b6a318e
commit c13da8b574
19 changed files with 426 additions and 296 deletions

View File

@ -0,0 +1,52 @@
<script>
import { goto } from '$app/navigation';
import {
Button,
Icon,
} from 'sveltestrap';
import { themes, themesIdx } from '$lib/stores/themes';
export let theme = null;
export let exercice = {};
export let query_selected = null;
</script>
<h2>
{#if query_selected}
<Button
class="float-start"
color="link"
on:click={() => goto(theme?`themes/${theme.id}/${exercice.id}`:`exercices/${exercice.id}`)}
>
<Icon name="chevron-left" />
</Button>
{:else if theme}
<Button
class="float-start"
color="link"
on:click={() => goto('themes/' + theme.id)}
>
<Icon name="chevron-left" />
</Button>
{:else}
<Button
class="float-start"
color="link"
on:click={() => goto('exercices/')}
>
<Icon name="chevron-left" />
</Button>
{/if}
{exercice.title}
{#if exercice.wip}
<Icon name="cone-striped" />
{/if}
{#if $themes.length && $themesIdx[exercice.id_theme]}
<small>
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
</small>
<a href="../{$themesIdx[exercice.id_theme].urlid}/{exercice.urlid}" target="_self" class="float-right ml-2 btn btn-sm btn-info"><Icon name="play-fill" /> Site du challenge</a>
{/if}
</h2>

View File

@ -0,0 +1,38 @@
<script>
import {
Col,
Container,
Row,
} from 'sveltestrap';
import ExerciceHeader from '$lib/components/ExerciceHeader.svelte';
import QAItems from '$lib/components/QAItems.svelte';
export let theme = null;
export let exercice = {};
export let qaitems = null;
export let query_selected = null;
</script>
<ExerciceHeader
{exercice}
{query_selected}
{theme}
/>
<Container fluid class="flex-fill d-flex">
<Row class="flex-fill">
<Col md={3} class="px-0 py-2" style="background: #e7e8e9">
{#key countCreation}
<QAItems
{exercice}
queries={qaitems}
{query_selected}
{theme}
/>
{/key}
</Col>
<Col md={9} class="d-flex flex-column py-2">
<slot></slot>
</Col>
</Row>
</Container>

View File

@ -1,105 +0,0 @@
<script>
import { goto } from '$app/navigation';
import {
Button,
Col,
Container,
Icon,
Row,
Table,
} from 'sveltestrap';
import QAItems from '$lib/components/QAItems.svelte';
import QAItem from '$lib/components/QAItem.svelte';
import QANewItem from '$lib/components/QANewItem.svelte';
import { themes, themesIdx } from '$lib/stores/themes';
if ($themes.length == 0) {
themes.refresh();
}
export let theme_id = null;
export let exercice = {};
let query_selected = null;
let countCreation = 0;
</script>
<h2>
{#if query_selected}
<Button
class="float-start"
color="link"
on:click={() => query_selected = null}
>
<Icon name="chevron-left" />
</Button>
{:else if theme_id}
<Button
class="float-start"
color="link"
on:click={() => goto('themes/' + theme_id)}
>
<Icon name="chevron-left" />
</Button>
{:else}
<Button
class="float-start"
color="link"
on:click={() => goto('exercices/')}
>
<Icon name="chevron-left" />
</Button>
{/if}
{exercice.title}
{#if exercice.wip}
<Icon name="cone-striped" />
{/if}
{#if $themes.length && $themesIdx[exercice.id_theme]}
<small>
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
</small>
<a href="../{$themesIdx[exercice.id_theme].urlid}/{exercice.urlid}" target="_self" class="float-right ml-2 btn btn-sm btn-info"><Icon name="play-fill" /> Site du challenge</a>
{/if}
</h2>
<Container fluid class="flex-fill d-flex">
<Row class="flex-fill">
<Col md={3} class="px-0 py-2" style="background: #e7e8e9">
{#key countCreation}
<QAItems
bind:query_selected={query_selected}
{exercice}
/>
{/key}
</Col>
<Col md={9} class="d-flex flex-column py-2">
{#if query_selected}
<QAItem
bind:query_selected={query_selected}
on:update-queries={() => countCreation++}
/>
{:else}
<Row class="mb-3">
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html exercice.statement.replace("$FILES$", "../files")}
</div>
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html exercice.overview.replace("$FILES$", "../files")}
</div>
</Row>
<QANewItem
{exercice}
on:new-query={() => countCreation++}
/>
{/if}
</Col>
</Row>
</Container>

View File

@ -1,5 +1,5 @@
<script>
import { createEventDispatcher } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import {
Alert,
@ -20,19 +20,19 @@
import { ToastsStore } from '$lib/stores/toasts';
import { viewIdx } from '$lib/stores/todo';
const dispatch = createEventDispatcher();
export let query_selected;
export let theme = null;
export let exercice = null;
export let query;
let query_commentsP;
let thumbs = [];
let thumb_me = [];
let has_comments = false;
$: updateComments(query_selected)
$: updateComments(query)
function updateComments(query_selected) {
if (query_selected) {
query_commentsP = getQAComments(query_selected.id);
function updateComments(query) {
if (query) {
query_commentsP = getQAComments(query.id);
query_commentsP.then((comments) => {
thumbs = [];
thumb_me = [];
@ -65,8 +65,8 @@
let submissionInProgress = false;
function addComment() {
submissionInProgress = true;
newComment.save(query_selected.id).then(() => {
query_commentsP = getQAComments(query_selected.id);
newComment.save(query.id).then(() => {
query_commentsP = getQAComments(query.id);
newComment = new QAComment();
submissionInProgress = false;
}).catch((err) => {
@ -78,8 +78,8 @@
}
function updateQA() {
query_selected.save().then(() => {
dispatch("update-queries");
query.save().then(() => {
invalidate(`api/exercices/${exercice.id}/qa`);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -88,9 +88,9 @@
}
function solveQA() {
query_selected.solved = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
query.solved = new Date();
query.save().then(() => {
invalidate(`api/exercices/${exercice.id}/qa`);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -99,9 +99,9 @@
}
function reopenQA() {
query_selected.solved = null;
query_selected.save().then(() => {
dispatch("update-queries");
query.solved = null;
query.save().then(() => {
invalidate(`api/exercices/${exercice.id}/qa`);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -110,9 +110,9 @@
}
function closeQA() {
query_selected.closed = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
query.closed = new Date();
query.save().then(() => {
invalidate(`api/exercices/${exercice.id}/qa`);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -120,10 +120,10 @@
})
}
function deleteQA() {
query_selected.delete().then(() => {
query_selected = null;
dispatch("update-queries");
async function deleteQA() {
query.delete().then(async () => {
await invalidate(`api/exercices/${exercice.id}/qa`);
goto(theme?`themes/${theme.id}/${exercice.id}`:`exercices/${exercice.id}`);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -134,15 +134,15 @@
function deleteMyThumbs() {
if (thumb_me.length) {
for (const c of thumb_me) {
c.delete(query_selected.id);
c.delete(query.id);
}
dispatch("update-queries");
updateComments(query);
}
}
function deleteComment(comment) {
comment.delete(query_selected.id).then(() => {
dispatch("update-queries");
comment.delete(query.id).then(() => {
updateComments(query);
}).catch((err) => {
ToastsStore.addErrorToast({
msg: err,
@ -151,20 +151,20 @@
}
</script>
{#if query_selected}
{#if query}
<Card>
<CardHeader>
<div class="d-flex justify-content-between align-items-center">
<h4 class="card-title fw-bold mb-0">{query_selected.subject}</h4>
<h4 class="card-title fw-bold mb-0">{query.subject}</h4>
<div>
{#if $auth && !query_selected.solved && $viewIdx[query_selected.id_exercice]}
{#if $auth && !query.solved && $viewIdx[query.id_exercice]}
<Button on:click={solveQA} color="success">
<Icon name="check" />
Marquer comme résolu
</Button>
{/if}
{#if $auth && $auth.id_team == query_selected.id_team}
{#if query_selected.solved && !query_selected.closed && (query_selected.subject != "RAS" || query_selected.state != "ok")}
{#if $auth && $auth.id_team == query.id_team}
{#if query.solved && !query.closed && (query.subject != "RAS" || query.state != "ok")}
<Button on:click={closeQA} color="success">
<Icon name="check" />
Valider la résolution
@ -174,7 +174,7 @@
Réouvrir
</Button>
{/if}
{#if (!query_selected.solved && !has_comments) || (query_selected.subject == "RAS" && query_selected.state == "ok" && !has_comments)}
{#if (!query.solved && !has_comments) || (query.subject == "RAS" && query.state == "ok" && !has_comments)}
<Button on:click={deleteQA} color="danger">
<Icon name="trash-fill" />
Supprimer
@ -192,10 +192,10 @@
Qui ?
</div>
<div class="value">
{query_selected.user.split("@")[0]}
{query.user.split("@")[0]}
</div>
<div class="text-muted">
(team #{query_selected.id_team})
(team #{query.id_team})
</div>
</div>
</Col>
@ -205,7 +205,7 @@
<div class="heading">
État
</div>
<BadgeState class="value" state={query_selected.state} />
<BadgeState class="value" state={query.state} />
<div
class="text-muted"
title={thumbs.join(', ')}
@ -227,7 +227,7 @@
Date de création
</div>
<div class="value">
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
<DateFormat date={query.creation} dateStyle="long" timeStyle="medium" />
</div>
</div>
</Col>
@ -238,8 +238,8 @@
Date de résolution
</div>
<div class="value">
{#if query_selected.solved}
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
{#if query.solved}
<DateFormat date={query.solved} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
@ -253,8 +253,8 @@
Date de clôture
</div>
<div class="value">
{#if query_selected.closed}
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
{#if query.closed}
<DateFormat date={query.closed} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}

View File

@ -1,4 +1,6 @@
<script>
import { goto, invalidate } from '$app/navigation';
import {
Button,
Icon,
@ -11,11 +13,10 @@
export { className as class };
let className = '';
export let theme = null;
export let exercice = { };
export let query_selected = null;
let queriesP;
$: queriesP = getExerciceQA(exercice.id);
export let queries = [];
let thumbInProgress = null;
function thumbUp(qid) {
@ -25,56 +26,50 @@
});
thumb.save(qid).then(() => {
thumbInProgress = null;
invalidate(`api/exercices/${exercice.id}/qa`);
})
}
function updateQueries() {
queriesP = getExerciceQA(exercice.id);
}
</script>
{#await queriesP}
{:then queries}
<div class="list-group {className}" class:list-group-flush={true}>
{#if queries.length}
{#each queries as q (q.id)}
<button
type="button"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
class:active={query_selected && q.id == query_selected.id}
aria-current="true"
on:click={() => query_selected = q}
>
<div class="text-truncate">
<BadgeState state={q.state} />
{#if !q.solved}
<strong>{q.subject}</strong>
{:else if !q.closed}
{q.subject}
{:else}
<s>{q.subject}</s>
{/if}
</div>
{#if !q.closed}
<button
type="button"
class="btn btn-sm btn-info"
disabled={thumbInProgress !== null}
on:click|preventDefault={() => thumbUp(q.id)}
>
{#if thumbInProgress == q.id}
<Spinner size="sm" />
{:else}
<Icon name="hand-thumbs-up-fill" />
{/if}
</button>
<div class="list-group {className}" class:list-group-flush={true}>
{#if queries.length}
{#each queries as q (q.id)}
<button
type="button"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
class:active={query_selected && q.id == query_selected}
aria-current="true"
on:click={() => goto(theme?`themes/${theme.id}/${exercice.id}/${q.id}`:`exercices/${exercice.id}/${q.id}`)}
>
<div class="text-truncate">
<BadgeState state={q.state} />
{#if !q.solved}
<strong>{q.subject}</strong>
{:else if !q.closed}
{q.subject}
{:else}
<s>{q.subject}</s>
{/if}
</button>
{/each}
{:else}
<div class="fw-bold text-center">
Aucune requête enregistrée
</div>
{/if}
</div>
{/await}
</div>
{#if !q.closed}
<button
type="button"
class="btn btn-sm btn-info"
disabled={thumbInProgress !== null}
on:click|preventDefault={() => thumbUp(q.id)}
>
{#if thumbInProgress == q.id}
<Spinner size="sm" />
{:else}
<Icon name="hand-thumbs-up-fill" />
{/if}
</button>
{/if}
</button>
{/each}
{:else}
<div class="fw-bold text-center">
Aucune requête enregistrée
</div>
{/if}
</div>

View File

@ -1,5 +1,5 @@
<script>
import { createEventDispatcher } from 'svelte';
import { invalidate, goto } from '$app/navigation';
import {
Button,
@ -9,8 +9,7 @@
import { QAQuery, QAStates } from '$lib/qa';
import { ToastsStore } from '$lib/stores/toasts';
const dispatch = createEventDispatcher();
export let theme = null;
export let exercice = {};
let newQuery = new QAQuery();
@ -25,10 +24,11 @@
newQuery.content = "Difficulté : " + newQuery.difficulty + "\n" + newQuery.content;
if (newQuery.timecount)
newQuery.content = "Temps passé : " + newQuery.timecount + "\n" + newQuery.content;
newQuery.save().then((res) => {
dispatch("new-query");
newQuery.save().then(async (res) => {
newQuery = new QAQuery();
creationInProgress = false;
await invalidate(`api/exercices/${exercice.id}/qa`);
goto(theme?`themes/${theme.id}/${exercice.id}/${res.id}`:`exercices/${exercice.id}/${res.id}`)
}).catch((err) => {
creationInProgress = false;
newQuery.content = myContent;

View File

@ -0,0 +1,13 @@
import { getExercice } from '$lib/exercices';
import { getExerciceQA } from '$lib/qa.js';
/** @type {import('./$types').PageLoad} */
export async function load({ depends, params }) {
const exercice = getExercice(params.eid)
depends(`api/exercices/${params.eid}`);
const qaitems = getExerciceQA(params.eid);
depends(`api/exercices/${params.eid}/qa`);
return { exercice, qaitems };
}

View File

@ -0,0 +1,20 @@
<script>
import { page } from '$app/stores';
import ExerciceLayout from '$lib/components/ExerciceLayout.svelte';
import { themes } from '$lib/stores/themes';
if ($themes.length == 0) {
themes.refresh();
}
export let data;
</script>
<ExerciceLayout
exercice={data.exercice}
qaitems={data.qaitems}
query_selected={$page.params.qid}
>
<slot></slot>
</ExerciceLayout>

View File

@ -1,26 +1,34 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
Button,
Col,
Container,
Icon,
Spinner,
Row,
Table,
} from 'sveltestrap';
import { getExercice } from '$lib/exercices';
import ExerciceQA from '$lib/components/ExerciceQA.svelte';
import QANewItem from '$lib/components/QANewItem.svelte';
let exerciceP = getExercice($page.params.eid);
export let data;
</script>
{#await exerciceP}
<Container class="mt-2 mb-5">
<div class="d-flex justify-content-center">
<Spinner size="lg" />
</div>
</Container>
{:then exercice}
<ExerciceQA {exercice} />
{/await}
<Row class="mb-3">
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html data.exercice.statement.replace("$FILES$", "../files")}
</div>
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html data.exercice.overview.replace("$FILES$", "../files")}
</div>
</Row>
<QANewItem
theme={data.theme}
exercice={data.exercice}
/>

View File

@ -0,0 +1,21 @@
import { error } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
const { exercice, qaitems } = await parent();
let query_selected = null;
for (const qaitem of qaitems) {
if (qaitem.id == params.qid) {
query_selected = qaitem;
}
}
if (!query_selected) {
throw error(404, {
message: 'Not found'
});
}
return { exercice, qaitems, query_selected };
}

View File

@ -0,0 +1,10 @@
<script>
import QAItem from '$lib/components/QAItem.svelte';
export let data;
</script>
<QAItem
exercice={data.exercice}
query={data.query_selected}
/>

View File

@ -1 +0,0 @@
<slot></slot>

View File

@ -0,0 +1,9 @@
import { getTheme } from '$lib/themes';
/** @type {import('./$types').PageLoad} */
export async function load({ depends, params }) {
const theme = getTheme(params.tid)
depends(`api/themes/${params.tid}`);
return { theme };
}

View File

@ -10,9 +10,9 @@
Spinner,
} from 'sveltestrap';
import { getTheme } from '$lib/themes';
import { fieldsExercices, getThemedExercices } from '$lib/exercices';
export let data;
let query = "";
function show(id) {
@ -21,67 +21,61 @@
</script>
<Container class="mt-2 mb-5">
{#await getTheme($page.params.tid)}
<div class="d-flex justify-content-center">
<Spinner size="lg" />
</div>
{:then theme}
<div class="d-flex align-items-end">
<Button
class="align-self-center"
color="link"
on:click={() => goto('themes/')}
>
<Icon name="chevron-left" />
</Button>
<h2>
{theme.name}
</h2>
<small class="m-2 mb-3 text-muted text-truncate">{@html theme.authors}</small>
</div>
<div class="d-flex align-items-end">
<Button
class="align-self-center"
color="link"
on:click={() => goto('themes/')}
>
<Icon name="chevron-left" />
</Button>
<h2>
{data.theme.name}
</h2>
<small class="m-2 mb-3 text-muted text-truncate">{@html data.theme.authors}</small>
</div>
{#if theme.intro}
<Container class="text-muted" style="overflow-y: auto; max-height: 34vh">
{@html theme.intro.replace("$FILES$", "../files")}
</Container>
{/if}
{#if data.theme.intro}
<Container class="text-muted" style="overflow-y: auto; max-height: 34vh">
{@html data.theme.intro.replace("$FILES$", "../files")}
</Container>
{/if}
{#await getThemedExercices($page.params.tid)}
{:then exercices}
<h3 class="mt-2">
Défis ({exercices.length})
</h3>
{#await getThemedExercices($page.params.tid)}
{:then exercices}
<h3 class="mt-2">
Défis ({exercices.length})
</h3>
<p>
<input type="search" class="form-control form-control-sm" placeholder="Search" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
{#each fieldsExercices as field}
<th>
{field}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each exercices as exercice (exercice.id)}
{#if exercice.title.indexOf(query) >= 0}
<tr on:click={() => show(exercice.id)}>
{#each fieldsExercices as field}
<td>
{@html exercice[field]}
{#if field == "title" && exercice.wip}
<Icon name="cone-striped" />
{/if}
</td>
{/each}
</tr>
{/if}
<p>
<input type="search" class="form-control form-control-sm" placeholder="Search" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
{#each fieldsExercices as field}
<th>
{field}
</th>
{/each}
</tbody>
</Table>
{/await}
</tr>
</thead>
<tbody>
{#each exercices as exercice (exercice.id)}
{#if exercice.title.indexOf(query) >= 0}
<tr on:click={() => show(exercice.id)}>
{#each fieldsExercices as field}
<td>
{@html exercice[field]}
{#if field == "title" && exercice.wip}
<Icon name="cone-striped" />
{/if}
</td>
{/each}
</tr>
{/if}
{/each}
</tbody>
</Table>
{/await}
</Container>

View File

@ -0,0 +1,15 @@
import { getExercice } from '$lib/exercices';
import { getExerciceQA } from '$lib/qa.js';
/** @type {import('./$types').PageLoad} */
export async function load({ depends, params, parent }) {
const { theme } = await parent();
const exercice = getExercice(params.eid)
depends(`api/exercices/${params.eid}`);
const qaitems = getExerciceQA(params.eid);
depends(`api/exercices/${params.eid}/qa`);
return { exercice, qaitems, theme };
}

View File

@ -0,0 +1,21 @@
<script>
import { page } from '$app/stores';
import ExerciceLayout from '$lib/components/ExerciceLayout.svelte';
import { themes } from '$lib/stores/themes';
if ($themes.length == 0) {
themes.refresh();
}
export let data;
</script>
<ExerciceLayout
exercice={data.exercice}
qaitems={data.qaitems}
query_selected={$page.params.qid}
theme={data.theme}
>
<slot></slot>
</ExerciceLayout>

View File

@ -1,26 +1,34 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
Button,
Col,
Container,
Icon,
Spinner,
Row,
Table,
} from 'sveltestrap';
import { getThemedExercice } from '$lib/exercices';
import ExerciceQA from '$lib/components/ExerciceQA.svelte';
import QANewItem from '$lib/components/QANewItem.svelte';
let exerciceP = getThemedExercice($page.params.tid, $page.params.eid);
export let data;
</script>
{#await exerciceP}
<Container class="mt-2 mb-5">
<div class="d-flex justify-content-center">
<Spinner size="lg" />
</div>
</Container>
{:then exercice}
<ExerciceQA theme_id={$page.params.tid} {exercice} />
{/await}
<Row class="mb-3">
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html data.exercice.statement.replace("$FILES$", "../files")}
</div>
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html data.exercice.overview.replace("$FILES$", "../files")}
</div>
</Row>
<QANewItem
theme={data.theme}
exercice={data.exercice}
/>

View File

@ -0,0 +1,21 @@
import { error } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export async function load({ params, parent }) {
const { exercice, qaitems, theme } = await parent();
let query_selected = null;
for (const qaitem of qaitems) {
if (qaitem.id == params.qid) {
query_selected = qaitem;
}
}
if (!query_selected) {
throw error(404, {
message: 'Not found'
});
}
return { exercice, qaitems, query_selected, theme };
}

View File

@ -0,0 +1,11 @@
<script>
import QAItem from '$lib/components/QAItem.svelte';
export let data;
</script>
<QAItem
exercice={data.exercice}
query={data.query_selected}
theme={data.theme}
/>