Créer/modifier/rejoindre/quitter un groupe de travail
This commit is contained in:
parent
bc9aa1721a
commit
4a340529da
@ -12,7 +12,7 @@ export function HomePage() {
|
||||
return (
|
||||
<Page title={userProfile ? 'Tableau de bord' : 'Accueil'}>
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<section className="mt-5">
|
||||
<WithLoader loading={loading}>
|
||||
{
|
||||
userProfile ?
|
||||
|
@ -34,9 +34,8 @@ export function WorkgroupsPanel() {
|
||||
|
||||
const selectTab = (tabIndex: number) => {
|
||||
setState(state => ({ ...state, selectedTab: tabIndex }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let workgroupsItems = [];
|
||||
|
||||
workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => {
|
||||
@ -52,7 +51,7 @@ export function WorkgroupsPanel() {
|
||||
|
||||
return (
|
||||
<nav className="panel is-info">
|
||||
<div className="level panel-heading">
|
||||
<div className="level panel-heading mb-0">
|
||||
<div className="level-left">
|
||||
<p className="level-item">
|
||||
Groupes de travail
|
||||
|
@ -8,14 +8,14 @@ import { WithLoader } from '../WithLoader';
|
||||
|
||||
export function ProfilePage() {
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ updatProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
|
||||
const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
|
||||
const isLoading = updateUserProfileMutation.loading || userProfileQuery.loading;
|
||||
|
||||
const { userProfile } = (userProfileQuery.data || {});
|
||||
|
||||
const onUserChange = (user: User) => {
|
||||
if (userProfile.name !== user.name) {
|
||||
updatProfile({ variables: {changes: { name: user.name }}});
|
||||
updateProfile({ variables: {changes: { name: user.name }}});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
|
||||
|
||||
export interface WithLoaderProps {
|
||||
loading?: boolean
|
||||
loading?: boolean|boolean[]
|
||||
}
|
||||
|
||||
export const WithLoader: FunctionComponent<WithLoaderProps> = ({ loading, children }) => {
|
||||
const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
loading ?
|
||||
isLoading ?
|
||||
<div>Chargement</div> :
|
||||
children
|
||||
}
|
||||
|
96
client/src/components/WorkgroupPage/InfoForm.tsx
Normal file
96
client/src/components/WorkgroupPage/InfoForm.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
|
||||
export interface InfoFormProps {
|
||||
workgroup: Workgroup
|
||||
onChange?: (workgroup: Workgroup) => void
|
||||
}
|
||||
|
||||
export function InfoForm({ workgroup, onChange }: InfoFormProps) {
|
||||
const [ state, setState ] = useState({
|
||||
changed: false,
|
||||
workgroup: {
|
||||
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
changed: false,
|
||||
workgroup: {
|
||||
id: workgroup && workgroup.id ? workgroup.id : '',
|
||||
name: workgroup && workgroup.name ? workgroup.name : '',
|
||||
createdAt: workgroup && workgroup.createdAt ? workgroup.createdAt : null,
|
||||
closedAt: workgroup && workgroup.closedAt ? workgroup.closedAt : null,
|
||||
}
|
||||
});
|
||||
}, [workgroup]);
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (!state.changed) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
onChange(state.workgroup as Workgroup);
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: false,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const onWorkgroupAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||
const value = evt.currentTarget.value;
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: true,
|
||||
workgroup: {
|
||||
...state.workgroup,
|
||||
[attrName]: value,
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form" style={{width: '100%'}}>
|
||||
<div className="field">
|
||||
<label className="label">Nom du groupe</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" value={state.workgroup.name}
|
||||
onChange={onWorkgroupAttrChange.bind(null, "name")} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
state.workgroup.createdAt ?
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.createdAt}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
{
|
||||
state.workgroup.closedAt ?
|
||||
<div className="field">
|
||||
<label className="label">Date de clôture</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.workgroup.closedAt}</p>
|
||||
</div>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
<div className="buttons is-right">
|
||||
<button disabled={!state.changed}
|
||||
className="button is-success" onClick={onSaveClick}>
|
||||
<span>Enregistrer</span>
|
||||
<span className="icon"><i className="fa fa-save"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
52
client/src/components/WorkgroupPage/InfoPanel.tsx
Normal file
52
client/src/components/WorkgroupPage/InfoPanel.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { User } from '../../types/user';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { InfoForm } from './InfoForm';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
import { useUpdateWorkgroupMutation, useCreateWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
export interface InfoPanelProps {
|
||||
workgroup: Workgroup
|
||||
}
|
||||
|
||||
export const InfoPanel: FunctionComponent<InfoPanelProps> = ({ workgroup }) => {
|
||||
const [ updateWorkgroup, updateWorkgroupMutation ] = useUpdateWorkgroupMutation();
|
||||
const [ createWorkgroup, createWorkgroupMutation ] = useCreateWorkgroupMutation();
|
||||
const history = useHistory();
|
||||
const isLoading = updateWorkgroupMutation.loading || createWorkgroupMutation.loading;
|
||||
|
||||
const onWorkgroupChange = (formWorkgroup: Workgroup) => {
|
||||
const variables: any = { changes: {} };
|
||||
|
||||
if (workgroup.name !== formWorkgroup.name) {
|
||||
variables.changes.name = formWorkgroup.name;
|
||||
}
|
||||
|
||||
if (Object.keys(variables.changes).length === 0) return;
|
||||
|
||||
const isCreation = workgroup.id === '';
|
||||
if (isCreation) {
|
||||
createWorkgroup({variables})
|
||||
.then(({ data: { createWorkgroup } }) => {
|
||||
history.push(`/workgroups/${createWorkgroup.id}`);
|
||||
});
|
||||
} else {
|
||||
variables.workgroupId = workgroup.id;
|
||||
updateWorkgroup({variables});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="panel">
|
||||
<p className="panel-heading">
|
||||
Informations
|
||||
</p>
|
||||
<div className="panel-block">
|
||||
<WithLoader loading={isLoading}>
|
||||
<InfoForm workgroup={workgroup} onChange={onWorkgroupChange} />
|
||||
</WithLoader>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
35
client/src/components/WorkgroupPage/MembersPanel.tsx
Normal file
35
client/src/components/WorkgroupPage/MembersPanel.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { User } from '../../types/user';
|
||||
|
||||
export interface MembersPanelProps {
|
||||
users: User[]
|
||||
}
|
||||
|
||||
export const MembersPanel: FunctionComponent<MembersPanelProps> = ({ users }) => {
|
||||
return (
|
||||
<nav className="panel">
|
||||
<p className="panel-heading">
|
||||
Membres
|
||||
</p>
|
||||
{
|
||||
users.map(u => {
|
||||
return (
|
||||
<div key={`user-${u.id}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className="fas fa-user" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{`${ u.name ? u.name : '' } - `}</span><span className="is-italic">{`${u.email}`}</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
users.length === 0 ?
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<p className="is-italic">Aucun membre pour l'instant.</p>
|
||||
</a> :
|
||||
null
|
||||
}
|
||||
</nav>
|
||||
);
|
||||
}
|
@ -1,20 +1,126 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState, Fragment } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
import { useParams } from 'react-router';
|
||||
import { useWorkgroupsQuery } from '../../gql/queries/workgroups';
|
||||
import { useUserProfileQuery } from '../../gql/queries/profile';
|
||||
import { MembersPanel } from './MembersPanel';
|
||||
import { User } from '../../types/user';
|
||||
import { InfoPanel } from './InfoPanel';
|
||||
import { Workgroup } from '../../types/workgroup';
|
||||
import { useJoinWorkgroupMutation, useLeaveWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||
|
||||
export function WorkgroupPage() {
|
||||
const { id } = useParams();
|
||||
const workgroupsQuery = useWorkgroupsQuery({
|
||||
variables:{
|
||||
filter: {
|
||||
ids: [id],
|
||||
}
|
||||
}
|
||||
});
|
||||
const userProfileQuery = useUserProfileQuery();
|
||||
const [ joinWorkgroup, joinWorkgroupMutation ] = useJoinWorkgroupMutation();
|
||||
const [ leaveWorkgroup, leaveWorkgroupMutation ] = useLeaveWorkgroupMutation();
|
||||
const [ state, setState ] = useState({
|
||||
userProfileId: '',
|
||||
workgroup: {
|
||||
id: '',
|
||||
name: '',
|
||||
closedAt: null,
|
||||
createdAt: null,
|
||||
members: [],
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!workgroupsQuery.data) return;
|
||||
setState(state => ({...state, workgroup:{ ...state.workgroup, ...workgroupsQuery.data.workgroups[0]}}));
|
||||
}, [workgroupsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userProfileQuery.data) return;
|
||||
setState(state => ({...state, userProfileId: userProfileQuery.data.userProfile.id }));
|
||||
}, [userProfileQuery.data]);
|
||||
|
||||
const onJoinWorkgroupClick = () => {
|
||||
joinWorkgroup({
|
||||
variables: {
|
||||
workgroupId: state.workgroup.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onLeaveWorkgroupClick = () => {
|
||||
leaveWorkgroup({
|
||||
variables: {
|
||||
workgroupId: state.workgroup.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isNew = state.workgroup.id === '';
|
||||
const isWorkgroupMember = state.workgroup.members.some(u => u.id === state.userProfileId);
|
||||
|
||||
return (
|
||||
<Page title="Groupe de travail">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<h2 className="is-size-2 subtitle">Groupe de travail</h2>
|
||||
<WithLoader loading={false}>
|
||||
|
||||
</WithLoader>
|
||||
<section className="mt-5">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
{
|
||||
isNew ?
|
||||
<div className="level-item">
|
||||
<div>
|
||||
<h2 className="is-size-3 title is-spaced">Nouveau</h2>
|
||||
<h3 className="is-size-5 subtitle">Groupe de travail</h3>
|
||||
</div>
|
||||
</div> :
|
||||
<div className="level-item">
|
||||
<div>
|
||||
<h2 className="is-size-3 title is-spaced">{state.workgroup.name}</h2>
|
||||
<h3 className="is-size-5 subtitle">Groupe de travail</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="buttons is-right level-item">
|
||||
{
|
||||
isNew ? null :
|
||||
<Fragment>
|
||||
{
|
||||
isWorkgroupMember ?
|
||||
<Fragment>
|
||||
<button onClick={onLeaveWorkgroupClick} className="button is-info is-warning is-medium">
|
||||
<span>Quitter</span>
|
||||
<span className="icon"><i className="fas fa-user-minus"></i></span>
|
||||
</button>
|
||||
<button className="button is-danger is-medium">
|
||||
<span>Clôre</span>
|
||||
<span className="icon"><i className="far fa-times-circle"></i></span>
|
||||
</button>
|
||||
</Fragment> :
|
||||
<button onClick={onJoinWorkgroupClick} className="button is-info is-medium">
|
||||
<span>Rejoindre</span>
|
||||
<span className="icon"><i className="fas fa-user-plus"></i></span>
|
||||
</button>
|
||||
}
|
||||
</Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WithLoader loading={[workgroupsQuery.loading, userProfileQuery.loading, joinWorkgroupMutation.loading, leaveWorkgroupMutation.loading]}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<InfoPanel workgroup={state.workgroup as Workgroup} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<MembersPanel users={state.workgroup.members as User[]} />
|
||||
</div>
|
||||
</div>
|
||||
</WithLoader>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
|
77
client/src/gql/mutations/workgroups.tsx
Normal file
77
client/src/gql/mutations/workgroups.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||
|
||||
const MUTATION_UPDATE_WORKGROUP = gql`
|
||||
mutation updateWorkgroup($workgroupId: ID!, $changes: WorkgroupChanges!) {
|
||||
updateWorkgroup(workgroupId: $workgroupId, changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUpdateWorkgroupMutation() {
|
||||
return useMutation(MUTATION_UPDATE_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_CREATE_WORKGROUP = gql`
|
||||
mutation createWorkgroup($changes: WorkgroupChanges!) {
|
||||
createWorkgroup(changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useCreateWorkgroupMutation() {
|
||||
return useMutation(MUTATION_CREATE_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_JOIN_WORKGROUP = gql`
|
||||
mutation joinWorkgroup($workgroupId: ID!) {
|
||||
joinWorkgroup(workgroupId: $workgroupId) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useJoinWorkgroupMutation() {
|
||||
return useMutation(MUTATION_JOIN_WORKGROUP);
|
||||
}
|
||||
|
||||
const MUTATION_LEAVE_WORKGROUP = gql`
|
||||
mutation leaveWorkgroup($workgroupId: ID!) {
|
||||
leaveWorkgroup(workgroupId: $workgroupId) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
name,
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useLeaveWorkgroupMutation() {
|
||||
return useMutation(MUTATION_LEAVE_WORKGROUP);
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
|
||||
const QUERY_WORKGROUP = gql`
|
||||
query workgroups {
|
||||
workgroups {
|
||||
query workgroups($filter: WorkgroupsFilter) {
|
||||
workgroups(filter: $filter) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
closedAt,
|
||||
members {
|
||||
id,
|
||||
email
|
||||
email,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,11 @@ type Workgroup {
|
||||
members: [User]!
|
||||
}
|
||||
|
||||
input WorkgroupsFilter {
|
||||
ids: [ID]
|
||||
}
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
workgroups: [Workgroup]!
|
||||
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||
return handleUserProfile(ctx)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Workgroups(ctx context.Context) ([]*model1.Workgroup, error) {
|
||||
return handleWorkgroups(ctx)
|
||||
func (r *queryResolver) Workgroups(ctx context.Context, filter *model1.WorkgroupsFilter) ([]*model1.Workgroup, error) {
|
||||
return handleWorkgroups(ctx, filter)
|
||||
}
|
||||
|
||||
func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) {
|
||||
@ -39,13 +39,3 @@ func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupRe
|
||||
type queryResolver struct{ *Resolver }
|
||||
type userResolver 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 *workgroupResolver) Users(ctx context.Context, obj *model1.Workgroup) ([]*model1.User, error) {
|
||||
return obj.Members, nil
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func handleWorkgroups(ctx context.Context) ([]*model.Workgroup, error) {
|
||||
func handleWorkgroups(ctx context.Context, filter *model.WorkgroupsFilter) ([]*model.Workgroup, error) {
|
||||
db, err := getDB(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
@ -16,7 +16,15 @@ func handleWorkgroups(ctx context.Context) ([]*model.Workgroup, error) {
|
||||
|
||||
repo := model.NewWorkgroupRepository(db)
|
||||
|
||||
workgroups, err := repo.FindWorkgroups(ctx)
|
||||
criteria := make([]interface{}, 0)
|
||||
|
||||
if filter != nil {
|
||||
if len(filter.Ids) > 0 {
|
||||
criteria = append(criteria, "id in (?)", filter.Ids)
|
||||
}
|
||||
}
|
||||
|
||||
workgroups, err := repo.FindWorkgroups(ctx, criteria...)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user