From 8fc7b243c8183e546f4ef78382475df8a34fd899 Mon Sep 17 00:00:00 2001 From: AxelRagobert Date: Thu, 16 May 2019 17:25:09 +0200 Subject: [PATCH 1/2] FEATURE: Duplicate portfolio (closes #190) --- etapes | 2 + package.json | 5 +- reverse-proxy/package.json | 18 ++ reverse-proxy/server.js | 21 ++ .../Authenticated/Authenticated.jsx | 37 ++- src/components/Duplication/Duplicator.jsx | 213 ++++++++++++++++++ src/components/Item/Item.jsx | 10 +- src/components/Outliner/Outliner.jsx | 8 +- src/components/Portfolio/Portfolio.jsx | 14 +- src/config/config.json | 3 +- src/images/icons8-plus-64.png | Bin 0 -> 1198 bytes src/styles/App.css | 102 ++++++++- 12 files changed, 405 insertions(+), 28 deletions(-) create mode 100644 etapes create mode 100644 reverse-proxy/package.json create mode 100644 reverse-proxy/server.js create mode 100644 src/components/Duplication/Duplicator.jsx create mode 100644 src/images/icons8-plus-64.png diff --git a/etapes b/etapes new file mode 100644 index 00000000..7b129309 --- /dev/null +++ b/etapes @@ -0,0 +1,2 @@ +modifier etc host +ajouter reverse proxy en local diff --git a/package.json b/package.json index 5818b5c2..70d7d742 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,18 @@ "dependencies": { "bootstrap": "^4.1.1", "hypertopic": "^3.1.4", + "jquery": "^3.4.1", "js-tree": "^2.0.1", "json-groupby": "^1.0.2", "open-iconic": "^1.1.1", "query-string": "^4.3.4", "react": "^16.4.0", "react-autosuggest": "^9.3.4", + "react-bootstrap": "^1.0.0-beta.8", "react-dom": "^16.4.0", "react-router-dom": "^4.1.1", - "sort-by": "^1.2.0" + "sort-by": "^1.2.0", + "styled-components": "^4.2.0" }, "devDependencies": { "react-scripts": "^3.0.1", diff --git a/reverse-proxy/package.json b/reverse-proxy/package.json new file mode 100644 index 00000000..bc62d589 --- /dev/null +++ b/reverse-proxy/package.json @@ -0,0 +1,18 @@ +{ + "name": "reverse-proxy", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.17.1", + "express-http-proxy": "^1.5.1", + "http-proxy": "^1.17.0" + } +} diff --git a/reverse-proxy/server.js b/reverse-proxy/server.js new file mode 100644 index 00000000..eeac98e6 --- /dev/null +++ b/reverse-proxy/server.js @@ -0,0 +1,21 @@ +var express = require('express') +var cors = require('cors') +const proxy = require('express-http-proxy'); + +var app = express() + +var corsOptions = { + origin: true, + optionsSuccessStatus: 200, + credentials: true +} + +app.use(cors(corsOptions)); +app.use(proxy('argos2.test.hypertopic.org')); + + + + +app.listen(80, function () { + console.log('CORS-enabled web server listening on port 80') +}) \ No newline at end of file diff --git a/src/components/Authenticated/Authenticated.jsx b/src/components/Authenticated/Authenticated.jsx index 0394c93a..deaddcfc 100644 --- a/src/components/Authenticated/Authenticated.jsx +++ b/src/components/Authenticated/Authenticated.jsx @@ -1,5 +1,7 @@ import React, { Component } from 'react'; import conf from '../../config/config.json'; +import Duplicator from '../Duplication/Duplicator' +import { Dropdown, DropdownButton, ButtonGroup, Button, Form } from 'react-bootstrap'; const SESSION_URI = conf.services[0] + '/_session'; @@ -18,24 +20,39 @@ class Authenticated extends Component { render() { if (this.state.user) { + if(this.props.portfolio){ + return ( +
+ + Se déconnecter + + +
+ ); + } + return ( -
{this.state.user} - Se déconnecter +
+ + Se déconnecter +
); } if (this.state.ask) { return( -
- this.login = x} /> - this.password = x} type="password" /> - -
+
+ this.login = x} placeholder="Nom d'utilisateur" /> + this.password = x} placeholder="Mot de passe" /> + + ); } return (
- Se connecter... +
); } @@ -82,7 +99,9 @@ class Authenticated extends Component { _closeSession() { fetch(SESSION_URI, {method:'DELETE', credentials:'include'}) - .then(() => this.setState({user: ''})); + .then(() => { + this.setState({user: ''}) + }); } componentDidMount() { diff --git a/src/components/Duplication/Duplicator.jsx b/src/components/Duplication/Duplicator.jsx new file mode 100644 index 00000000..df9e021e --- /dev/null +++ b/src/components/Duplication/Duplicator.jsx @@ -0,0 +1,213 @@ +import React, { Component } from 'react'; +import { Modal, Button, Dropdown } from 'react-bootstrap'; +import $ from 'jquery' +var hypertopic = require("hypertopic") + +class Duplicator extends Component { + + constructor(props) { + super(props); + + this.state = { + showModal: false, + showModalConfirmation: false, + showToast: false, + corpora: [], + viewpoints: [] + }; + this.handleShow = this.handleShow.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleValidate = this.handleValidate.bind(this); + this.handleConfirm = this.handleConfirm.bind(this); + this.onChangeCheckBox = this.onChangeCheckBox.bind(this); + this.closeToast = this.closeToast.bind(this) + this.addUserToEntity = this.addUserToEntity.bind(this) + } + + handleClose() { + this.setState({ showModal: false, showModalConfirmation: false }); + } + + handleShow() { + this.setState({ showModal: true }); + } + + handleValidate() { + if(this.state.viewpoints.length > 0 || this.state.corpora.length > 0) { + console.log(this.state.viewpoints) + console.log(this.state.corpora) + this.nameDuplicatedPortfolio = $('#portfolioDuplicatedName').val() + this.handleClose(); + this.setState({ showModalConfirmation: true}) + } + } + + async handleConfirm() { + this.setState({ showModalConfirmation: false, showToast: true }); + + var ids = this.state.viewpoints.concat(this.state.corpora) + let that = this + + await Promise.all(ids.map(id => that.addUserToEntity(id))) + //refresh page + this.setState({showToast: false }); + window.location.replace("http://" + this.nameDuplicatedPortfolio + ".local:3000"); + + } + + addUserToEntity(user){ + let db = hypertopic([ + "http://localhost", + "http://steatite.hypertopic.org" + ]); + + const _error = (x) => { + console.error(x.message) + return x + }; + + return db.get({_id:user}) + .then( (x) => { + if(!x.users.includes(this.nameDuplicatedPortfolio)) + x.users.push(this.nameDuplicatedPortfolio) + return x + }) + .then(db.post) + .then((x) => { + console.log(x) + }) + .catch(_error); + } + + closeToast() { + this.setState(() => { + return { + showToast: false + }; + }); + } + onChangeCheckBox(e) { + $('.checkCorpora').each(function() { + if ($(this).is(":checked")) { + $('.Corpus').each(function() { + $(this).prop('checked', true); + $(this).prop('disabled', true); + }); + }else{ + $('.Corpus').each(function() { + $(this).prop('disabled', false); + }); + } + }); + + $('.checkViewPoints').each(function() { + if ($(this).is(":checked")) { + $('.ViewPoint').each(function() { + $(this).prop('checked', true); + $(this).prop('disabled', true); + }); + }else{ + $('.ViewPoint').each(function() { + $(this).prop('disabled', false); + }); + } + }); + + var selectedViewPoints = []; + $('.ViewPoint').each(function() { + if ($(this).is(":checked")) { + selectedViewPoints.push($(this).attr('value')); + } + }); + + var selectedCorpora = []; + $('.Corpus').each(function() { + if ($(this).is(":checked")) { + selectedCorpora.push($(this).attr('value')); + } + }); + + this.setState({ + corpora: selectedCorpora, + viewpoints: selectedViewPoints + }) + } + + render() { + let name = this.props.userConnected + '-' + this.props.portfolio + let corpora = this.props.corpora.map((v, i) => +
+ + {v.id} +
+ ); + + let viewpoints = this.props.viewpoints.map((v, i) => +
+ + {v.name} +
+ ); + + + return ( +
+ + +

Redirection à la nouvelle page...

+
+
+ + + +

Créer le nouveau portfolio

+
+ + + + +
+ + + +

Nom du portfolio

+ +
+

Corpus

+
Tout
+
+
+
+ {corpora} +
+
+
+

Points de vue

+
Tout
+
+
+
+ {viewpoints} +
+
+ + + + +
+ Dupliquer +
+ + ) + } +} + +export default Duplicator; diff --git a/src/components/Item/Item.jsx b/src/components/Item/Item.jsx index 34ac29d2..910c8e6b 100644 --- a/src/components/Item/Item.jsx +++ b/src/components/Item/Item.jsx @@ -54,10 +54,12 @@ class Item extends Component {
- - - Retour à l'accueil - +
    +
  • + Retour à l'accueil +
  • +
  • +
diff --git a/src/components/Outliner/Outliner.jsx b/src/components/Outliner/Outliner.jsx index af646c0e..877ac22e 100644 --- a/src/components/Outliner/Outliner.jsx +++ b/src/components/Outliner/Outliner.jsx @@ -30,10 +30,12 @@ class Outliner extends React.Component {
- - +
    +
  • Retour à l'accueil - +
  • +
  • +
diff --git a/src/components/Portfolio/Portfolio.jsx b/src/components/Portfolio/Portfolio.jsx index 5bca632d..67b7a1f7 100644 --- a/src/components/Portfolio/Portfolio.jsx +++ b/src/components/Portfolio/Portfolio.jsx @@ -20,8 +20,9 @@ class Portfolio extends Component { corpora: [], items: [], selectedItems: [], - topicsItems: new Map() + topicsItems: new Map(), }; + this.user = conf.user || window.location.hostname.split('.', 1)[0]; this._updateSelection(); } @@ -30,12 +31,15 @@ class Portfolio extends Component { let viewpoints = this._getViewpoints(); let corpora = this._getCorpora(); let status = this._getStatus(); + return (
- - {status} +
    + {status} +
  • +
@@ -101,9 +105,9 @@ class Portfolio extends Component { let uri = '?' + queryString.stringify({ t: this._toggleTopic(this.selection, t) }); - return + return
  • {topic.name} - ; +
  • ; }); return topics.length ? topics : 'Tous les items'; } diff --git a/src/config/config.json b/src/config/config.json index a0134e68..c2f102e0 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1,7 +1,6 @@ { - "user": "vitraux", "services": [ - "http://argos2.test.hypertopic.org", + "http://localhost", "http://steatite.hypertopic.org" ] } diff --git a/src/images/icons8-plus-64.png b/src/images/icons8-plus-64.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7126c0c9c0b3a5c5e5f9b6024ae6860b60a494 GIT binary patch literal 1198 zcmV;f1X25mP)Xc8E+jt)LDGtrp%BCGhCAc~;k zLUg6zW8q2=3@Er1e1W)8a4W_KDsBXE5ho8t#TP-%LnXQil0+sGltgDf7u~szr>pz6 zT~&SWIQ_$2)TQqK|L5H5>f5Kzl{$3j(4oW9g=<3un3OanX@jJ3Nf${PG1E>-IwePI!-Tu&A?X) z0=M`AxF(kG;5`6XF6kLb4@)Zhc4j2)lr$}AucZBwj+r^5lGaIDFKJ8uT2;a~FX=@| zk2&W~hWHNxFa~^H@=eVFZv(dhcEhW~JwmqR-^sar_!KxlMQn8pUJd-}F*Ogo3XDh8 zngF(!B;hb{Wh||_hNgg7kC_9&^@(*4dUR)j8&m086E^`TJtkfN&Tc_}1@N**|99Zp z)Z*j7Z=NN;Evc~I%M-cwLL3DyP9?Y;_|g;n{YeDtd3w;3gwJTO>DCc@*0by$i7xct zeLd>&^;B$;%@l7%FGUpz0Pu?O_7|`z7P$lV7=eAjIZ*^609Z-!?(k78@|!)^eekW$ zuGt8DTRoS;R4A{2orZLKFsb4vAm@C`(C!E&+++ynfOV0~rbxg9#l31n)o1!WTHa&E z?>p!Ah1pDpynyn7dHe3hguo$_%{zk}{+m1c3dniyGSojdCA!2A&5%9%#xj~J0V{wL zhW^5``Bj&|4Q9M+pwHel;hg(J(r)v%jiraG1lY}Ex~ZuF!2Sj(-3Y4^Fk!}f8{26g z)~k)BhpGhFO3Q)9cG`#adO_L5Gb>0|oAHr4%1t-4x}S5-)v04zuQrw*s#d_cW_+TC z@UY;x8LupxSd{>q>1emAVM0{`X3e-Xi$e%oPPei2P_+jfS5~V(jWsIP@(ZYWjTWx? zx}U3wHD)|hHnD02{9wjW9)ene^=f13p(+9U&3Ll0o%Uh9+E{w1O29W}yrr?7_F>E8 zHkSTaDi1^b$o}__!~F^(Z0k!HT0^!SQA?LU>7rQ0UAz}TX1l#JCX`?RuF*)vL} z`C4U62F(DBs< zi{uXzZU1FvQqq{DwUW-uUy^h*e;tzalcaC8*#9!qp+kob9Toxp20wb+ow;nzg#Z8m M07*qoM6N<$g6GgOZU6uP literal 0 HcmV?d00001 diff --git a/src/styles/App.css b/src/styles/App.css index cdbd6e39..20abd56e 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -6,6 +6,7 @@ background-color: #8b0000; color: ivory; margin-bottom: 0; + position: relative; } .App header .logo { @@ -27,16 +28,39 @@ h1 a, h1 a:hover { padding: 4px; } -.Authenticated { - position: absolute; - right: 0; +.Status ul::before { + content:"D"; + margin: 1px auto 1px 1px; + visibility: hidden; + padding: 5px; +} +.Status li:last-child { + margin-left: auto; +} +.Status ul { + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} +.Status li { + display: flex; + margin: 1px; + padding: 5px; } -.Authenticated a { +.SeConnecterButton { color: lightgrey; padding: 0 8px; } +.UserAuth { + margin-right: 10px; +} + .Authenticated input:not([type='submit']) { background: dimgrey; color: white; @@ -48,6 +72,10 @@ h1 a, h1 a:hover { color: lightgrey; } +.ProfilActions { + background-color: red; +} + .App-content { min-height: 100vh; } @@ -408,3 +436,69 @@ ul.Outliner, .btn:not(.btn-xs) .oi { vertical-align: middle; } + +.Duplicator { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-end; + color: ivory; + font-size: 1.15rem; + font-weight: 500; +} + +.Modal-Group { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; +} + +.Modal-Title { + justify-content: space-between; +} + + +.Modal-Input { + width: 100%; + margin-bottom: 30px; +} + +.Modal-CheckBox { + margin-right: 10px; +} +.toast-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0rem; + color: #6c757d; + background-color: hsla(0, 0%, 100%, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} +.toast-body { + padding: 0.75rem; +} +button.close { + padding: 0; + background-color: transparent; + border: 0; + appearance: none; + font-size: 1rem; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: 0.5; +} + +.FormConnect { + display: flex; + flex-direction: row; +} + +.form-control { + margin: 0 5px 0 5px; +} \ No newline at end of file From fded1adeb5814fbe3d042406b147b126a2d8f0ac Mon Sep 17 00:00:00 2001 From: Robin Lallier Date: Sun, 19 May 2019 11:27:45 +0200 Subject: [PATCH 2/2] TEST: Duplicate a portfolio (closes #190) --- Gemfile | 1 + features/duplicate_portfolio.feature | 17 ++++++++++++ features/items_with_same_topic.feature | 2 +- features/step_definitions/portfolio.rb | 34 +++++++++++++++++++++-- reverse-proxy/launcher.sh | 1 + src/components/Duplication/Duplicator.jsx | 18 ++++++------ src/config/config.json | 5 ++-- 7 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 features/duplicate_portfolio.feature create mode 100644 reverse-proxy/launcher.sh diff --git a/Gemfile b/Gemfile index 06fa1491..7b178925 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,4 @@ source "https://rubygems.org" gem "cucumber" gem "cuprite" +gem "rspec" diff --git a/features/duplicate_portfolio.feature b/features/duplicate_portfolio.feature new file mode 100644 index 00000000..3dba2448 --- /dev/null +++ b/features/duplicate_portfolio.feature @@ -0,0 +1,17 @@ +#language: fr + +Fonctionnalité: Dupliquer un portfolio pour un usage privé + +Contexte: + + Soit "vitraux" le portfolio ouvert + Soit le point de vue "Histoire des religions" rattaché au portfolio "vitraux" + Soit le corpus "Vitraux - Dr. Krieger" rattaché au portfolio "vitraux" + +Scénario: Dupliquer un portfolio + + Soit l'utilisateur est connecté + Quand on créé une copie du portfolio appelée "undefined" avec le corpus "Vitraux - Dr. Krieger" et le point de vue "Histoire des religions" + Alors le titre affiché est "undefined" + Et un des corpus affichés est "Vitraux - Dr. Krieger" + Et un des points de vue affichés est "Histoire des religions" diff --git a/features/items_with_same_topic.feature b/features/items_with_same_topic.feature index 0fc0c88e..97b00a2e 100644 --- a/features/items_with_same_topic.feature +++ b/features/items_with_same_topic.feature @@ -14,7 +14,7 @@ Scénario: Soit "vitraux" le portfolio ouvert Soit "BSS 007" l'item affiché - Quand on choisit la rubrique "Figuration du donateur" + Quand on choisit la rubrique "Figuration du donateur" contenue dans la rubrique "Donateur" Alors le titre affiché est "VITRAUX" Et l'item "BSS 007" est affiché Et l'item "BSS 018" est affiché diff --git a/features/step_definitions/portfolio.rb b/features/step_definitions/portfolio.rb index ea92eea7..b12f8625 100644 --- a/features/step_definitions/portfolio.rb +++ b/features/step_definitions/portfolio.rb @@ -4,7 +4,7 @@ Capybara.run_server = false Capybara.default_driver = :cuprite Capybara.javascript_driver = :cuprite -Capybara.app_host = "http://localhost:3000" +Capybara.app_host = "http://vitraux.local:3000" Capybara.default_max_wait_time = 10 def getUUID(itemName) @@ -48,6 +48,13 @@ def getUUID(itemName) # On the remote servers end +Soit("l'utilisateur est connecté") do + find_link(href: '#login').click + fill_in("Nom d'utilisateur", with: "alice") + fill_in("Mot de passe", with: "whiterabbit") + click_on('Confirmer') +end + Soit("{string} le portfolio spécifié dans la configuration") do |portfolio| case portfolio when "vitraux" @@ -106,10 +113,34 @@ def getUUID(itemName) click_on topic end +Quand("on choisit la rubrique {string} contenue dans la rubrique {string}") do |string, string2| + click_on string + click_on string2 +end + Quand("on choisit l'item {string}") do |item| click_on item end +def in_modal() + f = find('.modal-content') +end + +Quand("on créé une copie du portfolio appelée {string} avec le corpus {string} et le point de vue {string}") do |name, corpus, viewpoint| + case name + when "undefined" + pending "alternate configuration" + else + click_button('alice') + click_link('Dupliquer') + in_modal.fill_in('copyName', with: name) + in_modal.check(corpus) + in_modal.check(viewpoint) + in_modal.click_on('Valider') + in_modal.click_on('Confirmer') + end +end + # Outcomes Alors("le titre affiché est {string}") do |portfolio| @@ -139,4 +170,3 @@ def getUUID(itemName) Alors ("l'item {string} n'est pas affiché") do |item| expect(page).not_to have_content item end - diff --git a/reverse-proxy/launcher.sh b/reverse-proxy/launcher.sh new file mode 100644 index 00000000..a1708ef0 --- /dev/null +++ b/reverse-proxy/launcher.sh @@ -0,0 +1 @@ +sudo sed -i '/^127.0.0.1 localhost/ s/$/ vitraux.local dupp-vitraux.local/' /etc/hosts \ No newline at end of file diff --git a/src/components/Duplication/Duplicator.jsx b/src/components/Duplication/Duplicator.jsx index df9e021e..433a3665 100644 --- a/src/components/Duplication/Duplicator.jsx +++ b/src/components/Duplication/Duplicator.jsx @@ -23,11 +23,11 @@ class Duplicator extends Component { this.closeToast = this.closeToast.bind(this) this.addUserToEntity = this.addUserToEntity.bind(this) } - + handleClose() { this.setState({ showModal: false, showModalConfirmation: false }); } - + handleShow() { this.setState({ showModal: true }); } @@ -133,22 +133,22 @@ class Duplicator extends Component { }) } - render() { + render() { let name = this.props.userConnected + '-' + this.props.portfolio let corpora = this.props.corpora.map((v, i) =>
    - + {v.id}
    ); let viewpoints = this.props.viewpoints.map((v, i) =>
    - + {v.name}
    ); - + return (
    @@ -175,7 +175,7 @@ class Duplicator extends Component {

    Nom du portfolio

    - +

    Corpus

    Tout
    @@ -187,7 +187,7 @@ class Duplicator extends Component {

    Points de vue

    -
    Tout
    +
    Tout

    @@ -205,7 +205,7 @@ class Duplicator extends Component { Dupliquer
    - + ) } } diff --git a/src/config/config.json b/src/config/config.json index c2f102e0..ea96605e 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1,6 +1,7 @@ { + "user": "vitraux", "services": [ - "http://localhost", + "http://argos2.test.hypertopic.org", "http://steatite.hypertopic.org" ] -} +} \ No newline at end of file