Bugzilla – Attachment 131031 Details for
Bug 30160
Rewrite cities admin view in React
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30160: Cities view rewrite using React
Bug-30160-Cities-view-rewrite-using-React.patch (text/plain), 35.79 KB, created by
Jonathan Druart
on 2022-02-23 09:25:00 UTC
(
hide
)
Description:
Bug 30160: Cities view rewrite using React
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2022-02-23 09:25:00 UTC
Size:
35.79 KB
patch
obsolete
>From b91fabfddb0f9043766e655672fd9cb059ae4a85 Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Wed, 23 Feb 2022 10:04:18 +0100 >Subject: [PATCH] Bug 30160: Cities view rewrite using React > >This patch, for discussion, is trying to provide the easier >implementation of the cities CRUD operation using React. > >The react-bootstrap library is used to take profit of the different >bootstrap elements. > >Note that it's for me an initiation at React and I don't have much skills. >I've tried to provide the more concise and readable approach, with as >less bugs as possible. Feedback welcome, especially if you know React >better than me! >The bad thing with this approach is that we are loosing the history, >clicking browser's back button won't work as before. I don't think we >can use the react-router approach as we are not having a full React app. > >TODO: >* There are some UI glitches on the add/edit and delete form (TODO) >* Translation strings? > >Test plan: >% yarn install >Hit Home > Administration > Cities >Add, edit and delete cities > >Note that if you want to modify the JS code here you will need to run >% gulp build_js >in order to regenerate the built version. >--- > admin/cities.pl | 68 ------- > gulpfile.js | 20 +- > .../prog/en/modules/admin/cities.tt | 191 +----------------- > koha-tmpl/intranet-tmpl/prog/js/datatables.js | 8 +- > koha-tmpl/intranet-tmpl/prog/js/src/Cities.js | 161 +++++++++++++++ > .../prog/js/src/cities/AddCity.js | 121 +++++++++++ > .../prog/js/src/cities/CityList.js | 176 ++++++++++++++++ > .../prog/js/src/cities/DeleteCity.js | 67 ++++++ > koha-tmpl/intranet-tmpl/prog/js/src/index.js | 5 + > package.json | 25 ++- > 10 files changed, 578 insertions(+), 264 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/Cities.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/src/index.js > >diff --git a/admin/cities.pl b/admin/cities.pl >index f16c9d7a50f..9e02c85e607 100755 >--- a/admin/cities.pl >+++ b/admin/cities.pl >@@ -28,9 +28,6 @@ use Koha::Cities; > > my $input = CGI->new; > my $city_name_filter = $input->param('city_name_filter') // q||; >-my $cityid = $input->param('cityid'); >-my $op = $input->param('op') || 'list'; >-my @messages; > > my ( $template, $loggedinuser, $cookie ) = get_template_and_user( > { template_name => "admin/cities.tt", >@@ -40,73 +37,8 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( > } > ); > >-my $dbh = C4::Context->dbh; >-if ( $op eq 'add_form' ) { >- my $city; >- if ($cityid) { >- $city = Koha::Cities->find($cityid); >- } >- >- $template->param( city => $city, ); >-} elsif ( $op eq 'add_validate' ) { >- my $city_name = $input->param('city_name'); >- my $city_state = $input->param('city_state'); >- my $city_zipcode = $input->param('city_zipcode'); >- my $city_country = $input->param('city_country'); >- >- if ($cityid) { >- my $city = Koha::Cities->find($cityid); >- $city->city_name($city_name); >- $city->city_state($city_state); >- $city->city_zipcode($city_zipcode); >- $city->city_country($city_country); >- eval { $city->store; }; >- if ($@) { >- push @messages, { type => 'error', code => 'error_on_update' }; >- } else { >- push @messages, { type => 'message', code => 'success_on_update' }; >- } >- } else { >- my $city = Koha::City->new( >- { city_name => $city_name, >- city_state => $city_state, >- city_zipcode => $city_zipcode, >- city_country => $city_country, >- } >- ); >- eval { $city->store; }; >- if ($@) { >- push @messages, { type => 'error', code => 'error_on_insert' }; >- } else { >- push @messages, { type => 'message', code => 'success_on_insert' }; >- } >- } >- $city_name = q||; >- $op = 'list'; >-} elsif ( $op eq 'delete_confirm' ) { >- my $city = Koha::Cities->find($cityid); >- $template->param( city => $city, ); >-} elsif ( $op eq 'delete_confirmed' ) { >- my $city = Koha::Cities->find($cityid); >- my $deleted = eval { $city->delete; }; >- >- if ( $@ or not $deleted ) { >- push @messages, { type => 'error', code => 'error_on_delete' }; >- } else { >- push @messages, { type => 'message', code => 'success_on_delete' }; >- } >- $op = 'list'; >-} >- >-if ( $op eq 'list' ) { >- $template->param( cities_count => Koha::Cities->search->count ); >-} >- > $template->param( >- cityid => $cityid, > city_name_filter => $city_name_filter, >- messages => \@messages, >- op => $op, > ); > > output_html_with_http_headers $input, $cookie, $template->output; >diff --git a/gulpfile.js b/gulpfile.js >index bf1f5150ef8..dc738521f47 100644 >--- a/gulpfile.js >+++ b/gulpfile.js >@@ -8,6 +8,9 @@ const fs = require('fs'); > const os = require('os'); > const path = require('path'); > const util = require('util'); >+const tap = require( "gulp-tap" ); >+const browserify = require( "browserify" ); >+const babelify = require( "babelify" ); > > const sass = require("gulp-sass"); > const cssnano = require("gulp-cssnano"); >@@ -81,8 +84,20 @@ function build() { > })) // Append "-rtl" to the filename. > .pipe(dest(css_base)); > } >+} >+ >+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" ) ); > >- return stream; > } > > const poTasks = { >@@ -358,6 +373,7 @@ function getLanguages () { > } > > exports.build = build; >+exports.build_js = build_js; > exports.css = css; > > exports['po:create'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].create))); >@@ -366,4 +382,4 @@ exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract)); > > exports.default = function () { > watch(css_base + "/src/**/*.scss", series('css')); >-} >+}; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/cities.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/cities.tt >index 2138aac1242..5fc417f3d86 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/cities.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/cities.tt >@@ -69,135 +69,7 @@ > <div class="row"> > <div class="col-sm-10 col-sm-push-2"> > <main> >- >-[% FOR m IN messages %] >- <div class="dialog [% m.type | html %]"> >- [% SWITCH m.code %] >- [% CASE 'error_on_update' %] >- An error occurred when updating this city. Perhaps it already exists. >- [% CASE 'error_on_insert' %] >- An error occurred when adding this city. The city id might already exist. >- [% CASE 'error_on_delete' %] >- An error occurred when deleting this city. Check the logs. >- [% CASE 'success_on_update' %] >- City updated successfully. >- [% CASE 'success_on_insert' %] >- City added successfully. >- [% CASE 'success_on_delete' %] >- City deleted successfully. >- [% CASE 'already_exists' %] >- This city already exists. >- [% CASE %] >- [% m.code | html %] >- [% END %] >- </div> >-[% END %] >- >-[% IF op == 'add_form' %] >- [% IF city %] >- <h1>Modify a city</h1> >- [% ELSE %] >- <h1>New city</h1> >- [% END %] >- >- <form action="/cgi-bin/koha/admin/cities.pl" name="Aform" method="post" class="validated"> >- <input type="hidden" name="op" value="add_validate" /> >- <input type="hidden" name="cityid" value="[% city.cityid | html %]" /> >- >- <fieldset class="rows"> >- <ol> >- [% IF city %] >- <li><span class="label">City ID: </span>[% city.cityid | html %]</li> >- [% END %] >- <li> >- <label for="city_name" class="required">City: </label> >- <input type="text" name="city_name" id="city_name" size="80" maxlength="100" value="[% city.city_name | html %]" required="required" class="required" /> <span class="required">Required</span> >- </li> >- <li> >- <label for="city_state">State: </label> >- <input type="text" name="city_state" id="city_state" size="80" maxlength="100" value="[% city.city_state | html %]" /> >- </li> >- <li> >- <label for="city_zipcode" class="required">ZIP/Postal code: </label> >- <input type="text" name="city_zipcode" id="city_zipcode" size="20" maxlength="20" value="[% city.city_zipcode | html %]" required="required" class="required" /> <span class="required">Required</span> >- </li> >- <li> >- <label for="city_country">Country: </label> >- <input type="text" name="city_country" id="city_country" size="80" maxlength="100" value="[% city.city_country | html %]" /> >- </li> >- </ol> >- </fieldset> >- >- <fieldset class="action"> >- <input type="submit" value="Submit" /> >- <a class="cancel" href="/cgi-bin/koha/admin/cities.pl">Cancel</a> >- </fieldset> >- </form> >-[% END %] >- >-[% IF op == 'delete_confirm' %] >- <div class="dialog alert"> >- <h3>Delete city "[% city.city_name | html %]?"</h3> >- <table> >- <tr><th>City id</th> >- <td>[% city.cityid | html %]</td> >- </tr> >- <tr><th>City</th> >- <td>[% city.city_name | html %]</td> >- </tr> >- <tr><th>State</th> >- <td>[% city.city_state | html %]</td> >- </tr> >- <tr><th>ZIP/Postal code</th> >- <td>[% city.city_zipcode | html %]</td> >- </tr> >- <tr><th>Country</th> >- <td>[% city.city_country | html %]</td> >- </tr> >- </table> >- <form action="/cgi-bin/koha/admin/cities.pl" method="post"> >- <input type="hidden" name="op" value="delete_confirmed" /> >- <input type="hidden" name="cityid" value="[% city.cityid | html %]" /> >- <button type="submit" class="approve"><i class="fa fa-fw fa-check"></i> Yes, delete</button> >- </form> >- <form action="/cgi-bin/koha/admin/cities.pl" method="get"> >- <button type="submit" class="deny"><i class="fa fa-fw fa-remove"></i> No, do not delete</button> >- </form> >- </div> >-[% END %] >- >-[% IF op == 'list' %] >- >- <div id="toolbar" class="btn-toolbar"> >- <a class="btn btn-default" id="newcity" href="/cgi-bin/koha/admin/cities.pl?op=add_form"><i class="fa fa-plus"></i> New city</a> >- </div> >- >- <h2>Cities</h2> >- [% IF city_name_filter %] >- Searching: [% city_name_filter | html %] >- [% END %] >- >- [% IF cities_count > 0 %] >- <div class="table_cities_table_controls"></div> >- <table id="table_cities"> >- <thead> >- <tr> >- <th>City ID</th> >- <th>City</th> >- <th>State</th> >- <th>ZIP/Postal code</th> >- <th>Country</th> >- <th data-class-name="actions noExport">Actions</th> >- </tr> >- </thead> >- </table> >- [% ELSE %] >- <div class="dialog message"> >- There are no cities defined. <a href="/cgi-bin/koha/admin/cities.pl?op=add_form">Create a new city</a>. >- </div> >- [% END %] >-[% END %] >- >+ <div id="cities_container"></div> > </main> > </div> <!-- /.col-sm-10.col-sm-push-2 --> > >@@ -213,10 +85,8 @@ > [% INCLUDE 'datatables.inc' %] > [% INCLUDE 'columns_settings.inc' %] > <script> >- > var columns_settings = [% TablesSettings.GetColumns( 'admin', 'cities', 'table_cities', 'json' ) | $raw %]; >- $(document).ready(function() { >- var cities_table_url = '/api/v1/cities?'; >+ var cities_table_url = '/api/v1/cities?'; > > [% IF city_name_filter %] > var city_name_filter = { >@@ -226,62 +96,7 @@ > }; > cities_table_url += 'q='+ encodeURIComponent(JSON.stringify(city_name_filter)); > [% END %] >- >- var cities_table = $("#table_cities").kohaTable({ >- "ajax": { >- "url": cities_table_url >- }, >- "order": [[ 1, "asc" ]], >- "columnDefs": [ { >- "targets": [0,1,2,3,4], >- "render": function (data, type, row, meta) { >- if ( type == 'display' ) { >- return data.escapeHtml(); >- } >- return data; >- } >- } ], >- "columns": [ >- { >- "data": "city_id", >- "searchable": true, >- "orderable": true >- }, >- { >- "data": "name", >- "searchable": true, >- "orderable": true >- }, >- { >- "data": "state", >- "searchable": true, >- "orderable": true >- }, >- { >- "data": "postal_code", >- "searchable": true, >- "orderable": true >- }, >- { >- "data": "country", >- "searchable": true, >- "orderable": true >- }, >- { >- "data": function( row, type, val, meta ) { >- >- var result = '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/admin/cities.pl?op=add_form&cityid='+ encodeURIComponent(row.city_id) +'"><i class="fa fa-pencil" aria-hidden="true"></i> '+_("Edit")+'</a>'+"\n"; >- result += '<a class="btn btn-default btn-xs" role="button" href="/cgi-bin/koha/admin/cities.pl?op=delete_confirm&cityid='+ encodeURIComponent(row.city_id) +'"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Delete")+'</a>'; >- return result; >- >- }, >- "searchable": false, >- "orderable": false >- } >- ] >- }, columns_settings, 1); >- >- }); > </script> >+ [% Asset.js("js/built/index.js") | $raw %] > [% END %] > [% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >index c95a9723284..99d7d470d72 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >@@ -505,10 +505,6 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > $.fn.kohaTable = function(options, columns_settings, add_filters, default_filters) { > var settings = null; > >- if ( add_filters ) { >- $(this).find('thead tr').clone(true).appendTo( $(this).find('thead') ); >- } >- > if(options) { > if(!options.criteria || ['contains', 'starts_with', 'ends_with', 'exact'].indexOf(options.criteria.toLowerCase()) === -1) options.criteria = 'contains'; > options.criteria = options.criteria.toLowerCase(); >@@ -769,8 +765,12 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) { > > if ( add_filters ) { > var table_dt = table.DataTable(); >+ >+ $(this).find('thead tr').clone().appendTo( $(this).find('thead') ); >+ > $(this).find('thead tr:eq(1) th').each( function (i) { > var is_searchable = table_dt.settings()[0].aoColumns[i].bSearchable; >+ $(this).removeClass('sorting'); > if ( is_searchable ) { > var title = $(this).text(); > var existing_search = table_dt.column(i).search(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js b/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js >new file mode 100644 >index 00000000000..70b5c815ee2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/src/Cities.js >@@ -0,0 +1,161 @@ >+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 ( >+ <div className="App"> >+ {this.state.op == 'list' && <div id="toolbar" className="btn-toolbar"><Button id="newcity" variant="default" onClick={() => this.onCreate()}><FontAwesomeIcon icon={faPlus} /> New city</Button></div>} >+ {this.state.message && <div><Alert key="message_info" variant="info" transition={false}>{this.state.message}</Alert></div>} >+ {this.state.error && <div><Alert key="message_error" variant="warning" transition={false}>Error: {this.state.error.message}</Alert></div>} >+ {this.state.op == 'list' && <CityList editCity={this.editCity} confirmDeleteCity={this.confirmDeleteCity} cities={this.state.cities} />} >+ {this.state.op == 'add' && <AddCity onFormSubmit={this.addCity} onFormCancel={this.cancel} city={this.state.city} />} >+ {this.state.op == 'delete' && <DeleteCity onFormSubmit={this.deleteCity} onFormCancel={this.cancel} city={this.state.city} />} >+ </div> >+ ) >+ } >+} >+ >+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 >new file mode 100644 >index 00000000000..57205c9c031 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/src/cities/AddCity.js >@@ -0,0 +1,121 @@ >+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 = <h2>Edit city</h2> >+ } else { >+ pageTitle = <h2>New city</h2> >+ } >+ >+ return( >+ <div> >+ {pageTitle} >+ <Form onSubmit={this.handleSubmit} className="validated"> >+ <fieldset className="rows"> >+ <Form.Group controlId="name" as={Row}> >+ <Form.Label column sm={4} className="required">City:</Form.Label> >+ <Col sm={8}> >+ <Form.Control >+ type="text" >+ name="name" >+ value={this.state.name} >+ onChange={this.handleChange} >+ placeholder="City name" >+ required="required"/> >+ <span className="required">Required</span> >+ </Col> >+ </Form.Group> >+ <Form.Group controlId="state" as={Row}> >+ <Form.Label column sm={4}>State:</Form.Label> >+ <Col sm={8}> >+ <Form.Control >+ type="text" >+ name="state" >+ value={this.state.state} >+ onChange={this.handleChange} >+ placeholder="City State"/> >+ </Col> >+ </Form.Group> >+ <Form.Group controlId="postal_code" as={Row}> >+ <Form.Label column sm={4} className="required">ZIP/Postal code:</Form.Label> >+ <Col sm={8}> >+ <Form.Control >+ type="text" >+ name="postal_code" >+ value={this.state.postal_code} >+ onChange={this.handleChange} >+ placeholder="City postal code" >+ required="required"/> >+ <span className="required">Required</span> >+ </Col> >+ </Form.Group> >+ <Form.Group controlId="country" as={Row}> >+ <Form.Label column sm={4}>Country:</Form.Label> >+ <Col sm={8}> >+ <Form.Control >+ type="text" >+ name="country" >+ value={this.state.country} >+ onChange={this.handleChange} >+ placeholder="City country"/> >+ </Col> >+ </Form.Group> >+ </fieldset> >+ <fieldset className="action"> >+ <Form.Group> >+ <Form.Control type="hidden" name="id" value={this.state.city_id} /> >+ <Button variant="default" type="submit">Submit</Button> >+ <a href="#" onClick={(e) => this.handleCancel(e)}>Cancel</a> >+ </Form.Group> >+ </fieldset> >+ </Form> >+ </div> >+ ) >+ } >+} >+ >+export default AddCity; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js b/koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js >new file mode 100644 >index 00000000000..c68bab36533 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/src/cities/CityList.js >@@ -0,0 +1,176 @@ >+import React, { Component } 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 ( >+ <Button variant="default" size="sm" onClick={this.handleClick}><FontAwesomeIcon icon={faPencil} size="xs" /> Edit</Button> >+ ) >+ } >+} >+ >+class DeleteButton extends Component{ >+ constructor(props){ >+ super(props); >+ this.handleClick = this.handleClick.bind(this); >+ } >+ handleClick(){ >+ this.props.handleClick(this.props.city_id); >+ } >+ >+ render(){ >+ return ( >+ <Button variant="default" size="sm" onClick={this.handleClick}><FontAwesomeIcon icon={faTrash} size="xs" /> Delete</Button> >+ ) >+ } >+} >+ >+class Table extends Component { >+ constructor(props){ >+ super(props); >+ } >+ >+ componentDidMount() { >+ >+ const editCity = this.props.editCity; >+ const confirmDeleteCity = this.props.confirmDeleteCity; >+ $(this.refs.main).kohaTable({ >+ "ajax": { >+ "url": cities_table_url, >+ }, >+ "order": [[ 1, "asc" ]], >+ "columnDefs": [ { >+ "targets": [0,1,2,3,4], >+ "render": function (data, type, row, meta) { >+ if ( type == 'display' ) { >+ return escape_str(data); >+ } >+ return data; >+ } >+ } ], >+ "columns": [ >+ { >+ "title": __("City ID"), >+ "data": "city_id", >+ "searchable": true, >+ "orderable": true >+ }, >+ { >+ "title": __("City"), >+ "data": "name", >+ "searchable": true, >+ "orderable": true >+ }, >+ { >+ "title": __("State"), >+ "data": "state", >+ "searchable": true, >+ "orderable": true >+ }, >+ { >+ "title": __("ZIP/Postal code"), >+ "data": "postal_code", >+ "searchable": true, >+ "orderable": true >+ }, >+ { >+ "title": __("Country"), >+ "data": "country", >+ "searchable": true, >+ "orderable": true >+ }, >+ { >+ "title": __("Actions"), >+ "data": function( row, type, val, meta ) { >+ return '<div class="actions" data-city_id="'+row.city_id+'"></div>'; >+ }, >+ "searchable": false, >+ "orderable": false >+ } >+ ], >+ drawCallback: function(settings){ >+ $.each($(this).find(".actions"), function(index, e) { >+ ReactDOM.render(<span><EditButton city_id={$(e).data('city_id')} handleClick={editCity} /><DeleteButton city_id={$(e).data('city_id')} handleClick={confirmDeleteCity} /></span>, e); >+ }); >+ } >+ >+ }, columns_settings, 1); >+ } >+ >+ componentWillUnmount(){ >+ $('.data-table-wrapper') >+ .find('table') >+ .DataTable() >+ .destroy(true); >+ } >+ >+ shouldComponentUpdate() { >+ return true; >+ } >+ >+ render() { >+ return ( >+ <div> >+ <table ref="main" /> >+ </div>); >+ } >+} >+ >+class CityList extends React.Component { >+ constructor(props) { >+ super(props); >+ this.state = { >+ error: null, >+ cities: [], >+ } >+ >+ if ( props.cities ){ >+ this.state.cities = props.cities >+ } >+ } >+ >+ componentDidMount() { >+ const apiUrl = '/api/v1/cities'; >+ >+ fetch(apiUrl) >+ .then(res => res.json()) >+ .then( >+ (result) => { >+ this.setState({ >+ cities: result >+ }); >+ }, >+ (error) => { >+ this.setState({ error }); >+ } >+ ) >+ } >+ >+ render() { >+ const { error, cities } = this.state; >+ >+ if(error) { >+ return ("Error: {error.message}") >+ } else if(cities.length) { >+ return(<div> >+ <h1>Cities</h1> >+ <Table editCity={this.props.editCity} confirmDeleteCity={this.props.confirmDeleteCity} /> >+ </div>) >+ } else { >+ return(<div><h1>Cities</h1><div className="dialog message">There are no cities defined.</div></div>) >+ } >+ } >+} >+ >+export default CityList; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js b/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js >new file mode 100644 >index 00000000000..b2bb037eded >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/src/cities/DeleteCity.js >@@ -0,0 +1,67 @@ >+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 = <h2>Delete city</h2> >+ >+ return( >+ <div> >+ <Row> >+ {pageTitle} >+ <Form onSubmit={this.handleSubmit}> >+ <fieldset className="rows"> >+ <h3>Delete city "{this.state.name}"?</h3> >+ <Form.Group controlId="city_id" as={Row}> >+ <Form.Label column sm={4}>City id:</Form.Label> >+ <Col sm={8}>{this.state.city_id}</Col> >+ </Form.Group> >+ <Form.Group controlId="name" as={Row}> >+ <Form.Label column sm={4}>City:</Form.Label> >+ <Col sm={8}>{this.state.name}</Col> >+ </Form.Group> >+ <Form.Group controlId="state" as={Row}> >+ <Form.Label column sm={4}>State:</Form.Label> >+ <Col sm={8}>{this.state.state}</Col> >+ </Form.Group> >+ <Form.Group controlId="postal_code" as={Row}> >+ <Form.Label column sm={4}>ZIP/Postal code:</Form.Label> >+ <Col sm={8}>{this.state.postal_code}</Col> >+ </Form.Group> >+ <Form.Group controlId="country" as={Row}> >+ <Form.Label column sm={4}>Country:</Form.Label> >+ <Col sm={8}>{this.state.country}</Col> >+ </Form.Group> >+ </fieldset> >+ <fieldset className="action"> >+ <Button variant="default" className="approve" type="submit"><FontAwesomeIcon icon={faCheck} /> Yes, Delete</Button> >+ <Button variant="default" className="deny" onClick={(e) => this.handleCancel(e)}><FontAwesomeIcon icon={faRemove} /> No, do not remove</Button> >+ </fieldset> >+ </Form> >+ </Row> >+ </div> >+ ) >+ } >+} >+ >+export default DeleteCity; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/src/index.js b/koha-tmpl/intranet-tmpl/prog/js/src/index.js >new file mode 100644 >index 00000000000..06f48cac9a8 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/src/index.js >@@ -0,0 +1,5 @@ >+import React from 'react'; >+import ReactDOM from 'react-dom'; >+import App from './Cities'; >+ >+ReactDOM.render(<App />, document.getElementById('cities_container')); >diff --git a/package.json b/package.json >index ac7ff7bcd78..0dc659a0b70 100644 >--- a/package.json >+++ b/package.json >@@ -7,7 +7,18 @@ > "test": "test" > }, > "dependencies": { >+ "@babel/core": "^7.0.0-beta.3", >+ "@babel/preset-env": "^7.0.0-beta.3", >+ "@babel/preset-react": "^7.0.0-beta.3", >+ "@fortawesome/fontawesome-svg-core": "^1.3.0", >+ "@fortawesome/free-regular-svg-icons": "^6.0.0", >+ "@fortawesome/free-solid-svg-icons": "^6.0.0", >+ "@fortawesome/react-fontawesome": "^0.1.17", >+ "@types/react-dom": "^17.0.11", >+ "babel-core": "^7.0.0-beta.3", >+ "babelify": "^10.0.0", > "bootstrap": "^4.5.2", >+ "browserify": "^17.0.0", > "gulp": "^4.0.2", > "gulp-autoprefixer": "^4.0.0", > "gulp-concat-po": "^1.0.0", >@@ -17,10 +28,17 @@ > "gulp-rtlcss": "^1.4.1", > "gulp-sass": "^3.1.0", > "gulp-sourcemaps": "^2.6.1", >+ "gulp-tap": "^1.0.1", > "js-yaml": "^3.13.1", > "lodash": "^4.17.12", > "merge-stream": "^2.0.0", >- "minimist": "^1.2.5" >+ "minimist": "^1.2.5", >+ "react": "^17.0.2", >+ "react-bootstrap": "^2.1.2", >+ "react-dom": "^17.0.2", >+ "react-router": "^6.2.1", >+ "react-router-dom": "^6.2.1", >+ "react-script": "^2.0.5" > }, > "scripts": { > "build": "node_modules/.bin/gulp build", >@@ -37,5 +55,8 @@ > "js-yaml": "^3.13.1" > }, > "author": "", >- "license": "GPL-3.0" >+ "license": "GPL-3.0", >+ "devDependencies": { >+ "@types/react": "^17.0.39" >+ } > } >-- >2.25.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 30160
: 131031 |
131169