Merge branch 'develop' into dist/ubuntu/bionic/develop

This commit is contained in:
wpetit 2020-10-12 15:59:57 +02:00
commit 42f7ba3473
14 changed files with 254 additions and 148 deletions

View File

@ -45,7 +45,7 @@ export const App: FunctionComponent<AppProps> = () => {
<Route path="/unauthorized" exact component={UnauthorizedPage} /> <Route path="/unauthorized" exact component={UnauthorizedPage} />
<PrivateRoute path="/profile" exact component={ProfilePage} /> <PrivateRoute path="/profile" exact component={ProfilePage} />
<PrivateRoute path="/workgroups/:id" exact component={WorkgroupPage} /> <PrivateRoute path="/workgroups/:id" exact component={WorkgroupPage} />
<PrivateRoute path="/decisions/:id" exact component={DecisionSupportFilePage} /> <PrivateRoute path="/decisions/:id" component={DecisionSupportFilePage} />
<PrivateRoute path="/dashboard" exact component={DashboardPage} /> <PrivateRoute path="/dashboard" exact component={DashboardPage} />
<PrivateRoute path="/logout" exact component={LogoutPage} /> <PrivateRoute path="/logout" exact component={LogoutPage} />
<Route component={() => <Redirect to="/" />} /> <Route component={() => <Redirect to="/" />} />

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision'; import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision';
import { ItemPanel, TabDefinition, Item } from './ItemPanel'; import { ItemPanel, TabDefinition, Item } from '../ItemPanel';
import { useUserProfile } from '../../gql/queries/profile'; import { useUserProfile } from '../../gql/queries/profile';
import { inWorkgroup } from '../../types/workgroup'; import { inWorkgroup } from '../../types/workgroup';
import { useDecisionSupportFiles } from '../../gql/queries/dsf'; import { useDecisionSupportFiles } from '../../gql/queries/dsf';

View File

@ -2,7 +2,7 @@ import React, { } from 'react';
import { Workgroup, inWorkgroup } from '../../types/workgroup'; import { Workgroup, inWorkgroup } from '../../types/workgroup';
import { useWorkgroups } from '../../gql/queries/workgroups'; import { useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfile } from '../../gql/queries/profile'; import { useUserProfile } from '../../gql/queries/profile';
import { ItemPanel, Item } from './ItemPanel'; import { ItemPanel, Item } from '../ItemPanel';
export function WorkgroupsPanel() { export function WorkgroupsPanel() {
const { workgroups } = useWorkgroups(); const { workgroups } = useWorkgroups();

View File

@ -4,7 +4,7 @@ import { useWorkgroups } from "../gql/queries/workgroups";
import { useDecisionSupportFiles } from "../gql/queries/dsf"; import { useDecisionSupportFiles } from "../gql/queries/dsf";
export interface DecisioSupportFileLinkProps { export interface DecisioSupportFileLinkProps {
decisionSupportFileId: number decisionSupportFileId: number|string
} }
export const DecisionSupportFileLink: FunctionComponent<DecisioSupportFileLinkProps> = ({ decisionSupportFileId }) => { export const DecisionSupportFileLink: FunctionComponent<DecisioSupportFileLinkProps> = ({ decisionSupportFileId }) => {

View File

@ -49,90 +49,88 @@ export const ClarificationSection: FunctionComponent<ClarificationSectionProps>
return ( return (
<section> <section>
<div className="box"> <div className="field">
<div className="field"> <label className="label is-medium">Intitulé du dossier</label>
<label className="label is-medium">Intitulé du dossier</label> <div className="control">
<div className="control"> <input className="input is-medium" type="text" readOnly={readOnly} value={dsf.title} onChange={onTitleChange} />
<input className="input is-medium" type="text" readOnly={readOnly} value={dsf.title} onChange={onTitleChange} /> </div>
</div>
<div className="field">
<label className="label is-medium">Quelle décision devons nous prendre ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.objectives}
onChange={onSectionAttrChange.bind(null, 'objectives')}
placeholder="Décrire globalement les tenants et aboutissants de la décision à prendre."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.</p>
</div>
<div className="field">
<label className="label is-medium">Pourquoi devons nous prendre cette décision ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.motivations}
onChange={onSectionAttrChange.bind(null, 'motivations')}
placeholder="Décrire pourquoi il est important de prendre cette décision."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer si des obligations légales pèsent sur cette prise de décision.</p>
</div>
<div className="field">
<label className="label is-medium">Portée de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'scope')}
value={state.section.scope}>
<option></option>
<option value="individual">Individuelle</option>
<option value="identified-group">Groupe identifié</option>
<option value="collective">Collective</option>
</select>
</div> </div>
</div> </div>
<div className="field"> </div>
<label className="label is-medium">Quelle décision devons nous prendre ?</label> <div className="field">
<div className="control"> <label className="label is-medium">Nature de la décision</label>
<textarea className="textarea is-medium" <div className="control">
readOnly={readOnly} <div className="select is-medium">
value={state.section.objectives} <select
onChange={onSectionAttrChange.bind(null, 'objectives')} disabled={readOnly}
placeholder="Décrire globalement les tenants et aboutissants de la décision à prendre." onChange={onSectionAttrChange.bind(null, 'nature')}
rows={10}> value={state.section.nature}>
</textarea> <option></option>
</div> <option value="operational">Opérationnelle</option>
<p className="help is-info"><i className="fa fa-info-circle"></i> Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.</p> <option value="tactic">Tactique</option>
</div> <option value="strategic">Stratégique</option>
<div className="field"> </select>
<label className="label is-medium">Pourquoi devons nous prendre cette décision ?</label>
<div className="control">
<textarea className="textarea is-medium"
readOnly={readOnly}
value={state.section.motivations}
onChange={onSectionAttrChange.bind(null, 'motivations')}
placeholder="Décrire pourquoi il est important de prendre cette décision."
rows={10}>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer si des obligations légales pèsent sur cette prise de décision.</p>
</div>
<div className="field">
<label className="label is-medium">Portée de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'scope')}
value={state.section.scope}>
<option></option>
<option value="individual">Individuelle</option>
<option value="identified-group">Groupe identifié</option>
<option value="collective">Collective</option>
</select>
</div>
</div>
</div>
<div className="field">
<label className="label is-medium">Nature de la décision</label>
<div className="control">
<div className="select is-medium">
<select
disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'nature')}
value={state.section.nature}>
<option></option>
<option value="operational">Opérationnelle</option>
<option value="tactic">Tactique</option>
<option value="strategic">Stratégique</option>
</select>
</div>
</div> </div>
</div> </div>
</div>
<div className="columns"> <div className="columns">
<div className="column"> <div className="column">
<label className="checkbox"> <label className="checkbox">
<input type="checkbox" <input type="checkbox"
className="is-medium" className="is-medium"
disabled={readOnly} disabled={readOnly}
onChange={onSectionAttrChange.bind(null, 'hasDeadline')} onChange={onSectionAttrChange.bind(null, 'hasDeadline')}
checked={state.section.hasDeadline} /> checked={state.section.hasDeadline} />
<span className="ml-1 has-text-weight-bold is-size-5">Existe t'il une échéance particulière pour cette décision ?</span> <span className="ml-1 has-text-weight-bold is-size-5">Existe t'il une échéance particulière pour cette décision ?</span>
</label> </label>
<div className="field"> <div className="field">
<div className="control"> <div className="control">
<input disabled={!state.section.hasDeadline} <input disabled={!state.section.hasDeadline}
readOnly={readOnly} readOnly={readOnly}
value={state.section.deadline ? asDate(state.section.deadline).toISOString().substr(0, 10) : ''} value={state.section.deadline ? asDate(state.section.deadline).toISOString().substr(0, 10) : ''}
onChange={onDeadlineChange} onChange={onDeadlineChange}
type="date" className="input is-medium" /> type="date" className="input is-medium" />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -32,19 +32,17 @@ export const DecisionReportSection: FunctionComponent<DecisionReportSectionProps
return ( return (
<section> <section>
<div className="box"> <div className="field">
<div className="field"> <label className="label is-medium">Compte rendu du vote</label>
<label className="label is-medium">Compte rendu du vote</label> <div className="control">
<div className="control"> <textarea className="textarea is-medium"
<textarea className="textarea is-medium" readOnly={readOnly}
readOnly={readOnly} value={state.section.report}
value={state.section.report} onChange={onSectionAttrChange.bind(null, 'report')}
onChange={onSectionAttrChange.bind(null, 'report')} rows={20}>
rows={20}> </textarea>
</textarea>
</div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer le résultat du vote et les éléments de contexte liés à la prise de décision.</p>
</div> </div>
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer le résultat du vote et les éléments de contexte liés à la prise de décision.</p>
</div> </div>
</section> </section>
); );

