Merge branch 'develop' into dist/ubuntu/bionic/develop
This commit is contained in:
commit
d02ae87a55
|
@ -1125,9 +1125,9 @@
|
||||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-free": {
|
"@fortawesome/fontawesome-free": {
|
||||||
"version": "5.13.0",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.0.tgz",
|
||||||
"integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==",
|
"integrity": "sha512-wXetjQBNMTP59MAYNR1tdahMDOLx3FYj3PKdso7PLFLDpTvmAIqhSSEqnSTmWKahRjD+Sh5I5635+5qaoib5lw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
|
@ -3197,6 +3197,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
|
||||||
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
|
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
|
||||||
},
|
},
|
||||||
|
"bulma-timeline": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bulma-timeline/-/bulma-timeline-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-gCUOcSUuzHoeVMkCpLF49j5Z5yl78XQ+KgJcT+1ju5WIGgBgVytRUob/dw5NHAxPLO2rmcvwYNbCJFp7w4WT4Q=="
|
||||||
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||||
|
@ -4484,9 +4489,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"elliptic": {
|
"elliptic": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||||
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
|
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bn.js": "^4.4.0",
|
"bn.js": "^4.4.0",
|
||||||
|
@ -6551,9 +6556,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.15",
|
"version": "4.17.20",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
},
|
},
|
||||||
"loglevel": {
|
"loglevel": {
|
||||||
"version": "1.6.8",
|
"version": "1.6.8",
|
||||||
|
@ -7052,9 +7057,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-forge": {
|
"node-forge": {
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||||
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
|
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-gyp": {
|
"node-gyp": {
|
||||||
|
@ -8745,12 +8750,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"selfsigned": {
|
"selfsigned": {
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
|
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
|
||||||
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
|
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-forge": "0.9.0"
|
"node-forge": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||||
"@babel/preset-env": "^7.7.1",
|
"@babel/preset-env": "^7.7.1",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@types/node": "^13.13.4",
|
"@types/node": "^13.13.4",
|
||||||
"@types/react-dom": "^16.9.7",
|
"@types/react-dom": "^16.9.7",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
|
@ -55,6 +55,7 @@
|
||||||
"@types/qs": "^6.9.3",
|
"@types/qs": "^6.9.3",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"bulma": "^0.9.0",
|
"bulma": "^0.9.0",
|
||||||
|
"bulma-timeline": "^3.0.4",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
|
|
@ -74,7 +74,7 @@ const UserSessionCheck: FunctionComponent<UserSessionCheckProps> = ({ setLoggedI
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
setLoggedIn(user.id !== '');
|
setLoggedIn(user && user.id !== '');
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,29 +1,35 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||||
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
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() {
|
export function Dashboard() {
|
||||||
|
const { events } = useEvents({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="columns">
|
<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 />
|
<DecisionSupportFilePanel />
|
||||||
</div>
|
</div>
|
||||||
<div className="column is-3">
|
<div className="column is-3">
|
||||||
<WorkgroupsPanel />
|
<WorkgroupsPanel />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ import { useWorkgroups } from '../../gql/queries/workgroups';
|
||||||
import { useUserProfile } from '../../gql/queries/profile';
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
import { inWorkgroup } from '../../types/workgroup';
|
import { inWorkgroup } from '../../types/workgroup';
|
||||||
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
||||||
import { asDate } from '../../util/date';
|
import { asDate, formatDate } from '../../util/date';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export interface MetadataPanelProps extends DecisionSupportFileUpdaterProps {};
|
export interface MetadataPanelProps extends DecisionSupportFileUpdaterProps {};
|
||||||
|
@ -81,13 +81,13 @@ export const MetadataPanel: FunctionComponent<MetadataPanelProps> = ({ dsf, upda
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="label">Créé le</div>
|
<div className="label">Créé le</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p>{asDate(dsf.createdAt).toISOString()}</p>
|
<p>{formatDate(dsf.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="label">Voté le</div>
|
<div className="label">Voté le</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p>{dsf.votedAt ? dsf.votedAt : '--'}</p>
|
<p>{dsf.votedAt ? formatDate(dsf.votedAt) : '--'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { Fragment, useState } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import logo from '../resources/logo.svg';
|
import logo from '../resources/logo.svg';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { Config } from '../config';
|
import { Config } from '../config';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useLoggedIn } from '../hooks/useLoggedIn';
|
import { useLoggedIn } from '../hooks/useLoggedIn';
|
||||||
|
|
|
@ -25,12 +25,14 @@ export function ProfilePage() {
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column is-6 is-offset-3">
|
<div className="column is-6 is-offset-3">
|
||||||
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
<div className="box">
|
||||||
<WithLoader loading={isLoading || !userProfile}>
|
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||||
{
|
<WithLoader loading={isLoading || !userProfile}>
|
||||||
<UserForm onChange={onUserChange} user={userProfile} />
|
{
|
||||||
}
|
<UserForm onChange={onUserChange} user={userProfile} />
|
||||||
</WithLoader>
|
}
|
||||||
|
</WithLoader>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||||
import { User } from '../types/user';
|
import { User } from '../types/user';
|
||||||
|
import { formatDate } from '../util/date';
|
||||||
|
|
||||||
export interface UserFormProps {
|
export interface UserFormProps {
|
||||||
user: User
|
user: User
|
||||||
|
@ -62,13 +63,13 @@ export function UserForm({ user, onChange }: UserFormProps) {
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Date de dernière connexion</label>
|
<label className="label">Date de dernière connexion</label>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p className="input is-static">{state.user.connectedAt}</p>
|
<p className="input is-static">{formatDate(state.user.connectedAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Date de création</label>
|
<label className="label">Date de création</label>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p className="input is-static">{state.user.createdAt}</p>
|
<p className="input is-static">{formatDate(state.user.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="buttons is-right">
|
<div className="buttons is-right">
|
||||||
|
|
|
@ -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 React, { useState, ChangeEvent, useEffect } from 'react';
|
||||||
import { Workgroup } from '../../types/workgroup';
|
import { Workgroup } from '../../types/workgroup';
|
||||||
import { useIsAuthorized } from '../../gql/queries/authorization';
|
import { useIsAuthorized } from '../../gql/queries/authorization';
|
||||||
|
import { formatDate } from '../../util/date';
|
||||||
|
|
||||||
export interface InfoFormProps {
|
export interface InfoFormProps {
|
||||||
workgroup: Workgroup
|
workgroup: Workgroup
|
||||||
|
@ -80,7 +81,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Date de création</label>
|
<label className="label">Date de création</label>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p className="input is-static">{state.workgroup.createdAt}</p>
|
<p className="input is-static">{formatDate(state.workgroup.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>:
|
</div>:
|
||||||
null
|
null
|
||||||
|
@ -90,7 +91,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Date de clôture</label>
|
<label className="label">Date de clôture</label>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<p className="input is-static">{state.workgroup.closedAt}</p>
|
<p className="input is-static">{formatDate(state.workgroup.closedAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>:
|
</div>:
|
||||||
null
|
null
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
import { Event } from '../../types/event';
|
||||||
|
|
||||||
|
export const QUERY_EVENTS = gql`
|
||||||
|
query events($filter: EventFilter) {
|
||||||
|
events(filter: $filter) {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
type
|
||||||
|
objectType
|
||||||
|
objectId
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useEventsQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
return useQuery(QUERY_EVENTS, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvents<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const { data, loading, error } = useGraphQLData<Event[]>(
|
||||||
|
QUERY_EVENTS, 'events', [], options
|
||||||
|
);
|
||||||
|
return { events: data, loading, error };
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import '@fortawesome/fontawesome-free/js/solid'
|
||||||
import '@fortawesome/fontawesome-free/js/regular'
|
import '@fortawesome/fontawesome-free/js/regular'
|
||||||
import '@fortawesome/fontawesome-free/js/brands'
|
import '@fortawesome/fontawesome-free/js/brands'
|
||||||
import './resources/favicon.png';
|
import './resources/favicon.png';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App />,
|
<App />,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
@import 'bulma/bulma.sass';
|
@import 'bulma/bulma.sass';
|
||||||
|
@import 'bulma-timeline/dist/css/bulma-timeline.sass';
|
||||||
|
@import '_bulma-timeline.scss';
|
||||||
@import '_base.scss';
|
@import '_base.scss';
|
||||||
@import '_loader.scss';
|
@import '_loader.scss';
|
|
@ -0,0 +1,12 @@
|
||||||
|
.timeline {
|
||||||
|
.timeline-item {
|
||||||
|
.timeline-marker {
|
||||||
|
&.is-icon {
|
||||||
|
> svg {
|
||||||
|
color: $white;
|
||||||
|
font-size: $timeline-icon-size !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
id: string
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
user: User
|
||||||
|
objectType: string
|
||||||
|
objectId: number
|
||||||
|
type: string
|
||||||
|
}
|
|
@ -2,3 +2,15 @@ export function asDate(d: string|Date): Date {
|
||||||
if (typeof d === 'string') return new Date(d);
|
if (typeof d === 'string') return new Date(d);
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intl = Intl.DateTimeFormat(navigator.language, {
|
||||||
|
weekday: 'long',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric', minute: 'numeric', second: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatDate(d: Date|string): string {
|
||||||
|
d = asDate(d);
|
||||||
|
return intl.format(d);
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ var initialModels = []interface{}{
|
||||||
&model.User{},
|
&model.User{},
|
||||||
&model.Workgroup{},
|
&model.Workgroup{},
|
||||||
&model.DecisionSupportFile{},
|
&model.DecisionSupportFile{},
|
||||||
|
&model.Event{},
|
||||||
}
|
}
|
||||||
|
|
||||||
func m000initialSchema() orm.Migration {
|
func m000initialSchema() orm.Migration {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"reflect"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
"gitlab.com/wpetit/goweb/middleware/container"
|
"gitlab.com/wpetit/goweb/middleware/container"
|
||||||
|
@ -12,6 +12,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
||||||
|
user, db, err := getSessionUser(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
authorized, err := isAuthorized(ctx, &model.DecisionSupportFile{}, model.ActionCreate)
|
authorized, err := isAuthorized(ctx, &model.DecisionSupportFile{}, model.ActionCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
|
@ -21,9 +26,6 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
|
||||||
return nil, errs.WithStack(ErrForbidden)
|
return nil, errs.WithStack(ErrForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctn := container.Must(ctx)
|
|
||||||
db := orm.Must(ctn).DB()
|
|
||||||
|
|
||||||
repo := model.NewDSFRepository(db)
|
repo := model.NewDSFRepository(db)
|
||||||
|
|
||||||
dsf, err := repo.Create(ctx, changes)
|
dsf, err := repo.Create(ctx, changes)
|
||||||
|
@ -31,21 +33,29 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, dsf); err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return dsf, nil
|
return dsf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
||||||
ctn := container.Must(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
db := orm.Must(ctn).DB()
|
|
||||||
|
|
||||||
repo := model.NewDSFRepository(db)
|
|
||||||
|
|
||||||
dsf, err := repo.Find(ctx, id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authorized, err := isAuthorized(ctx, dsf, model.ActionUpdate)
|
repo := model.NewDSFRepository(db)
|
||||||
|
|
||||||
|
prevDsf, err := repo.Find(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authorized, err := isAuthorized(ctx, prevDsf, model.ActionUpdate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -54,11 +64,31 @@ func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *mo
|
||||||
return nil, errs.WithStack(ErrForbidden)
|
return nil, errs.WithStack(ErrForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
dsf, err = repo.Update(ctx, id, changes)
|
dsf, err := repo.Update(ctx, id, changes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.WithStack(err)
|
return nil, errs.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if changes != nil && changes.Status != nil && prevDsf.Status != *changes.Status {
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeStatusChanged, dsf); err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes != nil && changes.Title != nil && prevDsf.Title != *changes.Title {
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeTitleChanged, dsf); err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes != nil && !reflect.DeepEqual(prevDsf.Sections, dsf.Sections) {
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, dsf); err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dsf, nil
|
return dsf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,13 +118,3 @@ func handleDecisionSupportFiles(ctx context.Context, filter *model.DecisionSuppo
|
||||||
|
|
||||||
return dsfs, nil
|
return dsfs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSections(ctx context.Context, dsf *model.DecisionSupportFile) (map[string]interface{}, error) {
|
|
||||||
sections := make(map[string]interface{})
|
|
||||||
|
|
||||||
if err := json.Unmarshal(dsf.Sections.RawMessage, §ions); err != nil {
|
|
||||||
return nil, errs.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
|
errs "github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/middleware/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleEvents(ctx context.Context, filter *model.EventFilter) ([]*model.Event, error) {
|
||||||
|
ctn := container.Must(ctx)
|
||||||
|
db := orm.Must(ctn).DB()
|
||||||
|
|
||||||
|
repo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
events, err := repo.Search(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
|
@ -18,6 +18,16 @@ type Workgroup {
|
||||||
members: [User]!
|
members: [User]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Event {
|
||||||
|
id: ID!
|
||||||
|
type: String!
|
||||||
|
createdAt: Time!
|
||||||
|
updatedAt: Time!
|
||||||
|
objectId: ID!
|
||||||
|
objectType: String!
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
input WorkgroupsFilter {
|
input WorkgroupsFilter {
|
||||||
ids: [ID]
|
ids: [ID]
|
||||||
}
|
}
|
||||||
|
@ -44,9 +54,19 @@ input AuthorizationObject {
|
||||||
decisionSupportFileId: ID
|
decisionSupportFileId: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input EventFilter {
|
||||||
|
objectType: String
|
||||||
|
objectId: ID
|
||||||
|
userId: ID
|
||||||
|
type: String
|
||||||
|
from: Time
|
||||||
|
to: Time
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
userProfile: User
|
userProfile: User
|
||||||
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
||||||
decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]!
|
decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]!
|
||||||
|
events(filter: EventFilter): [Event]!
|
||||||
isAuthorized(action: String!, object: AuthorizationObject!): Boolean!
|
isAuthorized(action: String!, object: AuthorizationObject!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,16 @@ func (r *decisionSupportFileResolver) ID(ctx context.Context, obj *model1.Decisi
|
||||||
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
|
func (r *eventResolver) ID(ctx context.Context, obj *model1.Event) (string, error) {
|
||||||
return handleSections(ctx, obj)
|
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventResolver) Type(ctx context.Context, obj *model1.Event) (string, error) {
|
||||||
|
return string(obj.Type), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventResolver) ObjectID(ctx context.Context, obj *model1.Event) (string, error) {
|
||||||
|
return strconv.FormatUint(uint64(obj.ObjectID), 10), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||||
|
@ -31,6 +39,10 @@ func (r *queryResolver) DecisionSupportFiles(ctx context.Context, filter *model1
|
||||||
return handleDecisionSupportFiles(ctx, filter)
|
return handleDecisionSupportFiles(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) Events(ctx context.Context, filter *model1.EventFilter) ([]*model1.Event, error) {
|
||||||
|
return handleEvents(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *queryResolver) IsAuthorized(ctx context.Context, action string, object model1.AuthorizationObject) (bool, error) {
|
func (r *queryResolver) IsAuthorized(ctx context.Context, action string, object model1.AuthorizationObject) (bool, error) {
|
||||||
return handleIsAuthorized(ctx, action, object)
|
return handleIsAuthorized(ctx, action, object)
|
||||||
}
|
}
|
||||||
|
@ -48,6 +60,9 @@ func (r *Resolver) DecisionSupportFile() generated.DecisionSupportFileResolver {
|
||||||
return &decisionSupportFileResolver{r}
|
return &decisionSupportFileResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event returns generated.EventResolver implementation.
|
||||||
|
func (r *Resolver) Event() generated.EventResolver { return &eventResolver{r} }
|
||||||
|
|
||||||
// Query returns generated.QueryResolver implementation.
|
// Query returns generated.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
@ -58,6 +73,17 @@ func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
|
||||||
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
|
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
|
||||||
|
|
||||||
type decisionSupportFileResolver struct{ *Resolver }
|
type decisionSupportFileResolver struct{ *Resolver }
|
||||||
|
type eventResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type userResolver struct{ *Resolver }
|
type userResolver struct{ *Resolver }
|
||||||
type workgroupResolver struct{ *Resolver }
|
type workgroupResolver struct{ *Resolver }
|
||||||
|
|
||||||
|
// !!! WARNING !!!
|
||||||
|
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||||
|
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||||
|
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||||
|
// it when you're done.
|
||||||
|
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||||
|
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
|
||||||
|
return obj.Sections, nil
|
||||||
|
}
|
||||||
|
|
|
@ -72,6 +72,12 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeJoined, workgroup); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +108,12 @@ func handleLeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeLeaved, workgroup); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +127,7 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
|
||||||
return nil, errs.WithStack(ErrForbidden)
|
return nil, errs.WithStack(ErrForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := getDB(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -127,11 +139,17 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, workgroup); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||||
db, err := getDB(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -157,11 +175,17 @@ func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeClosed, workgroup); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||||
db, err := getDB(ctx)
|
user, db, err := getSessionUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -187,5 +211,11 @@ func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes mode
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventRepo := model.NewEventRepository(db)
|
||||||
|
|
||||||
|
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, workgroup); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return workgroup, nil
|
return workgroup, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,55 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/jinzhu/gorm/dialects/postgres"
|
"github.com/jinzhu/gorm/dialects/postgres"
|
||||||
|
errs "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ObjectTypeDecisionSupportFile = "dsf"
|
||||||
|
|
||||||
type DecisionSupportFile struct {
|
type DecisionSupportFile struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Sections postgres.Jsonb `json:"sections"`
|
SectionsJSON postgres.Jsonb `json:"-" gorm:"column:sections;"`
|
||||||
Status string `json:"status"`
|
Sections map[string]interface{} `gorm:"-"`
|
||||||
WorkgroupID uint `json:"-"`
|
Status string `json:"status"`
|
||||||
Workgroup *Workgroup `json:"workgroup"`
|
WorkgroupID uint `json:"-"`
|
||||||
VotedAt time.Time `json:"votedAt"`
|
Workgroup *Workgroup `json:"workgroup" gorm:"association_autoupdate:false"`
|
||||||
ClosedAt time.Time `json:"closedAt"`
|
VotedAt time.Time `json:"votedAt"`
|
||||||
|
ClosedAt time.Time `json:"closedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DecisionSupportFile) ObjectID() uint {
|
||||||
|
return f.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DecisionSupportFile) ObjectType() string {
|
||||||
|
return ObjectTypeDecisionSupportFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DecisionSupportFile) BeforeSave() error {
|
||||||
|
rawSections, err := json.Marshal(f.Sections)
|
||||||
|
if err != nil {
|
||||||
|
return errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.SectionsJSON = postgres.Jsonb{RawMessage: rawSections}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DecisionSupportFile) AfterFind() (err error) {
|
||||||
|
sections := make(map[string]interface{})
|
||||||
|
|
||||||
|
if err := json.Unmarshal(f.SectionsJSON.RawMessage, §ions); err != nil {
|
||||||
|
return errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Sections = sections
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,9 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/jinzhu/gorm/dialects/postgres"
|
|
||||||
errs "github.com/pkg/errors"
|
errs "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,12 +67,7 @@ func (r *DSFRepository) updateFromChanges(dsf *DecisionSupportFile, changes *Dec
|
||||||
dsf.Workgroup = wg
|
dsf.Workgroup = wg
|
||||||
|
|
||||||
if changes.Sections != nil {
|
if changes.Sections != nil {
|
||||||
rawSections, err := json.Marshal(changes.Sections)
|
dsf.Sections = changes.Sections
|
||||||
if err != nil {
|
|
||||||
return errs.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dsf.Sections = postgres.Jsonb{RawMessage: rawSections}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if changes.Title != nil {
|
if changes.Title != nil {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeCreated EventType = "created"
|
||||||
|
EventTypeUpdated EventType = "updated"
|
||||||
|
EventTypeLeaved EventType = "leaved"
|
||||||
|
EventTypeJoined EventType = "joined"
|
||||||
|
EventTypeClosed EventType = "closed"
|
||||||
|
EventTypeStatusChanged EventType = "status-changed"
|
||||||
|
EventTypeTitleChanged EventType = "title-changed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventObject interface {
|
||||||
|
ObjectID() uint
|
||||||
|
ObjectType() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID uint `json:"-"`
|
||||||
|
User *User `json:"user" gorm:"association_autoupdate:false"`
|
||||||
|
ObjectType string `json:"objectType"`
|
||||||
|
ObjectID uint `json:"objectId"`
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
errs "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventRepository) Add(ctx context.Context, user *User, eventType EventType, obj EventObject) (*Event, error) {
|
||||||
|
evt := &Event{
|
||||||
|
Type: eventType,
|
||||||
|
User: user,
|
||||||
|
ObjectID: obj.ObjectID(),
|
||||||
|
ObjectType: obj.ObjectType(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.db.Save(&evt).Error; err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EventRepository) Search(ctx context.Context, filter *EventFilter) ([]*Event, error) {
|
||||||
|
query := r.db.Model(&Event{}).Preload("User")
|
||||||
|
|
||||||
|
if filter == nil {
|
||||||
|
filter = &EventFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ObjectID != nil {
|
||||||
|
query = query.Where("object_id = ?", filter.ObjectID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ObjectType != nil {
|
||||||
|
query = query.Where("object_type = ?", filter.ObjectType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.UserID != nil {
|
||||||
|
query = query.Where("user_id = ?", filter.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Type != nil {
|
||||||
|
query = query.Where("type = ?", filter.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.From != nil {
|
||||||
|
query = query.Where("created_at >= ?", filter.From)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.To != nil {
|
||||||
|
query = query.Where("created_at <= ?", filter.To)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Order("created_at DESC")
|
||||||
|
|
||||||
|
events := make([]*Event, 0)
|
||||||
|
|
||||||
|
if err := query.Find(&events).Error; err != nil {
|
||||||
|
return nil, errs.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventRepository(db *gorm.DB) *EventRepository {
|
||||||
|
return &EventRepository{db}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ type User struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Email string `json:"email" gorm:"unique;not null"`
|
Email string `json:"email" gorm:"unique;not null"`
|
||||||
ConnectedAt time.Time `json:"connectedAt"`
|
ConnectedAt time.Time `json:"connectedAt"`
|
||||||
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
|
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;association_autoupdate:false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileChanges struct {
|
type ProfileChanges struct {
|
||||||
|
|
|
@ -6,11 +6,21 @@ import (
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ObjectTypeWorkgroup = "workgroup"
|
||||||
|
|
||||||
type Workgroup struct {
|
type Workgroup struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
ClosedAt time.Time `json:"closedAt"`
|
ClosedAt time.Time `json:"closedAt"`
|
||||||
Members []*User `gorm:"many2many:users_workgroups;"`
|
Members []*User `gorm:"many2many:users_workgroups;association_autoupdate:false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Workgroup) ObjectID() uint {
|
||||||
|
return w.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Workgroup) ObjectType() string {
|
||||||
|
return ObjectTypeWorkgroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkgroupChanges struct {
|
type WorkgroupChanges struct {
|
||||||
|
|
|
@ -106,29 +106,35 @@ func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, wo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||||
user := &User{}
|
|
||||||
|
|
||||||
err := r.db.First(user, "id = ?", userID).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "could not find user")
|
|
||||||
}
|
|
||||||
|
|
||||||
workgroup := &Workgroup{}
|
workgroup := &Workgroup{}
|
||||||
workgroup.ID = workgroupID
|
workgroup.ID = workgroupID
|
||||||
|
|
||||||
err = r.db.Model(user).
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
Association("Workgroups").
|
user := &User{}
|
||||||
Delete(workgroup).
|
err := tx.First(user, "id = ?", userID).Error
|
||||||
Error
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not find user")
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
err = tx.Model(user).
|
||||||
return nil, errors.Wrap(err, "could not add user to workgroup")
|
Association("Workgroups").
|
||||||
}
|
Delete(workgroup).
|
||||||
|
Error
|
||||||
|
|
||||||
err = r.db.Model(workgroup).
|
if err != nil {
|
||||||
Preload("Members").
|
return errors.Wrap(err, "could not add user to workgroup")
|
||||||
First(workgroup, "id = ?", workgroupID).
|
}
|
||||||
Error
|
|
||||||
|
err = tx.Model(workgroup).
|
||||||
|
Preload("Members").
|
||||||
|
First(workgroup, "id = ?", workgroupID).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue