From e03c076caba6a4ba9e535a15c7d80dccc228422f Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Mon, 28 Feb 2022 16:50:16 +0100 Subject: [PATCH] Bug 30160: Don't use classes but hooks The modern way to use react is to not use classes but functions, as well as useEffects and useState https://en.reactjs.org/docs/hooks-intro.html --- gulpfile.js | 45 +++-- koha-tmpl/intranet-tmpl/prog/js/src/Cities.js | 161 ------------------ .../prog/js/src/cities/AddCity.js | 121 ------------- .../prog/js/src/cities/DeleteCity.js | 67 -------- .../prog/js/src/components/Cities.js | 144 ++++++++++++++++ .../prog/js/src/components/cities/AddCity.js | 104 +++++++++++ .../js/src/components/cities/DeleteCity.js | 57 +++++++ .../cities/ListCity.js} | 134 +++++---------- koha-tmpl/intranet-tmpl/prog/js/src/index.js | 4 +- package.json | 4 +- 10 files changed, 390 insertions(+), 451 deletions(-) delete mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/Cities.js delete mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js delete mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/components/Cities.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/components/cities/AddCity.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/components/cities/DeleteCity.js rename koha-tmpl/intranet-tmpl/prog/js/src/{cities/CityList.js => components/cities/ListCity.js} (54%) diff --git a/gulpfile.js b/gulpfile.js index dc738521f47..4913936581d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -11,6 +11,8 @@ const util = require('util'); const tap = require( "gulp-tap" ); const browserify = require( "browserify" ); const babelify = require( "babelify" ); +const watchify = require('watchify'); +const source = require('vinyl-source-stream'); const sass = require("gulp-sass"); const cssnano = require("gulp-cssnano"); @@ -86,18 +88,39 @@ function build() { } } -function build_js(){ - - let bundler = browserify().transform(babelify, {presets: ["@babel/preset-env", "@babel/preset-react"]}); - - return src( js_base + '/src/index.js' ) - .pipe( tap( file => { - let base_start = file.path.indexOf( js_base ); - bundler.require( [ "react", "react-dom" ] ); - file.contents = bundler.add( file.path ).bundle(); - } ) ) - .pipe( dest( js_base+ "/built" ) ); +function build_js(){ + var bundler = browserify({ + entries: [js_base + '/src/index.js'], + debug: false, + cache: {}, + packageCache: {}, + extensions: ['.js', '.json', '.jsx'], + paths: [js_base + 'src'], + fullPaths: false + }); + + bundler = bundler.transform(babelify, {presets: ["@babel/preset-env", "@babel/preset-react"]}); + + function bundle(b) { + var startMs = Date.now(); + var db = b.bundle() + .on('error', function(err) { + console.log(err.message); + this.emit('end'); + }) + .pipe(source('../built/index.js')) + db.pipe(dest(js_base + '/src')); + console.log('Updated bundle file in', (Date.now() - startMs) + 'ms'); + return db; + } + + bundler = watchify(bundler) + .on('update', function() { + bundle(bundler); + }); + + return bundle(bundler); } const poTasks = { diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js b/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js deleted file mode 100644 index 70b5c815ee2..00000000000 --- a/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js +++ /dev/null @@ -1,161 +0,0 @@ -import React, { Component } from 'react' -import { Container, Button, Alert } from 'react-bootstrap' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faPlus } from '@fortawesome/free-solid-svg-icons' -import CityList from './cities/CityList' -import AddCity from './cities/AddCity' -import DeleteCity from './cities/DeleteCity' - -class App extends Component { - constructor(props) { - super(props) - this.state = { - op: 'list', - error: null, - message: null, - city: {}, - cities: [], - } - } - - onCreate() { - this.setState({ op: 'add', city: {}, error: null, message: null }) - } - - addCity = city => { - let apiUrl - - apiUrl = '/api/v1/cities' - - const myHeaders = new Headers() - myHeaders.append('Content-Type', 'application/json') - - let method = 'POST' - if ( city.city_id ) { - method = 'PUT' - apiUrl += '/' + city.city_id - } - delete city.city_id - - const options = { - method: method, - body: JSON.stringify(city), - myHeaders - } - - fetch(apiUrl, options) - .then(response => { - if ( response.status == 200 ) { - this.setState({ - message: 'City updated', - error: null, - op: 'list', - }) - } else if ( response.status == 201 ) { - this.setState({ - message: 'City created', - error: null, - op: 'list', - }) - } else { - this.setState({ error: response.message }) - } - }, (error) => { - this.setState({ error }) - }) - } - - editCity = city_id => { - - const apiUrl = '/api/v1/cities/' + city_id - - const options = { - method: 'GET', - } - - fetch(apiUrl, options) - .then(res => res.json()) - .then( - (result) => { - this.setState({ - city: result, - op: 'add', - message: null, - error: null, - }) - }, (error) => { - this.setState({ error }) - } - ) - } - - confirmDeleteCity = city_id => { - const apiUrl = '/api/v1/cities/' + city_id - - const options = { - method: 'GET', - } - - fetch(apiUrl, options) - .then(res => res.json()) - .then( - (result) => { - this.setState({ - city: result, - op: 'delete', - message: null, - error: null, - }) - }, (error) => { - this.setState({ error }) - } - ) - } - - deleteCity = city_id => { - - const { cities } = this.state - const apiUrl = '/api/v1/cities/' + city_id - - const options = { - method: 'DELETE', - } - - fetch(apiUrl, options) - .then(response => { - if ( response.status == 204 ) { - this.setState({ - message: 'City deleted', - error: null, - op: 'list', - }) - } else { - this.setState({ error: response.message }) - } - }, (error) => { - this.setState({ error }) - } - ) - } - - cancel = event => { - event.preventDefault() - this.setState({op: 'list'}) - } - - render() { - - return ( -
- {this.state.op == 'list' &&
} - {this.state.message &&
{this.state.message}
} - {this.state.error &&
Error: {this.state.error.message}
} - {this.state.op == 'list' && } - {this.state.op == 'add' && } - {this.state.op == 'delete' && } -
- ) - } -} - -export default App diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js b/koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js deleted file mode 100644 index 57205c9c031..00000000000 --- a/koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import { Row, Form, Col, Button } from 'react-bootstrap'; - -class AddCity extends React.Component { - constructor(props) { - super(props); - this.initialState = { - city_id: '', - name: '', - state: '', - postal_code: '', - country: '' - } - - if(props.city.city_id){ - this.state = props.city - } else { - this.state = this.initialState; - } - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleCancel = this.handleCancel.bind(this); - } - - handleChange(event) { - const name = event.target.name; - const value = event.target.value; - - this.setState({ - [name]: value - }) - } - - handleSubmit(event) { - event.preventDefault(); - this.props.onFormSubmit(this.state); - } - - handleCancel(event) { - event.preventDefault(); - this.props.onFormCancel(event); - } - - render() { - - let pageTitle; - if(this.state.city_id) { - pageTitle =

Edit city

- } else { - pageTitle =

New city

- } - - return( -
- {pageTitle} -
-
- - City: - - - Required - - - - State: - - - - - - ZIP/Postal code: - - - Required - - - - Country: - - - - -
-
- - - - this.handleCancel(e)}>Cancel - -
-
-
- ) - } -} - -export default AddCity; diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js b/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js deleted file mode 100644 index b2bb037eded..00000000000 --- a/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Row, Form, Col, Button } from 'react-bootstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck, faRemove } from '@fortawesome/free-solid-svg-icons' - -class DeleteCity extends React.Component { - constructor(props) { - super(props); - this.state = props.city; - - this.handleSubmit = this.handleSubmit.bind(this); - this.handleCancel = this.handleCancel.bind(this); - } - - handleSubmit(event) { - event.preventDefault(); - this.props.onFormSubmit(this.state.city_id); - } - handleCancel(event) { - event.preventDefault(); - this.props.onFormCancel(event); - } - - render() { - - let pageTitle =

