commit
2e90b7512d
58 changed files with 2612 additions and 1323 deletions
54
.babelrc
54
.babelrc
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@babel/env",
|
|
||||||
{
|
|
||||||
"modules": false,
|
|
||||||
"targets": {
|
|
||||||
"browsers": [
|
|
||||||
"> 1%",
|
|
||||||
"Chrome >= 50",
|
|
||||||
"IE >= 11",
|
|
||||||
"Edge >= 14",
|
|
||||||
"Firefox >= 50",
|
|
||||||
"Opera >= 40",
|
|
||||||
"Safari >= 8",
|
|
||||||
"iOS >= 8"
|
|
||||||
],
|
|
||||||
"uglify": true
|
|
||||||
},
|
|
||||||
"forceAllTransforms": true,
|
|
||||||
"useBuiltIns": "entry"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-transform-destructuring",
|
|
||||||
"@babel/plugin-syntax-dynamic-import",
|
|
||||||
[
|
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
{
|
|
||||||
"useBuiltIns": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{
|
|
||||||
"helpers": false,
|
|
||||||
"regenerator": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-regenerator",
|
|
||||||
{
|
|
||||||
"async": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
{
|
|
||||||
"loose": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
8
.browserslistrc
Normal file
8
.browserslistrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
> 1%
|
||||||
|
Chrome >= 50
|
||||||
|
IE >= 11
|
||||||
|
Edge >= 14
|
||||||
|
Firefox >= 50
|
||||||
|
Opera >= 40
|
||||||
|
Safari >= 8
|
||||||
|
iOS >= 8
|
15
.eslintrc.js
15
.eslintrc.js
|
@ -1,5 +1,6 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
parser: 'babel-eslint',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2017,
|
ecmaVersion: 2017,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
|
@ -8,18 +9,24 @@ module.exports = {
|
||||||
'process': true,
|
'process': true,
|
||||||
'gon': true
|
'gon': true
|
||||||
},
|
},
|
||||||
plugins: ['prettier'],
|
plugins: ['prettier', 'react-hooks'],
|
||||||
extends: ['eslint:recommended', 'prettier'],
|
extends: ['eslint:recommended', 'prettier', 'plugin:react/recommended'],
|
||||||
env: {
|
env: {
|
||||||
es6: true,
|
es6: true,
|
||||||
browser: true
|
browser: true
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'prettier/prettier': 'error'
|
'prettier/prettier': 'error',
|
||||||
|
'react-hooks/rules-of-hooks': 'error'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['config/webpack/**/*.js'],
|
files: ['config/webpack/**/*.js', 'babel.config.js', 'postcss.config.js'],
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true
|
||||||
}
|
}
|
||||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -26,3 +26,9 @@ storage/
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
/.vscode
|
/.vscode
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
||||||
|
yarn-debug.log*
|
||||||
|
.yarn-integrity
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
plugins:
|
|
||||||
postcss-import: {}
|
|
||||||
postcss-cssnext: {}
|
|
3
Gemfile
3
Gemfile
|
@ -49,6 +49,7 @@ gem 'rack-mini-profiler'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails-i18n' # Locales par défaut
|
gem 'rails-i18n' # Locales par défaut
|
||||||
gem 'rake-progressbar', require: false
|
gem 'rake-progressbar', require: false
|
||||||
|
gem 'react-rails'
|
||||||
gem 'rest-client'
|
gem 'rest-client'
|
||||||
gem 'rgeo-geojson'
|
gem 'rgeo-geojson'
|
||||||
gem 'sanitize-url'
|
gem 'sanitize-url'
|
||||||
|
@ -63,7 +64,7 @@ gem 'spreadsheet_architect'
|
||||||
gem 'turbolinks' # Turbolinks makes following links in your web application faster
|
gem 'turbolinks' # Turbolinks makes following links in your web application faster
|
||||||
gem 'typhoeus'
|
gem 'typhoeus'
|
||||||
gem 'warden'
|
gem 'warden'
|
||||||
gem 'webpacker', '>= 4.0.x'
|
gem 'webpacker'
|
||||||
gem 'zxcvbn-ruby', require: 'zxcvbn'
|
gem 'zxcvbn-ruby', require: 'zxcvbn'
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -99,6 +99,10 @@ GEM
|
||||||
axlsx_styler (0.2.0)
|
axlsx_styler (0.2.0)
|
||||||
activesupport (>= 3.1)
|
activesupport (>= 3.1)
|
||||||
axlsx (>= 2.0, < 4)
|
axlsx (>= 2.0, < 4)
|
||||||
|
babel-source (5.8.35)
|
||||||
|
babel-transpiler (0.7.0)
|
||||||
|
babel-source (>= 4.0, < 6)
|
||||||
|
execjs (~> 2.0)
|
||||||
bcrypt (3.1.12)
|
bcrypt (3.1.12)
|
||||||
bindata (2.4.4)
|
bindata (2.4.4)
|
||||||
bindex (0.5.0)
|
bindex (0.5.0)
|
||||||
|
@ -149,6 +153,7 @@ GEM
|
||||||
execjs
|
execjs
|
||||||
coffee-script-source (1.12.2)
|
coffee-script-source (1.12.2)
|
||||||
concurrent-ruby (1.1.5)
|
concurrent-ruby (1.1.5)
|
||||||
|
connection_pool (2.2.2)
|
||||||
copy_carrierwave_file (1.3.0)
|
copy_carrierwave_file (1.3.0)
|
||||||
carrierwave (>= 0.9)
|
carrierwave (>= 0.9)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
|
@ -278,7 +283,7 @@ GEM
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.5.3)
|
i18n (1.6.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
jaro_winkler (1.5.2)
|
jaro_winkler (1.5.2)
|
||||||
|
@ -465,6 +470,12 @@ GEM
|
||||||
rb-fsevent (0.10.3)
|
rb-fsevent (0.10.3)
|
||||||
rb-inotify (0.10.0)
|
rb-inotify (0.10.0)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
|
react-rails (2.4.7)
|
||||||
|
babel-transpiler (>= 0.7.0)
|
||||||
|
connection_pool
|
||||||
|
execjs
|
||||||
|
railties (>= 3.2)
|
||||||
|
tilt
|
||||||
regexp_parser (1.3.0)
|
regexp_parser (1.3.0)
|
||||||
request_store (1.4.1)
|
request_store (1.4.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
|
@ -637,7 +648,7 @@ GEM
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff
|
hashdiff
|
||||||
webpacker (4.0.0.rc.2)
|
webpacker (4.0.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
|
@ -720,6 +731,7 @@ DEPENDENCIES
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rake-progressbar
|
rake-progressbar
|
||||||
|
react-rails
|
||||||
rest-client
|
rest-client
|
||||||
rgeo-geojson
|
rgeo-geojson
|
||||||
rspec-rails
|
rspec-rails
|
||||||
|
@ -747,7 +759,7 @@ DEPENDENCIES
|
||||||
warden
|
warden
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
webpacker (>= 4.0.x)
|
webpacker
|
||||||
xray-rails
|
xray-rails
|
||||||
zxcvbn-ruby
|
zxcvbn-ruby
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
@import "colors";
|
@import "colors";
|
||||||
@import "constants";
|
@import "constants";
|
||||||
|
|
||||||
#champs-editor {
|
.type-de-champ {
|
||||||
.spinner {
|
background-color: $white;
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-top: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggable-item {
|
|
||||||
border: 1px solid $border-grey;
|
border: 1px solid $border-grey;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -18,24 +11,11 @@
|
||||||
.handle {
|
.handle {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.move {
|
||||||
text-align: center;
|
margin-right: 10px;
|
||||||
flex-grow: 1;
|
margin-bottom: 5px;
|
||||||
font-size: 14px;
|
|
||||||
color: $light-grey;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background-color: $medium-red;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type-header-section {
|
&.type-header-section {
|
||||||
|
@ -46,12 +26,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.type-header-section) {
|
|
||||||
input.error {
|
|
||||||
border: 1px solid $medium-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
&.section {
|
&.section {
|
||||||
padding: 10px 10px 0 10px;
|
padding: 10px 10px 0 10px;
|
||||||
|
@ -67,7 +41,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.shift-left {
|
&.shift-left {
|
||||||
margin-left: 35px;
|
margin-left: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.head {
|
&.head {
|
||||||
|
@ -112,8 +86,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.champs-editor {
|
||||||
.footer {
|
.footer {
|
||||||
margin-bottom: 70px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
|
@ -130,3 +105,4 @@
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -175,6 +175,7 @@ class ApplicationController < ActionController::Base
|
||||||
if gestionnaire_signed_in? &&
|
if gestionnaire_signed_in? &&
|
||||||
sensitive_path &&
|
sensitive_path &&
|
||||||
Flipflop.enable_email_login_token? &&
|
Flipflop.enable_email_login_token? &&
|
||||||
|
!IPService.ip_trusted?(request.headers['X-Forwarded-For']) &&
|
||||||
!trusted_device?
|
!trusted_device?
|
||||||
|
|
||||||
# return at this location
|
# return at this location
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module NewAdministrateur
|
module NewAdministrateur
|
||||||
class TypesDeChampController < AdministrateurController
|
class TypesDeChampController < AdministrateurController
|
||||||
before_action :retrieve_procedure, only: [:create, :update, :destroy]
|
before_action :retrieve_procedure, only: [:create, :update, :move, :destroy]
|
||||||
before_action :procedure_locked?, only: [:create, :update, :destroy]
|
before_action :procedure_locked?, only: [:create, :update, :move, :destroy]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
type_de_champ = TypeDeChamp.new(type_de_champ_create_params)
|
type_de_champ = TypeDeChamp.new(type_de_champ_create_params)
|
||||||
|
@ -25,6 +25,15 @@ module NewAdministrateur
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def move
|
||||||
|
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
|
||||||
|
new_index = params[:order_place].to_i
|
||||||
|
|
||||||
|
@procedure.move_type_de_champ(type_de_champ, new_index)
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
|
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
|
||||||
|
|
||||||
|
@ -39,38 +48,54 @@ module NewAdministrateur
|
||||||
def serialize_type_de_champ(type_de_champ)
|
def serialize_type_de_champ(type_de_champ)
|
||||||
{
|
{
|
||||||
type_de_champ: type_de_champ.as_json(
|
type_de_champ: type_de_champ.as_json(
|
||||||
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
|
except: [
|
||||||
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
|
:created_at,
|
||||||
|
:options,
|
||||||
|
:order_place,
|
||||||
|
:parent_id,
|
||||||
|
:private,
|
||||||
|
:procedure_id,
|
||||||
|
:stable_id,
|
||||||
|
:type,
|
||||||
|
:updated_at
|
||||||
|
],
|
||||||
|
methods: [
|
||||||
|
:cadastres,
|
||||||
|
:drop_down_list_value,
|
||||||
|
:parcelles_agricoles,
|
||||||
|
:piece_justificative_template_filename,
|
||||||
|
:piece_justificative_template_url,
|
||||||
|
:quartiers_prioritaires
|
||||||
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def type_de_champ_create_params
|
def type_de_champ_create_params
|
||||||
params.required(:type_de_champ).permit(:libelle,
|
params.required(:type_de_champ).permit(:cadastres,
|
||||||
:description,
|
:description,
|
||||||
:order_place,
|
:drop_down_list_value,
|
||||||
:type_champ,
|
:libelle,
|
||||||
:private,
|
|
||||||
:parent_id,
|
|
||||||
:mandatory,
|
:mandatory,
|
||||||
:piece_justificative_template,
|
:order_place,
|
||||||
:quartiers_prioritaires,
|
|
||||||
:cadastres,
|
|
||||||
:parcelles_agricoles,
|
:parcelles_agricoles,
|
||||||
:drop_down_list_value).merge(procedure: @procedure)
|
:parent_id,
|
||||||
|
:piece_justificative_template,
|
||||||
|
:private,
|
||||||
|
:quartiers_prioritaires,
|
||||||
|
:type_champ).merge(procedure: @procedure)
|
||||||
end
|
end
|
||||||
|
|
||||||
def type_de_champ_update_params
|
def type_de_champ_update_params
|
||||||
params.required(:type_de_champ).permit(:libelle,
|
params.required(:type_de_champ).permit(:cadastres,
|
||||||
:description,
|
:description,
|
||||||
:order_place,
|
:drop_down_list_value,
|
||||||
:type_champ,
|
:libelle,
|
||||||
:mandatory,
|
:mandatory,
|
||||||
|
:parcelles_agricoles,
|
||||||
:piece_justificative_template,
|
:piece_justificative_template,
|
||||||
:quartiers_prioritaires,
|
:quartiers_prioritaires,
|
||||||
:cadastres,
|
:type_champ)
|
||||||
:parcelles_agricoles,
|
|
||||||
:drop_down_list_value)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,23 +36,21 @@ module ProcedureHelper
|
||||||
|
|
||||||
def types_de_champ_data(procedure)
|
def types_de_champ_data(procedure)
|
||||||
{
|
{
|
||||||
type: "champ",
|
isAnnotation: false,
|
||||||
types_de_champ_options: types_de_champ_options.to_json,
|
typeDeChampsTypes: types_de_champ_types,
|
||||||
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json,
|
typeDeChamps: types_de_champ_as_json(procedure.types_de_champ),
|
||||||
save_url: procedure_types_de_champ_path(procedure),
|
baseUrl: procedure_types_de_champ_path(procedure),
|
||||||
direct_upload_url: rails_direct_uploads_url,
|
directUploadUrl: rails_direct_uploads_url
|
||||||
drag_icon_url: image_url("icons/drag.svg")
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def types_de_champ_private_data(procedure)
|
def types_de_champ_private_data(procedure)
|
||||||
{
|
{
|
||||||
type: "annotation",
|
isAnnotation: true,
|
||||||
types_de_champ_options: types_de_champ_options.to_json,
|
typeDeChampsTypes: types_de_champ_types,
|
||||||
types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json,
|
typeDeChamps: types_de_champ_as_json(procedure.types_de_champ_private),
|
||||||
save_url: procedure_types_de_champ_path(procedure),
|
baseUrl: procedure_types_de_champ_path(procedure),
|
||||||
direct_upload_url: rails_direct_uploads_url,
|
directUploadUrl: rails_direct_uploads_url
|
||||||
drag_icon_url: image_url("icons/drag.svg")
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -63,20 +61,37 @@ module ProcedureHelper
|
||||||
TypeDeChamp.type_champs.fetch(:repetition) => :champ_repetition?
|
TypeDeChamp.type_champs.fetch(:repetition) => :champ_repetition?
|
||||||
}
|
}
|
||||||
|
|
||||||
def types_de_champ_options
|
def types_de_champ_types
|
||||||
types_de_champ = TypeDeChamp.type_de_champs_list_fr
|
types_de_champ_types = TypeDeChamp.type_de_champs_list_fr
|
||||||
|
|
||||||
types_de_champ.select! do |tdc|
|
types_de_champ_types.select! do |tdc|
|
||||||
toggle = TOGGLES[tdc.last]
|
toggle = TOGGLES[tdc.last]
|
||||||
toggle.blank? || Flipflop.send(toggle)
|
toggle.blank? || Flipflop.send(toggle)
|
||||||
end
|
end
|
||||||
|
|
||||||
types_de_champ
|
types_de_champ_types
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPES_DE_CHAMP_BASE = {
|
TYPES_DE_CHAMP_BASE = {
|
||||||
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
|
except: [
|
||||||
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
|
:created_at,
|
||||||
|
:options,
|
||||||
|
:order_place,
|
||||||
|
:parent_id,
|
||||||
|
:private,
|
||||||
|
:procedure_id,
|
||||||
|
:stable_id,
|
||||||
|
:type,
|
||||||
|
:updated_at
|
||||||
|
],
|
||||||
|
methods: [
|
||||||
|
:cadastres,
|
||||||
|
:drop_down_list_value,
|
||||||
|
:parcelles_agricoles,
|
||||||
|
:piece_justificative_template_filename,
|
||||||
|
:piece_justificative_template_url,
|
||||||
|
:quartiers_prioritaires
|
||||||
|
]
|
||||||
}
|
}
|
||||||
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
|
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
|
||||||
.merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE })
|
.merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE })
|
||||||
|
|
35
app/javascript/components/TypesDeChampEditor/Flash.js
Normal file
35
app/javascript/components/TypesDeChampEditor/Flash.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export default class Flash {
|
||||||
|
constructor(isAnnotation) {
|
||||||
|
this.element = document.querySelector('#flash_messages');
|
||||||
|
this.isAnnotation = isAnnotation;
|
||||||
|
}
|
||||||
|
success() {
|
||||||
|
if (this.isAnnotation) {
|
||||||
|
this.add('Annotations privées enregistrées.');
|
||||||
|
} else {
|
||||||
|
this.add('Formulaire enregistré.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error(message) {
|
||||||
|
this.add(message, true);
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.element.innerHTML = '';
|
||||||
|
}
|
||||||
|
add(message, isError) {
|
||||||
|
const html = `<div id="flash_message" class="center">
|
||||||
|
<div class="alert alert-fixed ${
|
||||||
|
isError ? 'alert-danger' : 'alert-success'
|
||||||
|
}">
|
||||||
|
${message}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
this.element.innerHTML = html;
|
||||||
|
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.clear();
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { to, getJSON } from '@utils';
|
||||||
|
|
||||||
|
export default class OperationsQueue {
|
||||||
|
constructor(baseUrl) {
|
||||||
|
this.queue = [];
|
||||||
|
this.isRunning = false;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
this.isRunning = true;
|
||||||
|
const operation = this.queue.shift();
|
||||||
|
await this.exec(operation);
|
||||||
|
this.run();
|
||||||
|
} else {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue(operation) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.queue.push({ ...operation, resolve, reject });
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(operation) {
|
||||||
|
const { path, method, payload, resolve, reject } = operation;
|
||||||
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
const [data, xhr] = await to(getJSON(url, payload, method));
|
||||||
|
|
||||||
|
if (xhr) {
|
||||||
|
handleError(xhr, reject);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(xhr, reject) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
errors: [message]
|
||||||
|
} = JSON.parse(xhr.responseText);
|
||||||
|
reject(message);
|
||||||
|
} catch (e) {
|
||||||
|
reject(xhr.responseText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function DescriptionInput({ isVisible, handler }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<label htmlFor={handler.id}>Description</label>
|
||||||
|
<textarea
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
value={handler.value || ''}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
rows={3}
|
||||||
|
cols={40}
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DescriptionInput.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
handler: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DescriptionInput;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function LibelleInput({ isVisible, handler }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="cell libelle">
|
||||||
|
<label htmlFor={handler.id}>Libellé</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
value={handler.value}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LibelleInput.propTypes = {
|
||||||
|
handler: PropTypes.object,
|
||||||
|
isVisible: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LibelleInput;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function MandatoryInput({ isVisible, handler }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<label htmlFor={handler.id}>Obligatoire</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
checked={!!handler.value}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MandatoryInput.propTypes = {
|
||||||
|
handler: PropTypes.object,
|
||||||
|
isVisible: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MandatoryInput;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
function MoveButton({ isVisible, icon, onClick }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<button className="button small icon-only move" onClick={onClick}>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveButton.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoveButton;
|
|
@ -0,0 +1,227 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { sortableElement, sortableHandle } from 'react-sortable-hoc';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import DescriptionInput from './DescriptionInput';
|
||||||
|
import LibelleInput from './LibelleInput';
|
||||||
|
import MandatoryInput from './MandatoryInput';
|
||||||
|
import MoveButton from './MoveButton';
|
||||||
|
import TypeDeChampCarteOption from './TypeDeChampCarteOption';
|
||||||
|
import TypeDeChampCarteOptions from './TypeDeChampCarteOptions';
|
||||||
|
import TypeDeChampDropDownOptions from './TypeDeChampDropDownOptions';
|
||||||
|
import TypeDeChampPieceJustificative from './TypeDeChampPieceJustificative';
|
||||||
|
import TypeDeChampRepetitionOptions from './TypeDeChampRepetitionOptions';
|
||||||
|
import TypeDeChampTypesSelect from './TypeDeChampTypesSelect';
|
||||||
|
|
||||||
|
const TypeDeChamp = sortableElement(
|
||||||
|
({ typeDeChamp, dispatch, idx: index, isFirstItem, isLastItem, state }) => {
|
||||||
|
const isDropDown = [
|
||||||
|
'drop_down_list',
|
||||||
|
'multiple_drop_down_list',
|
||||||
|
'linked_drop_down_list'
|
||||||
|
].includes(typeDeChamp.type_champ);
|
||||||
|
const isFile = typeDeChamp.type_champ === 'piece_justificative';
|
||||||
|
const isCarte = typeDeChamp.type_champ === 'carte';
|
||||||
|
const isExplication = typeDeChamp.type_champ === 'explication';
|
||||||
|
const isHeaderSection = typeDeChamp.type_champ === 'header_section';
|
||||||
|
const isRepetition = typeDeChamp.type_champ === 'repetition';
|
||||||
|
const canBeMandatory =
|
||||||
|
!isHeaderSection && !isExplication && !state.isAnnotation;
|
||||||
|
|
||||||
|
const updateHandlers = createUpdateHandlers(
|
||||||
|
dispatch,
|
||||||
|
typeDeChamp,
|
||||||
|
index,
|
||||||
|
state.prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
const typeDeChampsTypesForRepetition = state.typeDeChampsTypes.filter(
|
||||||
|
([, type]) => !EXCLUDE_FROM_REPETITION.includes(type)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isLastItem ? state.lastTypeDeChampRef : null}
|
||||||
|
data-index={index}
|
||||||
|
className={`type-de-champ form flex column justify-start ${
|
||||||
|
isHeaderSection ? 'type-header-section' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex justify-start section head ${
|
||||||
|
!isHeaderSection ? 'hr' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<DragHandle />
|
||||||
|
<TypeDeChampTypesSelect
|
||||||
|
handler={updateHandlers.type_champ}
|
||||||
|
options={state.typeDeChampsTypes}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-start delete">
|
||||||
|
<button
|
||||||
|
className="button small icon-only danger"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({ type: 'removeTypeDeChamp', params: { typeDeChamp } })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="trash" title="Supprimer" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex justify-start section ${
|
||||||
|
isDropDown || isFile || isCarte ? 'hr' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex column justify-start">
|
||||||
|
<MoveButton
|
||||||
|
isVisible={!isFirstItem}
|
||||||
|
icon="arrow-up"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'moveTypeDeChampUp',
|
||||||
|
params: { typeDeChamp }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MoveButton
|
||||||
|
isVisible={!isLastItem}
|
||||||
|
icon="arrow-down"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'moveTypeDeChampDown',
|
||||||
|
params: { typeDeChamp }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex column justify-start">
|
||||||
|
<LibelleInput handler={updateHandlers.libelle} isVisible={true} />
|
||||||
|
<MandatoryInput
|
||||||
|
handler={updateHandlers.mandatory}
|
||||||
|
isVisible={canBeMandatory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<DescriptionInput
|
||||||
|
isVisible={!isHeaderSection}
|
||||||
|
handler={updateHandlers.description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-start section shift-left">
|
||||||
|
<TypeDeChampDropDownOptions
|
||||||
|
isVisible={isDropDown}
|
||||||
|
handler={updateHandlers.drop_down_list_value}
|
||||||
|
/>
|
||||||
|
<TypeDeChampPieceJustificative
|
||||||
|
isVisible={isFile}
|
||||||
|
directUploadUrl={state.directUploadUrl}
|
||||||
|
filename={typeDeChamp.piece_justificative_template_filename}
|
||||||
|
handler={updateHandlers.piece_justificative_template}
|
||||||
|
url={typeDeChamp.piece_justificative_template_url}
|
||||||
|
/>
|
||||||
|
<TypeDeChampCarteOptions isVisible={isCarte}>
|
||||||
|
<TypeDeChampCarteOption
|
||||||
|
label="Quartiers prioritaires"
|
||||||
|
handler={updateHandlers.quartiers_prioritaires}
|
||||||
|
/>
|
||||||
|
<TypeDeChampCarteOption
|
||||||
|
label="Cadastres"
|
||||||
|
handler={updateHandlers.cadastres}
|
||||||
|
/>
|
||||||
|
<TypeDeChampCarteOption
|
||||||
|
label="Parcelles Agricoles"
|
||||||
|
handler={updateHandlers.parcelles_agricoles}
|
||||||
|
/>
|
||||||
|
</TypeDeChampCarteOptions>
|
||||||
|
<TypeDeChampRepetitionOptions
|
||||||
|
isVisible={isRepetition}
|
||||||
|
state={{
|
||||||
|
...state,
|
||||||
|
typeDeChampsTypes: typeDeChampsTypesForRepetition,
|
||||||
|
prefix: `repetition-${index}`,
|
||||||
|
typeDeChamps: typeDeChamp.types_de_champ || []
|
||||||
|
}}
|
||||||
|
typeDeChamp={typeDeChamp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TypeDeChamp.propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
idx: PropTypes.number,
|
||||||
|
isFirstItem: PropTypes.bool,
|
||||||
|
isLastItem: PropTypes.bool,
|
||||||
|
state: PropTypes.object,
|
||||||
|
typeDeChamp: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
const DragHandle = sortableHandle(() => (
|
||||||
|
<div className="handle button small icon-only">
|
||||||
|
<FontAwesomeIcon icon="arrows-alt-v" size="lg" />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
function createUpdateHandler(dispatch, typeDeChamp, field, index, prefix) {
|
||||||
|
return {
|
||||||
|
id: `${prefix ? `${prefix}-` : ''}champ-${index}-${field}`,
|
||||||
|
name: field,
|
||||||
|
value: typeDeChamp[field],
|
||||||
|
onChange: ({ target }) =>
|
||||||
|
dispatch({
|
||||||
|
type: 'updateTypeDeChamp',
|
||||||
|
params: {
|
||||||
|
typeDeChamp,
|
||||||
|
field,
|
||||||
|
value: readValue(target)
|
||||||
|
},
|
||||||
|
done: () => dispatch({ type: 'refresh' })
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUpdateHandlers(dispatch, typeDeChamp, index, prefix) {
|
||||||
|
return FIELDS.reduce((handlers, field) => {
|
||||||
|
handlers[field] = createUpdateHandler(
|
||||||
|
dispatch,
|
||||||
|
typeDeChamp,
|
||||||
|
field,
|
||||||
|
index,
|
||||||
|
prefix
|
||||||
|
);
|
||||||
|
return handlers;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FIELDS = [
|
||||||
|
'cadastres',
|
||||||
|
'description',
|
||||||
|
'drop_down_list_value',
|
||||||
|
'libelle',
|
||||||
|
'mandatory',
|
||||||
|
'order_place',
|
||||||
|
'parcelles_agricoles',
|
||||||
|
'parent_id',
|
||||||
|
'piece_justificative_template',
|
||||||
|
'private',
|
||||||
|
'quartiers_prioritaires',
|
||||||
|
'type_champ'
|
||||||
|
];
|
||||||
|
|
||||||
|
function readValue(input) {
|
||||||
|
return input.type === 'checkbox' ? input.checked : input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXCLUDE_FROM_REPETITION = [
|
||||||
|
'carte',
|
||||||
|
'dossier_link',
|
||||||
|
'repetition',
|
||||||
|
'siret'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default TypeDeChamp;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function TypeDeChampCarteOption({ label, handler }) {
|
||||||
|
return (
|
||||||
|
<label htmlFor={handler.id}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
checked={!!handler.value}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampCarteOption.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
handler: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChampCarteOption;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function TypeDeChampCarteOptions({ isVisible, children }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<label>Utilisation de la cartographie</label>
|
||||||
|
<div className="carte-options">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampCarteOptions.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
children: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChampCarteOptions;
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function TypeDeChampDropDownOptions({ isVisible, value, handler }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<label htmlFor={handler.id}>Liste déroulante</label>
|
||||||
|
<textarea
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
value={value}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
rows={3}
|
||||||
|
cols={40}
|
||||||
|
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampDropDownOptions.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
value: PropTypes.string,
|
||||||
|
handler: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChampDropDownOptions;
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Uploader from '../../../shared/activestorage/uploader';
|
||||||
|
|
||||||
|
function TypeDeChampPieceJustificative({
|
||||||
|
isVisible,
|
||||||
|
url,
|
||||||
|
filename,
|
||||||
|
handler,
|
||||||
|
directUploadUrl
|
||||||
|
}) {
|
||||||
|
if (isVisible) {
|
||||||
|
const hasFile = !!filename;
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<label htmlFor={handler.id}>Modèle</label>
|
||||||
|
<FileInformation isVisible={hasFile} url={url} filename={filename} />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
onChange={onFileChange(handler, directUploadUrl)}
|
||||||
|
className="small-margin small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampPieceJustificative.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
url: PropTypes.string,
|
||||||
|
filename: PropTypes.string,
|
||||||
|
handler: PropTypes.object,
|
||||||
|
directUploadUrl: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileInformation({ isVisible, url, filename }) {
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||||
|
{filename}
|
||||||
|
</a>
|
||||||
|
<br /> Modifier :
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInformation.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
url: PropTypes.string,
|
||||||
|
filename: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function onFileChange(handler, directUploadUrl) {
|
||||||
|
return async ({ target }) => {
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const signedId = await uploadFile(target, file, directUploadUrl);
|
||||||
|
handler.onChange({
|
||||||
|
target: {
|
||||||
|
value: signedId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(input, file, directUploadUrl) {
|
||||||
|
const controller = new Uploader(input, file, directUploadUrl);
|
||||||
|
return controller.start().then(signedId => {
|
||||||
|
input.value = null;
|
||||||
|
return signedId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypeDeChampPieceJustificative;
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { useReducer, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { SortableContainer, addChampLabel } from '../utils';
|
||||||
|
import TypeDeChamp from './TypeDeChamp';
|
||||||
|
import typeDeChampsReducer from '../typeDeChampsReducer';
|
||||||
|
|
||||||
|
function TypeDeChampRepetitionOptions({
|
||||||
|
isVisible,
|
||||||
|
state: parentState,
|
||||||
|
typeDeChamp
|
||||||
|
}) {
|
||||||
|
const lastTypeDeChampRef = useRef(null);
|
||||||
|
const [state, dispatch] = useReducer(typeDeChampsReducer, {
|
||||||
|
...parentState,
|
||||||
|
lastTypeDeChampRef
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
return (
|
||||||
|
<div className="repetition flex-grow cell">
|
||||||
|
<SortableContainer
|
||||||
|
onSortEnd={params => dispatch({ type: 'onSortTypeDeChamps', params })}
|
||||||
|
useDragHandle
|
||||||
|
>
|
||||||
|
{state.typeDeChamps.map((typeDeChamp, index) => (
|
||||||
|
<TypeDeChamp
|
||||||
|
dispatch={dispatch}
|
||||||
|
idx={index}
|
||||||
|
index={index}
|
||||||
|
isFirstItem={index === 0}
|
||||||
|
isLastItem={index === state.typeDeChamps.length - 1}
|
||||||
|
key={`champ-${typeDeChamp.id}`}
|
||||||
|
state={state}
|
||||||
|
typeDeChamp={typeDeChamp}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContainer>
|
||||||
|
<button
|
||||||
|
className="button"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'addNewRepetitionTypeDeChamp',
|
||||||
|
params: { typeDeChamp },
|
||||||
|
done: () => dispatch({ type: 'refresh' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{addChampLabel(state.isAnnotation)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampRepetitionOptions.propTypes = {
|
||||||
|
isVisible: PropTypes.bool,
|
||||||
|
state: PropTypes.object,
|
||||||
|
typeDeChamp: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChampRepetitionOptions;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function TypeDeChampTypesSelect({ handler, options }) {
|
||||||
|
return (
|
||||||
|
<div className="cell">
|
||||||
|
<select
|
||||||
|
id={handler.id}
|
||||||
|
name={handler.name}
|
||||||
|
onChange={handler.onChange}
|
||||||
|
value={handler.value}
|
||||||
|
className="small-margin small inline"
|
||||||
|
>
|
||||||
|
{options.map(([label, key]) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChampTypesSelect.propTypes = {
|
||||||
|
handler: PropTypes.object,
|
||||||
|
options: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChampTypesSelect;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useReducer, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { SortableContainer, addChampLabel } from '../utils';
|
||||||
|
import TypeDeChamp from './TypeDeChamp';
|
||||||
|
import typeDeChampsReducer from '../typeDeChampsReducer';
|
||||||
|
|
||||||
|
function TypeDeChamps({ state: rootState, typeDeChamps }) {
|
||||||
|
const lastTypeDeChampRef = useRef(null);
|
||||||
|
const [state, dispatch] = useReducer(typeDeChampsReducer, {
|
||||||
|
...rootState,
|
||||||
|
lastTypeDeChampRef,
|
||||||
|
typeDeChamps
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.typeDeChamps.length === 0) {
|
||||||
|
dispatch({
|
||||||
|
type: 'addFirstTypeDeChamp',
|
||||||
|
done: () => dispatch({ type: 'refresh' })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="champs-editor">
|
||||||
|
<SortableContainer
|
||||||
|
onSortEnd={params => dispatch({ type: 'onSortTypeDeChamps', params })}
|
||||||
|
lockAxis="y"
|
||||||
|
useDragHandle
|
||||||
|
>
|
||||||
|
{state.typeDeChamps.map((typeDeChamp, index) => (
|
||||||
|
<TypeDeChamp
|
||||||
|
dispatch={dispatch}
|
||||||
|
idx={index}
|
||||||
|
index={index}
|
||||||
|
isFirstItem={index === 0}
|
||||||
|
isLastItem={index === state.typeDeChamps.length - 1}
|
||||||
|
key={`champ-${typeDeChamp.id}`}
|
||||||
|
state={state}
|
||||||
|
typeDeChamp={typeDeChamp}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContainer>
|
||||||
|
<div className="footer"> </div>
|
||||||
|
<div className="buttons">
|
||||||
|
<button
|
||||||
|
className="button"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch({
|
||||||
|
type: 'addNewTypeDeChamp',
|
||||||
|
done: () => dispatch({ type: 'refresh' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{addChampLabel(state.isAnnotation)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button primary"
|
||||||
|
onClick={() => state.flash.success()}
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeDeChamps.propTypes = {
|
||||||
|
state: PropTypes.object,
|
||||||
|
typeDeChamps: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeDeChamps;
|
63
app/javascript/components/TypesDeChampEditor/index.js
Normal file
63
app/javascript/components/TypesDeChampEditor/index.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowsAltV,
|
||||||
|
faArrowUp,
|
||||||
|
faTrash
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import Flash from './Flash';
|
||||||
|
import OperationsQueue from './OperationsQueue';
|
||||||
|
import TypeDeChamps from './components/TypeDeChamps';
|
||||||
|
|
||||||
|
library.add(faArrowDown, faArrowsAltV, faArrowUp, faTrash);
|
||||||
|
|
||||||
|
class TypesDeChampEditor extends Component {
|
||||||
|
constructor({
|
||||||
|
baseUrl,
|
||||||
|
typeDeChampsTypes,
|
||||||
|
directUploadUrl,
|
||||||
|
isAnnotation,
|
||||||
|
typeDeChamps
|
||||||
|
}) {
|
||||||
|
super({ typeDeChamps });
|
||||||
|
const defaultTypeDeChampAttributes = {
|
||||||
|
type_champ: 'text',
|
||||||
|
types_de_champ: [],
|
||||||
|
private: isAnnotation,
|
||||||
|
libelle: `${isAnnotation ? 'Nouvelle annotation' : 'Nouveau champ'} ${
|
||||||
|
typeDeChampsTypes[0][0]
|
||||||
|
}`
|
||||||
|
};
|
||||||
|
this.state = {
|
||||||
|
flash: new Flash(isAnnotation),
|
||||||
|
queue: new OperationsQueue(baseUrl),
|
||||||
|
defaultTypeDeChampAttributes,
|
||||||
|
typeDeChampsTypes,
|
||||||
|
directUploadUrl,
|
||||||
|
isAnnotation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TypeDeChamps state={this.state} typeDeChamps={this.props.typeDeChamps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TypesDeChampEditor.propTypes = {
|
||||||
|
baseUrl: PropTypes.string,
|
||||||
|
directUploadUrl: PropTypes.string,
|
||||||
|
isAnnotation: PropTypes.bool,
|
||||||
|
typeDeChamps: PropTypes.array,
|
||||||
|
typeDeChampsTypes: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createReactUJSElement(props) {
|
||||||
|
return React.createElement(TypesDeChampEditor, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypesDeChampEditor;
|
51
app/javascript/components/TypesDeChampEditor/operations.js
Normal file
51
app/javascript/components/TypesDeChampEditor/operations.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
export function createTypeDeChampOperation(typeDeChamp, queue) {
|
||||||
|
return queue
|
||||||
|
.enqueue({
|
||||||
|
path: '',
|
||||||
|
method: 'post',
|
||||||
|
payload: { type_de_champ: typeDeChamp }
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
handleResponseData(typeDeChamp, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyTypeDeChampOperation(typeDeChamp, queue) {
|
||||||
|
return queue.enqueue({
|
||||||
|
path: `/${typeDeChamp.id}`,
|
||||||
|
method: 'delete',
|
||||||
|
payload: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveTypeDeChampOperation(typeDeChamp, index, queue) {
|
||||||
|
return queue.enqueue({
|
||||||
|
path: `/${typeDeChamp.id}/move`,
|
||||||
|
method: 'patch',
|
||||||
|
payload: { order_place: index }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTypeDeChampOperation(typeDeChamp, queue) {
|
||||||
|
return queue
|
||||||
|
.enqueue({
|
||||||
|
path: `/${typeDeChamp.id}`,
|
||||||
|
method: 'patch',
|
||||||
|
payload: { type_de_champ: typeDeChamp }
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
handleResponseData(typeDeChamp, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResponseData(typeDeChamp, { type_de_champ }) {
|
||||||
|
for (let field of RESPONSE_FIELDS) {
|
||||||
|
typeDeChamp[field] = type_de_champ[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESPONSE_FIELDS = [
|
||||||
|
'id',
|
||||||
|
'piece_justificative_template_filename',
|
||||||
|
'piece_justificative_template_url'
|
||||||
|
];
|
|
@ -0,0 +1,184 @@
|
||||||
|
import scrollToComponent from 'react-scroll-to-component';
|
||||||
|
import { debounce } from '@utils';
|
||||||
|
import {
|
||||||
|
createTypeDeChampOperation,
|
||||||
|
destroyTypeDeChampOperation,
|
||||||
|
moveTypeDeChampOperation,
|
||||||
|
updateTypeDeChampOperation
|
||||||
|
} from './operations';
|
||||||
|
|
||||||
|
export default function typeDeChampsReducer(state, { type, params, done }) {
|
||||||
|
switch (type) {
|
||||||
|
case 'addNewTypeDeChamp':
|
||||||
|
return addNewTypeDeChamp(state, state.typeDeChamps, done);
|
||||||
|
case 'addFirstTypeDeChamp':
|
||||||
|
return addFirstTypeDeChamp(state, state.typeDeChamps, done);
|
||||||
|
case 'addNewRepetitionTypeDeChamp':
|
||||||
|
return addNewRepetitionTypeDeChamp(
|
||||||
|
state,
|
||||||
|
state.typeDeChamps,
|
||||||
|
params.typeDeChamp,
|
||||||
|
done
|
||||||
|
);
|
||||||
|
case 'updateTypeDeChamp':
|
||||||
|
return updateTypeDeChamp(state, state.typeDeChamps, params, done);
|
||||||
|
case 'removeTypeDeChamp':
|
||||||
|
return removeTypeDeChamp(state, state.typeDeChamps, params);
|
||||||
|
case 'moveTypeDeChampUp':
|
||||||
|
return moveTypeDeChampUp(state, state.typeDeChamps, params);
|
||||||
|
case 'moveTypeDeChampDown':
|
||||||
|
return moveTypeDeChampDown(state, state.typeDeChamps, params);
|
||||||
|
case 'onSortTypeDeChamps':
|
||||||
|
return onSortTypeDeChamps(state, state.typeDeChamps, params);
|
||||||
|
case 'refresh':
|
||||||
|
return { ...state, typeDeChamps: [...state.typeDeChamps] };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action "${type}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewTypeDeChamp(state, typeDeChamps, done) {
|
||||||
|
const typeDeChamp = {
|
||||||
|
...state.defaultTypeDeChampAttributes,
|
||||||
|
order_place: typeDeChamps.length
|
||||||
|
};
|
||||||
|
|
||||||
|
createTypeDeChampOperation(typeDeChamp, state.queue)
|
||||||
|
.then(() => {
|
||||||
|
state.flash.success();
|
||||||
|
done();
|
||||||
|
if (state.lastTypeDeChampRef) {
|
||||||
|
scrollToComponent(state.lastTypeDeChampRef.current);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: [...typeDeChamps, typeDeChamp]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewRepetitionTypeDeChamp(state, typeDeChamps, typeDeChamp, done) {
|
||||||
|
return addNewTypeDeChamp(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
defaultTypeDeChampAttributes: {
|
||||||
|
...state.defaultTypeDeChampAttributes,
|
||||||
|
parent_id: typeDeChamp.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typeDeChamps,
|
||||||
|
done
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFirstTypeDeChamp(state, typeDeChamps, done) {
|
||||||
|
const typeDeChamp = { ...state.defaultTypeDeChampAttributes, order_place: 0 };
|
||||||
|
|
||||||
|
createTypeDeChampOperation(typeDeChamp, state.queue)
|
||||||
|
.then(() => done())
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: [...typeDeChamps, typeDeChamp]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTypeDeChamp(
|
||||||
|
state,
|
||||||
|
typeDeChamps,
|
||||||
|
{ typeDeChamp, field, value },
|
||||||
|
done
|
||||||
|
) {
|
||||||
|
typeDeChamp[field] = value;
|
||||||
|
|
||||||
|
getUpdateHandler(typeDeChamp, state)(done);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: [...typeDeChamps]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTypeDeChamp(state, typeDeChamps, { typeDeChamp }) {
|
||||||
|
destroyTypeDeChampOperation(typeDeChamp, state.queue)
|
||||||
|
.then(() => state.flash.success())
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: arrayRemove(typeDeChamps, typeDeChamp)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveTypeDeChampUp(state, typeDeChamps, { typeDeChamp }) {
|
||||||
|
const oldIndex = typeDeChamps.indexOf(typeDeChamp);
|
||||||
|
const newIndex = oldIndex - 1;
|
||||||
|
|
||||||
|
moveTypeDeChampOperation(typeDeChamp, newIndex, state.queue)
|
||||||
|
.then(() => state.flash.success())
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveTypeDeChampDown(state, typeDeChamps, { typeDeChamp }) {
|
||||||
|
const oldIndex = typeDeChamps.indexOf(typeDeChamp);
|
||||||
|
const newIndex = oldIndex + 1;
|
||||||
|
|
||||||
|
moveTypeDeChampOperation(typeDeChamp, newIndex, state.queue)
|
||||||
|
.then(() => state.flash.success())
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSortTypeDeChamps(state, typeDeChamps, { oldIndex, newIndex }) {
|
||||||
|
moveTypeDeChampOperation(typeDeChamps[oldIndex], newIndex, state.queue)
|
||||||
|
.then(() => state.flash.success())
|
||||||
|
.catch(message => state.flash.error(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
typeDeChamps: arrayMove(typeDeChamps, oldIndex, newIndex)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayRemove(array, item) {
|
||||||
|
array = Array.from(array);
|
||||||
|
array.splice(array.indexOf(item), 1);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMove(array, from, to) {
|
||||||
|
array = Array.from(array);
|
||||||
|
array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateHandlers = new WeakMap();
|
||||||
|
function getUpdateHandler(typeDeChamp, { queue, flash }) {
|
||||||
|
let handler = updateHandlers.get(typeDeChamp);
|
||||||
|
if (!handler) {
|
||||||
|
handler = debounce(
|
||||||
|
done =>
|
||||||
|
updateTypeDeChampOperation(typeDeChamp, queue)
|
||||||
|
.then(() => {
|
||||||
|
flash.success();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(message => flash.error(message)),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
updateHandlers.set(typeDeChamp, handler);
|
||||||
|
}
|
||||||
|
return handler;
|
||||||
|
}
|
14
app/javascript/components/TypesDeChampEditor/utils.js
Normal file
14
app/javascript/components/TypesDeChampEditor/utils.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { sortableContainer } from 'react-sortable-hoc';
|
||||||
|
|
||||||
|
export const SortableContainer = sortableContainer(({ children }) => {
|
||||||
|
return <ul>{children}</ul>;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function addChampLabel(isAnnotation) {
|
||||||
|
if (isAnnotation) {
|
||||||
|
return 'Ajouter une annotation';
|
||||||
|
} else {
|
||||||
|
return 'Ajouter un champ';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,254 +0,0 @@
|
||||||
import { getJSON, debounce } from '@utils';
|
|
||||||
import Uploader from '../../shared/activestorage/uploader';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: ['state', 'index', 'item'],
|
|
||||||
computed: {
|
|
||||||
isValid() {
|
|
||||||
if (this.deleted) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (this.libelle) {
|
|
||||||
return !!this.libelle.trim();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
itemClassName() {
|
|
||||||
const classNames = [`draggable-item-${this.index}`];
|
|
||||||
if (this.isHeaderSection) {
|
|
||||||
classNames.push('type-header-section');
|
|
||||||
}
|
|
||||||
return classNames.join(' ');
|
|
||||||
},
|
|
||||||
isDropDown() {
|
|
||||||
return [
|
|
||||||
'drop_down_list',
|
|
||||||
'multiple_drop_down_list',
|
|
||||||
'linked_drop_down_list'
|
|
||||||
].includes(this.typeChamp);
|
|
||||||
},
|
|
||||||
isFile() {
|
|
||||||
return this.typeChamp === 'piece_justificative';
|
|
||||||
},
|
|
||||||
isCarte() {
|
|
||||||
return this.typeChamp === 'carte';
|
|
||||||
},
|
|
||||||
isExplication() {
|
|
||||||
return this.typeChamp === 'explication';
|
|
||||||
},
|
|
||||||
isHeaderSection() {
|
|
||||||
return this.typeChamp === 'header_section';
|
|
||||||
},
|
|
||||||
isRepetition() {
|
|
||||||
return this.typeChamp === 'repetition';
|
|
||||||
},
|
|
||||||
options() {
|
|
||||||
const options = this.item.options || {};
|
|
||||||
for (let key of Object.keys(options)) {
|
|
||||||
options[key] = castBoolean(options[key]);
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
attribute() {
|
|
||||||
if (this.state.isAnnotation) {
|
|
||||||
return 'types_de_champ_private_attributes';
|
|
||||||
} else {
|
|
||||||
return 'types_de_champ_attributes';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
payload() {
|
|
||||||
const payload = {
|
|
||||||
libelle: this.libelle,
|
|
||||||
type_champ: this.typeChamp,
|
|
||||||
mandatory: this.mandatory,
|
|
||||||
description: this.description,
|
|
||||||
drop_down_list_value: this.dropDownListValue,
|
|
||||||
order_place: this.index
|
|
||||||
};
|
|
||||||
if (this.pieceJustificativeTemplate) {
|
|
||||||
payload.piece_justificative_template = this.pieceJustificativeTemplate;
|
|
||||||
}
|
|
||||||
if (this.state.parentId) {
|
|
||||||
payload.parent_id = this.state.parentId;
|
|
||||||
}
|
|
||||||
if (!this.id && this.state.isAnnotation) {
|
|
||||||
payload.private = true;
|
|
||||||
}
|
|
||||||
Object.assign(payload, this.options);
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
saveUrl() {
|
|
||||||
if (this.id) {
|
|
||||||
return `${this.state.saveUrl}/${this.id}`;
|
|
||||||
}
|
|
||||||
return this.state.saveUrl;
|
|
||||||
},
|
|
||||||
savePayload() {
|
|
||||||
if (this.deleted) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return { type_de_champ: this.payload };
|
|
||||||
},
|
|
||||||
saveMethod() {
|
|
||||||
if (this.deleted) {
|
|
||||||
return 'delete';
|
|
||||||
} else if (this.id) {
|
|
||||||
return 'patch';
|
|
||||||
}
|
|
||||||
return 'post';
|
|
||||||
},
|
|
||||||
typesDeChamp() {
|
|
||||||
return this.item.types_de_champ;
|
|
||||||
},
|
|
||||||
typesDeChampOptions() {
|
|
||||||
return this.state.typesDeChampOptions.filter(
|
|
||||||
([, typeChamp]) => !EXCLUDE_FROM_REPETITION.includes(typeChamp)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
stateForRepetition() {
|
|
||||||
return Object.assign({}, this.state, {
|
|
||||||
typesDeChamp: this.typesDeChamp,
|
|
||||||
typesDeChampOptions: this.typesDeChampOptions,
|
|
||||||
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
|
|
||||||
parentId: this.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: this.item.id,
|
|
||||||
typeChamp: this.item.type_champ,
|
|
||||||
libelle: this.item.libelle,
|
|
||||||
mandatory: this.item.mandatory,
|
|
||||||
description: this.item.description,
|
|
||||||
pieceJustificativeTemplate: null,
|
|
||||||
pieceJustificativeTemplateUrl: this.item.piece_justificative_template_url,
|
|
||||||
pieceJustificativeTemplateFilename: this.item
|
|
||||||
.piece_justificative_template_filename,
|
|
||||||
dropDownListValue: this.item.drop_down_list_value,
|
|
||||||
deleted: false,
|
|
||||||
isSaving: false,
|
|
||||||
isUploading: false,
|
|
||||||
hasChanges: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
index() {
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.debouncedSave = debounce(() => this.save(), 500);
|
|
||||||
this.debouncedUpload = debounce(evt => this.upload(evt), 500);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
removeChamp() {
|
|
||||||
if (this.id) {
|
|
||||||
this.deleted = true;
|
|
||||||
this.debouncedSave();
|
|
||||||
} else {
|
|
||||||
const index = this.state.typesDeChamp.indexOf(this.item);
|
|
||||||
this.state.typesDeChamp.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nameFor(name) {
|
|
||||||
return `${this.state.prefix}[${this.attribute}][${this.index}][${name}]`;
|
|
||||||
},
|
|
||||||
elementIdFor(name) {
|
|
||||||
const prefix = this.state.prefix.replace(/\[/g, '_').replace(/\]/g, '');
|
|
||||||
return `${prefix}_${this.attribute}_${this.index}_${name}`;
|
|
||||||
},
|
|
||||||
addChamp() {
|
|
||||||
this.typesDeChamp.push({
|
|
||||||
type_champ: 'text',
|
|
||||||
types_de_champ: []
|
|
||||||
});
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
this.hasChanges = true;
|
|
||||||
if (this.isValid) {
|
|
||||||
if (this.state.inFlight === 0) {
|
|
||||||
this.state.flash.clear();
|
|
||||||
}
|
|
||||||
this.debouncedSave();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
upload(evt) {
|
|
||||||
if (this.isUploading) {
|
|
||||||
this.debouncedUpload();
|
|
||||||
} else {
|
|
||||||
const input = evt.target;
|
|
||||||
const file = input.files[0];
|
|
||||||
if (file) {
|
|
||||||
this.isUploading = true;
|
|
||||||
const controller = new Uploader(
|
|
||||||
input,
|
|
||||||
file,
|
|
||||||
this.state.directUploadUrl
|
|
||||||
);
|
|
||||||
controller.start().then(signed_id => {
|
|
||||||
this.pieceJustificativeTemplate = signed_id;
|
|
||||||
this.isUploading = false;
|
|
||||||
this.debouncedSave();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
input.value = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
save() {
|
|
||||||
if (this.isSaving) {
|
|
||||||
this.debouncedSave();
|
|
||||||
} else {
|
|
||||||
this.isSaving = true;
|
|
||||||
this.state.inFlight++;
|
|
||||||
getJSON(this.saveUrl, this.savePayload, this.saveMethod)
|
|
||||||
.then(data => {
|
|
||||||
this.onSuccess(data);
|
|
||||||
})
|
|
||||||
.catch(xhr => {
|
|
||||||
this.onError(xhr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
|
||||||
if (data && data.type_de_champ) {
|
|
||||||
this.id = data.type_de_champ.id;
|
|
||||||
this.pieceJustificativeTemplateUrl =
|
|
||||||
data.type_de_champ.piece_justificative_template_url;
|
|
||||||
this.pieceJustificativeTemplateFilename =
|
|
||||||
data.type_de_champ.piece_justificative_template_filename;
|
|
||||||
this.pieceJustificativeTemplate = null;
|
|
||||||
}
|
|
||||||
this.state.inFlight--;
|
|
||||||
this.isSaving = false;
|
|
||||||
this.hasChanges = false;
|
|
||||||
|
|
||||||
if (this.state.inFlight === 0) {
|
|
||||||
this.state.flash.success();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError(xhr) {
|
|
||||||
this.isSaving = false;
|
|
||||||
this.state.inFlight--;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
errors: [message]
|
|
||||||
} = JSON.parse(xhr.responseText);
|
|
||||||
this.state.flash.error(message);
|
|
||||||
} catch (e) {
|
|
||||||
this.state.flash.error(xhr.responseText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const EXCLUDE_FROM_REPETITION = [
|
|
||||||
'carte',
|
|
||||||
'dossier_link',
|
|
||||||
'repetition',
|
|
||||||
'siret'
|
|
||||||
];
|
|
||||||
|
|
||||||
function castBoolean(value) {
|
|
||||||
return value && value != 0;
|
|
||||||
}
|
|
|
@ -1,181 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="deleted" v-if="deleted">
|
|
||||||
<input type="hidden" :name="nameFor('id')" :value="id">
|
|
||||||
<input type="hidden" :name="nameFor('_destroy')" value="true">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="draggable-item flex column justify-start" v-else :class="itemClassName">
|
|
||||||
<div class="flex justify-start section head" :class="{ hr: !isHeaderSection }">
|
|
||||||
<div class="handle">
|
|
||||||
<img :src="state.dragIconUrl" alt="">
|
|
||||||
</div>
|
|
||||||
<div class="cell">
|
|
||||||
<select
|
|
||||||
:id="elementIdFor('type_champ')"
|
|
||||||
:name="nameFor('type_champ')"
|
|
||||||
v-model="typeChamp"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small inline">
|
|
||||||
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
|
|
||||||
{{ option[0] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start delete">
|
|
||||||
<button class="button danger" @click.prevent="removeChamp">
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start section" :class="{ hr: isDropDown || isFile || isCarte }">
|
|
||||||
<div class="flex column justify-start shift-left">
|
|
||||||
<div class="cell libelle">
|
|
||||||
<label :for="elementIdFor('libelle')">
|
|
||||||
Libellé
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:id="elementIdFor('libelle')"
|
|
||||||
:name="nameFor('libelle')"
|
|
||||||
v-model="libelle"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small"
|
|
||||||
:class="{ error: hasChanges && !isValid }">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
|
|
||||||
<label :for="elementIdFor('mandatory')">
|
|
||||||
Obligatoire
|
|
||||||
</label>
|
|
||||||
<input :name="nameFor('mandatory')" type="hidden" value="0">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:id="elementIdFor('mandatory')"
|
|
||||||
:name="nameFor('mandatory')"
|
|
||||||
v-model="mandatory"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small"
|
|
||||||
value="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start">
|
|
||||||
<div class="cell" v-show="!isHeaderSection">
|
|
||||||
<label :for="elementIdFor('description')">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
:id="elementIdFor('description')"
|
|
||||||
:name="nameFor('description')"
|
|
||||||
v-model="description"
|
|
||||||
@change="update"
|
|
||||||
rows=3
|
|
||||||
cols=40
|
|
||||||
class="small-margin small">
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-start section shift-left" v-show="!isHeaderSection">
|
|
||||||
<div class="cell" v-show="isDropDown">
|
|
||||||
<label :for="elementIdFor('drop_down_list')">
|
|
||||||
Liste déroulante
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
:id="elementIdFor('drop_down_list')"
|
|
||||||
:name="nameFor('drop_down_list_attributes[value]')"
|
|
||||||
v-model="dropDownListValue"
|
|
||||||
@change="update"
|
|
||||||
rows=3
|
|
||||||
cols=40
|
|
||||||
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
|
||||||
class="small-margin small">
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="cell" v-show="isFile">
|
|
||||||
<label :for="elementIdFor('piece_justificative_template')">
|
|
||||||
Modèle
|
|
||||||
</label>
|
|
||||||
<template v-if="pieceJustificativeTemplateUrl">
|
|
||||||
<a :href="pieceJustificativeTemplateUrl" rel="noopener" target="_blank">
|
|
||||||
{{pieceJustificativeTemplateFilename}}
|
|
||||||
</a>
|
|
||||||
<br> Modifier :
|
|
||||||
</template>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
:id="elementIdFor('piece_justificative_template')"
|
|
||||||
:name="nameFor('piece_justificative_template')"
|
|
||||||
@change="upload"
|
|
||||||
class="small-margin small">
|
|
||||||
</div>
|
|
||||||
<div class="cell" v-show="isCarte">
|
|
||||||
<label>
|
|
||||||
Utilisation de la cartographie
|
|
||||||
</label>
|
|
||||||
<div class="carte-options">
|
|
||||||
<label :for="elementIdFor('quartiers_prioritaires')">
|
|
||||||
<input :name="nameFor('quartiers_prioritaires')" type="hidden" value="0">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:id="elementIdFor('quartiers_prioritaires')"
|
|
||||||
:name="nameFor('quartiers_prioritaires')"
|
|
||||||
v-model="options.quartiers_prioritaires"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small"
|
|
||||||
value="1">
|
|
||||||
Quartiers prioritaires
|
|
||||||
</label>
|
|
||||||
<label :for="elementIdFor('cadastres')">
|
|
||||||
<input :name="nameFor('cadastres')" type="hidden" value="0">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:id="elementIdFor('cadastres')"
|
|
||||||
:name="nameFor('cadastres')"
|
|
||||||
v-model="options.cadastres"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small"
|
|
||||||
value="1">
|
|
||||||
Cadastres
|
|
||||||
</label>
|
|
||||||
<label :for="elementIdFor('parcelles_agricoles')">
|
|
||||||
<input :name="nameFor('parcelles_agricoles')" type="hidden" value="0">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:id="elementIdFor('parcelles_agricoles')"
|
|
||||||
:name="nameFor('parcelles_agricoles')"
|
|
||||||
v-model="options.parcelles_agricoles"
|
|
||||||
@change="update"
|
|
||||||
class="small-margin small"
|
|
||||||
value="1">
|
|
||||||
Parcelles Agricoles
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow cell" v-show="isRepetition">
|
|
||||||
<Draggable :list="typesDeChamp" :options="{handle:'.handle'}">
|
|
||||||
<DraggableItem
|
|
||||||
v-for="(item, index) in typesDeChamp"
|
|
||||||
:state="stateForRepetition"
|
|
||||||
:index="index"
|
|
||||||
:item="item"
|
|
||||||
:key="item.id" />
|
|
||||||
</Draggable>
|
|
||||||
|
|
||||||
<button class="button" @click.prevent="addChamp">
|
|
||||||
<template v-if="state.isAnnotation">
|
|
||||||
Ajouter une annotation
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Ajouter un champ
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="meta">
|
|
||||||
<input type="hidden" :name="nameFor('order_place')" :value="index">
|
|
||||||
<input type="hidden" :name="nameFor('id')" :value="id">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./DraggableItem.js"></script>
|
|
|
@ -1,14 +0,0 @@
|
||||||
export default {
|
|
||||||
props: ['state', 'version'],
|
|
||||||
methods: {
|
|
||||||
addChamp() {
|
|
||||||
this.state.typesDeChamp.push({
|
|
||||||
type_champ: 'text',
|
|
||||||
types_de_champ: []
|
|
||||||
});
|
|
||||||
},
|
|
||||||
save() {
|
|
||||||
this.state.flash.success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="champs-editor">
|
|
||||||
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
|
|
||||||
<DraggableItem
|
|
||||||
v-for="(item, index) in state.typesDeChamp"
|
|
||||||
:state="state"
|
|
||||||
:index="index"
|
|
||||||
:item="item"
|
|
||||||
:key="item.id" />
|
|
||||||
</Draggable>
|
|
||||||
|
|
||||||
<div class="footer"></div>
|
|
||||||
<div class="buttons">
|
|
||||||
<button class="button" v-scroll-to="'.footer'" @click.prevent="addChamp">
|
|
||||||
<template v-if="state.isAnnotation">
|
|
||||||
Ajouter une annotation
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Ajouter un champ
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="button primary" @click.prevent="save">Enregistrer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./DraggableList.js"></script>
|
|
|
@ -1,90 +0,0 @@
|
||||||
import Vue from 'vue';
|
|
||||||
import Draggable from 'vuedraggable';
|
|
||||||
import VueScrollTo from 'vue-scrollto';
|
|
||||||
|
|
||||||
import DraggableItem from './DraggableItem';
|
|
||||||
import DraggableList from './DraggableList';
|
|
||||||
|
|
||||||
Vue.component('Draggable', Draggable);
|
|
||||||
Vue.component('DraggableItem', DraggableItem);
|
|
||||||
Vue.use(VueScrollTo, { duration: 1500, easing: 'ease' });
|
|
||||||
|
|
||||||
addEventListener('DOMContentLoaded', () => {
|
|
||||||
const el = document.querySelector('#champs-editor');
|
|
||||||
if (el) {
|
|
||||||
initEditor(el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function initEditor(el) {
|
|
||||||
const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
|
|
||||||
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
|
|
||||||
directUploadUrl,
|
|
||||||
dragIconUrl,
|
|
||||||
saveUrl,
|
|
||||||
isAnnotation: el.dataset.type === 'annotation',
|
|
||||||
prefix: 'procedure',
|
|
||||||
inFlight: 0,
|
|
||||||
flash: new Flash()
|
|
||||||
};
|
|
||||||
|
|
||||||
// We add an initial type de champ here if form is empty
|
|
||||||
if (state.typesDeChamp.length === 0) {
|
|
||||||
state.typesDeChamp.push({
|
|
||||||
type_champ: 'text',
|
|
||||||
types_de_champ: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el,
|
|
||||||
data: {
|
|
||||||
state
|
|
||||||
},
|
|
||||||
render(h) {
|
|
||||||
return h(DraggableList, {
|
|
||||||
props: {
|
|
||||||
state: this.state
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class Flash {
|
|
||||||
constructor(isAnnotation) {
|
|
||||||
this.element = document.querySelector('#flash_messages');
|
|
||||||
this.isAnnotation = isAnnotation;
|
|
||||||
}
|
|
||||||
success() {
|
|
||||||
if (this.isAnnotation) {
|
|
||||||
this.add('Annotations privées enregistrées.');
|
|
||||||
} else {
|
|
||||||
this.add('Formulaire enregistré.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error(message) {
|
|
||||||
this.add(message, true);
|
|
||||||
}
|
|
||||||
clear() {
|
|
||||||
this.element.innerHTML = '';
|
|
||||||
}
|
|
||||||
add(message, isError) {
|
|
||||||
const html = `<div id="flash_message" class="center">
|
|
||||||
<div class="alert alert-fixed ${
|
|
||||||
isError ? 'alert-danger' : 'alert-success'
|
|
||||||
}">
|
|
||||||
${message}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
this.element.innerHTML = html;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.clear();
|
|
||||||
}, 6000);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,9 @@ import * as ActiveStorage from 'activestorage';
|
||||||
import Chartkick from 'chartkick';
|
import Chartkick from 'chartkick';
|
||||||
import Highcharts from 'highcharts';
|
import Highcharts from 'highcharts';
|
||||||
|
|
||||||
|
import ReactUJS from '../shared/react-ujs';
|
||||||
|
import reactComponents from '../shared/react-components';
|
||||||
|
|
||||||
import '../shared/activestorage/ujs';
|
import '../shared/activestorage/ujs';
|
||||||
import '../shared/rails-ujs-fix';
|
import '../shared/rails-ujs-fix';
|
||||||
import '../shared/safari-11-file-xhr-workaround';
|
import '../shared/safari-11-file-xhr-workaround';
|
||||||
|
@ -23,8 +26,6 @@ import '../new_design/champs/carte';
|
||||||
import '../new_design/champs/linked-drop-down-list';
|
import '../new_design/champs/linked-drop-down-list';
|
||||||
import '../new_design/champs/repetition';
|
import '../new_design/champs/repetition';
|
||||||
|
|
||||||
import '../new_design/administrateur/champs-editor';
|
|
||||||
|
|
||||||
import { toggleCondidentielExplanation } from '../new_design/avis';
|
import { toggleCondidentielExplanation } from '../new_design/avis';
|
||||||
import { scrollMessagerie } from '../new_design/messagerie';
|
import { scrollMessagerie } from '../new_design/messagerie';
|
||||||
import { showMotivation, motivationCancel } from '../new_design/state-button';
|
import { showMotivation, motivationCancel } from '../new_design/state-button';
|
||||||
|
@ -48,6 +49,9 @@ Rails.start();
|
||||||
Turbolinks.start();
|
Turbolinks.start();
|
||||||
ActiveStorage.start();
|
ActiveStorage.start();
|
||||||
|
|
||||||
|
const loader = new ReactUJS(reactComponents);
|
||||||
|
loader.start();
|
||||||
|
|
||||||
// Expose globals
|
// Expose globals
|
||||||
window.DS = window.DS || DS;
|
window.DS = window.DS || DS;
|
||||||
window.Chartkick = Chartkick;
|
window.Chartkick = Chartkick;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Include runtime-polyfills for older browsers.
|
// Include runtime-polyfills for older browsers.
|
||||||
// Due to .babelrc's 'useBuiltIns', only polyfills actually
|
// Due to babel.config.js's 'useBuiltIns', only polyfills actually
|
||||||
// required by the browsers we support will be included.
|
// required by the browsers we support will be included.
|
||||||
import '@babel/polyfill';
|
import '@babel/polyfill';
|
||||||
import 'dom4';
|
import 'dom4';
|
||||||
|
|
8
app/javascript/shared/react-components.js
vendored
Normal file
8
app/javascript/shared/react-components.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default function reactComponents(className) {
|
||||||
|
switch (className) {
|
||||||
|
case 'TypesDeChampEditor':
|
||||||
|
return import('components/TypesDeChampEditor').then(
|
||||||
|
mod => mod.createReactUJSElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
61
app/javascript/shared/react-ujs.js
vendored
Normal file
61
app/javascript/shared/react-ujs.js
vendored
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
// This attribute holds the name of component which should be mounted
|
||||||
|
// example: `data-react-class="MyApp.Items.EditForm"`
|
||||||
|
const CLASS_NAME_ATTR = 'data-react-class';
|
||||||
|
|
||||||
|
// This attribute holds JSON stringified props for initializing the component
|
||||||
|
// example: `data-react-props="{\"item\": { \"id\": 1, \"name\": \"My Item\"} }"`
|
||||||
|
const PROPS_ATTR = 'data-react-props';
|
||||||
|
|
||||||
|
// This attribute holds which method to use between: ReactDOM.hydrate, ReactDOM.render
|
||||||
|
const RENDER_ATTR = 'data-hydrate';
|
||||||
|
|
||||||
|
function findDOMNodes() {
|
||||||
|
return document.querySelectorAll(`[${CLASS_NAME_ATTR}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ReactUJS {
|
||||||
|
constructor(loadComponent) {
|
||||||
|
this.loadComponent = loadComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async mountComponents() {
|
||||||
|
const nodes = findDOMNodes();
|
||||||
|
|
||||||
|
for (let node of nodes) {
|
||||||
|
const className = node.getAttribute(CLASS_NAME_ATTR);
|
||||||
|
const createReactUJSElement = await this.loadComponent(className).catch(
|
||||||
|
() => null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createReactUJSElement) {
|
||||||
|
const message = "Cannot find component: '" + className + "'";
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
'%c[react-rails] %c' + message + ' for element',
|
||||||
|
'font-weight: bold',
|
||||||
|
'',
|
||||||
|
node
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
message + '. Make sure your component is available to render.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const propsJson = node.getAttribute(PROPS_ATTR);
|
||||||
|
const props = propsJson && JSON.parse(propsJson);
|
||||||
|
const hydrate = node.getAttribute(RENDER_ATTR);
|
||||||
|
|
||||||
|
if (hydrate && typeof ReactDOM.hydrate === 'function') {
|
||||||
|
ReactDOM.hydrate(createReactUJSElement(props), node);
|
||||||
|
} else {
|
||||||
|
ReactDOM.render(createReactUJSElement(props), node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
addEventListener('turbolinks:load', () => this.mountComponents());
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,10 @@ export function on(selector, eventName, fn) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function to(promise) {
|
||||||
|
return promise.then(result => [result]).catch(error => [null, error]);
|
||||||
|
}
|
||||||
|
|
||||||
function offset(element) {
|
function offset(element) {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -410,8 +410,48 @@ class Procedure < ApplicationRecord
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def move_type_de_champ(type_de_champ, new_index)
|
||||||
|
types_de_champ, collection_attribute_name = if type_de_champ.parent&.repetition?
|
||||||
|
if type_de_champ.parent.private?
|
||||||
|
[type_de_champ.parent.types_de_champ, :types_de_champ_private_attributes]
|
||||||
|
else
|
||||||
|
[type_de_champ.parent.types_de_champ, :types_de_champ_attributes]
|
||||||
|
end
|
||||||
|
elsif type_de_champ.private?
|
||||||
|
[self.types_de_champ_private, :types_de_champ_private_attributes]
|
||||||
|
else
|
||||||
|
[self.types_de_champ, :types_de_champ_attributes]
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes = move_type_de_champ_attributes(types_de_champ.to_a, type_de_champ, new_index)
|
||||||
|
|
||||||
|
if type_de_champ.parent&.repetition?
|
||||||
|
attributes = [
|
||||||
|
{
|
||||||
|
id: type_de_champ.parent.id,
|
||||||
|
libelle: type_de_champ.parent.libelle,
|
||||||
|
types_de_champ_attributes: attributes
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
update!(collection_attribute_name => attributes)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
|
||||||
|
old_index = types_de_champ.index(type_de_champ)
|
||||||
|
types_de_champ.insert(new_index, types_de_champ.delete_at(old_index))
|
||||||
|
.map.with_index do |type_de_champ, index|
|
||||||
|
{
|
||||||
|
id: type_de_champ.id,
|
||||||
|
libelle: type_de_champ.libelle,
|
||||||
|
order_place: index
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def claim_path_ownership!(path)
|
def claim_path_ownership!(path)
|
||||||
procedure = Procedure.joins(:administrateurs)
|
procedure = Procedure.joins(:administrateurs)
|
||||||
.where(administrateurs: { id: administrateur_ids })
|
.where(administrateurs: { id: administrateur_ids })
|
||||||
|
|
36
app/services/ip_service.rb
Normal file
36
app/services/ip_service.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
class IPService
|
||||||
|
class << self
|
||||||
|
def ip_trusted?(ip)
|
||||||
|
ip_address = parse_address(ip)
|
||||||
|
|
||||||
|
if ip_address.nil?
|
||||||
|
false
|
||||||
|
elsif trusted_networks.present?
|
||||||
|
trusted_networks.any? { |network| network.include?(ip_address) }
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def trusted_networks
|
||||||
|
if ENV['TRUSTED_NETWORKS'].present?
|
||||||
|
ENV['TRUSTED_NETWORKS']
|
||||||
|
.split
|
||||||
|
.map { |string| parse_address(string) }
|
||||||
|
.compact
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_address(address)
|
||||||
|
begin
|
||||||
|
IPAddr.new(address)
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,4 @@
|
||||||
%h1 Configuration des annotations privées
|
%h1 Configuration des annotations privées
|
||||||
%br
|
%br
|
||||||
|
|
||||||
= form_for @procedure, remote: true, html: { class: 'form' } do |form|
|
= react_component("TypesDeChampEditor", types_de_champ_private_data(@procedure))
|
||||||
#champs-editor{ data: types_de_champ_private_data(@procedure) }
|
|
||||||
.spinner
|
|
||||||
|
|
|
@ -7,6 +7,4 @@
|
||||||
%h1 Configuration des champs
|
%h1 Configuration des champs
|
||||||
%br
|
%br
|
||||||
|
|
||||||
= form_for @procedure, remote: true, html: { class: 'form' } do |form|
|
= react_component("TypesDeChampEditor", types_de_champ_data(@procedure))
|
||||||
#champs-editor{ data: types_de_champ_data(@procedure) }
|
|
||||||
.spinner
|
|
||||||
|
|
83
babel.config.js
Normal file
83
babel.config.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
module.exports = function(api) {
|
||||||
|
var validEnv = ['development', 'test', 'production'];
|
||||||
|
var currentEnv = api.env();
|
||||||
|
var isDevelopmentEnv = api.env('development');
|
||||||
|
var isProductionEnv = api.env('production');
|
||||||
|
var isTestEnv = api.env('test');
|
||||||
|
|
||||||
|
if (!validEnv.includes(currentEnv)) {
|
||||||
|
throw new Error(
|
||||||
|
'Please specify a valid `NODE_ENV` or ' +
|
||||||
|
'`BABEL_ENV` environment variables. Valid values are "development", ' +
|
||||||
|
'"test", and "production". Instead, received: ' +
|
||||||
|
JSON.stringify(currentEnv) +
|
||||||
|
'.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
isTestEnv && [
|
||||||
|
require('@babel/preset-env').default,
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
node: 'current'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
(isProductionEnv || isDevelopmentEnv) && [
|
||||||
|
require('@babel/preset-env').default,
|
||||||
|
{
|
||||||
|
forceAllTransforms: true,
|
||||||
|
useBuiltIns: 'entry',
|
||||||
|
modules: false,
|
||||||
|
exclude: ['transform-typeof-symbol']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require('@babel/preset-react').default,
|
||||||
|
{
|
||||||
|
development: isDevelopmentEnv || isTestEnv,
|
||||||
|
useBuiltIns: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
].filter(Boolean),
|
||||||
|
plugins: [
|
||||||
|
require('babel-plugin-macros'),
|
||||||
|
require('@babel/plugin-syntax-dynamic-import').default,
|
||||||
|
isTestEnv && require('babel-plugin-dynamic-import-node'),
|
||||||
|
require('@babel/plugin-transform-destructuring').default,
|
||||||
|
[
|
||||||
|
require('@babel/plugin-proposal-class-properties').default,
|
||||||
|
{
|
||||||
|
loose: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require('@babel/plugin-proposal-object-rest-spread').default,
|
||||||
|
{
|
||||||
|
useBuiltIns: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require('@babel/plugin-transform-runtime').default,
|
||||||
|
{
|
||||||
|
helpers: false,
|
||||||
|
regenerator: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
require('@babel/plugin-transform-regenerator').default,
|
||||||
|
{
|
||||||
|
async: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isProductionEnv && [
|
||||||
|
require('babel-plugin-transform-react-remove-prop-types').default,
|
||||||
|
{
|
||||||
|
removeImport: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
].filter(Boolean)
|
||||||
|
};
|
||||||
|
};
|
|
@ -57,6 +57,8 @@ API_ENTREPRISE_KEY=""
|
||||||
|
|
||||||
PIPEDRIVE_KEY=""
|
PIPEDRIVE_KEY=""
|
||||||
|
|
||||||
|
TRUSTED_NETWORKS=""
|
||||||
|
|
||||||
SKYLIGHT_AUTHENTICATION_KEY=""
|
SKYLIGHT_AUTHENTICATION_KEY=""
|
||||||
|
|
||||||
LOGRAGE_ENABLED="disabled"
|
LOGRAGE_ENABLED="disabled"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# See .babelrc
|
# See .browserslistrc
|
||||||
Browser.modern_rules.clear
|
Browser.modern_rules.clear
|
||||||
Browser.modern_rules << -> b { b.chrome? && b.version.to_i >= 50 }
|
Browser.modern_rules << -> b { b.chrome? && b.version.to_i >= 50 }
|
||||||
Browser.modern_rules << -> b { b.ie? && b.version.to_i >= 11 && !b.compatibility_view? }
|
Browser.modern_rules << -> b { b.ie? && b.version.to_i >= 11 && !b.compatibility_view? }
|
||||||
|
|
|
@ -372,7 +372,11 @@ Rails.application.routes.draw do
|
||||||
get 'annotations'
|
get 'annotations'
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :types_de_champ, only: [:create, :update, :destroy]
|
resources :types_de_champ, only: [:create, :update, :destroy] do
|
||||||
|
member do
|
||||||
|
patch :move
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :services, except: [:show] do
|
resources :services, except: [:show] do
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { environment } = require('@rails/webpacker');
|
const { environment } = require('@rails/webpacker');
|
||||||
const { VueLoaderPlugin } = require('vue-loader');
|
|
||||||
const vue = require('./loaders/vue');
|
|
||||||
|
|
||||||
const resolve = {
|
const resolve = {
|
||||||
alias: {
|
alias: {
|
||||||
|
@ -10,7 +8,4 @@ const resolve = {
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.config.merge({ resolve });
|
environment.config.merge({ resolve });
|
||||||
|
|
||||||
environment.plugins.append('VueLoaderPlugin', new VueLoaderPlugin());
|
|
||||||
environment.loaders.append('vue', vue);
|
|
||||||
module.exports = environment;
|
module.exports = environment;
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
test: /\.vue(\.erb)?$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'vue-loader'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -3,8 +3,11 @@
|
||||||
default: &default
|
default: &default
|
||||||
source_path: app/javascript
|
source_path: app/javascript
|
||||||
source_entry_path: packs
|
source_entry_path: packs
|
||||||
|
public_root_path: public
|
||||||
public_output_path: packs
|
public_output_path: packs
|
||||||
cache_path: tmp/cache/webpacker
|
cache_path: tmp/cache/webpacker
|
||||||
|
check_yarn_integrity: false
|
||||||
|
webpack_compile_output: false
|
||||||
|
|
||||||
# Additional paths webpack should lookup modules
|
# Additional paths webpack should lookup modules
|
||||||
# ['app/assets', 'engine/foo/app/assets']
|
# ['app/assets', 'engine/foo/app/assets']
|
||||||
|
@ -13,8 +16,25 @@ default: &default
|
||||||
# Reload manifest.json on all requests so we reload latest compiled packs
|
# Reload manifest.json on all requests so we reload latest compiled packs
|
||||||
cache_manifest: false
|
cache_manifest: false
|
||||||
|
|
||||||
|
# Extract and emit a css file
|
||||||
|
extract_css: false
|
||||||
|
|
||||||
|
static_assets_extensions:
|
||||||
|
- .jpg
|
||||||
|
- .jpeg
|
||||||
|
- .png
|
||||||
|
- .gif
|
||||||
|
- .tiff
|
||||||
|
- .ico
|
||||||
|
- .svg
|
||||||
|
- .eot
|
||||||
|
- .otf
|
||||||
|
- .ttf
|
||||||
|
- .woff
|
||||||
|
- .woff2
|
||||||
|
|
||||||
extensions:
|
extensions:
|
||||||
- .vue
|
- .mjs
|
||||||
- .js
|
- .js
|
||||||
- .sass
|
- .sass
|
||||||
- .scss
|
- .scss
|
||||||
|
@ -32,6 +52,9 @@ development:
|
||||||
<<: *default
|
<<: *default
|
||||||
compile: true
|
compile: true
|
||||||
|
|
||||||
|
# Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules
|
||||||
|
check_yarn_integrity: true
|
||||||
|
|
||||||
# Reference: https://webpack.js.org/configuration/dev-server/
|
# Reference: https://webpack.js.org/configuration/dev-server/
|
||||||
dev_server:
|
dev_server:
|
||||||
https: false
|
https: false
|
||||||
|
@ -49,7 +72,7 @@ development:
|
||||||
headers:
|
headers:
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
watch_options:
|
watch_options:
|
||||||
ignored: /node_modules/
|
ignored: '**/node_modules/**'
|
||||||
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
@ -65,5 +88,8 @@ production:
|
||||||
# Production depends on precompilation of packs prior to booting for performance.
|
# Production depends on precompilation of packs prior to booting for performance.
|
||||||
compile: false
|
compile: false
|
||||||
|
|
||||||
|
# Extract and emit a css file
|
||||||
|
extract_css: true
|
||||||
|
|
||||||
# Cache manifest.json for performance
|
# Cache manifest.json for performance
|
||||||
cache_manifest: true
|
cache_manifest: true
|
||||||
|
|
30
package.json
30
package.json
|
@ -1,32 +1,42 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.15",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.7.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"@rails/webpacker": "4.0.0-pre.3",
|
"@rails/webpacker": "4.0.0-pre.3",
|
||||||
"@sentry/browser": "^4.6.5",
|
"@sentry/browser": "^4.6.5",
|
||||||
"@turf/area": "^6.0.1",
|
"@turf/area": "^6.0.1",
|
||||||
"activestorage": "^5.2.2-rc1",
|
"activestorage": "^5.2.2",
|
||||||
"autocomplete.js": "^0.31.0",
|
"autocomplete.js": "^0.36.0",
|
||||||
"chartkick": "^3.0.1",
|
"chartkick": "^3.0.1",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.0",
|
||||||
"dom4": "^2.1.3",
|
"dom4": "^2.1.3",
|
||||||
"highcharts": "^6.1.2",
|
"highcharts": "^6.1.2",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"leaflet": "^1.3.4",
|
|
||||||
"leaflet-freedraw": "^2.9.0",
|
"leaflet-freedraw": "^2.9.0",
|
||||||
"rails-ujs": "^5.2.1",
|
"leaflet": "^1.3.4",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"rails-ujs": "^5.2.2",
|
||||||
"ramda": "^0.25.0",
|
"ramda": "^0.25.0",
|
||||||
|
"react_ujs": "^2.4.4",
|
||||||
|
"react-dom": "^16.8.4",
|
||||||
|
"react-scroll-to-component": "^1.0.2",
|
||||||
|
"react-sortable-hoc": "^1.7.1",
|
||||||
|
"react": "^16.8.4",
|
||||||
"select2": "^4.0.6-rc.1",
|
"select2": "^4.0.6-rc.1",
|
||||||
"turbolinks": "^5.2.0",
|
"turbolinks": "^5.2.0"
|
||||||
"vue": "^2.5.21",
|
|
||||||
"vue-loader": "^15.5.1",
|
|
||||||
"vue-template-compiler": "^2.5.21",
|
|
||||||
"vuedraggable": "^2.16.0",
|
|
||||||
"vue-scrollto": "^2.13.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"babel-plugin-macros": "^2.5.0",
|
||||||
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"eclint": "^2.8.0",
|
"eclint": "^2.8.0",
|
||||||
"eslint": "^5.9.0",
|
"eslint": "^5.9.0",
|
||||||
"eslint-config-prettier": "^3.3.0",
|
"eslint-config-prettier": "^3.3.0",
|
||||||
"eslint-plugin-prettier": "^3.0.0",
|
"eslint-plugin-prettier": "^3.0.0",
|
||||||
|
"eslint-plugin-react": "^7.12.4",
|
||||||
|
"eslint-plugin-react-hooks": "^1.5.1",
|
||||||
"prettier": "^1.15.3",
|
"prettier": "^1.15.3",
|
||||||
"webpack-dev-server": "^3.1.9"
|
"webpack-dev-server": "^3.1.9"
|
||||||
},
|
},
|
||||||
|
|
12
postcss.config.js
Normal file
12
postcss.config.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('postcss-import'),
|
||||||
|
require('postcss-flexbugs-fixes'),
|
||||||
|
require('postcss-preset-env')({
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: 'no-2009'
|
||||||
|
},
|
||||||
|
stage: 3
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
|
@ -158,6 +158,7 @@ describe ApplicationController, type: :controller do
|
||||||
allow(@controller).to receive(:sensitive_path).and_return(sensitive_path)
|
allow(@controller).to receive(:sensitive_path).and_return(sensitive_path)
|
||||||
allow(@controller).to receive(:send_login_token_or_bufferize)
|
allow(@controller).to receive(:send_login_token_or_bufferize)
|
||||||
allow(@controller).to receive(:store_location_for)
|
allow(@controller).to receive(:store_location_for)
|
||||||
|
allow(IPService).to receive(:ip_trusted?).and_return(ip_trusted)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { @controller.send(:redirect_if_untrusted) }
|
subject { @controller.send(:redirect_if_untrusted) }
|
||||||
|
@ -173,6 +174,9 @@ describe ApplicationController, type: :controller do
|
||||||
Flipflop::FeatureSet.current.test!.switch!(:enable_email_login_token, true)
|
Flipflop::FeatureSet.current.test!.switch!(:enable_email_login_token, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the ip is not trusted' do
|
||||||
|
let(:ip_trusted) { false }
|
||||||
|
|
||||||
context 'when the device is trusted' do
|
context 'when the device is trusted' do
|
||||||
let(:trusted_device) { true }
|
let(:trusted_device) { true }
|
||||||
|
|
||||||
|
@ -181,12 +185,16 @@ describe ApplicationController, type: :controller do
|
||||||
it { expect(@controller).not_to have_received(:redirect_to) }
|
it { expect(@controller).not_to have_received(:redirect_to) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the feature is activated' do
|
context 'when the feature is activated' do
|
||||||
before do
|
before do
|
||||||
Flipflop::FeatureSet.current.test!.switch!(:enable_email_login_token, true)
|
Flipflop::FeatureSet.current.test!.switch!(:enable_email_login_token, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the ip is untrusted' do
|
||||||
|
let(:ip_trusted) { false }
|
||||||
|
|
||||||
context 'when the device is not trusted' do
|
context 'when the device is not trusted' do
|
||||||
let(:trusted_device) { false }
|
let(:trusted_device) { false }
|
||||||
|
|
||||||
|
@ -197,6 +205,19 @@ describe ApplicationController, type: :controller do
|
||||||
it { expect(@controller).to have_received(:store_location_for) }
|
it { expect(@controller).to have_received(:store_location_for) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the ip is trusted' do
|
||||||
|
let(:ip_trusted) { true }
|
||||||
|
|
||||||
|
context 'when the device is not trusted' do
|
||||||
|
let(:trusted_device) { false }
|
||||||
|
|
||||||
|
before { subject }
|
||||||
|
|
||||||
|
it { expect(@controller).not_to have_received(:redirect_to) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -76,15 +76,15 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
||||||
page.refresh
|
page.refresh
|
||||||
expect(page).to have_current_path(champs_procedure_path(Procedure.last))
|
expect(page).to have_current_path(champs_procedure_path(Procedure.last))
|
||||||
|
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#champ-0-libelle')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
fill_in 'champ-0-libelle', with: 'libelle de champ'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
within '.buttons' do
|
within '.buttons' do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
|
expect(page).to have_selector('#champ-1-libelle')
|
||||||
|
|
||||||
click_on Procedure.last.libelle
|
click_on Procedure.last.libelle
|
||||||
click_on 'onglet-pieces'
|
click_on 'onglet-pieces'
|
||||||
|
@ -105,7 +105,7 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
||||||
scenario 'After adding champ and file, make publication' do
|
scenario 'After adding champ and file, make publication' do
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
fill_in 'champ-0-libelle', with: 'libelle de champ'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ feature 'As an administrateur I can edit types de champ', js: true do
|
||||||
within '.buttons' do
|
within '.buttons' do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#champ-0-libelle')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
|
@ -35,24 +35,24 @@ feature 'As an administrateur I can edit types de champ', js: true do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
expect(page).not_to have_content('Formulaire enregistré')
|
page.refresh
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0'
|
fill_in 'champ-0-libelle', with: 'libellé de champ 0'
|
||||||
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
|
fill_in 'champ-1-libelle', with: 'libellé de champ 1'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#champ-0-libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
|
expect(page).to have_selector('#champ-1-libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle')
|
expect(page).to have_selector('#champ-2-libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
|
expect(page).to have_selector('#champ-3-libelle')
|
||||||
|
|
||||||
within '.draggable-item-2' do
|
within '.type-de-champ[data-index="2"]' do
|
||||||
click_on 'Supprimer'
|
click_on 'Supprimer'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
|
expect(page).not_to have_selector('#champ-3-libelle')
|
||||||
fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
|
fill_in 'champ-2-libelle', with: 'libellé de champ 2'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
|
@ -64,44 +64,45 @@ feature 'As an administrateur I can edit types de champ', js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Remove champs" do
|
it "Remove champs" do
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
click_on 'Supprimer'
|
click_on 'Supprimer'
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
expect(page).not_to have_content('Supprimer')
|
expect(page).to have_content('Supprimer', count: 1)
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
expect(page).to have_content('Supprimer', count: 1)
|
expect(page).to have_content('Supprimer', count: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Only add valid champs" do
|
it "Only add valid champs" do
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description')
|
expect(page).to have_selector('#champ-0-description')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
|
fill_in 'champ-0-libelle', with: ''
|
||||||
|
fill_in 'champ-0-description', with: 'déscription du champ'
|
||||||
blur
|
blur
|
||||||
expect(page).not_to have_content('Formulaire enregistré')
|
expect(page).not_to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||||
blur
|
blur
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Add repetition champ" do
|
it "Add repetition champ" do
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#champ-0-libelle')
|
||||||
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ')
|
select('Bloc répétable', from: 'champ-0-type_champ')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'champ-0-libelle', with: 'libellé de champ'
|
||||||
blur
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
within '.flex-grow' do
|
within '.type-de-champ .repetition' do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1'
|
fill_in 'repetition-0-champ-0-libelle', with: 'libellé de champ 1'
|
||||||
blur
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
@ -111,16 +112,16 @@ feature 'As an administrateur I can edit types de champ', js: true do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
|
|
||||||
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ')
|
select('Bloc répétable', from: 'champ-0-type_champ')
|
||||||
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
|
fill_in 'champ-0-libelle', with: 'libellé de champ 2'
|
||||||
blur
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Supprimer', count: 3)
|
expect(page).to have_content('Supprimer', count: 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Add carte champ" do
|
it "Add carte champ" do
|
||||||
select('Carte', from: 'procedure_types_de_champ_attributes_0_type_champ')
|
select('Carte', from: 'champ-0-type_champ')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ carte'
|
fill_in 'champ-0-libelle', with: 'libellé de champ carte'
|
||||||
blur
|
blur
|
||||||
check 'Quartiers prioritaires'
|
check 'Quartiers prioritaires'
|
||||||
expect(page).to have_content('Formulaire enregistré')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
|
@ -819,4 +819,80 @@ describe Procedure do
|
||||||
it { expect(procedure.usual_instruction_time).to be_nil }
|
it { expect(procedure.usual_instruction_time).to be_nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#move_type_de_champ' do
|
||||||
|
let(:procedure) { create(:procedure) }
|
||||||
|
|
||||||
|
context 'type_de_champ' do
|
||||||
|
let(:type_de_champ) { create(:type_de_champ_text, order_place: 0, procedure: procedure) }
|
||||||
|
let!(:type_de_champ1) { create(:type_de_champ_text, order_place: 1, procedure: procedure) }
|
||||||
|
let!(:type_de_champ2) { create(:type_de_champ_text, order_place: 2, procedure: procedure) }
|
||||||
|
|
||||||
|
it 'move down' do
|
||||||
|
procedure.move_type_de_champ(type_de_champ, 2)
|
||||||
|
|
||||||
|
type_de_champ.reload
|
||||||
|
procedure.reload
|
||||||
|
|
||||||
|
expect(procedure.types_de_champ.index(type_de_champ)).to eq(2)
|
||||||
|
expect(type_de_champ.order_place).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'repetition' do
|
||||||
|
let!(:type_de_champ_repetition) do
|
||||||
|
create(:type_de_champ_repetition, types_de_champ: [
|
||||||
|
type_de_champ,
|
||||||
|
type_de_champ1,
|
||||||
|
type_de_champ2
|
||||||
|
], procedure: procedure)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'move down' do
|
||||||
|
procedure.move_type_de_champ(type_de_champ, 2)
|
||||||
|
|
||||||
|
type_de_champ.reload
|
||||||
|
procedure.reload
|
||||||
|
|
||||||
|
expect(type_de_champ.parent.types_de_champ.index(type_de_champ)).to eq(2)
|
||||||
|
expect(type_de_champ.order_place).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'private' do
|
||||||
|
let!(:type_de_champ_repetition) do
|
||||||
|
create(:type_de_champ_repetition, types_de_champ: [
|
||||||
|
type_de_champ,
|
||||||
|
type_de_champ1,
|
||||||
|
type_de_champ2
|
||||||
|
], private: true, procedure: procedure)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'move down' do
|
||||||
|
procedure.move_type_de_champ(type_de_champ, 2)
|
||||||
|
|
||||||
|
type_de_champ.reload
|
||||||
|
procedure.reload
|
||||||
|
|
||||||
|
expect(type_de_champ.parent.types_de_champ.index(type_de_champ)).to eq(2)
|
||||||
|
expect(type_de_champ.order_place).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'private' do
|
||||||
|
let(:type_de_champ) { create(:type_de_champ_text, order_place: 0, private: true, procedure: procedure) }
|
||||||
|
let!(:type_de_champ1) { create(:type_de_champ_text, order_place: 1, private: true, procedure: procedure) }
|
||||||
|
let!(:type_de_champ2) { create(:type_de_champ_text, order_place: 2, private: true, procedure: procedure) }
|
||||||
|
|
||||||
|
it 'move down' do
|
||||||
|
procedure.move_type_de_champ(type_de_champ, 2)
|
||||||
|
|
||||||
|
type_de_champ.reload
|
||||||
|
procedure.reload
|
||||||
|
|
||||||
|
expect(procedure.types_de_champ_private.index(type_de_champ)).to eq(2)
|
||||||
|
expect(type_de_champ.order_place).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
49
spec/services/ip_service_spec.rb
Normal file
49
spec/services/ip_service_spec.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe IPService do
|
||||||
|
describe '.ip_trusted?' do
|
||||||
|
subject { IPService.ip_trusted?(ip) }
|
||||||
|
|
||||||
|
context 'when the ip is nil' do
|
||||||
|
let(:ip) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to be(false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the ip is defined' do
|
||||||
|
let(:ip) { '192.168.1.10' }
|
||||||
|
|
||||||
|
context 'when it belongs to a trusted network' do
|
||||||
|
before do
|
||||||
|
ENV['TRUSTED_NETWORKS'] = '10.0.0.0/8 192.168.0.0/16 bad_network'
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be(true) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it does not belong to a trusted network' do
|
||||||
|
before do
|
||||||
|
ENV['TRUSTED_NETWORKS'] = '10.0.0.0/8'
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a trusted network is defined' do
|
||||||
|
before { ENV['TRUSTED_NETWORKS'] = '10.0.0.0/8' }
|
||||||
|
|
||||||
|
context 'when the ip is nil' do
|
||||||
|
let(:ip) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to be(false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the ip is badly formatted' do
|
||||||
|
let(:ip) { 'yop' }
|
||||||
|
|
||||||
|
it { is_expected.to be(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue