From 7bf4c4f0803ae6af9b1b909c7fcef6e10aa1730a Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 16 Jul 2020 20:43:41 +0200 Subject: [PATCH 1/7] Base de tableau de bord --- client/src/components/HomePage/Dashboard.tsx | 46 ++++++++++++++++++++ client/src/components/HomePage/HomePage.tsx | 25 ++++++----- 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 client/src/components/HomePage/Dashboard.tsx diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/HomePage/Dashboard.tsx new file mode 100644 index 0000000..408087a --- /dev/null +++ b/client/src/components/HomePage/Dashboard.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +export function Dashboard() { + return ( +
+
+
+
+
+

Groupes de travail

+
+
+ +
+
+
+
+
+
+
+
+

D.à.Ds

+
+
+ +
+
+
TODO
+
+
+
+
+
+
+

Assemblées

+
+
+ +
+
+
TODO
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index 7607cee..5521d49 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -1,26 +1,27 @@ import React, { useEffect } from 'react'; import { Page } from '../Page'; -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; import { RootState } from '../../store/reducers/root'; +import { Dashboard } from './Dashboard'; export function HomePage() { const currentUser = useSelector((state: RootState) => state.auth.currentUser); return ( - +
-
-
-
- { - currentUser && currentUser.email ? -

Bonjour {currentUser.name ? currentUser.name : currentUser.email} !

: -

Veuillez vous authentifier.

- } + { + currentUser ? + : +
+
+
+

Veuillez vous authentifier.

+
-
-
+
+ }
From 676ddf3bc8a6fb9a91ca1e52bff0fb12960537f5 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 21 Jul 2020 18:10:03 +0200 Subject: [PATCH 2/7] Base d'API backend pour la manipulation des groupes de travail Types: type Workgroup { id: ID! name: String createdAt: Time! closedAt: Time members: [User]! } Mutations: joinWorkgroup(workgroupId: ID!): Workgroup! leaveWorkgroup(workgroupId: ID!): Workgroup! createWorkgroup(changes: WorkgroupChanges!): Workgroup! closeWorkgroup(workgroupId: ID!): Workgroup! updateWorkgroup(workgroupId: ID!, changes: WorkgroupChanges!): Workgroup! Queries: workgroups: [Workgroup]! --- cmd/server/migration.go | 1 + internal/graph/helper.go | 23 +++ internal/graph/mutation.graphql | 9 ++ internal/graph/mutation.resolvers.go | 20 +++ internal/graph/query.graphql | 11 ++ internal/graph/query.resolvers.go | 31 ++++ ...ser_profile.go => user_profile_handler.go} | 25 +--- internal/graph/workgroup_handler.go | 134 +++++++++++++++++ internal/model/user.go | 16 +- internal/model/user_repository.go | 5 +- internal/model/workgroup.go | 18 +++ internal/model/workgroup_repository.go | 140 ++++++++++++++++++ 12 files changed, 402 insertions(+), 31 deletions(-) rename internal/graph/{user_profile.go => user_profile_handler.go} (55%) create mode 100644 internal/graph/workgroup_handler.go create mode 100644 internal/model/workgroup.go create mode 100644 internal/model/workgroup_repository.go diff --git a/cmd/server/migration.go b/cmd/server/migration.go index 083ee3e..f4fc39a 100644 --- a/cmd/server/migration.go +++ b/cmd/server/migration.go @@ -79,6 +79,7 @@ func applyMigration(ctx context.Context, ctn *service.Container) error { // nolint: gochecknoglobals var initialModels = []interface{}{ &model.User{}, + &model.Workgroup{}, } func m000initialSchema() orm.Migration { diff --git a/internal/graph/helper.go b/internal/graph/helper.go index 73ec676..3351cc7 100644 --- a/internal/graph/helper.go +++ b/internal/graph/helper.go @@ -3,7 +3,9 @@ package graph import ( "context" + "forge.cadoles.com/Cadoles/daddy/internal/model" "forge.cadoles.com/Cadoles/daddy/internal/orm" + "forge.cadoles.com/Cadoles/daddy/internal/session" "github.com/jinzhu/gorm" "github.com/pkg/errors" @@ -23,3 +25,24 @@ func getDB(ctx context.Context) (*gorm.DB, error) { return orm.DB(), nil } + +func getSessionUser(ctx context.Context) (*model.User, *gorm.DB, error) { + db, err := getDB(ctx) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + userEmail, err := session.UserEmail(ctx) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + repo := model.NewUserRepository(db) + + user, err := repo.FindUserByEmail(ctx, userEmail) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return user, db, nil +} diff --git a/internal/graph/mutation.graphql b/internal/graph/mutation.graphql index 3f0f71a..7dcc5da 100644 --- a/internal/graph/mutation.graphql +++ b/internal/graph/mutation.graphql @@ -2,6 +2,15 @@ input ProfileChanges { name: String } +input WorkgroupChanges { + name: String +} + type Mutation { updateProfile(changes: ProfileChanges!): User! + joinWorkgroup(workgroupId: ID!): Workgroup! + leaveWorkgroup(workgroupId: ID!): Workgroup! + createWorkgroup(changes: WorkgroupChanges!): Workgroup! + closeWorkgroup(workgroupId: ID!): Workgroup! + updateWorkgroup(workgroupId: ID!, changes: WorkgroupChanges!): Workgroup! } \ No newline at end of file diff --git a/internal/graph/mutation.resolvers.go b/internal/graph/mutation.resolvers.go index c979e19..7f8872c 100644 --- a/internal/graph/mutation.resolvers.go +++ b/internal/graph/mutation.resolvers.go @@ -14,6 +14,26 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.Prof return handleUpdateUserProfile(ctx, changes) } +func (r *mutationResolver) JoinWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleJoinWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) LeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleLeaveWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) CreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) { + return handleCreateWorkgroup(ctx, changes) +} + +func (r *mutationResolver) CloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleCloseWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) UpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { + return handleUpdateWorkgroup(ctx, workgroupID, changes) +} + // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } diff --git a/internal/graph/query.graphql b/internal/graph/query.graphql index b11434c..e4921eb 100644 --- a/internal/graph/query.graphql +++ b/internal/graph/query.graphql @@ -1,12 +1,23 @@ scalar Time type User { + id: ID! name: String email: String! connectedAt: Time! createdAt: Time! + workgroups:[Workgroup]! +} + +type Workgroup { + id: ID! + name: String + createdAt: Time! + closedAt: Time + members: [User]! } type Query { userProfile: User + workgroups: [Workgroup]! } diff --git a/internal/graph/query.resolvers.go b/internal/graph/query.resolvers.go index 33af4b3..3a394aa 100644 --- a/internal/graph/query.resolvers.go +++ b/internal/graph/query.resolvers.go @@ -5,6 +5,7 @@ package graph import ( "context" + "strconv" "forge.cadoles.com/Cadoles/daddy/internal/graph/generated" model1 "forge.cadoles.com/Cadoles/daddy/internal/model" @@ -14,7 +15,37 @@ 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 *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) { + return strconv.FormatUint(uint64(obj.ID), 10), nil +} + +func (r *workgroupResolver) ID(ctx context.Context, obj *model1.Workgroup) (string, error) { + return strconv.FormatUint(uint64(obj.ID), 10), nil +} + // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } +// User returns generated.UserResolver implementation. +func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } + +// Workgroup returns generated.WorkgroupResolver implementation. +func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} } + 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 +} diff --git a/internal/graph/user_profile.go b/internal/graph/user_profile_handler.go similarity index 55% rename from internal/graph/user_profile.go rename to internal/graph/user_profile_handler.go index 8298352..62564ea 100644 --- a/internal/graph/user_profile.go +++ b/internal/graph/user_profile_handler.go @@ -3,26 +3,12 @@ package graph import ( "context" - "forge.cadoles.com/Cadoles/daddy/internal/session" - "forge.cadoles.com/Cadoles/daddy/internal/model" "github.com/pkg/errors" ) func handleUserProfile(ctx context.Context) (*model.User, error) { - db, err := getDB(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - userEmail, err := session.UserEmail(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - repo := model.NewUserRepository(db) - - user, err := repo.FindUserByEmail(ctx, userEmail) + user, _, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) } @@ -31,12 +17,7 @@ func handleUserProfile(ctx context.Context) (*model.User, error) { } func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) { - db, err := getDB(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - userEmail, err := session.UserEmail(ctx) + user, db, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) } @@ -49,7 +30,7 @@ func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) userChanges.Name = changes.Name } - user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges) + user, err = repo.UpdateUserByEmail(ctx, user.Email, userChanges) if err != nil { return nil, errors.WithStack(err) } diff --git a/internal/graph/workgroup_handler.go b/internal/graph/workgroup_handler.go new file mode 100644 index 0000000..286d016 --- /dev/null +++ b/internal/graph/workgroup_handler.go @@ -0,0 +1,134 @@ +package graph + +import ( + "context" + "strconv" + + "forge.cadoles.com/Cadoles/daddy/internal/model" + "github.com/pkg/errors" +) + +func handleWorkgroups(ctx context.Context) ([]*model.Workgroup, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroups, err := repo.FindWorkgroups(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroups, nil +} + +func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + user, db, err := getSessionUser(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.AddUserToWorkgroup(ctx, user.ID, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + user, db, err := getSessionUser(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.CreateWorkgroup(ctx, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.CloseWorkgroup(ctx, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.UpdateWorkgroup(ctx, workgroupID, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func parseWorkgroupID(workgroupID string) (uint, error) { + workgroupID64, err := strconv.ParseUint(workgroupID, 10, 32) + if err != nil { + return 0, errors.WithStack(err) + } + + return uint(workgroupID64), nil +} diff --git a/internal/model/user.go b/internal/model/user.go index f2877b5..79a88e0 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -1,13 +1,17 @@ package model -import "time" +import ( + "time" + + "github.com/jinzhu/gorm" +) type User struct { - ID *uint `gorm:"primary_key"` - Name *string `json:"name"` - Email string `json:"email" gorm:"unique;not null"` - ConnectedAt time.Time `json:"connectedAt"` - CreatedAt time.Time `json:"createdAt"` + gorm.Model + Name *string `json:"name"` + Email string `json:"email" gorm:"unique;not null"` + ConnectedAt time.Time `json:"connectedAt"` + Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"` } type ProfileChanges struct { diff --git a/internal/model/user_repository.go b/internal/model/user_repository.go index ec24928..f84a70c 100644 --- a/internal/model/user_repository.go +++ b/internal/model/user_repository.go @@ -15,8 +15,7 @@ type UserRepository struct { func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) { user := &User{ - Email: email, - CreatedAt: time.Now(), + Email: email, } err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { @@ -44,7 +43,7 @@ func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*Us Email: email, } - err := r.db.First(user, "email = ?", email).Error + err := r.db.Model(user).Preload("Workgroups").First(user, "email = ?", email).Error if err != nil { return nil, errors.Wrap(err, "could not find user") } diff --git a/internal/model/workgroup.go b/internal/model/workgroup.go new file mode 100644 index 0000000..64b2c0a --- /dev/null +++ b/internal/model/workgroup.go @@ -0,0 +1,18 @@ +package model + +import ( + "time" + + "github.com/jinzhu/gorm" +) + +type Workgroup struct { + gorm.Model + Name *string `json:"name"` + ClosedAt time.Time `json:"closedAt"` + Members []*User `gorm:"many2many:users_workgroups;"` +} + +type WorkgroupChanges struct { + Name *string `json:"name"` +} diff --git a/internal/model/workgroup_repository.go b/internal/model/workgroup_repository.go new file mode 100644 index 0000000..283c1fe --- /dev/null +++ b/internal/model/workgroup_repository.go @@ -0,0 +1,140 @@ +package model + +import ( + "context" + "time" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" +) + +type WorkgroupRepository struct { + db *gorm.DB +} + +func (r *WorkgroupRepository) FindWorkgroups(ctx context.Context, criteria ...interface{}) ([]*Workgroup, error) { + workgroups := make([]*Workgroup, 0) + if err := r.db.Model(&Workgroup{}).Preload("Members").Find(&workgroups, criteria...).Error; err != nil { + return nil, errors.WithStack(err) + } + + return workgroups, nil +} + +func (r *WorkgroupRepository) UpdateWorkgroup(ctx context.Context, workgroupID uint, changes WorkgroupChanges) (*Workgroup, error) { + workgroup := &Workgroup{ + Name: changes.Name, + } + workgroup.ID = workgroupID + + err := r.db.Model(workgroup). + Update(workgroup). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) CreateWorkgroup(ctx context.Context, changes WorkgroupChanges) (*Workgroup, error) { + workgroup := &Workgroup{ + Name: changes.Name, + } + + if err := r.db.Model(&Workgroup{}).Create(workgroup).Error; err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) CloseWorkgroup(ctx context.Context, workgroupID uint) (*Workgroup, error) { + workgroup := &Workgroup{} + + err := r.db.Model(workgroup). + Where("id = ?", workgroupID). + UpdateColumn("closedAt", time.Now()). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) { + user := &User{} + + err := r.db.Model(user).Preload("Workgroups").First(user, "id = ?", userID).Error + if err != nil { + return nil, errors.Wrap(err, "could not find user") + } + + workgroup := &Workgroup{} + workgroup.ID = workgroupID + + err = r.db.Model(user). + Association("Workgroups"). + Append(workgroup). + Error + + if err != nil { + return nil, errors.Wrap(err, "could not add user to workgroup") + } + + err = r.db.Model(workgroup). + Preload("Members"). + First(workgroup, "id = ?", workgroupID). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +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.ID = workgroupID + + err = r.db.Model(user). + Association("Workgroups"). + Delete(workgroup). + Error + + if err != nil { + return nil, errors.Wrap(err, "could not add user to workgroup") + } + + err = r.db.Model(workgroup). + Preload("Members"). + First(workgroup, "id = ?", workgroupID). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository { + return &WorkgroupRepository{db} +} From 8708e300206a71c9c2615c667c89f84d2bdca52e Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 21 Jul 2020 18:12:02 +0200 Subject: [PATCH 3/7] Interface de gestion des groupes de travail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Récupération et affichage des groupes existants - Création d'un nouveau groupe - Modification d'un groupe existant - Rejoindre/quitter un groupe de travail --- client/package-lock.json | 6 +- client/package.json | 2 +- client/src/components/HomePage/Dashboard.tsx | 19 ++-- .../components/HomePage/WorkgroupsPanel.tsx | 96 +++++++++++++++++++ client/src/store/actions/workgroups.ts | 19 ++++ client/src/store/reducers/auth.ts | 1 + client/src/store/reducers/root.ts | 3 + client/src/store/reducers/workgroups.ts | 38 ++++++++ client/src/store/sagas/root.ts | 2 + client/src/store/sagas/workgroups.ts | 25 +++++ client/src/types/user.ts | 1 + client/src/types/workgroup.ts | 9 ++ client/src/util/daddy.ts | 19 ++++ 13 files changed, 224 insertions(+), 16 deletions(-) create mode 100644 client/src/components/HomePage/WorkgroupsPanel.tsx create mode 100644 client/src/store/actions/workgroups.ts create mode 100644 client/src/store/reducers/workgroups.ts create mode 100644 client/src/store/sagas/workgroups.ts create mode 100644 client/src/types/workgroup.ts diff --git a/client/package-lock.json b/client/package-lock.json index 635b1c5..e23fc13 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3235,9 +3235,9 @@ "dev": true }, "bulma": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz", - "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz", + "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==" }, "bulma-switch": { "version": "2.0.0", diff --git a/client/package.json b/client/package.json index d5cf484..a5edbbc 100644 --- a/client/package.json +++ b/client/package.json @@ -58,7 +58,7 @@ "apollo-link-http": "^1.5.17", "apollo-link-ws": "^1.0.20", "apollo-utilities": "^1.3.4", - "bulma": "^0.7.2", + "bulma": "^0.9.0", "bulma-switch": "^2.0.0", "graphql": "^15.3.0", "graphql-request": "^2.0.0", diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/HomePage/Dashboard.tsx index 408087a..925ae03 100644 --- a/client/src/components/HomePage/Dashboard.tsx +++ b/client/src/components/HomePage/Dashboard.tsx @@ -1,25 +1,20 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchWorkgroups } from '../../store/actions/workgroups'; +import { RootState } from '../../store/reducers/root'; +import { WorkgroupsPanel } from './WorkgroupsPanel'; export function Dashboard() { return (
-
-
-
-

Groupes de travail

-
-
- -
-
-
+
-

D.à.Ds

+

D.A.Ds

diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx new file mode 100644 index 0000000..5e6f48a --- /dev/null +++ b/client/src/components/HomePage/WorkgroupsPanel.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../store/reducers/root'; +import { fetchWorkgroups } from '../../store/actions/workgroups'; +import { Workgroup } from '../../types/workgroup'; +import { User } from '../../types/user'; +import { Link } from 'react-router-dom'; + +export function WorkgroupsPanel() { + const dispatch = useDispatch(); + const workgroups = useSelector(state => state.workgroups.workgroupsById); + const currentUserId = useSelector(state => state.auth.currentUser.id); + + const filterTabs = [ + { + label: "Mes groupes", + filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => { + return wg.members.some((u: User) => u.id === currentUserId); + }) + }, + { + label: "Ouverts", + filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !wg.closedAt) + }, + { + label: "Clôs", + filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !!wg.closedAt) + } + ]; + + const [ state, setState ] = useState({ selectedTab: 0 }); + + const selectTab = (tabIndex: number) => { + setState(state => ({ ...state, selectedTab: tabIndex })); + } + + useEffect(() => { + dispatch(fetchWorkgroups()); + }, []); + + const workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { + return ( + + + + + {wg.name} + + ); + }) + + return ( + + ) +} \ No newline at end of file diff --git a/client/src/store/actions/workgroups.ts b/client/src/store/actions/workgroups.ts new file mode 100644 index 0000000..6894390 --- /dev/null +++ b/client/src/store/actions/workgroups.ts @@ -0,0 +1,19 @@ +import { Action } from "redux"; +import { Workgroup } from "../../types/workgroup"; + +export const FETCH_WORKGROUPS_REQUEST = 'FETCH_WORKGROUPS_REQUEST'; +export const FETCH_WORKGROUPS_SUCCESS = 'FETCH_WORKGROUPS_SUCCESS'; +export const FETCH_WORKGROUPS_FAILURE = 'FETCH_WORKGROUPS_FAILURE'; + +export interface fetchWorkgroupsRequestAction extends Action { + +} + +export interface fetchWorkgroupsSuccessAction extends Action { + workgroups: [Workgroup] +} + + +export function fetchWorkgroups(): fetchWorkgroupsRequestAction { + return { type: FETCH_WORKGROUPS_REQUEST } +} \ No newline at end of file diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts index 2192326..c37ba47 100644 --- a/client/src/store/reducers/auth.ts +++ b/client/src/store/reducers/auth.ts @@ -31,6 +31,7 @@ function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction) ...state, isAuthenticated: true, currentUser: { + id: '', email } }; diff --git a/client/src/store/reducers/root.ts b/client/src/store/reducers/root.ts index 5835800..337b8b0 100644 --- a/client/src/store/reducers/root.ts +++ b/client/src/store/reducers/root.ts @@ -1,13 +1,16 @@ import { combineReducers } from 'redux'; import { flagsReducer, FlagsState } from './flags'; import { authReducer, AuthState } from './auth'; +import { workgroupsReducer, WorkgroupsState } from './workgroups'; export interface RootState { auth: AuthState, flags: FlagsState, + workgroups: WorkgroupsState, } export const rootReducer = combineReducers({ flags: flagsReducer, auth: authReducer, + workgroups: workgroupsReducer, }); \ No newline at end of file diff --git a/client/src/store/reducers/workgroups.ts b/client/src/store/reducers/workgroups.ts new file mode 100644 index 0000000..c692167 --- /dev/null +++ b/client/src/store/reducers/workgroups.ts @@ -0,0 +1,38 @@ +import { Action } from "redux"; +import { User } from "../../types/user"; +import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; +import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile"; +import { Workgroup } from "../../types/workgroup"; +import { FETCH_WORKGROUPS_SUCCESS, fetchWorkgroupsSuccessAction } from "../actions/workgroups"; + +export interface WorkgroupsState { + workgroupsById: { [id in string]: Workgroup } +} + +const defaultState = { + workgroupsById: {} +}; + +export function workgroupsReducer(state = defaultState, action: Action): WorkgroupsState { + switch (action.type) { + case FETCH_WORKGROUPS_SUCCESS: + return handleFetchWorkgroups(state, action as fetchWorkgroupsSuccessAction); + + } + return state; +} + +function handleFetchWorkgroups(state: WorkgroupsState, { workgroups }: fetchWorkgroupsSuccessAction): WorkgroupsState { + const workgroupsById = { + ...state.workgroupsById, + }; + + workgroups.forEach(wg => { + workgroupsById[wg.id] = wg; + }); + + return { + ...state, + workgroupsById, + }; +}; \ No newline at end of file diff --git a/client/src/store/sagas/root.ts b/client/src/store/sagas/root.ts index 6a1c770..b09eca5 100644 --- a/client/src/store/sagas/root.ts +++ b/client/src/store/sagas/root.ts @@ -2,11 +2,13 @@ import { all } from 'redux-saga/effects'; import { failureRootSaga } from './failure'; import { initRootSaga } from './init'; import { profileRootSaga } from './profile'; +import { workgroupsRootSaga } from './workgroups'; export function* rootSaga() { yield all([ initRootSaga(), failureRootSaga(), profileRootSaga(), + workgroupsRootSaga(), ]); } diff --git a/client/src/store/sagas/workgroups.ts b/client/src/store/sagas/workgroups.ts new file mode 100644 index 0000000..a6d9cc3 --- /dev/null +++ b/client/src/store/sagas/workgroups.ts @@ -0,0 +1,25 @@ +import { getClient } from "../../util/daddy"; +import { Config } from "../../config"; +import { all, takeLatest, put } from "redux-saga/effects"; +import { FETCH_WORKGROUPS_SUCCESS, FETCH_WORKGROUPS_FAILURE, FETCH_WORKGROUPS_REQUEST } from "../actions/workgroups"; +import { Workgroup } from "../../types/workgroup"; + +export function* workgroupsRootSaga() { + yield all([ + takeLatest(FETCH_WORKGROUPS_REQUEST, fetchWorkgroupsSaga), + ]); +} + +export function* fetchWorkgroupsSaga() { + const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); + + let workgroups: [Workgroup]; + try { + workgroups = yield client.fetchWorkgroups().then(result => result.workgroups); + } catch(err) { + yield put({ type: FETCH_WORKGROUPS_FAILURE, err }); + return; + } + + yield put({type: FETCH_WORKGROUPS_SUCCESS, workgroups }); +} \ No newline at end of file diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 4fc773d..2fae681 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -1,4 +1,5 @@ export interface User { + id: string email: string name?: string connectedAt?: Date diff --git a/client/src/types/workgroup.ts b/client/src/types/workgroup.ts new file mode 100644 index 0000000..459f29a --- /dev/null +++ b/client/src/types/workgroup.ts @@ -0,0 +1,9 @@ +import { User } from "./user"; + +export interface Workgroup { + id: string + name: string + createdAt: Date + closedAt: Date + members: [User] +} \ No newline at end of file diff --git a/client/src/util/daddy.ts b/client/src/util/daddy.ts index 8c439fd..aa48178 100644 --- a/client/src/util/daddy.ts +++ b/client/src/util/daddy.ts @@ -79,6 +79,7 @@ export class DaddyClient { query: gql` query { userProfile { + id, name, email, createdAt, @@ -89,6 +90,24 @@ export class DaddyClient { .then(this.assertAuthorization) } + fetchWorkgroups() { + return this.gql.query({ + query: gql` + query { + workgroups { + id, + name, + createdAt, + closedAt, + members { + id + } + } + }` + }) + .then(this.assertAuthorization) + } + updateProfile(changes: ProfileChanges) { return this.gql.mutate({ variables: { From c4373cce465fde5eb8fa2873ac2f806a83c0bce1 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 21 Jul 2020 22:25:39 +0200 Subject: [PATCH 4/7] Remplacement de Redux/Saga par Apollo --- client/package-lock.json | 173 ++++-------------- client/package.json | 12 +- client/src/components/App.tsx | 18 +- client/src/components/HomePage/Dashboard.tsx | 5 +- client/src/components/HomePage/HomePage.tsx | 20 +- .../components/HomePage/WorkgroupsPanel.tsx | 41 +++-- client/src/components/Navbar.tsx | 46 ++--- .../components/ProfilePage/ProfilePage.tsx | 23 ++- client/src/components/UserForm.tsx | 10 +- client/src/components/WithLoader.tsx | 22 +++ client/src/gql/client.tsx | 21 +++ client/src/gql/mutations/profile.tsx | 15 ++ client/src/gql/queries/profile.tsx | 16 ++ client/src/gql/queries/workgroups.tsx | 20 ++ client/src/index.tsx | 7 +- client/src/sass/_all.scss | 1 - client/src/store/actions/auth.ts | 11 -- client/src/store/actions/profile.ts | 40 ---- client/src/store/actions/workgroups.ts | 19 -- client/src/store/reducers/auth.ts | 58 ------ client/src/store/reducers/flags.ts | 32 ---- client/src/store/reducers/root.ts | 16 -- client/src/store/reducers/workgroups.ts | 38 ---- client/src/store/sagas/failure.ts | 21 --- client/src/store/sagas/init.ts | 12 -- client/src/store/sagas/profile.ts | 46 ----- client/src/store/sagas/root.ts | 14 -- client/src/store/sagas/workgroups.ts | 25 --- client/src/store/selectors/flags.ts | 7 - client/src/store/store.ts | 30 --- client/src/util/daddy.ts | 134 -------------- internal/route/mount.go | 1 - internal/session/user_email.go | 4 +- 33 files changed, 230 insertions(+), 728 deletions(-) create mode 100644 client/src/components/WithLoader.tsx create mode 100644 client/src/gql/client.tsx create mode 100644 client/src/gql/mutations/profile.tsx create mode 100644 client/src/gql/queries/profile.tsx create mode 100644 client/src/gql/queries/workgroups.tsx delete mode 100644 client/src/store/actions/auth.ts delete mode 100644 client/src/store/actions/profile.ts delete mode 100644 client/src/store/actions/workgroups.ts delete mode 100644 client/src/store/reducers/auth.ts delete mode 100644 client/src/store/reducers/flags.ts delete mode 100644 client/src/store/reducers/root.ts delete mode 100644 client/src/store/reducers/workgroups.ts delete mode 100644 client/src/store/sagas/failure.ts delete mode 100644 client/src/store/sagas/init.ts delete mode 100644 client/src/store/sagas/profile.ts delete mode 100644 client/src/store/sagas/root.ts delete mode 100644 client/src/store/sagas/workgroups.ts delete mode 100644 client/src/store/selectors/flags.ts delete mode 100644 client/src/store/store.ts delete mode 100644 client/src/util/daddy.ts diff --git a/client/package-lock.json b/client/package-lock.json index e23fc13..8552a0f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4,6 +4,43 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apollo/client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.0.2.tgz", + "integrity": "sha512-4ighan5Anlj4tK/tdUHs4Mi1njqXZ7AxRCVolz/H702DjPphAJfm+FRkIadPTmwz+OLO+d+tX+6V1VBshf02rg==", + "requires": { + "@types/zen-observable": "^0.8.0", + "@wry/context": "^0.5.2", + "@wry/equality": "^0.1.9", + "fast-json-stable-stringify": "^2.0.0", + "graphql-tag": "^2.10.4", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.12.1", + "prop-types": "^15.7.2", + "symbol-observable": "^1.2.0", + "ts-invariant": "^0.4.4", + "tslib": "^1.10.0", + "zen-observable": "^0.8.14" + }, + "dependencies": { + "@wry/context": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz", + "integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==", + "requires": { + "tslib": "^1.9.3" + } + }, + "optimism": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.12.1.tgz", + "integrity": "sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==", + "requires": { + "@wry/context": "^0.5.2" + } + } + } + }, "@babel/code-frame": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", @@ -1230,7 +1267,8 @@ "@types/node": { "version": "13.13.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz", - "integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==" + "integrity": "sha512-zWz/8NEPxoXNT9YyF2osqyA9WjssZukYpgI4UYZpOjcyqwIUqWGkcCionaEb9Ki+FULyPyvNFpg/329Kd2/pbw==", + "dev": true }, "@types/prop-types": { "version": "15.7.3", @@ -1551,15 +1589,6 @@ "@xtuc/long": "4.2.2" } }, - "@wry/context": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", - "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", - "requires": { - "@types/node": ">=6", - "tslib": "^1.9.3" - } - }, "@wry/equality": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", @@ -1744,93 +1773,6 @@ } } }, - "apollo-cache": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz", - "integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==", - "requires": { - "apollo-utilities": "^1.3.4", - "tslib": "^1.10.0" - } - }, - "apollo-cache-inmemory": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.6.tgz", - "integrity": "sha512-L8pToTW/+Xru2FFAhkZ1OA9q4V4nuvfoPecBM34DecAugUZEBhI2Hmpgnzq2hTKZ60LAMrlqiASm0aqAY6F8/A==", - "requires": { - "apollo-cache": "^1.3.5", - "apollo-utilities": "^1.3.4", - "optimism": "^0.10.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0" - } - }, - "apollo-client": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz", - "integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==", - "requires": { - "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.3.5", - "apollo-link": "^1.0.0", - "apollo-utilities": "1.3.4", - "symbol-observable": "^1.0.2", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0", - "zen-observable": "^0.8.0" - } - }, - "apollo-link": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", - "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", - "requires": { - "apollo-utilities": "^1.3.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3", - "zen-observable-ts": "^0.8.21" - } - }, - "apollo-link-http": { - "version": "1.5.17", - "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", - "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", - "requires": { - "apollo-link": "^1.2.14", - "apollo-link-http-common": "^0.2.16", - "tslib": "^1.9.3" - } - }, - "apollo-link-http-common": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", - "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", - "requires": { - "apollo-link": "^1.2.14", - "ts-invariant": "^0.4.0", - "tslib": "^1.9.3" - } - }, - "apollo-link-ws": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz", - "integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==", - "requires": { - "apollo-link": "^1.2.14", - "tslib": "^1.9.3" - } - }, - "apollo-utilities": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", - "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", - "requires": { - "@wry/equality": "^0.1.2", - "fast-json-stable-stringify": "^2.0.0", - "ts-invariant": "^0.4.0", - "tslib": "^1.10.0" - } - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -3239,11 +3181,6 @@ "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz", "integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ==" }, - "bulma-switch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz", - "integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA==" - }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -5615,11 +5552,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==" }, - "graphql-request": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz", - "integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ==" - }, "graphql-tag": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz", @@ -6507,11 +6439,6 @@ "verror": "1.10.0" } }, - "jwt-decode": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", - "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" - }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -7466,14 +7393,6 @@ "is-wsl": "^1.1.0" } }, - "optimism": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", - "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", - "requires": { - "@wry/context": "^0.4.0" - } - }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -8048,11 +7967,6 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" - }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -11006,15 +10920,6 @@ "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, - "zen-observable-ts": { - "version": "0.8.21", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", - "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", - "requires": { - "tslib": "^1.9.3", - "zen-observable": "^0.8.0" - } } } } diff --git a/client/package.json b/client/package.json index a5edbbc..ad76110 100644 --- a/client/package.json +++ b/client/package.json @@ -51,20 +51,10 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { + "@apollo/client": "^3.0.2", "@types/qs": "^6.9.3", - "apollo-cache-inmemory": "^1.6.6", - "apollo-client": "^2.6.10", - "apollo-link": "^1.2.14", - "apollo-link-http": "^1.5.17", - "apollo-link-ws": "^1.0.20", - "apollo-utilities": "^1.3.4", "bulma": "^0.9.0", - "bulma-switch": "^2.0.0", "graphql": "^15.3.0", - "graphql-request": "^2.0.0", - "graphql-tag": "^2.10.4", - "jwt-decode": "^2.2.0", - "qs": "^6.9.4", "react": "^16.12.0", "react-dom": "^16.12.0", "react-redux": "^7.1.3", diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx index ec0367f..af9e243 100644 --- a/client/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -1,22 +1,18 @@ import React from 'react'; import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { HomePage } from './HomePage/HomePage'; -import { store } from '../store/store'; -import { Provider } from 'react-redux'; import { ProfilePage } from './ProfilePage/ProfilePage'; export class App extends React.Component { render() { return ( - - - - - - } /> - - - + + + + + } /> + + ); } } \ No newline at end of file diff --git a/client/src/components/HomePage/Dashboard.tsx b/client/src/components/HomePage/Dashboard.tsx index 925ae03..916fba1 100644 --- a/client/src/components/HomePage/Dashboard.tsx +++ b/client/src/components/HomePage/Dashboard.tsx @@ -1,7 +1,4 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchWorkgroups } from '../../store/actions/workgroups'; -import { RootState } from '../../store/reducers/root'; +import React from 'react'; import { WorkgroupsPanel } from './WorkgroupsPanel'; export function Dashboard() { diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index 5521d49..bd01c65 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -1,18 +1,26 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Page } from '../Page'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../store/reducers/root'; import { Dashboard } from './Dashboard'; +import { useUserProfileQuery } from '../../gql/queries/profile'; +import { Loader } from '../Loader'; export function HomePage() { - const currentUser = useSelector((state: RootState) => state.auth.currentUser); + const { data, loading } = useUserProfileQuery(); + + if (loading) { + return ( + + ); + } + + const { userProfile } = (data || {}); return ( - +
{ - currentUser ? + userProfile ? :
diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx index 5e6f48a..6648bc0 100644 --- a/client/src/components/HomePage/WorkgroupsPanel.tsx +++ b/client/src/components/HomePage/WorkgroupsPanel.tsx @@ -1,44 +1,49 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../store/reducers/root'; -import { fetchWorkgroups } from '../../store/actions/workgroups'; import { Workgroup } from '../../types/workgroup'; import { User } from '../../types/user'; import { Link } from 'react-router-dom'; +import { useWorkgroupsQuery } from '../../gql/queries/workgroups'; +import { useUserProfileQuery } from '../../gql/queries/profile'; +import { Loader } from '../Loader'; export function WorkgroupsPanel() { - const dispatch = useDispatch(); - const workgroups = useSelector(state => state.workgroups.workgroupsById); - const currentUserId = useSelector(state => state.auth.currentUser.id); + const workgroupsQuery = useWorkgroupsQuery(); + const userProfileQuery = useUserProfileQuery(); + const [ state, setState ] = useState({ selectedTab: 0 }); + + const isLoading = userProfileQuery.loading || workgroupsQuery.loading; + if (isLoading) { + return ; + } + + let { data: { userProfile }} = userProfileQuery; + let { data: { workgroups }} = workgroupsQuery; const filterTabs = [ { label: "Mes groupes", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => { - return wg.members.some((u: User) => u.id === currentUserId); + filter: workgroups => workgroups.filter((wg: Workgroup) => { + return wg.members.some((u: User) => u.id === userProfile.id); }) }, { label: "Ouverts", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !wg.closedAt) + filter: workgroups => workgroups.filter((wg: Workgroup) => !wg.closedAt) }, { label: "Clôs", - filter: workgroups => Object.values(workgroups).filter((wg: Workgroup) => !!wg.closedAt) + filter: workgroups => workgroups.filter((wg: Workgroup) => !!wg.closedAt) } ]; - const [ state, setState ] = useState({ selectedTab: 0 }); - const selectTab = (tabIndex: number) => { setState(state => ({ ...state, selectedTab: tabIndex })); } + - useEffect(() => { - dispatch(fetchWorkgroups()); - }, []); - - const workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { + let workgroupsItems = []; + + workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { return ( @@ -47,7 +52,7 @@ export function WorkgroupsPanel() { {wg.name} ); - }) + }); return (
diff --git a/client/src/components/UserForm.tsx b/client/src/components/UserForm.tsx index 1ba6c7a..8e94f4e 100644 --- a/client/src/components/UserForm.tsx +++ b/client/src/components/UserForm.tsx @@ -10,11 +10,11 @@ export function UserForm({ user, onChange }: UserFormProps) { const [ state, setState ] = useState({ changed: false, user: { - name: '', - email: '', - createdAt: null, - connectedAt: null, - ...user, + id: user && user.id ? user.id : '', + name: user && user.name ? user.name : '', + email: user && user.email ? user.email : '', + createdAt: user && user.createdAt ? user.createdAt : null, + connectedAt: user && user.connectedAt ? user.connectedAt : null, } }); diff --git a/client/src/components/WithLoader.tsx b/client/src/components/WithLoader.tsx new file mode 100644 index 0000000..33f5441 --- /dev/null +++ b/client/src/components/WithLoader.tsx @@ -0,0 +1,22 @@ +import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react'; + +export interface WithLoaderProps { + loading?: boolean +} + +export const WithLoader: FunctionComponent = ({ loading, children }) => { + return ( + + { + loading ? +
+
+
+
+
+
: + children + } +
+ ) +} \ No newline at end of file diff --git a/client/src/gql/client.tsx b/client/src/gql/client.tsx new file mode 100644 index 0000000..bbe3c37 --- /dev/null +++ b/client/src/gql/client.tsx @@ -0,0 +1,21 @@ +import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; +import { Config } from '../config'; +import { WebSocketLink } from "@apollo/client/link/ws"; +import { RetryLink } from "@apollo/client/link/retry"; +import { SubscriptionClient } from "subscriptions-transport-ws"; + + +const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, { + reconnect: true +}); + +const link = new RetryLink().split( + (operation) => operation.operationName === 'subscription', + new WebSocketLink(subscriptionClient), + new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' }) +); + +export const client = new ApolloClient({ + cache: new InMemoryCache(), + link: link, +}); \ No newline at end of file diff --git a/client/src/gql/mutations/profile.tsx b/client/src/gql/mutations/profile.tsx new file mode 100644 index 0000000..9a681a4 --- /dev/null +++ b/client/src/gql/mutations/profile.tsx @@ -0,0 +1,15 @@ +import { gql, useQuery, useMutation } from '@apollo/client'; + +const MUTATION_UPDATE_USER_PROFILE = gql` +mutation updateUserProfile($changes: ProfileChanges!) { + updateProfile(changes: $changes) { + id, + name, + createdAt, + connectedAt, + } +}`; + +export function useUpdateUserProfileMutation() { + return useMutation(MUTATION_UPDATE_USER_PROFILE); +} \ No newline at end of file diff --git a/client/src/gql/queries/profile.tsx b/client/src/gql/queries/profile.tsx new file mode 100644 index 0000000..128c394 --- /dev/null +++ b/client/src/gql/queries/profile.tsx @@ -0,0 +1,16 @@ +import { gql, useQuery } from '@apollo/client'; + +const QUERY_USER_PROFILE = gql` +query userProfile { + userProfile { + id, + name, + email, + createdAt, + connectedAt + } +}`; + +export function useUserProfileQuery() { + return useQuery(QUERY_USER_PROFILE, { fetchPolicy: "network-only" }); +} \ No newline at end of file diff --git a/client/src/gql/queries/workgroups.tsx b/client/src/gql/queries/workgroups.tsx new file mode 100644 index 0000000..e8c7d7a --- /dev/null +++ b/client/src/gql/queries/workgroups.tsx @@ -0,0 +1,20 @@ +import { gql, useQuery } from '@apollo/client'; + +const QUERY_WORKGROUP = gql` + query workgroups { + workgroups { + id, + name, + createdAt, + closedAt, + members { + id, + email + } + } + } +`; + +export function useWorkgroupsQuery(options = {}) { + return useQuery(QUERY_WORKGROUP, options); +} \ No newline at end of file diff --git a/client/src/index.tsx b/client/src/index.tsx index 9dde128..cee36c1 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -2,15 +2,18 @@ import './sass/_all.scss'; import React from 'react'; import ReactDOM from 'react-dom'; import { App } from './components/App'; -import { Config } from './config'; +import { client } from './gql/client'; import '@fortawesome/fontawesome-free/js/fontawesome' import '@fortawesome/fontawesome-free/js/solid' import '@fortawesome/fontawesome-free/js/regular' import '@fortawesome/fontawesome-free/js/brands' import './resources/favicon.png'; +import { ApolloProvider } from '@apollo/client'; ReactDOM.render( - , + + + , document.getElementById('app') ); diff --git a/client/src/sass/_all.scss b/client/src/sass/_all.scss index 8f13fd0..c08fa8c 100644 --- a/client/src/sass/_all.scss +++ b/client/src/sass/_all.scss @@ -1,4 +1,3 @@ @import 'bulma/bulma.sass'; -@import 'bulma-switch/dist/css/bulma-switch.sass'; @import '_base.scss'; @import '_loader.scss'; \ No newline at end of file diff --git a/client/src/store/actions/auth.ts b/client/src/store/actions/auth.ts deleted file mode 100644 index 8a8c608..0000000 --- a/client/src/store/actions/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Action } from "redux"; - -export const SET_CURRENT_USER = 'SET_CURRENT_USER'; - -export interface setCurrentUserAction extends Action { - email: string -} - -export function setCurrentUser(email: string): setCurrentUserAction { - return { type: SET_CURRENT_USER, email }; -} \ No newline at end of file diff --git a/client/src/store/actions/profile.ts b/client/src/store/actions/profile.ts deleted file mode 100644 index 8e51900..0000000 --- a/client/src/store/actions/profile.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; - -export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST'; -export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS'; -export const FETCH_PROFILE_FAILURE = 'FETCH_PROFILE_FAILURE'; - -export interface fetchProfileRequestAction extends Action { - -} - -export interface fetchProfileSuccessAction extends Action { - profile: User -} - - -export function fetchProfile(): fetchProfileRequestAction { - return { type: FETCH_PROFILE_REQUEST } -} - -export const UPDATE_PROFILE_REQUEST = 'UPDATE_PROFILE_REQUEST'; -export const UPDATE_PROFILE_SUCCESS = 'UPDATE_PROFILE_SUCCESS'; -export const UPDATE_PROFILE_FAILURE = 'UPDATE_PROFILE_FAILURE'; - -export interface ProfileChanges { - name?: string -} - -export interface updateProfileRequestAction extends Action { - changes: ProfileChanges -} - -export interface updateProfileSuccessAction extends Action { - profile: User -} - - -export function updateProfile(changes: ProfileChanges): updateProfileRequestAction { - return { type: UPDATE_PROFILE_REQUEST, changes } -} \ No newline at end of file diff --git a/client/src/store/actions/workgroups.ts b/client/src/store/actions/workgroups.ts deleted file mode 100644 index 6894390..0000000 --- a/client/src/store/actions/workgroups.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Action } from "redux"; -import { Workgroup } from "../../types/workgroup"; - -export const FETCH_WORKGROUPS_REQUEST = 'FETCH_WORKGROUPS_REQUEST'; -export const FETCH_WORKGROUPS_SUCCESS = 'FETCH_WORKGROUPS_SUCCESS'; -export const FETCH_WORKGROUPS_FAILURE = 'FETCH_WORKGROUPS_FAILURE'; - -export interface fetchWorkgroupsRequestAction extends Action { - -} - -export interface fetchWorkgroupsSuccessAction extends Action { - workgroups: [Workgroup] -} - - -export function fetchWorkgroups(): fetchWorkgroupsRequestAction { - return { type: FETCH_WORKGROUPS_REQUEST } -} \ No newline at end of file diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts deleted file mode 100644 index c37ba47..0000000 --- a/client/src/store/reducers/auth.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; -import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; -import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile"; - -export interface AuthState { - isAuthenticated: boolean - currentUser: User -} - -const defaultState = { - isAuthenticated: false, - currentUser: null, -}; - -export function authReducer(state = defaultState, action: Action): AuthState { - switch (action.type) { - case SET_CURRENT_USER: - return handleSetCurrentUser(state, action as setCurrentUserAction); - case FETCH_PROFILE_SUCCESS: - return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction); - case UPDATE_PROFILE_SUCCESS: - return handleFetchProfileSuccess(state, action as updateProfileSuccessAction); - - } - return state; -} - -function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - id: '', - email - } - }; -}; - -function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - ...profile, - } - }; -}; - -function handleUpdateProfileSuccess(state: AuthState, { profile }: updateProfileSuccessAction): AuthState { - return { - ...state, - isAuthenticated: true, - currentUser: { - ...profile, - } - }; -}; \ No newline at end of file diff --git a/client/src/store/reducers/flags.ts b/client/src/store/reducers/flags.ts deleted file mode 100644 index f42701b..0000000 --- a/client/src/store/reducers/flags.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Action } from "redux"; - -export interface FlagsState { - actions: { [actionName: string]: ActionState } -} - -export interface ActionState { - isLoading: boolean -} - -const defaultState = { - actions: {} -}; - -export function flagsReducer(state = defaultState, action: Action): FlagsState { - const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type); - - if(!matches) return state; - - const actionPrefix = matches[1]; - - return { - ...state, - actions: { - ...state.actions, - [actionPrefix]: { - isLoading: matches[2] === 'REQUEST' - } - } - }; - -} \ No newline at end of file diff --git a/client/src/store/reducers/root.ts b/client/src/store/reducers/root.ts deleted file mode 100644 index 337b8b0..0000000 --- a/client/src/store/reducers/root.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { combineReducers } from 'redux'; -import { flagsReducer, FlagsState } from './flags'; -import { authReducer, AuthState } from './auth'; -import { workgroupsReducer, WorkgroupsState } from './workgroups'; - -export interface RootState { - auth: AuthState, - flags: FlagsState, - workgroups: WorkgroupsState, -} - -export const rootReducer = combineReducers({ - flags: flagsReducer, - auth: authReducer, - workgroups: workgroupsReducer, -}); \ No newline at end of file diff --git a/client/src/store/reducers/workgroups.ts b/client/src/store/reducers/workgroups.ts deleted file mode 100644 index c692167..0000000 --- a/client/src/store/reducers/workgroups.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Action } from "redux"; -import { User } from "../../types/user"; -import { SET_CURRENT_USER, setCurrentUserAction } from "../actions/auth"; -import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction, updateProfileSuccessAction, UPDATE_PROFILE_SUCCESS, updateProfileRequestAction } from "../actions/profile"; -import { Workgroup } from "../../types/workgroup"; -import { FETCH_WORKGROUPS_SUCCESS, fetchWorkgroupsSuccessAction } from "../actions/workgroups"; - -export interface WorkgroupsState { - workgroupsById: { [id in string]: Workgroup } -} - -const defaultState = { - workgroupsById: {} -}; - -export function workgroupsReducer(state = defaultState, action: Action): WorkgroupsState { - switch (action.type) { - case FETCH_WORKGROUPS_SUCCESS: - return handleFetchWorkgroups(state, action as fetchWorkgroupsSuccessAction); - - } - return state; -} - -function handleFetchWorkgroups(state: WorkgroupsState, { workgroups }: fetchWorkgroupsSuccessAction): WorkgroupsState { - const workgroupsById = { - ...state.workgroupsById, - }; - - workgroups.forEach(wg => { - workgroupsById[wg.id] = wg; - }); - - return { - ...state, - workgroupsById, - }; -}; \ No newline at end of file diff --git a/client/src/store/sagas/failure.ts b/client/src/store/sagas/failure.ts deleted file mode 100644 index de90822..0000000 --- a/client/src/store/sagas/failure.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UnauthorizedError } from "../../util/daddy"; -import { all, takeEvery } from 'redux-saga/effects'; - -export function* failureRootSaga() { - yield all([ - takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga), - ]); -} - -export function* failuresSaga(action) { - if (action.error instanceof UnauthorizedError) { - // TODO Implements better authorization error handling - window.location.reload(); - } -} - -export function patternFromRegExp(re: any) { - return (action: any) => { - return re.test(action.type); - }; -} \ No newline at end of file diff --git a/client/src/store/sagas/init.ts b/client/src/store/sagas/init.ts deleted file mode 100644 index b809870..0000000 --- a/client/src/store/sagas/init.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { all, put } from "redux-saga/effects"; -import { fetchProfile } from "../actions/profile"; - -export function* initRootSaga() { - yield all([ - fetchUserProfileSaga(), - ]); -} - -export function* fetchUserProfileSaga() { - yield put(fetchProfile()); -} \ No newline at end of file diff --git a/client/src/store/sagas/profile.ts b/client/src/store/sagas/profile.ts deleted file mode 100644 index 9e0864a..0000000 --- a/client/src/store/sagas/profile.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { DaddyClient, getClient } from "../../util/daddy"; -import { Config } from "../../config"; -import { all, takeLatest, put, select } from "redux-saga/effects"; -import { FETCH_PROFILE_REQUEST, fetchProfile, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS, updateProfileRequestAction, UPDATE_PROFILE_REQUEST, UPDATE_PROFILE_FAILURE, UPDATE_PROFILE_SUCCESS } from "../actions/profile"; -import { SET_CURRENT_USER } from "../actions/auth"; -import { User } from "../../types/user"; - -export function* profileRootSaga() { - yield all([ - takeLatest(SET_CURRENT_USER, onCurrentUserChangeSaga), - takeLatest(FETCH_PROFILE_REQUEST, fetchProfileSaga), - takeLatest(UPDATE_PROFILE_REQUEST, updateProfileSaga), - ]); -} - -export function* onCurrentUserChangeSaga() { - yield put(fetchProfile()); -} - -export function* fetchProfileSaga() { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let profile: User; - try { - profile = yield client.fetchProfile().then(result => result.userProfile); - } catch(err) { - yield put({ type: FETCH_PROFILE_FAILURE, err }); - return; - } - - yield put({type: FETCH_PROFILE_SUCCESS, profile }); -} - -export function* updateProfileSaga({ changes }: updateProfileRequestAction) { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let profile: User; - try { - profile = yield client.updateProfile(changes).then(result => result.updateProfile); - } catch(err) { - yield put({ type: UPDATE_PROFILE_FAILURE, err }); - return; - } - - yield put({type: UPDATE_PROFILE_SUCCESS, profile }); -} \ No newline at end of file diff --git a/client/src/store/sagas/root.ts b/client/src/store/sagas/root.ts deleted file mode 100644 index b09eca5..0000000 --- a/client/src/store/sagas/root.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { all } from 'redux-saga/effects'; -import { failureRootSaga } from './failure'; -import { initRootSaga } from './init'; -import { profileRootSaga } from './profile'; -import { workgroupsRootSaga } from './workgroups'; - -export function* rootSaga() { - yield all([ - initRootSaga(), - failureRootSaga(), - profileRootSaga(), - workgroupsRootSaga(), - ]); -} diff --git a/client/src/store/sagas/workgroups.ts b/client/src/store/sagas/workgroups.ts deleted file mode 100644 index a6d9cc3..0000000 --- a/client/src/store/sagas/workgroups.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getClient } from "../../util/daddy"; -import { Config } from "../../config"; -import { all, takeLatest, put } from "redux-saga/effects"; -import { FETCH_WORKGROUPS_SUCCESS, FETCH_WORKGROUPS_FAILURE, FETCH_WORKGROUPS_REQUEST } from "../actions/workgroups"; -import { Workgroup } from "../../types/workgroup"; - -export function* workgroupsRootSaga() { - yield all([ - takeLatest(FETCH_WORKGROUPS_REQUEST, fetchWorkgroupsSaga), - ]); -} - -export function* fetchWorkgroupsSaga() { - const client = getClient(Config.graphQLEndpoint, Config.subscriptionEndpoint); - - let workgroups: [Workgroup]; - try { - workgroups = yield client.fetchWorkgroups().then(result => result.workgroups); - } catch(err) { - yield put({ type: FETCH_WORKGROUPS_FAILURE, err }); - return; - } - - yield put({type: FETCH_WORKGROUPS_SUCCESS, workgroups }); -} \ No newline at end of file diff --git a/client/src/store/selectors/flags.ts b/client/src/store/selectors/flags.ts deleted file mode 100644 index 1f51cb4..0000000 --- a/client/src/store/selectors/flags.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function selectFlagsIsLoading(state: any, ...actionPrefixes: any[]) { - const { actions } = state.flags; - return actionPrefixes.reduce((isLoading, prefix) => { - if (!(prefix in actions)) return isLoading; - return isLoading || actions[prefix].isLoading; - }, false); -}; \ No newline at end of file diff --git a/client/src/store/store.ts b/client/src/store/store.ts deleted file mode 100644 index 911fd81..0000000 --- a/client/src/store/store.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux' -import createSagaMiddleware from 'redux-saga' -import { rootReducer } from './reducers/root' -import { rootSaga } from './sagas/root' - -let reduxMiddlewares = []; - -if (process.env.NODE_ENV !== 'production') { - const createLogger = require('redux-logger').createLogger; - const loggerMiddleware = createLogger({ - collapsed: true, - diff: true - }); - reduxMiddlewares.push(loggerMiddleware); -} - -const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - -// create the saga middleware -const sagaMiddleware = createSagaMiddleware() -reduxMiddlewares.push(sagaMiddleware); - -// mount it on the Store -export const store = createStore( - rootReducer, - composeEnhancers(applyMiddleware(...reduxMiddlewares)), -) - -// then run the saga -sagaMiddleware.run(rootSaga); \ No newline at end of file diff --git a/client/src/util/daddy.ts b/client/src/util/daddy.ts deleted file mode 100644 index aa48178..0000000 --- a/client/src/util/daddy.ts +++ /dev/null @@ -1,134 +0,0 @@ -import ApolloClient, { DefaultOptions } from 'apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; -import { split } from 'apollo-link'; -import { HttpLink } from 'apollo-link-http'; -import { WebSocketLink } from 'apollo-link-ws'; -import { getMainDefinition, variablesInOperation } from 'apollo-utilities'; -import gql from 'graphql-tag'; -import { ProfileChanges } from '../store/actions/profile'; - -export class UnauthorizedError extends Error { - constructor(...args: any[]) { - super(...args) - Object.setPrototypeOf(this, UnauthorizedError.prototype); - } -} - -let client: DaddyClient - -export function getClient(graphQLEndpoint: string, subscriptionEndpoint: string): DaddyClient { - if (!client) { - client = new DaddyClient(graphQLEndpoint, subscriptionEndpoint); - } - - return client; -} - -export class DaddyClient { - - gql: ApolloClient - - constructor(graphQLEndpoint: string, subscriptionEndpoint: string) { - const wsLink = new WebSocketLink({ - uri: subscriptionEndpoint, - options: { - reconnect: true - } - }); - - const httpLink = new HttpLink({ - uri: graphQLEndpoint, - fetchOptions: { - mode: 'cors', - credentials: 'include', - } - }); - - const link = split( - ({ query }) => { - const definition = getMainDefinition(query); - return ( - definition.kind === 'OperationDefinition' && - definition.operation === 'subscription' - ); - }, - wsLink, - httpLink, - ); - - const defaultOptions: DefaultOptions = { - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'ignore', - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all', - }, - }; - - this.gql = new ApolloClient({ - link: link, - cache: new InMemoryCache(), - defaultOptions, - }); - } - - fetchProfile() { - return this.gql.query({ - query: gql` - query { - userProfile { - id, - name, - email, - createdAt, - connectedAt - } - }` - }) - .then(this.assertAuthorization) - } - - fetchWorkgroups() { - return this.gql.query({ - query: gql` - query { - workgroups { - id, - name, - createdAt, - closedAt, - members { - id - } - } - }` - }) - .then(this.assertAuthorization) - } - - updateProfile(changes: ProfileChanges) { - return this.gql.mutate({ - variables: { - changes, - }, - mutation: gql` - mutation updateProfile($changes: ProfileChanges!) { - updateProfile(changes: $changes) { - name, - email, - createdAt, - connectedAt - } - }`, - }) - .then(this.assertAuthorization) - } - - assertAuthorization({ status, data }: any) { - if (status === 401) return Promise.reject(new UnauthorizedError()); - return data; - } - -} \ No newline at end of file diff --git a/internal/route/mount.go b/internal/route/mount.go index 973424d..af4d08b 100644 --- a/internal/route/mount.go +++ b/internal/route/mount.go @@ -33,7 +33,6 @@ func Mount(r *chi.Mux, config *config.Config) error { AllowCredentials: config.HTTP.CORS.AllowCredentials, Debug: config.Debug, }).Handler) - r.Use(oidc.Middleware) r.Use(session.UserEmailMiddleware) gql := handler.New( diff --git a/internal/session/user_email.go b/internal/session/user_email.go index b1dc0c3..171f23e 100644 --- a/internal/session/user_email.go +++ b/internal/session/user_email.go @@ -21,7 +21,9 @@ func UserEmailMiddleware(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { userEmail, err := GetUserEmail(w, r) if err != nil { - panic(errors.Wrap(err, "could not find user email")) + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + + return } ctx := WithUserEmail(r.Context(), userEmail) From bc9aa1721aae10c25c393efecac665175bdee7cf Mon Sep 17 00:00:00 2001 From: William Petit Date: Wed, 22 Jul 2020 09:18:50 +0200 Subject: [PATCH 5/7] Remplacement du Loader par WithLoader --- client/src/components/App.tsx | 2 + client/src/components/HomePage/HomePage.tsx | 10 +--- .../components/HomePage/WorkgroupsPanel.tsx | 58 +++++++++---------- client/src/components/Loader.tsx | 14 ----- .../components/ProfilePage/ProfilePage.tsx | 9 +-- client/src/components/WithLoader.tsx | 7 +-- .../WorkgroupPage/WorkgroupPage.tsx | 22 +++++++ client/src/gql/client.tsx | 5 +- client/src/gql/queries/profile.tsx | 2 +- 9 files changed, 62 insertions(+), 67 deletions(-) delete mode 100644 client/src/components/Loader.tsx create mode 100644 client/src/components/WorkgroupPage/WorkgroupPage.tsx diff --git a/client/src/components/App.tsx b/client/src/components/App.tsx index af9e243..89e3dca 100644 --- a/client/src/components/App.tsx +++ b/client/src/components/App.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom"; import { HomePage } from './HomePage/HomePage'; import { ProfilePage } from './ProfilePage/ProfilePage'; +import { WorkgroupPage } from './WorkgroupPage/WorkgroupPage'; export class App extends React.Component { render() { @@ -10,6 +11,7 @@ export class App extends React.Component { + } /> diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index bd01c65..a09d5f7 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -2,23 +2,18 @@ import React from 'react'; import { Page } from '../Page'; import { Dashboard } from './Dashboard'; import { useUserProfileQuery } from '../../gql/queries/profile'; -import { Loader } from '../Loader'; +import { WithLoader } from '../WithLoader'; export function HomePage() { const { data, loading } = useUserProfileQuery(); - if (loading) { - return ( - - ); - } - const { userProfile } = (data || {}); return (
+ { userProfile ? : @@ -30,6 +25,7 @@ export function HomePage() {
} +
diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx index 6648bc0..d98ef75 100644 --- a/client/src/components/HomePage/WorkgroupsPanel.tsx +++ b/client/src/components/HomePage/WorkgroupsPanel.tsx @@ -4,7 +4,7 @@ import { User } from '../../types/user'; import { Link } from 'react-router-dom'; import { useWorkgroupsQuery } from '../../gql/queries/workgroups'; import { useUserProfileQuery } from '../../gql/queries/profile'; -import { Loader } from '../Loader'; +import { WithLoader } from '../WithLoader'; export function WorkgroupsPanel() { const workgroupsQuery = useWorkgroupsQuery(); @@ -12,18 +12,14 @@ export function WorkgroupsPanel() { const [ state, setState ] = useState({ selectedTab: 0 }); const isLoading = userProfileQuery.loading || workgroupsQuery.loading; - if (isLoading) { - return ; - } - - let { data: { userProfile }} = userProfileQuery; - let { data: { workgroups }} = workgroupsQuery; + const { userProfile } = (userProfileQuery.data || {}); + const { workgroups } = (workgroupsQuery.data || {}); const filterTabs = [ { label: "Mes groupes", filter: workgroups => workgroups.filter((wg: Workgroup) => { - return wg.members.some((u: User) => u.id === userProfile.id); + return wg.members.some((u: User) => u.id === (userProfile ? userProfile.id : '')); }) }, { @@ -43,7 +39,7 @@ export function WorkgroupsPanel() { let workgroupsItems = []; - workgroupsItems = filterTabs[state.selectedTab].filter(workgroups).map((wg: Workgroup) => { + workgroupsItems = filterTabs[state.selectedTab].filter(workgroups || []).map((wg: Workgroup) => { return ( @@ -63,9 +59,9 @@ export function WorkgroupsPanel() {

- + + +
{/*
@@ -76,26 +72,28 @@ export function WorkgroupsPanel() {

*/} -

+ +

+ { + filterTabs.map((tab, i) => { + return ( + + {tab.label} + + ) + }) + } +

{ - filterTabs.map((tab, i) => { - return ( - - {tab.label} - - ) - }) + workgroupsItems.length > 0 ? + workgroupsItems : + + Aucun groupe dans cet catégorie pour l'instant. + } -

- { - workgroupsItems.length > 0 ? - workgroupsItems : - - Aucun groupe dans cet catégorie pour l'instant. - - } + ) } \ No newline at end of file diff --git a/client/src/components/Loader.tsx b/client/src/components/Loader.tsx deleted file mode 100644 index 5632248..0000000 --- a/client/src/components/Loader.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -export class Loader extends React.Component { - render() { - return ( -
-
-
-
-
-
- ) - } -} \ No newline at end of file diff --git a/client/src/components/ProfilePage/ProfilePage.tsx b/client/src/components/ProfilePage/ProfilePage.tsx index 536711e..4e53325 100644 --- a/client/src/components/ProfilePage/ProfilePage.tsx +++ b/client/src/components/ProfilePage/ProfilePage.tsx @@ -1,7 +1,6 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Page } from '../Page'; import { UserForm } from '../UserForm'; -import { Loader } from '../Loader'; import { User } from '../../types/user'; import { useUserProfileQuery } from '../../gql/queries/profile'; import { useUpdateUserProfileMutation } from '../../gql/mutations/profile'; @@ -27,11 +26,9 @@ export function ProfilePage() {

Mon profil

- + { - userProfile ? - : - + }
diff --git a/client/src/components/WithLoader.tsx b/client/src/components/WithLoader.tsx index 33f5441..1a5abf3 100644 --- a/client/src/components/WithLoader.tsx +++ b/client/src/components/WithLoader.tsx @@ -9,12 +9,7 @@ export const WithLoader: FunctionComponent = ({ loading, childr { loading ? -
-
-
-
-
-
: +
Chargement
: children }
diff --git a/client/src/components/WorkgroupPage/WorkgroupPage.tsx b/client/src/components/WorkgroupPage/WorkgroupPage.tsx new file mode 100644 index 0000000..14a0011 --- /dev/null +++ b/client/src/components/WorkgroupPage/WorkgroupPage.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react'; +import { Page } from '../Page'; +import { WithLoader } from '../WithLoader'; + +export function WorkgroupPage() { + return ( + +
+
+
+
+

Groupe de travail

+ + + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/gql/client.tsx b/client/src/gql/client.tsx index bbe3c37..3aff245 100644 --- a/client/src/gql/client.tsx +++ b/client/src/gql/client.tsx @@ -4,12 +4,11 @@ import { WebSocketLink } from "@apollo/client/link/ws"; import { RetryLink } from "@apollo/client/link/retry"; import { SubscriptionClient } from "subscriptions-transport-ws"; - const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, { - reconnect: true + reconnect: true, }); -const link = new RetryLink().split( +const link = new RetryLink({attempts: {max: 2}}).split( (operation) => operation.operationName === 'subscription', new WebSocketLink(subscriptionClient), new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' }) diff --git a/client/src/gql/queries/profile.tsx b/client/src/gql/queries/profile.tsx index 128c394..573fc04 100644 --- a/client/src/gql/queries/profile.tsx +++ b/client/src/gql/queries/profile.tsx @@ -12,5 +12,5 @@ query userProfile { }`; export function useUserProfileQuery() { - return useQuery(QUERY_USER_PROFILE, { fetchPolicy: "network-only" }); + return useQuery(QUERY_USER_PROFILE); } \ No newline at end of file From 4a340529da4f09aa0df5993dd15a40e3e6cac96f Mon Sep 17 00:00:00 2001 From: William Petit Date: Wed, 22 Jul 2020 22:25:03 +0200 Subject: [PATCH 6/7] =?UTF-8?q?Cr=C3=A9er/modifier/rejoindre/quitter=20un?= =?UTF-8?q?=20groupe=20de=20travail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/HomePage/HomePage.tsx | 2 +- .../components/HomePage/WorkgroupsPanel.tsx | 5 +- .../components/ProfilePage/ProfilePage.tsx | 4 +- client/src/components/WithLoader.tsx | 5 +- .../src/components/WorkgroupPage/InfoForm.tsx | 96 ++++++++++++++ .../components/WorkgroupPage/InfoPanel.tsx | 52 ++++++++ .../components/WorkgroupPage/MembersPanel.tsx | 35 +++++ .../WorkgroupPage/WorkgroupPage.tsx | 122 ++++++++++++++++-- client/src/gql/mutations/workgroups.tsx | 77 +++++++++++ client/src/gql/queries/workgroups.tsx | 7 +- internal/graph/query.graphql | 6 +- internal/graph/query.resolvers.go | 14 +- internal/graph/workgroup_handler.go | 12 +- 13 files changed, 403 insertions(+), 34 deletions(-) create mode 100644 client/src/components/WorkgroupPage/InfoForm.tsx create mode 100644 client/src/components/WorkgroupPage/InfoPanel.tsx create mode 100644 client/src/components/WorkgroupPage/MembersPanel.tsx create mode 100644 client/src/gql/mutations/workgroups.tsx diff --git a/client/src/components/HomePage/HomePage.tsx b/client/src/components/HomePage/HomePage.tsx index a09d5f7..2e8e26a 100644 --- a/client/src/components/HomePage/HomePage.tsx +++ b/client/src/components/HomePage/HomePage.tsx @@ -12,7 +12,7 @@ export function HomePage() { return (
-
+
{ userProfile ? diff --git a/client/src/components/HomePage/WorkgroupsPanel.tsx b/client/src/components/HomePage/WorkgroupsPanel.tsx index d98ef75..b59a601 100644 --- a/client/src/components/HomePage/WorkgroupsPanel.tsx +++ b/client/src/components/HomePage/WorkgroupsPanel.tsx @@ -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 (