qa: Improve design

This commit is contained in:
nemunaire 2022-11-07 03:47:48 +01:00
commit 0e19b59452
19 changed files with 487 additions and 241 deletions

View file

@ -0,0 +1,41 @@
<script>
import {
Badge,
} from 'sveltestrap';
export { className as class };
let className = '';
export let state = "ok";
let color = "";
$: {
switch (state) {
case "ok":
color = "success";
break;
case "issue-flag":
case "issue-statement":
case "issue-mcq":
case "issue-hint":
case "issue-file":
case "issue":
color = "danger";
break;
case "orthograph":
case "suggest":
color = "info";
break;
case "too-hard":
case "too-easy":
color = "warning";
break;
default:
color = "secondary";
break;
}
}
</script>
<Badge {color} class={className}>
{state}
</Badge>

View file

@ -3,12 +3,15 @@
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';
@ -16,49 +19,87 @@
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>
{#if $themesIdx[exercice.id_theme].exercices && $themesIdx[exercice.id_theme].exercices[exercice.id]}
<div class="btn-group" role="group">
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].previous}" title="Exercice précédent" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].previous} class="btn btn-sm btn-light"><Icon name="chevron-left" /></a>
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].next}" title="Exercice suivant" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].next} class="btn btn-sm btn-light"><Icon name="chevron-right" /></a>
</div>
{/if}
<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>
<div class="row mb-3">
<div
class="col-md-6"
style="overflow-y: auto; max-height: 50vh;"
>
{@html exercice.statement}
</div>
<div
class="col-md-6"
style="overflow-y: auto; max-height: 50vh;"
>
{@html exercice.overview}
</div>
</div>
<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}
</div>
<div
class="col-md-6"
style="overflow-y: auto; max-height: 40vh;"
>
{@html exercice.overview}
</div>
</Row>
<div class="mb-5">
<QANewItem
{exercice}
on:new-query={() => countCreation++}
/>
{#key countCreation}
<QAItems
{exercice}
/>
{/key}
</div>
<QANewItem
{exercice}
on:new-query={() => countCreation++}
/>
{/if}
</Col>
</Row>
</Container>

View file

@ -33,9 +33,9 @@
}
</script>
<Navbar color="dark" dark expand="md">
<Navbar color="dark" dark expand="xs">
<NavbarBrand href=".">
<img src="../img/fic.png" alt="FIC">
<img src="../img/fic.png" alt="FIC" height="26">
QA
</NavbarBrand>
<Nav navbar>
@ -45,7 +45,7 @@
active={activemenu === ''}
>
<Icon name="house-door" />
Accueil
<span class="d-none d-md-inline">Accueil</span>
</NavLink>
</NavItem>
<NavItem>
@ -54,7 +54,7 @@
active={activemenu === 'themes'}
>
<Icon name="box-seam" />
Scénarios
<span class="d-none d-md-inline">Scénarios</span>
</NavLink>
</NavItem>
<NavItem>
@ -63,7 +63,7 @@
active={activemenu === 'exercices'}
>
<Icon name="bar-chart-steps" />
Étapes
<span class="d-none d-md-inline">Étapes</span>
</NavLink>
</NavItem>
<NavItem>
@ -72,7 +72,7 @@
active={activemenu === 'teams'}
>
<Icon name="people" />
Équipes
<span class="d-none d-md-inline">Équipes</span>
</NavLink>
</NavItem>
<NavItem>
@ -81,12 +81,12 @@
active={activemenu === 'repositories'}
>
<Icon name="archive" />
Dépôts
<span class="d-none d-md-inline">Dépôts</span>
</NavLink>
</NavItem>
</Nav>
<Nav class="ms-auto text-light" navbar>
<NavItem class="ms-2">
<NavItem class="ms-2 text-truncate">
v{$version.version}
{#if $auth}&ndash; Logged as {$auth.name} (team #{$auth.id_team}){/if}
</NavItem>

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import {
Icon,
Spinner,
} from 'sveltestrap';
@ -45,6 +46,9 @@
<div class={className}>
<h3>Vos étapes</h3>
{#await my_exercicesP}
<div class="text-center">
<Spinner size="lg" />
</div>
{:then}
<table class="table table-stripped table-hover">
<thead>

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import {
Icon,
Spinner,
} from 'sveltestrap';
@ -30,8 +31,14 @@
<div class={className}>
<h3>Étapes à tester et valider</h3>
{#await todos.refresh()}
<div class="text-center">
<Spinner size="lg" />
</div>
{:then}
{#await exo_doneP}
<div class="text-center">
<Spinner size="lg" />
</div>
{:then exo_done}
<table class="table table-stripped table-hover">
<thead>

View file

@ -2,24 +2,63 @@
import { createEventDispatcher } from 'svelte';
import {
Alert,
Button,
Card,
CardBody,
CardHeader,
Col,
Icon,
Row,
Spinner,
} from 'sveltestrap';
import BadgeState from '$lib/components/BadgeState.svelte';
import DateFormat from '$lib/components/DateFormat.svelte';
import { getQAComments, QAComment } from '$lib/qa.js';
import { getQAComments, QAComment } from '$lib/qa';
import { auth } from '$lib/stores/auth';
import { viewIdx } from '$lib/stores/todo';
const dispatch = createEventDispatcher();
export let query_selected;
let query_commentsP;
$: query_commentsP = getQAComments(query_selected.id);
let thumbs = [];
let thumb_me = [];
let has_comments = false;
$: updateComments(query_selected)
function updateComments(query_selected) {
if (query_selected) {
query_commentsP = getQAComments(query_selected.id);
query_commentsP.then((comments) => {
thumbs = [];
thumb_me = [];
has_comments = false;
for (const c of comments) {
has_comments = true;
if (c.content == "+1") {
let found = false;
for (const t of thumbs) {
if (t == c.user) {
found = true;
break;
}
}
if (!found) {
thumbs.push(c.user);
}
if (c.user == $auth.name) {
thumb_me.push(c);
}
}
}
})
}
}
let newComment = new QAComment();
let submissionInProgress = false;
@ -65,119 +104,182 @@
dispatch("update-queries");
})
}
function deleteMyThumbs() {
if (thumb_me.length) {
for (const c of thumb_me) {
c.delete(query_selected.id);
}
dispatch("update-queries");
}
}
function deleteComment(comment) {
comment.delete(query_selected.id).then(() => {
dispatch("update-queries");
})
}
</script>
<Card>
<CardHeader>
<div class="d-flex justify-content-between">
<h4 class="card-title mb-0">{query_selected.subject}</h4>
<div>
{#if $auth && $auth.id_team == query_selected.id_team}
{#if query_selected.solved && !query_selected.closed}
<Button on:click={closeQA} color="success">
<Icon name="check" />
Valider la résolution
</Button>
<Button on:click={reopenQA} color="danger">
<Icon name="x" />
Réouvrir
</Button>
{/if}
{#if !query_selected.solved}
<Button on:click={deleteQA} color="danger">
<Icon name="trash-fill" />
Supprimer
</Button>
{/if}
{:else if $auth && !query_selected.solved}
<Button on:click={solveQA} color="success">
<Icon name="check" />
Marquer comme résolu
</Button>
{/if}
</div>
</div>
</CardHeader>
<CardBody>
<div class="row">
<dl class="col row">
<dt class="col-3">Qui ?</dt>
<dd class="col-9">{query_selected.user} (team #{query_selected.id_team})</dd>
<dt class="col-3">État</dt>
<dd class="col-9">{query_selected.state}</dd>
<dt class="col-3">Date de création</dt>
<dd class="col-9">
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
</dd>
<dt class="col-3">Date de résolution</dt>
<dd class="col-9">
{#if query_selected.solved}
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
<dt class="col-3">Date de clôture</dt>
<dd class="col-9">
{#if query_selected.closed}
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
</dl>
<div class="col-auto">
{#if $auth && $auth.id_team == query_selected.id_team}
<Button on:click={updateQA} color="primary">
<Icon name="upload" />
Mettre à jour
</Button>
{/if}
</div>
</div>
{#await query_commentsP}
<div class="d-flex">
<Spinner />
{#if query_selected}
<Card>
<CardHeader>
<div class="d-flex justify-content-between align-items-center">
<h4 class="card-title fw-bold mb-0">{query_selected.subject}</h4>
<div>
Chargement des commentaires en cours&hellip;
{#if $auth && !query_selected.solved && $viewIdx[query_selected.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")}
<Button on:click={closeQA} color="success">
<Icon name="check" />
Valider la résolution
</Button>
<Button on:click={reopenQA} color="danger">
<Icon name="x" />
Réouvrir
</Button>
{/if}
{#if (!query_selected.solved && !has_comments) || (query_selected.subject == "RAS" && query_selected.state == "ok" && !has_comments)}
<Button on:click={deleteQA} color="danger">
<Icon name="trash-fill" />
Supprimer
</Button>
{/if}
{/if}
</div>
</div>
{:then query_comments}
<table class="table table-striped">
{#each query_comments as comment (comment.id)}
<tr>
<td style="white-space: pre-line">
Le <DateFormat date={comment.date} dateStyle="medium" timeStyle="short" />, <strong>{comment.user}</strong> a écrit&nbsp;: {comment.content}
</td>
</tr>
{/each}
</table>
{/await}
<form on:submit|preventDefault={addComment}>
<label for="newComment">Répondre :</label>
<textarea
class="form-control"
placeholder="Ajouter un commentaire"
rows="2"
id="newComment"
bind:value={newComment.content}
></textarea>
<Button
type="submit"
color="primary"
class="mt-1 float-right"
disabled={!newComment.content || newComment.content.length == 0 || submissionInProgress}
>
{#if submissionInProgress}
<Spinner size="sm" />
{/if}
Ajouter le commentaire
</Button>
</form>
</CardHeader>
<CardBody>
<Row class="level" cols={5}>
<Col>
<div class="level-item">
<div class="heading">
Qui ?
</div>
<div class="value">
{query_selected.user.split("@")[0]}
</div>
<div class="text-muted">
(team #{query_selected.id_team})
</div>
</div>
</Col>
</CardBody>
</Card>
<Col>
<div class="level-item">
<div class="heading">
État
</div>
<BadgeState class="value" state={query_selected.state} />
<div
class="text-muted"
title={thumbs.join(', ')}
on:click={deleteMyThumbs}
>
<Icon
name="hand-thumbs-up-fill"
class={thumb_me.length?"text-info":""}
style={thumb_me.length?"cursor: pointer;":""}
/>
{thumbs.length}
</div>
</div>
</Col>
<Col>
<div class="level-item">
<div class="heading">
Date de création
</div>
<div class="value">
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
</div>
</div>
</Col>
<Col>
<div class="level-item">
<div class="heading">
Date de résolution
</div>
<div class="value">
{#if query_selected.solved}
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</div>
</div>
</Col>
<Col>
<div class="level-item">
<div class="heading">
Date de clôture
</div>
<div class="value">
{#if query_selected.closed}
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</div>
</div>
</Col>
</Row>
<hr>
{#await query_commentsP then query_comments}
{#each query_comments as comment (comment.id)}
{#if comment.content != "+1"}
<Alert fade={false}>
<div style="white-space: pre-line">{comment.content}</div>
<div class="d-flex justify-content-end align-items-center">
{#if comment.user == $auth.name}
<Button
size="sm"
color="danger"
class="me-2"
on:click={() => deleteComment(comment)}
>
<Icon name="trash-fill" />
</Button>
{/if}
<em>
Par <strong>{comment.user}</strong>, le <DateFormat date={comment.date} dateStyle="medium" timeStyle="short" />
</em>
</div>
</Alert>
{/if}
{/each}
{/await}
<form on:submit|preventDefault={addComment}>
<textarea
class="form-control"
placeholder="Ajouter votre commentaire"
rows="2"
id="newComment"
bind:value={newComment.content}
></textarea>
{#if newComment.content && newComment.content.length > 0}
<Button
type="submit"
color="primary"
class="mt-1 float-right"
disabled={submissionInProgress}
>
{#if submissionInProgress}
<Spinner size="sm" />
{/if}
Ajouter le commentaire
</Button>
{/if}
</form>
</CardBody>
</Card>
{/if}

View file

@ -6,13 +6,14 @@
} from 'sveltestrap';
import { getExerciceQA, QAComment } from '$lib/qa.js';
import QAItem from '$lib/components/QAItem.svelte';
import BadgeState from '$lib/components/BadgeState.svelte';
export { className as class };
let className = '';
export let exercice = { };
export let query_selected = null;
const fields = [ "state", "subject" ];
let queriesP;
$: queriesP = getExerciceQA(exercice.id);
@ -34,62 +35,46 @@
{#await queriesP}
{:then queries}
<table
class="table table-bordered table-striped"
class:table-hover={queries.length}
class:table-sm={queries.length}
>
<thead class="thead-dark">
<tr>
{#each fields as field}
<th>
{ field }
</th>
{/each}
<th></th>
</tr>
</thead>
<div class="list-group {className}" class:list-group-flush={true}>
{#if queries.length}
<tbody>
{#each queries as q (q.id)}
<tr on:click={() => query_selected = q} class:bg-warning={query_selected && q.id == query_selected.id}>
{#each fields as field}
<td>
{@html q[field]}
</td>
{/each}
<td>
<button
type="button"
class="btn btn-sm btn-light"
disabled={thumbInProgress !== null}
on:click|preventDefault={() => thumbUp(q.id)}
>
{#if thumbInProgress == q.id}
<Spinner size="sm" />
{:else}
<Icon name="hand-thumbs-up" />
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
{:else}
<tbody>
<tr>
<td colspan={fields.length+1} class="font-weight-bold text-info text-center">
Aucune requête enregistrée
</td>
</tr>
</tbody>
{#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>
{/if}
</button>
{/each}
{:else}
<div class="fw-bold text-center">
Aucune requête enregistrée
</div>
{/if}
</table>
{#if query_selected}
<QAItem
bind:query_selected={query_selected}
on:update-queries={updateQueries}
/>
{/if}
</div>
{/await}