Créer/modifier/rejoindre/quitter un groupe de travail

This commit is contained in:
wpetit 2020-07-22 22:25:03 +02:00
parent bc9aa1721a
commit 4a340529da
13 changed files with 403 additions and 34 deletions

View File

@ -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 ?

View File

@ -34,8 +34,7 @@ export function WorkgroupsPanel() {
const selectTab = (tabIndex: number) => {
setState(state => ({ ...state, selectedTab: tabIndex }));
}
};
let workgroupsItems = [];
@ -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

View File

@ -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 }}});
}
};

View File

@ -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
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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">
<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 is-6 is-offset-3">
<h2 className="is-size-2 subtitle">Groupe de travail</h2>
<WithLoader loading={false}>
<div className="column">
<InfoPanel workgroup={state.workgroup as Workgroup} />
</div>
<div className="column">
<MembersPanel users={state.workgroup.members as User[]} />
</div>
</div>
</WithLoader>
</div>
</div>
</section>
</div>
</Page>

View 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);
}

View File

@ -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
}
}
}

View File

@ -17,7 +17,11 @@ type Workgroup {
members: [User]!
}
input WorkgroupsFilter {
ids: [ID]
}
type Query {
userProfile: User
workgroups: [Workgroup]!
workgroups(filter: WorkgroupsFilter): [Workgroup]!
}

View File

@ -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
}

View File

@ -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)
}