This commit is contained in:
Nikita Gopienko 2019-09-06 11:54:46 +03:00
commit 154965b1ba
104 changed files with 4714 additions and 646 deletions

1
build/install/snap/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
!*/

View File

@ -0,0 +1,7 @@
#!/bin/bash
rm -dfr parts
rm -dfr prime
rm -dfr stage
#snapcraft

View File

@ -0,0 +1,4 @@
!GlobalState
assets:
build-packages: []
build-snaps: []

View 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])

View 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
View 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

View 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
}

View 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

View 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

View 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

View 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
}

View 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;" "$@"

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
location /.well-known/acme-challenge {
root /var/www/onlyoffice/Data/certs/;
}

View File

@ -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;
}

View File

@ -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 / /;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View 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;
}

View 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

View 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

View File

@ -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>

View File

@ -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>

View 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";
}

View 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()

View 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 -

File diff suppressed because it is too large Load Diff

View 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
}

View File

@ -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"

View File

@ -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));

View File

@ -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

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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);

View File

@ -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" }}

View File

@ -198,6 +198,8 @@ const SectionFilterContent = ({
getSortData={getSortData.bind(this, t)}
selectedFilterData={selectedFilterData}
onFilter={onFilter}
directionAscLabel={t("DirectionAscLabel")}
directionDescLabel={t("DirectionDescLabel")}
/>
);
};

View File

@ -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)));

View File

@ -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"
}

View File

@ -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>
);

View File

@ -6,7 +6,7 @@ const DepartmentField = React.memo((props) => {
isRequired,
hasError,
labelText,
departments,
onRemoveDepartment
} = props;

View File

@ -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;
}
`;

View File

@ -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>
);
});

View File

@ -6,7 +6,7 @@ const RadioField = React.memo((props) => {
isRequired,
hasError,
labelText,
radioName,
radioValue,
radioOptions,

View File

@ -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>

View File

@ -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>
);

View File

@ -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>
</>
);

View File

@ -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}
/>
</>

View 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));
});
};
}

View 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;

View File

@ -42,7 +42,6 @@ export function toEmployeeWrapper(profile) {
password: "",
birthday: "",
sex: "male",
passwordType: "link",
workFrom: "",
location: "",
title: "",

View File

@ -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;

View File

@ -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";

View File

@ -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"
}
]
});
}

View File

@ -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"

View 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;

View File

@ -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);

View File

@ -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}}"
}

View File

@ -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>

View File

@ -4,5 +4,5 @@
"Password": "Password",
"RegistrationEmailWatermark": "Your registration email",
"TitleCloudOfficeApplications": "Cloud Office Applications"
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
}

View File

@ -0,0 +1 @@
export const welcomePageTitle = 'Cloud Office Applications';

View File

@ -11,6 +11,23 @@
"RegistrationEmailWatermark",
"LoginButton"
]
},
"Confirm": {
"Resource": [
"InviteTitle",
"LoginRegistryButton",
"LoginWithAccount",
"Email",
"InvitePassword",
"FirstName",
"CopyEmailAndPassword",
"ErrorPasswordMessage",
"ErrorPasswordLength",
"ErrorPasswordNoDigits",
"ErrorPasswordNoUpperCase",
"ErrorPasswordNoSpecialSymbols",
"LastName"
]
}
},
"Layout": {

View File

@ -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",

View 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;

View File

@ -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}
>

View File

@ -27,7 +27,7 @@ Backdrop.propTypes = {
Backdrop.defaultProps = {
visible: false,
zIndex: 100
zIndex: 500
};
export default Backdrop;

View File

@ -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'};

View File

@ -31,6 +31,7 @@ const DateInput = props => {
iconColor="#A3A9AE"
onIconClick={iconClick}
scale={true}
tabIndex={props.tabIndex}
/>
}
{...props}

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
`;

View File

@ -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'};
}
`;

View File

@ -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'};
}
`;

View File

@ -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>

View File

@ -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 &&

View File

@ -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'};
}
`;

View File

@ -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;
}

View File

@ -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
? `

View File

@ -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'};
}

View File

@ -6,7 +6,6 @@ const StyledSection = styled.section`
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden auto;
`;
class Section extends React.Component {

View File

@ -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;
}
`;

View 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;

View File

@ -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;

View File

@ -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'

View 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})`;

View File

@ -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 };

View File

@ -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"

View 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` | - | | | |

View 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>
);
});

View File

@ -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 |

View File

@ -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:')}

View File

@ -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