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/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/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 20340c8b..6f84e9cb 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 @@ -62,6 +62,13 @@ def getPassword(username) # 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" @@ -132,10 +139,31 @@ def getPassword(username) 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') Quand("l'utilisateur exclue la rubrique {string}") do |topic| click_on topic end diff --git a/package.json b/package.json index e369bbea..453abf42 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/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/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..433a3665 --- /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 5a507ccd..dc853452 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(); } @@ -29,13 +30,16 @@ class Portfolio extends Component { render() { let viewpoints = this._getViewpoints(); let corpora = this._getCorpora(); + let status = this._getStatus(); return (
- - +
    + {status} +
  • +
@@ -92,6 +96,30 @@ class Portfolio extends Component { return null; } + _getStatus() { + let topics = this.selection.map(t => { + let topic = this._getTopic(t); + if (!topic) { + return 'Thème inconnu'; + } + let uri = '?' + queryString.stringify({ + t: this._toggleTopic(this.selection, t) + }); + return
  • + {topic.name} +
  • ; + }); + return topics.length ? topics : 'Tous les items'; + } + + _toggleTopic(array, item) { + let s = new Set(array); + if (!s.delete(item)) { + s.add(item); + } + return [...s]; + } + _updateSelection() { try { this.selectionJSON = JSON.parse(queryString.parse(window.location.search).t); diff --git a/src/config/config.json b/src/config/config.json index a0134e68..ea96605e 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -4,4 +4,4 @@ "http://argos2.test.hypertopic.org", "http://steatite.hypertopic.org" ] -} +} \ No newline at end of file diff --git a/src/images/icons8-plus-64.png b/src/images/icons8-plus-64.png new file mode 100644 index 00000000..ac7126c0 Binary files /dev/null and b/src/images/icons8-plus-64.png differ diff --git a/src/styles/App.css b/src/styles/App.css index 2eef3563..627a96a0 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; } @@ -423,3 +451,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