View File

@ -11,6 +11,7 @@ import { OptionsSection } from './OptionsSection';
import { useIsAuthorized } from '../../gql/queries/authorization'; import { useIsAuthorized } from '../../gql/queries/authorization';
import { TimelinePanel } from './TimelinePanel'; import { TimelinePanel } from './TimelinePanel';
import { DecisionReportSection } from './DecisionReportSection'; import { DecisionReportSection } from './DecisionReportSection';
import { RoutedTabs } from '../RoutedTabs';
export interface DecisionSupportFilePageProps { export interface DecisionSupportFilePageProps {
@ -30,10 +31,8 @@ export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageP
const [ state, setState ] = useState({ const [ state, setState ] = useState({
dsf: newDecisionSupportFile(), dsf: newDecisionSupportFile(),
saved: true, saved: true,
selectedTabIndex: 0,
}); });
const { isAuthorized } = useIsAuthorized({ const { isAuthorized } = useIsAuthorized({
variables: { variables: {
action: 'update', action: 'update',
@ -48,9 +47,26 @@ export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageP
setState(state => ({ ...state, dsf: { ...state.dsf, ...dsf }})) setState(state => ({ ...state, dsf: { ...state.dsf, ...dsf }}))
}, [ decisionSupportFiles ]); }, [ decisionSupportFiles ]);
const selectTab = (tabIndex: number) => { const tabs = [
setState(state => ({ ...state, selectedTabIndex: tabIndex })); {
}; name: "Clarifier la proposition",
icon: "fas fa-pen",
route: '/info',
render: () => (<ClarificationSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
},
{
name: "Explorer les options",
icon: "fas fa-search",
route: '/options',
render: () => (<OptionsSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
},
{
name: "Prendre la décision",
icon: "fas fa-person-booth",
route: '/vote',
render: () => (<DecisionReportSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
}
];
const updateDSF = (dsf: DecisionSupportFile) => { const updateDSF = (dsf: DecisionSupportFile) => {
setState(state => { setState(state => {
@ -130,46 +146,9 @@ export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageP
</section> </section>
<div className="columns mt-3"> <div className="columns mt-3">
<div className="column is-8"> <div className="column is-8">
<div className="tabs is-medium is-toggle"> <div className="box">
<ul> <RoutedTabs baseRoute={`/decisions/${id}`} tabs={tabs} />
<li className={`has-background-white ${state.selectedTabIndex === 0 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 0)}>
<a>
<span className="icon is-small"><i className="fas fa-pen" aria-hidden="true"></i></span>
<span>Clarifier la proposition</span>
</a>
</li>
<li className={`has-background-white ${state.selectedTabIndex === 1 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 1)}>
<a>
<span className="icon is-small"><i className="fas fa-search" aria-hidden="true"></i></span>
<span>Explorer les options</span>
</a>
</li>
<li className={`has-background-white ${state.selectedTabIndex === 2 ? 'is-active': ''}`}
onClick={selectTab.bind(null, 2)}>
<a>
<span className="icon is-small"><i className="fas fa-person-booth" aria-hidden="true"></i></span>
<span>Prendre la décision</span>
</a>
</li>
</ul>
</div> </div>
{
state.selectedTabIndex === 0 ?
<ClarificationSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> :
null
}
{
state.selectedTabIndex === 1 ?
<OptionsSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> :
null
}
{
state.selectedTabIndex === 2 ?
<DecisionReportSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> :
null
}
</div> </div>
<div className="column is-4"> <div className="column is-4">
<MetadataPanel readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} /> <MetadataPanel readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />

View File

@ -75,8 +75,7 @@ export const OptionsSection: FunctionComponent<OptionsSectionProps> = ({ dsf, up
return ( return (
<section> <section>
<h4 id="options-section" className="is-size-4 title is-spaced"><a href="#options-section">Explorer les options</a></h4> <h4 id="options-section" className="is-size-4 title is-spaced">Explorer les options</h4>
<div className="box">
<div className="table-container"> <div className="table-container">
<table className={`table is-bordered is-striped is-hoverable is-fullwidth`}> <table className={`table is-bordered is-striped is-hoverable is-fullwidth`}>
<thead> <thead>
@ -154,7 +153,6 @@ export const OptionsSection: FunctionComponent<OptionsSectionProps> = ({ dsf, up
</tfoot> </tfoot>
</table> </table>
</div> </div>
</div>
</section> </section>
); );
}; };

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent, useState, useEffect } from "react"; import React, { FunctionComponent, useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { WithLoader } from "../WithLoader"; import { WithLoader } from "./WithLoader";
export interface Item { export interface Item {
id: string id: string

View File

@ -0,0 +1,79 @@
import React, { FunctionComponent, ReactNode, useEffect, useState } from 'react';
import { useHistory, useLocation, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
export interface Tab {
route: string
name: string
icon ?: string
render: (tab: Tab) => ReactNode
}
export interface RoutedTabsProps {
tabs: Tab[]
baseRoute?: string
defaultTabIndex?: number
}
export const RoutedTabs: FunctionComponent<RoutedTabsProps> = ({ tabs, baseRoute, defaultTabIndex }) => {
const history = useHistory();
const location = useLocation();
const tabRoute = (route: string): string => {
return `${baseRoute}${route}`;
};
const [ selectedTabIndex, setSelectedTabIndex ] = useState(defaultTabIndex || 0);
const expectedTab = tabs[selectedTabIndex];
const expectedTabRoute = tabRoute(expectedTab.route);
let matchExpectedTabRoute = useRouteMatch(expectedTabRoute);
useEffect(() => {
if (matchExpectedTabRoute) return;
const newTabIndex = tabs.findIndex(t => location.pathname === tabRoute(t.route));
if (newTabIndex !== -1) {
selectTab(newTabIndex);
return;
}
history.push(expectedTabRoute);
}, [matchExpectedTabRoute]);
const selectTab = (tabIndex: number) => {
setSelectedTabIndex(tabIndex);
const newTab = tabs[tabIndex];
history.push(tabRoute(newTab.route));
};
return (
<React.Fragment>
<div className="tabs is-medium is-boxed">
<ul>
{
tabs.map((t: Tab, i: number) => {
return (
<li key={`tab-${i}`} className={`has-background-white ${selectedTabIndex === i ? 'is-active': ''}`}
onClick={selectTab.bind(null, i)}>
<a>
{
t.icon ?
<span className="icon is-small"><i className={t.icon} aria-hidden="true"></i></span> :
null
}
<span>{t.name}</span>
</a>
</li>
);
})
}
</ul>
</div>
{ expectedTab.render(expectedTab) }
</React.Fragment>
);
}

View File

@ -0,0 +1,48 @@
import React, { FunctionComponent } from 'react';
import { Link } from 'react-router-dom';
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
import { DecisionSupportFile } from '../../types/decision';
import { User } from '../../types/user';
import { DecisionSupportFileLink } from '../DecisionSupportFileLink';
import { WorkgroupLink } from '../WorkgroupLink';
export interface DecisionSupportFilePanelProps {
workgroupId: string
}
export const DecisionSupportFilePanel: FunctionComponent<DecisionSupportFilePanelProps> = ({ workgroupId }) => {
const { decisionSupportFiles } = useDecisionSupportFiles({
variables: {
filter: {
workgroups: [workgroupId],
}
}
})
return (
<nav className="panel">
<p className="panel-heading">
Dossiers d'aide à la décision
</p>
{
decisionSupportFiles.map((dsf: DecisionSupportFile) => {
return (
<Link to={`/decisions/${dsf.id}`} key={`dsf-${dsf.id}`} className="panel-block">
<span className="panel-icon">
<i className="fas fa-file" aria-hidden="true"></i>
</span>
<span>{dsf.title}</span>
</Link>
);
})
}
{
decisionSupportFiles.length === 0 ?
<a className="panel-block has-text-centered is-block">
<p className="is-italic">Aucun dossier pour l'instant.</p>
</a> :
null
}
</nav>
);
}

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState, Fragment } from 'react'; import React, { useEffect, useState, Fragment } from 'react';
import { Page } from '../Page'; import { Page } from '../Page';
import { WithLoader } from '../WithLoader';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useWorkgroupsQuery, useWorkgroups } from '../../gql/queries/workgroups'; import { useWorkgroups } from '../../gql/queries/workgroups';
import { useUserProfileQuery, useUserProfile } from '../../gql/queries/profile'; import { useUserProfile } from '../../gql/queries/profile';
import { MembersPanel } from './MembersPanel'; import { MembersPanel } from './MembersPanel';
import { User } from '../../types/user'; import { User } from '../../types/user';
import { InfoPanel } from './InfoPanel'; import { InfoPanel } from './InfoPanel';
import { Workgroup } from '../../types/workgroup'; import { Workgroup } from '../../types/workgroup';
import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups'; import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups';
import { TimelinePanel } from './TimelinePanel'; import { TimelinePanel } from './TimelinePanel';
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
export function WorkgroupPage() { export function WorkgroupPage() {
const { id } = useParams(); const { id } = useParams<any>();
const { workgroups } = useWorkgroups({ const { workgroups } = useWorkgroups({
variables:{ variables:{
filter: { filter: {
@ -140,6 +140,7 @@ export function WorkgroupPage() {
</div> </div>
<div className="column is-4"> <div className="column is-4">
<MembersPanel users={state.workgroup.members as User[]} /> <MembersPanel users={state.workgroup.members as User[]} />
<DecisionSupportFilePanel workgroupId={state.workgroup.id} />
</div> </div>
<div className="column is-4"> <div className="column is-4">
<TimelinePanel workgroup={state.workgroup} /> <TimelinePanel workgroup={state.workgroup} />

View File

@ -46,6 +46,7 @@ type DecisionSupportFile {
input DecisionSupportFileFilter { input DecisionSupportFileFilter {
ids: [ID] ids: [ID]
workgroups: [ID]
} }
input AuthorizationObject { input AuthorizationObject {

View File

@ -108,6 +108,10 @@ func (r *DSFRepository) Search(ctx context.Context, filter *DecisionSupportFileF
if filter.Ids != nil { if filter.Ids != nil {
query = query.Where("id in (?)", filter.Ids) query = query.Where("id in (?)", filter.Ids)
} }
if filter.Workgroups != nil {
query = query.Where("workgroup_id in (?)", filter.Workgroups)
}
} }
dsfs := make([]*DecisionSupportFile, 0) dsfs := make([]*DecisionSupportFile, 0)