Merge branch 'master' of https://github.com/ONLYOFFICE/CommunityServer-AspNetCore
This commit is contained in:
commit
154965b1ba
1
build/install/snap/.gitignore
vendored
Normal file
1
build/install/snap/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
!*/
|
7
build/install/snap/run.sh
Normal file
7
build/install/snap/run.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -dfr parts
|
||||
rm -dfr prime
|
||||
rm -dfr stage
|
||||
|
||||
#snapcraft
|
4
build/install/snap/snap/.snapcraft/state
Normal file
4
build/install/snap/snap/.snapcraft/state
Normal file
@ -0,0 +1,4 @@
|
||||
!GlobalState
|
||||
assets:
|
||||
build-packages: []
|
||||
build-snaps: []
|
Binary file not shown.
27
build/install/snap/snap/plugins/x-redis.py
Normal file
27
build/install/snap/snap/plugins/x-redis.py
Normal file
@ -0,0 +1,27 @@
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import snapcraft
|
||||
from snapcraft.plugins import make
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RedisPlugin(make.MakePlugin):
|
||||
|
||||
def build(self):
|
||||
super(make.MakePlugin, self).build()
|
||||
|
||||
command = ['make']
|
||||
|
||||
if self.options.makefile:
|
||||
command.extend(['-f', self.options.makefile])
|
||||
|
||||
if self.options.make_parameters:
|
||||
command.extend(self.options.make_parameters)
|
||||
|
||||
self.run(command + ['-j{}'.format(self.project.parallel_build_count)])
|
||||
self.run(command + ['install', 'PREFIX=' + self.installdir])
|
153
build/install/snap/snap/snapcraft.yaml
Normal file
153
build/install/snap/snap/snapcraft.yaml
Normal file
@ -0,0 +1,153 @@
|
||||
name: onlyoffice-communityserver
|
||||
version: "10.0.0"
|
||||
summary: ""
|
||||
description: ""
|
||||
grade: stable
|
||||
confinement: devmode
|
||||
|
||||
apps:
|
||||
nginx:
|
||||
command: start_nginx
|
||||
daemon: simple
|
||||
restart-condition: always
|
||||
plugs: [network, network-bind]
|
||||
mysql:
|
||||
command: start_mysql
|
||||
stop-command: support-files/mysql.server stop
|
||||
daemon: simple
|
||||
restart-condition: always
|
||||
plugs: [network, network-bind]
|
||||
mysql-client:
|
||||
command: mysql --defaults-file=$SNAP_DATA/mysql/root.ini
|
||||
plugs: [network, network-bind]
|
||||
mysqldump:
|
||||
command: mysqldump --defaults-file=$SNAP_DATA/mysql/root.ini --lock-tables onlyoffice
|
||||
plugs: [network, network-bind]
|
||||
hooks:
|
||||
configure:
|
||||
plugs: [network, network-bind]
|
||||
parts:
|
||||
python:
|
||||
plugin: python
|
||||
python-version: python3
|
||||
|
||||
node:
|
||||
plugin: nodejs
|
||||
node-engine: 12.9.1
|
||||
|
||||
nginx:
|
||||
plugin: autotools
|
||||
source: https://github.com/nginx/nginx.git
|
||||
source-type: git
|
||||
# Need the prepare step because configure script resides in an unintuitive
|
||||
# location.
|
||||
override-build: |
|
||||
cp auto/configure .
|
||||
snapcraftctl build
|
||||
build-packages:
|
||||
- libpcre3
|
||||
- libpcre3-dev
|
||||
- zlib1g-dev
|
||||
stage:
|
||||
# Remove scripts that we'll be replacing with our own
|
||||
- -conf/nginx.conf
|
||||
|
||||
nginx-customizations:
|
||||
plugin: dump
|
||||
source: src/nginx/
|
||||
|
||||
# Download the boost headers for MySQL. Note that the version used may need to
|
||||
# be updated if the version of MySQL changes.
|
||||
boost:
|
||||
plugin: dump
|
||||
source: https://github.com/kyrofa/boost_tarball/raw/master/boost_1_59_0.tar.gz
|
||||
source-checksum: sha1/5123209db194d66d69a9cfa5af8ff473d5941d97
|
||||
# When building MySQL, the headers in the source directory 'boost/' are
|
||||
# required. Previously, using the 'copy' plugin, the whole archive was put
|
||||
# under 'boost/', making the headers reside in 'boost/boost/'. Due to a bug,
|
||||
# we now only stage the 'boost/' directory without moving it.
|
||||
#
|
||||
# Bug: https://bugs.launchpad.net/snapcraft/+bug/1757093
|
||||
stage:
|
||||
- boost/
|
||||
prime:
|
||||
- -*
|
||||
|
||||
mysql:
|
||||
plugin: cmake
|
||||
source: https://github.com/mysql/mysql-server.git
|
||||
source-tag: mysql-5.7.22
|
||||
source-depth: 1
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
git apply $SNAPCRAFT_STAGE/support-compile-time-disabling-of-setpriority.patch
|
||||
after: [boost, patches]
|
||||
configflags:
|
||||
- -DWITH_BOOST=$SNAPCRAFT_STAGE
|
||||
- -DWITH_INNODB_PAGE_CLEANER_PRIORITY=OFF
|
||||
- -DCMAKE_INSTALL_PREFIX=/
|
||||
- -DBUILD_CONFIG=mysql_release
|
||||
- -DWITH_UNIT_TESTS=OFF
|
||||
- -DWITH_EMBEDDED_SERVER=OFF
|
||||
- -DWITH_ARCHIVE_STORAGE_ENGINE=OFF
|
||||
- -DWITH_BLACKHOLE_STORAGE_ENGINE=OFF
|
||||
- -DWITH_FEDERATED_STORAGE_ENGINE=OFF
|
||||
- -DWITH_PARTITION_STORAGE_ENGINE=OFF
|
||||
- -DINSTALL_MYSQLTESTDIR=
|
||||
build-packages:
|
||||
- wget
|
||||
- g++
|
||||
- cmake
|
||||
- bison
|
||||
- libncurses5-dev
|
||||
- libaio-dev
|
||||
stage:
|
||||
# Remove scripts that we'll be replacing with our own
|
||||
- -support-files/mysql.server
|
||||
- -COPYING
|
||||
prime:
|
||||
# Remove scripts that we'll be replacing with our own
|
||||
- -support-files/mysql.server
|
||||
|
||||
# Remove unused binaries that waste space
|
||||
- -bin/innochecksum
|
||||
- -bin/lz4_decompress
|
||||
- -bin/myisam*
|
||||
- -bin/mysqladmin
|
||||
- -bin/mysqlbinlog
|
||||
- -bin/mysql_client_test
|
||||
- -bin/mysql_config*
|
||||
- -bin/mysqld_multi
|
||||
- -bin/mysqlimport
|
||||
- -bin/mysql_install_db
|
||||
- -bin/mysql_plugin
|
||||
- -bin/mysqlpump
|
||||
- -bin/mysql_secure_installation
|
||||
- -bin/mysqlshow
|
||||
- -bin/mysqlslap
|
||||
- -bin/mysql_ssl_rsa_setup
|
||||
- -bin/mysqltest
|
||||
- -bin/mysql_tzinfo_to_sql
|
||||
- -bin/perror
|
||||
- -bin/replace
|
||||
- -bin/resolveip
|
||||
- -bin/resolve_stack_dump
|
||||
- -bin/zlib_decompress
|
||||
|
||||
# Copy over our MySQL scripts
|
||||
mysql-customizations:
|
||||
plugin: dump
|
||||
source: src/mysql/
|
||||
|
||||
patches:
|
||||
source: src/patches
|
||||
plugin: dump
|
||||
prime:
|
||||
- -*
|
||||
|
||||
|
||||
hooks:
|
||||
plugin: dump
|
||||
source: src/hooks/
|
||||
organize:
|
||||
bin/: snap/hooks/
|
10
build/install/snap/src/hooks/bin/configure
vendored
Executable file
10
build/install/snap/src/hooks/bin/configure
vendored
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck source=src/hooks/utilities/hook-utilities
|
||||
. "$SNAP/utilities/hook-utilities"
|
||||
|
||||
# Signal to services that the configure hook is running. Useful to ensure
|
||||
# services don't restart until the configuration transaction has completed.
|
||||
set_configure_hook_running
|
||||
trap 'set_configure_hook_not_running' EXIT
|
||||
|
32
build/install/snap/src/hooks/utilities/hook-utilities
Executable file
32
build/install/snap/src/hooks/utilities/hook-utilities
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
|
||||
CONFIGURE_LOCKFILE="/tmp/locks/configure-hook"
|
||||
|
||||
mkdir -p "$(dirname $CONFIGURE_LOCKFILE)"
|
||||
chmod 750 "$(dirname $CONFIGURE_LOCKFILE)"
|
||||
|
||||
configure_hook_running()
|
||||
{
|
||||
[ -f "$CONFIGURE_LOCKFILE" ]
|
||||
}
|
||||
|
||||
set_configure_hook_running()
|
||||
{
|
||||
touch "$CONFIGURE_LOCKFILE"
|
||||
}
|
||||
|
||||
set_configure_hook_not_running()
|
||||
{
|
||||
rm -f "$CONFIGURE_LOCKFILE"
|
||||
}
|
||||
|
||||
wait_for_configure_hook()
|
||||
{
|
||||
if configure_hook_running; then
|
||||
printf "Waiting for configure hook... "
|
||||
while configure_hook_running; do
|
||||
sleep 1
|
||||
done
|
||||
printf "done\n"
|
||||
fi
|
||||
}
|
97
build/install/snap/src/mysql/bin/start_mysql
Executable file
97
build/install/snap/src/mysql/bin/start_mysql
Executable file
@ -0,0 +1,97 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck source=src/mysql/utilities/mysql-utilities
|
||||
. "$SNAP/utilities/mysql-utilities"
|
||||
|
||||
root_option_file="$SNAP_DATA/mysql/root.ini"
|
||||
new_install=false
|
||||
|
||||
# Make sure the database is initialized (this is safe to run if already
|
||||
# initialized)
|
||||
if mysqld --initialize-insecure --basedir="$SNAP" --datadir="$SNAP_DATA/mysql" --lc-messages-dir="$SNAP/share"; then
|
||||
new_install=true
|
||||
fi
|
||||
|
||||
set_mysql_setup_running
|
||||
|
||||
# Start mysql
|
||||
"$SNAP/support-files/mysql.server" start
|
||||
|
||||
# Initialize new installation if necessary.
|
||||
if [ $new_install = true ]; then
|
||||
# Generate a password for the root mysql user.
|
||||
printf "Generating root mysql password... "
|
||||
root_password="$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c64)"
|
||||
printf "done\n"
|
||||
|
||||
# Generate a password for the onlyoffice mysql user.
|
||||
printf "Generating onlyoffice mysql password... "
|
||||
onlyoffice_password="$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c64)"
|
||||
printf "done\n"
|
||||
|
||||
# Save root user information
|
||||
cat <<-EOF > "$root_option_file"
|
||||
[client]
|
||||
socket=$MYSQL_SOCKET
|
||||
user=root
|
||||
EOF
|
||||
chmod 600 "$root_option_file"
|
||||
|
||||
# Now set everything up in one step:
|
||||
# 1) Set the root user's password
|
||||
# 2) Create the onlyoffice user
|
||||
# 3) Create the onlyoffice database
|
||||
# 4) Grant the onlyoffice user privileges on the onlyoffice database
|
||||
printf "Setting up users and onlyoffice database... "
|
||||
if mysql --defaults-file="$root_option_file" <<-SQL
|
||||
ALTER USER 'root'@'localhost' IDENTIFIED BY '$root_password';
|
||||
CREATE USER 'onlyoffice'@'localhost' IDENTIFIED WITH mysql_native_password BY '$onlyoffice_password';
|
||||
CREATE DATABASE IF NOT EXISTS onlyoffice CHARACTER SET utf8 COLLATE 'utf8_general_ci';
|
||||
GRANT ALL PRIVILEGES ON onlyoffice.* TO 'onlyoffice'@'localhost' IDENTIFIED WITH mysql_native_password BY '$onlyoffice_password';
|
||||
SQL
|
||||
then
|
||||
printf "done\n"
|
||||
else
|
||||
echo "Failed to initialize-- reverting..."
|
||||
"$SNAP/support-files/mysql.server" stop
|
||||
rm -rf "$SNAP_DATA"/mysql/*
|
||||
fi
|
||||
|
||||
# Now the root mysql user has a password. Save that as well.
|
||||
echo "password=$root_password" >> "$root_option_file"
|
||||
else
|
||||
# Okay, this isn't a new installation. However, we recently changed
|
||||
# the location of MySQL's socket. Make sure the root
|
||||
# option file is updated to look there instead of the old location.
|
||||
sed -ri "s|(socket\s*=\s*)/var/snap/.*mysql.sock|\1$MYSQL_SOCKET|" "$root_option_file"
|
||||
fi
|
||||
|
||||
# Wait here until mysql is running
|
||||
wait_for_mysql -f
|
||||
|
||||
# Check and upgrade mysql tables if necessary. This will return 0 if the upgrade
|
||||
# succeeded, in which case we need to restart mysql.
|
||||
echo "Checking/upgrading mysql tables if necessary..."
|
||||
if mysql_upgrade --defaults-file="$root_option_file"; then
|
||||
echo "Restarting mysql server after upgrade..."
|
||||
"$SNAP/support-files/mysql.server" restart
|
||||
|
||||
# Wait for server to come back after upgrade
|
||||
wait_for_mysql -f
|
||||
fi
|
||||
|
||||
# If this was a new installation, wait until the server is all up and running
|
||||
# before saving off the onlyoffice user's password. This way the presence of the
|
||||
# file can be used as a signal that mysql is ready to be used.
|
||||
if [ $new_install = true ]; then
|
||||
mysql_set_onlyoffice_password "$onlyoffice_password"
|
||||
fi
|
||||
|
||||
set_mysql_setup_not_running
|
||||
|
||||
# Wait here until mysql exits (turn a forking service into simple). This is
|
||||
# only needed for Ubuntu Core 15.04, as 16.04 supports forking services.
|
||||
pid=$(mysql_pid)
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
8
build/install/snap/src/mysql/my.cnf
Normal file
8
build/install/snap/src/mysql/my.cnf
Normal file
@ -0,0 +1,8 @@
|
||||
[mysqld]
|
||||
user=root
|
||||
secure-file-priv=NULL
|
||||
skip-networking
|
||||
sql_mode = 'NO_ENGINE_SUBSTITUTION'
|
||||
max_connections = 1000
|
||||
max_allowed_packet = 1048576000
|
||||
group_concat_max_len = 2048
|
BIN
build/install/snap/src/mysql/support-files/.mysql.server.swp
Normal file
BIN
build/install/snap/src/mysql/support-files/.mysql.server.swp
Normal file
Binary file not shown.
315
build/install/snap/src/mysql/support-files/mysql.server
Executable file
315
build/install/snap/src/mysql/support-files/mysql.server
Executable file
@ -0,0 +1,315 @@
|
||||
#!/bin/sh
|
||||
# Copyright Abandoned 1996 TCX DataKonsult AB & Monty Program KB & Detron HB
|
||||
# This file is public domain and comes with NO WARRANTY of any kind
|
||||
|
||||
# MySQL daemon start/stop script.
|
||||
|
||||
# Usually this is put in /etc/init.d (at least on machines SYSV R4 based
|
||||
# systems) and linked to /etc/rc3.d/S99mysql and /etc/rc0.d/K01mysql.
|
||||
# When this is done the mysql server will be started when the machine is
|
||||
# started and shut down when the systems goes down.
|
||||
|
||||
# Comments to support chkconfig on RedHat Linux
|
||||
# chkconfig: 2345 64 36
|
||||
# description: A very fast and reliable SQL database engine.
|
||||
|
||||
# Comments to support LSB init script conventions
|
||||
### BEGIN INIT INFO
|
||||
# Provides: mysql
|
||||
# Required-Start: $local_fs $network $remote_fs
|
||||
# Should-Start: ypbind nscd ldap ntpd xntpd
|
||||
# Required-Stop: $local_fs $network $remote_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: start and stop MySQL
|
||||
# Description: MySQL is a very fast and reliable SQL database engine.
|
||||
### END INIT INFO
|
||||
|
||||
# If you install MySQL on some other places than /, then you
|
||||
# have to do one of the following things for this script to work:
|
||||
#
|
||||
# - Run this script from within the MySQL installation directory
|
||||
# - Create a /etc/my.cnf file with the following information:
|
||||
# [mysqld]
|
||||
# basedir=<path-to-mysql-installation-directory>
|
||||
# - Add the above to any other configuration file (for example ~/.my.ini)
|
||||
# and copy my_print_defaults to /usr/bin
|
||||
# - Add the path to the mysql-installation-directory to the basedir variable
|
||||
# below.
|
||||
#
|
||||
# If you want to affect other MySQL variables, you should make your changes
|
||||
# in the /etc/my.cnf, ~/.my.cnf or other MySQL configuration files.
|
||||
|
||||
# If you change base dir, you must also change datadir. These may get
|
||||
# overwritten by settings in the MySQL configuration files.
|
||||
|
||||
# shellcheck source=src/mysql/utilities/mysql-utilities
|
||||
. "$SNAP/utilities/mysql-utilities"
|
||||
|
||||
basedir="$SNAP"
|
||||
datadir="$SNAP_DATA/mysql"
|
||||
|
||||
# Default value, in seconds, afterwhich the script should timeout waiting
|
||||
# for server start.
|
||||
# Value here is overriden by value in my.cnf.
|
||||
# 0 means don't wait at all
|
||||
# Negative numbers mean to wait indefinitely
|
||||
service_startup_timeout=900
|
||||
|
||||
# Lock directory for RedHat / SuSE.
|
||||
lockdir="$SNAP_DATA/mysql/lock"
|
||||
lock_file_path="$lockdir/mysql"
|
||||
|
||||
# The following variables are only set for letting mysql.server find things.
|
||||
|
||||
# Set some defaults
|
||||
mysqld_pid_file_path="$MYSQL_PIDFILE"
|
||||
if test -z "$basedir"
|
||||
then
|
||||
basedir=/
|
||||
bindir=//bin
|
||||
if test -z "$datadir"
|
||||
then
|
||||
datadir=//data
|
||||
fi
|
||||
libexecdir=//bin
|
||||
else
|
||||
bindir="$basedir/bin"
|
||||
if test -z "$datadir"
|
||||
then
|
||||
datadir="$basedir/data"
|
||||
fi
|
||||
libexecdir="$basedir/libexec"
|
||||
fi
|
||||
|
||||
#
|
||||
# Use LSB init script functions for printing messages, if possible
|
||||
#
|
||||
lsb_functions="/lib/lsb/init-functions"
|
||||
if test -f $lsb_functions ; then
|
||||
. $lsb_functions
|
||||
else
|
||||
log_success_msg()
|
||||
{
|
||||
echo " SUCCESS! $*"
|
||||
}
|
||||
log_failure_msg()
|
||||
{
|
||||
echo " ERROR! $*"
|
||||
}
|
||||
fi
|
||||
|
||||
PATH="/sbin:/usr/sbin:/bin:/usr/bin:$basedir/bin"
|
||||
export PATH
|
||||
|
||||
mode=$1 # start or stop
|
||||
|
||||
[ $# -ge 1 ] && shift
|
||||
|
||||
|
||||
other_args="$*" # uncommon, but needed when called from an RPM upgrade action
|
||||
# Expected: "--skip-networking --skip-grant-tables"
|
||||
# They are not checked here, intentionally, as it is the resposibility
|
||||
# of the "spec" file author to give correct arguments only.
|
||||
|
||||
# Upstream mysql stuff, no need to fix this
|
||||
# shellcheck disable=SC2116,SC2039
|
||||
case "$(echo "testing\c")","$(echo -n testing)" in
|
||||
*c*,-n*) echo_n="" echo_c="" ;;
|
||||
*c*,*) echo_n=-n echo_c="" ;;
|
||||
*) echo_n="" echo_c='\c' ;;
|
||||
esac
|
||||
|
||||
wait_for_pid () {
|
||||
verb="$1" # created | removed
|
||||
pid="$2" # process ID of the program operating on the pid-file
|
||||
pid_file_path="$3" # path to the PID file.
|
||||
|
||||
i=0
|
||||
avoid_race_condition="by checking again"
|
||||
|
||||
while test "$i" -ne "$service_startup_timeout" ; do
|
||||
|
||||
case "$verb" in
|
||||
'created')
|
||||
# wait for a PID-file to pop into existence.
|
||||
test -s "$pid_file_path" && i='' && break
|
||||
;;
|
||||
'removed')
|
||||
# wait for this PID-file to disappear
|
||||
test ! -s "$pid_file_path" && i='' && break
|
||||
;;
|
||||
*)
|
||||
echo "wait_for_pid () usage: wait_for_pid created|removed pid pid_file_path"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# if server isn't running, then pid-file will never be updated
|
||||
if test -n "$pid"; then
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
: # the server still runs
|
||||
else
|
||||
# The server may have exited between the last pid-file check and now.
|
||||
if test -n "$avoid_race_condition"; then
|
||||
avoid_race_condition=""
|
||||
continue # Check again.
|
||||
fi
|
||||
|
||||
# there's nothing that will affect the file.
|
||||
log_failure_msg "The server quit without updating PID file ($pid_file_path)."
|
||||
return 1 # not waiting any more.
|
||||
fi
|
||||
fi
|
||||
|
||||
echo $echo_n ".$echo_c"
|
||||
i=$((i + 1))
|
||||
sleep 1
|
||||
|
||||
done
|
||||
|
||||
if test -z "$i" ; then
|
||||
log_success_msg
|
||||
return 0
|
||||
else
|
||||
log_failure_msg
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Set pid file if not given
|
||||
#
|
||||
if test -z "$mysqld_pid_file_path"
|
||||
then
|
||||
mysqld_pid_file_path="$datadir"/"$(hostname)".pid
|
||||
else
|
||||
case "$mysqld_pid_file_path" in
|
||||
/* ) ;;
|
||||
* ) mysqld_pid_file_path="$datadir/$mysqld_pid_file_path" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
case "$mode" in
|
||||
'start')
|
||||
# Start daemon
|
||||
|
||||
# Safeguard (relative paths, core dumps..)
|
||||
cd "$basedir" || exit
|
||||
|
||||
echo $echo_n "Starting MySQL"
|
||||
if test -x "$bindir/mysqld_safe"
|
||||
then
|
||||
# Give extra arguments to mysqld with the my.cnf file. This script
|
||||
# may be overwritten at next upgrade.
|
||||
"$bindir/mysqld_safe" --datadir="$datadir" --pid-file="$mysqld_pid_file_path" --lc-messages-dir="$SNAP/share" --socket="$MYSQL_SOCKET" "$other_args" >/dev/null 2>&1 &
|
||||
wait_for_pid created "$!" "$mysqld_pid_file_path"; return_value=$?
|
||||
|
||||
# Make lock for RedHat / SuSE
|
||||
if test -w "$lockdir"
|
||||
then
|
||||
touch "$lock_file_path"
|
||||
fi
|
||||
|
||||
exit $return_value
|
||||
else
|
||||
log_failure_msg "Couldn't find MySQL server ($bindir/mysqld_safe)"
|
||||
fi
|
||||
;;
|
||||
|
||||
'stop')
|
||||
# Stop daemon. We use a signal here to avoid having to know the
|
||||
# root password.
|
||||
|
||||
if test -s "$mysqld_pid_file_path"
|
||||
then
|
||||
# signal mysqld_safe that it needs to stop
|
||||
touch "$mysqld_pid_file_path.shutdown"
|
||||
|
||||
mysqld_pid="$(cat "$mysqld_pid_file_path")"
|
||||
|
||||
if (kill -0 "$mysqld_pid" 2>/dev/null)
|
||||
then
|
||||
echo $echo_n "Shutting down MySQL"
|
||||
kill "$mysqld_pid"
|
||||
# mysqld should remove the pid file when it exits, so wait for it.
|
||||
wait_for_pid removed "$mysqld_pid" "$mysqld_pid_file_path"; return_value=$?
|
||||
else
|
||||
log_failure_msg "MySQL server process #$mysqld_pid is not running!"
|
||||
rm "$mysqld_pid_file_path"
|
||||
fi
|
||||
|
||||
# Delete lock for RedHat / SuSE
|
||||
if test -f "$lock_file_path"
|
||||
then
|
||||
rm -f "$lock_file_path"
|
||||
fi
|
||||
exit $return_value
|
||||
else
|
||||
log_failure_msg "MySQL server PID file could not be found!"
|
||||
fi
|
||||
;;
|
||||
|
||||
'restart')
|
||||
# Stop the service and regardless of whether it was
|
||||
# running or not, start it again.
|
||||
if $0 stop "$other_args"; then
|
||||
$0 start "$other_args"
|
||||
else
|
||||
log_failure_msg "Failed to stop running server, so refusing to try to start."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
'reload'|'force-reload')
|
||||
if test -s "$mysqld_pid_file_path" ; then
|
||||
read -r mysqld_pid < "$mysqld_pid_file_path"
|
||||
kill -HUP "$mysqld_pid" && log_success_msg "Reloading service MySQL"
|
||||
touch "$mysqld_pid_file_path"
|
||||
else
|
||||
log_failure_msg "MySQL PID file could not be found!"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
'status')
|
||||
# First, check to see if pid file exists
|
||||
if test -s "$mysqld_pid_file_path" ; then
|
||||
read -r mysqld_pid < "$mysqld_pid_file_path"
|
||||
if kill -0 "$mysqld_pid" 2>/dev/null ; then
|
||||
log_success_msg "MySQL running ($mysqld_pid)"
|
||||
exit 0
|
||||
else
|
||||
log_failure_msg "MySQL is not running, but PID file exists"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Try to find appropriate mysqld process
|
||||
mysqld_pid="$(pidof "$libexecdir/mysqld")"
|
||||
|
||||
# test if multiple pids exist
|
||||
pid_count="$(echo "$mysqld_pid" | wc -w)"
|
||||
if test "$pid_count" -gt 1 ; then
|
||||
log_failure_msg "Multiple MySQL running but PID file could not be found ($mysqld_pid)"
|
||||
exit 5
|
||||
elif test -z "$mysqld_pid" ; then
|
||||
if test -f "$lock_file_path" ; then
|
||||
log_failure_msg "MySQL is not running, but lock file ($lock_file_path) exists"
|
||||
exit 2
|
||||
fi
|
||||
log_failure_msg "MySQL is not running"
|
||||
exit 3
|
||||
else
|
||||
log_failure_msg "MySQL is running but PID file could not be found"
|
||||
exit 4
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# usage
|
||||
basename="$(basename "$0")"
|
||||
echo "Usage: $basename {start|stop|restart|reload|force-reload|status} [ MySQL server options ]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
72
build/install/snap/src/mysql/utilities/mysql-utilities
Executable file
72
build/install/snap/src/mysql/utilities/mysql-utilities
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
|
||||
export MYSQL_PIDFILE="/tmp/pids/mysql.pid"
|
||||
export MYSQL_SOCKET="/tmp/sockets/mysql.sock"
|
||||
export ONLYOFFICE_PASSWORD_FILE="$SNAP_DATA/mysql/onlyoffice_password"
|
||||
MYSQL_SETUP_LOCKFILE="/tmp/locks/mysql-setup"
|
||||
|
||||
mkdir -p "$(dirname "$MYSQL_PIDFILE")"
|
||||
mkdir -p "$(dirname "$MYSQL_SOCKET")"
|
||||
chmod 750 "$(dirname "$MYSQL_PIDFILE")"
|
||||
chmod 750 "$(dirname "$MYSQL_SOCKET")"
|
||||
|
||||
mysql_is_running()
|
||||
{
|
||||
# Arguments:
|
||||
# -f: Force the check, i.e. ignore if it's currently in setup
|
||||
[ -f "$MYSQL_PIDFILE" ] && [ -S "$MYSQL_SOCKET" ] && (! mysql_setup_running || [ "$1" = "-f" ])
|
||||
}
|
||||
|
||||
wait_for_mysql()
|
||||
{
|
||||
# Arguments:
|
||||
# -f: Force the check, i.e. ignore if it's currently in setup
|
||||
if ! mysql_is_running "$@"; then
|
||||
printf "Waiting for MySQL... "
|
||||
while ! mysql_is_running "$@"; do
|
||||
sleep 1
|
||||
done
|
||||
printf "done\n"
|
||||
fi
|
||||
}
|
||||
|
||||
mysql_setup_running()
|
||||
{
|
||||
[ -f "$MYSQL_SETUP_LOCKFILE" ]
|
||||
}
|
||||
|
||||
set_mysql_setup_running()
|
||||
{
|
||||
touch "$MYSQL_SETUP_LOCKFILE"
|
||||
}
|
||||
|
||||
set_mysql_setup_not_running()
|
||||
{
|
||||
rm -f "$MYSQL_SETUP_LOCKFILE"
|
||||
}
|
||||
|
||||
mysql_pid()
|
||||
{
|
||||
if mysql_is_running; then
|
||||
cat "$MYSQL_PIDFILE"
|
||||
else
|
||||
echo "Unable to get MySQL PID as it's not yet running" >&2
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
mysql_set_onlyoffice_password()
|
||||
{
|
||||
echo "$1" > "$ONLYOFFICE_PASSWORD_FILE"
|
||||
chmod 600 "$ONLYOFFICE_PASSWORD_FILE"
|
||||
}
|
||||
|
||||
mysql_get_onlyoffice_password()
|
||||
{
|
||||
if [ -f "$ONLYOFFICE_PASSWORD_FILE" ]; then
|
||||
cat "$ONLYOFFICE_PASSWORD_FILE"
|
||||
else
|
||||
echo "MySQL ONLYOFFICE password has not yet been generated" >&2
|
||||
echo ""
|
||||
fi
|
||||
}
|
12
build/install/snap/src/nginx/bin/start_nginx
Executable file
12
build/install/snap/src/nginx/bin/start_nginx
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck source=src/nginx/utilities/nginx-utilities
|
||||
. "$SNAP/utilities/nginx-utilities"
|
||||
|
||||
cp -dfr ${SNAP}/config/nginx-config/* ${SNAP_DATA}/nginx/config
|
||||
|
||||
sed -e "s|\${SNAP}|$SNAP|;s|\${SNAP_DATA}|$SNAP_DATA|;s|\${NGINX_PIDFILE}|$NGINX_PIDFILE|;s|\${ONLYOFFICE_SOCKET}|$ONLYOFFICE_SOCKET|;s|\${ONLYOFFICE_API_SYSTEM_SOCKET}|$ONLYOFFICE_API_SYSTEM_SOCKET|" -i ${SNAP_DATA}/nginx/config/conf.d/onlyoffice
|
||||
sed -e "s|\${SNAP}|$SNAP|;s|\${SNAP_DATA}|$SNAP_DATA|;s|\${NGINX_PIDFILE}|$NGINX_PIDFILE|;s|\${ONLYOFFICE_SOCKET}|$ONLYOFFICE_SOCKET|;s|\${ONLYOFFICE_API_SYSTEM_SOCKET}|$ONLYOFFICE_API_SYSTEM_SOCKET|" -i ${SNAP_DATA}/nginx/config/conf.d/includes/onlyoffice-communityserver-common.conf
|
||||
sed -e "s|\${SNAP}|$SNAP|;s|\${SNAP_DATA}|$SNAP_DATA|;s|\${NGINX_PIDFILE}|$NGINX_PIDFILE|;s|\${ONLYOFFICE_SOCKET}|$ONLYOFFICE_SOCKET|;s|\${ONLYOFFICE_API_SYSTEM_SOCKET}|$ONLYOFFICE_API_SYSTEM_SOCKET|" -i ${SNAP_DATA}/nginx/config/nginx.conf
|
||||
|
||||
exec "$SNAP/sbin/nginx" "-c" "$SNAP_DATA/nginx/config/nginx.conf" "-p" "$SNAP_DATA/nginx" "-g" "daemon off;" "$@"
|
@ -0,0 +1,43 @@
|
||||
upstream fastcgi_backend {
|
||||
server unix:/var/run/onlyoffice/onlyoffice.socket;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
fastcgi_keep_conn on;
|
||||
fastcgi_index Default.aspx;
|
||||
fastcgi_intercept_errors on;
|
||||
|
||||
|
||||
include fastcgi_params;
|
||||
|
||||
fastcgi_param HTTP_X_REWRITER_URL $http_x_rewriter_url;
|
||||
fastcgi_param SERVER_NAME $host;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO "";
|
||||
|
||||
fastcgi_read_timeout 600;
|
||||
fastcgi_send_timeout 600;
|
||||
|
||||
|
||||
location / {
|
||||
root /var/www/onlyoffice/WebStudio/;
|
||||
expires 0;
|
||||
add_header Cache-Control no-cache;
|
||||
rewrite ^(.*)$ /StartConfigure.htm break;
|
||||
}
|
||||
|
||||
location /api {
|
||||
fastcgi_pass fastcgi_backend;
|
||||
break;
|
||||
}
|
||||
|
||||
location ~* ^/(warmup[2-9]?)/ {
|
||||
rewrite /warmup([^/]*)/(.*) /$2 break;
|
||||
fastcgi_pass unix:/var/run/onlyoffice/onlyoffice$1.socket;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,129 @@
|
||||
upstream fastcgi_backend_apisystem {
|
||||
server unix:/var/run/onlyoffice/onlyofficeApiSystem.socket;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream fastcgi_backend {
|
||||
server unix:/var/run/onlyoffice/onlyoffice.socket;
|
||||
keepalive {{ONLYOFFICE_NIGNX_KEEPLIVE}};
|
||||
}
|
||||
|
||||
fastcgi_cache_path /var/cache/nginx/onlyoffice
|
||||
levels=1:2
|
||||
keys_zone=onlyoffice:16m
|
||||
max_size=256m
|
||||
inactive=1d;
|
||||
|
||||
geo $ip_external {
|
||||
default 1;
|
||||
{{DOCKER_ONLYOFFICE_SUBNET}} 0;
|
||||
127.0.0.1 0;
|
||||
}
|
||||
|
||||
map $http_host $this_host {
|
||||
"" $host;
|
||||
default $http_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $the_scheme {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $the_host {
|
||||
default $http_x_forwarded_host;
|
||||
"" $this_host;
|
||||
}
|
||||
|
||||
## Normal HTTP host
|
||||
server {
|
||||
listen 0.0.0.0:80;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
root /nowhere; ## root doesn't have to be a valid path since we are redirecting
|
||||
|
||||
location / {
|
||||
if ($ip_external) {
|
||||
## Redirects all traffic to the HTTPS host
|
||||
rewrite ^ https://$host$request_uri? permanent;
|
||||
}
|
||||
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_pass https://127.0.0.1;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_ssl_verify off;
|
||||
}
|
||||
}
|
||||
|
||||
## HTTPS host
|
||||
server {
|
||||
listen 0.0.0.0:443 ssl;
|
||||
listen [::]:443 ssl default_server;
|
||||
server_tokens off;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
## Increase this if you want to upload large attachments
|
||||
client_max_body_size 100m;
|
||||
|
||||
## Strong SSL Security
|
||||
## https://cipherli.st/
|
||||
ssl on;
|
||||
ssl_certificate {{SSL_CERTIFICATE_PATH}};
|
||||
ssl_certificate_key {{SSL_KEY_PATH}};
|
||||
ssl_verify_client {{SSL_VERIFY_CLIENT}};
|
||||
ssl_client_certificate {{CA_CERTIFICATES_PATH}};
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off; # Requires nginx >= 1.5.9
|
||||
|
||||
add_header Strict-Transport-Security "max-age={{ONLYOFFICE_HTTPS_HSTS_MAXAGE}}; includeSubDomains; preload" always;
|
||||
# add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
|
||||
## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
|
||||
## Replace with your ssl_trusted_certificate. For more info see:
|
||||
## - https://medium.com/devops-programming/4445f4862461
|
||||
## - https://www.ruby-forum.com/topic/4419319
|
||||
## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
ssl_trusted_certificate {{SSL_OCSP_CERTIFICATE_PATH}};
|
||||
resolver 8.8.8.8 8.8.4.4 127.0.0.11 valid=300s; # Can change to your DNS resolver if desired
|
||||
resolver_timeout 10s;
|
||||
|
||||
## [Optional] Generate a stronger DHE parameter:
|
||||
## cd /etc/ssl/certs
|
||||
## sudo openssl dhparam -out dhparam.pem 4096
|
||||
##
|
||||
ssl_dhparam {{SSL_DHPARAM_PATH}};
|
||||
|
||||
large_client_header_buffers 4 16k;
|
||||
|
||||
set $X_REWRITER_URL $the_scheme://$the_host;
|
||||
|
||||
if ($http_x_rewriter_url != '') {
|
||||
set $X_REWRITER_URL $http_x_rewriter_url ;
|
||||
}
|
||||
|
||||
|
||||
include /etc/nginx/includes/onlyoffice-communityserver-*.conf;
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,83 @@
|
||||
location / {
|
||||
root ${SNAP}/var/www/onlyoffice/WebStudio/;
|
||||
index index.html index.htm default.aspx Default.aspx;
|
||||
|
||||
client_max_body_size 4G;
|
||||
|
||||
fastcgi_pass fastcgi_backend;
|
||||
fastcgi_keep_conn on;
|
||||
|
||||
error_page 404 /404.htm;
|
||||
|
||||
gzip off;
|
||||
gzip_comp_level 2;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/html application/x-javascript text/css application/xml;
|
||||
|
||||
fastcgi_index Default.aspx;
|
||||
fastcgi_intercept_errors on;
|
||||
|
||||
include ${SNAP}/conf/fastcgi_params;
|
||||
|
||||
fastcgi_param HTTP_X_REWRITER_URL $X_REWRITER_URL;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO "";
|
||||
|
||||
fastcgi_read_timeout 600;
|
||||
fastcgi_send_timeout 600;
|
||||
|
||||
location ~* (^\/(?:skins|products|addons).*\.(?:jpg|jpeg|gif|png|svg|ico)$)|(.*bundle/(?!clientscript).*) {
|
||||
fastcgi_pass fastcgi_backend;
|
||||
|
||||
fastcgi_temp_path ${SNAP_DATA}/nginx/cache/tmp 1 2;
|
||||
fastcgi_cache onlyoffice;
|
||||
fastcgi_cache_key "$scheme|$request_method|$host|$request_uri|$query_string";
|
||||
fastcgi_cache_use_stale updating error timeout invalid_header http_500;
|
||||
fastcgi_cache_valid 1d;
|
||||
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
|
||||
|
||||
add_header X-Fastcgi-Cache $upstream_cache_status;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
expires max;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
location /apisystem {
|
||||
rewrite /apisystem(.*) /$1 break;
|
||||
|
||||
root ${SNAP}/var/www/onlyoffice/ApiSystem/;
|
||||
index index.html index.htm default.aspx Default.aspx;
|
||||
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header X-Frame-Options DENY;
|
||||
|
||||
client_max_body_size 4G;
|
||||
|
||||
fastcgi_keep_conn on;
|
||||
fastcgi_pass fastcgi_backend_apisystem;
|
||||
|
||||
include ${SNAP}/conf/fastcgi_params;
|
||||
|
||||
set $X_REWRITER_URL $scheme://$http_host;
|
||||
|
||||
if ($http_x_rewriter_url != '') {
|
||||
set $X_REWRITER_URL $http_x_rewriter_url ;
|
||||
}
|
||||
|
||||
|
||||
fastcgi_param HTTP_X_REWRITER_URL $X_REWRITER_URL;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO "";
|
||||
|
||||
fastcgi_read_timeout 600;
|
||||
fastcgi_send_timeout 600;
|
||||
}
|
||||
|
||||
location /filesData {
|
||||
rewrite /filesData/var/www/onlyoffice/Data/Products/Files(.*) /$1 break;
|
||||
root ${SNAP_DATA}/onlyoffice/Data/Products/Files;
|
||||
internal;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
upstream fastcgi_backend_apisystem {
|
||||
server unix:/var/run/onlyoffice/onlyofficeApiSystem.socket;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream fastcgi_backend {
|
||||
server unix:/var/run/onlyoffice/onlyoffice.socket;
|
||||
keepalive {{ONLYOFFICE_NIGNX_KEEPLIVE}};
|
||||
}
|
||||
|
||||
fastcgi_cache_path /var/cache/nginx/onlyoffice
|
||||
levels=1:2
|
||||
keys_zone=onlyoffice:16m
|
||||
max_size=256m
|
||||
inactive=1d;
|
||||
|
||||
map $http_host $this_host {
|
||||
"" $host;
|
||||
default $http_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $the_scheme {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $the_host {
|
||||
default $http_x_forwarded_host;
|
||||
"" $this_host;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
|
||||
large_client_header_buffers 4 16k;
|
||||
|
||||
set $X_REWRITER_URL $the_scheme://$the_host;
|
||||
|
||||
if ($http_x_rewriter_url != '') {
|
||||
set $X_REWRITER_URL $http_x_rewriter_url ;
|
||||
}
|
||||
|
||||
include /etc/nginx/includes/onlyoffice-communityserver-*.conf;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
location /.well-known/acme-challenge {
|
||||
root /var/www/onlyoffice/Data/certs/;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
location /controlpanel {
|
||||
proxy_pass http://{{CONTROL_PANEL_HOST_ADDR}};
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
|
||||
}
|
||||
|
||||
location /sso/ {
|
||||
proxy_pass http://{{SERVICE_SSO_AUTH_HOST_ADDR}}:9834;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
|
||||
|
||||
proxy_redirect / /;
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
location ~* ^/ds-vpath/ {
|
||||
rewrite /ds-vpath/(.*) /$1 break;
|
||||
proxy_pass {{DOCUMENT_SERVER_HOST_ADDR}};
|
||||
proxy_redirect off;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $the_host/ds-vpath;
|
||||
proxy_set_header X-Forwarded-Proto $the_scheme;
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
location /addons/talk/http-poll/httppoll.ashx {
|
||||
proxy_pass http://localhost:5280/http-poll/;
|
||||
proxy_buffering off;
|
||||
client_max_body_size 10m;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
}
|
||||
|
||||
|
||||
location /socketio {
|
||||
rewrite /socketio/(.*) /$1 break;
|
||||
proxy_pass http://localhost:9899;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto "http";
|
||||
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
|
||||
}
|
||||
|
||||
|
||||
location /healthcheck {
|
||||
rewrite /healthcheck(.*) /$1 break;
|
||||
proxy_pass http://localhost:9810;
|
||||
proxy_redirect ~*/(.*) /healthcheck/$1;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
upstream fastcgi_backend_apisystem {
|
||||
server unix:${ONLYOFFICE_API_SYSTEM_SOCKET};
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream fastcgi_backend {
|
||||
server unix:${ONLYOFFICE_SOCKET};
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
fastcgi_cache_path ${SNAP_DATA}/nginx/cache/onlyoffice
|
||||
levels=1:2
|
||||
keys_zone=onlyoffice:16m
|
||||
max_size=256m
|
||||
inactive=1d;
|
||||
|
||||
map $http_host $this_host {
|
||||
"" $host;
|
||||
default $http_host;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $the_scheme {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $the_host {
|
||||
default $http_x_forwarded_host;
|
||||
"" $this_host;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
|
||||
large_client_header_buffers 4 16k;
|
||||
|
||||
set $X_REWRITER_URL $the_scheme://$the_host;
|
||||
|
||||
if ($http_x_rewriter_url != '') {
|
||||
set $X_REWRITER_URL $http_x_rewriter_url ;
|
||||
}
|
||||
|
||||
include ${SNAP_DATA}/nginx/config/conf.d/includes/onlyoffice-communityserver-*.conf;
|
||||
}
|
30
build/install/snap/src/nginx/config/nginx-config/nginx.conf
Normal file
30
build/install/snap/src/nginx/config/nginx-config/nginx.conf
Normal file
@ -0,0 +1,30 @@
|
||||
user root;
|
||||
worker_processes 2;
|
||||
|
||||
error_log ${SNAP_DATA}/nginx/logs/error.log warn;
|
||||
pid ${NGINX_PIDFILE};
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1048576;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include ${SNAP}/conf/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log ${SNAP_DATA}/nginx/logs/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
include ${SNAP_DATA}/nginx/config/conf.d/onlyoffice;
|
||||
}
|
17
build/install/snap/src/nginx/utilities/nginx-utilities
Executable file
17
build/install/snap/src/nginx/utilities/nginx-utilities
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
export NGINX_PIDFILE="/tmp/pids/nginx.pid"
|
||||
|
||||
mkdir -p "$(dirname "$NGINX_PIDFILE")"
|
||||
chmod 750 "$(dirname "$NGINX_PIDFILE")"
|
||||
|
||||
mkdir -p ${SNAP_DATA}/nginx/logs
|
||||
chmod 750 ${SNAP_DATA}/nginx/logs
|
||||
|
||||
mkdir -p ${SNAP_DATA}/nginx/cache
|
||||
chmod 750 ${SNAP_DATA}/nginx/cache
|
||||
|
||||
mkdir -p ${SNAP_DATA}/nginx/config
|
||||
chmod 750 ${SNAP_DATA}/nginx/config
|
||||
|
||||
|
87
build/install/snap/src/onlyoffice/bin/start_monoserve
Executable file
87
build/install/snap/src/onlyoffice/bin/start_monoserve
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
# shellcheck source=src/mysql/utilities/mysql-utilities
|
||||
. "$SNAP/utilities/mysql-utilities"
|
||||
|
||||
# shellcheck source=src/onlyoffice/utilities/monoserve-utilities
|
||||
. "${SNAP}/utilities/monoserve-utilities"
|
||||
|
||||
# shellcheck source=src/redis/utilities/redis-utilities
|
||||
. "$SNAP/utilities/redis-utilities"
|
||||
|
||||
|
||||
wait_for_redis
|
||||
wait_for_mysql
|
||||
|
||||
DB_NAME="onlyoffice";
|
||||
DB_HOST="localhost";
|
||||
DB_USER="onlyoffice";
|
||||
DB_PWD=$( mysql_get_onlyoffice_password );
|
||||
|
||||
|
||||
ONLYOFFICE_CORE_MACHINEKEY=$( onlyoffice_get_core_machine_key );
|
||||
|
||||
cp -drf ${SNAP}/config/hyperfastcgi-config/* ${SNAP_DATA}/hyperfastcgi/
|
||||
|
||||
[ -e ${ONLYOFFCE_SOCKET} ] && rm -f ${ONLYOFFCE_SOCKET}
|
||||
|
||||
sed -e "s|\${SNAP}|$SNAP|;s|\${SNAP_DATA}|$SNAP_DATA|;s|\${ONLYOFFICE_SOCKET}|$ONLYOFFICE_SOCKET|" -i ${SNAP_DATA}/hyperfastcgi/onlyoffice
|
||||
|
||||
mkdir -p ${SNAP_DATA}/onlyoffice/config/WebStudio/
|
||||
cp -dfr ${SNAP}/var/www/onlyoffice/WebStudio/*.config ${SNAP_DATA}/onlyoffice/config/WebStudio/
|
||||
|
||||
sed "/core.machinekey/s!value=\".*\"!value=\"${ONLYOFFICE_CORE_MACHINEKEY}\"!g" -i ${SNAP_DATA}/onlyoffice/config/WebStudio/web.appsettings.config
|
||||
sed "s!/var/log/onlyoffice/!${SNAP_DATA}/onlyoffice/logs/!g" -i ${SNAP_DATA}/onlyoffice/config/WebStudio/web.log4net.config
|
||||
sed "s|\.*\\\Data\\\|${SNAP_DATA}/onlyoffice/data/|g" -i ${SNAP_DATA}/onlyoffice/config/WebStudio/web.storage.config
|
||||
|
||||
sed "s|Password=.*;|Password=${DB_PWD};|g" -i ${SNAP_DATA}/onlyoffice/config/WebStudio/web.connections.config
|
||||
sed "s|User\\s*ID=.*;|User\\s*ID=${DB_USER};|g" -i ${SNAP_DATA}/onlyoffice/config/WebStudio/web.connections.config
|
||||
|
||||
|
||||
export ONLYOFFICE_APP_CONFIG_FILE="${SNAP_DATA}/onlyoffice/config/WebStudio/Web.config";
|
||||
|
||||
MYSQL="mysql -h$DB_HOST -u$DB_USER -p$DB_PWD -S$MYSQL_SOCKET";
|
||||
|
||||
DB_TABLES_COUNT=$($MYSQL --silent --skip-column-names -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}'");
|
||||
|
||||
if [ "${DB_TABLES_COUNT}" -eq "0" ]; then
|
||||
|
||||
$MYSQL "$DB_NAME" < $SNAP/var/www/onlyoffice/Sql/onlyoffice.sql
|
||||
$MYSQL "$DB_NAME" < $SNAP/var/www/onlyoffice/Sql/onlyoffice.data.sql
|
||||
$MYSQL "$DB_NAME" < $SNAP/var/www/onlyoffice/Sql/onlyoffice.resources.sql
|
||||
fi
|
||||
|
||||
for i in $(ls $SNAP/var/www/onlyoffice/Sql/onlyoffice.upgrade*); do
|
||||
$MYSQL "$DB_NAME" < ${i};
|
||||
done
|
||||
|
||||
# export mono variables
|
||||
export MONO_IOMAP=all
|
||||
export MONO_ASPNET_WEBCONFIG_CACHESIZE=2000
|
||||
export MONO_THREADS_PER_CPU=2000
|
||||
export MONO_OPTIONS="--server"
|
||||
export MONO_GC_PARAMS=nursery-size=64m
|
||||
|
||||
PKG_DIR=$SNAP/usr
|
||||
|
||||
export MONO_PATH=$PKG_DIR/lib/mono/4.5
|
||||
export MONO_CONFIG=$SNAP/etc/mono/config
|
||||
export MONO_CFG_DIR=$SNAP/etc
|
||||
export C_INCLUDE_PATH=${PKG_DIR}/include
|
||||
export MONO_REGISTRY_PATH=~/.mono/registry
|
||||
export MONO_GAC_PREFIX=$PKG_DIR/lib/mono/gac/
|
||||
#export LD_LIBRARY_PATH=$PKG_DIR/lib:$LD_LIBRARY_PATH
|
||||
|
||||
export LD_RUN_PATH=$LD_LIBRARY_PATH
|
||||
#export LD_DEBUG=files
|
||||
|
||||
export PKG_CONFIG_PATH=$PKG_DIR/lib/pkgconfig:$PKG_CONFIG_PATH
|
||||
export ACLOCAL_PATH=${PKG_DIR}/share/aclocal
|
||||
#export MONO_LOG_LEVEL=debug
|
||||
#export FONTCONFIG_PATH=${PKG_DIR}/etc/fonts
|
||||
#export XDG_DATA_HOME=${PKG_DIR}/etc/fonts
|
||||
|
||||
|
||||
exec ${SNAP}/usr/bin/mono ${SNAP}/usr/lib/hyperfastcgi/4.0/HyperFastCgi.exe /config=${SNAP_DATA}/hyperfastcgi/onlyoffice /logfile=${SNAP_DATA}/onlyoffice/logs/onlyoffice.log
|
@ -0,0 +1,24 @@
|
||||
<configuration>
|
||||
<server type="HyperFastCgi.ApplicationServers.SimpleApplicationServer">
|
||||
<root-dir>${SNAP}/var/www/onlyoffice/WebStudio</root-dir>
|
||||
<threads min-worker="40" max-worker="0" min-io="4" max-io="0" />
|
||||
</server>
|
||||
<listener type="HyperFastCgi.Listeners.NativeListener">
|
||||
<apphost-transport type="HyperFastCgi.Transports.NativeTransport">
|
||||
<multithreading>Task</multithreading>
|
||||
</apphost-transport>
|
||||
<protocol>Unix</protocol>
|
||||
<address>//666@${ONLYOFFICE_SOCKET}</address>
|
||||
</listener>
|
||||
<apphost type="HyperFastCgi.AppHosts.AspNet.AspNetApplicationHost">
|
||||
<log level="Error" write-to-console="false" />
|
||||
<add-trailing-slash>false</add-trailing-slash>
|
||||
</apphost>
|
||||
<web-applications>
|
||||
<web-application>
|
||||
<name>onlyoffice</name>
|
||||
<vpath>/</vpath>
|
||||
<path>.</path>
|
||||
</web-application>
|
||||
</web-applications>
|
||||
</configuration>
|
@ -0,0 +1,24 @@
|
||||
<configuration>
|
||||
<server type="HyperFastCgi.ApplicationServers.SimpleApplicationServer">
|
||||
<root-dir>${SNAP}/var/www/onlyoffice/ApiSystem</root-dir>
|
||||
<threads min-worker="40" max-worker="0" min-io="4" max-io="0" />
|
||||
</server>
|
||||
<listener type="HyperFastCgi.Listeners.NativeListener">
|
||||
<apphost-transport type="HyperFastCgi.Transports.NativeTransport">
|
||||
<multithreading>Task</multithreading>
|
||||
</apphost-transport>
|
||||
<protocol>Unix</protocol>
|
||||
<address>//666@${ONLYOFFICE_API_SYSTEM_SOCKET}</address>
|
||||
</listener>
|
||||
<apphost type="HyperFastCgi.AppHosts.AspNet.AspNetApplicationHost">
|
||||
<log level="Error" write-to-console="false" />
|
||||
<add-trailing-slash>false</add-trailing-slash>
|
||||
</apphost>
|
||||
<web-applications>
|
||||
<web-application>
|
||||
<name>onlyofficeApiSystem</name>
|
||||
<vpath>/</vpath>
|
||||
<path>.</path>
|
||||
</web-application>
|
||||
</web-applications>
|
||||
</configuration>
|
36
build/install/snap/src/onlyoffice/utilities/monoserve-utilities
Executable file
36
build/install/snap/src/onlyoffice/utilities/monoserve-utilities
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
export ONLYOFFICE_SOCKET="/tmp/sockets/onlyoffice.socket"
|
||||
export ONLYOFFICE_API_SYSTEM_SOCKET="/tmp/sockets/onlyofficeApiSystem.socket"
|
||||
export ONLYOFFICE_CORE_MACHINEKEY_FILE="$SNAP_DATA/onlyoffice/.onlyoffice_core_machine_key"
|
||||
|
||||
mkdir -p "$(dirname "$ONLYOFFICE_SOCKET")"
|
||||
chmod 750 "$(dirname "$ONLYOFFICE_SOCKET")"
|
||||
|
||||
mkdir -p "$(dirname "$ONLYOFFICE_API_SYSTEM_SOCKET")"
|
||||
chmod 750 "$(dirname "$ONLYOFFICE_API_SYSTEM_SOCKET")"
|
||||
|
||||
mkdir -p "$(dirname "$ONLYOFFICE_CORE_MACHINEKEY_FILE")"
|
||||
chmod 750 "$(dirname "$ONLYOFFICE_CORE_MACHINEKEY_FILE")"
|
||||
|
||||
mkdir -p $SNAP_DATA/hyperfastcgi
|
||||
chmod 750 $SNAP_DATA/hyperfastcgi
|
||||
|
||||
mkdir -p $SNAP_DATA/onlyoffice/logs
|
||||
chmod 750 $SNAP_DATA/onlyoffice/logs
|
||||
|
||||
mkdir -p $SNAP_DATA/onlyoffice/data
|
||||
chmod 750 $SNAP_DATA/onlyoffice/data
|
||||
|
||||
mkdir -p $SNAP_DATA/onlyoffice/config
|
||||
chmod 750 $SNAP_DATA/onlyoffice/config
|
||||
|
||||
onlyoffice_get_core_machine_key() {
|
||||
if [ ! -f "$ONLYOFFICE_CORE_MACHINEKEY_FILE" ]; then
|
||||
echo "$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c64)" > ${ONLYOFFICE_CORE_MACHINEKEY_FILE};
|
||||
chmod 600 ${ONLYOFFICE_CORE_MACHINEKEY_FILE};
|
||||
fi
|
||||
|
||||
cat "$ONLYOFFICE_CORE_MACHINEKEY_FILE";
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
From bb6c86ca997b2ca1b052cb83e91152220fe149ad Mon Sep 17 00:00:00 2001
|
||||
From: Kyle Fazzari <oracle@status.e4ward.com>
|
||||
Date: Fri, 25 Mar 2016 15:03:38 +0000
|
||||
Subject: [PATCH] Support compile-time disabling of setpriority().
|
||||
|
||||
This is to support running on systems such as Snappy Ubuntu Core,
|
||||
e.g. heavily confined using seccomp filters. In such a situation,
|
||||
without this commit, MySQL is aborted as soon as it tries to call
|
||||
setpriority(). With this commit, MySQL can be built without
|
||||
setpriority() by using -DWITH_INNODB_PAGE_CLEANER_PRIORITY=OFF,
|
||||
thus supporting such systems.
|
||||
|
||||
Signed-off-by: Kyle Fazzari <oracle@status.e4ward.com>
|
||||
---
|
||||
storage/innobase/buf/buf0flu.cc | 12 ++++++------
|
||||
storage/innobase/innodb.cmake | 5 +++++
|
||||
2 files changed, 11 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/storage/innobase/buf/buf0flu.cc b/storage/innobase/buf/buf0flu.cc
|
||||
index 5a8a3567e0f..0961f757b1a 100644
|
||||
--- a/storage/innobase/buf/buf0flu.cc
|
||||
+++ b/storage/innobase/buf/buf0flu.cc
|
||||
@@ -2952,7 +2952,7 @@ pc_wait_finished(
|
||||
return(all_succeeded);
|
||||
}
|
||||
|
||||
-#ifdef UNIV_LINUX
|
||||
+#if defined(UNIV_LINUX) && defined(SET_PAGE_CLEANER_PRIORITY)
|
||||
/**
|
||||
Set priority for page_cleaner threads.
|
||||
@param[in] priority priority intended to set
|
||||
@@ -2967,7 +2967,7 @@ buf_flush_page_cleaner_set_priority(
|
||||
return(getpriority(PRIO_PROCESS, (pid_t)syscall(SYS_gettid))
|
||||
== priority);
|
||||
}
|
||||
-#endif /* UNIV_LINUX */
|
||||
+#endif /* UNIV_LINUX && SET_PAGE_CLEANER_PRIORITY */
|
||||
|
||||
#ifdef UNIV_DEBUG
|
||||
/** Loop used to disable page cleaner threads. */
|
||||
@@ -3113,7 +3113,7 @@ DECLARE_THREAD(buf_flush_page_cleaner_coordinator)(
|
||||
<< os_thread_pf(os_thread_get_curr_id());
|
||||
#endif /* UNIV_DEBUG_THREAD_CREATION */
|
||||
|
||||
-#ifdef UNIV_LINUX
|
||||
+#if defined(UNIV_LINUX) && defined(SET_PAGE_CLEANER_PRIORITY)
|
||||
/* linux might be able to set different setting for each thread.
|
||||
worth to try to set high priority for page cleaner threads */
|
||||
if (buf_flush_page_cleaner_set_priority(
|
||||
@@ -3126,7 +3126,7 @@ DECLARE_THREAD(buf_flush_page_cleaner_coordinator)(
|
||||
" page cleaner thread priority can be changed."
|
||||
" See the man page of setpriority().";
|
||||
}
|
||||
-#endif /* UNIV_LINUX */
|
||||
+#endif /* UNIV_LINUX && SET_PAGE_CLEANER_PRIORITY */
|
||||
|
||||
buf_page_cleaner_is_active = true;
|
||||
|
||||
@@ -3481,7 +3481,7 @@ DECLARE_THREAD(buf_flush_page_cleaner_worker)(
|
||||
page_cleaner->n_workers++;
|
||||
mutex_exit(&page_cleaner->mutex);
|
||||
|
||||
-#ifdef UNIV_LINUX
|
||||
+#if defined(UNIV_LINUX) && defined(SET_PAGE_CLEANER_PRIORITY)
|
||||
/* linux might be able to set different setting for each thread
|
||||
worth to try to set high priority for page cleaner threads */
|
||||
if (buf_flush_page_cleaner_set_priority(
|
||||
@@ -3490,7 +3490,7 @@ DECLARE_THREAD(buf_flush_page_cleaner_worker)(
|
||||
ib::info() << "page_cleaner worker priority: "
|
||||
<< buf_flush_page_cleaner_priority;
|
||||
}
|
||||
-#endif /* UNIV_LINUX */
|
||||
+#endif /* UNIV_LINUX && SET_PAGE_CLEANER_PRIORITY */
|
||||
|
||||
while (true) {
|
||||
os_event_wait(page_cleaner->is_requested);
|
||||
diff --git a/storage/innobase/innodb.cmake b/storage/innobase/innodb.cmake
|
||||
index a90fe67f492..0d0a3ad7e3b 100644
|
||||
--- a/storage/innobase/innodb.cmake
|
||||
+++ b/storage/innobase/innodb.cmake
|
||||
@@ -38,6 +38,11 @@ IF(UNIX)
|
||||
LINK_LIBRARIES(aio)
|
||||
ENDIF()
|
||||
|
||||
+ OPTION(WITH_INNODB_PAGE_CLEANER_PRIORITY "Set a high priority for page cleaner threads" ON)
|
||||
+ IF(WITH_INNODB_PAGE_CLEANER_PRIORITY)
|
||||
+ ADD_DEFINITIONS("-DSET_PAGE_CLEANER_PRIORITY")
|
||||
+ ENDIF()
|
||||
+
|
||||
ELSEIF(CMAKE_SYSTEM_NAME STREQUAL "SunOS")
|
||||
ADD_DEFINITIONS("-DUNIV_SOLARIS")
|
||||
ENDIF()
|
12
build/install/snap/src/redis/bin/start-redis-server
Executable file
12
build/install/snap/src/redis/bin/start-redis-server
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# shellcheck source=src/redis/utilities/redis-utilities
|
||||
. "$SNAP/utilities/redis-utilities"
|
||||
|
||||
mkdir -p "${SNAP_DATA}/redis"
|
||||
chmod 750 "${SNAP_DATA}/redis"
|
||||
|
||||
# redis doesn't support environment variables in its config files. Thankfully
|
||||
# it supports reading the config file from stdin though, so we'll rewrite the
|
||||
# config file on the fly and pipe it in.
|
||||
sed -e "s|\${SNAP_DATA}|$SNAP_DATA|;s|\${REDIS_PIDFILE}|$REDIS_PIDFILE|;s|\${REDIS_SOCKET}|$REDIS_SOCKET|" "$SNAP/config/redis/redis.conf" | redis-server -
|
1023
build/install/snap/src/redis/config/redis.conf
Normal file
1023
build/install/snap/src/redis/config/redis.conf
Normal file
File diff suppressed because it is too large
Load Diff
35
build/install/snap/src/redis/utilities/redis-utilities
Executable file
35
build/install/snap/src/redis/utilities/redis-utilities
Executable file
@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
export REDIS_PIDFILE="/tmp/pids/redis.pid"
|
||||
export REDIS_SOCKET="/tmp/sockets/redis.sock"
|
||||
|
||||
mkdir -p "$(dirname "$REDIS_PIDFILE")"
|
||||
mkdir -p "$(dirname "$REDIS_SOCKET")"
|
||||
chmod 750 "$(dirname "$REDIS_PIDFILE")"
|
||||
chmod 750 "$(dirname "$REDIS_SOCKET")"
|
||||
|
||||
redis_is_running()
|
||||
{
|
||||
[ -f "$REDIS_PIDFILE" ] && [ -S "$REDIS_SOCKET" ]
|
||||
}
|
||||
|
||||
wait_for_redis()
|
||||
{
|
||||
if ! redis_is_running; then
|
||||
printf "Waiting for redis... "
|
||||
while ! redis_is_running; do
|
||||
sleep 1
|
||||
done
|
||||
printf "done\n"
|
||||
fi
|
||||
}
|
||||
|
||||
redis_pid()
|
||||
{
|
||||
if redis_is_running; then
|
||||
cat "$REDIS_PIDFILE"
|
||||
else
|
||||
echo "Unable to get redis PID as it's not yet running" >&2
|
||||
echo ""
|
||||
fi
|
||||
}
|
@ -30,7 +30,6 @@
|
||||
"react-window": "^1.8.5",
|
||||
"reactstrap": "8.0.1",
|
||||
"redux": "4.0.4",
|
||||
"redux-form": "^8.2.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"styled-components": "^4.3.2",
|
||||
"universal-cookie": "^4.0.2"
|
||||
|
@ -1,110 +0,0 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { Field, reduxForm, SubmissionError } from "redux-form";
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
Text,
|
||||
InputBlock,
|
||||
Icons,
|
||||
SelectedItem
|
||||
} from "asc-web-components";
|
||||
import submit from "./submit";
|
||||
import validate from "./validate";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { department, headOfDepartment, typeUser } from './../../../../../../helpers/customNames';
|
||||
|
||||
const generateItems = numItems =>
|
||||
Array(numItems)
|
||||
.fill(true)
|
||||
.map(_ => Math.random()
|
||||
.toString(36)
|
||||
.substr(2)
|
||||
);
|
||||
|
||||
const GroupForm = props => {
|
||||
const { error, handleSubmit, submitting, initialValues, history } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const selectedList = generateItems(100);
|
||||
|
||||
console.log(selectedList);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
history.goBack();
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submit)}>
|
||||
<div>
|
||||
<label htmlFor="group-name">
|
||||
<Text.Body as="span" isBold={true}>{t('CustomDepartmentName', { department })}:</Text.Body>
|
||||
</label>
|
||||
<div style={{width: "320px"}}>
|
||||
<TextInput id="group-name" name="group-name" scale={true} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<label htmlFor="head-selector">
|
||||
<Text.Body as="span" isBold={true}>{t('CustomHeadOfDepartment', { headOfDepartment })}:</Text.Body>
|
||||
</label>
|
||||
<InputBlock
|
||||
id="head-selector"
|
||||
value={t('CustomAddEmployee', { typeUser })}
|
||||
iconName="ExpanderDownIcon"
|
||||
iconSize={8}
|
||||
isIconFill={true}
|
||||
iconColor="#A3A9AE"
|
||||
scale={false}
|
||||
isReadOnly={true}
|
||||
>
|
||||
<Icons.CatalogEmployeeIcon size="medium" />
|
||||
</InputBlock>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<label htmlFor="employee-selector">
|
||||
<Text.Body as="span" isBold={true}>Members:</Text.Body>
|
||||
</label>
|
||||
<InputBlock
|
||||
id="employee-selector"
|
||||
value={t('CustomAddEmployee', { typeUser })}
|
||||
iconName="ExpanderDownIcon"
|
||||
iconSize={8}
|
||||
isIconFill={true}
|
||||
iconColor="#A3A9AE"
|
||||
scale={false}
|
||||
isReadOnly={true}
|
||||
>
|
||||
<Icons.CatalogGuestIcon size="medium" />
|
||||
</InputBlock>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px", display: "flex", flexWrap: "wrap", flexDirection: "row" }}>
|
||||
{selectedList.map(item =>
|
||||
<SelectedItem
|
||||
text={`Fake User-${item}`}
|
||||
onClick={(e) => console.log("onClose", e.target)}
|
||||
isInline={true}
|
||||
style={{ marginRight: "8px", marginBottom: "8px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{error && <strong>{error}</strong>}</div>
|
||||
<div style={{ marginTop: "60px" }}>
|
||||
<Button label={t('SaveButton')} primary type="submit" isDisabled={submitting} size="big" />
|
||||
<Button
|
||||
label={t('CancelButton')}
|
||||
style={{ marginLeft: "8px" }}
|
||||
size="big"
|
||||
isDisabled={submitting}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
validate,
|
||||
form: "groupForm",
|
||||
enableReinitialize: true
|
||||
})(withRouter(GroupForm));
|
@ -1,20 +0,0 @@
|
||||
import { SubmissionError } from 'redux-form'
|
||||
|
||||
function submit (values) {
|
||||
function successCallback (res) {
|
||||
if (res.data && res.data.error) {
|
||||
window.alert(res.data.error.message);
|
||||
} else {
|
||||
console.log(res);
|
||||
window.alert('Success');
|
||||
}
|
||||
}
|
||||
|
||||
function errorCallback (error) {
|
||||
throw new SubmissionError({
|
||||
_error: error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default submit
|
@ -1,19 +0,0 @@
|
||||
function validate (values) {
|
||||
const errors = {};
|
||||
|
||||
if (!values.firstName) {
|
||||
errors.firstName = 'required field';
|
||||
}
|
||||
|
||||
if (!values.lastName) {
|
||||
errors.lastName = 'required field';
|
||||
}
|
||||
|
||||
if (!values.email) {
|
||||
errors.email = 'required field';
|
||||
}
|
||||
|
||||
return errors
|
||||
};
|
||||
|
||||
export default validate;
|
@ -1,13 +1,97 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import GroupForm from './Form/groupForm'
|
||||
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
Text,
|
||||
InputBlock,
|
||||
Icons,
|
||||
SelectedItem
|
||||
} from "asc-web-components";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { department, headOfDepartment, typeUser } from '../../../../../helpers/customNames';
|
||||
|
||||
const SectionBodyContent = (props) => {
|
||||
const {group} = props;
|
||||
const { history, group } = props;
|
||||
const [value, setValue] = useState(group ? group.name : "");
|
||||
const [error, setError] = useState(null);
|
||||
const [inLoading, setInLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const groupMembers = group && group.members ? group.members : [];
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
history.goBack();
|
||||
}, [history]);
|
||||
|
||||
console.log("Group render", props);
|
||||
|
||||
return (
|
||||
<GroupForm initialValues={group} />
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="group-name">
|
||||
<Text.Body as="span" isBold={true}>{t('CustomDepartmentName', { department })}:</Text.Body>
|
||||
</label>
|
||||
<div style={{width: "320px"}}>
|
||||
<TextInput id="group-name" name="group-name" scale={true} value={value} onChange={(e) => setValue(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<label htmlFor="head-selector">
|
||||
<Text.Body as="span" isBold={true}>{t('CustomHeadOfDepartment', { headOfDepartment })}:</Text.Body>
|
||||
</label>
|
||||
<InputBlock
|
||||
id="head-selector"
|
||||
value={t('CustomAddEmployee', { typeUser })}
|
||||
iconName="ExpanderDownIcon"
|
||||
iconSize={8}
|
||||
isIconFill={true}
|
||||
iconColor="#A3A9AE"
|
||||
scale={false}
|
||||
isReadOnly={true}
|
||||
>
|
||||
<Icons.CatalogEmployeeIcon size="medium" />
|
||||
</InputBlock>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<label htmlFor="employee-selector">
|
||||
<Text.Body as="span" isBold={true}>Members:</Text.Body>
|
||||
</label>
|
||||
<InputBlock
|
||||
id="employee-selector"
|
||||
value={t('CustomAddEmployee', { typeUser })}
|
||||
iconName="ExpanderDownIcon"
|
||||
iconSize={8}
|
||||
isIconFill={true}
|
||||
iconColor="#A3A9AE"
|
||||
scale={false}
|
||||
isReadOnly={true}
|
||||
>
|
||||
<Icons.CatalogGuestIcon size="medium" />
|
||||
</InputBlock>
|
||||
</div>
|
||||
<div style={{ marginTop: "16px", display: "flex", flexWrap: "wrap", flexDirection: "row" }}>
|
||||
{groupMembers.map(member =>
|
||||
<SelectedItem
|
||||
text={member.displayName}
|
||||
onClick={(e) => console.log("onClose", e.target)}
|
||||
isInline={true}
|
||||
style={{ marginRight: "8px", marginBottom: "8px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{error && <strong>{error}</strong>}</div>
|
||||
<div style={{ marginTop: "60px" }}>
|
||||
<Button label={t('SaveButton')} primary type="submit" isDisabled={inLoading} size="big" />
|
||||
<Button
|
||||
label={t('CancelButton')}
|
||||
style={{ marginLeft: "8px" }}
|
||||
size="big"
|
||||
isDisabled={inLoading}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,25 +1,55 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { PageLayout } from "asc-web-components";
|
||||
import { PageLayout, Loader } from "asc-web-components";
|
||||
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
|
||||
import { SectionHeaderContent, SectionBodyContent } from './Section';
|
||||
import i18n from "./i18n";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { fetchGroup } from "../../../store/group/actions";
|
||||
|
||||
class GroupAction extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { match, fetchGroup } = this.props;
|
||||
const { groupId } = match.params;
|
||||
|
||||
if (groupId) {
|
||||
fetchGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { match, fetchGroup } = this.props;
|
||||
const { groupId } = match.params;
|
||||
const prevUserId = prevProps.match.params.groupId;
|
||||
|
||||
if (groupId !== undefined && groupId !== prevUserId) {
|
||||
fetchGroup(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log("GroupAction render")
|
||||
|
||||
const { group, match } = this.props;
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PageLayout
|
||||
{group || !match.params.groupId
|
||||
? <PageLayout
|
||||
articleHeaderContent={<ArticleHeaderContent />}
|
||||
articleMainButtonContent={<ArticleMainButtonContent />}
|
||||
articleBodyContent={<ArticleBodyContent />}
|
||||
sectionHeaderContent={<SectionHeaderContent />}
|
||||
sectionBodyContent={<SectionBodyContent />}
|
||||
sectionBodyContent={<SectionBodyContent group={group} />}
|
||||
/>
|
||||
: <PageLayout
|
||||
articleHeaderContent={<ArticleHeaderContent />}
|
||||
articleMainButtonContent={<ArticleMainButtonContent />}
|
||||
articleBodyContent={<ArticleBodyContent />}
|
||||
sectionBodyContent={<Loader className="pageLoader" type="rombs" size={40} />}
|
||||
/>
|
||||
}
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
@ -27,8 +57,9 @@ class GroupAction extends React.Component {
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
settings: state.auth.settings
|
||||
settings: state.auth.settings,
|
||||
group: state.group.targetGroup
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(GroupAction);
|
||||
export default connect(mapStateToProps, { fetchGroup })(GroupAction);
|
@ -86,6 +86,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="OkBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
const { onLoading } = this.props;
|
||||
@ -107,6 +108,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label="Cancel"
|
||||
size="medium"
|
||||
primary={false}
|
||||
onClick={this.onDialogClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
@ -142,6 +144,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="OkBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
toastr.success(
|
||||
@ -153,6 +156,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label="Cancel"
|
||||
size="medium"
|
||||
primary={false}
|
||||
onClick={this.onDialogClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
@ -215,6 +219,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="OkBtn"
|
||||
label="OK"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
const { onLoading, filter, fetchPeople } = this.props;
|
||||
@ -232,6 +237,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="ReassignBtn"
|
||||
label="Reassign data"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
toastr.success("Context action: Reassign profile");
|
||||
@ -242,6 +248,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label="Cancel"
|
||||
size="medium"
|
||||
primary={false}
|
||||
onClick={this.onDialogClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
@ -268,6 +275,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="OkBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={() => {
|
||||
const { onLoading } = this.props;
|
||||
@ -289,6 +297,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label="Cancel"
|
||||
size="medium"
|
||||
primary={false}
|
||||
onClick={this.onDialogClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
|
@ -198,6 +198,8 @@ const SectionFilterContent = ({
|
||||
getSortData={getSortData.bind(this, t)}
|
||||
selectedFilterData={selectedFilterData}
|
||||
onFilter={onFilter}
|
||||
directionAscLabel={t("DirectionAscLabel")}
|
||||
directionDescLabel={t("DirectionDescLabel")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import {
|
||||
GroupButtonsMenu,
|
||||
DropDownItem,
|
||||
@ -28,20 +29,7 @@ import {
|
||||
deleteUsers
|
||||
} from "../../../../../store/services/api";
|
||||
|
||||
const contextOptions = t => {
|
||||
return [
|
||||
{
|
||||
key: "edit-group",
|
||||
label: t("EditButton"),
|
||||
onClick: toastr.success.bind(this, "Edit group action")
|
||||
},
|
||||
{
|
||||
key: "delete-group",
|
||||
label: t("DeleteButton"),
|
||||
onClick: toastr.success.bind(this, "Delete group action")
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
const wrapperStyle = {
|
||||
display: "flex",
|
||||
@ -63,7 +51,9 @@ const SectionHeaderContent = props => {
|
||||
updateUserStatus,
|
||||
updateUserType,
|
||||
onLoading,
|
||||
filter
|
||||
filter,
|
||||
history,
|
||||
settings
|
||||
} = props;
|
||||
|
||||
const selectedUserIds = getSelectionIds(selection);
|
||||
@ -157,6 +147,27 @@ const SectionHeaderContent = props => {
|
||||
}
|
||||
];
|
||||
|
||||
const onEditGroup = useCallback(() => history.push(`${settings.homepage}/group/edit/${group.id}`), [history, settings, group]);
|
||||
|
||||
const onDeleteGroup = useCallback(() => {
|
||||
toastr.success("Delete group action");
|
||||
}, []);
|
||||
|
||||
const getContextOptions = useCallback(() => {
|
||||
return [
|
||||
{
|
||||
key: "edit-group",
|
||||
label: t("EditButton"),
|
||||
onClick: onEditGroup
|
||||
},
|
||||
{
|
||||
key: "delete-group",
|
||||
label: t("DeleteButton"),
|
||||
onClick: onDeleteGroup
|
||||
}
|
||||
];
|
||||
}, [t, onEditGroup, onDeleteGroup]);
|
||||
|
||||
return isHeaderVisible ? (
|
||||
<div style={{ margin: "0 -16px" }}>
|
||||
<GroupButtonsMenu
|
||||
@ -181,7 +192,7 @@ const SectionHeaderContent = props => {
|
||||
iconName="VerticalDotsIcon"
|
||||
size={16}
|
||||
color="#A3A9AE"
|
||||
getData={contextOptions.bind(this, t)}
|
||||
getData={getContextOptions.bind(this, t)}
|
||||
isDisabled={false}
|
||||
/>
|
||||
)}
|
||||
@ -196,11 +207,12 @@ const mapStateToProps = state => {
|
||||
group: getSelectedGroup(state.people.groups, state.people.selectedGroup),
|
||||
selection: state.people.selection,
|
||||
isAdmin: isAdmin(state.auth.user),
|
||||
filter: state.people.filter
|
||||
filter: state.people.filter,
|
||||
settings: state.auth.settings
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ updateUserStatus, updateUserType, fetchPeople }
|
||||
)(withTranslation()(SectionHeaderContent));
|
||||
)(withTranslation()(withRouter(SectionHeaderContent)));
|
||||
|
@ -47,5 +47,8 @@
|
||||
"CustomMakeGuest": "Make {{typeGuest, lowercase}}",
|
||||
"CustomDepartment": "{{department}}",
|
||||
"CountPerPage": "{{count}} per page",
|
||||
"PageOfTotalPage": "{{page}} of {{totalPage}}"
|
||||
"PageOfTotalPage": "{{page}} of {{totalPage}}",
|
||||
|
||||
"DirectionAscLabel":"A-Z",
|
||||
"DirectionDescLabel":"Z-A"
|
||||
}
|
@ -10,7 +10,8 @@ const DateField = React.memo((props) => {
|
||||
inputName,
|
||||
inputValue,
|
||||
inputIsDisabled,
|
||||
inputOnChange
|
||||
inputOnChange,
|
||||
inputTabIndex
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -25,6 +26,7 @@ const DateField = React.memo((props) => {
|
||||
disabled={inputIsDisabled}
|
||||
onChange={inputOnChange}
|
||||
hasError={hasError}
|
||||
tabIndex={inputTabIndex}
|
||||
/>
|
||||
</FieldContainer>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ const DepartmentField = React.memo((props) => {
|
||||
isRequired,
|
||||
hasError,
|
||||
labelText,
|
||||
|
||||
|
||||
departments,
|
||||
onRemoveDepartment
|
||||
} = props;
|
||||
|
@ -1,11 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
import { device } from 'asc-web-components'
|
||||
import { utils } from 'asc-web-components'
|
||||
|
||||
const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media ${device.tablet} {
|
||||
.field-input {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
line-height: 32px;
|
||||
display: flex;
|
||||
|
||||
label:not(:first-child) {
|
||||
margin-left: 33px;
|
||||
}
|
||||
}
|
||||
|
||||
@media ${utils.device.tablet} {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
@ -1,59 +1,31 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components';
|
||||
import { device, FieldContainer, RadioButtonGroup, InputBlock, Icons, Link } from 'asc-web-components'
|
||||
|
||||
const PasswordBlock = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 32px;
|
||||
flex-direction: row;
|
||||
|
||||
.refresh-btn, .copy-link {
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
|
||||
@media ${device.tablet} {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.copy-link {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const InputContainer = styled.div`
|
||||
width: 352px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
import { FieldContainer, RadioButtonGroup, PasswordInput } from 'asc-web-components'
|
||||
|
||||
const PasswordField = React.memo((props) => {
|
||||
const {
|
||||
isRequired,
|
||||
hasError,
|
||||
labelText,
|
||||
|
||||
passwordSettings,
|
||||
|
||||
radioName,
|
||||
radioValue,
|
||||
radioOptions,
|
||||
radioIsDisabled,
|
||||
radioOnChange,
|
||||
|
||||
|
||||
inputName,
|
||||
emailInputName,
|
||||
inputValue,
|
||||
inputIsDisabled,
|
||||
inputOnChange,
|
||||
|
||||
inputIconOnClick,
|
||||
inputShowPassword,
|
||||
|
||||
refreshIconOnClick,
|
||||
inputTabIndex,
|
||||
|
||||
copyLinkText,
|
||||
copyLinkOnClick
|
||||
} = props;
|
||||
|
||||
|
||||
const tooltipPasswordLength = 'from ' + passwordSettings.minLength + ' to 30 characters';
|
||||
|
||||
return (
|
||||
<FieldContainer
|
||||
isRequired={isRequired}
|
||||
@ -68,34 +40,25 @@ const PasswordField = React.memo((props) => {
|
||||
onClick={radioOnChange}
|
||||
className="radio-group"
|
||||
/>
|
||||
<PasswordBlock>
|
||||
<InputContainer>
|
||||
<InputBlock
|
||||
name={inputName}
|
||||
hasError={hasError}
|
||||
isDisabled={inputIsDisabled}
|
||||
iconName="EyeIcon"
|
||||
value={inputValue}
|
||||
onIconClick={inputIconOnClick}
|
||||
onChange={inputOnChange}
|
||||
scale={true}
|
||||
type={inputShowPassword ? "text" : "password"}
|
||||
/>
|
||||
<Icons.RefreshIcon
|
||||
size="medium"
|
||||
onClick={refreshIconOnClick}
|
||||
className="refresh-btn"
|
||||
/>
|
||||
</InputContainer>
|
||||
<Link
|
||||
type="action"
|
||||
isHovered={true}
|
||||
onClick={copyLinkOnClick}
|
||||
className="copy-link"
|
||||
>
|
||||
{copyLinkText}
|
||||
</Link>
|
||||
</PasswordBlock>
|
||||
<PasswordInput
|
||||
inputName={inputName}
|
||||
emailInputName={emailInputName}
|
||||
inputValue={inputValue}
|
||||
inputWidth="320px"
|
||||
inputTabIndex={inputTabIndex}
|
||||
onChange={inputOnChange}
|
||||
clipActionResource={copyLinkText}
|
||||
clipEmailResource='E-mail: '
|
||||
clipPasswordResource='Password: '
|
||||
tooltipPasswordTitle='Password must contain:'
|
||||
tooltipPasswordLength={tooltipPasswordLength}
|
||||
tooltipPasswordDigits='digits'
|
||||
tooltipPasswordCapital='capital letters'
|
||||
tooltipPasswordSpecial='special characters (!@#$%^&*)'
|
||||
generatorSpecial='!@#$%^&*'
|
||||
passwordSettings={passwordSettings}
|
||||
isDisabled={inputIsDisabled}
|
||||
/>
|
||||
</FieldContainer>
|
||||
);
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ const RadioField = React.memo((props) => {
|
||||
isRequired,
|
||||
hasError,
|
||||
labelText,
|
||||
|
||||
|
||||
radioName,
|
||||
radioValue,
|
||||
radioOptions,
|
||||
|
@ -16,10 +16,12 @@ const TextChangeField = React.memo((props) => {
|
||||
|
||||
inputName,
|
||||
inputValue,
|
||||
inputTabIndex,
|
||||
|
||||
buttonText,
|
||||
buttonIsDisabled,
|
||||
buttonOnClick
|
||||
buttonOnClick,
|
||||
buttonTabIndex
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -34,6 +36,7 @@ const TextChangeField = React.memo((props) => {
|
||||
value={inputValue}
|
||||
isDisabled={true}
|
||||
hasError={hasError}
|
||||
tabIndex={inputTabIndex}
|
||||
/>
|
||||
<Button
|
||||
label={buttonText}
|
||||
@ -41,6 +44,7 @@ const TextChangeField = React.memo((props) => {
|
||||
isDisabled={buttonIsDisabled}
|
||||
size="medium"
|
||||
style={{ marginLeft: "8px" }}
|
||||
tabIndex={buttonTabIndex}
|
||||
/>
|
||||
</InputContainer>
|
||||
</FieldContainer>
|
||||
|
@ -10,7 +10,9 @@ const TextField = React.memo((props) => {
|
||||
inputName,
|
||||
inputValue,
|
||||
inputIsDisabled,
|
||||
inputOnChange
|
||||
inputOnChange,
|
||||
inputAutoFocussed,
|
||||
inputTabIndex
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@ -26,6 +28,8 @@ const TextField = React.memo((props) => {
|
||||
onChange={inputOnChange}
|
||||
hasError={hasError}
|
||||
className="field-input"
|
||||
isAutoFocussed={inputAutoFocussed}
|
||||
tabIndex={inputTabIndex}
|
||||
/>
|
||||
</FieldContainer>
|
||||
);
|
||||
|
@ -21,11 +21,10 @@ class CreateUserForm extends React.Component {
|
||||
|
||||
this.validate = this.validate.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.onTextChange = this.onTextChange.bind(this);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onBirthdayDateChange = this.onBirthdayDateChange.bind(this);
|
||||
this.onWorkFromDateChange = this.onWorkFromDateChange.bind(this);
|
||||
this.onGroupClose = this.onGroupClose.bind(this);
|
||||
this.onShowPassword = this.onShowPassword.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
}
|
||||
|
||||
@ -36,22 +35,22 @@ class CreateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
mapPropsToState = (props) => {
|
||||
const isVisitor = props.match.params.type === "guest";
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
showPassword: false,
|
||||
errors: {
|
||||
firstName: false,
|
||||
lastName: false,
|
||||
email: false,
|
||||
password: false,
|
||||
},
|
||||
profile: toEmployeeWrapper({ isVisitor: isVisitor})
|
||||
profile: toEmployeeWrapper({
|
||||
isVisitor: props.match.params.type === "guest",
|
||||
passwordType: "link"
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
onTextChange(event) {
|
||||
onInputChange(event) {
|
||||
var stateCopy = Object.assign({}, this.state);
|
||||
stateCopy.profile[event.target.name] = event.target.value;
|
||||
this.setState(stateCopy)
|
||||
@ -75,27 +74,25 @@ class CreateUserForm extends React.Component {
|
||||
this.setState(stateCopy)
|
||||
}
|
||||
|
||||
onShowPassword() {
|
||||
this.setState({showPassword: !this.state.showPassword});
|
||||
}
|
||||
|
||||
validate() {
|
||||
const { profile } = this.state;
|
||||
const emailRegex = /.+@.+\..+/;
|
||||
const errors = {
|
||||
firstName: !this.state.profile.firstName,
|
||||
lastName: !this.state.profile.lastName,
|
||||
email: !this.state.profile.email,
|
||||
password: this.state.profile.passwordType === "temp" && !this.state.profile.password
|
||||
firstName: !profile.firstName,
|
||||
lastName: !profile.lastName,
|
||||
email: !emailRegex.test(profile.email),
|
||||
password: profile.passwordType === "temp" && !profile.password
|
||||
};
|
||||
const hasError = errors.firstName || errors.lastName || errors.email || errors.password;
|
||||
this.setState({errors: errors});
|
||||
this.setState({ errors: errors });
|
||||
return !hasError;
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
if(!this.validate())
|
||||
if (!this.validate())
|
||||
return false;
|
||||
|
||||
this.setState({isLoading: true});
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
this.props.createProfile(this.state.profile)
|
||||
.then((profile) => {
|
||||
@ -104,7 +101,7 @@ class CreateUserForm extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(error.message)
|
||||
this.setState({isLoading: false})
|
||||
this.setState({ isLoading: false })
|
||||
});
|
||||
}
|
||||
|
||||
@ -113,120 +110,130 @@ class CreateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, errors, profile } = this.state;
|
||||
const { t, settings } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContainer>
|
||||
<AvatarContainer>
|
||||
<Avatar
|
||||
size="max"
|
||||
role={getUserRole(this.state.profile)}
|
||||
role={getUserRole(profile)}
|
||||
editing={true}
|
||||
editLabel={this.props.t("AddPhoto")}
|
||||
editLabel={t("AddPhoto")}
|
||||
/>
|
||||
</AvatarContainer>
|
||||
<MainFieldsContainer>
|
||||
<TextField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.firstName}
|
||||
labelText={`${this.props.t("FirstName")}:`}
|
||||
hasError={errors.firstName}
|
||||
labelText={`${t("FirstName")}:`}
|
||||
inputName="firstName"
|
||||
inputValue={this.state.profile.firstName}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.firstName}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputAutoFocussed={true}
|
||||
inputTabIndex={1}
|
||||
/>
|
||||
<TextField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.lastName}
|
||||
labelText={`${this.props.t("LastName")}:`}
|
||||
hasError={errors.lastName}
|
||||
labelText={`${t("LastName")}:`}
|
||||
inputName="lastName"
|
||||
inputValue={this.state.profile.lastName}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.lastName}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={2}
|
||||
/>
|
||||
<TextField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.email}
|
||||
labelText={`${this.props.t("Email")}:`}
|
||||
hasError={errors.email}
|
||||
labelText={`${t("Email")}:`}
|
||||
inputName="email"
|
||||
inputValue={this.state.profile.email}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.email}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={3}
|
||||
/>
|
||||
<PasswordField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.password}
|
||||
labelText={`${this.props.t("Password")}:`}
|
||||
hasError={errors.password}
|
||||
labelText={`${t("Password")}:`}
|
||||
radioName="passwordType"
|
||||
radioValue={this.state.profile.passwordType}
|
||||
radioValue={profile.passwordType}
|
||||
radioOptions={[
|
||||
{ value: 'link', label: this.props.t("ActivationLink")},
|
||||
{ value: 'temp', label: this.props.t("TemporaryPassword")}
|
||||
{ value: 'link', label: t("ActivationLink") },
|
||||
{ value: 'temp', label: t("TemporaryPassword") }
|
||||
]}
|
||||
radioIsDisabled={this.state.isLoading}
|
||||
radioOnChange={this.onTextChange}
|
||||
radioIsDisabled={isLoading}
|
||||
radioOnChange={this.onInputChange}
|
||||
inputName="password"
|
||||
inputValue={this.state.profile.password}
|
||||
inputIsDisabled={this.state.isLoading || this.state.profile.passwordType === "link"}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputIconOnClick={this.onShowPassword}
|
||||
inputShowPassword={this.state.showPassword}
|
||||
refreshIconOnClick={()=>{}}
|
||||
copyLinkText={this.props.t("CopyEmailAndPassword")}
|
||||
copyLinkOnClick={()=>{}}
|
||||
emailInputName="email"
|
||||
inputValue={profile.password}
|
||||
inputIsDisabled={isLoading || profile.passwordType === "link"}
|
||||
inputOnChange={this.onInputChange}
|
||||
copyLinkText={t("CopyEmailAndPassword")}
|
||||
inputTabIndex={4}
|
||||
passwordSettings={settings.passwordSettings}
|
||||
/>
|
||||
<DateField
|
||||
labelText={`${this.props.t("Birthdate")}:`}
|
||||
labelText={`${t("Birthdate")}:`}
|
||||
inputName="birthday"
|
||||
inputValue={this.state.profile.birthday ? new Date(this.state.profile.birthday) : undefined}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputValue={profile.birthday ? new Date(profile.birthday) : undefined}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onBirthdayDateChange}
|
||||
inputTabIndex={5}
|
||||
/>
|
||||
<RadioField
|
||||
labelText={`${this.props.t("Sex")}:`}
|
||||
labelText={`${t("Sex")}:`}
|
||||
radioName="sex"
|
||||
radioValue={this.state.profile.sex}
|
||||
radioValue={profile.sex}
|
||||
radioOptions={[
|
||||
{ value: 'male', label: this.props.t("SexMale")},
|
||||
{ value: 'female', label: this.props.t("SexFemale")}
|
||||
{ value: 'male', label: t("SexMale") },
|
||||
{ value: 'female', label: t("SexFemale") }
|
||||
]}
|
||||
radioIsDisabled={this.state.isLoading}
|
||||
radioOnChange={this.onTextChange}
|
||||
radioIsDisabled={isLoading}
|
||||
radioOnChange={this.onInputChange}
|
||||
/>
|
||||
<DateField
|
||||
labelText={`${this.props.t("CustomEmployedSinceDate", { employedSinceDate })}:`}
|
||||
labelText={`${t("CustomEmployedSinceDate", { employedSinceDate })}:`}
|
||||
inputName="workFrom"
|
||||
inputValue={this.state.profile.workFrom ? new Date(this.state.profile.workFrom) : undefined}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputValue={profile.workFrom ? new Date(profile.workFrom) : undefined}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onWorkFromDateChange}
|
||||
inputTabIndex={6}
|
||||
/>
|
||||
<TextField
|
||||
labelText={`${this.props.t("Location")}:`}
|
||||
labelText={`${t("Location")}:`}
|
||||
inputName="location"
|
||||
inputValue={this.state.profile.location}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.location}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={7}
|
||||
/>
|
||||
<TextField
|
||||
labelText={`${this.props.t("CustomPosition", { position })}:`}
|
||||
labelText={`${t("CustomPosition", { position })}:`}
|
||||
inputName="title"
|
||||
inputValue={this.state.profile.title}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.title}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={8}
|
||||
/>
|
||||
<DepartmentField
|
||||
labelText={`${this.props.t("CustomDepartment", { department })}:`}
|
||||
departments={this.state.profile.groups}
|
||||
labelText={`${t("CustomDepartment", { department })}:`}
|
||||
departments={profile.groups}
|
||||
onRemoveDepartment={this.onGroupClose}
|
||||
/>
|
||||
</MainFieldsContainer>
|
||||
</MainContainer>
|
||||
<div>
|
||||
<Text.ContentHeader>{this.props.t("Comments")}</Text.ContentHeader>
|
||||
<Textarea name="notes" value={this.state.profile.notes} isDisabled={this.state.isLoading} onChange={this.onTextChange}/>
|
||||
<Text.ContentHeader>{t("Comments")}</Text.ContentHeader>
|
||||
<Textarea name="notes" value={profile.notes} isDisabled={isLoading} onChange={this.onInputChange} tabIndex={9}/>
|
||||
</div>
|
||||
<div style={{marginTop: "60px"}}>
|
||||
<Button label={this.props.t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={this.state.isLoading} size="big"/>
|
||||
<Button label={this.props.t("CancelButton")} onClick={this.onCancel} isDisabled={this.state.isLoading} size="big" style={{ marginLeft: "8px" }}/>
|
||||
<div style={{ marginTop: "60px" }}>
|
||||
<Button label={t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={isLoading} size="big" tabIndex={10} />
|
||||
<Button label={t("CancelButton")} onClick={this.onCancel} isDisabled={isLoading} size="big" style={{ marginLeft: "8px" }} tabIndex={11} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { withRouter } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
import { Avatar, Button, Textarea, Text, toastr, ModalDialog } from 'asc-web-components'
|
||||
import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput } from 'asc-web-components'
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { toEmployeeWrapper, getUserRole, updateProfile } from '../../../../../store/profile/actions';
|
||||
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
|
||||
@ -21,13 +21,19 @@ class UpdateUserForm extends React.Component {
|
||||
|
||||
this.validate = this.validate.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.onTextChange = this.onTextChange.bind(this);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onUserTypeChange = this.onUserTypeChange.bind(this);
|
||||
this.onBirthdayDateChange = this.onBirthdayDateChange.bind(this);
|
||||
this.onWorkFromDateChange = this.onWorkFromDateChange.bind(this);
|
||||
this.onGroupClose = this.onGroupClose.bind(this);
|
||||
this.onCancel = this.onCancel.bind(this);
|
||||
|
||||
this.onDialogShow = this.onDialogShow.bind(this);
|
||||
this.onEmailChange = this.onEmailChange.bind(this);
|
||||
this.onSendEmailChangeInstructions = this.onSendEmailChangeInstructions.bind(this);
|
||||
this.onPasswordChange = this.onPasswordChange.bind(this);
|
||||
this.onSendPasswordChangeInstructions = this.onSendPasswordChangeInstructions.bind(this);
|
||||
this.onPhoneChange = this.onPhoneChange.bind(this);
|
||||
this.onSendPhoneChangeInstructions = this.onSendPhoneChangeInstructions.bind(this);
|
||||
this.onDialogClose = this.onDialogClose.bind(this);
|
||||
}
|
||||
|
||||
@ -40,23 +46,33 @@ class UpdateUserForm extends React.Component {
|
||||
mapPropsToState = (props) => {
|
||||
return {
|
||||
isLoading: false,
|
||||
isDialogVisible: false,
|
||||
errors: {
|
||||
firstName: false,
|
||||
lastName: false,
|
||||
email: false,
|
||||
password: false,
|
||||
},
|
||||
profile: toEmployeeWrapper(props.profile)
|
||||
profile: toEmployeeWrapper(props.profile),
|
||||
dialog: {
|
||||
visible: false,
|
||||
header: "",
|
||||
body: "",
|
||||
buttons: [],
|
||||
newEmail: "",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onTextChange(event) {
|
||||
onInputChange(event) {
|
||||
var stateCopy = Object.assign({}, this.state);
|
||||
stateCopy.profile[event.target.name] = event.target.value;
|
||||
this.setState(stateCopy)
|
||||
}
|
||||
|
||||
onUserTypeChange(event) {
|
||||
var stateCopy = Object.assign({}, this.state);
|
||||
stateCopy.profile.isVisitor = event.target.value === "true";
|
||||
this.setState(stateCopy)
|
||||
}
|
||||
|
||||
onBirthdayDateChange(value) {
|
||||
var stateCopy = Object.assign({}, this.state);
|
||||
stateCopy.profile.birthday = value ? value.toJSON() : null;
|
||||
@ -76,14 +92,13 @@ class UpdateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
validate() {
|
||||
const { profile } = this.state;
|
||||
const errors = {
|
||||
firstName: !this.state.profile.firstName,
|
||||
lastName: !this.state.profile.lastName,
|
||||
email: !this.state.profile.email,
|
||||
password: this.state.profile.passwordType === "temp" && !this.state.profile.password
|
||||
firstName: !profile.firstName,
|
||||
lastName: !profile.lastName,
|
||||
};
|
||||
const hasError = errors.firstName || errors.lastName || errors.email || errors.password;
|
||||
this.setState({errors: errors});
|
||||
const hasError = errors.firstName || errors.lastName;
|
||||
this.setState({ errors: errors });
|
||||
return !hasError;
|
||||
}
|
||||
|
||||
@ -108,142 +123,241 @@ class UpdateUserForm extends React.Component {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
onDialogShow() {
|
||||
this.setState({isDialogVisible: true})
|
||||
onEmailChange(event) {
|
||||
const dialog = {
|
||||
visible: true,
|
||||
header: "Change email",
|
||||
body: (
|
||||
<Text.Body>
|
||||
<span style={{display: "block", marginBottom: "8px"}}>The activation instructions will be sent to the entered email</span>
|
||||
<TextInput
|
||||
id="new-email"
|
||||
scale={true}
|
||||
isAutoFocussed={true}
|
||||
value={event.target.value}
|
||||
onChange={this.onEmailChange}
|
||||
/>
|
||||
</Text.Body>
|
||||
),
|
||||
buttons: [
|
||||
<Button
|
||||
key="SendBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={this.onSendEmailChangeInstructions}
|
||||
/>
|
||||
],
|
||||
newEmail: event.target.value
|
||||
};
|
||||
this.setState({ dialog: dialog })
|
||||
}
|
||||
|
||||
onSendEmailChangeInstructions() {
|
||||
toastr.success("Context action: Change email");
|
||||
this.onDialogClose();
|
||||
}
|
||||
|
||||
onPasswordChange() {
|
||||
const dialog = {
|
||||
visible: true,
|
||||
header: "Change password",
|
||||
body: (
|
||||
<Text.Body>
|
||||
Send the password change instructions to the <a href={`mailto:${this.state.profile.email}`}>${this.state.profile.email}</a> email address
|
||||
</Text.Body>
|
||||
),
|
||||
buttons: [
|
||||
<Button
|
||||
key="SendBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={this.onSendPasswordChangeInstructions}
|
||||
/>
|
||||
]
|
||||
};
|
||||
this.setState({ dialog: dialog })
|
||||
}
|
||||
|
||||
onSendPasswordChangeInstructions() {
|
||||
toastr.success("Context action: Change password");
|
||||
this.onDialogClose();
|
||||
}
|
||||
|
||||
onPhoneChange() {
|
||||
const dialog = {
|
||||
visible: true,
|
||||
header: "Change phone",
|
||||
body: (
|
||||
<Text.Body>
|
||||
The instructions on how to change the user mobile number will be sent to the user email address
|
||||
</Text.Body>
|
||||
),
|
||||
buttons: [
|
||||
<Button
|
||||
key="SendBtn"
|
||||
label="Send"
|
||||
size="medium"
|
||||
primary={true}
|
||||
onClick={this.onSendPhoneChangeInstructions}
|
||||
/>
|
||||
]
|
||||
};
|
||||
this.setState({ dialog: dialog })
|
||||
}
|
||||
|
||||
onSendPhoneChangeInstructions() {
|
||||
toastr.success("Context action: Change phone");
|
||||
this.onDialogClose();
|
||||
}
|
||||
|
||||
onDialogClose() {
|
||||
this.setState({isDialogVisible: false})
|
||||
const dialog = { visible: false };
|
||||
this.setState({ dialog: dialog })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, errors, profile, dialog } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContainer>
|
||||
<AvatarContainer>
|
||||
<Avatar
|
||||
size="max"
|
||||
role={getUserRole(this.state.profile)}
|
||||
source={this.state.profile.avatarMax}
|
||||
userName={this.state.profile.displayName}
|
||||
role={getUserRole(profile)}
|
||||
source={profile.avatarMax}
|
||||
userName={profile.displayName}
|
||||
editing={true}
|
||||
editLabel={this.props.t("EditPhoto")}
|
||||
editLabel={t("EditPhoto")}
|
||||
/>
|
||||
</AvatarContainer>
|
||||
<MainFieldsContainer>
|
||||
<TextChangeField
|
||||
labelText={`${this.props.t("Email")}:`}
|
||||
labelText={`${t("Email")}:`}
|
||||
inputName="email"
|
||||
inputValue={this.state.profile.email}
|
||||
buttonText={this.props.t("ChangeButton")}
|
||||
buttonIsDisabled={this.state.isLoading}
|
||||
buttonOnClick={this.onDialogShow}
|
||||
inputValue={profile.email}
|
||||
buttonText={t("ChangeButton")}
|
||||
buttonIsDisabled={isLoading}
|
||||
buttonOnClick={this.onEmailChange}
|
||||
buttonTabIndex={1}
|
||||
/>
|
||||
<TextChangeField
|
||||
labelText={`${this.props.t("Password")}:`}
|
||||
labelText={`${t("Password")}:`}
|
||||
inputName="password"
|
||||
inputValue={this.state.profile.password}
|
||||
buttonText={this.props.t("ChangeButton")}
|
||||
buttonIsDisabled={this.state.isLoading}
|
||||
buttonOnClick={this.onDialogShow}
|
||||
inputValue={profile.password}
|
||||
buttonText={t("ChangeButton")}
|
||||
buttonIsDisabled={isLoading}
|
||||
buttonOnClick={this.onPasswordChange}
|
||||
buttonTabIndex={2}
|
||||
/>
|
||||
<TextChangeField
|
||||
labelText={`${this.props.t("Phone")}:`}
|
||||
labelText={`${t("Phone")}:`}
|
||||
inputName="phone"
|
||||
inputValue={this.state.profile.phone}
|
||||
buttonText={this.props.t("ChangeButton")}
|
||||
buttonIsDisabled={this.state.isLoading}
|
||||
buttonOnClick={this.onDialogShow}
|
||||
inputValue={profile.phone}
|
||||
buttonText={t("ChangeButton")}
|
||||
buttonIsDisabled={isLoading}
|
||||
buttonOnClick={this.onPhoneChange}
|
||||
buttonTabIndex={3}
|
||||
/>
|
||||
<TextField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.firstName}
|
||||
labelText={`${this.props.t("FirstName")}:`}
|
||||
hasError={errors.firstName}
|
||||
labelText={`${t("FirstName")}:`}
|
||||
inputName="firstName"
|
||||
inputValue={this.state.profile.firstName}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.firstName}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputAutoFocussed={true}
|
||||
inputTabIndex={4}
|
||||
/>
|
||||
<TextField
|
||||
isRequired={true}
|
||||
hasError={this.state.errors.lastName}
|
||||
labelText={`${this.props.t("LastName")}:`}
|
||||
hasError={errors.lastName}
|
||||
labelText={`${t("LastName")}:`}
|
||||
inputName="lastName"
|
||||
inputValue={this.state.profile.lastName}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.lastName}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={5}
|
||||
/>
|
||||
<DateField
|
||||
labelText={`${this.props.t("Birthdate")}:`}
|
||||
labelText={`${t("Birthdate")}:`}
|
||||
inputName="birthday"
|
||||
inputValue={this.state.profile.birthday ? new Date(this.state.profile.birthday) : undefined}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputValue={profile.birthday ? new Date(profile.birthday) : undefined}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onBirthdayDateChange}
|
||||
inputTabIndex={6}
|
||||
/>
|
||||
<RadioField
|
||||
labelText={`${this.props.t("Sex")}:`}
|
||||
labelText={`${t("Sex")}:`}
|
||||
radioName="sex"
|
||||
radioValue={this.state.profile.sex}
|
||||
radioValue={profile.sex}
|
||||
radioOptions={[
|
||||
{ value: 'male', label: this.props.t("SexMale")},
|
||||
{ value: 'female', label: this.props.t("SexFemale")}
|
||||
{ value: 'male', label: t("SexMale")},
|
||||
{ value: 'female', label: t("SexFemale")}
|
||||
]}
|
||||
radioIsDisabled={this.state.isLoading}
|
||||
radioOnChange={this.onTextChange}
|
||||
radioIsDisabled={isLoading}
|
||||
radioOnChange={this.onInputChange}
|
||||
/>
|
||||
<RadioField
|
||||
labelText={`${this.props.t("UserType")}:`}
|
||||
radioName="sex"
|
||||
radioValue={this.state.profile.isVisitor.toString()}
|
||||
labelText={`${t("UserType")}:`}
|
||||
radioName="isVisitor"
|
||||
radioValue={profile.isVisitor.toString()}
|
||||
radioOptions={[
|
||||
{ value: "true", label: this.props.t("CustomTypeGuest", { typeGuest })},
|
||||
{ value: "false", label: this.props.t("CustomTypeUser", { typeUser })}
|
||||
{ value: "true", label: t("CustomTypeGuest", { typeGuest })},
|
||||
{ value: "false", label: t("CustomTypeUser", { typeUser })}
|
||||
]}
|
||||
radioIsDisabled={this.state.isLoading}
|
||||
radioOnChange={this.onTextChange}
|
||||
radioIsDisabled={isLoading}
|
||||
radioOnChange={this.onUserTypeChange}
|
||||
/>
|
||||
<DateField
|
||||
labelText={`${this.props.t("CustomEmployedSinceDate", { employedSinceDate })}:`}
|
||||
labelText={`${t("CustomEmployedSinceDate", { employedSinceDate })}:`}
|
||||
inputName="workFrom"
|
||||
inputValue={this.state.profile.workFrom ? new Date(this.state.profile.workFrom) : undefined}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputValue={profile.workFrom ? new Date(profile.workFrom) : undefined}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onWorkFromDateChange}
|
||||
inputTabIndex={7}
|
||||
/>
|
||||
<TextField
|
||||
labelText={`${this.props.t("Location")}:`}
|
||||
labelText={`${t("Location")}:`}
|
||||
inputName="location"
|
||||
inputValue={this.state.profile.location}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.location}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={8}
|
||||
/>
|
||||
<TextField
|
||||
labelText={`${this.props.t("CustomPosition", { position })}:`}
|
||||
labelText={`${t("CustomPosition", { position })}:`}
|
||||
inputName="title"
|
||||
inputValue={this.state.profile.title}
|
||||
inputIsDisabled={this.state.isLoading}
|
||||
inputOnChange={this.onTextChange}
|
||||
inputValue={profile.title}
|
||||
inputIsDisabled={isLoading}
|
||||
inputOnChange={this.onInputChange}
|
||||
inputTabIndex={9}
|
||||
/>
|
||||
<DepartmentField
|
||||
labelText={`${this.props.t("CustomDepartment", { department })}:`}
|
||||
departments={this.state.profile.groups}
|
||||
labelText={`${t("CustomDepartment", { department })}:`}
|
||||
departments={profile.groups}
|
||||
onRemoveDepartment={this.onGroupClose}
|
||||
/>
|
||||
</MainFieldsContainer>
|
||||
</MainContainer>
|
||||
<div>
|
||||
<Text.ContentHeader>{this.props.t("Comments")}</Text.ContentHeader>
|
||||
<Textarea name="notes" value={this.state.profile.notes} isDisabled={this.state.isLoading} onChange={this.onTextChange}/>
|
||||
<Text.ContentHeader>{t("Comments")}</Text.ContentHeader>
|
||||
<Textarea name="notes" value={profile.notes} isDisabled={isLoading} onChange={this.onInputChange} tabIndex={10}/>
|
||||
</div>
|
||||
<div style={{marginTop: "60px"}}>
|
||||
<Button label={this.props.t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={this.state.isLoading} size="big"/>
|
||||
<Button label={this.props.t("CancelButton")} onClick={this.onCancel} isDisabled={this.state.isLoading} size="big" style={{ marginLeft: "8px" }}/>
|
||||
<Button label={t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={isLoading} size="big" tabIndex={11}/>
|
||||
<Button label={t("CancelButton")} onClick={this.onCancel} isDisabled={isLoading} size="big" style={{ marginLeft: "8px" }} tabIndex={12}/>
|
||||
</div>
|
||||
|
||||
<ModalDialog
|
||||
visible={this.state.isDialogVisible}
|
||||
headerContent={"Change something"}
|
||||
bodyContent={<p>Send the something instructions?</p>}
|
||||
footerContent={<Button label="Send" primary={true} onClick={this.onDialogClose} />}
|
||||
visible={dialog.visible}
|
||||
headerContent={dialog.header}
|
||||
bodyContent={dialog.body}
|
||||
footerContent={dialog.buttons}
|
||||
onClose={this.onDialogClose}
|
||||
/>
|
||||
</>
|
||||
|
34
products/ASC.People/Client/src/store/group/actions.js
Normal file
34
products/ASC.People/Client/src/store/group/actions.js
Normal file
@ -0,0 +1,34 @@
|
||||
import * as api from "../../store/services/api";
|
||||
|
||||
export const SET_GROUP = "SET_PROFILE";
|
||||
export const CLEAN_GROUP = "CLEAN_PROFILE";
|
||||
|
||||
export function setGroup(targetGroup) {
|
||||
return {
|
||||
type: SET_GROUP,
|
||||
targetGroup
|
||||
};
|
||||
}
|
||||
|
||||
export function resetGroup() {
|
||||
return {
|
||||
type: CLEAN_GROUP
|
||||
};
|
||||
}
|
||||
|
||||
export function checkResponseError(res) {
|
||||
if (res && res.data && res.data.error) {
|
||||
console.error(res.data.error);
|
||||
throw new Error(res.data.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchGroup(groupId) {
|
||||
return dispatch => {
|
||||
api.getGroup(groupId)
|
||||
.then(res => {
|
||||
checkResponseError(res);
|
||||
dispatch(setGroup(res.data.response || null));
|
||||
});
|
||||
};
|
||||
}
|
20
products/ASC.People/Client/src/store/group/reducers.js
Normal file
20
products/ASC.People/Client/src/store/group/reducers.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { SET_GROUP, CLEAN_GROUP } from "./actions";
|
||||
|
||||
const initialState = {
|
||||
targetGroup: null
|
||||
};
|
||||
|
||||
const groupReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case SET_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
targetGroup: action.targetGroup
|
||||
});
|
||||
case CLEAN_GROUP:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default groupReducer;
|
@ -42,7 +42,6 @@ export function toEmployeeWrapper(profile) {
|
||||
password: "",
|
||||
birthday: "",
|
||||
sex: "male",
|
||||
passwordType: "link",
|
||||
workFrom: "",
|
||||
location: "",
|
||||
title: "",
|
||||
|
@ -2,13 +2,13 @@ import { combineReducers } from 'redux';
|
||||
import authReducer from './auth/reducers';
|
||||
import peopleReducer from './people/reducers';
|
||||
import profileReducer from './profile/reducers';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import groupReducer from './group/reducers';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
people: peopleReducer,
|
||||
profile: profileReducer,
|
||||
form: formReducer
|
||||
group: groupReducer
|
||||
});
|
||||
|
||||
export default rootReducer;
|
@ -35,6 +35,12 @@ export function getSettings() {
|
||||
: axios.get(`${API_URL}/settings.json`);
|
||||
}
|
||||
|
||||
export function getPortalPasswordSettings() {
|
||||
return IS_FAKE
|
||||
? fakeApi.getPortalPasswordSettings()
|
||||
: axios.get(`${API_URL}/settings/security/password`);
|
||||
}
|
||||
|
||||
export function getUser(userId) {
|
||||
return IS_FAKE
|
||||
? fakeApi.getUser()
|
||||
@ -64,13 +70,17 @@ export function updateUser(data) {
|
||||
}
|
||||
|
||||
export function getInitInfo() {
|
||||
return axios.all([getUser(), getModulesList(), getSettings()]).then(
|
||||
axios.spread(function(userResp, modulesResp, settingsResp) {
|
||||
return Promise.resolve({
|
||||
return axios.all([getUser(), getModulesList(), getSettings(), getPortalPasswordSettings()]).then(
|
||||
axios.spread(function(userResp, modulesResp, settingsResp, passwordSettingsResp) {
|
||||
let info = {
|
||||
user: userResp.data.response,
|
||||
modules: modulesResp.data.response,
|
||||
settings: settingsResp.data.response
|
||||
});
|
||||
};
|
||||
|
||||
info.settings.passwordSettings = passwordSettingsResp.data.response;
|
||||
|
||||
return Promise.resolve(info);
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -119,6 +129,12 @@ export function deleteUsers(userIds) {
|
||||
.then(CheckError);
|
||||
}
|
||||
|
||||
export function getGroup(groupId) {
|
||||
return IS_FAKE
|
||||
? fakeApi.getGroup(groupId)
|
||||
: axios.get(`${API_URL}/group/${groupId}.json`);
|
||||
}
|
||||
|
||||
function CheckError(res) {
|
||||
if (res.data && res.data.error) {
|
||||
const error = res.data.error.message || "Unknown error has happened";
|
||||
|
@ -53,6 +53,17 @@ export function getSettings() {
|
||||
return fakeResponse(data);
|
||||
}
|
||||
|
||||
export function getPortalPasswordSettings() {
|
||||
const data = {
|
||||
minLength: 6,
|
||||
upperCase: false,
|
||||
digits: false,
|
||||
specSymbols: false
|
||||
};
|
||||
|
||||
return fakeResponse(data);
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
const data = {
|
||||
index: "a",
|
||||
@ -503,8 +514,36 @@ export function deleteUsers(userIds) {
|
||||
|
||||
export function sendInstructionsToDelete() {
|
||||
return fakeResponse("Instruction has been sent successfully");
|
||||
};
|
||||
}
|
||||
|
||||
export function sendInstructionsToChangePassword() {
|
||||
return fakeResponse("Instruction has been sent successfully");
|
||||
};
|
||||
}
|
||||
|
||||
export function getGroup(groupId) {
|
||||
return fakeResponse({
|
||||
id: "06448c0a-7f10-4c6d-9ad4-f94de2235778",
|
||||
name: "All domain users",
|
||||
category: "00000000-0000-0000-0000-000000000000",
|
||||
parent: "00000000-0000-0000-0000-000000000000",
|
||||
description: null,
|
||||
manager: {
|
||||
id: "646a6cff-df57-4b83-8ffe-91a24910328c",
|
||||
displayName: "Alexey Safronov",
|
||||
avatarSmall:
|
||||
"/storage/userPhotos/root/646a6cff-df57-4b83-8ffe-91a24910328c_size_32-32.png?_=1787432031",
|
||||
profileUrl:
|
||||
"http://localhost:8092/products/people/profile.aspx?user=alexey.safronov1"
|
||||
},
|
||||
members: [
|
||||
{
|
||||
id: "646a6cff-df57-4b83-8ffe-91a24910328c",
|
||||
displayName: "Alexey Safronov",
|
||||
avatarSmall:
|
||||
"/storage/userPhotos/root/646a6cff-df57-4b83-8ffe-91a24910328c_size_32-32.png?_=1787432031",
|
||||
profileUrl:
|
||||
"http://localhost:8092/products/people/profile.aspx?user=alexey.safronov1"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
@ -1774,7 +1774,7 @@ asap@~2.0.3, asap@~2.0.6:
|
||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
||||
|
||||
"asc-web-components@file:../../../packages/asc-web-components":
|
||||
version "1.0.29"
|
||||
version "1.0.33"
|
||||
dependencies:
|
||||
"@emotion/core" "10.0.16"
|
||||
prop-types "^15.7.2"
|
||||
@ -3860,11 +3860,6 @@ es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14:
|
||||
es6-symbol "~3.1.1"
|
||||
next-tick "^1.0.0"
|
||||
|
||||
es6-error@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||
|
||||
es6-iterator@2.0.3, es6-iterator@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
|
||||
@ -4953,7 +4948,7 @@ hmac-drbg@^1.0.0:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0:
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
|
||||
integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
|
||||
@ -5198,7 +5193,7 @@ immer@1.10.0:
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
||||
|
||||
immutable@3.8.2, immutable@^3.8.1:
|
||||
immutable@^3.8.1:
|
||||
version "3.8.2"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
||||
@ -6491,7 +6486,7 @@ locate-path@^3.0.0:
|
||||
p-locate "^3.0.0"
|
||||
path-exists "^3.0.0"
|
||||
|
||||
lodash-es@4.17.15, lodash-es@^4.17.15:
|
||||
lodash-es@4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
@ -9218,24 +9213,6 @@ redux-devtools-extension@^2.13.8:
|
||||
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
|
||||
integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
|
||||
|
||||
redux-form@^8.2.6:
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-8.2.6.tgz#6840bbe9ed5b2aaef9dd82e6db3e5efcfddd69b1"
|
||||
integrity sha512-krmF7wl1C753BYpEpWIVJ5NM4lUJZFZc5GFUVgblT+jprB99VVBDyBcgrZM3gWWLOcncFyNsHcKNQQcFg8Uanw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
es6-error "^4.1.1"
|
||||
hoist-non-react-statics "^3.2.1"
|
||||
invariant "^2.2.4"
|
||||
is-promise "^2.1.0"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.15"
|
||||
prop-types "^15.6.1"
|
||||
react-is "^16.7.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
optionalDependencies:
|
||||
immutable "3.8.2"
|
||||
|
||||
redux-thunk@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
57
web/ASC.Web.Client/src/components/pages/Confirm/i18n.js
Normal file
57
web/ASC.Web.Client/src/components/pages/Confirm/i18n.js
Normal file
@ -0,0 +1,57 @@
|
||||
import i18n from "i18next";
|
||||
import Backend from "i18next-xhr-backend";
|
||||
|
||||
const newInstance = i18n.createInstance();
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
newInstance
|
||||
.use(Backend)
|
||||
.init({
|
||||
lng: 'en',
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
format: function (value, format) {
|
||||
if (format === 'lowercase') return value.toLowerCase();
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: true
|
||||
},
|
||||
backend: {
|
||||
loadPath: `/locales/Confirm/{{lng}}/{{ns}}.json`
|
||||
}
|
||||
});
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: require("./locales/en/translation.json")
|
||||
}
|
||||
};
|
||||
|
||||
newInstance.init({
|
||||
resources: resources,
|
||||
lng: 'en',
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
format: function (value, format) {
|
||||
if (format === 'lowercase') return value.toLowerCase();
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default newInstance;
|
@ -1,14 +1,248 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { withRouter } from "react-router";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import { Button, TextInput, PageLayout, Text, PasswordInput } from 'asc-web-components';
|
||||
import { Container, Row, Col } from 'reactstrap';
|
||||
import styled from 'styled-components';
|
||||
import { welcomePageTitle } from './../../../helpers/customNames';
|
||||
|
||||
const ConfirmContainer = styled(Container)`
|
||||
.confirm-block-title {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.login-row {
|
||||
margin: 23px 0 0;
|
||||
}
|
||||
`;
|
||||
const mdOptions = { size: 6, offset: 3 };
|
||||
|
||||
const passwordSettings = {
|
||||
minLength: 6,
|
||||
upperCase: true,
|
||||
digits: true,
|
||||
specSymbols: true
|
||||
};
|
||||
|
||||
const Confirm = (props) => {
|
||||
const { match } = props;
|
||||
const { t } = useTranslation('translation', { i18n });
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailValid, setEmailValid] = useState(true);
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [firstNameValid, setFirstNameValid] = useState(true);
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [lastNameValid, setLastNameValid] = useState(true);
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordValid, setPasswordValid] = useState(true);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const matchStr = JSON.stringify(match);
|
||||
const queryString = window.location.search.slice(1);
|
||||
const queryParams = queryString.split('&');
|
||||
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
|
||||
// const linkParams = Object.fromEntries(arrayOfQueryParams);
|
||||
const emailRegex = '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';
|
||||
const validationEmail = new RegExp(emailRegex);
|
||||
|
||||
const onSubmit = useCallback((e) => {
|
||||
//e.preventDefault();
|
||||
|
||||
errorText && setErrorText("");
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (!validationEmail.test(email.trim())) {
|
||||
hasError = true;
|
||||
setEmailValid(!hasError);
|
||||
}
|
||||
|
||||
if (!firstName.trim()) {
|
||||
hasError = true;
|
||||
setFirstNameValid(!hasError);
|
||||
}
|
||||
|
||||
if (!lastName.trim()) {
|
||||
hasError = true;
|
||||
setLastNameValid(!hasError);
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
hasError = true;
|
||||
setPasswordValid(!hasError);
|
||||
}
|
||||
|
||||
if (hasError)
|
||||
return false;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
}, [errorText, email, firstName, lastName, password, validationEmail]);
|
||||
|
||||
const onKeyPress = useCallback((target) => {
|
||||
if (target.code === "Enter") {
|
||||
onSubmit();
|
||||
}
|
||||
}, [onSubmit]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyPress);
|
||||
window.addEventListener('keyup', onKeyPress);
|
||||
// Remove event listeners on cleanup
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyPress);
|
||||
window.removeEventListener('keyup', onKeyPress);
|
||||
};
|
||||
}, [onKeyPress]);
|
||||
|
||||
return (
|
||||
<span>{matchStr}</span>
|
||||
<ConfirmContainer>
|
||||
|
||||
<Row className='confirm-block-title'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<Text.Body as='p' fontSize={18}>{t('InviteTitle')}</Text.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<a href='/login'>
|
||||
<img className='login-row' src="images/dark_general.png" alt="Logo" />
|
||||
</a>
|
||||
<Text.Body as='p' fontSize={24} color='#116d9d'>{t('CustomWelcomePageTitle', { welcomePageTitle })}</Text.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<TextInput
|
||||
id='name'
|
||||
name='name'
|
||||
value={firstName}
|
||||
placeholder={t('FirstName')}
|
||||
size='huge'
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
isAutoFocussed={true}
|
||||
autoComplete='given-name'
|
||||
isDisabled={isLoading}
|
||||
hasError={!firstNameValid}
|
||||
onChange={event => {
|
||||
setFirstName(event.target.value);
|
||||
!firstNameValid && setFirstNameValid(true);
|
||||
errorText && setErrorText("");
|
||||
}}
|
||||
onKeyDown={event => onKeyPress(event.target)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<TextInput
|
||||
id='surname'
|
||||
name='surname'
|
||||
value={lastName}
|
||||
placeholder={t('LastName')}
|
||||
size='huge'
|
||||
scale={true}
|
||||
tabIndex={2}
|
||||
autoComplete='family-name'
|
||||
isDisabled={isLoading}
|
||||
hasError={!lastNameValid}
|
||||
onChange={event => {
|
||||
setLastName(event.target.value);
|
||||
!lastNameValid && setLastNameValid(true);
|
||||
errorText && setErrorText("");
|
||||
}}
|
||||
onKeyDown={event => onKeyPress(event.target)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<TextInput
|
||||
type="email"
|
||||
id='email'
|
||||
name='email'
|
||||
value={email}
|
||||
placeholder={t('Email')}
|
||||
size='huge'
|
||||
scale={true}
|
||||
tabIndex={3}
|
||||
autoComplete='email'
|
||||
isDisabled={isLoading}
|
||||
hasError={!emailValid}
|
||||
onChange={event => {
|
||||
setEmail(event.target.value);
|
||||
!emailValid && setEmailValid(true);
|
||||
errorText && setErrorText("");
|
||||
}}
|
||||
onKeyDown={event => onKeyPress(event.target)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<PasswordInput
|
||||
inputName="password"
|
||||
emailInputName="email"
|
||||
inputValue={password}
|
||||
placeholder={t('InvitePassword')}
|
||||
size='huge'
|
||||
scale={true}
|
||||
tabIndex={4}
|
||||
maxLength={30}
|
||||
hasError={!passwordValid}
|
||||
onChange={event => {
|
||||
setPassword(event.target.value);
|
||||
!passwordValid && setPasswordValid(true);
|
||||
errorText && setErrorText("");
|
||||
onKeyPress(event.target);
|
||||
}}
|
||||
clipActionResource={t('CopyEmailAndPassword')}
|
||||
clipEmailResource={`${t('Email')}: `}
|
||||
clipPasswordResource={`${t('InvitePassword')}: `}
|
||||
tooltipPasswordTitle={`${t('ErrorPasswordMessage')}:`}
|
||||
tooltipPasswordLength={`${t('ErrorPasswordLength', { fromNumber: 6, toNumber: 30 })}:`}
|
||||
tooltipPasswordDigits={t('ErrorPasswordNoDigits')}
|
||||
tooltipPasswordCapital={t('ErrorPasswordNoUpperCase')}
|
||||
tooltipPasswordSpecial={`${t('ErrorPasswordNoSpecialSymbols')} (!@#$%^&*)`}
|
||||
generatorSpecial="!@#$%^&*"
|
||||
passwordSettings={passwordSettings}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<Button
|
||||
primary
|
||||
size='big'
|
||||
label={t('LoginRegistryButton')}
|
||||
tabIndex={5}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* <Row className='login-row'>
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<Text.Body as='p' fontSize={14}>{t('LoginWithAccount')}</Text.Body>
|
||||
</Col>
|
||||
</Row>
|
||||
*/}
|
||||
</ConfirmContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(Confirm);
|
||||
const ConfirmForm = (props) => (<PageLayout sectionBodyContent={<Confirm {...props} />} />);
|
||||
|
||||
export default withRouter(ConfirmForm);
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"InviteTitle": "You are invited to join this portal!",
|
||||
"LoginRegistryButton": "Join",
|
||||
"LoginWithAccount": "or login with:",
|
||||
"Email": "Email",
|
||||
"InvitePassword": "Password",
|
||||
"FirstName": "First Name",
|
||||
"LastName": "Last Name",
|
||||
"CopyEmailAndPassword": "Copy email and password",
|
||||
"ErrorPasswordMessage": "Password must contain",
|
||||
"ErrorPasswordLength": "from {{fromNumber}} to {{toNumber}} characters",
|
||||
"ErrorPasswordNoDigits": "digits",
|
||||
"ErrorPasswordNoUpperCase": "capital letters",
|
||||
"ErrorPasswordNoSpecialSymbols": "special characters",
|
||||
|
||||
|
||||
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
|
||||
}
|
@ -8,6 +8,7 @@ import { login } from '../../../store/auth/actions';
|
||||
import styled from 'styled-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import { welcomePageTitle } from './../../../helpers/customNames';
|
||||
|
||||
const FormContainer = styled(Container)`
|
||||
margin-top: 70px;
|
||||
@ -94,11 +95,11 @@ const Form = props => {
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyPress);
|
||||
window.addEventListener('keyup', onKeyPress);
|
||||
// Remove event listeners on cleanup
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyPress);
|
||||
window.removeEventListener('keyup', onKeyPress);
|
||||
};
|
||||
// Remove event listeners on cleanup
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyPress);
|
||||
window.removeEventListener('keyup', onKeyPress);
|
||||
};
|
||||
}, [onKeyPress]);
|
||||
|
||||
return (
|
||||
@ -107,7 +108,7 @@ const Form = props => {
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<Card className="login-card">
|
||||
<CardImg className="card-img" src="images/dark_general.png" alt="Logo" top />
|
||||
<CardTitle className="card-title">{t('TitleCloudOfficeApplications')}</CardTitle>
|
||||
<CardTitle className="card-title">{t('CustomWelcomePageTitle', { welcomePageTitle })}</CardTitle>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -168,7 +169,7 @@ const Form = props => {
|
||||
onClick={onSubmit} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Collapse isOpen={ !!errorText }>
|
||||
<Collapse isOpen={!!errorText}>
|
||||
<Row className="login-row">
|
||||
<Col sm="12" md={mdOptions}>
|
||||
<div className="alert alert-danger">{errorText}</div>
|
||||
|
@ -4,5 +4,5 @@
|
||||
"Password": "Password",
|
||||
"RegistrationEmailWatermark": "Your registration email",
|
||||
|
||||
"TitleCloudOfficeApplications": "Cloud Office Applications"
|
||||
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
|
||||
}
|
1
web/ASC.Web.Client/src/helpers/customNames.js
Normal file
1
web/ASC.Web.Client/src/helpers/customNames.js
Normal file
@ -0,0 +1 @@
|
||||
export const welcomePageTitle = 'Cloud Office Applications';
|
@ -11,6 +11,23 @@
|
||||
"RegistrationEmailWatermark",
|
||||
"LoginButton"
|
||||
]
|
||||
},
|
||||
"Confirm": {
|
||||
"Resource": [
|
||||
"InviteTitle",
|
||||
"LoginRegistryButton",
|
||||
"LoginWithAccount",
|
||||
"Email",
|
||||
"InvitePassword",
|
||||
"FirstName",
|
||||
"CopyEmailAndPassword",
|
||||
"ErrorPasswordMessage",
|
||||
"ErrorPasswordLength",
|
||||
"ErrorPasswordNoDigits",
|
||||
"ErrorPasswordNoUpperCase",
|
||||
"ErrorPasswordNoSpecialSymbols",
|
||||
"LastName"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Layout": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "asc-web-components",
|
||||
"version": "1.0.33",
|
||||
"version": "1.0.43",
|
||||
"description": "Ascensio System SIA component library",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/asc-web-components.cjs.js",
|
||||
@ -33,6 +33,7 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"rc-tree": "^2.1.2",
|
||||
"react-autosize-textarea": "^7.0.0",
|
||||
"react-avatar-edit": "^0.8.3",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-datepicker": "^2.8.0",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
|
218
web/ASC.Web.Components/src/components/avatar-editor/index.js
Normal file
218
web/ASC.Web.Components/src/components/avatar-editor/index.js
Normal file
@ -0,0 +1,218 @@
|
||||
import React, { memo } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import PropTypes from 'prop-types'
|
||||
import ModalDialog from '../modal-dialog'
|
||||
import Button from '../button'
|
||||
import { Text } from '../text'
|
||||
import Avatar from 'react-avatar-edit'
|
||||
import { default as ASCAvatar } from '../avatar/index'
|
||||
|
||||
const StyledASCAvatar = styled(ASCAvatar)`
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
`;
|
||||
const StyledAvatarContainer = styled.div`
|
||||
text-align: center;
|
||||
div:first-child {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
class AvatarEditorBody extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
croppedImage: null,
|
||||
src: this.props.image,
|
||||
hasMaxSizeError: false
|
||||
}
|
||||
this.onCrop = this.onCrop.bind(this)
|
||||
this.onClose = this.onClose.bind(this)
|
||||
this.onBeforeFileLoad = this.onBeforeFileLoad.bind(this)
|
||||
this.onFileLoad = this.onFileLoad.bind(this)
|
||||
|
||||
}
|
||||
onClose() {
|
||||
this.props.onCloseEditor();
|
||||
this.setState({ croppedImage: null })
|
||||
}
|
||||
onCrop(croppedImage) {
|
||||
this.props.onCropImage(croppedImage);
|
||||
this.setState({ croppedImage })
|
||||
}
|
||||
onBeforeFileLoad(elem) {
|
||||
if (elem.target.files[0].size > this.props.maxSize * 1000000) {
|
||||
this.setState({
|
||||
hasMaxSizeError: true
|
||||
});
|
||||
elem.target.value = "";
|
||||
}else if(this.state.hasMaxSizeError){
|
||||
this.setState({
|
||||
hasMaxSizeError: false
|
||||
});
|
||||
};
|
||||
}
|
||||
onFileLoad(file){
|
||||
let reader = new FileReader();
|
||||
let _this = this;
|
||||
reader.onloadend = () => {
|
||||
_this.props.onFileLoad(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
width={400}
|
||||
height={295}
|
||||
imageWidth={400}
|
||||
cropRadius={50}
|
||||
onCrop={this.onCrop}
|
||||
onClose={this.onClose}
|
||||
onBeforeFileLoad={this.onBeforeFileLoad}
|
||||
onFileLoad={this.onFileLoad}
|
||||
label={this.props.label}
|
||||
src={this.state.src}
|
||||
/>
|
||||
{this.state.croppedImage && (
|
||||
<div>
|
||||
<StyledASCAvatar
|
||||
size='max'
|
||||
role='user'
|
||||
source={this.state.croppedImage}
|
||||
editing={false}
|
||||
/>
|
||||
<StyledASCAvatar
|
||||
size='big'
|
||||
role='user'
|
||||
source={this.state.croppedImage}
|
||||
editing={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.hasMaxSizeError &&
|
||||
<Text.Body as='span' color="#ED7309" isBold={true}>
|
||||
{this.props.maxSizeErrorLabel}
|
||||
</Text.Body>
|
||||
}
|
||||
</StyledAvatarContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
defaultImage: null,
|
||||
croppedImage: null,
|
||||
visible: props.value
|
||||
};
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
|
||||
this.onCropImage = this.onCropImage.bind(this);
|
||||
this.onCloseEditor = this.onCloseEditor.bind(this);
|
||||
|
||||
this.onFileLoad = this.onFileLoad.bind(this);
|
||||
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
|
||||
|
||||
}
|
||||
onFileLoad(file){
|
||||
this.setState({ defaultImage: file });
|
||||
}
|
||||
onSaveButtonClick() {
|
||||
this.props.onSave({
|
||||
defaultImage: this.state.defaultImage,
|
||||
croppedImage: this.state.croppedImage
|
||||
});
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
onCloseEditor() {
|
||||
this.setState({
|
||||
croppedImage: null
|
||||
});
|
||||
}
|
||||
onCropImage(result) {
|
||||
this.setState({
|
||||
croppedImage: result
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.setState({ visible: false });
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.visible !== prevProps.visible) {
|
||||
this.setState({ visible: this.props.visible });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
visible={this.state.visible}
|
||||
headerContent={this.props.headerLabel}
|
||||
bodyContent={
|
||||
<AvatarEditorBody
|
||||
maxSize={this.props.maxSize}
|
||||
image={this.props.image}
|
||||
onCropImage={this.onCropImage}
|
||||
onCloseEditor={this.onCloseEditor}
|
||||
label={this.props.chooseFileLabel}
|
||||
maxSizeErrorLabel={this.props.maxSizeErrorLabel}
|
||||
onFileLoad={this.onFileLoad}
|
||||
/>
|
||||
}
|
||||
footerContent={[
|
||||
<Button
|
||||
key="SaveBtn"
|
||||
label={this.props.saveButtonLabel}
|
||||
primary={true}
|
||||
onClick={this.onSaveButtonClick}
|
||||
/>,
|
||||
<Button
|
||||
key="CancelBtn"
|
||||
label={this.props.cancelButtonLabel}
|
||||
onClick={this.onClose}
|
||||
style={{ marginLeft: "8px" }}
|
||||
/>
|
||||
]}
|
||||
onClose={this.props.onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AvatarEditor.propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
headerLabel: PropTypes.string,
|
||||
chooseFileLabel: PropTypes.string,
|
||||
saveButtonLabel: PropTypes.string,
|
||||
maxSizeErrorLabel: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
cancelButtonLabel: PropTypes.string,
|
||||
maxSize: PropTypes.number,
|
||||
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
AvatarEditor.defaultProps = {
|
||||
visible: false,
|
||||
maxSize: 1, //1MB
|
||||
chooseFileLabel: 'Choose a file',
|
||||
headerLabel: 'Edit Photo',
|
||||
saveButtonLabel: 'Save',
|
||||
cancelButtonLabel: 'Cancel',
|
||||
maxSizeErrorLabel: 'File is too big'
|
||||
};
|
||||
|
||||
export default AvatarEditor;
|
||||
|
||||
|
@ -56,7 +56,7 @@ const RoleWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const ImageStyled = styled.img`
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
|
||||
@ -119,9 +119,14 @@ const EditLink = styled.div`
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
a:hover {
|
||||
border-bottom: none
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
text-decoration: underline dashed;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -175,7 +180,6 @@ const Avatar = memo(props => {
|
||||
title={editLabel}
|
||||
isTextOverflow={true}
|
||||
fontSize={14}
|
||||
isHovered={true}
|
||||
color={whiteColor}
|
||||
onClick={editAction}
|
||||
>
|
||||
|
@ -27,7 +27,7 @@ Backdrop.propTypes = {
|
||||
|
||||
Backdrop.defaultProps = {
|
||||
visible: false,
|
||||
zIndex: 100
|
||||
zIndex: 500
|
||||
};
|
||||
|
||||
export default Backdrop;
|
@ -95,8 +95,8 @@ const StyledLabel = styled.div`
|
||||
const StyledArrowIcon = styled.div`
|
||||
display: flex;
|
||||
align-self: start;
|
||||
width: 8px;
|
||||
flex: 0 0 8px;
|
||||
width: ${props => props.needDisplay ? '8px' : '0px'};
|
||||
flex: 0 0 ${props => props.needDisplay ? '8px' : '0px'};
|
||||
margin-top: ${props => props.noBorder ? `5px` : `12px`};
|
||||
margin-right: ${props => props.needDisplay ? '8px' : '0px'};
|
||||
margin-left: ${props => props.needDisplay ? 'auto' : '0px'};
|
||||
|
@ -31,6 +31,7 @@ const DateInput = props => {
|
||||
iconColor="#A3A9AE"
|
||||
onIconClick={iconClick}
|
||||
scale={true}
|
||||
tabIndex={props.tabIndex}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
|
@ -1,13 +0,0 @@
|
||||
const size = {
|
||||
mobile: "375px",
|
||||
tablet: "768px",
|
||||
desktop: "1024px"
|
||||
};
|
||||
|
||||
const device = {
|
||||
mobile: `(max-width: ${size.mobile})`,
|
||||
tablet: `(max-width: ${size.tablet})`,
|
||||
desktop: `(max-width: ${size.desktop})`
|
||||
};
|
||||
|
||||
export default device;
|
@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components';
|
||||
import device from '../device'
|
||||
import styled, { css } from 'styled-components';
|
||||
import { tablet } from '../../utils/device'
|
||||
import Label from '../label'
|
||||
|
||||
const Container = styled.div`
|
||||
const horizontalCss = css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
.field-label {
|
||||
@ -13,30 +14,26 @@ const Container = styled.div`
|
||||
margin: 0;
|
||||
width: 110px;
|
||||
}
|
||||
`
|
||||
const verticalCss = css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
.field-input {
|
||||
width: 320px;
|
||||
.field-label {
|
||||
line-height: unset;
|
||||
margin: 0 0 4px 0;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
`
|
||||
|
||||
.radio-group {
|
||||
line-height: 32px;
|
||||
display: flex;
|
||||
const Container = styled.div`
|
||||
${props => props.vertical ? verticalCss : horizontalCss }
|
||||
|
||||
label:not(:first-child) {
|
||||
margin-left: 33px;
|
||||
}
|
||||
}
|
||||
|
||||
@media ${device.tablet} {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
.field-label {
|
||||
line-height: unset;
|
||||
margin: 0 0 4px 0;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
@media ${tablet} {
|
||||
${verticalCss}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -45,9 +42,9 @@ const Body = styled.div`
|
||||
`;
|
||||
|
||||
const FieldContainer = React.memo((props) => {
|
||||
const {isRequired, hasError, labelText, className, children} = props;
|
||||
const {isVertical, className, isRequired, hasError, labelText, children} = props;
|
||||
return (
|
||||
<Container className={className}>
|
||||
<Container vertical={isVertical} className={className}>
|
||||
<Label isRequired={isRequired} error={hasError} text={labelText} className="field-label"/>
|
||||
<Body>{children}</Body>
|
||||
</Container>
|
||||
|
@ -100,7 +100,7 @@ class FilterInput extends React.Component {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
sortDirection: props.selectedFilterData.sortDirection === "asc" ? true : false,
|
||||
sortDirection: props.selectedFilterData.sortDirection === "desc" ? true : false,
|
||||
sortId: props.getSortData().findIndex(x => x.key === props.selectedFilterData.sortId) != -1 ? props.selectedFilterData.sortId : props.getSortData().length > 0 ? props.getSortData()[0].key : "",
|
||||
searchText: props.selectedFilterData.inputValue || props.value,
|
||||
|
||||
@ -114,9 +114,10 @@ class FilterInput extends React.Component {
|
||||
|
||||
this.onClickSortItem = this.onClickSortItem.bind(this);
|
||||
this.onSortDirectionClick = this.onSortDirectionClick.bind(this);
|
||||
this.onChangeSortDirection = this.onChangeSortDirection.bind(this);
|
||||
this.onSearch = this.onSearch.bind(this);
|
||||
this.onChangeFilter = this.onChangeFilter.bind(this);
|
||||
|
||||
|
||||
this.onSearchChanged = this.onSearchChanged.bind(this);
|
||||
|
||||
this.getDefaultSelectedIndex = this.getDefaultSelectedIndex.bind(this);
|
||||
@ -139,6 +140,10 @@ class FilterInput extends React.Component {
|
||||
hideFilterItems: []
|
||||
})
|
||||
}
|
||||
onChangeSortDirection(key) {
|
||||
this.onFilter(this.state.filterValues, this.state.sortId, !!key ? "desc" : "asc");
|
||||
this.setState({ sortDirection: !!key });
|
||||
}
|
||||
getDefaultSelectedIndex() {
|
||||
const sortData = this.props.getSortData();
|
||||
if (sortData.length > 0) {
|
||||
@ -147,21 +152,21 @@ class FilterInput extends React.Component {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
onClickSortItem(item) {
|
||||
this.setState({ sortId: item.key });
|
||||
this.onFilter(this.state.filterValues, item.key, this.state.sortDirection ? "asc" : "desc");
|
||||
onClickSortItem(key) {
|
||||
this.setState({ sortId: key });
|
||||
this.onFilter(this.state.filterValues, key, this.state.sortDirection ? "desc" : "asc");
|
||||
}
|
||||
onSortDirectionClick() {
|
||||
|
||||
this.onFilter(this.state.filterValues, this.state.sortId, !this.state.sortDirection ? "asc" : "desc");
|
||||
this.onFilter(this.state.filterValues, this.state.sortId, !this.state.sortDirection ? "desc" : "asc");
|
||||
this.setState({ sortDirection: !this.state.sortDirection });
|
||||
}
|
||||
onSearchChanged(value) {
|
||||
this.setState({ searchText: value });
|
||||
this.onFilter(this.state.filterValues, this.state.sortId, this.state.sortDirection ? "asc" : "desc",value);
|
||||
this.onFilter(this.state.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc", value);
|
||||
}
|
||||
onSearch(result) {
|
||||
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "asc" : "desc");
|
||||
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc");
|
||||
}
|
||||
getFilterData() {
|
||||
const _this = this;
|
||||
@ -181,12 +186,12 @@ class FilterInput extends React.Component {
|
||||
}
|
||||
clearFilter() {
|
||||
this.setState({
|
||||
searchText:'',
|
||||
searchText: '',
|
||||
filterValues: [],
|
||||
openFilterItems: [],
|
||||
hideFilterItems: []
|
||||
});
|
||||
this.onFilter([], this.state.sortId, this.state.sortDirection ? "asc" : "desc", '');
|
||||
this.onFilter([], this.state.sortId, this.state.sortDirection ? "desc" : "asc", '');
|
||||
}
|
||||
updateFilter(inputFilterItems) {
|
||||
const currentFilterItems = inputFilterItems || cloneObjectsArray(this.state.filterValues);
|
||||
@ -223,7 +228,7 @@ class FilterInput extends React.Component {
|
||||
openFilterItems: newOpenFilterItems,
|
||||
hideFilterItems: newHideFilterItems
|
||||
});
|
||||
|
||||
|
||||
} else {
|
||||
this.setState({
|
||||
openFilterItems: currentFilterItems.slice(),
|
||||
@ -247,7 +252,7 @@ class FilterInput extends React.Component {
|
||||
item.key = item.key.replace(item.group + "_", '');
|
||||
return item;
|
||||
})
|
||||
this.onFilter(filterValues.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "asc" : "desc");
|
||||
this.onFilter(filterValues.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
|
||||
}
|
||||
onFilter(filterValues, sortId, sortDirection, searchText) {
|
||||
let cloneFilterValues = cloneObjectsArray(filterValues);
|
||||
@ -267,14 +272,14 @@ class FilterInput extends React.Component {
|
||||
searchText: result.inputValue,
|
||||
filterValues: result.filterValues,
|
||||
});
|
||||
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "asc" : "desc", result.inputValue);
|
||||
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc", result.inputValue);
|
||||
}
|
||||
onFilterRender() {
|
||||
if (this.isResizeUpdate) {
|
||||
this.isResizeUpdate = false;
|
||||
}
|
||||
|
||||
if(this.searchWrapper.current && this.filterWrapper.current){
|
||||
if (this.searchWrapper.current && this.filterWrapper.current) {
|
||||
const fullWidth = this.searchWrapper.current.getBoundingClientRect().width;
|
||||
const filterWidth = this.filterWrapper.current.getBoundingClientRect().width;
|
||||
if (fullWidth <= this.minWidth || filterWidth > fullWidth / 2) this.updateFilter();
|
||||
@ -325,7 +330,7 @@ class FilterInput extends React.Component {
|
||||
item.key = item.key.replace(item.group + "_", '');
|
||||
return item;
|
||||
})
|
||||
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "asc" : "desc");
|
||||
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
|
||||
this.setState({
|
||||
filterValues: currentFilterItems,
|
||||
openFilterItems: currentFilterItems,
|
||||
@ -361,14 +366,14 @@ class FilterInput extends React.Component {
|
||||
item.key = item.key.replace(item.group + "_", '');
|
||||
return item;
|
||||
})
|
||||
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "asc" : "desc");
|
||||
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.throttledResize);
|
||||
if(this.state.filterValues.length > 0) this.updateFilter();
|
||||
if (this.state.filterValues.length > 0) this.updateFilter();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.throttledResize);
|
||||
@ -382,7 +387,7 @@ class FilterInput extends React.Component {
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
sortDirection: nextProps.selectedFilterData.sortDirection === "asc" ? true : false,
|
||||
sortDirection: nextProps.selectedFilterData.sortDirection === "desc" ? true : false,
|
||||
sortId: this.props.getSortData().findIndex(x => x.key === nextProps.selectedFilterData.sortId) != -1 ? nextProps.selectedFilterData.sortId : "",
|
||||
filterValues: internalFilterData,
|
||||
searchText: nextProps.selectedFilterData.inputValue || this.props.value
|
||||
@ -456,10 +461,13 @@ class FilterInput extends React.Component {
|
||||
<SortComboBox
|
||||
options={this.props.getSortData()}
|
||||
isDisabled={this.props.isDisabled}
|
||||
onSelect={this.onClickSortItem}
|
||||
onChangeSortId={this.onClickSortItem}
|
||||
onChangeSortDirection={this.onChangeSortDirection}
|
||||
selectedOption={this.props.getSortData().length > 0 ? this.props.getSortData().find(x => x.key === this.state.sortId) : {}}
|
||||
onButtonClick={this.onSortDirectionClick}
|
||||
sortDirection={this.state.sortDirection}
|
||||
sortDirection={+this.state.sortDirection}
|
||||
directionAscLabel={this.props.directionAscLabel}
|
||||
directionDescLabel={this.props.directionDescLabel}
|
||||
/>
|
||||
</StyledFilterInput>
|
||||
|
||||
@ -470,6 +478,8 @@ class FilterInput extends React.Component {
|
||||
FilterInput.protoTypes = {
|
||||
autoRefresh: PropTypes.bool,
|
||||
selectedFilterData: PropTypes.object,
|
||||
directionAscLabel: PropTypes.string,
|
||||
directionDescLabel: PropTypes.string
|
||||
};
|
||||
|
||||
FilterInput.defaultProps = {
|
||||
@ -479,7 +489,9 @@ FilterInput.defaultProps = {
|
||||
sortId: '',
|
||||
filterValues: [],
|
||||
searchText: ''
|
||||
}
|
||||
},
|
||||
directionAscLabel: 'A-Z',
|
||||
directionDescLabel: 'Z-A'
|
||||
};
|
||||
|
||||
export default FilterInput;
|
@ -2,37 +2,111 @@ import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import ComboBox from '../combobox'
|
||||
import IconButton from '../icon-button';
|
||||
import DropDownItem from '../drop-down-item';
|
||||
import RadioButtonGroup from '../radio-button-group'
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const StyledIconButton = styled.div`
|
||||
transform: ${state => state.sortDirection ? 'scale(1, -1)' : 'scale(1)'};
|
||||
transform: ${state => !state.sortDirection ? 'scale(1, -1)' : 'scale(1)'};
|
||||
`;
|
||||
const StyledComboBox = styled(ComboBox)`
|
||||
display: block;
|
||||
float: left;
|
||||
width: 20%;
|
||||
margin-left: 8px;
|
||||
.display-block{
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
class SortComboBox extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSelect = this.onSelect.bind(this);
|
||||
|
||||
this.state = {
|
||||
sortDirection: this.props.sortDirection
|
||||
}
|
||||
this.onChangeSortId = this.onChangeSortId.bind(this);
|
||||
this.onChangeSortDirection = this.onChangeSortDirection.bind(this);
|
||||
this.onButtonClick = this.onButtonClick.bind(this);
|
||||
|
||||
}
|
||||
onSelect(item) {
|
||||
this.props.onSelect(item);
|
||||
onButtonClick() {
|
||||
typeof this.props.onChangeSortDirection === 'function' && this.props.onChangeSortDirection(+(this.state.sortDirection === 0 ? 1 : 0));
|
||||
this.setState({
|
||||
sortDirection: this.state.sortDirection === 0 ? 1 : 0
|
||||
});
|
||||
}
|
||||
|
||||
onChangeSortId(e) {
|
||||
typeof this.props.onChangeSortId === 'function' && this.props.onChangeSortId(e.target.value);
|
||||
}
|
||||
onChangeSortDirection(e) {
|
||||
this.setState({
|
||||
sortDirection: +e.target.value
|
||||
});
|
||||
typeof this.props.onChangeSortDirection === 'function' && this.props.onChangeSortDirection(+e.target.value);
|
||||
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !isEqual(this.props, nextProps);
|
||||
if (this.props.sortDirection !== nextProps.sortDirection) {
|
||||
this.setState({
|
||||
sortDirection: nextProps.sortDirection
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return (!isEqual(this.props, nextProps) || !isEqual(this.state, nextState));
|
||||
}
|
||||
render() {
|
||||
let sortArray = this.props.options.map(function (item) {
|
||||
item.value = item.key
|
||||
return item;
|
||||
});
|
||||
let sortDirectionArray = [
|
||||
{ value: '0', label: this.props.directionAscLabel },
|
||||
{ value: '1', label: this.props.directionDescLabel }
|
||||
];
|
||||
|
||||
const advancedOptions = (
|
||||
<>
|
||||
<DropDownItem>
|
||||
<RadioButtonGroup
|
||||
className="display-block"
|
||||
onClick={this.onChangeSortDirection}
|
||||
isDisabled={this.props.isDisabled}
|
||||
selected={this.state.sortDirection.toString()}
|
||||
spacing={0}
|
||||
name={'direction'}
|
||||
options={sortDirectionArray}
|
||||
/>
|
||||
</DropDownItem>
|
||||
<DropDownItem isSeparator />
|
||||
<DropDownItem>
|
||||
<RadioButtonGroup
|
||||
className="display-block"
|
||||
onClick={this.onChangeSortId}
|
||||
isDisabled={this.props.isDisabled}
|
||||
selected={this.props.selectedOption.key}
|
||||
spacing={0}
|
||||
name={'sort'}
|
||||
options={sortArray}
|
||||
/>
|
||||
</DropDownItem>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<StyledComboBox
|
||||
options={this.props.options}
|
||||
options={[]}
|
||||
advancedOptions={advancedOptions}
|
||||
isDisabled={this.props.isDisabled}
|
||||
onSelect={this.onSelect}
|
||||
selectedOption={this.props.selectedOption}
|
||||
scaled={false}
|
||||
size="content"
|
||||
directionX="right"
|
||||
|
||||
>
|
||||
<StyledIconButton sortDirection={this.props.sortDirection}>
|
||||
<StyledIconButton sortDirection={!!this.state.sortDirection}>
|
||||
<IconButton
|
||||
color={"#D8D8D8"}
|
||||
hoverColor={"#333"}
|
||||
@ -41,7 +115,7 @@ class SortComboBox extends React.Component {
|
||||
iconName={'ZASortingIcon'}
|
||||
isFill={true}
|
||||
isDisabled={this.props.isDisabled}
|
||||
onClick={this.props.onButtonClick}
|
||||
onClick={this.onButtonClick}
|
||||
/>
|
||||
</StyledIconButton>
|
||||
</StyledComboBox>
|
||||
@ -49,4 +123,20 @@ class SortComboBox extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SortComboBox.propTypes = {
|
||||
isDisabled: PropTypes.bool,
|
||||
sortDirection: PropTypes.number,
|
||||
onChangeSortId: PropTypes.func,
|
||||
onChangeSortDirection: PropTypes.func,
|
||||
onButtonClick: PropTypes.func,
|
||||
directionAscLabel: PropTypes.string,
|
||||
directionDescLabel: PropTypes.string
|
||||
}
|
||||
|
||||
SortComboBox.defaultProps = {
|
||||
isDisabled: false,
|
||||
sortDirection: 0
|
||||
}
|
||||
|
||||
|
||||
export default SortComboBox;
|
@ -150,6 +150,7 @@ class GroupButtonsMenu extends React.PureComponent {
|
||||
fontWeight={item.fontWeight}
|
||||
disabled={item.disabled}
|
||||
onClick={this.groupButtonClick.bind(this, item)}
|
||||
{...this.props}
|
||||
>
|
||||
{item.children}
|
||||
</GroupButton>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
import NavItem from './nav-item'
|
||||
import { Text } from '../../text'
|
||||
|
||||
@ -14,7 +14,7 @@ const StyledHeader = styled.header`
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
|
||||
const StyledMain = styled.main`
|
||||
height: 100vh;
|
||||
@ -10,7 +10,7 @@ const StyledMain = styled.main`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
padding: ${props => props.fullscreen ? '0' : '56px 0 0 0'};
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
import Scrollbar from '../../scrollbar';
|
||||
|
||||
const backgroundColor = '#0F4071';
|
||||
@ -17,7 +17,7 @@ const StyledNav = styled.nav`
|
||||
width: ${props => props.opened ? '240px' : '56px'};
|
||||
z-index: 200;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
width: ${props => props.opened ? '240px' : '0'};
|
||||
}
|
||||
`;
|
||||
|
@ -2,74 +2,69 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import Backdrop from '../backdrop'
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-top-left-radius: .3rem;
|
||||
border-top-right-radius: .3rem;
|
||||
`;
|
||||
|
||||
const HeaderTitle = styled.div`
|
||||
font-size: 1.5rem;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 1rem;
|
||||
margin: -1rem -1rem -1rem auto;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
border-bottom-right-radius: .3rem;
|
||||
border-bottom-left-radius: .3rem;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0,0,0,.2);
|
||||
border-radius: .3rem;
|
||||
outline: 0;
|
||||
`;
|
||||
import { Text } from '../text'
|
||||
|
||||
const Dialog = styled.div`
|
||||
position: relative;
|
||||
width: auto;
|
||||
max-width: 500px;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
padding: 0 16px 16px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
`;
|
||||
|
||||
const HeaderText = styled(Text.ContentHeader)`
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.a`
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:before, &:after {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
content: ' ';
|
||||
height: 16px;
|
||||
width: 1px;
|
||||
background-color: #D8D8D8;
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Body = styled.div`
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
`;
|
||||
|
||||
const Footer = styled.div``;
|
||||
|
||||
const ModalDialog = props => {
|
||||
//console.log("ModalDialog render");
|
||||
const { visible, headerContent, bodyContent, footerContent, onClose } = props;
|
||||
@ -80,8 +75,8 @@ const ModalDialog = props => {
|
||||
<Dialog>
|
||||
<Content>
|
||||
<Header>
|
||||
<HeaderTitle>{headerContent}</HeaderTitle>
|
||||
<CloseButton onClick={onClose}>×</CloseButton>
|
||||
<HeaderText>{headerContent}</HeaderText>
|
||||
<CloseButton onClick={onClose}></CloseButton>
|
||||
</Header>
|
||||
<Body>{bodyContent}</Body>
|
||||
<Footer>{footerContent}</Footer>
|
||||
|
@ -113,7 +113,7 @@ class PageLayout extends React.PureComponent {
|
||||
<>
|
||||
{
|
||||
this.state.isBackdropAvailable &&
|
||||
<Backdrop visible={this.state.isBackdropVisible} onClick={this.backdropClick}/>
|
||||
<Backdrop zIndex={400} visible={this.state.isBackdropVisible} onClick={this.backdropClick}/>
|
||||
}
|
||||
{
|
||||
this.state.isArticleAvailable &&
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
|
||||
const StyledArticleHeader = styled.div`
|
||||
border-bottom: 1px solid #ECEEF1;
|
||||
height: 56px;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: ${props => props.visible ? 'block' : 'none'};
|
||||
}
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet, mobile } from '../../../utils/device'
|
||||
import { Icons } from '../../icons'
|
||||
|
||||
const StyledArticlePinPanel = styled.div`
|
||||
@ -8,11 +8,11 @@ const StyledArticlePinPanel = styled.div`
|
||||
height: 56px;
|
||||
display: none;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media ${device.mobile} {
|
||||
@media ${mobile} {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
|
||||
const StyledArticle = styled.article`
|
||||
padding: 0 16px;
|
||||
@ -12,7 +12,7 @@ const StyledArticle = styled.article`
|
||||
transition: width .3s ease-in-out;
|
||||
overflow: hidden auto;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
${props => props.visible
|
||||
? props.pinned
|
||||
? `
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import device from '../../device'
|
||||
import { tablet } from '../../../utils/device'
|
||||
import { Icons } from '../../icons'
|
||||
|
||||
const StyledSectionToggler = styled.div`
|
||||
height: 64px;
|
||||
display: none;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: ${props => props.visible ? 'block' : 'none'};
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ const StyledSection = styled.section`
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden auto;
|
||||
`;
|
||||
|
||||
class Section extends React.Component {
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../button'
|
||||
import ComboBox from '../combobox'
|
||||
import device from '../device'
|
||||
import { mobile } from '../../utils/device'
|
||||
|
||||
|
||||
const StyledPaging = styled.div`
|
||||
@ -22,7 +22,7 @@ const StyledOnPage = styled.div`
|
||||
margin-left: auto;
|
||||
margin-right: 0px;
|
||||
|
||||
@media ${device.mobile} {
|
||||
@media ${mobile} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
383
web/ASC.Web.Components/src/components/password-input/index.js
Normal file
383
web/ASC.Web.Components/src/components/password-input/index.js
Normal file
@ -0,0 +1,383 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { tablet } from '../../utils/device';
|
||||
import InputBlock from '../input-block'
|
||||
import { Icons } from '../icons'
|
||||
import Link from '../link'
|
||||
import { Text } from '../text'
|
||||
import DropDown from '../drop-down'
|
||||
|
||||
const StyledInput = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 32px;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@media ${tablet} {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const PasswordProgress = styled.div`
|
||||
${props => props.inputWidth ? `width: ${props.inputWidth};` : `flex: auto;`}
|
||||
`;
|
||||
|
||||
const NewPasswordButton = styled.div`
|
||||
margin-left: 16px;
|
||||
margin-top: -6px;
|
||||
`;
|
||||
|
||||
const CopyLink = styled.div`
|
||||
margin-top: -6px;
|
||||
margin-left: 16px;
|
||||
|
||||
@media ${tablet} {
|
||||
width: 100%;
|
||||
margin-left: 0px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Progress = styled.div`
|
||||
border: 3px solid ${props => (!props.isDisabled && props.progressColor) ? props.progressColor : 'transparent'};
|
||||
border-radius: 2px;
|
||||
margin-top: -4px;
|
||||
width: ${props => props.progressWidth ? props.progressWidth + '%' : '0%'};
|
||||
`;
|
||||
|
||||
const StyledTooltipContainer = styled(Text.Body)`
|
||||
margin: 8px 16px 16px 16px;
|
||||
`;
|
||||
|
||||
const StyledTooltipItem = styled(Text.Body)`
|
||||
margin-left: 8px;
|
||||
height: 24px;
|
||||
color: ${props => props.valid ? '#44bb00' : '#B40404'};
|
||||
`;
|
||||
|
||||
class PasswordInput extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
type: props.inputType,
|
||||
progressColor: 'transparent',
|
||||
progressWidth: 0,
|
||||
inputValue: '',
|
||||
displayTooltip: false,
|
||||
validLength: false,
|
||||
validDigits: false,
|
||||
validCapital: false,
|
||||
validSpecial: false
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
displayTooltip: true
|
||||
});
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({
|
||||
displayTooltip: false
|
||||
});
|
||||
}
|
||||
|
||||
changeInputType = () => {
|
||||
const newType = this.state.type === 'text' ? 'password' : 'text';
|
||||
|
||||
this.setState({
|
||||
type: newType
|
||||
});
|
||||
}
|
||||
|
||||
testStrength = value => {
|
||||
const { generatorSpecial, passwordSettings } = this.props;
|
||||
const specSymbols = new RegExp('[' + generatorSpecial + ']');
|
||||
|
||||
let capital;
|
||||
let digits;
|
||||
let special;
|
||||
|
||||
passwordSettings.upperCase
|
||||
? capital = /[A-Z]/.test(value)
|
||||
: capital = true;
|
||||
|
||||
passwordSettings.digits
|
||||
? digits = /\d/.test(value)
|
||||
: digits = true;
|
||||
|
||||
passwordSettings.specSymbols
|
||||
? special = specSymbols.test(value)
|
||||
: special = true;
|
||||
|
||||
return {
|
||||
digits: digits,
|
||||
capital: capital,
|
||||
special: special,
|
||||
length: value.length >= passwordSettings.minLength
|
||||
};
|
||||
}
|
||||
|
||||
checkPassword = (value) => {
|
||||
const greenColor = '#44bb00';
|
||||
const redColor = '#B40404';
|
||||
const passwordValidation = this.testStrength(value);
|
||||
const progressScore = passwordValidation.digits
|
||||
&& passwordValidation.capital
|
||||
&& passwordValidation.special
|
||||
&& passwordValidation.length;
|
||||
const progressWidth = value.length * 100 / this.props.passwordSettings.minLength;
|
||||
const progressColor = progressScore
|
||||
? greenColor
|
||||
: (value.length === 0)
|
||||
? 'transparent'
|
||||
: redColor;
|
||||
|
||||
this.setState({
|
||||
progressColor: progressColor,
|
||||
progressWidth: progressWidth > 100 ? 100 : progressWidth,
|
||||
inputValue: value,
|
||||
validLength: passwordValidation.length,
|
||||
validDigits: passwordValidation.digits,
|
||||
validCapital: passwordValidation.capital,
|
||||
validSpecial: passwordValidation.special
|
||||
});
|
||||
}
|
||||
|
||||
onChangeAction = (e) => {
|
||||
this.props.onChange && this.props.onChange(e);
|
||||
this.checkPassword(e.target.value);
|
||||
}
|
||||
|
||||
onGeneratePassword = (e) => {
|
||||
if (this.props.isDisabled)
|
||||
return e.preventDefault();
|
||||
|
||||
const newPassword = this.getNewPassword();
|
||||
this.checkPassword(newPassword);
|
||||
}
|
||||
|
||||
getNewPassword = () => {
|
||||
const { passwordSettings, generatorSpecial } = this.props;
|
||||
|
||||
const length = passwordSettings.minLength;
|
||||
const string = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const numeric = '0123456789';
|
||||
const special = generatorSpecial;
|
||||
|
||||
let password = '';
|
||||
let character = '';
|
||||
|
||||
while (password.length < length) {
|
||||
const a = Math.ceil(string.length * Math.random() * Math.random());
|
||||
const b = Math.ceil(numeric.length * Math.random() * Math.random());
|
||||
const c = Math.ceil(special.length * Math.random() * Math.random());
|
||||
|
||||
let hold = string.charAt(a);
|
||||
|
||||
if (passwordSettings.upperCase) {
|
||||
hold = (password.length % 2 == 0)
|
||||
? (hold.toUpperCase())
|
||||
: (hold);
|
||||
}
|
||||
|
||||
character += hold;
|
||||
|
||||
if (passwordSettings.digits) {
|
||||
character += numeric.charAt(b);
|
||||
}
|
||||
|
||||
if (passwordSettings.specSymbols) {
|
||||
character += special.charAt(c);
|
||||
}
|
||||
|
||||
password = character;
|
||||
}
|
||||
|
||||
password = password
|
||||
.split('')
|
||||
.sort(() => 0.5 - Math.random())
|
||||
.join('');
|
||||
|
||||
return password.substr(0, length);
|
||||
}
|
||||
|
||||
copyToClipboard = emailInputName => {
|
||||
const { clipEmailResource, clipPasswordResource, isDisabled } = this.props;
|
||||
|
||||
if (isDisabled)
|
||||
return event.preventDefault();
|
||||
|
||||
const textField = document.createElement('textarea');
|
||||
const emailValue = document.getElementsByName(emailInputName)[0].value;
|
||||
|
||||
textField.innerText = clipEmailResource + emailValue + ' | ' + clipPasswordResource + this.state.inputValue;
|
||||
|
||||
document.body.appendChild(textField);
|
||||
textField.select();
|
||||
document.execCommand('copy');
|
||||
textField.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
inputName,
|
||||
isDisabled,
|
||||
scale,
|
||||
size,
|
||||
clipActionResource,
|
||||
tooltipPasswordTitle,
|
||||
tooltipPasswordLength,
|
||||
tooltipPasswordDigits,
|
||||
tooltipPasswordCapital,
|
||||
tooltipPasswordSpecial,
|
||||
emailInputName,
|
||||
inputWidth,
|
||||
passwordSettings,
|
||||
hasError,
|
||||
hasWarning,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
maxLength
|
||||
} = this.props;
|
||||
const {
|
||||
type,
|
||||
progressColor,
|
||||
progressWidth,
|
||||
inputValue,
|
||||
validLength,
|
||||
validDigits,
|
||||
validCapital,
|
||||
validSpecial,
|
||||
displayTooltip
|
||||
} = this.state;
|
||||
|
||||
const iconsColor = isDisabled ? '#D0D5DA' : '#A3A9AE';
|
||||
|
||||
const tooltipContent = (
|
||||
<StyledTooltipContainer forwardedAs='div' title={tooltipPasswordTitle}>
|
||||
{tooltipPasswordTitle}
|
||||
<StyledTooltipItem forwardedAs='div' title={tooltipPasswordLength} valid={validLength} >
|
||||
{tooltipPasswordLength}
|
||||
</StyledTooltipItem>
|
||||
{passwordSettings.digits &&
|
||||
<StyledTooltipItem forwardedAs='div' title={tooltipPasswordDigits} valid={validDigits} >
|
||||
{tooltipPasswordDigits}
|
||||
</StyledTooltipItem>
|
||||
}
|
||||
{passwordSettings.upperCase &&
|
||||
<StyledTooltipItem forwardedAs='div' title={tooltipPasswordCapital} valid={validCapital} >
|
||||
{tooltipPasswordCapital}
|
||||
</StyledTooltipItem>
|
||||
}
|
||||
{passwordSettings.specSymbols &&
|
||||
<StyledTooltipItem forwardedAs='div' title={tooltipPasswordSpecial} valid={validSpecial} >
|
||||
{tooltipPasswordSpecial}
|
||||
</StyledTooltipItem>
|
||||
}
|
||||
</StyledTooltipContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledInput>
|
||||
<PasswordProgress inputWidth={inputWidth}>
|
||||
<InputBlock
|
||||
name={inputName}
|
||||
hasError={false}
|
||||
isDisabled={isDisabled}
|
||||
iconName='EyeIcon'
|
||||
value={inputValue}
|
||||
onIconClick={this.changeInputType}
|
||||
onChange={this.onChangeAction}
|
||||
scale={scale}
|
||||
size={size}
|
||||
type={type}
|
||||
iconColor={iconsColor}
|
||||
isIconFill={true}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
placeholder={placeholder}
|
||||
tabIndex={tabIndex}
|
||||
maxLength={maxLength}
|
||||
autoComplete='new-password'
|
||||
>
|
||||
{displayTooltip &&
|
||||
<DropDown directionY='top' manualY='150%' isOpen={true}>
|
||||
{tooltipContent}
|
||||
</DropDown>
|
||||
}
|
||||
</InputBlock>
|
||||
<Progress progressColor={progressColor} progressWidth={progressWidth} isDisabled={isDisabled} />
|
||||
</PasswordProgress>
|
||||
<NewPasswordButton>
|
||||
<Icons.RefreshIcon
|
||||
size="medium"
|
||||
color={iconsColor}
|
||||
isfill={true}
|
||||
onClick={this.onGeneratePassword}
|
||||
/>
|
||||
</NewPasswordButton>
|
||||
<CopyLink>
|
||||
<Link
|
||||
type="action"
|
||||
isHovered={true}
|
||||
fontSize={13}
|
||||
color={iconsColor}
|
||||
onClick={this.copyToClipboard.bind(this, emailInputName)}
|
||||
>
|
||||
{clipActionResource}
|
||||
</Link>
|
||||
</CopyLink>
|
||||
</StyledInput>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
inputType: PropTypes.oneOf(['text', 'password']),
|
||||
inputName: PropTypes.string,
|
||||
emailInputName: PropTypes.string.isRequired,
|
||||
inputValue: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
|
||||
isDisabled: PropTypes.bool,
|
||||
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge']),
|
||||
scale: PropTypes.bool,
|
||||
|
||||
clipActionResource: PropTypes.string,
|
||||
clipEmailResource: PropTypes.string,
|
||||
clipPasswordResource: PropTypes.string,
|
||||
|
||||
tooltipPasswordTitle: PropTypes.string,
|
||||
tooltipPasswordLength: PropTypes.string,
|
||||
tooltipPasswordDigits: PropTypes.string,
|
||||
tooltipPasswordCapital: PropTypes.string,
|
||||
tooltipPasswordSpecial: PropTypes.string,
|
||||
|
||||
generatorSpecial: PropTypes.string,
|
||||
passwordSettings: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
inputType: 'password',
|
||||
inputName: 'passwordInput',
|
||||
|
||||
size: 'base',
|
||||
scale: true,
|
||||
|
||||
clipEmailResource: 'E-mail',
|
||||
clipPasswordResource: 'Password',
|
||||
|
||||
generatorSpecial: '!@#$%^&*'
|
||||
|
||||
}
|
||||
|
||||
export default PasswordInput;
|
@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import device from '../device';
|
||||
import { tablet } from '../../utils/device';
|
||||
|
||||
const truncateCss = css`
|
||||
white-space: nowrap;
|
||||
@ -12,17 +11,17 @@ const truncateCss = css`
|
||||
|
||||
const commonCss = css`
|
||||
margin: 0 8px;
|
||||
font-family: Open Sans;
|
||||
font-family: 'Open Sans';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const RowContainer = styled.div`
|
||||
width: 100%
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
@ -35,7 +34,7 @@ const MainContainerWrapper = styled.div`
|
||||
margin-right: auto;
|
||||
min-width: 140px;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
min-width: 140px;
|
||||
margin-right: 8px;
|
||||
margin-top: 6px;
|
||||
@ -61,7 +60,7 @@ const SideContainerWrapper = styled.div`
|
||||
width: 160px;
|
||||
color: ${props => props.color && props.color};
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@ -69,7 +68,7 @@ const SideContainerWrapper = styled.div`
|
||||
const TabletSideInfo = styled.div`
|
||||
display: none;
|
||||
|
||||
@media ${device.tablet} {
|
||||
@media ${tablet} {
|
||||
display: block;
|
||||
min-width: 160px;
|
||||
margin: 0 8px;
|
||||
|
@ -1,4 +1,3 @@
|
||||
export { default as device } from './components/device'
|
||||
export { default as Button } from './components/button'
|
||||
export { default as TextInput } from './components/text-input'
|
||||
export { default as DateInput } from './components/date-input'
|
||||
@ -12,6 +11,7 @@ export { default as GroupButtonsMenu } from './components/group-buttons-menu'
|
||||
export { default as TreeMenu } from './components/tree-menu'
|
||||
export { default as TreeNode } from './components/tree-menu-node'
|
||||
export { default as Avatar } from './components/avatar'
|
||||
export { default as AvatarEditor } from './components/avatar-editor'
|
||||
export { default as RequestLoader } from './components/request-loader'
|
||||
export { default as MainButton } from './components/main-button'
|
||||
export { default as ContextMenuButton } from './components/context-menu-button'
|
||||
@ -55,3 +55,4 @@ export { default as RowContainer } from './components/row-container'
|
||||
export { default as FieldContainer } from './components/field-container'
|
||||
export { default as utils } from './utils'
|
||||
export { default as DatePicker } from './components/calendar-new/date-input'
|
||||
export { default as PasswordInput } from './components/password-input'
|
||||
|
11
web/ASC.Web.Components/src/utils/device.js
Normal file
11
web/ASC.Web.Components/src/utils/device.js
Normal file
@ -0,0 +1,11 @@
|
||||
const size = {
|
||||
mobile: "375px",
|
||||
tablet: "768px",
|
||||
desktop: "1024px"
|
||||
};
|
||||
|
||||
export const mobile = `(max-width: ${size.mobile})`;
|
||||
|
||||
export const tablet = `(max-width: ${size.tablet})`;
|
||||
|
||||
export const desktop = `(max-width: ${size.desktop})`;
|
@ -1,4 +1,5 @@
|
||||
import * as array from './array';
|
||||
import * as event from './event';
|
||||
import * as device from './device'
|
||||
|
||||
export default { array, event };
|
||||
export default { array, event, device };
|
@ -4976,6 +4976,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
|
||||
|
||||
konva@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/konva/-/konva-2.5.1.tgz#cca611a9522e831e54cf57c508a1aed3f0ceac25"
|
||||
integrity sha512-YdHEWqmbWPieqIZuLx7JFGm9Ui08hSUaSJ2k2Ml8o5giFgJ0WmxAS0DPXIM+Ty2ADRagOHZfXSJ/skwYqqlwgQ==
|
||||
|
||||
lazy-cache@^0.2.3:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
|
||||
@ -6777,6 +6782,13 @@ react-autosize-textarea@^7.0.0:
|
||||
line-height "^0.3.1"
|
||||
prop-types "^15.5.6"
|
||||
|
||||
react-avatar-edit@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-avatar-edit/-/react-avatar-edit-0.8.3.tgz#0ebf21391328fc255429bdfbc782f795827109bf"
|
||||
integrity sha512-QEedh6DjDCSI7AUsUHHtfhxApCWC5hJAoywxUA5PtUdw03iIjEurgVqPOIt1UBHhU/Zk/9amElRF3oepN9JZSg==
|
||||
dependencies:
|
||||
konva "2.5.1"
|
||||
|
||||
react-custom-scrollbars@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz#830fd9502927e97e8a78c2086813899b2a8b66db"
|
||||
|
34
web/ASC.Web.Storybook/stories/avatar-editor/README.md
Normal file
34
web/ASC.Web.Storybook/stories/avatar-editor/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Avatar Editor
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { AvatarEditor } from 'asc-web-components';
|
||||
```
|
||||
|
||||
#### Description
|
||||
|
||||
Required to display user avatar editor on page.
|
||||
|
||||
#### Usage
|
||||
|
||||
```js
|
||||
<AvatarEditor
|
||||
visible={true}
|
||||
onSave={(data) =>{console.log(data.croppedImage, data.defaultImage)}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------------ | -------- | :------: | ----------------------------------------- | ------------------ | ----------------------------------------------------- |
|
||||
| `visible` | `bool` | - | | `false` | Display avatar editor or not |
|
||||
| `chooseFileLabel` | `string` | - | | `Choose a file` | |
|
||||
| `headerLabel` | `string` | - | | `Edit Photo` | |
|
||||
| `saveButtonLabel` | `string` | - | | `Save` | |
|
||||
| `cancelButtonLabel` | `string` | - | | `Cancel` | |
|
||||
| `maxSizeErrorLabel` | `string` | - | | `File is too big` | |
|
||||
| `maxSize` | `number` | - | | `1` | Max size of image |
|
||||
| `onSave` | `function` | - | | | |
|
||||
| `onClose` | `function` | - | | | |
|
72
web/ASC.Web.Storybook/stories/avatar-editor/index.stories.js
Normal file
72
web/ASC.Web.Storybook/stories/avatar-editor/index.stories.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withKnobs, boolean, text, select } from '@storybook/addon-knobs/react';
|
||||
import withReadme from 'storybook-readme/with-readme';
|
||||
import Readme from './README.md';
|
||||
import { AvatarEditor, Avatar } from 'asc-web-components';
|
||||
import Section from '../../.storybook/decorators/section';
|
||||
|
||||
class AvatarEditorStory extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
userImage: null
|
||||
}
|
||||
|
||||
this.openEditor = this.openEditor.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onSave = this.onSave.bind(this);
|
||||
|
||||
}
|
||||
onSave(result){
|
||||
action('onSave')(result);
|
||||
this.setState({
|
||||
userImage: result.croppedImage,
|
||||
isOpen: false
|
||||
})
|
||||
}
|
||||
openEditor(){
|
||||
this.setState({
|
||||
isOpen: true
|
||||
})
|
||||
}
|
||||
onClose(){
|
||||
action('onClose');
|
||||
this.setState({
|
||||
isOpen: false
|
||||
})
|
||||
}
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<Avatar
|
||||
size='max'
|
||||
role='user'
|
||||
source={this.state.userImage }
|
||||
editing={true}
|
||||
editAction={this.openEditor}
|
||||
/>
|
||||
<AvatarEditor
|
||||
visible={this.state.isOpen}
|
||||
onClose={this.onClose}
|
||||
onSave={this.onSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
storiesOf('Components|AvatarEditor', module)
|
||||
.addDecorator(withKnobs)
|
||||
.addDecorator(withReadme(Readme))
|
||||
.add('avatar editor', () => {
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<AvatarEditorStory />
|
||||
</Section>
|
||||
);
|
||||
});
|
@ -16,7 +16,7 @@ Responsive form field container
|
||||
|
||||
<FieldContainer labelText="Name:">
|
||||
<TextInput/>
|
||||
</FieldContainer>
|
||||
</FieldContainer>
|
||||
|
||||
```
|
||||
|
||||
@ -24,6 +24,7 @@ Responsive form field container
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------| -------- | :------: | -------| ------- | -------------------------------------------- |
|
||||
| `isVertical`| `bool` | - | - | false | Vertical or horizontal alignment |
|
||||
| `isRequired`| `bool` | - | - | false | Indicates that the field is required to fill |
|
||||
| `hasError` | `bool` | - | - | - | Indicates that the field is incorrect |
|
||||
| `hasError` | `bool` | - | - | false | Indicates that the field is incorrect |
|
||||
| `labelText` | `string` | - | - | - | Field label text |
|
@ -20,6 +20,7 @@ storiesOf('Components|FieldContainer', module)
|
||||
{({ value, set }) => (
|
||||
<Section>
|
||||
<FieldContainer
|
||||
isVertical={boolean('isVertical', false)}
|
||||
isRequired={boolean('isRequired', false)}
|
||||
hasError={boolean('hasError', false)}
|
||||
labelText={text('labelText', 'Name:')}
|
||||
|
@ -82,17 +82,17 @@ storiesOf('Components|Input', module)
|
||||
|
||||
const advancedOptions =
|
||||
<>
|
||||
<DropDownItem>
|
||||
<DropDownItem key='1'>
|
||||
<RadioButton value='asc' name='first' label='A-Z' isChecked={true} />
|
||||
</DropDownItem>
|
||||
<DropDownItem >
|
||||
<DropDownItem key='2'>
|
||||
<RadioButton value='desc' name='first' label='Z-A' />
|
||||
</DropDownItem>
|
||||
<DropDownItem isSeparator />
|
||||
<DropDownItem>
|
||||
<DropDownItem key='3' isSeparator />
|
||||
<DropDownItem key='4'>
|
||||
<RadioButton value='first' name='second' label='First name' />
|
||||
</DropDownItem>
|
||||
<DropDownItem>
|
||||
<DropDownItem key='5'>
|
||||
<RadioButton value='last' name='second' label='Last name' isChecked={true} />
|
||||
</DropDownItem>
|
||||
</>;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user