mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-02-06 06:56:54 +08:00
Merge pull request #740 from okriuchykhin/ok_SCI_1461
Add Webpacker and React [SCI-1461]
This commit is contained in:
commit
d671ad50b5
27 changed files with 5869 additions and 5 deletions
31
.babelrc
Normal file
31
.babelrc
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": "> 1%",
|
||||
"uglify": true
|
||||
},
|
||||
"useBuiltIns": true
|
||||
}
|
||||
],
|
||||
"react",
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-dynamic-import",
|
||||
[
|
||||
"transform-class-properties",
|
||||
{
|
||||
"spec": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["transform-es2015-modules-commonjs"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,21 @@
|
|||
{
|
||||
"extends": "google",
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jquery": true
|
||||
"jquery": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 6,
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"spaced-comment": [
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -53,3 +53,7 @@ rubocop_cache
|
|||
|
||||
# Ignore ESLint local instalation
|
||||
node_modules
|
||||
|
||||
# Added during webpacker install
|
||||
/public/packs
|
||||
/node_modules
|
||||
|
|
4
.postcssrc.yml
Normal file
4
.postcssrc.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
plugins:
|
||||
postcss-smart-import: {}
|
||||
precss: {}
|
||||
autoprefixer: {}
|
|
@ -2,7 +2,8 @@ FROM ruby:2.4.1
|
|||
MAINTAINER BioSistemika <info@biosistemika.com>
|
||||
|
||||
# additional dependecies
|
||||
RUN apt-get update -qq && \
|
||||
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
nodejs \
|
||||
postgresql-client \
|
||||
|
@ -10,6 +11,7 @@ RUN apt-get update -qq && \
|
|||
unison \
|
||||
sudo graphviz --no-install-recommends \
|
||||
libfile-mimeinfo-perl && \
|
||||
npm install -g yarn && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# heroku tools
|
||||
|
|
|
@ -2,7 +2,8 @@ FROM ruby:2.4.1
|
|||
MAINTAINER BioSistemika <info@biosistemika.com>
|
||||
|
||||
# additional dependecies
|
||||
RUN apt-get update -qq && \
|
||||
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
nodejs \
|
||||
postgresql-client \
|
||||
|
@ -10,6 +11,7 @@ RUN apt-get update -qq && \
|
|||
default-jre-headless \
|
||||
sudo graphviz --no-install-recommends \
|
||||
libfile-mimeinfo-perl && \
|
||||
npm install -g yarn && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV RAILS_ENV production
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -3,6 +3,7 @@ source 'http://rubygems.org'
|
|||
ruby '2.4.1'
|
||||
|
||||
gem 'rails', '5.1.1'
|
||||
gem 'webpacker', '~> 2.0'
|
||||
gem 'figaro'
|
||||
gem 'pg'
|
||||
gem 'devise', '~> 4.3.0'
|
||||
|
|
|
@ -360,6 +360,10 @@ GEM
|
|||
unicode-display_width (1.3.0)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webpacker (2.0)
|
||||
activesupport (>= 4.2)
|
||||
multi_json (~> 1.2)
|
||||
railties (>= 4.2)
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
|
@ -445,6 +449,7 @@ DEPENDENCIES
|
|||
tzinfo-data
|
||||
uglifier (>= 1.3.0)
|
||||
underscore-rails
|
||||
webpacker (~> 2.0)
|
||||
wicked_pdf
|
||||
wkhtmltopdf-heroku
|
||||
yomu
|
||||
|
|
10
app/javascript/packs/application.js
Normal file
10
app/javascript/packs/application.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint no-console:0 */
|
||||
// This file is automatically compiled by Webpack, along with any other files
|
||||
// present in this directory. You're encouraged to place your actual application logic in
|
||||
// a relevant structure within app/javascript and only use these pack files to reference
|
||||
// that code so it'll be compiled.
|
||||
//
|
||||
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
|
||||
// layout file, like app/views/layouts/application.html.erb
|
||||
|
||||
console.log('Hello World from Webpacker')
|
26
app/javascript/packs/hello_react.jsx
Normal file
26
app/javascript/packs/hello_react.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
|
||||
// like app/views/layouts/application.html.erb. All it does is render <div>Hello React</div> at the bottom
|
||||
// of the page.
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const Hello = props => (
|
||||
<div>Hello {props.name}!</div>
|
||||
)
|
||||
|
||||
Hello.defaultProps = {
|
||||
name: 'David'
|
||||
}
|
||||
|
||||
Hello.propTypes = {
|
||||
name: PropTypes.string
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ReactDOM.render(
|
||||
<Hello name="React" />,
|
||||
document.body.appendChild(document.createElement('div'))
|
||||
)
|
||||
})
|
28
bin/webpack
Executable file
28
bin/webpack
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env ruby
|
||||
$stdout.sync = true
|
||||
|
||||
require "shellwords"
|
||||
require "yaml"
|
||||
|
||||
ENV["RAILS_ENV"] ||= "development"
|
||||
RAILS_ENV = ENV["RAILS_ENV"]
|
||||
|
||||
ENV["NODE_ENV"] ||= RAILS_ENV
|
||||
NODE_ENV = ENV["NODE_ENV"]
|
||||
|
||||
APP_PATH = File.expand_path("../", __dir__)
|
||||
NODE_MODULES_PATH = File.join(APP_PATH, "node_modules")
|
||||
WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/#{NODE_ENV}.js")
|
||||
|
||||
unless File.exist?(WEBPACK_CONFIG)
|
||||
puts "Webpack configuration not found."
|
||||
puts "Please run bundle exec rails webpacker:install to install webpacker"
|
||||
exit!
|
||||
end
|
||||
|
||||
newenv = { "NODE_PATH" => NODE_MODULES_PATH.shellescape }
|
||||
cmdline = ["yarn", "run", "webpack", "--", "--config", WEBPACK_CONFIG] + ARGV
|
||||
|
||||
Dir.chdir(APP_PATH) do
|
||||
exec newenv, *cmdline
|
||||
end
|
43
bin/webpack-dev-server
Executable file
43
bin/webpack-dev-server
Executable file
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env ruby
|
||||
$stdout.sync = true
|
||||
|
||||
require "shellwords"
|
||||
require "yaml"
|
||||
|
||||
ENV["RAILS_ENV"] ||= "development"
|
||||
RAILS_ENV = ENV["RAILS_ENV"]
|
||||
|
||||
ENV["NODE_ENV"] ||= RAILS_ENV
|
||||
NODE_ENV = ENV["NODE_ENV"]
|
||||
|
||||
APP_PATH = File.expand_path("../", __dir__)
|
||||
CONFIG_FILE = File.join(APP_PATH, "config/webpacker.yml")
|
||||
NODE_MODULES_PATH = File.join(APP_PATH, "node_modules")
|
||||
WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/development.js")
|
||||
|
||||
def args(key)
|
||||
index = ARGV.index(key)
|
||||
index ? ARGV[index + 1] : nil
|
||||
end
|
||||
|
||||
begin
|
||||
dev_server = YAML.load_file(CONFIG_FILE)["development"]["dev_server"]
|
||||
|
||||
DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{args('--host') || dev_server["host"]}:#{args('--port') || dev_server["port"]}"
|
||||
|
||||
rescue Errno::ENOENT, NoMethodError
|
||||
puts "Webpack dev_server configuration not found in #{CONFIG_FILE}."
|
||||
puts "Please run bundle exec rails webpacker:install to install webpacker"
|
||||
exit!
|
||||
end
|
||||
|
||||
newenv = {
|
||||
"NODE_PATH" => NODE_MODULES_PATH.shellescape,
|
||||
"ASSET_HOST" => DEV_SERVER_HOST.shellescape
|
||||
}.freeze
|
||||
|
||||
cmdline = ["yarn", "run", "webpack-dev-server", "--", "--progress", "--color", "--config", WEBPACK_CONFIG] + ARGV
|
||||
|
||||
Dir.chdir(APP_PATH) do
|
||||
exec newenv, *cmdline
|
||||
end
|
35
config/webpack/configuration.js
Normal file
35
config/webpack/configuration.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Common configuration for webpacker loaded from config/webpacker.yml
|
||||
|
||||
const { join, resolve } = require('path')
|
||||
const { env } = require('process')
|
||||
const { safeLoad } = require('js-yaml')
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const configPath = resolve('config', 'webpacker.yml')
|
||||
const loadersDir = join(__dirname, 'loaders')
|
||||
const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]
|
||||
|
||||
function removeOuterSlashes(string) {
|
||||
return string.replace(/^\/*/, '').replace(/\/*$/, '')
|
||||
}
|
||||
|
||||
function formatPublicPath(host = '', path = '') {
|
||||
let formattedHost = removeOuterSlashes(host)
|
||||
if (formattedHost && !/^http/i.test(formattedHost)) {
|
||||
formattedHost = `//${formattedHost}`
|
||||
}
|
||||
const formattedPath = removeOuterSlashes(path)
|
||||
return `${formattedHost}/${formattedPath}/`
|
||||
}
|
||||
|
||||
const output = {
|
||||
path: resolve('public', settings.public_output_path),
|
||||
publicPath: formatPublicPath(env.ASSET_HOST, settings.public_output_path)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
settings,
|
||||
env,
|
||||
loadersDir,
|
||||
output
|
||||
}
|
32
config/webpack/development.js
Normal file
32
config/webpack/development.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const merge = require('webpack-merge')
|
||||
const sharedConfig = require('./shared.js')
|
||||
const { settings, output } = require('./configuration.js')
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
devtool: 'cheap-eval-source-map',
|
||||
|
||||
stats: {
|
||||
errorDetails: true
|
||||
},
|
||||
|
||||
output: {
|
||||
pathinfo: true
|
||||
},
|
||||
|
||||
devServer: {
|
||||
clientLogLevel: 'none',
|
||||
https: settings.dev_server.https,
|
||||
host: settings.dev_server.host,
|
||||
port: settings.dev_server.port,
|
||||
contentBase: output.path,
|
||||
publicPath: output.publicPath,
|
||||
compress: true,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
historyApiFallback: true,
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
}
|
||||
}
|
||||
})
|
12
config/webpack/loaders/assets.js
Normal file
12
config/webpack/loaders/assets.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const { env, publicPath } = require('../configuration.js')
|
||||
|
||||
module.exports = {
|
||||
test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
publicPath,
|
||||
name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]'
|
||||
}
|
||||
}]
|
||||
}
|
5
config/webpack/loaders/babel.js
Normal file
5
config/webpack/loaders/babel.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
test: /\.js(\.erb)?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
}
|
4
config/webpack/loaders/coffee.js
Normal file
4
config/webpack/loaders/coffee.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
test: /\.coffee(\.erb)?$/,
|
||||
loader: 'coffee-loader'
|
||||
}
|
9
config/webpack/loaders/erb.js
Normal file
9
config/webpack/loaders/erb.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
test: /\.erb$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
loader: 'rails-erb-loader',
|
||||
options: {
|
||||
runner: 'bin/rails runner'
|
||||
}
|
||||
}
|
5
config/webpack/loaders/react.js
vendored
Normal file
5
config/webpack/loaders/react.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
test: /\.(js|jsx)?(\.erb)?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
}
|
15
config/webpack/loaders/sass.js
Normal file
15
config/webpack/loaders/sass.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const { env } = require('../configuration.js')
|
||||
|
||||
module.exports = {
|
||||
test: /\.(scss|sass|css)$/i,
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [
|
||||
{ loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } },
|
||||
{ loader: 'postcss-loader', options: { sourceMap: true } },
|
||||
'resolve-url-loader',
|
||||
{ loader: 'sass-loader', options: { sourceMap: true } }
|
||||
]
|
||||
})
|
||||
}
|
35
config/webpack/production.js
Normal file
35
config/webpack/production.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
/* eslint global-require: 0 */
|
||||
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const CompressionPlugin = require('compression-webpack-plugin')
|
||||
const sharedConfig = require('./shared.js')
|
||||
|
||||
module.exports = merge(sharedConfig, {
|
||||
output: { filename: '[name]-[chunkhash].js' },
|
||||
devtool: 'source-map',
|
||||
stats: 'normal',
|
||||
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
minimize: true,
|
||||
sourceMap: true,
|
||||
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
|
||||
output: {
|
||||
comments: false
|
||||
}
|
||||
}),
|
||||
|
||||
new CompressionPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/
|
||||
})
|
||||
]
|
||||
})
|
58
config/webpack/shared.js
Normal file
58
config/webpack/shared.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
/* eslint global-require: 0 */
|
||||
/* eslint import/no-dynamic-require: 0 */
|
||||
|
||||
const webpack = require('webpack')
|
||||
const { basename, dirname, join, relative, resolve } = require('path')
|
||||
const { sync } = require('glob')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const ManifestPlugin = require('webpack-manifest-plugin')
|
||||
const extname = require('path-complete-extname')
|
||||
const { env, settings, output, loadersDir } = require('./configuration.js')
|
||||
|
||||
const extensionGlob = `**/*{${settings.extensions.join(',')}}*`
|
||||
const entryPath = join(settings.source_path, settings.source_entry_path)
|
||||
const packPaths = sync(join(entryPath, extensionGlob))
|
||||
|
||||
module.exports = {
|
||||
entry: packPaths.reduce(
|
||||
(map, entry) => {
|
||||
const localMap = map
|
||||
const namespace = relative(join(entryPath), dirname(entry))
|
||||
localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
|
||||
return localMap
|
||||
}, {}
|
||||
),
|
||||
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: output.path,
|
||||
publicPath: output.publicPath
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: sync(join(loadersDir, '*.js')).map(loader => require(loader))
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
||||
new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),
|
||||
new ManifestPlugin({
|
||||
publicPath: output.publicPath,
|
||||
writeToFileEmit: true
|
||||
})
|
||||
],
|
||||
|
||||
resolve: {
|
||||
extensions: settings.extensions,
|
||||
modules: [
|
||||
resolve(settings.source_path),
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
|
||||
resolveLoader: {
|
||||
modules: ['node_modules']
|
||||
}
|
||||
}
|
6
config/webpack/test.js
Normal file
6
config/webpack/test.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
const merge = require('webpack-merge')
|
||||
const sharedConfig = require('./shared.js')
|
||||
|
||||
module.exports = merge(sharedConfig, {})
|
38
config/webpacker.yml
Normal file
38
config/webpacker.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
default: &default
|
||||
source_path: app/javascript
|
||||
source_entry_path: packs
|
||||
public_output_path: packs
|
||||
|
||||
extensions:
|
||||
- .coffee
|
||||
- .erb
|
||||
- .js
|
||||
- .jsx
|
||||
- .ts
|
||||
- .vue
|
||||
- .sass
|
||||
- .scss
|
||||
- .css
|
||||
- .png
|
||||
- .svg
|
||||
- .gif
|
||||
- .jpeg
|
||||
- .jpg
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
dev_server:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
https: false
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
|
||||
public_output_path: packs-test
|
||||
|
||||
production:
|
||||
<<: *default
|
|
@ -29,6 +29,23 @@ services:
|
|||
- scinote_development_bundler:/usr/local/bundle/
|
||||
- scinote_development_files:/usr/src/app/public/system
|
||||
|
||||
webpack:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: scinote_web_development
|
||||
container_name: scinote_webpack_development
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: >
|
||||
bash -c "./bin/webpack-dev-server"
|
||||
environment:
|
||||
- RAILS_ENV=development
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
|
||||
volumes:
|
||||
scinote_development_postgres:
|
||||
scinote_development_bundler:
|
||||
|
|
37
package.json
37
package.json
|
@ -21,6 +21,41 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^3.7.1",
|
||||
"eslint-config-google": "^0.5.0"
|
||||
"eslint-config-google": "^0.5.0",
|
||||
"eslint-plugin-react": "^7.1.0",
|
||||
"webpack-dev-server": "^2.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^7.1.2",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-loader": "7.x",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"coffee-loader": "^0.7.3",
|
||||
"coffee-script": "^1.12.6",
|
||||
"compression-webpack-plugin": "^0.4.0",
|
||||
"css-loader": "^0.28.4",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"file-loader": "^0.11.2",
|
||||
"glob": "^7.1.2",
|
||||
"js-yaml": "^3.9.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"path-complete-extname": "^0.1.0",
|
||||
"postcss-loader": "^2.0.6",
|
||||
"postcss-smart-import": "^0.7.5",
|
||||
"precss": "^2.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"rails-erb-loader": "^5.0.2",
|
||||
"react": "^15.6.1",
|
||||
"react-dom": "^15.6.1",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"webpack": "^3.2.0",
|
||||
"webpack-manifest-plugin": "^1.1.2",
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue