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

@ -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,38 +1,12 @@
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,
@ -41,19 +15,17 @@ const PasswordField = React.memo((props) => {
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}
<PasswordInput
inputName={inputName}
emailInputName={emailInputName}
inputValue={inputValue}
inputWidth="320px"
inputTabIndex={inputTabIndex}
onChange={inputOnChange}
scale={true}
type={inputShowPassword ? "text" : "password"}
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}
/>
<Icons.RefreshIcon
size="medium"
onClick={refreshIconOnClick}
className="refresh-btn"
/>
</InputContainer>
<Link
type="action"
isHovered={true}
onClick={copyLinkOnClick}
className="copy-link"
>
{copyLinkText}
</Link>
</PasswordBlock>
</FieldContainer>
);
});

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,16 +74,14 @@ 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 });
@ -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" }}/>
<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,13 +92,12 @@ 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;
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;
@ -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>

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,23 +14,12 @@ const Container = styled.div`
margin: 0;
width: 110px;
}
.field-input {
width: 320px;
}
.radio-group {
line-height: 32px;
`
const verticalCss = css`
display: flex;
label:not(:first-child) {
margin-left: 33px;
}
}
@media ${device.tablet} {
flex-direction: column;
align-items: start;
margin: 0 0 16px 0;
.field-label {
line-height: unset;
@ -37,6 +27,13 @@ const Container = styled.div`
width: auto;
flex-grow: 1;
}
`
const Container = styled.div`
${props => props.vertical ? verticalCss : horizontalCss }
@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,6 +114,7 @@ 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);
@ -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;
@ -186,7 +191,7 @@ class FilterInput extends React.Component {
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);
@ -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,7 +272,7 @@ 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) {
@ -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,7 +366,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");
}
}
@ -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
}
onSelect(item) {
this.props.onSelect(item);
this.onChangeSortId = this.onChangeSortId.bind(this);
this.onChangeSortDirection = this.onChangeSortDirection.bind(this);
this.onButtonClick = this.onButtonClick.bind(this);
}
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

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

View File

@ -0,0 +1,79 @@
# PasswordInput
#### Description
Password entry field with advanced capabilities for displaying, validation of correspondence and generation based on settings.
Object with settings:
```js
{
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
}
```
Check for compliance with settings is carried out on fly. As you type in required number of characters, progress bar will fill up and when all conditions are met, the color will change from red to green.
Depending on screen width of device, input will change location of elements.
When setting focus to input, tooltip will be shown with progress in fulfilling conditions specified in settings. When unfocused, tooltip disappears.
You can apply all the parameters of the InputBlock component to the component.
#### Usage
```js
import { PasswordInput } from "asc-web-components";
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
};
<PasswordInput
inputName="demoPasswordInput"
emailInputName="demoEmailInput"
inputValue={value}
onChange={e => {
set(e.target.value);
}}
clipActionResource="Copy e-mail and password"
clipEmailResource="E-mail: "
clipPasswordResource="Password: "
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength="from 6 to 30 characters"
tooltipPasswordDigits="digits"
tooltipPasswordCapital="capital letters"
tooltipPasswordSpecial="special characters (!@#$%^&*)"
generatorSpecial="!@#$%^&*"
passwordSettings={settings}
isDisabled={false}
/>;
```
#### Properties
| Props | Type | Required | Values | Default | Description |
| ------------------- | --------- | :------: | ------------------ | --------- | --------------------------------------------------------- |
| `inputType` | `array` | - | `text`, `password` | `password`| It is necessary for correct display of values inside input|
| `inputName` | `string` | - | - | `passwordInput`| Input name |
| `emailInputName` | `string` | ✅ | - | - | Required to associate password field with email field |
| `inputValue` | `string` | - | - | - | Input value |
| `onChange` | `func` | - | - | - | Will be triggered whenever an PasswordInput typing |
| `clipActionResource`| `string` | - | - | - | Translation of text for copying email data and password |
| `clipEmailResource` | `string` | - | - | `E-mail` | Text translation email to copy |
| `clipPasswordResource`| `string` | - | - | `Password`| Text translation password to copy |
| `tooltipPasswordTitle`| `string` | - | - | - | Text translation tooltip |
| `tooltipPasswordLength`| `string` | - | - | - | Password text translation is long tooltip |
| `tooltipPasswordDigits`| `string` | - | - | - | Digit text translation tooltip |
| `tooltipPasswordCapital`| `string` |- | - | - | Capital text translation tooltip |
| `tooltipPasswordSpecial`| `string` |- | - | - | Special text translation tooltip |
| `generatorSpecial` | `string` | - | - | `!@#$%^&*`| Set of special characters for password generator and validator|
| `passwordSettings` | `object` | ✅ | - | - | Set of settings for password generator and validator |
| `isDisabled` | `bool` | - | - | `false` | Set input disabled |
| `inputWidth` | `string` | - | - | - | If you need to set input width manually |

View File

@ -0,0 +1,68 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { StringValue } from 'react-values';
import { withKnobs, boolean, text, select, number } from '@storybook/addon-knobs/react';
import withReadme from 'storybook-readme/with-readme';
import Readme from './README.md';
import { PasswordInput, TextInput } from 'asc-web-components';
import Section from '../../../.storybook/decorators/section';
storiesOf('Components|Input', module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('advanced password', () => {
const isDisabled = boolean('isDisabled', false);
const settingsUpperCase = boolean('settingsUpperCase', false);
const settingsDigits = boolean('settingsDigits', false);
const settingsSpecSymbols = boolean('settingsSpecSymbols', false);
const fakeSettings = {
minLength: 6,
upperCase: settingsUpperCase,
digits: settingsDigits,
specSymbols: settingsSpecSymbols
};
const tooltipPasswordLength = 'from ' + fakeSettings.minLength + ' to 30 characters';
return (
<Section>
<div style={{height: '110px'}}></div>
<TextInput
name='demoEmailInput'
size='base'
isDisabled={isDisabled}
isReadOnly={true}
scale={true}
value='demo@gmail.com'
/>
<br />
<StringValue>
{({ value, set }) => (
<PasswordInput
inputName='demoPasswordInput'
emailInputName='demoEmailInput'
inputValue={value}
onChange={e => {
set(e.target.value);
}}
clipActionResource='Copy e-mail and password'
clipEmailResource='E-mail: '
clipPasswordResource='Password: '
tooltipPasswordTitle='Password must contain:'
tooltipPasswordLength={tooltipPasswordLength}
tooltipPasswordDigits='digits'
tooltipPasswordCapital='capital letters'
tooltipPasswordSpecial='special characters (!@#$%^&*)'
generatorSpecial='!@#$%^&*'
passwordSettings={fakeSettings}
isDisabled={isDisabled}
placeholder='password'
maxLength={30}
/>
)}
</StringValue>
</Section>
)
});

Some files were not shown because too many files have changed in this diff Show More