Compare commits
94 Commits
feature/su
...
develop
Author | SHA1 | Date |
---|---|---|
wpetit | 4ee3de773c | |
wpetit | d4ca478b44 | |
wpetit | d10ce7c7ad | |
wpetit | 655ecd1a0f | |
wpetit | 70fe86a9a5 | |
wpetit | 17b44170d0 | |
wpetit | f2d6a72204 | |
wpetit | f752865d33 | |
wpetit | 85008d3265 | |
wpetit | f34b7e4439 | |
wpetit | b36ae791cb | |
wpetit | 50ec72fcf4 | |
wpetit | 0b93b0875e | |
wpetit | 6f757002b1 | |
wpetit | 18dc4135c4 | |
wpetit | 54e8cf23f7 | |
wpetit | 5649cd2aad | |
wpetit | f032e83e71 | |
wpetit | 19f0c8e0a4 | |
wpetit | d0cd9842ea | |
wpetit | 27458b5b94 | |
wpetit | e5152aa652 | |
wpetit | 1eaaa9065f | |
wpetit | 7d0831ee57 | |
wpetit | 0859202987 | |
wpetit | 7a6eedab9d | |
wpetit | 89a147565c | |
wpetit | 9b8adafe60 | |
wpetit | a3fa793706 | |
wpetit | 27720219ee | |
wpetit | f4528dd087 | |
wpetit | fb954a3e5b | |
wpetit | 92efdbd568 | |
wpetit | 137709adea | |
wpetit | 6cdbea92d1 | |
wpetit | f169169bc7 | |
wpetit | 61eacefd6c | |
wpetit | 11f54ab66e | |
wpetit | 772b09381c | |
wpetit | 978cc65c41 | |
wpetit | 596108b4f4 | |
wpetit | 6845e1ce50 | |
wpetit | 04b32772fc | |
wpetit | 17747e998d | |
wpetit | 12151ff613 | |
wpetit | 71102cfb3b | |
wpetit | 7dad33b6e4 | |
wpetit | 9c6ebae9bc | |
wpetit | 3ef495445a | |
wpetit | bc56c9dbae | |
tcornaut | c95fbf6915 | |
Teddy Cornaut | 952b1b6a8d | |
Teddy Cornaut | 4d5251c724 | |
Teddy Cornaut | 44d4db079a | |
Teddy Cornaut | 089d91a84c | |
Teddy Cornaut | 2d66888ed3 | |
Teddy Cornaut | 406202ddc4 | |
wpetit | 9cb5a63cc9 | |
wpetit | 0fe6e1f07a | |
wpetit | d6eae3a7d3 | |
wpetit | 39d266f701 | |
wpetit | f03a0c96dc | |
wpetit | 32c19bace3 | |
wpetit | 5790c91d82 | |
wpetit | 680614148c | |
wpetit | fc4912882a | |
wpetit | ac41b301a9 | |
wpetit | c0ee95234d | |
wpetit | 8d9d839acf | |
wpetit | e990184a0b | |
wpetit | 4a340529da | |
wpetit | bc9aa1721a | |
wpetit | c4373cce46 | |
wpetit | 8708e30020 | |
wpetit | 676ddf3bc8 | |
wpetit | 7bf4c4f080 | |
wpetit | 303ea6b1d6 | |
wpetit | ccf911322b | |
wpetit | 0cb6c7c67e | |
wpetit | 08bd11f4d9 | |
wpetit | 0d308acd5c | |
wpetit | 36c253d4d7 | |
wpetit | ed219ddd11 | |
wpetit | 758c166f27 | |
wpetit | 05dd505d6b | |
wpetit | 8b8f322630 | |
wpetit | 3bcebdfcd1 | |
wpetit | d0228b6c11 | |
wpetit | 00e331b985 | |
wpetit | 3fd8bf7e69 | |
wpetit | a096b506e2 | |
wpetit | 591112a800 | |
wpetit | 1120474ad9 | |
tcornaut | ff70a6d570 |
|
@ -0,0 +1,7 @@
|
||||||
|
DEBUG=true
|
||||||
|
OIDC_CLIENT_ID=daddy
|
||||||
|
OIDC_CLIENT_SECRET=daddycool
|
||||||
|
OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect
|
||||||
|
HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||||
|
HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ
|
||||||
|
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable"
|
|
@ -0,0 +1,5 @@
|
||||||
|
/vendor
|
||||||
|
/data
|
||||||
|
/bin
|
||||||
|
/.env
|
||||||
|
/release
|
53
Makefile
53
Makefile
|
@ -1,17 +1,28 @@
|
||||||
build:
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
build: build-server
|
||||||
|
|
||||||
|
docker:
|
||||||
docker-compose build
|
docker-compose build
|
||||||
|
|
||||||
deps:
|
generate:
|
||||||
cd frontend && npm install
|
go generate ./...
|
||||||
|
|
||||||
up: build
|
build-server:
|
||||||
( cd frontend && NODE_ENV=development npm run server ) & USER_ID=$(shell id -u) docker-compose up && wait
|
CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server
|
||||||
|
|
||||||
sg:
|
deps: generate
|
||||||
docker-compose exec -u $(shell id -u) super-graph sh
|
cd client && npm install
|
||||||
|
go get ./...
|
||||||
|
|
||||||
sgr:
|
client-dist:
|
||||||
docker-compose run -u $(shell id -u) super-graph sh
|
cd client && NODE_ENV=production npm run build
|
||||||
|
|
||||||
|
up: docker
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
watch:
|
||||||
|
go run github.com/cortesi/modd/cmd/modd
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker-compose down -v --remove-orphans
|
docker-compose down -v --remove-orphans
|
||||||
|
@ -19,5 +30,29 @@ down:
|
||||||
db-shell:
|
db-shell:
|
||||||
docker-compose exec postgres psql -Udaddy
|
docker-compose exec postgres psql -Udaddy
|
||||||
|
|
||||||
|
migrate: build-server
|
||||||
|
( set -o allexport && source .env && set +o allexport && bin/server -workdir "./cmd/server" -config ../../data/config.yml -migrate $(MIGRATE) )
|
||||||
|
|
||||||
|
migrate-latest:
|
||||||
|
$(MAKE) MIGRATE=latest migrate
|
||||||
|
|
||||||
|
migrate-up:
|
||||||
|
$(MAKE) MIGRATE=up migrate
|
||||||
|
|
||||||
|
migrate-down:
|
||||||
|
$(MAKE) MIGRATE=down migrate
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
hydra-shell:
|
hydra-shell:
|
||||||
docker-compose exec hydra /bin/sh
|
docker-compose exec hydra /bin/sh
|
||||||
|
|
||||||
|
.PHONY: release
|
||||||
|
release:
|
||||||
|
./misc/script/release
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf client/node_modules bin data .env internal/graph/generated internal/graph/server.go
|
||||||
|
rm -rf vendor
|
||||||
|
go clean -modcache
|
28
README.md
28
README.md
|
@ -17,39 +17,45 @@ Application de gestion des Dossiers d'Aide à la Décision (D.A.D.) à Cadoles.
|
||||||
```bash
|
```bash
|
||||||
git clone https://forge.cadoles.com/Cadoles/daddy.git # Cloner le projet
|
git clone https://forge.cadoles.com/Cadoles/daddy.git # Cloner le projet
|
||||||
cd daddy # Se placer dans le répertoire
|
cd daddy # Se placer dans le répertoire
|
||||||
make deps # Installer les dépendances NPM
|
make clean # On s'assure d'avoir un environnement propre
|
||||||
make up # Démarrer l'environnement de développement
|
make deps # Installer les dépendances
|
||||||
|
make up # Démarrer l'environnement docker-compose (hydra, hydra-passwordless et fake-smtp)
|
||||||
|
# Dans un second terminal
|
||||||
|
make watch # Suivre les modifications et compiler à la volée le backend et frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Les services suivants devraient être disponibles après démarrage de l'environnement:
|
Les services suivants devraient être disponibles après démarrage de l'environnement:
|
||||||
|
|
||||||
|Service|Type|Accès|Description|
|
|Service|Type|Accès|Description|
|
||||||
|-------|----|-----|-----------|
|
|-------|----|-----|-----------|
|
||||||
|Application React|HTTP (UI)|http://localhost:8081/|Page d'accueil de l'application React (serveur Webpack)|
|
|Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)|
|
||||||
|Interface Web GraphQL|HTTP (UI)|http://localhost:8080/|Interface Web de développement de l'API GraphQL **\***|
|
|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/playground|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)|
|
||||||
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8080/api/v1/graphql|Point d'entrée de l'API GraphQL|
|
|Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL|
|
||||||
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|
|Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)|
|
||||||
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|
|Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)|
|
||||||
|Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp)
|
|Serveur FakeSMTP|HTTP|http://localhost:8082|Interface web du serveur [FakeSMTP](https://forge.cadoles.com/wpetit/fake-smtp)
|
||||||
|Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement|
|
|Serveur PostgreSQL|TCP/IP (PostgreSQL)|`127.0.0.1:5432`|Port de connexion à la base de données PostgreSQL de développement|
|
||||||
|
|
||||||
**\*** Pensez à passer l'attribut `auth_fail_block: false` dans le fichier `backend/config/dev.yml` si vous voulez pouvoir utiliser cette interface sans avoir à définir l'entête `Authorization`.
|
|
||||||
|
|
||||||
#### Fichiers/répertoires notables
|
#### Fichiers/répertoires notables
|
||||||
|
|
||||||
|Chemin|Description|
|
|Chemin|Description|
|
||||||
|------------------|-----------|
|
|------------------|-----------|
|
||||||
|`docker-compose.yml`|Configuration de l'environnement Docker Compose|
|
|`docker-compose.yml`|Configuration de l'environnement Docker Compose|
|
||||||
|`frontend/src`|Sources du frontend ([React](https://reactjs.org))|
|
|`client/src`|Sources du frontend ([React](https://reactjs.org))|
|
||||||
|`backend/config/migrations`|Migrations SQL pour le backend, voir [la documentation de SuperGraph à ce sujet](https://supergraph.dev/docs/start#migrations)|
|
|
||||||
|
|
||||||
#### Commandes utiles
|
#### Commandes utiles
|
||||||
|
|
||||||
|Commande|Description|
|
|Commande|Description|
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
|`make up`|Démarrer l'environnement de développement, `Ctrl+C` pour le stopper.|
|
|`make up`|Démarrer l'environnement Docker Compose, `Ctrl+C` pour le stopper.|
|
||||||
|`make down`|Stopper et supprimer l'environnement de développement.|
|
|`make down`|Stopper et supprimer l'environnement Docker Compose.|
|
||||||
|
|`make watch`|Suerveiller les sources et recompiler à la volée le client/server.|
|
||||||
|`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.|
|
|`make db-shell`|Ouvrir une console `psql` sur la base de données de développement.|
|
||||||
|
|`make hydra-shell`|Ouvrir un shell interactif dans le conteneur Hydra. (`hydra --help` pour voir les commandes disponibles pour l'administration)|
|
||||||
|
|`make migrate-latest`|Migrer la base de données à la dernière version disponible du schéma.|
|
||||||
|
|`make migrate-down`|Migrer la base de données à la version précédente du schéma.|
|
||||||
|
|`make migrate-up`|Migrer la base de données à la version suivante du schéma.|
|
||||||
|
|`make clean`|Nettoyer l'environnement.|
|
||||||
|
|
||||||
#### Ressources
|
#### Ressources
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
/* fetchUser */
|
|
||||||
|
|
||||||
variables {
|
|
||||||
"email": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
query fetchUser {
|
|
||||||
user(where: {email: {eq: $email}}) {
|
|
||||||
id
|
|
||||||
created_at
|
|
||||||
updated_at
|
|
||||||
email,
|
|
||||||
full_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,225 +0,0 @@
|
||||||
app_name: "Daddy Dev"
|
|
||||||
host_port: 0.0.0.0:8080
|
|
||||||
web_ui: true
|
|
||||||
|
|
||||||
# debug, error, warn, info
|
|
||||||
log_level: debug
|
|
||||||
|
|
||||||
# enable or disable http compression (uses gzip)
|
|
||||||
http_compress: true
|
|
||||||
|
|
||||||
# When production mode is 'true' only queries
|
|
||||||
# from the allow list are permitted.
|
|
||||||
# When it's 'false' all queries are saved to the
|
|
||||||
# the allow list in ./config/allow.list
|
|
||||||
production: false
|
|
||||||
|
|
||||||
# Throw a 401 on auth failure for queries that need auth
|
|
||||||
auth_fail_block: true
|
|
||||||
|
|
||||||
# Latency tracing for database queries and remote joins
|
|
||||||
# the resulting latency information is returned with the
|
|
||||||
# response
|
|
||||||
enable_tracing: true
|
|
||||||
|
|
||||||
# Watch the config folder and reload Super Graph
|
|
||||||
# with the new configs when a change is detected
|
|
||||||
reload_on_config_change: true
|
|
||||||
|
|
||||||
# File that points to the database seeding script
|
|
||||||
# seed_file: seed.js
|
|
||||||
|
|
||||||
# Path pointing to where the migrations can be found
|
|
||||||
# this must be a relative path under the config path
|
|
||||||
migrations_path: ./migrations
|
|
||||||
|
|
||||||
# Secret key for general encryption operations like
|
|
||||||
# encrypting the cursor data
|
|
||||||
secret_key: supercalifajalistics
|
|
||||||
|
|
||||||
# CORS: A list of origins a cross-domain request can be executed from.
|
|
||||||
# If the special * value is present in the list, all origins will be allowed.
|
|
||||||
# An origin may contain a wildcard (*) to replace 0 or more
|
|
||||||
# characters (i.e.: http://*.domain.com).
|
|
||||||
cors_allowed_origins: ["*"]
|
|
||||||
cors_allowed_headers: ["Authorization", "Content-Type", "Mode"]
|
|
||||||
cors_allowed_methods: ["POST"]
|
|
||||||
|
|
||||||
# Debug Cross Origin Resource Sharing requests
|
|
||||||
cors_debug: false
|
|
||||||
|
|
||||||
# Postgres related environment Variables
|
|
||||||
# SG_DATABASE_HOST
|
|
||||||
# SG_DATABASE_PORT
|
|
||||||
# SG_DATABASE_USER
|
|
||||||
# SG_DATABASE_PASSWORD
|
|
||||||
|
|
||||||
# Auth related environment Variables
|
|
||||||
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
|
|
||||||
# SG_AUTH_RAILS_REDIS_URL
|
|
||||||
# SG_AUTH_RAILS_REDIS_PASSWORD
|
|
||||||
# SG_AUTH_JWT_PUBLIC_KEY_FILE
|
|
||||||
|
|
||||||
# inflections:
|
|
||||||
# person: people
|
|
||||||
# sheep: sheep
|
|
||||||
|
|
||||||
auth:
|
|
||||||
# Can be 'rails', 'jwt' or 'header'
|
|
||||||
type: jwt
|
|
||||||
#cookie: _supergraph_session
|
|
||||||
|
|
||||||
# Comment this out if you want to disable setting
|
|
||||||
# the user_id via a header for testing.
|
|
||||||
# Disable in production
|
|
||||||
#creds_in_header: false
|
|
||||||
|
|
||||||
jwt:
|
|
||||||
provider: hydra
|
|
||||||
jwks_url: http://hydra:4444/.well-known/jwks.json
|
|
||||||
|
|
||||||
# header:
|
|
||||||
# name: dnt
|
|
||||||
# exists: true
|
|
||||||
# value: localhost:8080
|
|
||||||
|
|
||||||
# You can add additional named auths to use with actions
|
|
||||||
# In this example actions using this auth can only be
|
|
||||||
# called from the Google Appengine Cron service that
|
|
||||||
# sets a special header to all it's requests
|
|
||||||
# auths:
|
|
||||||
# - name: from_taskqueue
|
|
||||||
# type: header
|
|
||||||
# header:
|
|
||||||
# name: X-Appengine-Cron
|
|
||||||
# exists: true
|
|
||||||
|
|
||||||
database:
|
|
||||||
type: postgres
|
|
||||||
host: localhost
|
|
||||||
port: 5432
|
|
||||||
dbname: daddy
|
|
||||||
user: daddy
|
|
||||||
password: daddy
|
|
||||||
|
|
||||||
#schema: "public"
|
|
||||||
#pool_size: 10
|
|
||||||
#max_retries: 0
|
|
||||||
log_level: "debug"
|
|
||||||
|
|
||||||
# Set session variable "user.id" to the user id
|
|
||||||
# Enable this if you need the user id in triggers, etc
|
|
||||||
set_user_id: true
|
|
||||||
|
|
||||||
# database ping timeout is used for db health checking
|
|
||||||
ping_timeout: 1m
|
|
||||||
|
|
||||||
# Define additional variables here to be used with filters
|
|
||||||
variables:
|
|
||||||
# admin_account_id: "5"
|
|
||||||
# admin_account_id: "sql:select id from users where admin = true limit 1"
|
|
||||||
|
|
||||||
|
|
||||||
# Field and table names that you wish to block
|
|
||||||
blocklist:
|
|
||||||
- ar_internal_metadata
|
|
||||||
- schema_migrations
|
|
||||||
- secret
|
|
||||||
- password
|
|
||||||
- encrypted
|
|
||||||
- token
|
|
||||||
|
|
||||||
# Create custom actions with their own api endpoints
|
|
||||||
# For example the below action will be available at /api/v1/actions/refresh_leaderboard_users
|
|
||||||
# A request to this url will execute the configured SQL query
|
|
||||||
# which in this case refreshes a materialized view in the database.
|
|
||||||
# The auth_name is from one of the configured auths
|
|
||||||
actions:
|
|
||||||
# - name: refresh_leaderboard_users
|
|
||||||
# sql: REFRESH MATERIALIZED VIEW CONCURRENTLY "leaderboard_users"
|
|
||||||
# auth_name: from_taskqueue
|
|
||||||
|
|
||||||
tables:
|
|
||||||
# - name: customers
|
|
||||||
# remotes:
|
|
||||||
# - name: payments
|
|
||||||
# id: stripe_id
|
|
||||||
# url: http://rails_app:3000/stripe/$id
|
|
||||||
# path: data
|
|
||||||
# # debug: true
|
|
||||||
# pass_headers:
|
|
||||||
# - cookie
|
|
||||||
# set_headers:
|
|
||||||
# - name: Host
|
|
||||||
# value: 0.0.0.0
|
|
||||||
# - name: Authorization
|
|
||||||
# value: Bearer <stripe_api_key>
|
|
||||||
|
|
||||||
# - # You can create new fields that have a
|
|
||||||
# # real db table backing them
|
|
||||||
# name: me
|
|
||||||
# table: users
|
|
||||||
|
|
||||||
|
|
||||||
roles_query: "select * from users where users.email = $user_id"
|
|
||||||
|
|
||||||
roles:
|
|
||||||
# Rôle par défaut si l'utilisateur n'existe pas dans la table `users`
|
|
||||||
- name: anon
|
|
||||||
tables:
|
|
||||||
- name: users
|
|
||||||
# insert:
|
|
||||||
# block: true
|
|
||||||
# query:
|
|
||||||
# block: true
|
|
||||||
# update:
|
|
||||||
# block: true
|
|
||||||
# delete:
|
|
||||||
# block: true
|
|
||||||
|
|
||||||
# Rôle par défaut si l'utilisateur existe dans la table `users`
|
|
||||||
# mais que la valeur de la colonne `role` n'est pas définie
|
|
||||||
- name: user
|
|
||||||
tables:
|
|
||||||
- name: users
|
|
||||||
insert:
|
|
||||||
block: true
|
|
||||||
query:
|
|
||||||
filters: ["{ email: { _eq: $user_id } }"]
|
|
||||||
update:
|
|
||||||
columns:
|
|
||||||
- full_name
|
|
||||||
filters: ["{ email: { _eq: $user_id } }"]
|
|
||||||
delete:
|
|
||||||
block: true
|
|
||||||
|
|
||||||
- name: admin
|
|
||||||
match: role = 'admin'
|
|
||||||
tables:
|
|
||||||
- name: users
|
|
||||||
|
|
||||||
# - name: products
|
|
||||||
# query:
|
|
||||||
# limit: 50
|
|
||||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
|
||||||
# disable_functions: false
|
|
||||||
|
|
||||||
# insert:
|
|
||||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
|
||||||
# presets:
|
|
||||||
# - user_id: "$user_id"
|
|
||||||
# - created_at: "now"
|
|
||||||
|
|
||||||
# update:
|
|
||||||
# filters: ["{ user_id: { eq: $user_id } }"]
|
|
||||||
# presets:
|
|
||||||
# - updated_at: "now"
|
|
||||||
|
|
||||||
# delete:
|
|
||||||
# block: true
|
|
||||||
|
|
||||||
# - name: admin
|
|
||||||
# match: id = 1000
|
|
||||||
# tables:
|
|
||||||
# - name: users
|
|
||||||
# filters: []
|
|
|
@ -1,17 +0,0 @@
|
||||||
-- Write your migrate up statements here
|
|
||||||
|
|
||||||
CREATE TABLE public.users (
|
|
||||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
full_name text,
|
|
||||||
email text UNIQUE NOT NULL CHECK (length(email) < 255),
|
|
||||||
created_at timestamptz NOT NULL NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at timestamptz NOT NULL NOT NULL DEFAULT NOW(),
|
|
||||||
role varchar(64)
|
|
||||||
);
|
|
||||||
|
|
||||||
---- create above / drop below ----
|
|
||||||
|
|
||||||
-- Write your down migrate statements here. If this migration is irreversible
|
|
||||||
-- then delete the separator line above.
|
|
||||||
|
|
||||||
DROP TABLE public.users
|
|
|
@ -1,67 +0,0 @@
|
||||||
# Inherit config from this other config file
|
|
||||||
# so I only need to overwrite some values
|
|
||||||
inherits: dev
|
|
||||||
|
|
||||||
app_name: "Backend Production"
|
|
||||||
host_port: 0.0.0.0:8080
|
|
||||||
web_ui: false
|
|
||||||
|
|
||||||
# debug, info, warn, error, fatal, panic, disable
|
|
||||||
log_level: "warn"
|
|
||||||
|
|
||||||
# enable or disable http compression (uses gzip)
|
|
||||||
http_compress: true
|
|
||||||
|
|
||||||
# When production mode is 'true' only queries
|
|
||||||
# from the allow list are permitted.
|
|
||||||
# When it's 'false' all queries are saved to the
|
|
||||||
# the allow list in ./config/allow.list
|
|
||||||
production: true
|
|
||||||
|
|
||||||
# Throw a 401 on auth failure for queries that need auth
|
|
||||||
auth_fail_block: true
|
|
||||||
|
|
||||||
# Latency tracing for database queries and remote joins
|
|
||||||
# the resulting latency information is returned with the
|
|
||||||
# response
|
|
||||||
enable_tracing: true
|
|
||||||
|
|
||||||
# File that points to the database seeding script
|
|
||||||
# seed_file: seed.js
|
|
||||||
|
|
||||||
# Path pointing to where the migrations can be found
|
|
||||||
# migrations_path: migrations
|
|
||||||
|
|
||||||
# Secret key for general encryption operations like
|
|
||||||
# encrypting the cursor data
|
|
||||||
# secret_key: supercalifajalistics
|
|
||||||
|
|
||||||
# Postgres related environment Variables
|
|
||||||
# SG_DATABASE_HOST
|
|
||||||
# SG_DATABASE_PORT
|
|
||||||
# SG_DATABASE_USER
|
|
||||||
# SG_DATABASE_PASSWORD
|
|
||||||
|
|
||||||
# Auth related environment Variables
|
|
||||||
# SG_AUTH_RAILS_COOKIE_SECRET_KEY_BASE
|
|
||||||
# SG_AUTH_RAILS_REDIS_URL
|
|
||||||
# SG_AUTH_RAILS_REDIS_PASSWORD
|
|
||||||
# SG_AUTH_JWT_PUBLIC_KEY_FILE
|
|
||||||
|
|
||||||
database:
|
|
||||||
type: postgres
|
|
||||||
host: db
|
|
||||||
port: 5432
|
|
||||||
dbname: backend_development
|
|
||||||
user: postgres
|
|
||||||
password: postgres
|
|
||||||
#pool_size: 10
|
|
||||||
#max_retries: 0
|
|
||||||
#log_level: "debug"
|
|
||||||
|
|
||||||
# Set session variable "user.id" to the user id
|
|
||||||
# Enable this if you need the user id in triggers, etc
|
|
||||||
set_user_id: false
|
|
||||||
|
|
||||||
# database ping timeout is used for db health checking
|
|
||||||
ping_timeout: 5m
|
|
|
@ -1,33 +0,0 @@
|
||||||
// Voir https://supergraph.dev/docs/seed
|
|
||||||
|
|
||||||
var users = [
|
|
||||||
{
|
|
||||||
full_name: 'Admin',
|
|
||||||
email: 'admin@cadoles.com',
|
|
||||||
role: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
full_name: 'User 1',
|
|
||||||
email: 'user1@cadoles.com',
|
|
||||||
role: 'user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
full_name: 'User 2',
|
|
||||||
email: 'user2@cadoles.com',
|
|
||||||
role: 'user',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
full_name: 'User 3',
|
|
||||||
email: 'user3@cadoles.com',
|
|
||||||
role: 'user',
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var user, i = 0; (user = users[i]); i++) {
|
|
||||||
var res = graphql(" \
|
|
||||||
mutation { \
|
|
||||||
user(insert: $data) { \
|
|
||||||
id \
|
|
||||||
} \
|
|
||||||
}", { data: user });
|
|
||||||
}
|
|
|
@ -1,9 +1,46 @@
|
||||||
{
|
{
|
||||||
"name": "dadd-",
|
"name": "daddy",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"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": {
|
"@babel/code-frame": {
|
||||||
"version": "7.10.1",
|
"version": "7.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
|
||||||
|
@ -1088,9 +1125,9 @@
|
||||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-free": {
|
"@fortawesome/fontawesome-free": {
|
||||||
"version": "5.13.0",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.0.tgz",
|
||||||
"integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==",
|
"integrity": "sha512-wXetjQBNMTP59MAYNR1tdahMDOLx3FYj3PKdso7PLFLDpTvmAIqhSSEqnSTmWKahRjD+Sh5I5635+5qaoib5lw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
|
@ -1372,6 +1409,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/zen-observable": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
|
||||||
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||||
|
@ -1547,6 +1589,14 @@
|
||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@wry/equality": {
|
||||||
|
"version": "0.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
||||||
|
"integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@xtuc/ieee754": {
|
"@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
|
@ -1875,8 +1925,7 @@
|
||||||
"async-limiter": {
|
"async-limiter": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"asynckit": {
|
"asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
@ -2724,6 +2773,11 @@
|
||||||
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
"integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"backo2": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
|
||||||
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
@ -2785,6 +2839,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"base-x": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"base64-js": {
|
"base64-js": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||||
|
@ -3073,6 +3135,14 @@
|
||||||
"pkg-up": "^2.0.0"
|
"pkg-up": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"bs58": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
|
||||||
|
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
|
||||||
|
"requires": {
|
||||||
|
"base-x": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"btoa": {
|
"btoa": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||||
|
@ -3123,14 +3193,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"bulma": {
|
"bulma": {
|
||||||
"version": "0.7.5",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.0.tgz",
|
||||||
"integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw=="
|
"integrity": "sha512-rV75CJkubNUroAt0qCRkjznZLoaXq/ctfMXsMvKSL84UetbSyx5REl96e8GoQ04G4Tkw0XF3STECffTOQrbzOQ=="
|
||||||
},
|
},
|
||||||
"bulma-switch": {
|
"bulma-timeline": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bulma-timeline/-/bulma-timeline-3.0.4.tgz",
|
||||||
"integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
|
"integrity": "sha512-gCUOcSUuzHoeVMkCpLF49j5Z5yl78XQ+KgJcT+1ju5WIGgBgVytRUob/dw5NHAxPLO2rmcvwYNbCJFp7w4WT4Q=="
|
||||||
},
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
@ -3965,6 +4035,11 @@
|
||||||
"randomfill": "^1.0.3"
|
"randomfill": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypto-js": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
|
||||||
|
},
|
||||||
"css": {
|
"css": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
|
||||||
|
@ -4419,9 +4494,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"elliptic": {
|
"elliptic": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||||
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
|
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bn.js": "^4.4.0",
|
"bn.js": "^4.4.0",
|
||||||
|
@ -5029,8 +5104,7 @@
|
||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fastparse": {
|
"fastparse": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
@ -5350,6 +5424,11 @@
|
||||||
"integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
|
"integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"get-browser-rtc": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-u81AyEUaftTvXDc7gWmkCd0dEdk="
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
@ -5499,10 +5578,15 @@
|
||||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"graphql-request": {
|
"graphql": {
|
||||||
"version": "2.0.0",
|
"version": "15.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz",
|
||||||
"integrity": "sha512-Ww3Ax+G3l2d+mPT8w7HC9LfrKjutnCKtnDq7ZZp2ghVk5IQDjwAk3/arRF1ix17Ky15rm0hrSKVKxRhIVlSuoQ=="
|
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
|
||||||
|
},
|
||||||
|
"graphql-tag": {
|
||||||
|
"version": "2.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz",
|
||||||
|
"integrity": "sha512-O7vG5BT3w6Sotc26ybcvLKNTdfr4GfsIVMD+LdYqXCeJIYPRyp8BIsDOUtxw7S1PYvRw5vH3278J2EDezR6mfA=="
|
||||||
},
|
},
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
|
@ -5981,8 +6065,7 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -6302,12 +6385,22 @@
|
||||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"isomorphic.js": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.1.5.tgz",
|
||||||
|
"integrity": "sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA=="
|
||||||
|
},
|
||||||
"isstream": {
|
"isstream": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"iterall": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
|
||||||
|
},
|
||||||
"js-base64": {
|
"js-base64": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
|
||||||
|
@ -6381,11 +6474,6 @@
|
||||||
"verror": "1.10.0"
|
"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": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
@ -6422,6 +6510,14 @@
|
||||||
"leven": "^3.1.0"
|
"leven": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lib0": {
|
||||||
|
"version": "0.2.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.34.tgz",
|
||||||
|
"integrity": "sha512-cqsVIMPgFlDtgQcpkt7HOY6W3sbYPIe3qxMnbRSwHTgiQancgm+TRDPx28mC6GUZ6lG6Nr0bIWf4Nog6dWUNUg==",
|
||||||
|
"requires": {
|
||||||
|
"isomorphic.js": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"load-json-file": {
|
"load-json-file": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
|
@ -6482,9 +6578,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.15",
|
"version": "4.17.20",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
},
|
},
|
||||||
"loglevel": {
|
"loglevel": {
|
||||||
"version": "1.6.8",
|
"version": "1.6.8",
|
||||||
|
@ -6983,9 +7079,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-forge": {
|
"node-forge": {
|
||||||
"version": "0.9.0",
|
"version": "0.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
|
||||||
"integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==",
|
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-gyp": {
|
"node-gyp": {
|
||||||
|
@ -7914,11 +8010,6 @@
|
||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
"dev": true
|
"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": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
|
@ -7937,11 +8028,15 @@
|
||||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"queue-microtask": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-eY/4Obve9cE5FK8YvC1cJsm5cr7XvAurul8UtBDJ2PR1p5NmAwHtvAt5ftcLtwYRCUKNhxCneZZlxmUDFoSeKA=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
@ -8583,8 +8678,7 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"safe-regex": {
|
"safe-regex": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
@ -8682,12 +8776,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"selfsigned": {
|
"selfsigned": {
|
||||||
"version": "1.10.7",
|
"version": "1.10.8",
|
||||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
|
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz",
|
||||||
"integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==",
|
"integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-forge": "0.9.0"
|
"node-forge": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
|
@ -8900,6 +8994,30 @@
|
||||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"simple-peer": {
|
||||||
|
"version": "9.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.7.2.tgz",
|
||||||
|
"integrity": "sha512-xeMyxa9B4V0eA6mf17fVr8nm2QhAYFu+ZZv8zkSFFTjJETGF227CshwobrIYZuspJglMD63egcevQXGOrTIsuA==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^4.0.1",
|
||||||
|
"get-browser-rtc": "^1.0.0",
|
||||||
|
"queue-microtask": "^1.1.0",
|
||||||
|
"randombytes": "^2.0.3",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
|
||||||
|
"requires": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||||
|
@ -9329,7 +9447,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
|
@ -9420,6 +9537,33 @@
|
||||||
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
|
||||||
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
|
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
|
||||||
},
|
},
|
||||||
|
"subscriptions-transport-ws": {
|
||||||
|
"version": "0.9.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz",
|
||||||
|
"integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==",
|
||||||
|
"requires": {
|
||||||
|
"backo2": "^1.0.2",
|
||||||
|
"eventemitter3": "^3.1.0",
|
||||||
|
"iterall": "^1.2.1",
|
||||||
|
"symbol-observable": "^1.0.4",
|
||||||
|
"ws": "^5.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
|
||||||
|
"requires": {
|
||||||
|
"async-limiter": "~1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
@ -9658,6 +9802,14 @@
|
||||||
"glob": "^7.1.2"
|
"glob": "^7.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ts-invariant": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||||
|
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ts-loader": {
|
"ts-loader": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz",
|
||||||
|
@ -9725,8 +9877,7 @@
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||||
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==",
|
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"tty-browserify": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
@ -10012,8 +10163,7 @@
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"util.promisify": {
|
"util.promisify": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
@ -10709,6 +10859,33 @@
|
||||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"y-protocols": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-QP3fCM7c2gGfUi2nqf8gspyO4VW23zv3kNqPNdD3wNxMbuNQenMyoDVZYEo12jzR4RQ3aaDfPK62Sf31SVOmfg==",
|
||||||
|
"requires": {
|
||||||
|
"lib0": "^0.2.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"y-webrtc": {
|
||||||
|
"version": "10.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.1.6.tgz",
|
||||||
|
"integrity": "sha512-b3pTIv9LcPuMb4nbDT3/kkgmcuQoTrBmaPbBqPH1LJMzI8HwYnMK8p5r0fBQJBI0YRor+i8BT15Evv1nQBP0zg==",
|
||||||
|
"requires": {
|
||||||
|
"lib0": "^0.2.32",
|
||||||
|
"simple-peer": "^9.7.2",
|
||||||
|
"ws": "^7.2.0",
|
||||||
|
"y-protocols": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": {
|
||||||
|
"version": "7.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
||||||
|
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||||
|
@ -10833,6 +11010,19 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"yjs": {
|
||||||
|
"version": "13.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.4.1.tgz",
|
||||||
|
"integrity": "sha512-kIh0sprCTzIm2qyr1VsovkvjKzD2GR4WcU/McJpLAEvImCJHA78Q3S6uSLnhZX0i7FQdrLPCRT8DtTPEH73jnw==",
|
||||||
|
"requires": {
|
||||||
|
"lib0": "^0.2.33"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zen-observable": {
|
||||||
|
"version": "0.8.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
||||||
|
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "dadd-",
|
"name": "daddy",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "Daddy",
|
"description": "Daddy",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||||
"@babel/preset-env": "^7.7.1",
|
"@babel/preset-env": "^7.7.1",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@types/node": "^13.13.4",
|
"@types/node": "^13.13.4",
|
||||||
"@types/react-dom": "^16.9.7",
|
"@types/react-dom": "^16.9.7",
|
||||||
"@types/react-redux": "^7.1.7",
|
"@types/react-redux": "^7.1.7",
|
||||||
|
@ -51,12 +51,13 @@
|
||||||
"webpack-dev-server": "^3.11.0"
|
"webpack-dev-server": "^3.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.0.2",
|
||||||
"@types/qs": "^6.9.3",
|
"@types/qs": "^6.9.3",
|
||||||
"bulma": "^0.7.2",
|
"bs58": "^4.0.1",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma": "^0.9.0",
|
||||||
"graphql-request": "^2.0.0",
|
"bulma-timeline": "^3.0.4",
|
||||||
"jwt-decode": "^2.2.0",
|
"crypto-js": "^4.0.0",
|
||||||
"qs": "^6.9.4",
|
"graphql": "^15.3.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
|
@ -65,6 +66,9 @@
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"styled-components": "^4.4.1",
|
"styled-components": "^4.4.1",
|
||||||
"typescript": "^3.8.3"
|
"subscriptions-transport-ws": "^0.9.17",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
|
"y-webrtc": "^10.1.6",
|
||||||
|
"yjs": "^13.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React, { FunctionComponent, useState, useEffect, Suspense } from 'react';
|
||||||
|
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||||
|
import { useUserProfile } from '../gql/queries/profile';
|
||||||
|
import { LoggedInContext, getSavedLoggedIn, saveLoggedIn } from '../hooks/useLoggedIn';
|
||||||
|
import { PrivateRoute } from './PrivateRoute';
|
||||||
|
import { useKonamiCode } from '../hooks/useKonamiCode';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { createClient } from '../util/apollo';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
import { AppLoader } from './AppLoader';
|
||||||
|
|
||||||
|
const LazyHomePage = React.lazy(() => import(/* webpackChunkName: "HomePage" */'./HomePage/HomePage'));
|
||||||
|
const LazyDashboardPage = React.lazy(() => import(/* webpackChunkName: "DashboardPage" */'./DashboardPage/DashboardPage'));
|
||||||
|
const LazyUnauthorizedPage = React.lazy(() => import(/* webpackChunkName: "UnauthorizedPage" */'./UnauthorizedPage/UnauthorizedPage'));
|
||||||
|
const LazyConferencePage = React.lazy(() => import(/* webpackChunkName: "ConferencePage" */'./ConferencePage/ConferencePage'));
|
||||||
|
const LazyDecisionSupportFilePage = React.lazy(() => import(/* webpackChunkName: "DecisionSupportFilePage" */'./DecisionSupportFilePage/DecisionSupportFilePage'));
|
||||||
|
const LazyProfilePage = React.lazy(() => import(/* webpackChunkName: "ProfilePage" */'./ProfilePage/ProfilePage'));
|
||||||
|
const LazyWorkgroupPage = React.lazy(() => import(/* webpackChunkName: "WorkgroupPage" */'./WorkgroupPage/WorkgroupPage'));
|
||||||
|
const LazyLogoutPage = React.lazy(() => import(/* webpackChunkName: "LogoutPage" */'./LogoutPage'));
|
||||||
|
|
||||||
|
export interface AppProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App: FunctionComponent<AppProps> = () => {
|
||||||
|
const [ loggedIn, setLoggedIn ] = useState(getSavedLoggedIn());
|
||||||
|
|
||||||
|
const client = createClient((loggedIn) => {
|
||||||
|
setLoggedIn(loggedIn);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
saveLoggedIn(loggedIn);
|
||||||
|
}, [loggedIn]);
|
||||||
|
|
||||||
|
const [ showBoneyM, setShowBoneyM ] = useState(false);
|
||||||
|
useKonamiCode(() => setShowBoneyM(true));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<AppLoader />}>
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<LoggedInContext.Provider value={loggedIn}>
|
||||||
|
<UserSessionCheck setLoggedIn={setLoggedIn} />
|
||||||
|
<BrowserRouter>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/" exact component={LazyHomePage} />
|
||||||
|
<Route path="/unauthorized" exact component={LazyUnauthorizedPage} />
|
||||||
|
<PrivateRoute path="/profile" exact component={LazyProfilePage} />
|
||||||
|
<PrivateRoute path="/conference" exact component={LazyConferencePage} />
|
||||||
|
<PrivateRoute path="/workgroups/:id" exact component={LazyWorkgroupPage} />
|
||||||
|
<PrivateRoute path="/decisions/:id" component={LazyDecisionSupportFilePage} />
|
||||||
|
<PrivateRoute path="/dashboard" exact component={LazyDashboardPage} />
|
||||||
|
<PrivateRoute path="/logout" exact component={LazyLogoutPage} />
|
||||||
|
<Route component={() => <Redirect to="/" />} />
|
||||||
|
</Switch>
|
||||||
|
</BrowserRouter>
|
||||||
|
{
|
||||||
|
showBoneyM ?
|
||||||
|
<Modal active={true} showCloseButton={true} onClose={() => setShowBoneyM(false)}>
|
||||||
|
<iframe width={560} height={315}
|
||||||
|
frameBorder={0}
|
||||||
|
allowFullScreen={true}
|
||||||
|
src="https://www.youtube.com/embed/uVzT5QEEQ2c?autoplay=1" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture">
|
||||||
|
</iframe>
|
||||||
|
</Modal> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</LoggedInContext.Provider>
|
||||||
|
</ApolloProvider>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSessionCheckProps {
|
||||||
|
setLoggedIn: (boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSessionCheck: FunctionComponent<UserSessionCheckProps> = ({ setLoggedIn }) => {
|
||||||
|
const { user, loading } = useUserProfile();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return;
|
||||||
|
setLoggedIn(user && user.id !== '');
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
export const AppLoader:FunctionComponent = () => {
|
||||||
|
return (
|
||||||
|
<div className="app-loader">
|
||||||
|
<i className="fas fa-spinner fa-spin fa-5x"></i>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
import React, { FunctionComponent, useEffect } from 'react';
|
||||||
|
import { Config } from '../../config';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { useConference } from '../../hooks/useConference';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { Gravatar } from './Gravatar';
|
||||||
|
|
||||||
|
export interface ConferencePageProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusHandRaised = 'hand-raised';
|
||||||
|
const StatusThumbsUp = 'thumbs-up';
|
||||||
|
const StatusThumbsDown = 'thumbs-down';
|
||||||
|
const StatusNoVote = 'no-vote';
|
||||||
|
|
||||||
|
export const ConferencePage:FunctionComponent<ConferencePageProps> = () => {
|
||||||
|
const { user } = useUserProfile();
|
||||||
|
const { uuid, data, setNickname, setEmail, ping, setStatus, forget } = useConference();
|
||||||
|
|
||||||
|
const currentStatus = data.statuses[uuid];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user.name && !user.email) return;
|
||||||
|
setNickname(user.name || user.email.split('@')[0]);
|
||||||
|
setEmail(user.email);
|
||||||
|
}, [user.name, user.email]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ping();
|
||||||
|
const intervalId = setInterval(() => ping(), Config.conferenceHeartbeatInterval + (Math.random() * Config.conferenceHeartbeatInterval/2));
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onStatusChange = (status: string) => {
|
||||||
|
setStatus(currentStatus === status ? '' : status);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Conference">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<section className="mt-5">
|
||||||
|
<h3 className="is-size-3">Mes actions</h3>
|
||||||
|
<div className="buttons has-addons">
|
||||||
|
<button
|
||||||
|
className={`button is-medium ${currentStatus === StatusHandRaised ? 'is-info is-selected' : ''}`}
|
||||||
|
onClick={onStatusChange.bind(null, StatusHandRaised)}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fa fa-hand-paper"></i>
|
||||||
|
</span>
|
||||||
|
<span>Lever la main</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`button is-medium ${currentStatus === StatusThumbsUp ? 'is-success is-selected' : ''}`}
|
||||||
|
onClick={onStatusChange.bind(null, StatusThumbsUp)}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fa fa-thumbs-up"></i>
|
||||||
|
</span>
|
||||||
|
<span>Voter pour</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`button is-medium ${currentStatus === StatusNoVote ? 'is-warning is-selected' : ''}`}
|
||||||
|
onClick={onStatusChange.bind(null, StatusNoVote)}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fa fa-mitten"></i>
|
||||||
|
</span>
|
||||||
|
<span>Ne se prononce pas</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`button is-medium ${currentStatus === StatusThumbsDown ? 'is-danger is-selected' : ''}`}
|
||||||
|
onClick={onStatusChange.bind(null, StatusThumbsDown)}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fa fa-thumbs-down"></i>
|
||||||
|
</span>
|
||||||
|
<span>Voter contre</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h3 className="is-size-3">Assemblée</h3>
|
||||||
|
<div className="columns mt-1 is-multiline">
|
||||||
|
<UserCard className="column is-narrow"
|
||||||
|
nickname={data.nicknames[uuid]}
|
||||||
|
status={currentStatus}
|
||||||
|
email={user.email} />
|
||||||
|
{
|
||||||
|
Object.keys(data.peers).map(p => {
|
||||||
|
const now = new Date();
|
||||||
|
const lastHeartBeat = new Date(data.peers[p]);
|
||||||
|
|
||||||
|
if (p === uuid) return null;
|
||||||
|
|
||||||
|
if (now.getTime() > lastHeartBeat.getTime() + Config.conferenceHeartbeatInterval*2) {
|
||||||
|
forget(p);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nickname = data.nicknames[p] || '???';
|
||||||
|
const email = data.emails[p] || '';
|
||||||
|
return (
|
||||||
|
<UserCard key={`peer-${p}`} className="column is-narrow"
|
||||||
|
nickname={nickname}
|
||||||
|
status={data.statuses[p]}
|
||||||
|
email={email} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConferencePage;
|
||||||
|
|
||||||
|
export interface UserCardProps {
|
||||||
|
nickname: string
|
||||||
|
email: string
|
||||||
|
className?: string
|
||||||
|
status: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserCard:FunctionComponent<UserCardProps> = ({ nickname, email, className, status }) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="box">
|
||||||
|
<div className="has-text-centered">
|
||||||
|
<div className="mb-1">
|
||||||
|
{ !status ? <span className="icon"><i className="far fa-2x fa-meh-blank"></i></span> : null }
|
||||||
|
{ status === StatusHandRaised ? <span className="icon has-text-info"><i className="fa fa-2x fa-hand-paper"></i></span> : null }
|
||||||
|
{ status === StatusThumbsUp ? <span className="icon has-text-success"><i className="fa fa-2x fa-thumbs-up"></i></span> : null }
|
||||||
|
{ status === StatusNoVote ? <span className="icon has-text-warning"><i className="fa fa-2x fa-mitten"></i></span> : null }
|
||||||
|
{ status === StatusThumbsDown ? <span className="icon has-text-danger"><i className="fa fa-2x fa-thumbs-down"></i></span> : null }
|
||||||
|
</div>
|
||||||
|
<figure className="image is-128x128 is-inline-block">
|
||||||
|
<Gravatar className="is-rounded" email={email} />
|
||||||
|
</figure>
|
||||||
|
<h4 className="is-size-4">{nickname}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||||
|
import md5 from 'crypto-js/md5';
|
||||||
|
|
||||||
|
export interface GravatarProps {
|
||||||
|
className?: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAvatarUrl = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=128';
|
||||||
|
|
||||||
|
export const Gravatar:FunctionComponent<GravatarProps> = ({ className, email }) => {
|
||||||
|
const [ avatarUrl, setAvatarUrl ] = useState(defaultAvatarUrl);
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = md5(email.trim().toLowerCase());
|
||||||
|
setAvatarUrl(`https://www.gravatar.com/avatar/${hash}?d=mp&s=128`);
|
||||||
|
}, [email]);
|
||||||
|
return (
|
||||||
|
<img className={className} src={avatarUrl} />
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { WorkgroupsPanel } from './WorkgroupsPanel';
|
||||||
|
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
||||||
|
import { Timeline } from '../Timeline';
|
||||||
|
import { useEvents } from '../../gql/queries/event';
|
||||||
|
|
||||||
|
const from = new Date();
|
||||||
|
from.setDate(from.getDate() - 7);
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
const { events } = useEvents({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-4">
|
||||||
|
<DecisionSupportFilePanel />
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<WorkgroupsPanel />
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="panel is-info">
|
||||||
|
<div className="level panel-heading mb-0">
|
||||||
|
<div className="level-left">
|
||||||
|
<div className="level-item">
|
||||||
|
Ces 7 derniers jours
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<button disabled={true} className="button level-item is-outlined is-info is-inverted">
|
||||||
|
<i className="icon fa fa-sliders-h"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel-block">
|
||||||
|
<Timeline events={events} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { Dashboard } from './Dashboard';
|
||||||
|
|
||||||
|
export function DashboardPage() {
|
||||||
|
return (
|
||||||
|
<Page title={'Tableau de bord'}>
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<section className="mt-5">
|
||||||
|
<Dashboard />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardPage;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision';
|
||||||
|
import { ItemPanel, TabDefinition, Item } from '../ItemPanel';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { inWorkgroup } from '../../types/workgroup';
|
||||||
|
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
|
||||||
|
|
||||||
|
export function DecisionSupportFilePanel() {
|
||||||
|
const { user } = useUserProfile();
|
||||||
|
const { decisionSupportFiles } = useDecisionSupportFiles();
|
||||||
|
|
||||||
|
const tabs: TabDefinition[] = [
|
||||||
|
{
|
||||||
|
label: 'Mes dossiers en cours',
|
||||||
|
itemFilter: (item: Item) => {
|
||||||
|
const dsf = item as DecisionSupportFile;
|
||||||
|
return (dsf.status === DecisionSupportFileStatus.Draft || dsf.status === DecisionSupportFileStatus.Ready) && inWorkgroup(user, dsf.workgroup);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Brouillons',
|
||||||
|
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Draft
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'À voter',
|
||||||
|
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Ready
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Votés',
|
||||||
|
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Voted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Clos',
|
||||||
|
itemFilter: (item: Item) => (item as DecisionSupportFile).status === DecisionSupportFileStatus.Closed
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemPanel
|
||||||
|
className='is-link'
|
||||||
|
title="Dossiers"
|
||||||
|
newItemUrl="/decisions/new"
|
||||||
|
items={decisionSupportFiles}
|
||||||
|
tabs={tabs}
|
||||||
|
itemIconClassName='fas fa-folder'
|
||||||
|
itemKey={item => item.id}
|
||||||
|
itemLabel={item => item.title}
|
||||||
|
itemUrl={item => `/decisions/${item.id}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { } from 'react';
|
||||||
|
import { Workgroup, inWorkgroup } from '../../types/workgroup';
|
||||||
|
import { useWorkgroups } from '../../gql/queries/workgroups';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { ItemPanel, Item } from '../ItemPanel';
|
||||||
|
|
||||||
|
export function WorkgroupsPanel() {
|
||||||
|
const { workgroups } = useWorkgroups();
|
||||||
|
const { user } = useUserProfile();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "Mes groupes en cours",
|
||||||
|
itemFilter: (item: Item) => {
|
||||||
|
const wg = item as Workgroup;
|
||||||
|
return wg.closedAt === null && inWorkgroup(user, wg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Ouverts",
|
||||||
|
itemFilter: (item: Item) => !(item as Workgroup).closedAt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Clos",
|
||||||
|
itemFilter: (item: Item) => !!(item as Workgroup).closedAt
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemPanel
|
||||||
|
className='is-info'
|
||||||
|
title="Groupes de travail"
|
||||||
|
newItemUrl="/workgroups/new"
|
||||||
|
items={workgroups}
|
||||||
|
tabs={tabs}
|
||||||
|
itemIconClassName='fas fa-users'
|
||||||
|
itemKey={item => item.id}
|
||||||
|
itemLabel={item => item.name}
|
||||||
|
itemUrl={item => `/workgroups/${item.id}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useWorkgroups } from "../gql/queries/workgroups";
|
||||||
|
import { useDecisionSupportFiles } from "../gql/queries/dsf";
|
||||||
|
|
||||||
|
export interface DecisioSupportFileLinkProps {
|
||||||
|
decisionSupportFileId: number|string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DecisionSupportFileLink: FunctionComponent<DecisioSupportFileLinkProps> = ({ decisionSupportFileId }) => {
|
||||||
|
const { decisionSupportFiles } = useDecisionSupportFiles({
|
||||||
|
fetchPolicy: "cache-first",
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
ids: [decisionSupportFileId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = decisionSupportFiles.length > 0 ? decisionSupportFiles[0].title : `#${decisionSupportFileId}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/decisions/${decisionSupportFileId}`}>{title}</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { FunctionComponent, useState } from 'react';
|
||||||
|
import { DecisionSupportFile } from '../../types/decision';
|
||||||
|
|
||||||
|
export interface AppendixPanelProps {
|
||||||
|
dsf: DecisionSupportFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppendixPanel: FunctionComponent<AppendixPanelProps> = ({ dsf }) => {
|
||||||
|
return (
|
||||||
|
<nav className="panel">
|
||||||
|
<p className="panel-heading">
|
||||||
|
Annexes
|
||||||
|
</p>
|
||||||
|
<div className="panel-block">
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React, { FunctionComponent, useState, ChangeEvent, useEffect } from 'react';
|
||||||
|
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
||||||
|
import { useDebounce } from '../../hooks/useDebounce';
|
||||||
|
import { asDate } from '../../util/date';
|
||||||
|
|
||||||
|
export interface ClarificationSectionProps extends DecisionSupportFileUpdaterProps {};
|
||||||
|
|
||||||
|
const ClarificationSectionName = 'clarification';
|
||||||
|
|
||||||
|
export const ClarificationSection: FunctionComponent<ClarificationSectionProps> = ({ dsf, updateDSF, readOnly }) => {
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
changed: false,
|
||||||
|
section: {
|
||||||
|
objectives: '',
|
||||||
|
motivations: '',
|
||||||
|
scope: '',
|
||||||
|
nature: '',
|
||||||
|
deadline: undefined,
|
||||||
|
hasDeadline: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.changed) return;
|
||||||
|
updateDSF({ ...dsf, sections: { ...dsf.sections, [ClarificationSectionName]: { ...state.section }} })
|
||||||
|
setState(state => ({ ...state, changed: false }));
|
||||||
|
}, [state.changed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dsf.sections[ClarificationSectionName]) return;
|
||||||
|
setState(state => ({ ...state, changed: false, section: {...state.section, ...dsf.sections[ClarificationSectionName] }}));
|
||||||
|
}, [dsf.sections[ClarificationSectionName]]);
|
||||||
|
|
||||||
|
const onTitleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const title = (evt.currentTarget).value;
|
||||||
|
updateDSF({ ...dsf, title });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSectionAttrChange = (attrName: string, evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
const value = target.hasOwnProperty('checked') ? target.checked : target.value;
|
||||||
|
setState(state => ({ ...state, changed: true, section: {...state.section, [attrName]: value }}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeadlineChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const deadline = evt.currentTarget.valueAsDate;
|
||||||
|
setState(state => ({ ...state, changed: true, section: { ...state.section, deadline }}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Intitulé du dossier</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input is-medium" type="text" readOnly={readOnly} value={dsf.title} onChange={onTitleChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Quelle décision devons nous prendre ?</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea is-medium"
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={state.section.objectives}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'objectives')}
|
||||||
|
placeholder="Décrire globalement les tenants et aboutissants de la décision à prendre."
|
||||||
|
rows={10}>
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<p className="help is-info"><i className="fa fa-info-circle"></i> Ne pas essayer de rentrer trop dans les détails ici. Préférer l'utilisation des annexes et y faire référence.</p>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Pourquoi devons nous prendre cette décision ?</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea is-medium"
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={state.section.motivations}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'motivations')}
|
||||||
|
placeholder="Décrire pourquoi il est important de prendre cette décision."
|
||||||
|
rows={10}>
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer si des obligations légales pèsent sur cette prise de décision.</p>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Portée de la décision</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-medium">
|
||||||
|
<select
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'scope')}
|
||||||
|
value={state.section.scope}>
|
||||||
|
<option></option>
|
||||||
|
<option value="individual">Individuelle</option>
|
||||||
|
<option value="identified-group">Groupe identifié</option>
|
||||||
|
<option value="collective">Collective</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Nature de la décision</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-medium">
|
||||||
|
<select
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'nature')}
|
||||||
|
value={state.section.nature}>
|
||||||
|
<option></option>
|
||||||
|
<option value="operational">Opérationnelle</option>
|
||||||
|
<option value="tactic">Tactique</option>
|
||||||
|
<option value="strategic">Stratégique</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input type="checkbox"
|
||||||
|
className="is-medium"
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'hasDeadline')}
|
||||||
|
checked={state.section.hasDeadline} />
|
||||||
|
<span className="ml-1 has-text-weight-bold is-size-5">Existe t'il une échéance particulière pour cette décision ?</span>
|
||||||
|
</label>
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<input disabled={!state.section.hasDeadline}
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={state.section.deadline ? asDate(state.section.deadline).toISOString().substr(0, 10) : ''}
|
||||||
|
onChange={onDeadlineChange}
|
||||||
|
type="date" className="input is-medium" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { FunctionComponent, useState, ChangeEvent, useEffect } from 'react';
|
||||||
|
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
||||||
|
|
||||||
|
export interface DecisionReportSectionProps extends DecisionSupportFileUpdaterProps {};
|
||||||
|
|
||||||
|
const DecisionReportSectionName = 'decision-report';
|
||||||
|
|
||||||
|
export const DecisionReportSection: FunctionComponent<DecisionReportSectionProps> = ({ dsf, updateDSF, readOnly }) => {
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
changed: false,
|
||||||
|
section: {
|
||||||
|
report: "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.changed) return;
|
||||||
|
updateDSF({ ...dsf, sections: { ...dsf.sections, [DecisionReportSectionName]: { ...state.section }} })
|
||||||
|
setState(state => ({ ...state, changed: false }));
|
||||||
|
}, [state.changed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dsf.sections[DecisionReportSectionName]) return;
|
||||||
|
setState(state => ({ ...state, changed: false, section: {...state.section, ...dsf.sections[DecisionReportSectionName] }}));
|
||||||
|
}, [dsf.sections[DecisionReportSectionName]]);
|
||||||
|
|
||||||
|
const onSectionAttrChange = (attrName: string, evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
const value = target.hasOwnProperty('checked') ? target.checked : target.value;
|
||||||
|
setState(state => ({ ...state, changed: true, section: {...state.section, [attrName]: value }}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-medium">Compte rendu du vote</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea is-medium"
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={state.section.report}
|
||||||
|
onChange={onSectionAttrChange.bind(null, 'report')}
|
||||||
|
rows={20}>
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<p className="help is-info"><i className="fa fa-info-circle"></i> Penser à indiquer le résultat du vote et les éléments de contexte liés à la prise de décision.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,164 @@
|
||||||
|
import React, { FunctionComponent, useState, useEffect } from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { ClarificationSection } from './ClarificationSection';
|
||||||
|
import { MetadataPanel } from './MetadataPanel';
|
||||||
|
import { AppendixPanel } from './AppendixPanel';
|
||||||
|
import { DecisionSupportFile, newDecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision';
|
||||||
|
import { useParams, useHistory } from 'react-router';
|
||||||
|
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
|
||||||
|
import { useCreateDecisionSupportFileMutation, useUpdateDecisionSupportFileMutation } from '../../gql/mutations/dsf';
|
||||||
|
import { OptionsSection } from './OptionsSection';
|
||||||
|
import { useIsAuthorized } from '../../gql/queries/authorization';
|
||||||
|
import { TimelinePanel } from './TimelinePanel';
|
||||||
|
import { DecisionReportSection } from './DecisionReportSection';
|
||||||
|
import { RoutedTabs } from '../RoutedTabs';
|
||||||
|
|
||||||
|
export interface DecisionSupportFilePageProps {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DecisionSupportFilePage: FunctionComponent<DecisionSupportFilePageProps> = () => {
|
||||||
|
const { id } = useParams<any>();
|
||||||
|
const history = useHistory();
|
||||||
|
const { decisionSupportFiles } = useDecisionSupportFiles({
|
||||||
|
variables:{
|
||||||
|
filter: {
|
||||||
|
ids: id !== 'new' ? [id] : undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
dsf: newDecisionSupportFile(),
|
||||||
|
saved: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAuthorized } = useIsAuthorized({
|
||||||
|
variables: {
|
||||||
|
action: 'update',
|
||||||
|
object: {
|
||||||
|
decisionSupportFileId: state.dsf.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, id === 'new');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dsf = decisionSupportFiles.length > 0 && decisionSupportFiles[0].id === id ? decisionSupportFiles[0] : {};
|
||||||
|
setState(state => ({ ...state, dsf: { ...state.dsf, ...dsf }}))
|
||||||
|
}, [ decisionSupportFiles ]);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: "Clarifier la proposition",
|
||||||
|
icon: "fas fa-pen",
|
||||||
|
route: '/info',
|
||||||
|
render: () => (<ClarificationSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Explorer les options",
|
||||||
|
icon: "fas fa-search",
|
||||||
|
route: '/options',
|
||||||
|
render: () => (<OptionsSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Prendre la décision",
|
||||||
|
icon: "fas fa-person-booth",
|
||||||
|
route: '/vote',
|
||||||
|
render: () => (<DecisionReportSection readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateDSF = (dsf: DecisionSupportFile) => {
|
||||||
|
setState(state => {
|
||||||
|
return { ...state, saved: false, dsf: { ...state.dsf, ...dsf } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [ createDecisionSupportFile ] = useCreateDecisionSupportFileMutation();
|
||||||
|
const [ updateDecisionSupportFile ] = useUpdateDecisionSupportFileMutation();
|
||||||
|
|
||||||
|
const saveDSF = () => {
|
||||||
|
const changes = {
|
||||||
|
title: state.dsf.title !== '' ? state.dsf.title : undefined,
|
||||||
|
status: state.dsf.status,
|
||||||
|
workgroupId: state.dsf.workgroup ? state.dsf.workgroup.id : undefined,
|
||||||
|
sections: state.dsf.sections,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!changes.workgroupId) return;
|
||||||
|
|
||||||
|
if (state.dsf.id === '') {
|
||||||
|
createDecisionSupportFile({
|
||||||
|
variables: { changes },
|
||||||
|
}).then(({ data }) => {
|
||||||
|
history.push(`/decisions/${data.createDecisionSupportFile.id}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateDecisionSupportFile({
|
||||||
|
variables: { changes, id: state.dsf.id },
|
||||||
|
}).then(({ data }) => {
|
||||||
|
setState(state => {
|
||||||
|
return { ...state, saved: true, dsf: { ...state.dsf, ...data.updateDecisionSupportFile } };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSave = !!state.dsf.workgroup && !state.saved;
|
||||||
|
const isNew = state.dsf.id === '';
|
||||||
|
const isClosed = state.dsf.status === DecisionSupportFileStatus.Closed;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Dossier d'Aide à la Décision">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<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">Dossier d'Aide à la Décision</h3>
|
||||||
|
</div>
|
||||||
|
</div> :
|
||||||
|
<div className="level-item">
|
||||||
|
<div>
|
||||||
|
<h2 className="is-size-3 title is-spaced">{state.dsf.title} <span className={`ml-3 tag is-warning is-medium ${ isAuthorized ? 'is-hidden' : ''}`}>Lecture seule</span></h2>
|
||||||
|
<h3 className="is-size-5 subtitle">Dossier d'Aide à la Décision <span className="is-italic">{ isClosed ? '(clos)' : null }</span></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<div className="level-item buttons">
|
||||||
|
{
|
||||||
|
isAuthorized ?
|
||||||
|
<button className="button is-medium is-success" disabled={!canSave} onClick={saveDSF}>
|
||||||
|
<span className="icon"><i className="fa fa-save"></i></span>
|
||||||
|
<span>Enregistrer</span>
|
||||||
|
</button> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className="columns mt-3">
|
||||||
|
<div className="column is-8">
|
||||||
|
<div className="box">
|
||||||
|
<RoutedTabs baseRoute={`/decisions/${id}`} tabs={tabs} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<MetadataPanel readOnly={!isAuthorized} dsf={state.dsf} updateDSF={updateDSF} />
|
||||||
|
{/* <AppendixPanel dsf={state.dsf} /> */}
|
||||||
|
<TimelinePanel dsf={state.dsf} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DecisionSupportFilePage;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DecisionSupportFile } from "../../types/decision";
|
||||||
|
|
||||||
|
export interface DecisionSupportFileUpdaterProps {
|
||||||
|
dsf: DecisionSupportFile
|
||||||
|
updateDSF: (dsf: DecisionSupportFile) => void
|
||||||
|
readOnly: boolean
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
import React, { FunctionComponent, useState, useEffect, ChangeEvent } from 'react';
|
||||||
|
import { DecisionSupportFile, DecisionSupportFileStatus } from '../../types/decision';
|
||||||
|
import { useWorkgroups } from '../../gql/queries/workgroups';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { inWorkgroup } from '../../types/workgroup';
|
||||||
|
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
||||||
|
import { asDate, formatDate } from '../../util/date';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface MetadataPanelProps extends DecisionSupportFileUpdaterProps {};
|
||||||
|
|
||||||
|
export const MetadataPanel: FunctionComponent<MetadataPanelProps> = ({ dsf, updateDSF, readOnly }) => {
|
||||||
|
const { user } = useUserProfile();
|
||||||
|
const { workgroups } = useWorkgroups();
|
||||||
|
|
||||||
|
const [ userOpenedWorkgroups, setUserOpenedWorkgroups ] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = workgroups.filter(wg => !wg.closedAt && inWorkgroup(user, wg));
|
||||||
|
setUserOpenedWorkgroups(filtered);
|
||||||
|
}, [workgroups, user])
|
||||||
|
|
||||||
|
const onStatusChanged = (evt: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const status = evt.currentTarget.value as DecisionSupportFileStatus;
|
||||||
|
updateDSF({ ...dsf, status });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWorkgroupChanged = (evt: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const workgroupId = evt.currentTarget.value;
|
||||||
|
const workgroup = workgroups.find(wg => wg.id === workgroupId);
|
||||||
|
updateDSF({ ...dsf, workgroup });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="panel">
|
||||||
|
<p className="panel-heading">
|
||||||
|
Métadonnées
|
||||||
|
</p>
|
||||||
|
<div className="panel-block">
|
||||||
|
<div style={{width:'100%'}}>
|
||||||
|
<div className="field">
|
||||||
|
<div className="label">Groupe de travail</div>
|
||||||
|
{
|
||||||
|
readOnly && dsf.workgroup !== null ?
|
||||||
|
<Link to={`/workgroups/${dsf.workgroup.id}`}>{dsf.workgroup.name}</Link> :
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={onWorkgroupChanged}
|
||||||
|
value={dsf.workgroup ? dsf.workgroup.id : ''}>
|
||||||
|
<option></option>
|
||||||
|
{
|
||||||
|
userOpenedWorkgroups.map(wg => {
|
||||||
|
return (
|
||||||
|
<option key={`wg-${wg.id}`} value={wg.id}>{wg.name}</option>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<div className="label">Statut</div>
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={onStatusChanged}
|
||||||
|
value={dsf.status}>
|
||||||
|
<option value="draft">Brouillon</option>
|
||||||
|
<option value="ready">Prêt à voter</option>
|
||||||
|
<option value="voted">Voté</option>
|
||||||
|
<option value="closed">Clôs</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<div className="label">Créé le</div>
|
||||||
|
<div className="control">
|
||||||
|
<p>{formatDate(dsf.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<div className="label">Voté le</div>
|
||||||
|
<div className="control">
|
||||||
|
<p>{dsf.votedAt ? formatDate(dsf.votedAt) : '--'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,158 @@
|
||||||
|
import React, { FunctionComponent, useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||||
|
import { DecisionSupportFileUpdaterProps } from './DecisionSupportFileUpdaterProps';
|
||||||
|
import { base58UUID } from "../../util/uuid";
|
||||||
|
|
||||||
|
export interface OptionsSectionProps extends DecisionSupportFileUpdaterProps {};
|
||||||
|
|
||||||
|
const OptionsSectionName = 'options';
|
||||||
|
|
||||||
|
export const OptionsSection: FunctionComponent<OptionsSectionProps> = ({ dsf, updateDSF, readOnly }) => {
|
||||||
|
interface OptionsSectionState {
|
||||||
|
changed: boolean
|
||||||
|
section: OptionsSection
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionsSection {
|
||||||
|
options: Option[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
pros: string
|
||||||
|
cons: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ state, setState ] = useState<OptionsSectionState>({
|
||||||
|
changed: false,
|
||||||
|
section: {
|
||||||
|
options: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.changed) return;
|
||||||
|
updateDSF({ ...dsf, sections: { ...dsf.sections, [OptionsSectionName]: { ...state.section }} })
|
||||||
|
setState(state => ({ ...state, changed: false }));
|
||||||
|
}, [state.changed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dsf.sections[OptionsSectionName]) return;
|
||||||
|
setState(state => ({ ...state, changed: false, section: {...state.section, ...dsf.sections[OptionsSectionName] }}));
|
||||||
|
}, [dsf.sections[OptionsSectionName]]);
|
||||||
|
|
||||||
|
function newOption(label: string, pros: string, cons: string): Option {
|
||||||
|
return {
|
||||||
|
id: base58UUID(),
|
||||||
|
label,
|
||||||
|
pros,
|
||||||
|
cons
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAddOptionClick = (evt: MouseEvent) => {
|
||||||
|
const options = JSON.parse(JSON.stringify(state.section.options))
|
||||||
|
const option = newOption("Décision", "", "");
|
||||||
|
options.push(option);
|
||||||
|
setState(state => ({ ...state, changed: true, section: { ...state.section, options }}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOptionChange = (id: number, attrName: string, evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const target = evt.currentTarget;
|
||||||
|
const value = target.hasOwnProperty('checked') ? target.checked : target.value;
|
||||||
|
const options = JSON.parse(JSON.stringify(state.section.options))
|
||||||
|
options[id][attrName] = value;
|
||||||
|
setState(state => ({ ...state, changed: true, section: { ...state.section, options }}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveOptionClick = (id: number, evt: MouseEvent) => {
|
||||||
|
if(confirm('Voulez-vous supprimer cette option ?')){
|
||||||
|
const options = JSON.parse(JSON.stringify(state.section.options))
|
||||||
|
options.splice(id, 1);
|
||||||
|
setState(state => ({ ...state, changed: true, section: { ...state.section, options }}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h4 id="options-section" className="is-size-4 title is-spaced">Explorer les options</h4>
|
||||||
|
<div className="table-container">
|
||||||
|
<table className={`table is-bordered is-striped is-hoverable is-fullwidth`}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Décision</th>
|
||||||
|
<th>Pours</th>
|
||||||
|
<th>Contres</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
state.section.options.map((o, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={`option-${o.id}`}>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
disabled={readOnly}
|
||||||
|
onClick={onRemoveOptionClick.bind(null, index)}
|
||||||
|
className="button is-danger is-small is-outlined">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea className="textarea"
|
||||||
|
readOnly={readOnly}
|
||||||
|
value={o.label}
|
||||||
|
onChange={onOptionChange.bind(null, index, 'label')}
|
||||||
|
placeholder="Décrire cette décision."
|
||||||
|
rows={10}>
|
||||||
|
</textarea>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea className="textarea is-success"
|
||||||
|
value={o.pros}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onChange={onOptionChange.bind(null, index, 'pros')}
|
||||||
|
placeholder="Décrire les avantages de cette décision."
|
||||||
|
rows={10}>
|
||||||
|
</textarea>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea className="textarea is-danger"
|
||||||
|
value={o.cons}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onChange={onOptionChange.bind(null, index, 'cons')}
|
||||||
|
placeholder="Décrire les désavantages de cette décision."
|
||||||
|
rows={10}>
|
||||||
|
</textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state.section.options.length === 0 ?
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td colSpan={4}>Aucune option pour l'instant.</td>
|
||||||
|
</tr> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<button
|
||||||
|
disabled={readOnly}
|
||||||
|
className="button is-primary is-pulled-right"
|
||||||
|
onClick={onAddOptionClick}>
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { FunctionComponent, useState } from 'react';
|
||||||
|
import { DecisionSupportFile } from '../../types/decision';
|
||||||
|
import { Timeline } from '../Timeline';
|
||||||
|
import { useEvents } from '../../gql/queries/event';
|
||||||
|
|
||||||
|
export interface TimelinePanelProps {
|
||||||
|
dsf: DecisionSupportFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimelinePanel: FunctionComponent<TimelinePanelProps> = ({ dsf }) => {
|
||||||
|
const { events } = useEvents({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
objectType: 'dsf',
|
||||||
|
objectId: dsf.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel">
|
||||||
|
<div className="level panel-heading mb-0">
|
||||||
|
<div className="level-left">
|
||||||
|
<div className="level-item">
|
||||||
|
Suivi des opérations
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel-block">
|
||||||
|
<Timeline events={events} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { WelcomeContent } from './WelcomeContent';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { useLoggedIn } from '../../hooks/useLoggedIn';
|
||||||
|
|
||||||
|
export function HomePage() {
|
||||||
|
const loggedIn = useLoggedIn();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loggedIn) history.push('/dashboard');
|
||||||
|
}, [loggedIn])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Accueil">
|
||||||
|
<WelcomeContent />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { FunctionComponent, Fragment } from "react";
|
||||||
|
|
||||||
|
export interface WelcomeContentProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WelcomeContent: FunctionComponent<WelcomeContentProps> = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section className="hero is-normal is-light is-bold">
|
||||||
|
<div className="hero-body has-text-centered">
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="title">
|
||||||
|
Bienvenue sur Daddy !
|
||||||
|
</h1>
|
||||||
|
<h2 className="subtitle">
|
||||||
|
L'outil de suivi de la vie d'entreprise démocratique.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className="box cta">
|
||||||
|
<p className="has-text-centered">
|
||||||
|
<span className="tag is-info">Attention</span> Le service est actuellement en alpha. L'accès est restreint aux adresses autorisées.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<section className="container mt-5">
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="card is-shady">
|
||||||
|
<div className="card-image has-text-centered">
|
||||||
|
<i className="fa fa-at mt-5" style={{fontSize: '8rem'}}></i>
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<div className="content">
|
||||||
|
<h4>Une adresse courriel et c'est parti !</h4>
|
||||||
|
<p>Pas de création de compte, pas de mot de passe à retenir. Entrez votre adresse courriel et commencez directement à travailler !</p>
|
||||||
|
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="card is-shady">
|
||||||
|
<div className="card-image has-text-centered">
|
||||||
|
<i className="fa fa-edit mt-5" style={{fontSize: '8rem'}}></i>
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<div className="content">
|
||||||
|
<h4>Préparer vos dossiers d'aide à la décision</h4>
|
||||||
|
<p>Une décision à prendre ? Un nouveau projet à lancer ? Crééz votre groupe de travail et rédigez un dossier pour faciliter la prise de décision collective !</p>
|
||||||
|
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="card is-shady">
|
||||||
|
<div className="card-image has-text-centered">
|
||||||
|
<i className="fa fa-users mt-5" style={{fontSize: '8rem'}}></i>
|
||||||
|
</div>
|
||||||
|
<div className="card-content">
|
||||||
|
<div className="content">
|
||||||
|
<h4>Travaillez collaborativement</h4>
|
||||||
|
<p>Éditez à plusieurs vos dossiers d'aide à la décision, suivi l'avancée des débats et retrouvez simplement les décisions prises dans l'entreprise.</p>
|
||||||
|
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React, { FunctionComponent, useState, useEffect } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: string
|
||||||
|
[propName: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabDefinition {
|
||||||
|
label: string
|
||||||
|
itemFilter?: (item: Item) => boolean
|
||||||
|
}
|
||||||
|
export interface ItemPanelProps {
|
||||||
|
className?: string
|
||||||
|
itemIconClassName?: string
|
||||||
|
title?: string
|
||||||
|
newItemUrl: string
|
||||||
|
isLoading?: boolean
|
||||||
|
items: Item[]
|
||||||
|
tabs?: TabDefinition[],
|
||||||
|
itemKey: (item: Item, index: number) => string
|
||||||
|
itemLabel: (item: Item, index: number) => string
|
||||||
|
itemUrl: (item: Item, index: number) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemPanel: FunctionComponent<ItemPanelProps> = (props) => {
|
||||||
|
const {
|
||||||
|
title, className, newItemUrl,
|
||||||
|
itemKey, itemLabel,
|
||||||
|
itemIconClassName, itemUrl,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const items = props.items || [];
|
||||||
|
const tabs = props.tabs || [];
|
||||||
|
|
||||||
|
const [ state, setState ] = useState({ selectedTab: 0, filteredItems: [] });
|
||||||
|
|
||||||
|
const filterItemsForTab = (tab: TabDefinition, items: Item[]) => {
|
||||||
|
const itemFilter = tab && typeof tab.itemFilter === 'function' ? tab.itemFilter : () => true;
|
||||||
|
return items.filter(itemFilter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectTab = (tabIndex: number) => {
|
||||||
|
setState(state => {
|
||||||
|
const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[tabIndex] : null;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedTab: tabIndex,
|
||||||
|
filteredItems: filterItemsForTab(newTab, items)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState(state => {
|
||||||
|
const { tabs, items } = props;
|
||||||
|
const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[state.selectedTab] : null;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filteredItems: filterItemsForTab(newTab, items),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [items, tabs]);
|
||||||
|
|
||||||
|
const itemElements = state.filteredItems.map((item: Item, i: number) => {
|
||||||
|
return (
|
||||||
|
<Link to={itemUrl(item, i)} key={`item-${itemKey(item, i)}`} className="panel-block">
|
||||||
|
<span className="panel-icon">
|
||||||
|
<i className={itemIconClassName} aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
{itemLabel(item, i)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`panel ${className}`}>
|
||||||
|
<div className="level is-mobile panel-heading mb-0">
|
||||||
|
<div className="level-left">
|
||||||
|
<p className="level-item">{title}</p>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<Link to={newItemUrl} className="button level-item is-outlined is-info is-inverted">
|
||||||
|
<i className="icon fa fa-plus"></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel-block">
|
||||||
|
<p className="control has-icons-left">
|
||||||
|
<input disabled={true} className="input" type="text" placeholder="Filtrer..." />
|
||||||
|
<span className="icon is-left">
|
||||||
|
<i className="fas fa-search" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="panel-tabs">
|
||||||
|
{
|
||||||
|
tabs.map((tab, i) => {
|
||||||
|
return (
|
||||||
|
<a key={`workgroup-tab-${i}`}
|
||||||
|
onClick={selectTab.bind(null, i)}
|
||||||
|
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||||
|
{tab.label}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
itemElements.length > 0 ?
|
||||||
|
itemElements :
|
||||||
|
<a className="panel-block has-text-centered is-block">
|
||||||
|
<em>Aucun élément pour l'instant.</em>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { FunctionComponent, useEffect } from "react";
|
||||||
|
import { saveLoggedIn } from "../hooks/useLoggedIn";
|
||||||
|
import { Config } from "../config";
|
||||||
|
|
||||||
|
export interface LogoutPageProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogoutPage: FunctionComponent<LogoutPageProps> = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
saveLoggedIn(false);
|
||||||
|
window.location.replace(Config.logoutURL);
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogoutPage;
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
import logo from '../resources/logo.svg';
|
||||||
|
import { Config } from '../config';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useLoggedIn } from '../hooks/useLoggedIn';
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
const loggedIn = useLoggedIn();
|
||||||
|
const [ isActive, setActive ] = useState(false);
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
setActive(active => !active);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<div className="navbar-brand">
|
||||||
|
<Link className="navbar-item" to="/">
|
||||||
|
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||||
|
<h1 className="is-size-4">Daddy</h1>
|
||||||
|
</Link>
|
||||||
|
<a role="button"
|
||||||
|
className={`navbar-burger ${isActive ? 'is-active' : ''}`}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
aria-label="menu"
|
||||||
|
aria-expanded="false">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||||
|
<div className="navbar-start">
|
||||||
|
{
|
||||||
|
loggedIn ?
|
||||||
|
<React.Fragment>
|
||||||
|
<Link to="/dashboard" className="navbar-item">
|
||||||
|
<i className="fa fa-columns"></i> Tableau de bord
|
||||||
|
</Link>
|
||||||
|
<Link to="/conference" className="navbar-item">
|
||||||
|
<i className="fa fa-users"></i> Conférence
|
||||||
|
</Link>
|
||||||
|
</React.Fragment> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="navbar-end">
|
||||||
|
<div className="navbar-item">
|
||||||
|
<div className="buttons">
|
||||||
|
{
|
||||||
|
loggedIn ?
|
||||||
|
<Fragment>
|
||||||
|
<Link to="/profile" className="button">
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-user"></i>
|
||||||
|
</span>
|
||||||
|
<span>Mon profil</span>
|
||||||
|
</Link>
|
||||||
|
<Link className="button is-warning is-small" to="/logout">
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-sign-out-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span>Déconnexion</span>
|
||||||
|
</Link>
|
||||||
|
</Fragment> :
|
||||||
|
<a className="button is-primary" href={Config.loginURL}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-sign-in-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span>S'identifier</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -25,6 +25,6 @@ export class Page extends React.PureComponent<PageProps> {
|
||||||
|
|
||||||
updateTitle() {
|
updateTitle() {
|
||||||
const { title } = this.props;
|
const { title } = this.props;
|
||||||
if (title !== undefined) window.document.title = title;
|
if (title !== undefined) window.document.title = title + ' - Daddy';
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { FunctionComponent, Component, ReactType } from "react"
|
||||||
|
import { Route, Redirect, RouteProps } from "react-router"
|
||||||
|
import { useLoggedIn } from "../hooks/useLoggedIn";
|
||||||
|
|
||||||
|
export interface PrivateRouteProps extends RouteProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateRoute: FunctionComponent<PrivateRouteProps> = ({component: Component, ...rest}) => {
|
||||||
|
const loggedIn = useLoggedIn();
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...rest}
|
||||||
|
render={(props) => loggedIn === true
|
||||||
|
? <Component {...props} />
|
||||||
|
: <Redirect to={{pathname: '/', state: {from: props.location}}} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { UserForm } from '../UserForm';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { useUserProfile } from '../../gql/queries/profile';
|
||||||
|
import { useUpdateUserProfileMutation } from '../../gql/mutations/profile';
|
||||||
|
|
||||||
|
export function ProfilePage() {
|
||||||
|
const { user, loading } = useUserProfile();
|
||||||
|
const [ updateProfile, updateUserProfileMutation ] = useUpdateUserProfileMutation();
|
||||||
|
const isLoading = updateUserProfileMutation.loading || loading;
|
||||||
|
|
||||||
|
const onUserChange = (user: User) => {
|
||||||
|
if (user.name !== user.name) {
|
||||||
|
updateProfile({ variables: {changes: { name: user.name }}});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Mon profil">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<section className="section">
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-6 is-offset-3">
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||||
|
{ !isLoading ? <UserForm onChange={onUserChange} user={user} /> : null }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfilePage;
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { FunctionComponent, ReactNode, useEffect, useState } from 'react';
|
||||||
|
import { useHistory, useLocation, useRouteMatch } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface Tab {
|
||||||
|
route: string
|
||||||
|
name: string
|
||||||
|
icon ?: string
|
||||||
|
render: (tab: Tab) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoutedTabsProps {
|
||||||
|
tabs: Tab[]
|
||||||
|
baseRoute?: string
|
||||||
|
defaultTabIndex?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const RoutedTabs: FunctionComponent<RoutedTabsProps> = ({ tabs, baseRoute, defaultTabIndex }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const tabRoute = (route: string): string => {
|
||||||
|
return `${baseRoute}${route}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [ selectedTabIndex, setSelectedTabIndex ] = useState(defaultTabIndex || 0);
|
||||||
|
const expectedTab = tabs[selectedTabIndex];
|
||||||
|
const expectedTabRoute = tabRoute(expectedTab.route);
|
||||||
|
|
||||||
|
let matchExpectedTabRoute = useRouteMatch(expectedTabRoute);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (matchExpectedTabRoute) return;
|
||||||
|
|
||||||
|
const newTabIndex = tabs.findIndex(t => location.pathname === tabRoute(t.route));
|
||||||
|
|
||||||
|
if (newTabIndex !== -1) {
|
||||||
|
selectTab(newTabIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.push(expectedTabRoute);
|
||||||
|
}, [matchExpectedTabRoute]);
|
||||||
|
|
||||||
|
const selectTab = (tabIndex: number) => {
|
||||||
|
setSelectedTabIndex(tabIndex);
|
||||||
|
const newTab = tabs[tabIndex];
|
||||||
|
history.push(tabRoute(newTab.route));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="tabs is-medium is-boxed">
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
tabs.map((t: Tab, i: number) => {
|
||||||
|
return (
|
||||||
|
<li key={`tab-${i}`} className={`has-background-white ${selectedTabIndex === i ? 'is-active': ''}`}
|
||||||
|
onClick={selectTab.bind(null, i)}>
|
||||||
|
<a>
|
||||||
|
{
|
||||||
|
t.icon ?
|
||||||
|
<span className="icon is-small"><i className={t.icon} aria-hidden="true"></i></span> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<span>{t.name}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{ expectedTab.render(expectedTab) }
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import { formatDate } from "../util/date";
|
||||||
|
import { Event } from "../types/event";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { WorkgroupLink } from "./WorkgroupLink";
|
||||||
|
import { DecisionSupportFileLink } from "./DecisionSupportFileLink";
|
||||||
|
|
||||||
|
export interface TimelineProps {
|
||||||
|
events?: Event[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Timeline: FunctionComponent<TimelineProps> = ({ events }) => {
|
||||||
|
events = debounceEvents(events) || [];
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="timeline" style={{width: '100%'}}>
|
||||||
|
{
|
||||||
|
events.map(evt => {
|
||||||
|
return (
|
||||||
|
<div key={evt.id} className="timeline-item">
|
||||||
|
{renderEventMarker(evt)}
|
||||||
|
<div className="timeline-content">
|
||||||
|
<p className="heading">{formatDate(evt.createdAt)}</p>
|
||||||
|
{renderEventContent(evt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
events.length === 0 ?
|
||||||
|
<p className="has-text-centered is-italic mb-1 mt-1">Aucun évènement.</p> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounceEvents(events: Event[]): Event[] {
|
||||||
|
const debounced = [];
|
||||||
|
for(let evt: Event, i = 0; (evt = events[i]); ++i) {
|
||||||
|
const prev = i > 0 ? events[i-1] : null;
|
||||||
|
|
||||||
|
if (!prev) {
|
||||||
|
debounced.push(evt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSame = evt.objectId === prev.objectId &&
|
||||||
|
evt.objectType === prev.objectType &&
|
||||||
|
evt.type === prev.type &&
|
||||||
|
evt.user.id === prev.user.id
|
||||||
|
;
|
||||||
|
|
||||||
|
if (isSame) continue;
|
||||||
|
|
||||||
|
debounced.push(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventMarkerMap = {
|
||||||
|
"closed": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-danger">
|
||||||
|
<i className="fa fa-times"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"created": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-success">
|
||||||
|
<i className="fa fa-plus"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"updated": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-info">
|
||||||
|
<i className="fa fa-pen"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"title-changed": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-info">
|
||||||
|
<i className="fa fa-pen"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"status-changed": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-primary">
|
||||||
|
<i className="fa fa-star"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"joined": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-info">
|
||||||
|
<i className="fa fa-users"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"leaved": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-warning">
|
||||||
|
<i className="fas fa-users-slash"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"voted": (evt:Event) => (
|
||||||
|
<div className="timeline-marker is-icon is-success">
|
||||||
|
<i className="fas fa-thumbs-up"></i>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEventMarker(evt: Event) {
|
||||||
|
const render = eventMarkerMap[evt.type];
|
||||||
|
if (!render) return ( <div className="timeline-marker"></div> );
|
||||||
|
return render(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventContentMap = {
|
||||||
|
"created": {
|
||||||
|
"workgroup": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le groupe de travail `}</span>
|
||||||
|
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a créé le dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title-changed": {
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le titre du dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status-changed": {
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le statut du dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"joined": {
|
||||||
|
"workgroup": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a rejoint le groupe de travail `}</span>
|
||||||
|
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"workgroup": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a mis à jour le groupe de travail `}</span>
|
||||||
|
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a modifié le dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"leaved": {
|
||||||
|
"workgroup": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a quitté le groupe de travail `}</span>
|
||||||
|
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"closed": {
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a clos le dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"workgroup": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`${evt.user.name ? evt.user.name : evt.user.email} a clos le groupe de travail `}</span>
|
||||||
|
"<WorkgroupLink workgroupId={evt.objectId} />".
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"voted": {
|
||||||
|
"dsf": (evt:Event) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<span>{`Le dossier d'aide à la décision `}</span>
|
||||||
|
"<DecisionSupportFileLink decisionSupportFileId={evt.objectId} />"
|
||||||
|
<span> a été voté.</span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderEventContent(evt: Event) {
|
||||||
|
const eventTypeMap = eventContentMap[evt.type];
|
||||||
|
const render = eventTypeMap && eventTypeMap[evt.objectType];
|
||||||
|
|
||||||
|
if (!eventTypeMap || !render) {
|
||||||
|
return (
|
||||||
|
<span className="is-italic">{`Type d'évènement "${evt.type}/${evt.objectType}" inconnu.`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(evt);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { Config } from '../../config';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
|
||||||
|
export interface UnauthorizedPageProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnauthorizedPage:FunctionComponent<UnauthorizedPageProps> = () => {
|
||||||
|
return (
|
||||||
|
<Page title="Non autorisé">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<section className="section">
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-6 is-offset-3">
|
||||||
|
<div className="message is-danger">
|
||||||
|
<div className="message-header">
|
||||||
|
<p><i className="fa fa-ban"></i> Non autorisé</p>
|
||||||
|
</div>
|
||||||
|
<div className="message-body">
|
||||||
|
<p>Vous n'êtes pas autorisé à accéder à cette page.</p>
|
||||||
|
<br />
|
||||||
|
<p>Votre compte est peut être désactivé, votre adresse courriel ne fait peut être
|
||||||
|
pas partie des domaines autorisés ou vous n'avez peut être pas les droits nécessaires pour effectuer cette opération.</p>
|
||||||
|
<div className="has-text-centered mt-5">
|
||||||
|
<a href={Config.logoutURL} className="is-warning button"><i className="fa fa-sign-out-alt"></i> Forcer la déconnexion</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnauthorizedPage;
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||||
|
import { User } from '../types/user';
|
||||||
|
import { formatDate } from '../util/date';
|
||||||
|
|
||||||
|
export interface UserFormProps {
|
||||||
|
user: User
|
||||||
|
onChange?: (user: User) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserForm({ user, onChange }: UserFormProps) {
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
changed: false,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSaveClick = () => {
|
||||||
|
if (!state.changed) return;
|
||||||
|
if (typeof onChange !== 'function') return;
|
||||||
|
onChange(state.user);
|
||||||
|
setState(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
changed: false,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||||
|
const value = evt.currentTarget.value;
|
||||||
|
setState(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
changed: true,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[attrName]: value,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Nom d'utilisateur</label>
|
||||||
|
<div className="control">
|
||||||
|
<input type="text" className="input" value={state.user.name}
|
||||||
|
onChange={onUserAttrChange.bind(null, "name")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Adresse courriel</label>
|
||||||
|
<div className="control">
|
||||||
|
<p className="input is-static">{state.user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Date de dernière connexion</label>
|
||||||
|
<div className="control">
|
||||||
|
<p className="input is-static">{formatDate(state.user.connectedAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Date de création</label>
|
||||||
|
<div className="control">
|
||||||
|
<p className="input is-static">{formatDate(state.user.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="buttons is-right">
|
||||||
|
<button disabled={!state.changed}
|
||||||
|
className="button is-primary" onClick={onSaveClick}>Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useWorkgroups } from "../gql/queries/workgroups";
|
||||||
|
|
||||||
|
export interface WorkgroupLinkProps {
|
||||||
|
workgroupId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkgroupLink: FunctionComponent<WorkgroupLinkProps> = ({ workgroupId }) => {
|
||||||
|
const { workgroups } = useWorkgroups({
|
||||||
|
fetchPolicy: "cache-first",
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
ids: [workgroupId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const workgroupName = workgroups.length > 0 ? workgroups[0].name : `#${workgroupId}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/workgroups/${workgroupId}`}>{workgroupName}</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useDecisionSupportFiles } from '../../gql/queries/dsf';
|
||||||
|
import { DecisionSupportFile } from '../../types/decision';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { DecisionSupportFileLink } from '../DecisionSupportFileLink';
|
||||||
|
import { WorkgroupLink } from '../WorkgroupLink';
|
||||||
|
|
||||||
|
export interface DecisionSupportFilePanelProps {
|
||||||
|
workgroupId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DecisionSupportFilePanel: FunctionComponent<DecisionSupportFilePanelProps> = ({ workgroupId }) => {
|
||||||
|
const { decisionSupportFiles } = useDecisionSupportFiles({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
workgroups: [workgroupId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="panel">
|
||||||
|
<p className="panel-heading">
|
||||||
|
Dossiers d'aide à la décision
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
decisionSupportFiles.map((dsf: DecisionSupportFile) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/decisions/${dsf.id}`} key={`dsf-${dsf.id}`} className="panel-block">
|
||||||
|
<span className="panel-icon">
|
||||||
|
<i className="fas fa-file" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span>{dsf.title}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
decisionSupportFiles.length === 0 ?
|
||||||
|
<a className="panel-block has-text-centered is-block">
|
||||||
|
<p className="is-italic">Aucun dossier pour l'instant.</p>
|
||||||
|
</a> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { useIsAuthorized } from '../../gql/queries/authorization';
|
||||||
|
import { formatDate } from '../../util/date';
|
||||||
|
|
||||||
|
export interface InfoFormProps {
|
||||||
|
workgroup: Workgroup
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAuthorized } = useIsAuthorized({
|
||||||
|
variables: {
|
||||||
|
action: 'update',
|
||||||
|
object: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, state.workgroup.id === '' ? true : false);
|
||||||
|
|
||||||
|
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}
|
||||||
|
disabled={!isAuthorized}
|
||||||
|
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">{formatDate(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">{formatDate(state.workgroup.closedAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<div className="buttons is-right">
|
||||||
|
<button disabled={!state.changed || !isAuthorized}
|
||||||
|
className="button is-success" onClick={onSaveClick}>
|
||||||
|
<span>Enregistrer</span>
|
||||||
|
<span className="icon"><i className="fa fa-save"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { InfoForm } from './InfoForm';
|
||||||
|
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">
|
||||||
|
{ !isLoading ? <InfoForm workgroup={workgroup} onChange={onWorkgroupChange} /> : null }
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { FunctionComponent, useState } from 'react';
|
||||||
|
import { DecisionSupportFile } from '../../types/decision';
|
||||||
|
import { Timeline } from '../Timeline';
|
||||||
|
import { useEvents } from '../../gql/queries/event';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
|
||||||
|
export interface TimelinePanelProps {
|
||||||
|
workgroup: Workgroup,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimelinePanel: FunctionComponent<TimelinePanelProps> = ({ workgroup }) => {
|
||||||
|
const { events } = useEvents({
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
objectType: 'workgroup',
|
||||||
|
objectId: workgroup.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel">
|
||||||
|
<div className="level panel-heading mb-0">
|
||||||
|
<div className="level-left">
|
||||||
|
<div className="level-item">
|
||||||
|
Suivi des opérations
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="panel-block">
|
||||||
|
<Timeline events={events} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React, { useEffect, useState, Fragment } from 'react';
|
||||||
|
import { Page } from '../Page';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { useWorkgroups } from '../../gql/queries/workgroups';
|
||||||
|
import { useUserProfile } 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, useCloseWorkgroupMutation } from '../../gql/mutations/workgroups';
|
||||||
|
import { TimelinePanel } from './TimelinePanel';
|
||||||
|
import { DecisionSupportFilePanel } from './DecisionSupportFilePanel';
|
||||||
|
|
||||||
|
export function WorkgroupPage() {
|
||||||
|
const { id } = useParams<any>();
|
||||||
|
const { workgroups } = useWorkgroups({
|
||||||
|
variables:{
|
||||||
|
filter: {
|
||||||
|
ids: [id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { user } = useUserProfile();
|
||||||
|
|
||||||
|
const [ joinWorkgroup ] = useJoinWorkgroupMutation();
|
||||||
|
const [ leaveWorkgroup ] = useLeaveWorkgroupMutation();
|
||||||
|
const [ closeWorkgroup ] = useCloseWorkgroupMutation();
|
||||||
|
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
userProfileId: '',
|
||||||
|
workgroup: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
closedAt: null,
|
||||||
|
createdAt: null,
|
||||||
|
members: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState(state => ({...state, workgroup:{ ...state.workgroup, ...workgroups[0]}}));
|
||||||
|
}, [workgroups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState(state => ({...state, userProfileId: user.id }));
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const onJoinWorkgroupClick = () => {
|
||||||
|
joinWorkgroup({
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeaveWorkgroupClick = () => {
|
||||||
|
leaveWorkgroup({
|
||||||
|
update: (cache, result) => {
|
||||||
|
cache.modify({
|
||||||
|
id: cache.identify(result.data.leaveWorkgroup),
|
||||||
|
fields: {
|
||||||
|
members(existingMembers, { readField }) {
|
||||||
|
return existingMembers.filter(
|
||||||
|
user => state.userProfileId !== readField('id', user)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseWorkgroupClick = () => {
|
||||||
|
closeWorkgroup({
|
||||||
|
variables: {
|
||||||
|
workgroupId: state.workgroup.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNew = state.workgroup.id === '';
|
||||||
|
const isWorkgroupMember = state.workgroup.members.some(u => u.id === state.userProfileId);
|
||||||
|
const isClosed = state.workgroup.closedAt !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page title="Groupe de travail">
|
||||||
|
<div className="container is-fluid">
|
||||||
|
<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 <span className="is-italic">{ isClosed ? '(clos)' : null }</span></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="level-right">
|
||||||
|
<div className="buttons is-right level-item">
|
||||||
|
{
|
||||||
|
isNew || isClosed ? 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-sign-out-alt"></i></span>
|
||||||
|
</button>
|
||||||
|
<button onClick={onCloseWorkgroupClick} className="button is-danger is-medium">
|
||||||
|
<span>Clore</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>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-4">
|
||||||
|
<InfoPanel workgroup={state.workgroup as Workgroup} />
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<MembersPanel users={state.workgroup.members as User[]} />
|
||||||
|
<DecisionSupportFilePanel workgroupId={state.workgroup.id} />
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<TimelinePanel workgroup={state.workgroup} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkgroupPage;
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const Config = {
|
||||||
|
loginURL: get<string>("loginURL", "http://localhost:8081/login"),
|
||||||
|
logoutURL: get<string>("logoutURL", "http://localhost:8081/logout"),
|
||||||
|
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"),
|
||||||
|
subscriptionEndpoint: get<string>("subscriptionEndpoint", "ws://localhost:8081/api/v1/graphql"),
|
||||||
|
conferenceHeartbeatInterval: get<number>("conferenceHeartbeatInterval", 10000),
|
||||||
|
frontendBaseURL: get<string>("frontendBaseURL", window.location.protocol + '//' + window.location.host + '/'),
|
||||||
|
};
|
||||||
|
|
||||||
|
function get<T>(key: string, defaultValue: T):T {
|
||||||
|
const config = window['__CONFIG__'] || {};
|
||||||
|
if (config && config.hasOwnProperty(key)) {
|
||||||
|
return config[key] as T;
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||||
|
import { QUERY_DECISION_SUPPORT_FILES } from '../queries/dsf';
|
||||||
|
|
||||||
|
export const MUTATION_CREATE_DECISION_SUPPORT_FILE = gql`
|
||||||
|
mutation createDecisionSupportFile($changes: DecisionSupportFileChanges!) {
|
||||||
|
createDecisionSupportFile(changes: $changes) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
sections,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
workgroup {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useCreateDecisionSupportFileMutation() {
|
||||||
|
return useMutation(MUTATION_CREATE_DECISION_SUPPORT_FILE, {
|
||||||
|
refetchQueries: [{query: QUERY_DECISION_SUPPORT_FILES}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MUTATION_UPDATE_DECISION_SUPPORT_FILE = gql`
|
||||||
|
mutation updateDecisionSupportFile($id: ID!, $changes: DecisionSupportFileChanges!) {
|
||||||
|
updateDecisionSupportFile(id: $id, changes: $changes) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
sections,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
workgroup {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useUpdateDecisionSupportFileMutation() {
|
||||||
|
return useMutation(MUTATION_UPDATE_DECISION_SUPPORT_FILE, {
|
||||||
|
refetchQueries: [{
|
||||||
|
query: QUERY_DECISION_SUPPORT_FILES,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { gql, useQuery, useMutation } from '@apollo/client';
|
||||||
|
|
||||||
|
export 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);
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { gql, useQuery, useMutation, FetchResult } from '@apollo/client';
|
||||||
|
import { QUERY_WORKGROUP } from '../queries/workgroups';
|
||||||
|
import { QUERY_IS_AUTHORIZED } from '../queries/authorization';
|
||||||
|
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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, {
|
||||||
|
refetchQueries: [{query: QUERY_WORKGROUP}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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, {
|
||||||
|
refetchQueries: ({ data }: FetchResult) => {
|
||||||
|
return [{
|
||||||
|
query: QUERY_IS_AUTHORIZED,
|
||||||
|
variables: {
|
||||||
|
action: 'update',
|
||||||
|
object: {
|
||||||
|
workgroupId: data.joinWorkgroup.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, {
|
||||||
|
refetchQueries: ({ data }: FetchResult) => {
|
||||||
|
return [{
|
||||||
|
query: QUERY_WORKGROUP,
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
ids: [data.leaveWorkgroup.id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: QUERY_IS_AUTHORIZED,
|
||||||
|
variables: {
|
||||||
|
action: 'update',
|
||||||
|
object: {
|
||||||
|
workgroupId: data.leaveWorkgroup.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const MUTATION_CLOSE_WORKGROUP = gql`
|
||||||
|
mutation closeWorkgroup($workgroupId: ID!) {
|
||||||
|
closeWorkgroup(workgroupId: $workgroupId) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useCloseWorkgroupMutation() {
|
||||||
|
return useMutation(MUTATION_CLOSE_WORKGROUP);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
|
export const QUERY_IS_AUTHORIZED = gql`
|
||||||
|
query isAuthorized($action: String!, $object: AuthorizationObject!) {
|
||||||
|
isAuthorized(action: $action, object: $object)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useIsAuthorizedQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
options = Object.assign({
|
||||||
|
fetchPolicy: 'cache-and-network'
|
||||||
|
}, options);
|
||||||
|
return useQuery(QUERY_IS_AUTHORIZED, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsAuthorized<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}, defaultValue = false) {
|
||||||
|
options = Object.assign({
|
||||||
|
fetchPolicy: 'cache-and-network'
|
||||||
|
}, options);
|
||||||
|
const { data, loading, error } = useGraphQLData<boolean>(
|
||||||
|
QUERY_IS_AUTHORIZED, 'isAuthorized', defaultValue, options
|
||||||
|
);
|
||||||
|
return { isAuthorized: data, loading, error };
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { DecisionSupportFile } from '../../types/decision';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
|
export const QUERY_DECISION_SUPPORT_FILES = gql`
|
||||||
|
query decisionSupportFiles($filter: DecisionSupportFileFilter) {
|
||||||
|
decisionSupportFiles(filter: $filter) {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
sections,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
votedAt,
|
||||||
|
status,
|
||||||
|
workgroup {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useDecisionSupportFilesQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
return useQuery(QUERY_DECISION_SUPPORT_FILES, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDecisionSupportFiles<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const { data, loading, error } = useGraphQLData<DecisionSupportFile[]>(
|
||||||
|
QUERY_DECISION_SUPPORT_FILES, 'decisionSupportFiles', [], options
|
||||||
|
);
|
||||||
|
return { decisionSupportFiles: data, loading, error };
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
import { Event } from '../../types/event';
|
||||||
|
|
||||||
|
export const QUERY_EVENTS = gql`
|
||||||
|
query events($filter: EventFilter) {
|
||||||
|
events(filter: $filter) {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
type
|
||||||
|
objectType
|
||||||
|
objectId
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useEventsQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
return useQuery(QUERY_EVENTS, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEvents<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const { data, loading, error } = useGraphQLData<Event[]>(
|
||||||
|
QUERY_EVENTS, 'events', [], options
|
||||||
|
);
|
||||||
|
return { events: data, loading, error };
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useQuery, DocumentNode, QueryHookOptions } from "@apollo/client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useGraphQLData<T, A = any, R = Record<string, any>>(q: DocumentNode, key: string, defaultValue: T, options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const query = useQuery(q, options);
|
||||||
|
const [ data, setData ] = useState<T>(defaultValue);
|
||||||
|
useEffect(() => {
|
||||||
|
setData(query.data ? query.data[key] as T : defaultValue);
|
||||||
|
}, [query.loading, query.data]);
|
||||||
|
return { data, loading: query.loading, error: query.error };
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { User } from '../../types/user';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
|
export const QUERY_USER_PROFILE = gql`
|
||||||
|
query userProfile {
|
||||||
|
userProfile {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
createdAt,
|
||||||
|
connectedAt
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export function useUserProfileQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
return useQuery(QUERY_USER_PROFILE, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserProfile<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const { data, loading, error } = useGraphQLData<User>(
|
||||||
|
QUERY_USER_PROFILE, 'userProfile', {id: '', email: ''}, options
|
||||||
|
);
|
||||||
|
return { user: data, loading, error };
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { gql, useQuery, QueryHookOptions } from '@apollo/client';
|
||||||
|
import { Workgroup } from '../../types/workgroup';
|
||||||
|
import { useGraphQLData } from './helper';
|
||||||
|
|
||||||
|
export const QUERY_WORKGROUP = gql`
|
||||||
|
query workgroups($filter: WorkgroupsFilter) {
|
||||||
|
workgroups(filter: $filter) {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
createdAt,
|
||||||
|
closedAt,
|
||||||
|
members {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function useWorkgroupsQuery<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
return useQuery(QUERY_WORKGROUP, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkgroups<A = any, R = Record<string, any>>(options: QueryHookOptions<A, R> = {}) {
|
||||||
|
const { data, loading, error } = useGraphQLData<Workgroup[]>(
|
||||||
|
QUERY_WORKGROUP, 'workgroups', [],
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return { workgroups: data, loading, error };
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import * as Y from 'yjs'
|
||||||
|
import { WebrtcProvider, } from 'y-webrtc'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { uuidV4 } from '../util/uuid';
|
||||||
|
|
||||||
|
const UUIDKey = 'conference-uuid';
|
||||||
|
let uuid = localStorage.getItem(UUIDKey);
|
||||||
|
|
||||||
|
if (!uuid) {
|
||||||
|
uuid = uuidV4();
|
||||||
|
localStorage.setItem(UUIDKey, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConference() {
|
||||||
|
const docRef = useRef(new Y.Doc());
|
||||||
|
|
||||||
|
const [ state, setState ] = useState({
|
||||||
|
data: {
|
||||||
|
emails: {},
|
||||||
|
nicknames: {},
|
||||||
|
statuses: {},
|
||||||
|
peers: {},
|
||||||
|
},
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setData = (key: string, value: any) => {
|
||||||
|
setState(state => ({...state, data: { ...state.data, [key]: value }}));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const doc = docRef.current;
|
||||||
|
const roomName = `${window.location.protocol}//${window.location.host}/daddy/conference`;
|
||||||
|
const provider = new WebrtcProvider(roomName, docRef.current);
|
||||||
|
|
||||||
|
const peers = doc.getMap('peers');
|
||||||
|
peers.observe(evt => setData('peers', evt.currentTarget.toJSON()));
|
||||||
|
|
||||||
|
const nicknames = doc.getMap('nicknames');
|
||||||
|
nicknames.observe(evt => setData('nicknames', evt.currentTarget.toJSON()));
|
||||||
|
|
||||||
|
const emails = doc.getMap('emails');
|
||||||
|
emails.observe(evt => setData('emails', evt.currentTarget.toJSON()));
|
||||||
|
|
||||||
|
const statuses = doc.getMap('statuses');
|
||||||
|
statuses.observe(evt => setData('statuses', evt.currentTarget.toJSON()));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
provider.destroy();
|
||||||
|
docRef.current.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: state.data,
|
||||||
|
uuid: state.uuid,
|
||||||
|
|
||||||
|
setStatus: (status: string) => {
|
||||||
|
const doc = docRef.current;
|
||||||
|
const statuses = doc.getMap('statuses');
|
||||||
|
statuses.set(state.uuid, status);
|
||||||
|
},
|
||||||
|
|
||||||
|
ping: () => {
|
||||||
|
const doc = docRef.current;
|
||||||
|
const peers = doc.getMap('peers');
|
||||||
|
peers.set(state.uuid, (new Date()).toJSON());
|
||||||
|
},
|
||||||
|
|
||||||
|
setNickname: (nickname: string) => {
|
||||||
|
console.log('setNickname', nickname);
|
||||||
|
const doc = docRef.current;
|
||||||
|
const nicknames = doc.getMap('nicknames');
|
||||||
|
nicknames.set(state.uuid, nickname);
|
||||||
|
},
|
||||||
|
|
||||||
|
setEmail: (email: string) => {
|
||||||
|
console.log('setEmail', email);
|
||||||
|
const doc = docRef.current;
|
||||||
|
const emails = doc.getMap('emails');
|
||||||
|
emails.set(state.uuid, email);
|
||||||
|
},
|
||||||
|
|
||||||
|
forget: (uuid: string) => {
|
||||||
|
const doc = docRef.current;
|
||||||
|
const peers = doc.getMap('peers');
|
||||||
|
peers.delete(uuid);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useDebounce(value, delay) {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[value, delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function useKonamiCode(cb: Function) {
|
||||||
|
const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
cursor = (e.keyCode == KONAMI_CODE[cursor]) ? cursor + 1 : 0;
|
||||||
|
if (cursor == KONAMI_CODE.length) {
|
||||||
|
cb();
|
||||||
|
cursor = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
return () => document.removeEventListener('keydown', onKeyDown);
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { useContext, useEffect } from "react";
|
||||||
|
|
||||||
|
const LOGGED_IN_KEY = 'loggedIn';
|
||||||
|
|
||||||
|
export const LoggedInContext = React.createContext(getSavedLoggedIn());
|
||||||
|
|
||||||
|
export const useLoggedIn = () => {
|
||||||
|
return useContext(LoggedInContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function saveLoggedIn(loggedIn: boolean) {
|
||||||
|
window.localStorage.setItem(LOGGED_IN_KEY, JSON.stringify(loggedIn));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSavedLoggedIn(): boolean {
|
||||||
|
try {
|
||||||
|
const loggedIn = JSON.parse(window.localStorage.getItem(LOGGED_IN_KEY));
|
||||||
|
return !!loggedIn;
|
||||||
|
} catch(err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="has-navbar-fixed-top">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
@ -1,8 +1,12 @@
|
||||||
|
import { Config } from './config';
|
||||||
|
|
||||||
|
declare var __webpack_public_path__: string;
|
||||||
|
__webpack_public_path__ = Config.frontendBaseURL;
|
||||||
|
|
||||||
import './sass/_all.scss';
|
import './sass/_all.scss';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { App } from './components/App';
|
import { App } from './components/App';
|
||||||
import { Config } from './config';
|
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||||
import '@fortawesome/fontawesome-free/js/solid'
|
import '@fortawesome/fontawesome-free/js/solid'
|
||||||
|
@ -10,6 +14,7 @@ import '@fortawesome/fontawesome-free/js/regular'
|
||||||
import '@fortawesome/fontawesome-free/js/brands'
|
import '@fortawesome/fontawesome-free/js/brands'
|
||||||
import './resources/favicon.png';
|
import './resources/favicon.png';
|
||||||
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App />,
|
<App />,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'bulma/bulma.sass';
|
||||||
|
@import 'bulma-timeline/dist/css/bulma-timeline.sass';
|
||||||
|
@import '_bulma-timeline.scss';
|
||||||
|
@import '_base.scss';
|
||||||
|
@import '_loader.scss';
|
|
@ -0,0 +1,27 @@
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
// Generated with https://www.svgbackgrounds.com/
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='351' height='292.5' viewBox='0 0 1080 900'%3E%3Cg fill-opacity='0.04'%3E%3Cpolygon fill='%23444' points='90 150 0 300 180 300'/%3E%3Cpolygon points='90 150 180 0 0 0'/%3E%3Cpolygon fill='%23AAA' points='270 150 360 0 180 0'/%3E%3Cpolygon fill='%23DDD' points='450 150 360 300 540 300'/%3E%3Cpolygon fill='%23999' points='450 150 540 0 360 0'/%3E%3Cpolygon points='630 150 540 300 720 300'/%3E%3Cpolygon fill='%23DDD' points='630 150 720 0 540 0'/%3E%3Cpolygon fill='%23444' points='810 150 720 300 900 300'/%3E%3Cpolygon fill='%23FFF' points='810 150 900 0 720 0'/%3E%3Cpolygon fill='%23DDD' points='990 150 900 300 1080 300'/%3E%3Cpolygon fill='%23444' points='990 150 1080 0 900 0'/%3E%3Cpolygon fill='%23DDD' points='90 450 0 600 180 600'/%3E%3Cpolygon points='90 450 180 300 0 300'/%3E%3Cpolygon fill='%23666' points='270 450 180 600 360 600'/%3E%3Cpolygon fill='%23AAA' points='270 450 360 300 180 300'/%3E%3Cpolygon fill='%23DDD' points='450 450 360 600 540 600'/%3E%3Cpolygon fill='%23999' points='450 450 540 300 360 300'/%3E%3Cpolygon fill='%23999' points='630 450 540 600 720 600'/%3E%3Cpolygon fill='%23FFF' points='630 450 720 300 540 300'/%3E%3Cpolygon points='810 450 720 600 900 600'/%3E%3Cpolygon fill='%23DDD' points='810 450 900 300 720 300'/%3E%3Cpolygon fill='%23AAA' points='990 450 900 600 1080 600'/%3E%3Cpolygon fill='%23444' points='990 450 1080 300 900 300'/%3E%3Cpolygon fill='%23222' points='90 750 0 900 180 900'/%3E%3Cpolygon points='270 750 180 900 360 900'/%3E%3Cpolygon fill='%23DDD' points='270 750 360 600 180 600'/%3E%3Cpolygon points='450 750 540 600 360 600'/%3E%3Cpolygon points='630 750 540 900 720 900'/%3E%3Cpolygon fill='%23444' points='630 750 720 600 540 600'/%3E%3Cpolygon fill='%23AAA' points='810 750 720 900 900 900'/%3E%3Cpolygon fill='%23666' points='810 750 900 600 720 600'/%3E%3Cpolygon fill='%23999' points='990 750 900 900 1080 900'/%3E%3Cpolygon fill='%23999' points='180 0 90 150 270 150'/%3E%3Cpolygon fill='%23444' points='360 0 270 150 450 150'/%3E%3Cpolygon fill='%23FFF' points='540 0 450 150 630 150'/%3E%3Cpolygon points='900 0 810 150 990 150'/%3E%3Cpolygon fill='%23222' points='0 300 -90 450 90 450'/%3E%3Cpolygon fill='%23FFF' points='0 300 90 150 -90 150'/%3E%3Cpolygon fill='%23FFF' points='180 300 90 450 270 450'/%3E%3Cpolygon fill='%23666' points='180 300 270 150 90 150'/%3E%3Cpolygon fill='%23222' points='360 300 270 450 450 450'/%3E%3Cpolygon fill='%23FFF' points='360 300 450 150 270 150'/%3E%3Cpolygon fill='%23444' points='540 300 450 450 630 450'/%3E%3Cpolygon fill='%23222' points='540 300 630 150 450 150'/%3E%3Cpolygon fill='%23AAA' points='720 300 630 450 810 450'/%3E%3Cpolygon fill='%23666' points='720 300 810 150 630 150'/%3E%3Cpolygon fill='%23FFF' points='900 300 810 450 990 450'/%3E%3Cpolygon fill='%23999' points='900 300 990 150 810 150'/%3E%3Cpolygon points='0 600 -90 750 90 750'/%3E%3Cpolygon fill='%23666' points='0 600 90 450 -90 450'/%3E%3Cpolygon fill='%23AAA' points='180 600 90 750 270 750'/%3E%3Cpolygon fill='%23444' points='180 600 270 450 90 450'/%3E%3Cpolygon fill='%23444' points='360 600 270 750 450 750'/%3E%3Cpolygon fill='%23999' points='360 600 450 450 270 450'/%3E%3Cpolygon fill='%23666' points='540 600 630 450 450 450'/%3E%3Cpolygon fill='%23222' points='720 600 630 750 810 750'/%3E%3Cpolygon fill='%23FFF' points='900 600 810 750 990 750'/%3E%3Cpolygon fill='%23222' points='900 600 990 450 810 450'/%3E%3Cpolygon fill='%23DDD' points='0 900 90 750 -90 750'/%3E%3Cpolygon fill='%23444' points='180 900 270 750 90 750'/%3E%3Cpolygon fill='%23FFF' points='360 900 450 750 270 750'/%3E%3Cpolygon fill='%23AAA' points='540 900 630 750 450 750'/%3E%3Cpolygon fill='%23FFF' points='720 900 810 750 630 750'/%3E%3Cpolygon fill='%23222' points='900 900 990 750 810 750'/%3E%3Cpolygon fill='%23222' points='1080 300 990 450 1170 450'/%3E%3Cpolygon fill='%23FFF' points='1080 300 1170 150 990 150'/%3E%3Cpolygon points='1080 600 990 750 1170 750'/%3E%3Cpolygon fill='%23666' points='1080 600 1170 450 990 450'/%3E%3Cpolygon fill='%23DDD' points='1080 900 1170 750 990 750'/%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-fullheight {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-margin-top-normal {
|
||||||
|
margin-top: $size-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-padding-small {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
.timeline {
|
||||||
|
.timeline-item {
|
||||||
|
.timeline-marker {
|
||||||
|
&.is-icon {
|
||||||
|
> svg {
|
||||||
|
color: $white;
|
||||||
|
font-size: $timeline-icon-size !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
.app-loader {
|
||||||
|
@extend body;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10000;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Workgroup } from "./workgroup";
|
||||||
|
|
||||||
|
export enum DecisionSupportFileStatus {
|
||||||
|
Draft = "draft",
|
||||||
|
Ready = "ready",
|
||||||
|
Voted = "voted",
|
||||||
|
Closed = "closed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecisionSupportFileSection {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// aka Dossier d'aide à la décision
|
||||||
|
export interface DecisionSupportFile {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
sections: {[name: string]: any}
|
||||||
|
status: DecisionSupportFileStatus
|
||||||
|
workgroup?: Workgroup,
|
||||||
|
createdAt: Date
|
||||||
|
votedAt?: Date
|
||||||
|
closedAt?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newDecisionSupportFile(): DecisionSupportFile {
|
||||||
|
return {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
sections: {},
|
||||||
|
status: DecisionSupportFileStatus.Draft,
|
||||||
|
workgroup: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
id: string
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
user: User
|
||||||
|
objectType: string
|
||||||
|
objectId: number
|
||||||
|
type: string
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
name?: string
|
||||||
|
connectedAt?: Date
|
||||||
|
createdAt?: Date
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { User } from "./user";
|
||||||
|
export interface Workgroup {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
createdAt: Date
|
||||||
|
closedAt: Date
|
||||||
|
members: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inWorkgroup(u: User, wg: Workgroup): boolean {
|
||||||
|
for (let m, i = 0; (m = wg.members[i]); i++) {
|
||||||
|
if(m.id === u.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
|
||||||
|
import { Config } from '../config';
|
||||||
|
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||||
|
import { RetryLink } from "@apollo/client/link/retry";
|
||||||
|
import { onError } from "@apollo/client/link/error";
|
||||||
|
import { SubscriptionClient } from "subscriptions-transport-ws";
|
||||||
|
import { User } from '../types/user';
|
||||||
|
|
||||||
|
export function createClient(setLoggedIn: (boolean) => void) {
|
||||||
|
const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, {
|
||||||
|
reconnect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorLink = onError(({ operation }) => {
|
||||||
|
const { response } = operation.getContext();
|
||||||
|
if (response.status === 401) setLoggedIn(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryLink = new RetryLink({attempts: {max: 2}}).split(
|
||||||
|
(operation) => operation.operationName === 'subscription',
|
||||||
|
new WebSocketLink(subscriptionClient),
|
||||||
|
new HttpLink({
|
||||||
|
uri: Config.graphQLEndpoint,
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const cache = new InMemoryCache({
|
||||||
|
typePolicies: {
|
||||||
|
Workgroup: {
|
||||||
|
fields: {
|
||||||
|
members: {
|
||||||
|
merge: mergeArrayByField<User>("id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ApolloClient<any>({
|
||||||
|
cache: cache,
|
||||||
|
link: from([
|
||||||
|
errorLink,
|
||||||
|
retryLink
|
||||||
|
]),
|
||||||
|
defaultOptions: {
|
||||||
|
watchQuery: {
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
errorPolicy: 'ignore',
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
errorPolicy: 'all',
|
||||||
|
},
|
||||||
|
mutate: {
|
||||||
|
errorPolicy: 'all',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeArrayByField<T>(fieldName: string) {
|
||||||
|
return (existing: T[] = [], incoming: T[], { readField, mergeObjects }) => {
|
||||||
|
const merged: any[] = existing ? existing.slice(0) : [];
|
||||||
|
|
||||||
|
const objectFieldToIndex: Record<string, number> = Object.create(null);
|
||||||
|
if (existing) {
|
||||||
|
existing.forEach((obj, index) => {
|
||||||
|
objectFieldToIndex[readField(fieldName, obj)] = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
incoming.forEach(obj => {
|
||||||
|
const field = readField(fieldName, obj);
|
||||||
|
const index = objectFieldToIndex[field];
|
||||||
|
if (typeof index === "number") {
|
||||||
|
merged[index] = mergeObjects(merged[index], obj);
|
||||||
|
} else {
|
||||||
|
objectFieldToIndex[name] = merged.length;
|
||||||
|
merged.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function asDate(d: string|Date): Date {
|
||||||
|
if (typeof d === 'string') return new Date(d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intl = Intl.DateTimeFormat(navigator.language, {
|
||||||
|
weekday: 'long',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric', minute: 'numeric', second: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatDate(d: Date|string): string {
|
||||||
|
d = asDate(d);
|
||||||
|
return intl.format(d);
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
|
||||||
|
const hex: string[] = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < 256; i++) {
|
||||||
|
hex[i] = (i < 16 ? '0' : '') + (i).toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuidV4(): string {
|
||||||
|
const r = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
|
||||||
|
r[6] = r[6] & 0x0f | 0x40;
|
||||||
|
r[8] = r[8] & 0x3f | 0x80;
|
||||||
|
|
||||||
|
return (
|
||||||
|
hex[r[0]] +
|
||||||
|
hex[r[1]] +
|
||||||
|
hex[r[2]] +
|
||||||
|
hex[r[3]] +
|
||||||
|
"-" +
|
||||||
|
hex[r[4]] +
|
||||||
|
hex[r[5]] +
|
||||||
|
"-" +
|
||||||
|
hex[r[6]] +
|
||||||
|
hex[r[7]] +
|
||||||
|
"-" +
|
||||||
|
hex[r[8]] +
|
||||||
|
hex[r[9]] +
|
||||||
|
"-" +
|
||||||
|
hex[r[10]] +
|
||||||
|
hex[r[11]] +
|
||||||
|
hex[r[12]] +
|
||||||
|
hex[r[13]] +
|
||||||
|
hex[r[14]] +
|
||||||
|
hex[r[15]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toUTF8Bytes(str: string): number[] {
|
||||||
|
var utf8 = unescape(encodeURIComponent(str));
|
||||||
|
|
||||||
|
var arr: number[] = [];
|
||||||
|
for (var i = 0; i < utf8.length; i++) {
|
||||||
|
arr.push(utf8.charCodeAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base58UUID(): string {
|
||||||
|
const uuid = uuidV4();
|
||||||
|
return bs58.encode(toUTF8Bytes(uuid));
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "es6",
|
"module": "es2020",
|
||||||
"lib": ["dom", "es6"],
|
"lib": ["dom", "es6"],
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
|
@ -11,7 +11,7 @@ const env = process.env;
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: `${env.NODE_ENV ? env.NODE_ENV : 'production'}`,
|
mode: `${env.NODE_ENV ? env.NODE_ENV : 'production'}`,
|
||||||
entry: './src/index.tsx',
|
entry: './src/index.tsx',
|
||||||
devtool: 'inline-source-map',
|
devtool: env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map',
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].[contenthash].js',
|
filename: '[name].[contenthash].js',
|
||||||
path: path.join(__dirname, 'dist')
|
path: path.join(__dirname, 'dist')
|
||||||
|
@ -22,7 +22,8 @@ module.exports = {
|
||||||
devServer: {
|
devServer: {
|
||||||
contentBase: path.join(__dirname, 'dist'),
|
contentBase: path.join(__dirname, 'dist'),
|
||||||
compress: true,
|
compress: true,
|
||||||
port: 8081,
|
host: '0.0.0.0',
|
||||||
|
port: 8080,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
writeToDisk: true,
|
writeToDisk: true,
|
||||||
},
|
},
|
|
@ -0,0 +1,120 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/mail"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/voter"
|
||||||
|
|
||||||
|
"github.com/wader/gormstore"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
|
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||||
|
oidc "forge.cadoles.com/wpetit/goweb-oidc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/service"
|
||||||
|
"gitlab.com/wpetit/goweb/service/build"
|
||||||
|
"gitlab.com/wpetit/goweb/service/session"
|
||||||
|
"gitlab.com/wpetit/goweb/session/gorilla"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Container, error) {
|
||||||
|
// Initialize and configure service container
|
||||||
|
ctn := service.NewContainer()
|
||||||
|
|
||||||
|
ctn.Provide(build.ServiceName, build.ServiceProvider(ProjectVersion, GitRef, BuildDate))
|
||||||
|
|
||||||
|
// Generate random cookie authentication key if none is set
|
||||||
|
if conf.HTTP.CookieAuthenticationKey == "" {
|
||||||
|
logger.Info(ctx, "could not find cookie authentication key. generating one...")
|
||||||
|
|
||||||
|
cookieAuthenticationKey, err := gorilla.GenerateRandomBytes(64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not generate cookie authentication key")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.HTTP.CookieAuthenticationKey = string(cookieAuthenticationKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate random cookie encryption key if none is set
|
||||||
|
if conf.HTTP.CookieEncryptionKey == "" {
|
||||||
|
logger.Info(ctx, "could not find cookie encryption key. generating one...")
|
||||||
|
|
||||||
|
cookieEncryptionKey, err := gorilla.GenerateRandomBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not generate cookie encryption key")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.HTTP.CookieEncryptionKey = string(cookieEncryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug))
|
||||||
|
|
||||||
|
orm, err := orm.From(ctn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and initialize HTTP session service provider
|
||||||
|
sessionStore := gormstore.NewOptions(
|
||||||
|
orm.DB(),
|
||||||
|
gormstore.Options{
|
||||||
|
TableName: "sessions",
|
||||||
|
SkipCreateTable: false,
|
||||||
|
},
|
||||||
|
[]byte(conf.HTTP.CookieAuthenticationKey),
|
||||||
|
[]byte(conf.HTTP.CookieEncryptionKey),
|
||||||
|
)
|
||||||
|
|
||||||
|
quit := make(chan struct{})
|
||||||
|
go sessionStore.PeriodicCleanup(1*time.Hour, quit)
|
||||||
|
|
||||||
|
// Define default cookie options
|
||||||
|
sessionStore.SessionOpts.Path = "/"
|
||||||
|
sessionStore.SessionOpts.HttpOnly = true
|
||||||
|
sessionStore.SessionOpts.Secure = conf.HTTP.CookieSecure
|
||||||
|
sessionStore.SessionOpts.MaxAge = conf.HTTP.CookieMaxAge
|
||||||
|
sessionStore.SessionOpts.SameSite = http.SameSiteLaxMode
|
||||||
|
|
||||||
|
ctn.Provide(
|
||||||
|
session.ServiceName,
|
||||||
|
gorilla.ServiceProvider("daddy", sessionStore),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create and expose config service provider
|
||||||
|
ctn.Provide(config.ServiceName, config.ServiceProvider(conf))
|
||||||
|
|
||||||
|
provider, err := oidc.NewProvider(ctx, conf.OIDC.IssuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not create oidc provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctn.Provide(oidc.ServiceName, oidc.ServiceProvider(
|
||||||
|
oidc.WithCredentials(conf.OIDC.ClientID, conf.OIDC.ClientSecret),
|
||||||
|
oidc.WithProvider(provider),
|
||||||
|
oidc.WithScopes("email", "openid"),
|
||||||
|
))
|
||||||
|
|
||||||
|
ctn.Provide(auth.ServiceName, auth.ServiceProvider(conf.Auth.Rules))
|
||||||
|
|
||||||
|
ctn.Provide(voter.ServiceName, voter.ServiceProvider(
|
||||||
|
voter.StrategyUnanimous,
|
||||||
|
model.NewDecisionSupportFileVoter(),
|
||||||
|
model.NewWorkgroupVoter(),
|
||||||
|
))
|
||||||
|
|
||||||
|
ctn.Provide(mail.ServiceName, mail.ServiceProvider(
|
||||||
|
mail.WithServer(conf.SMTP.Host, conf.SMTP.Port),
|
||||||
|
mail.WithCredentials(conf.SMTP.User, conf.SMTP.Password),
|
||||||
|
mail.WithTLS(conf.SMTP.UseStartTLS, conf.SMTP.InsecureSkipVerify),
|
||||||
|
))
|
||||||
|
|
||||||
|
return ctn, nil
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/route"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"gitlab.com/wpetit/goweb/middleware/container"
|
||||||
|
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoglobals
|
||||||
|
var (
|
||||||
|
configFile = ""
|
||||||
|
workdir = ""
|
||||||
|
dumpConfig = false
|
||||||
|
version = false
|
||||||
|
migrate = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var (
|
||||||
|
GitRef = "unknown"
|
||||||
|
ProjectVersion = "unknown"
|
||||||
|
BuildDate = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoinits
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&configFile, "config", configFile, "configuration file")
|
||||||
|
flag.StringVar(&workdir, "workdir", workdir, "working directory")
|
||||||
|
flag.BoolVar(&dumpConfig, "dump-config", dumpConfig, "dump configuration and exit")
|
||||||
|
flag.BoolVar(&version, "version", version, "show version and exit")
|
||||||
|
flag.StringVar(&migrate, "migrate", migrate, "migrate data schema version and exit, possible values: latest, down, up")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if version {
|
||||||
|
fmt.Printf("%s (%s) - %s\n", ProjectVersion, GitRef, BuildDate)
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to new working directory if defined
|
||||||
|
if workdir != "" {
|
||||||
|
if err := os.Chdir(workdir); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not change working directory",
|
||||||
|
logger.E(err),
|
||||||
|
logger.F("workdir", workdir),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration file if defined, use default configuration otherwise
|
||||||
|
var conf *config.Config
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if configFile != "" {
|
||||||
|
conf, err = config.NewFromFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", errors.Wrapf(err, " '%s'", configFile))
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not load config file",
|
||||||
|
logger.E(err),
|
||||||
|
logger.F("configFile", configFile),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dumpConfig {
|
||||||
|
conf = config.NewDumpDefault()
|
||||||
|
} else {
|
||||||
|
conf = config.NewDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump configuration if asked
|
||||||
|
if dumpConfig {
|
||||||
|
if err := config.Dump(conf, os.Stdout); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not dump config",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.WithEnvironment(conf); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not override config with environment",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(
|
||||||
|
ctx,
|
||||||
|
"starting",
|
||||||
|
logger.F("gitRef", GitRef),
|
||||||
|
logger.F("projectVersion", ProjectVersion),
|
||||||
|
logger.F("buildDate", BuildDate),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.Debug(ctx, "setting log format", logger.F("format", conf.Log.Format))
|
||||||
|
logger.SetFormat(conf.Log.Format)
|
||||||
|
|
||||||
|
logger.Debug(ctx, "setting log level", logger.F("level", conf.Log.Level.String()))
|
||||||
|
logger.SetLevel(conf.Log.Level)
|
||||||
|
|
||||||
|
useSentry := conf.Sentry.DSN != ""
|
||||||
|
|
||||||
|
if useSentry {
|
||||||
|
var sentryEnv string
|
||||||
|
if conf.Sentry.Environment == "" {
|
||||||
|
sentryEnv, _ = os.Hostname()
|
||||||
|
} else {
|
||||||
|
sentryEnv = conf.Sentry.Environment
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: conf.Sentry.DSN,
|
||||||
|
Debug: conf.Debug,
|
||||||
|
SampleRate: conf.Sentry.ServerSampleRate,
|
||||||
|
Release: ProjectVersion + "-" + GitRef,
|
||||||
|
Environment: sentryEnv,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could initialize sentry",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sentry.Flush(conf.Sentry.ServerFlushTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create service container
|
||||||
|
ctn, err := getServiceContainer(ctx, conf)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not create service container",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = container.WithContainer(ctx, ctn)
|
||||||
|
|
||||||
|
if migrate != "" {
|
||||||
|
if err := applyMigration(ctx, ctn); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not apply migration",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
go runTaskScheduler(ctx, conf)
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// Define base middlewares
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
|
if useSentry {
|
||||||
|
sentryMiddleware := sentryhttp.New(sentryhttp.Options{
|
||||||
|
Repanic: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Use(sentryMiddleware.Handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose service container on router
|
||||||
|
r.Use(container.ServiceContainer(ctn))
|
||||||
|
|
||||||
|
// Define routes
|
||||||
|
if err := route.Mount(r, conf); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not mount http routes",
|
||||||
|
logger.E(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(ctx, "listening", logger.F("address", conf.HTTP.Address))
|
||||||
|
if err := http.ListenAndServe(conf.HTTP.Address, r); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not listen",
|
||||||
|
logger.E(err),
|
||||||
|
logger.F("address", conf.HTTP.Address),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
"gitlab.com/wpetit/goweb/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
migrateUp = "up"
|
||||||
|
migrateLatest = "latest"
|
||||||
|
migrateDown = "down"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyMigration(ctx context.Context, ctn *service.Container) error {
|
||||||
|
orm, err := orm.From(ctn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migr := orm.Migration()
|
||||||
|
|
||||||
|
// Register available migrations
|
||||||
|
migr.Register(
|
||||||
|
m000initialSchema(),
|
||||||
|
)
|
||||||
|
|
||||||
|
currentVersion, err := migr.CurrentVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not retrieve current data schema version")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch migrate {
|
||||||
|
case migrateUp:
|
||||||
|
if err := migr.Up(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "could not apply up migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
case migrateLatest:
|
||||||
|
latestVersion, err := migr.LatestVersion()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not retrieve latest data schema version")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(
|
||||||
|
ctx,
|
||||||
|
"migrating data schema to latest version",
|
||||||
|
logger.F("currentVersion", currentVersion),
|
||||||
|
logger.F("latestVersion", latestVersion),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute migration to latest available version
|
||||||
|
if err := migr.Latest(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "could not migrate to latest data schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
case migrateDown:
|
||||||
|
if err := migr.Down(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "could not apply down migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.Errorf("unknown migration command: '%s'", migrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(
|
||||||
|
ctx,
|
||||||
|
"migration completed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var initialModels = []interface{}{
|
||||||
|
&model.User{},
|
||||||
|
&model.Workgroup{},
|
||||||
|
&model.DecisionSupportFile{},
|
||||||
|
&model.Event{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func m000initialSchema() orm.Migration {
|
||||||
|
return orm.NewDBMigration(
|
||||||
|
"00_initial_schema",
|
||||||
|
func(ctx context.Context, tx *gorm.DB) error {
|
||||||
|
for _, m := range initialModels {
|
||||||
|
if err := tx.AutoMigrate(m).Error; err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(ctx context.Context, tx *gorm.DB) error {
|
||||||
|
for i := len(initialModels) - 1; i >= 0; i-- {
|
||||||
|
if err := tx.DropTableIfExists(initialModels[i]).Error; err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/config"
|
||||||
|
"forge.cadoles.com/Cadoles/daddy/internal/task"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cronLogger struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||||
|
fields := l.createFields(keysAndValues)
|
||||||
|
logger.Info(l.ctx, msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||||
|
fields := l.createFields(keysAndValues)
|
||||||
|
fields = append(fields, logger.E(err))
|
||||||
|
logger.Error(l.ctx, msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *cronLogger) createFields(keysAndValues ...interface{}) []logger.Field {
|
||||||
|
fields := make([]logger.Field, 0)
|
||||||
|
|
||||||
|
var key string
|
||||||
|
|
||||||
|
for _, v := range keysAndValues {
|
||||||
|
children, ok := v.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, vv := range children {
|
||||||
|
if i%2 == 0 {
|
||||||
|
key = fmt.Sprintf("%v", vv)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, logger.F(key, vv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTaskScheduler(ctx context.Context, conf *config.Config) {
|
||||||
|
c := cron.New(
|
||||||
|
cron.WithLogger(&cronLogger{ctx}),
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks := map[string]task.Task{
|
||||||
|
conf.Task.Newsletter.CronSpec: task.NewNewsletter(
|
||||||
|
ctx,
|
||||||
|
conf.Task.Newsletter.TimeRange,
|
||||||
|
conf.Task.Newsletter.BaseURL,
|
||||||
|
conf.Task.Newsletter.ContentTemplate,
|
||||||
|
conf.Task.Newsletter.SubjectTemplate,
|
||||||
|
conf.SMTP.SenderAddress,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for spec, task := range tasks {
|
||||||
|
if _, err := c.AddFunc(spec, task.Run); err != nil {
|
||||||
|
logger.Fatal(
|
||||||
|
ctx,
|
||||||
|
"could not schedule task",
|
||||||
|
logger.F("task", task.Name()),
|
||||||
|
logger.E(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
}
|
|
@ -1,26 +1,5 @@
|
||||||
version: '2.4'
|
version: '2.4'
|
||||||
services:
|
services:
|
||||||
super-graph:
|
|
||||||
build:
|
|
||||||
context: ./misc/containers/super-graph
|
|
||||||
args:
|
|
||||||
- HTTP_PROXY=${HTTP_PROXY}
|
|
||||||
- HTTPS_PROXY=${HTTPS_PROXY}
|
|
||||||
- http_proxy=${http_proxy}
|
|
||||||
- https_proxy=${https_proxy}
|
|
||||||
environment:
|
|
||||||
- SG_DATABASE_HOST=postgres
|
|
||||||
- SG_DATABASE_USER=daddy
|
|
||||||
- SG_DATABASE_PASSWORD=daddy
|
|
||||||
- USER_ID=${USER_ID}
|
|
||||||
- GO_ENV=dev
|
|
||||||
volumes:
|
|
||||||
- ./backend:/app
|
|
||||||
links:
|
|
||||||
- postgres
|
|
||||||
ports:
|
|
||||||
- 8080:8080
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
build:
|
build:
|
||||||
context: ./misc/containers/postgres
|
context: ./misc/containers/postgres
|
||||||
|
@ -48,15 +27,12 @@ services:
|
||||||
SUPPORTED_CLAIMS: email,email_verified
|
SUPPORTED_CLAIMS: email,email_verified
|
||||||
SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX
|
SECRETS_SYSTEM: fAAya66yXNib52lbXpo16bxy1jD4NZrX
|
||||||
HYDRA_ADMIN_URL: http://localhost:4445
|
HYDRA_ADMIN_URL: http://localhost:4445
|
||||||
SERVE_PUBLIC_CORS_ENABLED: "true"
|
|
||||||
SERVE_PUBLIC_CORS_ALLOWED_ORIGINS: http://localhost:8081
|
|
||||||
WEBFINGER_JWKS_BROADCAST_KEYS: hydra.openid.id-token,hydra.jwt.access-token
|
|
||||||
ports:
|
ports:
|
||||||
- 4444:4444
|
- 4444:4444
|
||||||
command: hydra serve all --dangerous-force-http
|
command: hydra serve all --dangerous-force-http
|
||||||
|
|
||||||
hydra-passwordless:
|
hydra-passwordless:
|
||||||
image: bornholm/hydra-passwordless
|
image: bornholm/hydra-passwordless:latest@sha256:e6b335e3677dc937c62978890b42312a7486e4fe10208aa2670b1917489ec492
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
environment:
|
environment:
|
||||||
|
@ -72,6 +48,7 @@ services:
|
||||||
- SMTP_INSECURE_SKIP_VERIFY=true
|
- SMTP_INSECURE_SKIP_VERIFY=true
|
||||||
- HYDRA_BASE_URL=http://hydra:4445
|
- HYDRA_BASE_URL=http://hydra:4445
|
||||||
- HYDRA_FAKE_SSL_TERMINATION=false
|
- HYDRA_FAKE_SSL_TERMINATION=false
|
||||||
|
- NO_PROXY=hydra
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
image: bornholm/fake-smtp
|
image: bornholm/fake-smtp
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
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 { OAuth2Page } from './OAuth2Page/OAuth2Page';
|
|
||||||
|
|
||||||
export class App extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<Switch>
|
|
||||||
<Route path="/" exact component={HomePage} />
|
|
||||||
<Route path="/oauth2/:action" exact component={OAuth2Page} />
|
|
||||||
<Route component={() => <Redirect to="/" />} />
|
|
||||||
</Switch>
|
|
||||||
</BrowserRouter>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Page } from '../Page';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import { RootState } from '../../store/reducers/root';
|
|
||||||
|
|
||||||
export function HomePage() {
|
|
||||||
const currentUser = useSelector((state: RootState) => state.auth.currentUser);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title="Daddy - Accueil">
|
|
||||||
<div className="container is-fluid">
|
|
||||||
<section className="section">
|
|
||||||
<div className="columns">
|
|
||||||
<div className="column is-4 is-offset-4">
|
|
||||||
<div className="box">
|
|
||||||
{
|
|
||||||
currentUser && currentUser.full_name ?
|
|
||||||
<p>Bonjour <span className="has-text-weight-bold">{currentUser.full_name}</span> !</p> :
|
|
||||||
<p>Veuillez vous authentifier.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export class Loader extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="loader-container">
|
|
||||||
<div className="lds-ripple">
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import logo from '../resources/logo.svg';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { RootState } from '../store/reducers/root';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export function Navbar() {
|
|
||||||
const isAuthenticated = useSelector<RootState>(state => state.auth.isAuthenticated);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
|
||||||
<div className="container is-fluid">
|
|
||||||
<div className="navbar-brand">
|
|
||||||
<a className="navbar-item" href="#/">
|
|
||||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
|
||||||
<h1 className="is-size-4">Daddy</h1>
|
|
||||||
</a>
|
|
||||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
<span aria-hidden="true"></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="navbar-menu">
|
|
||||||
<div className="navbar-end">
|
|
||||||
<div className="navbar-item">
|
|
||||||
{
|
|
||||||
isAuthenticated ?
|
|
||||||
<Link className="button is-small" to="/oauth2/logout">
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fas fa-sign-out-alt"></i>
|
|
||||||
</span>
|
|
||||||
<span>Se déconnecter</span>
|
|
||||||
</Link> :
|
|
||||||
<Link className="button is-small" to="/oauth2/login">
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fas fa-sign-in-alt"></i>
|
|
||||||
</span>
|
|
||||||
<span>Se connecter</span>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,31 +0,0 @@
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Page } from '../Page';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { logout, login, handleOAuth2Callback } from '../../store/actions/auth';
|
|
||||||
|
|
||||||
export function OAuth2Page({ match, location, history }) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { action } = match.params;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
switch(action) {
|
|
||||||
case 'logout':
|
|
||||||
dispatch(logout());
|
|
||||||
history.push("/");
|
|
||||||
break;
|
|
||||||
case 'login':
|
|
||||||
dispatch(login());
|
|
||||||
break;
|
|
||||||
case 'callback':
|
|
||||||
dispatch(handleOAuth2Callback(location.search));
|
|
||||||
history.push("/");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, [action]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title="Daddy - OAuth2">
|
|
||||||
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
export const Config = {
|
|
||||||
// The OpenID Connect client_id
|
|
||||||
oauth2ClientId: get<string>("oauth2ClientId", "daddy"),
|
|
||||||
oauth2Scope: get<string>("oauth2Scope", "email email_verified openid offline_access"),
|
|
||||||
oauth2RedirectURI: get<string>("oauth2RedirectURI", "http://localhost:8081/oauth2/callback"),
|
|
||||||
oauth2Audience: get<string>("oauth2Audience", ""),
|
|
||||||
oauth2AuthorizeURL: get<string>("oauth2AuthorizeURL", "http://localhost:4444/oauth2/auth"),
|
|
||||||
oauth2TokenURL: get<string>("oauth2TokenURL", "http://localhost:4444/oauth2/token"),
|
|
||||||
oauth2LogoutURL: get<string>("oauth2LogoutURL", "http://localhost:4444/oauth2/sessions/logout"),
|
|
||||||
oauth2PostLogoutRedirectURI: get<string>("oauth2PostLogoutRedirectURI", "http://localhost:8081"),
|
|
||||||
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8080/api/v1/graphql")
|
|
||||||
};
|
|
||||||
|
|
||||||
function get<T>(key: string, defaultValue: T):T {
|
|
||||||
const config = window['__CONFIG__'] || {};
|
|
||||||
if (config && config.hasOwnProperty(key)) {
|
|
||||||
return config[key] as T;
|
|
||||||
} else {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
@import 'bulma/bulma.sass';
|
|
||||||
@import 'bulma-switch/dist/css/bulma-switch.sass';
|
|
||||||
@import '_base.scss';
|
|
||||||
@import '_loader.scss';
|
|
|
@ -1,21 +0,0 @@
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-fullheight {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-margin-top-normal {
|
|
||||||
margin-top: $size-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-padding-small {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
.loader-container {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-ripple {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
transform: scale(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-ripple div {
|
|
||||||
position: absolute;
|
|
||||||
border: 4px solid $grey;
|
|
||||||
opacity: 1;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lds-ripple div:nth-child(2) {
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lds-ripple {
|
|
||||||
0% {
|
|
||||||
top: 36px;
|
|
||||||
left: 36px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
import { AccessGrant } from "../../util/auth";
|
|
||||||
import { IdToken } from "../../types/idToken";
|
|
||||||
|
|
||||||
export const LOGOUT = "LOGOUT_REQUEST";
|
|
||||||
|
|
||||||
export function logout() {
|
|
||||||
return { type: LOGOUT };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LOGIN_REQUEST = "LOGIN_REQUEST";
|
|
||||||
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
|
|
||||||
export const LOGIN_FAILURE = "LOGIN_FAILURE";
|
|
||||||
|
|
||||||
export function login() {
|
|
||||||
return { type: LOGIN_REQUEST };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HANDLE_OAUTH2_CALLBACK_REQUEST = "HANDLE_OAUTH2_CALLBACK_REQUEST";
|
|
||||||
export const HANDLE_OAUTH2_CALLBACK_SUCCESS = "HANDLE_OAUTH2_CALLBACK_SUCCESS";
|
|
||||||
export const HANDLE_OAUTH2_CALLBACK_FAILURE = "HANDLE_OAUTH2_CALLBACK_FAILURE";
|
|
||||||
|
|
||||||
export interface handleOAuth2CallbackAction extends Action {
|
|
||||||
search: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleOAuth2Callback(search: string): handleOAuth2CallbackAction {
|
|
||||||
return { type: HANDLE_OAUTH2_CALLBACK_REQUEST, search };
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface handleOAuth2CallbackSuccessAction extends Action {
|
|
||||||
grant: AccessGrant
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleOAuth2CallbackSuccess(grant: AccessGrant): handleOAuth2CallbackSuccessAction {
|
|
||||||
return { type: HANDLE_OAUTH2_CALLBACK_SUCCESS, grant };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PARSE_ID_TOKEN_REQUEST = "PARSE_ID_TOKEN_REQUEST";
|
|
||||||
export const PARSE_ID_TOKEN_SUCCESS = "PARSE_ID_TOKEN_SUCCESS";
|
|
||||||
export const PARSE_ID_TOKEN_FAILURE = "PARSE_ID_TOKEN_FAILURE";
|
|
||||||
|
|
||||||
export interface parseIdTokenAction extends Action {
|
|
||||||
rawIdToken: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export function parseIdToken(rawIdToken: string): parseIdTokenAction {
|
|
||||||
return { type: PARSE_ID_TOKEN_REQUEST, rawIdToken };
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export interface parseIdTokenSuccessAction extends Action {
|
|
||||||
idToken: IdToken
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIdTokenSuccess(idToken: IdToken): parseIdTokenSuccessAction {
|
|
||||||
return { type: PARSE_ID_TOKEN_SUCCESS, idToken };
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
|
@ -1,19 +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 }
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { Action } from "redux";
|
|
||||||
import { User } from "../../types/user";
|
|
||||||
import { SET_CURRENT_USER, setCurrentUserAction, LOGOUT } from "../actions/auth";
|
|
||||||
import { FETCH_PROFILE_SUCCESS, fetchProfileSuccessAction } 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 LOGOUT:
|
|
||||||
return handleLogout(state);
|
|
||||||
case FETCH_PROFILE_SUCCESS:
|
|
||||||
return handleFetchProfileSuccess(state, action as fetchProfileSuccessAction);
|
|
||||||
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSetCurrentUser(state: AuthState, { email }: setCurrentUserAction): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
currentUser: {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleLogout(state: AuthState): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: false,
|
|
||||||
currentUser: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleFetchProfileSuccess(state: AuthState, { profile }: fetchProfileSuccessAction): AuthState {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isAuthenticated: true,
|
|
||||||
currentUser: {
|
|
||||||
...profile,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue