qa: Improve design
This commit is contained in:
parent
13588fc634
commit
0e19b59452
19 changed files with 493 additions and 247 deletions
11
qa/ui/src/app.css
Normal file
11
qa/ui/src/app.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.level .level-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.level .level-item .heading {
|
||||||
|
font-variant: small-caps;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.level .level-item .value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr" class="d-flex flex-column mh-100 h-100">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<meta name="robots" content="none">
|
<meta name="robots" content="none">
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="flex-fill d-flex flex-column">
|
||||||
%sveltekit.body%
|
%sveltekit.body%
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
41
qa/ui/src/lib/components/BadgeState.svelte
Normal file
41
qa/ui/src/lib/components/BadgeState.svelte
Normal 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>
|
|
@ -3,12 +3,15 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Col,
|
||||||
Container,
|
Container,
|
||||||
Icon,
|
Icon,
|
||||||
|
Row,
|
||||||
Table,
|
Table,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
import QAItems from '$lib/components/QAItems.svelte';
|
import QAItems from '$lib/components/QAItems.svelte';
|
||||||
|
import QAItem from '$lib/components/QAItem.svelte';
|
||||||
import QANewItem from '$lib/components/QANewItem.svelte';
|
import QANewItem from '$lib/components/QANewItem.svelte';
|
||||||
import { themes, themesIdx } from '$lib/stores/themes';
|
import { themes, themesIdx } from '$lib/stores/themes';
|
||||||
|
|
||||||
|
@ -16,49 +19,87 @@
|
||||||
themes.refresh();
|
themes.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let theme_id = null;
|
||||||
export let exercice = {};
|
export let exercice = {};
|
||||||
|
let query_selected = null;
|
||||||
let countCreation = 0;
|
let countCreation = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2>
|
<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}
|
{exercice.title}
|
||||||
|
{#if exercice.wip}
|
||||||
|
<Icon name="cone-striped" />
|
||||||
|
{/if}
|
||||||
{#if $themes.length && $themesIdx[exercice.id_theme]}
|
{#if $themes.length && $themesIdx[exercice.id_theme]}
|
||||||
<small>
|
<small>
|
||||||
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
|
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
|
||||||
</small>
|
</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>
|
<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}
|
{/if}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="row mb-3">
|
<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
|
<div
|
||||||
class="col-md-6"
|
class="col-md-6"
|
||||||
style="overflow-y: auto; max-height: 50vh;"
|
style="overflow-y: auto; max-height: 40vh;"
|
||||||
>
|
>
|
||||||
{@html exercice.statement}
|
{@html exercice.statement}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="col-md-6"
|
class="col-md-6"
|
||||||
style="overflow-y: auto; max-height: 50vh;"
|
style="overflow-y: auto; max-height: 40vh;"
|
||||||
>
|
>
|
||||||
{@html exercice.overview}
|
{@html exercice.overview}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Row>
|
||||||
|
|
||||||
<div class="mb-5">
|
|
||||||
<QANewItem
|
<QANewItem
|
||||||
{exercice}
|
{exercice}
|
||||||
on:new-query={() => countCreation++}
|
on:new-query={() => countCreation++}
|
||||||
/>
|
/>
|
||||||
{#key countCreation}
|
{/if}
|
||||||
<QAItems
|
</Col>
|
||||||
{exercice}
|
</Row>
|
||||||
/>
|
</Container>
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -33,9 +33,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar color="dark" dark expand="md">
|
<Navbar color="dark" dark expand="xs">
|
||||||
<NavbarBrand href=".">
|
<NavbarBrand href=".">
|
||||||
<img src="../img/fic.png" alt="FIC">
|
<img src="../img/fic.png" alt="FIC" height="26">
|
||||||
QA
|
QA
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
<Nav navbar>
|
<Nav navbar>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
active={activemenu === ''}
|
active={activemenu === ''}
|
||||||
>
|
>
|
||||||
<Icon name="house-door" />
|
<Icon name="house-door" />
|
||||||
Accueil
|
<span class="d-none d-md-inline">Accueil</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
active={activemenu === 'themes'}
|
active={activemenu === 'themes'}
|
||||||
>
|
>
|
||||||
<Icon name="box-seam" />
|
<Icon name="box-seam" />
|
||||||
Scénarios
|
<span class="d-none d-md-inline">Scénarios</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
active={activemenu === 'exercices'}
|
active={activemenu === 'exercices'}
|
||||||
>
|
>
|
||||||
<Icon name="bar-chart-steps" />
|
<Icon name="bar-chart-steps" />
|
||||||
Étapes
|
<span class="d-none d-md-inline">Étapes</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
active={activemenu === 'teams'}
|
active={activemenu === 'teams'}
|
||||||
>
|
>
|
||||||
<Icon name="people" />
|
<Icon name="people" />
|
||||||
Équipes
|
<span class="d-none d-md-inline">Équipes</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
|
@ -81,12 +81,12 @@
|
||||||
active={activemenu === 'repositories'}
|
active={activemenu === 'repositories'}
|
||||||
>
|
>
|
||||||
<Icon name="archive" />
|
<Icon name="archive" />
|
||||||
Dépôts
|
<span class="d-none d-md-inline">Dépôts</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav class="ms-auto text-light" navbar>
|
<Nav class="ms-auto text-light" navbar>
|
||||||
<NavItem class="ms-2">
|
<NavItem class="ms-2 text-truncate">
|
||||||
v{$version.version}
|
v{$version.version}
|
||||||
{#if $auth}– Logged as {$auth.name} (team #{$auth.id_team}){/if}
|
{#if $auth}– Logged as {$auth.name} (team #{$auth.id_team}){/if}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Icon,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
|
@ -45,6 +46,9 @@
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<h3>Vos étapes</h3>
|
<h3>Vos étapes</h3>
|
||||||
{#await my_exercicesP}
|
{#await my_exercicesP}
|
||||||
|
<div class="text-center">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
{:then}
|
{:then}
|
||||||
<table class="table table-stripped table-hover">
|
<table class="table table-stripped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Icon,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
|
@ -30,8 +31,14 @@
|
||||||
<div class={className}>
|
<div class={className}>
|
||||||
<h3>Étapes à tester et valider</h3>
|
<h3>Étapes à tester et valider</h3>
|
||||||
{#await todos.refresh()}
|
{#await todos.refresh()}
|
||||||
|
<div class="text-center">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
{:then}
|
{:then}
|
||||||
{#await exo_doneP}
|
{#await exo_doneP}
|
||||||
|
<div class="text-center">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
{:then exo_done}
|
{:then exo_done}
|
||||||
<table class="table table-stripped table-hover">
|
<table class="table table-stripped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -2,24 +2,63 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
Col,
|
||||||
Icon,
|
Icon,
|
||||||
|
Row,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
|
import BadgeState from '$lib/components/BadgeState.svelte';
|
||||||
import DateFormat from '$lib/components/DateFormat.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 { auth } from '$lib/stores/auth';
|
||||||
|
import { viewIdx } from '$lib/stores/todo';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let query_selected;
|
export let query_selected;
|
||||||
|
|
||||||
let query_commentsP;
|
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 newComment = new QAComment();
|
||||||
let submissionInProgress = false;
|
let submissionInProgress = false;
|
||||||
|
@ -65,15 +104,37 @@
|
||||||
dispatch("update-queries");
|
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>
|
</script>
|
||||||
|
|
||||||
|
{#if query_selected}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h4 class="card-title mb-0">{query_selected.subject}</h4>
|
<h4 class="card-title fw-bold mb-0">{query_selected.subject}</h4>
|
||||||
<div>
|
<div>
|
||||||
|
{#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 $auth && $auth.id_team == query_selected.id_team}
|
||||||
{#if query_selected.solved && !query_selected.closed}
|
{#if query_selected.solved && !query_selected.closed && (query_selected.subject != "RAS" || query_selected.state != "ok")}
|
||||||
<Button on:click={closeQA} color="success">
|
<Button on:click={closeQA} color="success">
|
||||||
<Icon name="check" />
|
<Icon name="check" />
|
||||||
Valider la résolution
|
Valider la résolution
|
||||||
|
@ -83,101 +144,142 @@
|
||||||
Réouvrir
|
Réouvrir
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !query_selected.solved}
|
{#if (!query_selected.solved && !has_comments) || (query_selected.subject == "RAS" && query_selected.state == "ok" && !has_comments)}
|
||||||
<Button on:click={deleteQA} color="danger">
|
<Button on:click={deleteQA} color="danger">
|
||||||
<Icon name="trash-fill" />
|
<Icon name="trash-fill" />
|
||||||
Supprimer
|
Supprimer
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if $auth && !query_selected.solved}
|
|
||||||
<Button on:click={solveQA} color="success">
|
|
||||||
<Icon name="check" />
|
|
||||||
Marquer comme résolu
|
|
||||||
</Button>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div class="row">
|
<Row class="level" cols={5}>
|
||||||
<dl class="col row">
|
<Col>
|
||||||
<dt class="col-3">Qui ?</dt>
|
<div class="level-item">
|
||||||
<dd class="col-9">{query_selected.user} (team #{query_selected.id_team})</dd>
|
<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>
|
||||||
|
|
||||||
<dt class="col-3">État</dt>
|
<Col>
|
||||||
<dd class="col-9">{query_selected.state}</dd>
|
<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>
|
||||||
|
|
||||||
<dt class="col-3">Date de création</dt>
|
<Col>
|
||||||
<dd class="col-9">
|
<div class="level-item">
|
||||||
|
<div class="heading">
|
||||||
|
Date de création
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
|
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
|
||||||
</dd>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<dt class="col-3">Date de résolution</dt>
|
<Col>
|
||||||
<dd class="col-9">
|
<div class="level-item">
|
||||||
|
<div class="heading">
|
||||||
|
Date de résolution
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
{#if query_selected.solved}
|
{#if query_selected.solved}
|
||||||
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
|
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</dd>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<dt class="col-3">Date de clôture</dt>
|
<Col>
|
||||||
<dd class="col-9">
|
<div class="level-item">
|
||||||
|
<div class="heading">
|
||||||
|
Date de clôture
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
{#if query_selected.closed}
|
{#if query_selected.closed}
|
||||||
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
|
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</dd>
|
</div>
|
||||||
</dl>
|
</div>
|
||||||
<div class="col-auto">
|
</Col>
|
||||||
{#if $auth && $auth.id_team == query_selected.id_team}
|
</Row>
|
||||||
<Button on:click={updateQA} color="primary">
|
<hr>
|
||||||
<Icon name="upload" />
|
{#await query_commentsP then query_comments}
|
||||||
Mettre à jour
|
{#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>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
<em>
|
||||||
|
Par <strong>{comment.user}</strong>, le <DateFormat date={comment.date} dateStyle="medium" timeStyle="short" />
|
||||||
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Alert>
|
||||||
{#await query_commentsP}
|
{/if}
|
||||||
<div class="d-flex">
|
|
||||||
<Spinner />
|
|
||||||
<div>
|
|
||||||
Chargement des commentaires en cours…
|
|
||||||
</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 : {comment.content}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
|
||||||
{/await}
|
{/await}
|
||||||
<form on:submit|preventDefault={addComment}>
|
<form on:submit|preventDefault={addComment}>
|
||||||
<label for="newComment">Répondre :</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Ajouter un commentaire"
|
placeholder="Ajouter votre commentaire"
|
||||||
rows="2"
|
rows="2"
|
||||||
id="newComment"
|
id="newComment"
|
||||||
bind:value={newComment.content}
|
bind:value={newComment.content}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
{#if newComment.content && newComment.content.length > 0}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="mt-1 float-right"
|
class="mt-1 float-right"
|
||||||
disabled={!newComment.content || newComment.content.length == 0 || submissionInProgress}
|
disabled={submissionInProgress}
|
||||||
>
|
>
|
||||||
{#if submissionInProgress}
|
{#if submissionInProgress}
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
{/if}
|
{/if}
|
||||||
Ajouter le commentaire
|
Ajouter le commentaire
|
||||||
</Button>
|
</Button>
|
||||||
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
import { getExerciceQA, QAComment } from '$lib/qa.js';
|
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 exercice = { };
|
||||||
export let query_selected = null;
|
export let query_selected = null;
|
||||||
|
|
||||||
const fields = [ "state", "subject" ];
|
|
||||||
|
|
||||||
let queriesP;
|
let queriesP;
|
||||||
$: queriesP = getExerciceQA(exercice.id);
|
$: queriesP = getExerciceQA(exercice.id);
|
||||||
|
|
||||||
|
@ -34,62 +35,46 @@
|
||||||
|
|
||||||
{#await queriesP}
|
{#await queriesP}
|
||||||
{:then queries}
|
{:then queries}
|
||||||
<table
|
<div class="list-group {className}" class:list-group-flush={true}>
|
||||||
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>
|
|
||||||
{#if queries.length}
|
{#if queries.length}
|
||||||
<tbody>
|
|
||||||
{#each queries as q (q.id)}
|
{#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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-light"
|
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}
|
disabled={thumbInProgress !== null}
|
||||||
on:click|preventDefault={() => thumbUp(q.id)}
|
on:click|preventDefault={() => thumbUp(q.id)}
|
||||||
>
|
>
|
||||||
{#if thumbInProgress == q.id}
|
{#if thumbInProgress == q.id}
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon name="hand-thumbs-up" />
|
<Icon name="hand-thumbs-up-fill" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
|
||||||
{:else}
|
{:else}
|
||||||
<tbody>
|
<div class="fw-bold text-center">
|
||||||
<tr>
|
|
||||||
<td colspan={fields.length+1} class="font-weight-bold text-info text-center">
|
|
||||||
Aucune requête enregistrée
|
Aucune requête enregistrée
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
{/if}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{#if query_selected}
|
|
||||||
<QAItem
|
|
||||||
bind:query_selected={query_selected}
|
|
||||||
on:update-queries={updateQueries}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -91,7 +91,7 @@ export class QAComment {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'Accept': 'application/json'}
|
headers: {'Accept': 'application/json'}
|
||||||
});
|
});
|
||||||
if (res.status == 200) {
|
if (res.status < 300) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
|
|
|
@ -37,3 +37,19 @@ export const exercicesIdx = derived(
|
||||||
return exercices_idx;
|
return exercices_idx;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const exercicesByTheme = derived(
|
||||||
|
exercices,
|
||||||
|
$exercices => {
|
||||||
|
const exercices_idx = { };
|
||||||
|
|
||||||
|
for (const e of $exercices) {
|
||||||
|
if (!exercices_idx[e.id_theme]) {
|
||||||
|
exercices_idx[e.id_theme] = []
|
||||||
|
}
|
||||||
|
exercices_idx[e.id_theme].push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exercices_idx;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { writable } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
|
|
||||||
import { getQAWork } from '$lib/todo'
|
import { getQAView, getQAWork } from '$lib/todo';
|
||||||
|
|
||||||
function createTodosStore() {
|
function createTodosStore() {
|
||||||
const { subscribe, set, update } = writable([]);
|
const { subscribe, set, update } = writable([]);
|
||||||
|
@ -24,3 +24,39 @@ function createTodosStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const todos = createTodosStore();
|
export const todos = createTodosStore();
|
||||||
|
|
||||||
|
function createViewStore() {
|
||||||
|
const { subscribe, set, update } = writable([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
|
||||||
|
set: (v) => {
|
||||||
|
update((m) => Object.assign(m, v));
|
||||||
|
},
|
||||||
|
|
||||||
|
update,
|
||||||
|
|
||||||
|
refresh: async () => {
|
||||||
|
const list = await getQAView();
|
||||||
|
update((m) => list);
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const view = createViewStore();
|
||||||
|
|
||||||
|
export const viewIdx = derived(
|
||||||
|
view,
|
||||||
|
$view => {
|
||||||
|
const idx = { };
|
||||||
|
|
||||||
|
for (const v of $view) {
|
||||||
|
idx[v.id_exercice] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Styles,
|
Styles,
|
||||||
|
@ -6,9 +8,13 @@
|
||||||
|
|
||||||
import Header from '$lib/components/Header.svelte';
|
import Header from '$lib/components/Header.svelte';
|
||||||
import { version } from '$lib/stores/auth';
|
import { version } from '$lib/stores/auth';
|
||||||
|
import { view } from '$lib/stores/todo';
|
||||||
|
|
||||||
version.refresh();
|
version.refresh();
|
||||||
setInterval(version.refresh, 30000);
|
setInterval(version.refresh, 30000);
|
||||||
|
|
||||||
|
view.refresh();
|
||||||
|
setInterval(view.refresh, 60000);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Col,
|
||||||
Container,
|
Container,
|
||||||
Row,
|
Row,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
@ -32,7 +33,11 @@
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Row>
|
<Row>
|
||||||
<MyTodo class="col-6" />
|
<Col>
|
||||||
<MyExercices class="col-6" />
|
<MyTodo />
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<MyExercices />
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
{#each fieldsExercices as field}
|
{#each fieldsExercices as field}
|
||||||
<td>
|
<td>
|
||||||
{@html exercice[field]}
|
{@html exercice[field]}
|
||||||
|
{#if field == "title" && exercice.wip}
|
||||||
|
<Icon name="cone-striped" />
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -15,19 +15,12 @@
|
||||||
let exerciceP = getExercice($page.params.eid);
|
let exerciceP = getExercice($page.params.eid);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container class="mt-2 mb-5">
|
|
||||||
{#await exerciceP}
|
{#await exerciceP}
|
||||||
|
<Container class="mt-2 mb-5">
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
{:then exercice}
|
{:then exercice}
|
||||||
<Button
|
|
||||||
class="float-start"
|
|
||||||
color="link"
|
|
||||||
on:click={() => goto('exercices')}
|
|
||||||
>
|
|
||||||
<Icon name="chevron-left" />
|
|
||||||
</Button>
|
|
||||||
<ExerciceQA {exercice} />
|
<ExerciceQA {exercice} />
|
||||||
{/await}
|
{/await}
|
||||||
</Container>
|
|
||||||
|
|
|
@ -41,9 +41,9 @@
|
||||||
{#if theme.name.indexOf(query) >= 0 || theme.authors.indexOf(query) >= 0 || theme.intro.indexOf(query) >= 0}
|
{#if theme.name.indexOf(query) >= 0 || theme.authors.indexOf(query) >= 0 || theme.intro.indexOf(query) >= 0}
|
||||||
<tr on:click={() => show(theme.id)}>
|
<tr on:click={() => show(theme.id)}>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<td>
|
<td class:text-end={field == "image"}>
|
||||||
{#if field == "image"}
|
{#if field == "image"}
|
||||||
<img src={"../files" + theme[field]} alt="Image du scénario">
|
<img src={"../files" + theme[field]} alt="Image du scénario" height="120">
|
||||||
{:else}
|
{:else}
|
||||||
{@html theme[field]}
|
{@html theme[field]}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -40,13 +40,13 @@
|
||||||
<small class="m-2 mb-3 text-muted text-truncate">{@html theme.authors}</small>
|
<small class="m-2 mb-3 text-muted text-truncate">{@html theme.authors}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container class="text-muted">
|
<Container class="text-muted" style="overflow-y: auto; max-height: 34vh">
|
||||||
{@html theme.intro}
|
{@html theme.intro}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{#await getThemedExercices($page.params.tid)}
|
{#await getThemedExercices($page.params.tid)}
|
||||||
{:then exercices}
|
{:then exercices}
|
||||||
<h3>
|
<h3 class="mt-2">
|
||||||
Défis ({exercices.length})
|
Défis ({exercices.length})
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
@ -70,6 +70,9 @@
|
||||||
{#each fieldsExercices as field}
|
{#each fieldsExercices as field}
|
||||||
<td>
|
<td>
|
||||||
{@html exercice[field]}
|
{@html exercice[field]}
|
||||||
|
{#if field == "title" && exercice.wip}
|
||||||
|
<Icon name="cone-striped" />
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -15,19 +15,12 @@
|
||||||
let exerciceP = getThemedExercice($page.params.tid, $page.params.eid);
|
let exerciceP = getThemedExercice($page.params.tid, $page.params.eid);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container class="mt-2 mb-5">
|
|
||||||
{#await exerciceP}
|
{#await exerciceP}
|
||||||
|
<Container class="mt-2 mb-5">
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
<Spinner size="lg" />
|
<Spinner size="lg" />
|
||||||
</div>
|
</div>
|
||||||
{:then exercice}
|
|
||||||
<Button
|
|
||||||
class="float-start"
|
|
||||||
color="link"
|
|
||||||
on:click={() => goto('themes/' + $page.params.tid)}
|
|
||||||
>
|
|
||||||
<Icon name="chevron-left" />
|
|
||||||
</Button>
|
|
||||||
<ExerciceQA {exercice} />
|
|
||||||
{/await}
|
|
||||||
</Container>
|
</Container>
|
||||||
|
{:then exercice}
|
||||||
|
<ExerciceQA theme_id={$page.params.tid} {exercice} />
|
||||||
|
{/await}
|
||||||
|
|
Reference in a new issue