Delete city

- - return( -
- - {pageTitle} -
-
-

Delete city "{this.state.name}"?

- - City id: - {this.state.city_id} - - - City: - {this.state.name} - - - State: - {this.state.state} - - - ZIP/Postal code: - {this.state.postal_code} - - - Country: - {this.state.country} - -
-
- - -
-
-
-
- ) - } -} - -export default DeleteCity; diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/components/Cities.js b/koha-tmpl/intranet-tmpl/prog/js/src/components/Cities.js new file mode 100644 index 00000000000..996d0d8a609 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/src/components/Cities.js @@ -0,0 +1,144 @@ +import React, { useState } from 'react'; +import { Container, Button, Alert } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faPlus } from '@fortawesome/free-solid-svg-icons' +import ListCity from './cities/ListCity' +import AddCity from './cities/AddCity' +import DeleteCity from './cities/DeleteCity' + +const Cities = () => { + const [op, setOp] = useState('list') + const [error, setError] = useState() + const [message, setMessage] = useState() + const [city, setCity] = useState({}) + + const onCreate = () => { + setOp('add'); + setCity({}); + setError(null); + setMessage(null); + } + + const addCity = city => { + let apiUrl + + apiUrl = '/api/v1/cities' + + const myHeaders = new Headers() + myHeaders.append('Content-Type', 'application/json') + + let method = 'POST' + if ( city.city_id ) { + method = 'PUT' + apiUrl += '/' + city.city_id + } + delete city.city_id + + const options = { + method: method, + body: JSON.stringify(city), + myHeaders + } + + fetch(apiUrl, options) + .then(response => { + if ( response.status == 200 ) { + setMessage('City updated') + setError(null) + setOp('list') + } else if ( response.status == 201 ) { + setMessage('City created') + setError(null) + setOp('list') + } else { + setError(response.message); + } + }, (e) => { + setError(e); + }) + } + + const editCity = city_id => { + + const apiUrl = '/api/v1/cities/' + city_id + + const options = { + method: 'GET', + } + + fetch(apiUrl, options) + .then(res => res.json()) + .then( + (result) => { + setCity(result) + setOp('add') + setMessage(null) + setError(null) + }, (e) => { + setError(e) + } + ) + } + + const confirmDeleteCity = city_id => { + const apiUrl = '/api/v1/cities/' + city_id + + const options = { + method: 'GET', + } + + fetch(apiUrl, options) + .then(res => res.json()) + .then( + (result) => { + setCity(result) + setOp('delete') + setMessage(null) + setError(null) + }, (e) => { + setError(e); + } + ) + } + + const deleteCity = city_id => { + + const apiUrl = '/api/v1/cities/' + city_id + + const options = { + method: 'DELETE', + } + + fetch(apiUrl, options) + .then(response => { + if ( response.status == 204 ) { + setMessage('City deleted') + setError(null) + setOp('list') + } else { + setError(response.message) + } + }, (e) => { + setError(e) + } + ) + } + + const cancel = event => { + event.preventDefault() + setOp('list') + } + + return ( +
+ {op == 'list' &&
} + {message &&
{message}
} + {error &&
Error: {error.message}
} + {op == 'list' && } + {op == 'add' && } + {op == 'delete' && } +
+ ) +} + +export default Cities diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/AddCity.js b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/AddCity.js new file mode 100644 index 00000000000..6dc64d0ae8b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/AddCity.js @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { Row, Form, Col, Button } from 'react-bootstrap'; + +const AddCity = ({ onFormSubmit, onFormCancel, city }) => { + + const initialState = { + city_id: '', + name: '', + state: '', + postal_code: '', + country: '' + } + const [myCity, setCity] = useState(city ? city : initialState) + + const handleChange = event => { + const name = event.target.name; + const value = event.target.value; + setCity({...myCity, [name]: value}) + } + + const handleSubmit = event => { + event.preventDefault(); + onFormSubmit(myCity); + } + + const handleCancel = event => { + event.preventDefault(); + onFormCancel(event); + } + + let pageTitle; + if(city.city_id) { + pageTitle =

Edit city

+ } else { + pageTitle =

New city

+ } + + return( +
+ {pageTitle} +
+
+ + City: + + + Required + + + + State: + + + + + + ZIP/Postal code: + + + Required + + + + Country: + + + + +
+
+ + + + handleCancel(e)}>Cancel + +
+
+
+ ) +} + +export default AddCity; diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/DeleteCity.js b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/DeleteCity.js new file mode 100644 index 00000000000..37bc4958bdf --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/DeleteCity.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Row, Form, Col, Button } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck, faRemove } from '@fortawesome/free-solid-svg-icons' + +const DeleteCity = ({ onFormSubmit, onFormCancel, city }) => { + + const handleSubmit = event => { + event.preventDefault(); + onFormSubmit(city.city_id); + } + const handleCancel = event => { + event.preventDefault(); + onFormCancel(event); + } + + let pageTitle =

Delete city

+ + return( +
+ + {pageTitle} +
+
+

Delete city "{city.name}"?

+ + City id: + {city.city_id} + + + City: + {city.name} + + + State: + {city.state} + + + ZIP/Postal code: + {city.postal_code} + + + Country: + {city.country} + +
+
+ + +
+
+
+
+ ) +} + +export default DeleteCity; diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/ListCity.js similarity index 54% rename from koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js rename to koha-tmpl/intranet-tmpl/prog/js/src/components/cities/ListCity.js index c68bab36533..d289a934478 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js +++ b/koha-tmpl/intranet-tmpl/prog/js/src/components/cities/ListCity.js @@ -1,51 +1,27 @@ -import React, { Component } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import ReactDOM from 'react-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faPencil, faTrash } from '@fortawesome/free-solid-svg-icons' import { Button, Alert } from 'react-bootstrap'; -class EditButton extends Component{ - constructor(props){ - super(props); - this.handleClick = this.handleClick.bind(this); - } - handleClick(){ - this.props.handleClick(this.props.city_id); - } - - render(){ - return ( - - ) - } +const EditButton = ({city_id, handleClick}) => { + return ( + + ) } -class DeleteButton extends Component{ - constructor(props){ - super(props); - this.handleClick = this.handleClick.bind(this); - } - handleClick(){ - this.props.handleClick(this.props.city_id); - } - - render(){ - return ( - - ) - } +const DeleteButton = ({city_id, handleClick}) => { + return ( + + ) } -class Table extends Component { - constructor(props){ - super(props); - } +const Table = ({editCity, confirmDeleteCity}) => { + const table_ref = React.createRef() - componentDidMount() { + useEffect(() => { - const editCity = this.props.editCity; - const confirmDeleteCity = this.props.confirmDeleteCity; - $(this.refs.main).kohaTable({ + $(table_ref.current).kohaTable({ "ajax": { "url": cities_table_url, }, @@ -106,71 +82,53 @@ class Table extends Component { } }, columns_settings, 1); - } - componentWillUnmount(){ - $('.data-table-wrapper') - .find('table') - .DataTable() - .destroy(true); - } - - shouldComponentUpdate() { - return true; - } - - render() { - return ( -
- - ); - } + return function cleanup(){ + $('.data-table-wrapper') + .find('table') + .DataTable() + .destroy(true); + } + }, []) + + return ( +
+
+ ) } -class CityList extends React.Component { - constructor(props) { - super(props); - this.state = { - error: null, - cities: [], - } - - if ( props.cities ){ - this.state.cities = props.cities - } - } +const ListCity = ({ editCity, confirmDeleteCity }) => { + const [cities, setCities] = useState() - componentDidMount() { + useEffect(() => { const apiUrl = '/api/v1/cities'; + let r = [] fetch(apiUrl) .then(res => res.json()) .then( (result) => { - this.setState({ - cities: result - }); + setCities(result) }, (error) => { - this.setState({ error }); + console.log(error) } ) - } - - render() { - const { error, cities } = this.state; - - if(error) { - return ("Error: {error.message}") - } else if(cities.length) { - return(
-

Cities

-
- ) - } else { - return(

Cities

There are no cities defined.
) - } + }, []) + + if(cities == null ) { + return (
+

Cities

+
Loading...
+
) + } else if(cities.length) { + return(
+

Cities

+
+ ) + } else { + return(

Cities

There are no cities defined.
) } } -export default CityList; +export default ListCity; diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/index.js b/koha-tmpl/intranet-tmpl/prog/js/src/index.js index 06f48cac9a8..4d04b54b5bf 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/src/index.js +++ b/koha-tmpl/intranet-tmpl/prog/js/src/index.js @@ -1,5 +1,5 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './Cities'; +import Cities from './components/Cities.js'; -ReactDOM.render(, document.getElementById('cities_container')); +ReactDOM.render(, document.getElementById('cities_container')); diff --git a/package.json b/package.json index 0dc659a0b70..914ee44b538 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "author": "", "license": "GPL-3.0", "devDependencies": { - "@types/react": "^17.0.39" + "@types/react": "^17.0.39", + "vinyl-source-stream": "^2.0.0", + "watchify": "^4.0.0" } } -- 2.25.1