Enregistrement et affichage d'un flux d'évènements
- Ajout d'une nouvelle entité "Event" - Affichage d'une "timeline" sur le tableau de bord - Création semi-automatique des évènements lors des modifications par les utilisateurs
This commit is contained in:
@ -74,7 +74,7 @@ const UserSessionCheck: FunctionComponent<UserSessionCheckProps> = ({ setLoggedI
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
setLoggedIn(user.id !== '');
|
||||
setLoggedIn(user && user.id !== '');
|
||||
}, [user]);
|
||||
|
||||
return null;
|
||||
|
@ -1,29 +1,35 @@
|
||||
import React from 'react';
|
||||
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
||||
import { Timeline } from '../Timeline';
|
||||
import { useEvents } from '../../gql/queries/event';
|
||||
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - 7);
|
||||
|
||||
export function Dashboard() {
|
||||
const { events } = useEvents({
|
||||
variables: {
|
||||
filter: {
|
||||
from,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column is-6">
|
||||
<div className="column is-5">
|
||||
<div className="box">
|
||||
<h3 className="is-size-3 mb-3">Ces 7 derniers jours</h3>
|
||||
<Timeline events={events} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<DecisionSupportFilePanel />
|
||||
</div>
|
||||
<div className="column is-3">
|
||||
<WorkgroupsPanel />
|
||||
</div>
|
||||
<div className="column is-3">
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<h3 className="is-size-3 subtitle level-item">Assemblées</h3>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<button disabled className="button is-primary level-item"><i className="fa fa-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>TODO</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
25
client/src/components/DecisionSupportFileLink.tsx
Normal file
25
client/src/components/DecisionSupportFileLink.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useWorkgroups } from "../gql/queries/workgroups";
|
||||
import { useDecisionSupportFiles } from "../gql/queries/dsf";
|
||||
|
||||
export interface DecisioSupportFileLinkProps {
|
||||
decisionSupportFileId: number
|
||||
}
|
||||
|
||||
export const DecisioSupportFileLink: FunctionComponent<DecisioSupportFileLinkProps> = ({ decisionSupportFileId }) => {
|
||||
const { decisionSupportFiles } = useDecisionSupportFiles({
|
||||
fetchPolicy: "cache-first",
|
||||
variables: {
|
||||
filter: {
|
||||
ids: [decisionSupportFileId]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const title = decisionSupportFiles.length > 0 ? decisionSupportFiles[0].title : `#${decisionSupportFileId}`;
|
||||
|
||||
return (
|
||||
<Link to={`/decisions/${decisionSupportFileId}`}>{title}</Link>
|
||||
);
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Config } from '../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useLoggedIn } from '../hooks/useLoggedIn';
|
||||
|
208
client/src/components/Timeline.tsx
Normal file
208
client/src/components/Timeline.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { formatDate } from "../util/date";
|
||||
import { Event } from "../types/event";
|
||||
import { Link } from "react-router-dom";
|
||||
import { WorkgroupLink } from "./WorkgroupLink";
|
||||
import { DecisioSupportFileLink } from "./DecisionSupportFileLink";
|
||||
|
||||
export interface TimelineProps {
|
||||
events?: Event[]
|
||||
}
|
||||
|
||||
export const Timeline: FunctionComponent<TimelineProps> = ({ events }) => {
|
||||
events = debounceEvents(events) || [];
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="timeline">
|
||||
{
|
||||
events.map(evt => {
|
||||
return (
|
||||
<div key={evt.id} className="timeline-item">
|
||||
{renderEventMarker(evt)}
|
||||
<div className="timeline-content">
|
||||
<p className="heading">{formatDate(evt.createdAt)}</p>
|
||||
{renderEventContent(evt)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
events.length === 0 ?
|
||||
<p className="has-text-centered is-italic">Aucun évènement.</p> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function debounceEvents(events: Event[]): Event[] {
|
||||
const debounced = [];
|
||||
for(let evt: Event, i = 0; (evt = events[i]); ++i) {
|
||||
const prev = i > 0 ? events[i-1] : null;
|
||||
|
||||
if (!prev) {
|
||||
debounced.push(evt);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isSame = evt.objectId === prev.objectId &&
|
||||
evt.objectType === prev.objectType &&
|
||||
evt.type === prev.type &&
|
||||
evt.user.id === prev.user.id
|
||||
;
|
||||
|
||||
if (isSame) continue;
|
||||
|
||||
debounced.push(evt);
|
||||
}
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
const eventMarkerMap = {
|
||||
"closed": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-danger">
|
||||
<i className="fa fa-times"></i>
|
||||
</div>
|
||||
),
|
||||
"created": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-success">
|
||||
<i className="fa fa-plus"></i>
|
||||
</div>
|
||||
),
|
||||
"updated": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-info">
|
||||
<i className="fa fa-pen"></i>
|
||||
</div>
|
||||
),
|
||||
"title-changed": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-info">
|
||||
<i className="fa fa-pen"></i>
|
||||
</div>
|
||||
),
|
||||
"status-changed": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-primary">
|
||||
<i className="fa fa-star"></i>
|
||||
</div>
|
||||
),
|
||||
"joined": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-info">
|
||||
<i className="fa fa-users"></i>
|
||||
</div>
|
||||
),
|
||||
"leaved": (evt:Event) => (
|
||||
<div className="timeline-marker is-icon is-warning">
|
||||
<i className="fas fa-users-slash"></i>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
function renderEventMarker(evt: Event) {
|
||||
const render = eventMarkerMap[evt.type];
|
||||
if (!render) return ( <div className="timeline-marker"></div> );
|
||||
return render(evt);
|
||||
}
|
||||
|
||||
const eventContentMap = {
|
||||
"created": {
|
||||
"workgroup": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le groupe de travail `}</span>
|
||||
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
"dsf": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le dossier d'aide à la décision `}</span>
|
||||
"<DecisioSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
"title-changed": {
|
||||
"dsf": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le titre du dossier d'aide à la décision `}</span>
|
||||
"<DecisioSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
},
|
||||
"status-changed": {
|
||||
"dsf": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le statut du dossier d'aide à la décision `}</span>
|
||||
"<DecisioSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
},
|
||||
"joined": {
|
||||
"workgroup": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a rejoint le groupe de travail `}</span>
|
||||
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
"updated": {
|
||||
"workgroup": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a mis à jour le groupe de travail `}</span>
|
||||
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
"dsf": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le dossier d'aide à la décision `}</span>
|
||||
"<DecisioSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
"leaved": {
|
||||
"workgroup": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a quitté le groupe de travail `}</span>
|
||||
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
"closed": {
|
||||
"workgroup": (evt:Event) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a clos le groupe de travail `}</span>
|
||||
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function renderEventContent(evt: Event) {
|
||||
const eventTypeMap = eventContentMap[evt.type];
|
||||
const render = eventTypeMap && eventTypeMap[evt.objectType];
|
||||
|
||||
if (!eventTypeMap || !render) {
|
||||
return (
|
||||
<span className="is-italic">{`Type d'évènement "${evt.type}/${evt.objectType}" inconnu.`}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return render(evt);
|
||||
}
|
24
client/src/components/WorkgroupLink.tsx
Normal file
24
client/src/components/WorkgroupLink.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useWorkgroups } from "../gql/queries/workgroups";
|
||||
|
||||
export interface WorkgroupLinkProps {
|
||||
workgroupId: number
|
||||
}
|
||||
|
||||
export const WorkgroupLink: FunctionComponent<WorkgroupLinkProps> = ({ workgroupId }) => {
|
||||
const { workgroups } = useWorkgroups({
|
||||
fetchPolicy: "cache-first",
|
||||
variables: {
|
||||
filter: {
|
||||
ids: [workgroupId]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const workgroupName = workgroups.length > 0 ? workgroups[0].name : `#${workgroupId}`;
|
||||
|
||||
return (
|
||||
<Link to={`/workgroups/${workgroupId}`}>{workgroupName}</Link>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { useIsAuthorized } from '../../gql/queries/authorization';
|
||||
import { formatDate } from '../../util/date';
|
||||
|
||||
export interface InfoFormProps {
|
||||
workgroup: Workgroup
|
||||
@ -80,7 +81,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.createdAt}</p>
|
||||
<p className="input is-static">{formatDate(state.workgroup.createdAt)}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
@ -90,7 +91,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||
<div className="field">
|
||||
<label className="label">Date de clôture</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.closedAt}</p>
|
||||
<p className="input is-static">{formatDate(state.workgroup.closedAt)}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
|
Reference in New Issue
Block a user