first commit
This commit is contained in:
34
backend/.gitignore
vendored
Normal file
34
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
node_modules
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||
295
backend/mvnw
vendored
Executable file
295
backend/mvnw
vendored
Executable file
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
backend/mvnw.cmd
vendored
Normal file
189
backend/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
88
backend/pom.xml
Normal file
88
backend/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.3</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>de.assecutor.hha</groupId>
|
||||
<artifactId>hha-backend</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>HHA Backend</name>
|
||||
<description>Spring Boot and Vaadin backoffice module for the HHA repository.</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<vaadin.version>25.0.7</vaadin.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-bom</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-dev</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webmvc-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-maven-plugin</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-frontend</id>
|
||||
<goals>
|
||||
<goal>build-frontend</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
1
backend/src/main/frontend/generated/app-shell-imports.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/app-shell-imports.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
1
backend/src/main/frontend/generated/css.generated.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/css.generated.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare const applyCss: (target: Node) => void;
|
||||
706
backend/src/main/frontend/generated/flow/Flow.tsx
Normal file
706
backend/src/main/frontend/generated/flow/Flow.tsx
Normal file
@@ -0,0 +1,706 @@
|
||||
/*
|
||||
* Copyright 2000-2026 Vaadin Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
/// <reference lib="es2018" />
|
||||
import { Flow as _Flow } from 'Frontend/generated/jar-resources/Flow.js';
|
||||
import React, { useCallback, useEffect, useReducer, useRef, useState, type ReactNode } from 'react';
|
||||
import { matchRoutes, useBlocker, useLocation, useNavigate, type NavigateOptions, useHref } from 'react-router';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const flow = new _Flow({
|
||||
imports: () => import('Frontend/generated/flow/generated-flow-imports.js')
|
||||
});
|
||||
|
||||
const router = {
|
||||
render() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const flowReact : { active: boolean } = {
|
||||
active: false,
|
||||
}
|
||||
|
||||
// ClickHandler for vaadin-router-go event is copied from vaadin/router click.js
|
||||
// @ts-ignore
|
||||
function getAnchorOrigin(anchor) {
|
||||
// IE11: on HTTP and HTTPS the default port is not included into
|
||||
// window.location.origin, so won't include it here either.
|
||||
const port = anchor.port;
|
||||
const protocol = anchor.protocol;
|
||||
const defaultHttp = protocol === 'http:' && port === '80';
|
||||
const defaultHttps = protocol === 'https:' && port === '443';
|
||||
const host =
|
||||
defaultHttp || defaultHttps
|
||||
? anchor.hostname // does not include the port number (e.g. www.example.org)
|
||||
: anchor.host; // does include the port number (e.g. www.example.org:80)
|
||||
return `${protocol}//${host}`;
|
||||
}
|
||||
|
||||
function normalizeURL(url: URL): void | string {
|
||||
// ignore click if baseURI does not match the document (external)
|
||||
if (!url.href.startsWith(document.baseURI)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize path against baseURI
|
||||
return '/' + url.href.slice(document.baseURI.length);
|
||||
}
|
||||
|
||||
function extractURL(event: MouseEvent): void | URL {
|
||||
// ignore the click if the default action is prevented
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if not with the primary mouse button
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if a modifier key is pressed
|
||||
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find the <a> element that the click is at (or within)
|
||||
let maybeAnchor = event.target;
|
||||
const path = event.composedPath
|
||||
? event.composedPath()
|
||||
: // @ts-ignore
|
||||
event.path || [];
|
||||
|
||||
// example to check: `for...of` loop here throws the "Not yet implemented" error
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const target = path[i];
|
||||
if (target.nodeName && target.nodeName.toLowerCase() === 'a') {
|
||||
maybeAnchor = target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
while (maybeAnchor && maybeAnchor.nodeName.toLowerCase() !== 'a') {
|
||||
// @ts-ignore
|
||||
maybeAnchor = maybeAnchor.parentNode;
|
||||
}
|
||||
|
||||
// ignore the click if not at an <a> element
|
||||
// @ts-ignore
|
||||
if (!maybeAnchor || maybeAnchor.nodeName.toLowerCase() !== 'a') {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = maybeAnchor as HTMLAnchorElement;
|
||||
|
||||
// ignore the click if the <a> element has a non-default target
|
||||
if (anchor.target && anchor.target.toLowerCase() !== '_self') {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if the <a> element has the 'download' attribute
|
||||
if (anchor.hasAttribute('download')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if the <a> element has the 'router-ignore' attribute
|
||||
if (anchor.hasAttribute('router-ignore')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if the target URL is a fragment on the current page
|
||||
if (anchor.pathname === window.location.pathname && anchor.hash !== '') {
|
||||
// @ts-ignore
|
||||
window.location.hash = anchor.hash;
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore the click if the target is external to the app
|
||||
// In IE11 HTMLAnchorElement does not have the `origin` property
|
||||
// @ts-ignore
|
||||
const origin = anchor.origin || getAnchorOrigin(anchor);
|
||||
if (origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new URL(anchor.href, anchor.baseURI);
|
||||
}
|
||||
|
||||
function extractPath(event: MouseEvent): void | string {
|
||||
const url = extractURL(event);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
return normalizeURL(url);
|
||||
}
|
||||
|
||||
export const registerGlobalClickHandler = () => {
|
||||
window.addEventListener('click', (event: MouseEvent) => {
|
||||
if (flowReact.active) {
|
||||
return;
|
||||
}
|
||||
const url = extractURL(event);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
// ignore click if baseURI does not match the document (external)
|
||||
if (!url.href.startsWith(document.baseURI)) {
|
||||
return;
|
||||
}
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Normalize path against baseURI
|
||||
const path = url.pathname + url.search + url.hash;
|
||||
const state = {...window.history.state}
|
||||
if (state.idx !== undefined) {
|
||||
state.idx = state.idx + 1;
|
||||
}
|
||||
window.history.pushState(state, '', path);
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, { capture: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire 'vaadin-navigated' event to inform components of navigation.
|
||||
* @param pathname pathname of navigation
|
||||
* @param search search of navigation
|
||||
*/
|
||||
function fireNavigated(pathname: string, search: string) {
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('vaadin-navigated', {
|
||||
detail: {
|
||||
pathname,
|
||||
search
|
||||
}
|
||||
})
|
||||
);
|
||||
// @ts-ignore
|
||||
delete window.Vaadin.Flow.navigation;
|
||||
});
|
||||
}
|
||||
|
||||
function postpone() {}
|
||||
|
||||
const prevent = () => postpone;
|
||||
|
||||
type RouterContainer = Awaited<ReturnType<(typeof flow.serverSideRoutes)[0]['action']>>;
|
||||
|
||||
type PortalEntry = {
|
||||
readonly children: ReactNode;
|
||||
readonly domNode: HTMLElement;
|
||||
};
|
||||
|
||||
type FlowPortalProps = React.PropsWithChildren<
|
||||
Readonly<{
|
||||
domNode: HTMLElement;
|
||||
onRemove(): void;
|
||||
}>
|
||||
>;
|
||||
|
||||
function FlowPortal({ children, domNode, onRemove }: FlowPortalProps) {
|
||||
useEffect(() => {
|
||||
domNode.addEventListener(
|
||||
'flow-portal-remove',
|
||||
(event: Event) => {
|
||||
event.preventDefault();
|
||||
onRemove();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}, []);
|
||||
|
||||
return createPortal(children, domNode);
|
||||
}
|
||||
|
||||
const ADD_FLOW_PORTAL = 'ADD_FLOW_PORTAL';
|
||||
|
||||
type AddFlowPortalAction = Readonly<{
|
||||
type: typeof ADD_FLOW_PORTAL;
|
||||
portal: React.ReactElement<FlowPortalProps>;
|
||||
}>;
|
||||
|
||||
function addFlowPortal(portal: React.ReactElement<FlowPortalProps>): AddFlowPortalAction {
|
||||
return {
|
||||
type: ADD_FLOW_PORTAL,
|
||||
portal
|
||||
};
|
||||
}
|
||||
|
||||
const REMOVE_FLOW_PORTAL = 'REMOVE_FLOW_PORTAL';
|
||||
|
||||
type RemoveFlowPortalAction = Readonly<{
|
||||
type: typeof REMOVE_FLOW_PORTAL;
|
||||
key: string;
|
||||
}>;
|
||||
|
||||
function removeFlowPortal(key: string): RemoveFlowPortalAction {
|
||||
return {
|
||||
type: REMOVE_FLOW_PORTAL,
|
||||
key
|
||||
};
|
||||
}
|
||||
|
||||
function flowPortalsReducer(
|
||||
portals: readonly React.ReactElement<FlowPortalProps>[],
|
||||
action: AddFlowPortalAction | RemoveFlowPortalAction
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ADD_FLOW_PORTAL:
|
||||
return [...portals, action.portal];
|
||||
case REMOVE_FLOW_PORTAL:
|
||||
return portals.filter(({ key }) => key !== action.key);
|
||||
default:
|
||||
return portals;
|
||||
}
|
||||
}
|
||||
|
||||
type NavigateOpts = {
|
||||
to: string;
|
||||
callback: boolean;
|
||||
opts?: NavigateOptions;
|
||||
};
|
||||
|
||||
type NavigateFn = (to: string, callback: boolean, opts?: NavigateOptions) => void;
|
||||
|
||||
let navigateInProgress = false;
|
||||
/**
|
||||
* A hook providing the `navigate(path: string, opts?: NavigateOptions)` function
|
||||
* with React Router API that has more consistent history updates. Uses internal
|
||||
* queue for processing navigate calls.
|
||||
*/
|
||||
function useQueuedNavigate(
|
||||
waitReference: React.MutableRefObject<Promise<void> | undefined>,
|
||||
navigated: React.MutableRefObject<boolean>
|
||||
): NavigateFn {
|
||||
const navigate = useNavigate();
|
||||
const navigateQueue = useRef<NavigateOpts[]>([]).current;
|
||||
const [navigateQueueLength, setNavigateQueueLength] = useState(0);
|
||||
|
||||
const dequeueNavigation = useCallback(() => {
|
||||
if (navigateInProgress) {
|
||||
dequeueNavigationAfterCurrentTask();
|
||||
return;
|
||||
}
|
||||
|
||||
const navigateArgs = navigateQueue.shift();
|
||||
if (navigateArgs === undefined) {
|
||||
// Empty queue, do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
const blockingNavigate = async () => {
|
||||
if (waitReference.current) {
|
||||
await waitReference.current;
|
||||
waitReference.current = undefined;
|
||||
}
|
||||
navigated.current = !navigateArgs.callback;
|
||||
navigateInProgress = true;
|
||||
navigate(navigateArgs.to, navigateArgs.opts);
|
||||
setNavigateQueueLength(navigateQueue.length);
|
||||
};
|
||||
blockingNavigate();
|
||||
}, [navigate, setNavigateQueueLength]);
|
||||
|
||||
const dequeueNavigationAfterCurrentTask = useCallback(() => {
|
||||
setTimeout(dequeueNavigation, 0);
|
||||
}, [dequeueNavigation]);
|
||||
|
||||
const enqueueNavigation = useCallback(
|
||||
(to: string, callback: boolean, opts?: NavigateOptions) => {
|
||||
navigateQueue.push({ to: to, callback: callback, opts: opts });
|
||||
setNavigateQueueLength(navigateQueue.length);
|
||||
if (navigateQueue.length === 1) {
|
||||
// The first navigation can be started right after any pending sync
|
||||
// jobs, which could add more navigations to the queue.
|
||||
dequeueNavigationAfterCurrentTask();
|
||||
}
|
||||
},
|
||||
[setNavigateQueueLength, dequeueNavigationAfterCurrentTask]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// The Flow component has rendered, but history might not be
|
||||
// updated yet, as React Router does it asynchronously.
|
||||
// Use microtask callback for history consistency.
|
||||
dequeueNavigationAfterCurrentTask();
|
||||
},
|
||||
[navigateQueueLength, dequeueNavigationAfterCurrentTask]
|
||||
);
|
||||
|
||||
return enqueueNavigation;
|
||||
}
|
||||
|
||||
const flowNavigation = () => {
|
||||
// @ts-ignore
|
||||
window.Vaadin.Flow.navigation = true;
|
||||
};
|
||||
|
||||
function Flow() {
|
||||
const ref = useRef<HTMLOutputElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||
navigated.current =
|
||||
navigated.current ||
|
||||
(nextLocation.pathname === currentLocation.pathname &&
|
||||
nextLocation.search === currentLocation.search &&
|
||||
nextLocation.hash === currentLocation.hash);
|
||||
return true;
|
||||
});
|
||||
const location = useLocation();
|
||||
const navigated = useRef<boolean>(false);
|
||||
const blockerHandled = useRef<boolean>(false);
|
||||
const fromAnchor = useRef<boolean>(false);
|
||||
const containerRef = useRef<RouterContainer | undefined>(undefined);
|
||||
const roundTrip = useRef<Promise<void> | undefined>(undefined);
|
||||
const queuedNavigate = useQueuedNavigate(roundTrip, navigated);
|
||||
const basename = useHref('/');
|
||||
|
||||
// portalsReducer function is used as state outside the Flow component.
|
||||
const [portals, dispatchPortalAction] = useReducer(flowPortalsReducer, []);
|
||||
|
||||
const addPortalEventHandler = useCallback(
|
||||
(event: CustomEvent<PortalEntry>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const key = Math.random().toString(36).slice(2);
|
||||
dispatchPortalAction(
|
||||
addFlowPortal(
|
||||
<FlowPortal
|
||||
key={key}
|
||||
domNode={event.detail.domNode}
|
||||
onRemove={() => dispatchPortalAction(removeFlowPortal(key))}
|
||||
>
|
||||
{event.detail.children}
|
||||
</FlowPortal>
|
||||
)
|
||||
);
|
||||
},
|
||||
[dispatchPortalAction]
|
||||
);
|
||||
|
||||
const navigateEventHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const path = extractPath(event);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
navigated.current = false;
|
||||
// When navigation is triggered by click on a link, fromAnchor is set to true
|
||||
// in order to get a server round-trip even when navigating to the same URL again
|
||||
fromAnchor.current = true;
|
||||
// @ts-ignore
|
||||
window.Vaadin.Flow.navigation = true;
|
||||
navigate(path);
|
||||
// Dispatch close event for overlay drawer on click navigation.
|
||||
window.dispatchEvent(new CustomEvent('close-overlay-drawer'));
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const vaadinRouterGoEventHandler = useCallback(
|
||||
(event: CustomEvent<URL>) => {
|
||||
const url = event.detail;
|
||||
const path = normalizeURL(url);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
navigate(path);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const vaadinNavigateEventHandler = useCallback(
|
||||
(event: CustomEvent<{ state: unknown; url: string; replace?: boolean; callback: boolean }>) => {
|
||||
// @ts-ignore
|
||||
window.Vaadin.Flow.navigation = true;
|
||||
// clean base uri away if for instance redirected to http://localhost/path/user?id=10
|
||||
// else the whole http... will be appended to the url see #19580
|
||||
const path = event.detail.url.startsWith(document.baseURI)
|
||||
? '/' + event.detail.url.slice(document.baseURI.length)
|
||||
: '/' + event.detail.url;
|
||||
fromAnchor.current = false;
|
||||
queuedNavigate(path, event.detail.callback, { state: event.detail.state, replace: event.detail.replace });
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const redirect = useCallback(
|
||||
(path: string) => {
|
||||
return () => {
|
||||
navigate(path, { replace: true });
|
||||
};
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
window.addEventListener('vaadin-router-go', vaadinRouterGoEventHandler);
|
||||
// @ts-ignore
|
||||
window.addEventListener('vaadin-navigate', vaadinNavigateEventHandler);
|
||||
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
window.removeEventListener('vaadin-router-go', vaadinRouterGoEventHandler);
|
||||
// @ts-ignore
|
||||
window.removeEventListener('vaadin-navigate', vaadinNavigateEventHandler);
|
||||
};
|
||||
}, [vaadinRouterGoEventHandler, vaadinNavigateEventHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
window.addEventListener("popstate", flowNavigation);
|
||||
window.addEventListener('click', navigateEventHandler);
|
||||
flowReact.active = true;
|
||||
|
||||
return () => {
|
||||
containerRef.current?.parentNode?.removeChild(containerRef.current);
|
||||
containerRef.current?.removeEventListener('flow-portal-add', addPortalEventHandler as EventListener);
|
||||
containerRef.current = undefined;
|
||||
// @ts-ignore
|
||||
window.removeEventListener("popstate", flowNavigation);
|
||||
window.removeEventListener('click', navigateEventHandler);
|
||||
flowReact.active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === 'blocked') {
|
||||
if (blockerHandled.current) {
|
||||
// Blocker is handled and the new navigation
|
||||
// gets queued to be executed after the current handling ends.
|
||||
const { pathname, state } = blocker.location;
|
||||
// Clear base name to not get /baseName/basename/path
|
||||
const pathNoBase = pathname.substring(basename.length);
|
||||
// path should always start with / else react-router will append to current url
|
||||
queuedNavigate(pathNoBase.startsWith('/') ? pathNoBase : '/' + pathNoBase, true, {
|
||||
state: state,
|
||||
replace: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
blockerHandled.current = true;
|
||||
let blockingPromise: any;
|
||||
roundTrip.current = new Promise<void>(
|
||||
(resolve, reject) => (blockingPromise = { resolve: resolve, reject: reject })
|
||||
);
|
||||
// Release blocker handling after promise is fulfilled
|
||||
roundTrip.current.then(
|
||||
() => (blockerHandled.current = false),
|
||||
() => (blockerHandled.current = false)
|
||||
);
|
||||
|
||||
// Proceed to the blocked location, unless the navigation originates from a click on a link.
|
||||
// In that case continue with function execution and perform a server round-trip
|
||||
if (navigated.current && !fromAnchor.current) {
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
navigateInProgress = false;
|
||||
return;
|
||||
}
|
||||
fromAnchor.current = false;
|
||||
const { pathname, search } = blocker.location;
|
||||
const routes = ((window as any)?.Vaadin?.routesConfig || []) as any[];
|
||||
let matched = matchRoutes(Array.from(routes), pathname);
|
||||
|
||||
// Navigation between server routes
|
||||
// @ts-ignore
|
||||
if (matched && matched.filter((path) => path.route?.element?.type?.name === Flow.name).length != 0) {
|
||||
containerRef.current?.onBeforeEnter?.call(
|
||||
containerRef?.current,
|
||||
{ pathname, search },
|
||||
{
|
||||
prevent() {
|
||||
blocker.reset();
|
||||
blockingPromise.resolve();
|
||||
navigateInProgress = false;
|
||||
navigated.current = false;
|
||||
},
|
||||
redirect,
|
||||
continue() {
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
navigateInProgress = false;
|
||||
}
|
||||
},
|
||||
router
|
||||
);
|
||||
navigated.current = true;
|
||||
} else {
|
||||
// For covering the 'server -> client' use case
|
||||
Promise.resolve(
|
||||
containerRef.current?.onBeforeLeave?.call(
|
||||
containerRef?.current,
|
||||
{
|
||||
pathname,
|
||||
search
|
||||
},
|
||||
{ prevent },
|
||||
router
|
||||
)
|
||||
).then((cmd: unknown) => {
|
||||
if (cmd === postpone && containerRef.current) {
|
||||
// postponed navigation: expose existing blocker to Flow
|
||||
containerRef.current.serverConnected = (cancel) => {
|
||||
if (cancel) {
|
||||
blocker.reset();
|
||||
} else {
|
||||
blocker.proceed();
|
||||
}
|
||||
blockingPromise.resolve();
|
||||
navigateInProgress = false;
|
||||
};
|
||||
} else {
|
||||
// permitted navigation: proceed with the blocker
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
navigateInProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [blocker.state, blocker.location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === 'blocked') {
|
||||
return;
|
||||
}
|
||||
if (navigated.current) {
|
||||
navigated.current = false;
|
||||
fireNavigated(location.pathname, location.search);
|
||||
return;
|
||||
}
|
||||
flow.serverSideRoutes[0]
|
||||
.action({ pathname: location.pathname, search: location.search })
|
||||
.then((container) => {
|
||||
const outlet = ref.current?.parentNode;
|
||||
if (outlet && outlet !== container.parentNode) {
|
||||
outlet.append(container);
|
||||
container.addEventListener('flow-portal-add', addPortalEventHandler as EventListener);
|
||||
containerRef.current = container;
|
||||
}
|
||||
return container.onBeforeEnter?.call(
|
||||
container,
|
||||
{ pathname: location.pathname, search: location.search },
|
||||
{
|
||||
prevent,
|
||||
redirect,
|
||||
continue() {
|
||||
fireNavigated(location.pathname, location.search);
|
||||
}
|
||||
},
|
||||
router
|
||||
);
|
||||
})
|
||||
.then((result: unknown) => {
|
||||
if (typeof result === 'function') {
|
||||
result();
|
||||
}
|
||||
});
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<output ref={ref} style={{ display: 'none' }} />
|
||||
{portals}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Flow.type = 'FlowContainer'; // This is for copilot to recognize this
|
||||
|
||||
export const serverSideRoutes = [{ path: '/*', element: <Flow /> }];
|
||||
|
||||
/**
|
||||
* Load the script for an exported WebComponent with the given tag
|
||||
*
|
||||
* @param tag name of the exported web-component to load
|
||||
*
|
||||
* @returns Promise(resolve, reject) that is fulfilled on script load
|
||||
*/
|
||||
export const loadComponentScript = (tag: String): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = `/web-component/${tag}.js`;
|
||||
script.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function (err) {
|
||||
reject(err);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
};
|
||||
|
||||
interface Properties {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load WebComponent script and create a React element for the WebComponent.
|
||||
*
|
||||
* @param tag custom web-component tag name.
|
||||
* @param props optional Properties object to create element attributes with
|
||||
* @param onload optional callback to be called for script onload
|
||||
* @param onerror optional callback for error loading the script
|
||||
*/
|
||||
export const reactElement = (tag: string, props?: Properties, onload?: () => void, onerror?: (err: any) => void) => {
|
||||
loadComponentScript(tag).then(
|
||||
() => onload?.(),
|
||||
(err) => {
|
||||
if (onerror) {
|
||||
onerror(err);
|
||||
} else {
|
||||
console.error(`Failed to load script for ${tag}.`, err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (props) {
|
||||
return React.createElement(tag, props);
|
||||
}
|
||||
return React.createElement(tag);
|
||||
};
|
||||
|
||||
export default Flow;
|
||||
|
||||
// @ts-ignore
|
||||
if (import.meta.hot) {
|
||||
// @ts-ignore
|
||||
import.meta.hot.accept((newModule) => {
|
||||
// A hot module replace for Flow.tsx happens when any JS/TS imported through @JsModule
|
||||
// or similar is updated because this updates generated-flow-imports.js and that in turn
|
||||
// is imported by this file. We have no means of hot replacing those files, e.g. some
|
||||
// custom lit element so we need to reload the page. */
|
||||
if (newModule) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
329
backend/src/main/frontend/generated/flow/ReactAdapter.tsx
Normal file
329
backend/src/main/frontend/generated/flow/ReactAdapter.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright 2000-2026 Vaadin Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { createElement, type Dispatch, type ReactElement, type ReactNode, useEffect, useReducer } from 'react';
|
||||
|
||||
type FlowStateKeyChangedAction<K extends string, V> = Readonly<{
|
||||
type: 'stateKeyChanged';
|
||||
key: K;
|
||||
value: V;
|
||||
}>;
|
||||
|
||||
type FlowStateReducerAction = FlowStateKeyChangedAction<string, unknown>;
|
||||
|
||||
function stateReducer<S extends Readonly<Record<string, unknown>>>(state: S, action: FlowStateReducerAction): S {
|
||||
switch (action.type) {
|
||||
case 'stateKeyChanged':
|
||||
const { value } = action;
|
||||
return {
|
||||
...state,
|
||||
key: value
|
||||
} as S;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
type DispatchEvent<T> = T extends undefined ? () => boolean : (value: T) => boolean;
|
||||
|
||||
const emptyAction: Dispatch<unknown> = () => {};
|
||||
|
||||
/**
|
||||
* An object with APIs exposed for using in the {@link ReactAdapterElement#render}
|
||||
* implementation.
|
||||
*/
|
||||
export type RenderHooks = {
|
||||
/**
|
||||
* A hook API for using stateful JS properties of the Web Component from
|
||||
* the React `render()`.
|
||||
*
|
||||
* @typeParam T - Type of the state value
|
||||
*
|
||||
* @param key - Web Component property name, which is used for two-way
|
||||
* value propagation from the server and back.
|
||||
* @param initialValue - Fallback initial value (optional). Only applies if
|
||||
* the Java component constructor does not invoke `setState`.
|
||||
* @returns A tuple with two values:
|
||||
* 1. The current state.
|
||||
* 2. The `set` function for changing the state and triggering render
|
||||
* @protected
|
||||
*/
|
||||
readonly useState: ReactAdapterElement['useState'];
|
||||
|
||||
/**
|
||||
* A hook helper to simplify dispatching a `CustomEvent` on the Web
|
||||
* Component from React.
|
||||
*
|
||||
* @typeParam T - The type for `event.detail` value (optional).
|
||||
*
|
||||
* @param type - The `CustomEvent` type string.
|
||||
* @param options - The settings for the `CustomEvent`.
|
||||
* @returns The `dispatch` function. The function parameters change
|
||||
* depending on the `T` generic type:
|
||||
* - For `undefined` type (default), has no parameters.
|
||||
* - For other types, has one parameter for the `event.detail` value of that type.
|
||||
* @protected
|
||||
*/
|
||||
readonly useCustomEvent: ReactAdapterElement['useCustomEvent'];
|
||||
|
||||
/**
|
||||
* A hook helper to generate the content element with name attribute to bind
|
||||
* the server-side Flow element for this component.
|
||||
*
|
||||
* This is used together with {@link ReactAdapterComponent::getContentElement}
|
||||
* to have server-side component attach to the correct client element.
|
||||
*
|
||||
* Usage as follows:
|
||||
*
|
||||
* const content = hooks.useContent('content');
|
||||
* return <>
|
||||
* {content}
|
||||
* </>;
|
||||
*
|
||||
* Note! Not adding the 'content' element into the dom will have the
|
||||
* server throw a IllegalStateException for element with tag name not found.
|
||||
*
|
||||
* @param name - The name attribute of the element
|
||||
*/
|
||||
readonly useContent: ReactAdapterElement['useContent'];
|
||||
};
|
||||
|
||||
interface ReadyCallbackFunction {
|
||||
(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for Web Components that render using React. Enables creating
|
||||
* adapters for integrating React components with Flow. Intended for use with
|
||||
* `ReactAdapterComponent` Flow Java class.
|
||||
*/
|
||||
export abstract class ReactAdapterElement extends HTMLElement {
|
||||
#root: Root | undefined = undefined;
|
||||
#rootRendered: boolean = false;
|
||||
#rendering: ReactNode | undefined = undefined;
|
||||
|
||||
#state: Record<string, unknown> = Object.create(null);
|
||||
#stateSetters = new Map<string, Dispatch<unknown>>();
|
||||
#customEvents = new Map<string, DispatchEvent<unknown>>();
|
||||
#dispatchFlowState: Dispatch<FlowStateReducerAction> = emptyAction;
|
||||
|
||||
#readyCallback = new Map<string, ReadyCallbackFunction>();
|
||||
|
||||
readonly #renderHooks: RenderHooks;
|
||||
|
||||
readonly #Wrapper: () => ReactElement | null;
|
||||
|
||||
#unmounting?: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#renderHooks = {
|
||||
useState: this.useState.bind(this),
|
||||
useCustomEvent: this.useCustomEvent.bind(this),
|
||||
useContent: this.useContent.bind(this)
|
||||
};
|
||||
this.#Wrapper = this.#renderWrapper.bind(this);
|
||||
this.#markAsUsed();
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
this.#rendering = createElement(this.#Wrapper);
|
||||
const createNewRoot = this.dispatchEvent(
|
||||
new CustomEvent('flow-portal-add', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
children: this.#rendering,
|
||||
domNode: this
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (!createNewRoot || this.#root) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#unmounting;
|
||||
|
||||
this.#root = createRoot(this);
|
||||
this.#maybeRenderRoot();
|
||||
this.#root.render(this.#rendering);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a callback for specified element identifier to be called when
|
||||
* react element is ready.
|
||||
* <p>
|
||||
* For internal use only. May be renamed or removed in a future release.
|
||||
*
|
||||
* @param id element identifier that callback is for
|
||||
* @param readyCallback callback method to be informed on element ready state
|
||||
* @internal
|
||||
*/
|
||||
public addReadyCallback(id: string, readyCallback: ReadyCallbackFunction) {
|
||||
this.#readyCallback.set(id, readyCallback);
|
||||
}
|
||||
|
||||
public async disconnectedCallback() {
|
||||
if (!this.#root) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('flow-portal-remove', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
children: this.#rendering,
|
||||
domNode: this
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.#unmounting = Promise.resolve();
|
||||
await this.#unmounting;
|
||||
this.#root.unmount();
|
||||
this.#root = undefined;
|
||||
}
|
||||
this.#rootRendered = false;
|
||||
this.#rendering = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook API for using stateful JS properties of the Web Component from
|
||||
* the React `render()`.
|
||||
*
|
||||
* @typeParam T - Type of the state value
|
||||
*
|
||||
* @param key - Web Component property name, which is used for two-way
|
||||
* value propagation from the server and back.
|
||||
* @param initialValue - Fallback initial value (optional). Only applies if
|
||||
* the Java component constructor does not invoke `setState`.
|
||||
* @returns A tuple with two values:
|
||||
* 1. The current state.
|
||||
* 2. The `set` function for changing the state and triggering render
|
||||
* @protected
|
||||
*/
|
||||
protected useState<T>(key: string, initialValue?: T): [value: T, setValue: Dispatch<T>] {
|
||||
if (this.#stateSetters.has(key)) {
|
||||
return [this.#state[key] as T, this.#stateSetters.get(key)!];
|
||||
}
|
||||
|
||||
const value = ((this as Record<string, unknown>)[key] as T) ?? initialValue!;
|
||||
this.#state[key] = value;
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
get(): T {
|
||||
return this.#state[key];
|
||||
},
|
||||
set(nextValue: T) {
|
||||
this.#state[key] = nextValue;
|
||||
this.#dispatchFlowState({ type: 'stateKeyChanged', key, value });
|
||||
}
|
||||
});
|
||||
|
||||
const dispatchChangedEvent = this.useCustomEvent<{ value: T }>(`${key}-changed`, { detail: { value } });
|
||||
const setValue = (value: T) => {
|
||||
this.#state[key] = value;
|
||||
dispatchChangedEvent({ value });
|
||||
this.#dispatchFlowState({ type: 'stateKeyChanged', key, value });
|
||||
};
|
||||
this.#stateSetters.set(key, setValue as Dispatch<unknown>);
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook helper to simplify dispatching a `CustomEvent` on the Web
|
||||
* Component from React.
|
||||
*
|
||||
* @typeParam T - The type for `event.detail` value (optional).
|
||||
*
|
||||
* @param type - The `CustomEvent` type string.
|
||||
* @param options - The settings for the `CustomEvent`.
|
||||
* @returns The `dispatch` function. The function parameters change
|
||||
* depending on the `T` generic type:
|
||||
* - For `undefined` type (default), has no parameters.
|
||||
* - For other types, has one parameter for the `event.detail` value of that type.
|
||||
* @protected
|
||||
*/
|
||||
protected useCustomEvent<T = undefined>(type: string, options: CustomEventInit<T> = {}): DispatchEvent<T> {
|
||||
if (!this.#customEvents.has(type)) {
|
||||
const dispatch = ((detail?: T) => {
|
||||
const eventInitDict =
|
||||
detail === undefined
|
||||
? options
|
||||
: {
|
||||
...options,
|
||||
detail
|
||||
};
|
||||
const event = new CustomEvent(type, eventInitDict);
|
||||
return this.dispatchEvent(event);
|
||||
}) as DispatchEvent<T>;
|
||||
this.#customEvents.set(type, dispatch as DispatchEvent<unknown>);
|
||||
return dispatch;
|
||||
}
|
||||
return this.#customEvents.get(type)! as DispatchEvent<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Web Component render function. To be implemented by users with React.
|
||||
*
|
||||
* @param hooks - the adapter APIs exposed for the implementation.
|
||||
* @protected
|
||||
*/
|
||||
protected abstract render(hooks: RenderHooks): ReactElement | null;
|
||||
|
||||
/**
|
||||
* Prepare content container for Flow to bind server Element to.
|
||||
*
|
||||
* @param name container name attribute matching server name attribute
|
||||
* @protected
|
||||
*/
|
||||
protected useContent(name: string): ReactElement | null {
|
||||
useEffect(() => {
|
||||
this.#readyCallback.get(name)?.();
|
||||
}, []);
|
||||
return createElement('flow-content-container', { name, style: { display: 'contents' } });
|
||||
}
|
||||
|
||||
#maybeRenderRoot() {
|
||||
if (this.#rootRendered || !this.#root) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#root.render(createElement(this.#Wrapper));
|
||||
this.#rootRendered = true;
|
||||
}
|
||||
|
||||
#renderWrapper(): ReactElement | null {
|
||||
const [state, dispatchFlowState] = useReducer(stateReducer, this.#state);
|
||||
this.#state = state;
|
||||
this.#dispatchFlowState = dispatchFlowState;
|
||||
return this.render(this.#renderHooks);
|
||||
}
|
||||
|
||||
#markAsUsed(): void {
|
||||
// @ts-ignore
|
||||
let vaadinObject = window.Vaadin || {};
|
||||
// @ts-ignore
|
||||
if (vaadinObject.developmentMode) {
|
||||
vaadinObject.registrations = vaadinObject.registrations || [];
|
||||
vaadinObject.registrations.push({
|
||||
is: 'ReactAdapterElement',
|
||||
version: '25.0.8'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||
import '@vaadin/button/src/vaadin-button.js';
|
||||
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||
import '@vaadin/icons/vaadin-iconset.js';
|
||||
import '@vaadin/icon/src/vaadin-icon.js';
|
||||
1
backend/src/main/frontend/generated/flow/generated-flow-imports.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/flow/generated-flow-imports.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
@@ -0,0 +1,29 @@
|
||||
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||
import '@vaadin/button/src/vaadin-button.js';
|
||||
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||
import '@vaadin/icons/vaadin-iconset.js';
|
||||
import '@vaadin/icon/src/vaadin-icon.js';
|
||||
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||
|
||||
const loadOnDemand = (key) => {
|
||||
const pending = [];
|
||||
if (key === '1e3e1195126ded8f48ea9283d0ff579a0216bfc273c30bdac8e5152f029351ee') {
|
||||
pending.push(import('./chunks/chunk-bb2f082c2d2806895673c8e73955a0121cc631902992dbe3083e4b276ee78c5f.js'));
|
||||
}
|
||||
return Promise.all(pending);
|
||||
}
|
||||
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||
window.Vaadin.Flow.loadOnDemand = loadOnDemand;
|
||||
window.Vaadin.Flow.resetFocus = () => {
|
||||
let ae=document.activeElement;
|
||||
while(ae&&ae.shadowRoot) ae = ae.shadowRoot.activeElement;
|
||||
return !ae || ae.blur() || ae.focus() || true;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';
|
||||
|
||||
import '@vaadin/app-layout/src/vaadin-app-layout.js';
|
||||
import '@vaadin/vertical-layout/src/vaadin-vertical-layout.js';
|
||||
import '@vaadin/app-layout/src/vaadin-drawer-toggle.js';
|
||||
import '@vaadin/button/src/vaadin-button.js';
|
||||
import '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav.js';
|
||||
import '@vaadin/side-nav/src/vaadin-side-nav-item.js';
|
||||
import '@vaadin/icons/vaadin-iconset.js';
|
||||
import '@vaadin/icon/src/vaadin-icon.js';
|
||||
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||
|
||||
const loadOnDemand = (key) => {
|
||||
const pending = [];
|
||||
if (key === '1e3e1195126ded8f48ea9283d0ff579a0216bfc273c30bdac8e5152f029351ee') {
|
||||
pending.push(import('./chunks/chunk-bb2f082c2d2806895673c8e73955a0121cc631902992dbe3083e4b276ee78c5f.js'));
|
||||
}
|
||||
return Promise.all(pending);
|
||||
}
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||
window.Vaadin.Flow.loadOnDemand = loadOnDemand;
|
||||
window.Vaadin.Flow.resetFocus = () => {
|
||||
let ae=document.activeElement;
|
||||
while(ae&&ae.shadowRoot) ae = ae.shadowRoot.activeElement;
|
||||
return !ae || ae.blur() || ae.focus() || true;
|
||||
}
|
||||
26
backend/src/main/frontend/generated/index.tsx
Normal file
26
backend/src/main/frontend/generated/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/******************************************************************************
|
||||
* This file is auto-generated by Vaadin.
|
||||
* If you want to customize the entry point, you can copy this file or create
|
||||
* your own `index.tsx` in your frontend directory.
|
||||
* By default, the `index.tsx` file should be in `./frontend/` folder.
|
||||
*
|
||||
* NOTE:
|
||||
* - You need to restart the dev-server after adding the new `index.tsx` file.
|
||||
* After that, all modifications to `index.tsx` are recompiled automatically.
|
||||
* - `index.js` is also supported if you don't want to use TypeScript.
|
||||
******************************************************************************/
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router';
|
||||
import { router } from 'Frontend/generated/routes.js';
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
const outlet = document.getElementById('outlet')!;
|
||||
let root = (outlet as any)._root ?? createRoot(outlet);
|
||||
(outlet as any)._root = root;
|
||||
root.render(createElement(App));
|
||||
|
||||
78
backend/src/main/frontend/generated/jar-resources/Flow.d.ts
vendored
Normal file
78
backend/src/main/frontend/generated/jar-resources/Flow.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
export interface FlowConfig {
|
||||
imports?: () => Promise<any>;
|
||||
}
|
||||
interface AppConfig {
|
||||
productionMode: boolean;
|
||||
appId: string;
|
||||
uidl: any;
|
||||
}
|
||||
interface AppInitResponse {
|
||||
appConfig: AppConfig;
|
||||
pushScript?: string;
|
||||
}
|
||||
interface Router {
|
||||
render: (ctx: NavigationParameters, shouldUpdateHistory: boolean) => Promise<void>;
|
||||
}
|
||||
interface HTMLRouterContainer extends HTMLElement {
|
||||
onBeforeEnter?: (ctx: NavigationParameters, cmd: PreventAndRedirectCommands, router: Router) => void | Promise<any>;
|
||||
onBeforeLeave?: (ctx: NavigationParameters, cmd: PreventCommands, router: Router) => void | Promise<any>;
|
||||
serverConnected?: (cancel: boolean, url?: NavigationParameters) => void;
|
||||
serverPaused?: () => void;
|
||||
}
|
||||
interface FlowRoute {
|
||||
action: (params: NavigationParameters) => Promise<HTMLRouterContainer>;
|
||||
path: string;
|
||||
}
|
||||
export interface NavigationParameters {
|
||||
pathname: string;
|
||||
search?: string;
|
||||
}
|
||||
export interface PreventCommands {
|
||||
prevent: () => any;
|
||||
continue?: () => any;
|
||||
}
|
||||
export interface PreventAndRedirectCommands extends PreventCommands {
|
||||
redirect: (route: string) => any;
|
||||
}
|
||||
/**
|
||||
* Client API for flow UI operations.
|
||||
*/
|
||||
export declare class Flow {
|
||||
config: FlowConfig;
|
||||
response?: AppInitResponse;
|
||||
pathname: string;
|
||||
container: HTMLRouterContainer;
|
||||
private isActive;
|
||||
private baseRegex;
|
||||
private appShellTitle;
|
||||
private navigation;
|
||||
constructor(config?: FlowConfig);
|
||||
/**
|
||||
* Return a `route` object for vaadin-router in an one-element array.
|
||||
*
|
||||
* The `FlowRoute` object `path` property handles any route,
|
||||
* and the `action` returns the flow container without updating the content,
|
||||
* delaying the actual Flow server call to the `onBeforeEnter` phase.
|
||||
*
|
||||
* This is a specific API for its use with `vaadin-router`.
|
||||
*/
|
||||
get serverSideRoutes(): [FlowRoute];
|
||||
loadingStarted(): void;
|
||||
loadingFinished(): void;
|
||||
private get action();
|
||||
private flowLeave;
|
||||
private flowNavigate;
|
||||
private getFlowRoutePath;
|
||||
private getFlowRouteQuery;
|
||||
private flowInit;
|
||||
private loadScript;
|
||||
private findNonce;
|
||||
private injectAppIdScript;
|
||||
private flowInitClient;
|
||||
private flowInitUi;
|
||||
private collectBrowserDetails;
|
||||
private addConnectionIndicator;
|
||||
private offlineStubAction;
|
||||
private isFlowClientLoaded;
|
||||
}
|
||||
export {};
|
||||
497
backend/src/main/frontend/generated/jar-resources/Flow.js
Normal file
497
backend/src/main/frontend/generated/jar-resources/Flow.js
Normal file
@@ -0,0 +1,497 @@
|
||||
import { ConnectionIndicator, ConnectionState } from '@vaadin/common-frontend';
|
||||
class FlowUiInitializationError extends Error {
|
||||
}
|
||||
// flow uses body for keeping references
|
||||
const flowRoot = window.document.body;
|
||||
const $wnd = window;
|
||||
const ROOT_NODE_ID = 1; // See StateTree.java
|
||||
function getClients() {
|
||||
return Object.keys($wnd.Vaadin.Flow.clients)
|
||||
.filter((key) => key !== 'TypeScript')
|
||||
.map((id) => $wnd.Vaadin.Flow.clients[id]);
|
||||
}
|
||||
function sendEvent(eventName, data) {
|
||||
getClients().forEach((client) => client.sendEventMessage(ROOT_NODE_ID, eventName, data));
|
||||
}
|
||||
// In the future could be replaced with RegExp.escape()
|
||||
function escapeRegExp(pattern) {
|
||||
return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
/**
|
||||
* Client API for flow UI operations.
|
||||
*/
|
||||
export class Flow {
|
||||
config;
|
||||
response = undefined;
|
||||
pathname = '';
|
||||
container;
|
||||
// flag used to inform Testbench whether a server route is in progress
|
||||
isActive = false;
|
||||
baseRegex = /^\//;
|
||||
appShellTitle;
|
||||
navigation = '';
|
||||
constructor(config) {
|
||||
// Set window.name early so @PreserveOnRefresh can use it to identify the browser tab
|
||||
// Only set if not already set to preserve any existing value
|
||||
if (!window.name) {
|
||||
window.name = `v-${Math.random()}`;
|
||||
}
|
||||
flowRoot.$ = flowRoot.$ || [];
|
||||
this.config = config || {};
|
||||
// TB checks for the existence of window.Vaadin.Flow in order
|
||||
// to consider that TB needs to wait for `initFlow()`.
|
||||
$wnd.Vaadin = $wnd.Vaadin || {};
|
||||
$wnd.Vaadin.Flow = $wnd.Vaadin.Flow || {};
|
||||
$wnd.Vaadin.Flow.clients = {
|
||||
TypeScript: {
|
||||
isActive: () => this.isActive
|
||||
}
|
||||
};
|
||||
// Set browser details collection function as global for use by refresh()
|
||||
$wnd.Vaadin.Flow.getBrowserDetailsParameters = this.collectBrowserDetails.bind(this);
|
||||
// Regular expression used to remove the app-context
|
||||
const elm = document.head.querySelector('base');
|
||||
this.baseRegex = new RegExp(`^${
|
||||
// IE11 does not support document.baseURI
|
||||
escapeRegExp(decodeURIComponent((document.baseURI || (elm && elm.href) || '/').replace(/^https?:\/\/[^/]+/i, '')))}`);
|
||||
this.appShellTitle = document.title;
|
||||
// Put a vaadin-connection-indicator in the dom
|
||||
this.addConnectionIndicator();
|
||||
}
|
||||
/**
|
||||
* Return a `route` object for vaadin-router in an one-element array.
|
||||
*
|
||||
* The `FlowRoute` object `path` property handles any route,
|
||||
* and the `action` returns the flow container without updating the content,
|
||||
* delaying the actual Flow server call to the `onBeforeEnter` phase.
|
||||
*
|
||||
* This is a specific API for its use with `vaadin-router`.
|
||||
*/
|
||||
get serverSideRoutes() {
|
||||
return [
|
||||
{
|
||||
path: '(.*)',
|
||||
action: this.action
|
||||
}
|
||||
];
|
||||
}
|
||||
loadingStarted() {
|
||||
// Make Testbench know that server request is in progress
|
||||
this.isActive = true;
|
||||
$wnd.Vaadin.connectionState.loadingStarted();
|
||||
}
|
||||
loadingFinished() {
|
||||
// Make Testbench know that server request has finished
|
||||
this.isActive = false;
|
||||
$wnd.Vaadin.connectionState.loadingFinished();
|
||||
if ($wnd.Vaadin.listener) {
|
||||
// Listeners registered, do not register again.
|
||||
return;
|
||||
}
|
||||
$wnd.Vaadin.listener = {};
|
||||
// Listen for click on router-links -> 'link' navigation trigger
|
||||
// and on <a> nodes -> 'client' navigation trigger.
|
||||
// Use capture phase to detect prevented / stopped events.
|
||||
document.addEventListener('click', (_e) => {
|
||||
if (_e.target) {
|
||||
if (_e.composedPath().some((node) => node instanceof HTMLElement && node.hasAttribute('router-link'))) {
|
||||
this.navigation = 'link';
|
||||
}
|
||||
else if (_e.composedPath().some((node) => node.nodeName === 'A')) {
|
||||
this.navigation = 'client';
|
||||
}
|
||||
}
|
||||
}, {
|
||||
capture: true
|
||||
});
|
||||
}
|
||||
get action() {
|
||||
// Return a function which is bound to the flow instance, thus we can use
|
||||
// the syntax `...serverSideRoutes` in vaadin-router.
|
||||
return async (params) => {
|
||||
// Store last action pathname so as we can check it in events
|
||||
this.pathname = params.pathname;
|
||||
if ($wnd.Vaadin.connectionState.online) {
|
||||
try {
|
||||
await this.flowInit();
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof FlowUiInitializationError) {
|
||||
// error initializing Flow: assume connection lost
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||
return this.offlineStubAction();
|
||||
}
|
||||
else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// insert an offline stub
|
||||
return this.offlineStubAction();
|
||||
}
|
||||
// When an action happens, navigation will be resolved `onBeforeEnter`
|
||||
this.container.onBeforeEnter = (ctx, cmd) => this.flowNavigate(ctx, cmd);
|
||||
// For covering the 'server -> client' use case
|
||||
this.container.onBeforeLeave = (ctx, cmd) => this.flowLeave(ctx, cmd);
|
||||
return this.container;
|
||||
};
|
||||
}
|
||||
// Send a remote call to `JavaScriptBootstrapUI` to check
|
||||
// whether navigation has to be cancelled.
|
||||
async flowLeave(ctx, cmd) {
|
||||
// server -> server, viewing offline stub, or browser is offline
|
||||
const { connectionState } = $wnd.Vaadin;
|
||||
if (this.pathname === ctx.pathname || !this.isFlowClientLoaded() || connectionState.offline) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
// 'server -> client'
|
||||
return new Promise((resolve) => {
|
||||
this.loadingStarted();
|
||||
// The callback to run from server side to cancel navigation
|
||||
this.container.serverConnected = (cancel) => {
|
||||
resolve(cmd && cancel ? cmd.prevent() : cmd?.continue?.());
|
||||
this.loadingFinished();
|
||||
};
|
||||
// Call server side to check whether we can leave the view
|
||||
sendEvent('ui-leave-navigation', { route: this.getFlowRoutePath(ctx), query: this.getFlowRouteQuery(ctx) });
|
||||
});
|
||||
}
|
||||
// Send the remote call to `JavaScriptBootstrapUI` to render the flow
|
||||
// route specified by the context
|
||||
async flowNavigate(ctx, cmd) {
|
||||
if (this.response) {
|
||||
return new Promise((resolve) => {
|
||||
this.loadingStarted();
|
||||
// The callback to run from server side once the view is ready
|
||||
this.container.serverConnected = (cancel, redirectContext) => {
|
||||
if (cmd && cancel) {
|
||||
resolve(cmd.prevent());
|
||||
}
|
||||
else if (cmd && cmd.redirect && redirectContext) {
|
||||
resolve(cmd.redirect(redirectContext.pathname));
|
||||
}
|
||||
else {
|
||||
cmd?.continue?.();
|
||||
this.container.style.display = '';
|
||||
resolve(this.container);
|
||||
}
|
||||
this.loadingFinished();
|
||||
};
|
||||
this.container.serverPaused = () => {
|
||||
this.loadingFinished();
|
||||
};
|
||||
// Call server side to navigate to the given route
|
||||
sendEvent('ui-navigate', {
|
||||
route: this.getFlowRoutePath(ctx),
|
||||
query: this.getFlowRouteQuery(ctx),
|
||||
appShellTitle: this.appShellTitle,
|
||||
historyState: history.state,
|
||||
trigger: this.navigation
|
||||
});
|
||||
// Default to history navigation trigger.
|
||||
// Link and client cases are handled by click listener in loadingFinished().
|
||||
this.navigation = 'history';
|
||||
});
|
||||
}
|
||||
else {
|
||||
// No server response => offline or erroneous connection
|
||||
return Promise.resolve(this.container);
|
||||
}
|
||||
}
|
||||
getFlowRoutePath(context) {
|
||||
return decodeURIComponent(context.pathname).replace(this.baseRegex, '');
|
||||
}
|
||||
getFlowRouteQuery(context) {
|
||||
return (context.search && context.search.substring(1)) || '';
|
||||
}
|
||||
// import flow client modules and initialize UI in server side.
|
||||
async flowInit() {
|
||||
// Do not start flow twice
|
||||
if (!this.isFlowClientLoaded()) {
|
||||
$wnd.Vaadin.Flow.nonce = this.findNonce();
|
||||
// show flow progress indicator
|
||||
this.loadingStarted();
|
||||
// Initialize server side UI
|
||||
this.response = await this.flowInitUi();
|
||||
const { pushScript, appConfig } = this.response;
|
||||
if (typeof pushScript === 'string') {
|
||||
await this.loadScript(pushScript);
|
||||
}
|
||||
const { appId } = appConfig;
|
||||
// we use a custom tag for the flow app container
|
||||
// This must be created before bootstrapMod.init is called as that call
|
||||
// can handle a UIDL from the server, which relies on the container being available
|
||||
const tag = `flow-container-${appId.toLowerCase()}`;
|
||||
const serverCreatedContainer = document.querySelector(tag);
|
||||
if (serverCreatedContainer) {
|
||||
this.container = serverCreatedContainer;
|
||||
}
|
||||
else {
|
||||
this.container = document.createElement(tag);
|
||||
this.container.id = appId;
|
||||
}
|
||||
flowRoot.$[appId] = this.container;
|
||||
// Load bootstrap script with server side parameters
|
||||
const bootstrapMod = await import('./FlowBootstrap');
|
||||
bootstrapMod.init(this.response);
|
||||
// Load custom modules defined by user
|
||||
if (typeof this.config.imports === 'function') {
|
||||
this.injectAppIdScript(appId);
|
||||
await this.config.imports();
|
||||
}
|
||||
// Load flow-client module
|
||||
const clientMod = await import('./FlowClient');
|
||||
await this.flowInitClient(clientMod);
|
||||
// hide flow progress indicator
|
||||
this.loadingFinished();
|
||||
}
|
||||
// It might be that components created from server expect that their content has been rendered.
|
||||
// Appending eagerly the container we avoid these kind of errors.
|
||||
// Note that the client router will move this container to the outlet if the navigation succeed
|
||||
if (this.container && !this.container.isConnected) {
|
||||
this.container.style.display = 'none';
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
return this.response;
|
||||
}
|
||||
async loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.onload = () => resolve();
|
||||
script.onerror = reject;
|
||||
script.src = url;
|
||||
const { nonce } = $wnd.Vaadin.Flow;
|
||||
if (nonce !== undefined) {
|
||||
script.setAttribute('nonce', nonce);
|
||||
}
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
findNonce() {
|
||||
let nonce;
|
||||
const scriptTags = document.head.getElementsByTagName('script');
|
||||
for (const scriptTag of scriptTags) {
|
||||
if (scriptTag.nonce) {
|
||||
nonce = scriptTag.nonce;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nonce;
|
||||
}
|
||||
injectAppIdScript(appId) {
|
||||
const appIdWithoutHashCode = appId.substring(0, appId.lastIndexOf('-'));
|
||||
const scriptAppId = document.createElement('script');
|
||||
scriptAppId.type = 'module';
|
||||
scriptAppId.setAttribute('data-app-id', appIdWithoutHashCode);
|
||||
const { nonce } = $wnd.Vaadin.Flow;
|
||||
if (nonce !== undefined) {
|
||||
scriptAppId.setAttribute('nonce', nonce);
|
||||
}
|
||||
document.body.append(scriptAppId);
|
||||
}
|
||||
// After the flow-client javascript module has been loaded, this initializes flow UI
|
||||
// in the browser.
|
||||
async flowInitClient(clientMod) {
|
||||
clientMod.init();
|
||||
// client init is async, we need to loop until initialized
|
||||
return new Promise((resolve) => {
|
||||
const intervalId = setInterval(() => {
|
||||
// client `isActive() == true` while initializing or processing
|
||||
const initializing = getClients().reduce((prev, client) => prev || client.isActive(), false);
|
||||
if (!initializing) {
|
||||
clearInterval(intervalId);
|
||||
resolve();
|
||||
}
|
||||
}, 5);
|
||||
});
|
||||
}
|
||||
// Returns the `appConfig` object
|
||||
async flowInitUi() {
|
||||
// appConfig was sent in the index.html request
|
||||
const initial = $wnd.Vaadin && $wnd.Vaadin.TypeScript && $wnd.Vaadin.TypeScript.initial;
|
||||
if (initial) {
|
||||
$wnd.Vaadin.TypeScript.initial = undefined;
|
||||
return Promise.resolve(initial);
|
||||
}
|
||||
// send a request to the `JavaScriptBootstrapHandler`
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const httpRequest = xhr;
|
||||
// Collect browser details to send with init request as JSON
|
||||
const browserDetails = this.collectBrowserDetails();
|
||||
const browserDetailsParam = browserDetails
|
||||
? `&v-browserDetails=${encodeURIComponent(JSON.stringify(browserDetails))}`
|
||||
: '';
|
||||
const requestPath = `?v-r=init&location=${encodeURIComponent(this.getFlowRoutePath(location))}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}${browserDetailsParam}`;
|
||||
httpRequest.open('GET', requestPath);
|
||||
httpRequest.onerror = () => reject(new FlowUiInitializationError(`Invalid server response when initializing Flow UI.
|
||||
${httpRequest.status}
|
||||
${httpRequest.responseText}`));
|
||||
httpRequest.onload = () => {
|
||||
const contentType = httpRequest.getResponseHeader('content-type');
|
||||
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||
resolve(JSON.parse(httpRequest.responseText));
|
||||
}
|
||||
else {
|
||||
httpRequest.onerror();
|
||||
}
|
||||
};
|
||||
httpRequest.send();
|
||||
});
|
||||
}
|
||||
// Collects browser details parameters
|
||||
collectBrowserDetails() {
|
||||
const params = {};
|
||||
/* Screen height and width */
|
||||
params['v-sh'] = $wnd.screen.height;
|
||||
params['v-sw'] = $wnd.screen.width;
|
||||
/* Browser window dimensions */
|
||||
params['v-wh'] = $wnd.innerHeight;
|
||||
params['v-ww'] = $wnd.innerWidth;
|
||||
/* Body element dimensions */
|
||||
params['v-bh'] = $wnd.document.body.clientHeight;
|
||||
params['v-bw'] = $wnd.document.body.clientWidth;
|
||||
/* Current time */
|
||||
const date = new Date();
|
||||
params['v-curdate'] = date.getTime();
|
||||
/* Current timezone offset (including DST shift) */
|
||||
const tzo1 = date.getTimezoneOffset();
|
||||
/* Compare the current tz offset with the first offset from the end
|
||||
of the year that differs --- if less that, we are in DST, otherwise
|
||||
we are in normal time */
|
||||
let dstDiff = 0;
|
||||
let rawTzo = tzo1;
|
||||
for (let m = 12; m > 0; m -= 1) {
|
||||
date.setUTCMonth(m);
|
||||
const tzo2 = date.getTimezoneOffset();
|
||||
if (tzo1 !== tzo2) {
|
||||
dstDiff = tzo1 > tzo2 ? tzo1 - tzo2 : tzo2 - tzo1;
|
||||
rawTzo = tzo1 > tzo2 ? tzo1 : tzo2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Time zone offset */
|
||||
params['v-tzo'] = tzo1;
|
||||
/* DST difference */
|
||||
params['v-dstd'] = dstDiff;
|
||||
/* Time zone offset without DST */
|
||||
params['v-rtzo'] = rawTzo;
|
||||
/* DST in effect? */
|
||||
params['v-dston'] = tzo1 !== rawTzo;
|
||||
/* Time zone id (if available) */
|
||||
try {
|
||||
params['v-tzid'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
catch (err) {
|
||||
params['v-tzid'] = '';
|
||||
}
|
||||
/* Window name */
|
||||
if ($wnd.name) {
|
||||
params['v-wn'] = $wnd.name;
|
||||
}
|
||||
/* Detect touch device support */
|
||||
let supportsTouch = false;
|
||||
try {
|
||||
$wnd.document.createEvent('TouchEvent');
|
||||
supportsTouch = true;
|
||||
}
|
||||
catch (e) {
|
||||
/* Chrome and IE10 touch detection */
|
||||
supportsTouch = 'ontouchstart' in $wnd || typeof $wnd.navigator.msMaxTouchPoints !== 'undefined';
|
||||
}
|
||||
params['v-td'] = supportsTouch;
|
||||
/* Device Pixel Ratio */
|
||||
params['v-pr'] = $wnd.devicePixelRatio;
|
||||
if ($wnd.navigator.platform) {
|
||||
params['v-np'] = $wnd.navigator.platform;
|
||||
}
|
||||
/* Color scheme from CSS color-scheme property */
|
||||
const colorScheme = getComputedStyle(document.documentElement).colorScheme.trim();
|
||||
// "normal" is the default value and means no color scheme is set
|
||||
params['v-cs'] = colorScheme && colorScheme !== 'normal' ? colorScheme : '';
|
||||
/* Theme name - detect which theme is in use */
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
let themeName = '';
|
||||
if (computedStyle.getPropertyValue('--vaadin-lumo-theme').trim()) {
|
||||
themeName = 'lumo';
|
||||
}
|
||||
else if (computedStyle.getPropertyValue('--vaadin-aura-theme').trim()) {
|
||||
themeName = 'aura';
|
||||
}
|
||||
params['v-tn'] = themeName;
|
||||
/* Stringify each value (they are parsed on the server side) */
|
||||
const stringParams = {};
|
||||
Object.keys(params).forEach((key) => {
|
||||
const value = params[key];
|
||||
if (typeof value !== 'undefined') {
|
||||
stringParams[key] = value.toString();
|
||||
}
|
||||
});
|
||||
return stringParams;
|
||||
}
|
||||
// Create shared connection state store and connection indicator
|
||||
addConnectionIndicator() {
|
||||
// add connection indicator to DOM
|
||||
ConnectionIndicator.create();
|
||||
// Listen to browser online/offline events and update the loading indicator accordingly.
|
||||
// Note: if flow-client is loaded, it instead handles the state transitions.
|
||||
$wnd.addEventListener('online', () => {
|
||||
if (!this.isFlowClientLoaded()) {
|
||||
// Send an HTTP HEAD request for sw.js to verify server reachability.
|
||||
// We do not expect sw.js to be cached, so the request goes to the
|
||||
// server rather than being served from local cache.
|
||||
// Require network-level failure to revert the state to CONNECTION_LOST
|
||||
// (HTTP error code is ok since it still verifies server's presence).
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.RECONNECTING;
|
||||
const http = new XMLHttpRequest();
|
||||
http.open('HEAD', 'sw.js');
|
||||
http.onload = () => {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTED;
|
||||
};
|
||||
http.onerror = () => {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||
};
|
||||
// Postpone request to reduce potential net::ERR_INTERNET_DISCONNECTED
|
||||
// errors that sometimes occurs even if browser says it is online
|
||||
setTimeout(() => http.send(), 50);
|
||||
}
|
||||
});
|
||||
$wnd.addEventListener('offline', () => {
|
||||
if (!this.isFlowClientLoaded()) {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||
}
|
||||
});
|
||||
}
|
||||
async offlineStubAction() {
|
||||
const offlineStub = document.createElement('iframe');
|
||||
const offlineStubPath = './offline-stub.html';
|
||||
offlineStub.setAttribute('src', offlineStubPath);
|
||||
offlineStub.setAttribute('style', 'width: 100%; height: 100%; border: 0');
|
||||
this.response = undefined;
|
||||
let onlineListener;
|
||||
const removeOfflineStubAndOnlineListener = () => {
|
||||
if (onlineListener !== undefined) {
|
||||
$wnd.Vaadin.connectionState.removeStateChangeListener(onlineListener);
|
||||
onlineListener = undefined;
|
||||
}
|
||||
};
|
||||
offlineStub.onBeforeEnter = (ctx, _cmds, router) => {
|
||||
onlineListener = () => {
|
||||
if ($wnd.Vaadin.connectionState.online) {
|
||||
removeOfflineStubAndOnlineListener();
|
||||
router.render(ctx, false);
|
||||
}
|
||||
};
|
||||
$wnd.Vaadin.connectionState.addStateChangeListener(onlineListener);
|
||||
};
|
||||
offlineStub.onBeforeLeave = (_ctx, _cmds, _router) => {
|
||||
removeOfflineStubAndOnlineListener();
|
||||
};
|
||||
return offlineStub;
|
||||
}
|
||||
isFlowClientLoaded() {
|
||||
return this.response !== undefined;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=Flow.js.map
|
||||
File diff suppressed because one or more lines are too long
1
backend/src/main/frontend/generated/jar-resources/FlowBootstrap.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/jar-resources/FlowBootstrap.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const init: (appInitResponse: any) => void;
|
||||
@@ -0,0 +1,201 @@
|
||||
/* This is a copy of the regular `BootstrapHandler.js` in the flow-server
|
||||
module, but with the following modifications:
|
||||
- The main function is exported as an ES module for lazy initialization.
|
||||
- Application configuration is passed as a parameter instead of using
|
||||
replacement placeholders as in the regular bootstrapping.
|
||||
- It reuses `Vaadin.Flow.clients` if exists.
|
||||
- Fixed lint errors.
|
||||
*/
|
||||
const init = function (appInitResponse) {
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||
|
||||
var apps = {};
|
||||
var widgetsets = {};
|
||||
|
||||
var log;
|
||||
if (typeof window.console === undefined || !window.location.search.match(/[&?]debug(&|$)/)) {
|
||||
/* If no console.log present, just use a no-op */
|
||||
log = function () {};
|
||||
} else if (typeof window.console.log === 'function') {
|
||||
/* If it's a function, use it with apply */
|
||||
log = function () {
|
||||
window.console.log.apply(window.console, arguments);
|
||||
};
|
||||
} else {
|
||||
/* In IE, its a native function for which apply is not defined, but it works
|
||||
without a proper 'this' reference */
|
||||
log = window.console.log;
|
||||
}
|
||||
|
||||
var isInitializedInDom = function (appId) {
|
||||
var appDiv = document.getElementById(appId);
|
||||
if (!appDiv) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < appDiv.childElementCount; i++) {
|
||||
var className = appDiv.childNodes[i].className;
|
||||
/* If the app div contains a child with the class
|
||||
'v-app-loading' we have only received the HTML
|
||||
but not yet started the widget set
|
||||
(UIConnector removes the v-app-loading div). */
|
||||
if (className && className.indexOf('v-app-loading') != -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/*
|
||||
* Needed for Testbench compatibility, but prevents any Vaadin 7 app from
|
||||
* bootstrapping unless the legacy vaadinBootstrap.js file is loaded before
|
||||
* this script.
|
||||
*/
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||
|
||||
/*
|
||||
* Needed for wrapping custom javascript functionality in the components (i.e. connectors)
|
||||
*/
|
||||
window.Vaadin.Flow.tryCatchWrapper = function (originalFunction, component) {
|
||||
return function () {
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const result = originalFunction.apply(this, arguments);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`There seems to be an error in ${component}:
|
||||
${error.message}
|
||||
Please submit an issue to https://github.com/vaadin/flow-components/issues/new/choose`
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (!window.Vaadin.Flow.initApplication) {
|
||||
window.Vaadin.Flow.clients = window.Vaadin.Flow.clients || {};
|
||||
|
||||
window.Vaadin.Flow.initApplication = function (appId, config) {
|
||||
var testbenchId = appId.replace(/-\d+$/, '');
|
||||
|
||||
if (apps[appId]) {
|
||||
if (
|
||||
window.Vaadin &&
|
||||
window.Vaadin.Flow &&
|
||||
window.Vaadin.Flow.clients &&
|
||||
window.Vaadin.Flow.clients[testbenchId] &&
|
||||
window.Vaadin.Flow.clients[testbenchId].initializing
|
||||
) {
|
||||
throw new Error('Application ' + appId + ' is already being initialized');
|
||||
}
|
||||
if (isInitializedInDom(appId)) {
|
||||
if (appInitResponse.appConfig.productionMode) {
|
||||
throw new Error('Application ' + appId + ' already initialized');
|
||||
}
|
||||
|
||||
// Remove old contents for Flow
|
||||
var appDiv = document.getElementById(appId);
|
||||
for (var i = 0; i < appDiv.childElementCount; i++) {
|
||||
appDiv.childNodes[i].remove();
|
||||
}
|
||||
|
||||
// For devMode reset app config and restart widgetset as client
|
||||
// is up and running after hrm update.
|
||||
const getConfig = function (name) {
|
||||
return config[name];
|
||||
};
|
||||
|
||||
/* Export public data */
|
||||
const app = {
|
||||
getConfig: getConfig
|
||||
};
|
||||
apps[appId] = app;
|
||||
|
||||
if (widgetsets['client'].callback) {
|
||||
log('Starting from bootstrap', appId);
|
||||
widgetsets['client'].callback(appId);
|
||||
} else {
|
||||
log('Setting pending startup', appId);
|
||||
widgetsets['client'].pendingApps.push(appId);
|
||||
}
|
||||
return apps[appId];
|
||||
}
|
||||
}
|
||||
|
||||
log('init application', appId, config);
|
||||
|
||||
window.Vaadin.Flow.clients[testbenchId] = {
|
||||
isActive: function () {
|
||||
return true;
|
||||
},
|
||||
initializing: true,
|
||||
productionMode: mode
|
||||
};
|
||||
|
||||
var getConfig = function (name) {
|
||||
var value = config[name];
|
||||
return value;
|
||||
};
|
||||
|
||||
/* Export public data */
|
||||
var app = {
|
||||
getConfig: getConfig
|
||||
};
|
||||
apps[appId] = app;
|
||||
|
||||
var widgetset = 'client';
|
||||
widgetsets[widgetset] = {
|
||||
pendingApps: []
|
||||
};
|
||||
if (widgetsets[widgetset].callback) {
|
||||
log('Starting from bootstrap', appId);
|
||||
widgetsets[widgetset].callback(appId);
|
||||
} else {
|
||||
log('Setting pending startup', appId);
|
||||
widgetsets[widgetset].pendingApps.push(appId);
|
||||
}
|
||||
|
||||
return app;
|
||||
};
|
||||
window.Vaadin.Flow.getAppIds = function () {
|
||||
var ids = [];
|
||||
for (var id in apps) {
|
||||
if (Object.prototype.hasOwnProperty.call(apps, id)) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
window.Vaadin.Flow.getApp = function (appId) {
|
||||
return apps[appId];
|
||||
};
|
||||
window.Vaadin.Flow.registerWidgetset = function (widgetset, callback) {
|
||||
log('Widgetset registered', widgetset);
|
||||
var ws = widgetsets[widgetset];
|
||||
if (ws && ws.pendingApps) {
|
||||
ws.callback = callback;
|
||||
for (var i = 0; i < ws.pendingApps.length; i++) {
|
||||
var appId = ws.pendingApps[i];
|
||||
log('Starting from register widgetset', appId);
|
||||
callback(appId);
|
||||
}
|
||||
ws.pendingApps = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
log('Flow bootstrap loaded');
|
||||
if (appInitResponse.appConfig.productionMode && typeof window.__gwtStatsEvent != 'function') {
|
||||
window.Vaadin.Flow.gwtStatsEvents = [];
|
||||
window.__gwtStatsEvent = function (event) {
|
||||
window.Vaadin.Flow.gwtStatsEvents.push(event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
var config = appInitResponse.appConfig;
|
||||
var mode = appInitResponse.appConfig.productionMode;
|
||||
window.Vaadin.Flow.initApplication(config.appId, config);
|
||||
};
|
||||
|
||||
export { init };
|
||||
1
backend/src/main/frontend/generated/jar-resources/FlowClient.d.ts
vendored
Normal file
1
backend/src/main/frontend/generated/jar-resources/FlowClient.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const init: () => void;
|
||||
1090
backend/src/main/frontend/generated/jar-resources/FlowClient.js
Normal file
1090
backend/src/main/frontend/generated/jar-resources/FlowClient.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
import { Outlet } from 'react-router';
|
||||
import { ReactAdapterElement } from "Frontend/generated/flow/ReactAdapter.js";
|
||||
import React from "react";
|
||||
|
||||
class ReactRouterOutletElement extends ReactAdapterElement {
|
||||
public async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.style.display = 'contents';
|
||||
}
|
||||
|
||||
protected render(): React.ReactElement | null {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('react-router-outlet', ReactRouterOutletElement);
|
||||
@@ -0,0 +1,284 @@
|
||||
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
||||
import { timeOut } from '@vaadin/component-base/src/async.js';
|
||||
import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
|
||||
|
||||
window.Vaadin.Flow.comboBoxConnector = {};
|
||||
window.Vaadin.Flow.comboBoxConnector.initLazy = (comboBox) => {
|
||||
// Check whether the connector was already initialized for the ComboBox
|
||||
if (comboBox.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
comboBox.$connector = {};
|
||||
|
||||
// holds pageIndex -> callback pairs of subsequent indexes (current active range)
|
||||
const pageCallbacks = {};
|
||||
let cache = {};
|
||||
let lastFilter = '';
|
||||
const placeHolder = new window.Vaadin.ComboBoxPlaceholder();
|
||||
|
||||
const serverFacade = (() => {
|
||||
// Private variables
|
||||
let lastFilterSentToServer = '';
|
||||
let dataCommunicatorResetNeeded = false;
|
||||
|
||||
// Public methods
|
||||
const needsDataCommunicatorReset = () => (dataCommunicatorResetNeeded = true);
|
||||
const getLastFilterSentToServer = () => lastFilterSentToServer;
|
||||
const requestData = (startIndex, endIndex, params) => {
|
||||
const count = endIndex - startIndex;
|
||||
const filter = params.filter;
|
||||
|
||||
comboBox.$server.setViewportRange(startIndex, count, filter);
|
||||
lastFilterSentToServer = filter;
|
||||
if (dataCommunicatorResetNeeded) {
|
||||
comboBox.$server.resetDataCommunicator();
|
||||
dataCommunicatorResetNeeded = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
needsDataCommunicatorReset,
|
||||
getLastFilterSentToServer,
|
||||
requestData
|
||||
};
|
||||
})();
|
||||
|
||||
const clearPageCallbacks = (pages = Object.keys(pageCallbacks)) => {
|
||||
// Flush and empty the existing requests
|
||||
pages.forEach((page) => {
|
||||
pageCallbacks[page]([], comboBox.size);
|
||||
delete pageCallbacks[page];
|
||||
|
||||
// Empty the comboBox's internal cache without invoking observers by filling
|
||||
// the filteredItems array with placeholders (comboBox will request for data when it
|
||||
// encounters a placeholder)
|
||||
const pageStart = parseInt(page) * comboBox.pageSize;
|
||||
const pageEnd = pageStart + comboBox.pageSize;
|
||||
const end = Math.min(pageEnd, comboBox.filteredItems.length);
|
||||
for (let i = pageStart; i < end; i++) {
|
||||
comboBox.filteredItems[i] = placeHolder;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
comboBox.dataProvider = function (params, callback) {
|
||||
if (params.pageSize != comboBox.pageSize) {
|
||||
throw 'Invalid pageSize';
|
||||
}
|
||||
|
||||
if (comboBox._clientSideFilter) {
|
||||
// For clientside filter we first make sure we have all data which we also
|
||||
// filter based on comboBox.filter. While later we only filter clientside data.
|
||||
|
||||
if (cache[0]) {
|
||||
performClientSideFilter(cache[0], params.filter, callback);
|
||||
return;
|
||||
} else {
|
||||
// If client side filter is enabled then we need to first ask all data
|
||||
// and filter it on client side, otherwise next time when user will
|
||||
// input another filter, eg. continue to type, the local cache will be only
|
||||
// what was received for the first filter, which may not be the whole
|
||||
// data from server (keep in mind that client side filter is enabled only
|
||||
// when the items count does not exceed one page).
|
||||
params.filter = '';
|
||||
}
|
||||
}
|
||||
|
||||
const filterChanged = params.filter !== lastFilter;
|
||||
if (filterChanged) {
|
||||
cache = {};
|
||||
lastFilter = params.filter;
|
||||
comboBox._filterDebouncer = Debouncer.debounce(comboBox._filterDebouncer, timeOut.after(500), () => {
|
||||
if (serverFacade.getLastFilterSentToServer() === params.filter) {
|
||||
// Fixes the case when the filter changes
|
||||
// to something else and back to the original value
|
||||
// within debounce timeout, and the
|
||||
// DataCommunicator thinks it doesn't need to send data
|
||||
serverFacade.needsDataCommunicatorReset();
|
||||
}
|
||||
if (params.filter !== lastFilter) {
|
||||
throw new Error("Expected params.filter to be '" + lastFilter + "' but was '" + params.filter + "'");
|
||||
}
|
||||
// Remove the debouncer before clearing page callbacks.
|
||||
// This makes sure that they are executed.
|
||||
comboBox._filterDebouncer = undefined;
|
||||
// Call the method again after debounce.
|
||||
clearPageCallbacks();
|
||||
comboBox.dataProvider(params, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Postpone the execution of new callbacks if there is an active debouncer.
|
||||
// They will be executed when the page callbacks are cleared within the debouncer.
|
||||
if (comboBox._filterDebouncer) {
|
||||
pageCallbacks[params.page] = callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache[params.page]) {
|
||||
// This may happen after skipping pages by scrolling fast
|
||||
commitPage(params.page, callback);
|
||||
} else {
|
||||
pageCallbacks[params.page] = callback;
|
||||
const maxRangeCount = Math.max(params.pageSize * 2, 500); // Max item count in active range
|
||||
const activePages = Object.keys(pageCallbacks).map((page) => parseInt(page));
|
||||
const rangeMin = Math.min(...activePages);
|
||||
const rangeMax = Math.max(...activePages);
|
||||
|
||||
if (activePages.length * params.pageSize > maxRangeCount) {
|
||||
if (params.page === rangeMin) {
|
||||
clearPageCallbacks([String(rangeMax)]);
|
||||
} else {
|
||||
clearPageCallbacks([String(rangeMin)]);
|
||||
}
|
||||
comboBox.dataProvider(params, callback);
|
||||
} else if (rangeMax - rangeMin + 1 !== activePages.length) {
|
||||
// Wasn't a sequential page index, clear the cache so combo-box will request for new pages
|
||||
clearPageCallbacks();
|
||||
} else {
|
||||
// The requested page was sequential, extend the requested range
|
||||
const startIndex = params.pageSize * rangeMin;
|
||||
const endIndex = params.pageSize * (rangeMax + 1);
|
||||
|
||||
serverFacade.requestData(startIndex, endIndex, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
comboBox.$connector.clear = (start, length) => {
|
||||
const firstPageToClear = Math.floor(start / comboBox.pageSize);
|
||||
const numberOfPagesToClear = Math.ceil(length / comboBox.pageSize);
|
||||
|
||||
for (let i = firstPageToClear; i < firstPageToClear + numberOfPagesToClear; i++) {
|
||||
delete cache[i];
|
||||
}
|
||||
};
|
||||
|
||||
comboBox.$connector.filter = (item, filter) => {
|
||||
filter = filter ? filter.toString().toLowerCase() : '';
|
||||
return comboBox._getItemLabel(item, comboBox.itemLabelPath).toString().toLowerCase().indexOf(filter) > -1;
|
||||
};
|
||||
|
||||
comboBox.$connector.set = (index, items, filter) => {
|
||||
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index % comboBox.pageSize != 0) {
|
||||
throw 'Got new data to index ' + index + ' which is not aligned with the page size of ' + comboBox.pageSize;
|
||||
}
|
||||
|
||||
if (index === 0 && items.length === 0 && pageCallbacks[0]) {
|
||||
// Makes sure that the dataProvider callback is called even when server
|
||||
// returns empty data set (no items match the filter).
|
||||
cache[0] = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPageToSet = index / comboBox.pageSize;
|
||||
const updatedPageCount = Math.ceil(items.length / comboBox.pageSize);
|
||||
|
||||
for (let i = 0; i < updatedPageCount; i++) {
|
||||
let page = firstPageToSet + i;
|
||||
let slice = items.slice(i * comboBox.pageSize, (i + 1) * comboBox.pageSize);
|
||||
|
||||
cache[page] = slice;
|
||||
}
|
||||
};
|
||||
|
||||
comboBox.$connector.updateData = (items) => {
|
||||
const itemsMap = new Map(items.map((item) => [item.key, item]));
|
||||
|
||||
comboBox.filteredItems = comboBox.filteredItems.map((item) => {
|
||||
return itemsMap.get(item.key) || item;
|
||||
});
|
||||
};
|
||||
|
||||
comboBox.$connector.updateSize = function (newSize) {
|
||||
if (!comboBox._clientSideFilter) {
|
||||
// FIXME: It may be that this size set is unnecessary, since when
|
||||
// providing data to combobox via callback we may use data's size.
|
||||
// However, if this size reflect the whole data size, including
|
||||
// data not fetched yet into client side, and combobox expect it
|
||||
// to be set as such, the at least, we don't need it in case the
|
||||
// filter is clientSide only, since it'll increase the height of
|
||||
// the popup at only at first user filter to this size, while the
|
||||
// filtered items count are less.
|
||||
comboBox.size = newSize;
|
||||
}
|
||||
};
|
||||
|
||||
comboBox.$connector.reset = function () {
|
||||
// Cancel pending requests, as clearCache below will set the combo
|
||||
// in a state where it will always request new data, regardless
|
||||
// what is in the cache already.
|
||||
if (comboBox._filterDebouncer) {
|
||||
comboBox._filterDebouncer.cancel();
|
||||
comboBox._filterDebouncer = undefined;
|
||||
}
|
||||
clearPageCallbacks();
|
||||
cache = {};
|
||||
comboBox.clearCache();
|
||||
};
|
||||
|
||||
comboBox.$connector.confirm = function (id, filter) {
|
||||
if (filter != serverFacade.getLastFilterSentToServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're done applying changes from this batch, resolve pending
|
||||
// callbacks
|
||||
let activePages = Object.getOwnPropertyNames(pageCallbacks);
|
||||
for (let i = 0; i < activePages.length; i++) {
|
||||
let page = activePages[i];
|
||||
|
||||
if (cache[page]) {
|
||||
commitPage(page, pageCallbacks[page]);
|
||||
}
|
||||
}
|
||||
|
||||
// Let server know we're done
|
||||
comboBox.$server.confirmUpdate(id);
|
||||
};
|
||||
|
||||
const commitPage = function (page, callback) {
|
||||
let data = cache[page];
|
||||
|
||||
if (comboBox._clientSideFilter) {
|
||||
performClientSideFilter(data, comboBox.filter, callback);
|
||||
} else {
|
||||
// Remove the data if server-side filtering, but keep it for client-side
|
||||
// filtering
|
||||
delete cache[page];
|
||||
|
||||
// FIXME: It may be that we ought to provide data.length instead of
|
||||
// comboBox.size and remove updateSize function.
|
||||
callback(data, comboBox.size);
|
||||
}
|
||||
};
|
||||
|
||||
// Perform filter on client side (here) using the items from specified page
|
||||
// and submitting the filtered items to specified callback.
|
||||
// The filter used is the one from combobox, not the lastFilter stored since
|
||||
// that may not reflect user's input.
|
||||
const performClientSideFilter = function (page, filter, callback) {
|
||||
let filteredItems = page;
|
||||
|
||||
if (filter) {
|
||||
filteredItems = page.filter((item) => comboBox.$connector.filter(item, filter));
|
||||
}
|
||||
|
||||
callback(filteredItems, filteredItems.length);
|
||||
};
|
||||
|
||||
// Prevent setting the custom value as the 'value'-prop automatically
|
||||
comboBox.addEventListener('custom-value-set', (e) => e.preventDefault());
|
||||
|
||||
comboBox.itemClassNameGenerator = function (item) {
|
||||
return item.className || '';
|
||||
};
|
||||
};
|
||||
|
||||
window.Vaadin.ComboBoxPlaceholder = ComboBoxPlaceholder;
|
||||
@@ -0,0 +1,122 @@
|
||||
function getContainer(appId, nodeId) {
|
||||
try {
|
||||
return window.Vaadin.Flow.clients[appId].getByNodeId(nodeId);
|
||||
} catch (error) {
|
||||
console.error('Could not get node %s from app %s', nodeId, appId);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the connector for a context menu element.
|
||||
*
|
||||
* @param {HTMLElement} contextMenu
|
||||
* @param {string} appId
|
||||
*/
|
||||
function initLazy(contextMenu, appId) {
|
||||
if (contextMenu.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
contextMenu.$connector = {
|
||||
/**
|
||||
* Generates and assigns the items to the context menu.
|
||||
*
|
||||
* @param {number} nodeId
|
||||
*/
|
||||
generateItems(nodeId) {
|
||||
const items = generateItemsTree(appId, nodeId);
|
||||
|
||||
contextMenu.items = items;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an items tree compatible with the context-menu web component
|
||||
* by traversing the given Flow DOM tree of context menu item nodes
|
||||
* whose root node is identified by the `nodeId` argument.
|
||||
*
|
||||
* The app id is required to access the store of Flow DOM nodes.
|
||||
*
|
||||
* @param {string} appId
|
||||
* @param {number} nodeId
|
||||
*/
|
||||
function generateItemsTree(appId, nodeId) {
|
||||
const container = getContainer(appId, nodeId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Array.from(container.children).map((child) => {
|
||||
const item = {
|
||||
component: child,
|
||||
checked: child._checked,
|
||||
keepOpen: child._keepOpen,
|
||||
className: child.className,
|
||||
theme: child.__theme
|
||||
};
|
||||
// Do not hardcode tag name to allow `vaadin-menu-bar-item`
|
||||
if (child._hasVaadinItemMixin && child._containerNodeId) {
|
||||
item.children = generateItemsTree(appId, child._containerNodeId);
|
||||
}
|
||||
child._item = item;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the checked state for a context menu item.
|
||||
*
|
||||
* This method is supposed to be called when the context menu item is closed,
|
||||
* so there is no need for triggering a re-render eagarly.
|
||||
*
|
||||
* @param {HTMLElement} component
|
||||
* @param {boolean} checked
|
||||
*/
|
||||
function setChecked(component, checked) {
|
||||
if (component._item) {
|
||||
component._item.checked = checked;
|
||||
|
||||
// Set the attribute in the connector to show the checkmark
|
||||
// without having to re-render the whole menu while opened.
|
||||
if (component._item.keepOpen) {
|
||||
component.toggleAttribute('menu-item-checked', checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keep open state for a context menu item.
|
||||
*
|
||||
* @param {HTMLElement} component
|
||||
* @param {boolean} keepOpen
|
||||
*/
|
||||
function setKeepOpen(component, keepOpen) {
|
||||
if (component._item) {
|
||||
component._item.keepOpen = keepOpen;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme for a context menu item.
|
||||
*
|
||||
* This method is supposed to be called when the context menu item is closed,
|
||||
* so there is no need for triggering a re-render eagarly.
|
||||
*
|
||||
* @param {HTMLElement} component
|
||||
* @param {string | undefined | null} theme
|
||||
*/
|
||||
function setTheme(component, theme) {
|
||||
if (component._item) {
|
||||
component._item.theme = theme;
|
||||
}
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.contextMenuConnector = {
|
||||
initLazy,
|
||||
generateItemsTree,
|
||||
setChecked,
|
||||
setKeepOpen,
|
||||
setTheme
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as Gestures from '@vaadin/component-base/src/gestures.js';
|
||||
|
||||
function init(target) {
|
||||
if (target.$contextMenuTargetConnector) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.$contextMenuTargetConnector = {
|
||||
openOnHandler(e) {
|
||||
// used by Grid to prevent context menu on selection column click
|
||||
if (target.preventContextMenu && target.preventContextMenu(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.$contextMenuTargetConnector.openEvent = e;
|
||||
let detail = {};
|
||||
if (target.getContextMenuBeforeOpenDetail) {
|
||||
detail = target.getContextMenuBeforeOpenDetail(e);
|
||||
}
|
||||
target.dispatchEvent(
|
||||
new CustomEvent('vaadin-context-menu-before-open', {
|
||||
detail: detail
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
updateOpenOn(eventType) {
|
||||
this.removeListener();
|
||||
this.openOnEventType = eventType;
|
||||
|
||||
customElements.whenDefined('vaadin-context-menu').then(() => {
|
||||
if (Gestures.gestures[eventType]) {
|
||||
Gestures.addListener(target, eventType, this.openOnHandler);
|
||||
} else {
|
||||
target.addEventListener(eventType, this.openOnHandler);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeListener() {
|
||||
if (this.openOnEventType) {
|
||||
if (Gestures.gestures[this.openOnEventType]) {
|
||||
Gestures.removeListener(target, this.openOnEventType, this.openOnHandler);
|
||||
} else {
|
||||
target.removeEventListener(this.openOnEventType, this.openOnHandler);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
openMenu(contextMenu) {
|
||||
contextMenu.open(this.openEvent);
|
||||
},
|
||||
|
||||
removeConnector() {
|
||||
this.removeListener();
|
||||
target.$contextMenuTargetConnector = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.contextMenuTargetConnector = { init };
|
||||
@@ -0,0 +1 @@
|
||||
// Full cdn version: 25.0.7-undefined
|
||||
3
backend/src/main/frontend/generated/jar-resources/copilot.d.ts
vendored
Normal file
3
backend/src/main/frontend/generated/jar-resources/copilot.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { registerImporter, createChildrenDefinitions } from './copilot/figma-public/figma-api';
|
||||
export type { FigmaNode, ImportMetadata } from './copilot/figma-public/figma-api';
|
||||
export type { ComponentDefinition, ComponentDefinitionProperties } from './copilot/shared/flow-utils';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { aB as a, aA as i } from "./copilot/copilot-BvIxHaRg.js";
|
||||
export {
|
||||
a as createChildrenDefinitions,
|
||||
i as registerImporter
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { M as t, w as n, j as a, ay as i, b as o } from "./copilot-BvIxHaRg.js";
|
||||
class l extends t {
|
||||
constructor() {
|
||||
super(...arguments), this.eventBusRemovers = [], this.messageHandlers = {}, this.handleESC = (e) => {
|
||||
const s = n.getPanelByTag(this.tagName);
|
||||
!a.active && s && !s.individual || e.key === "Escape" && i(this);
|
||||
};
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
onEventBus(e, s) {
|
||||
this.eventBusRemovers.push(o.on(e, s));
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.addESCListener();
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(), this.eventBusRemovers.forEach((e) => e()), this.removeESCListener();
|
||||
}
|
||||
addESCListener() {
|
||||
document.addEventListener("keydown", this.handleESC);
|
||||
}
|
||||
removeESCListener() {
|
||||
document.removeEventListener("keydown", this.handleESC);
|
||||
}
|
||||
onCommand(e, s) {
|
||||
this.messageHandlers[e] = s;
|
||||
}
|
||||
handleMessage(e) {
|
||||
return this.messageHandlers[e.command] ? (this.messageHandlers[e.command].call(this, e), !0) : !1;
|
||||
}
|
||||
}
|
||||
export {
|
||||
l as B
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
|
||||
import { j as p, D as g, a6 as m, a7 as R, as as b, a8 as $, at as y, M as F, E as u, au as x, y as w, av as T, r as f } from "./copilot-BvIxHaRg.js";
|
||||
import { r as v } from "./state-BGGS46O3.js";
|
||||
import { B as O } from "./base-panel-C5as2IDv.js";
|
||||
import { i as h } from "./icons-DpjjuYvb.js";
|
||||
const S = "copilot-features-panel{padding:var(--space-100);font:var(--copilot-font-xs);display:grid;grid-template-columns:auto 1fr;gap:var(--space-50);height:auto}copilot-features-panel a{display:flex;align-items:center;justify-self:end;gap:var(--space-50);white-space:nowrap}copilot-features-panel a svg{height:12px;width:12px;min-height:12px;min-width:12px}";
|
||||
var q = Object.defineProperty, C = Object.getOwnPropertyDescriptor, o = (t, e, a, s) => {
|
||||
for (var r = s > 1 ? void 0 : s ? C(e, a) : e, i = t.length - 1, n; i >= 0; i--)
|
||||
(n = t[i]) && (r = (s ? n(e, a, r) : n(r)) || r);
|
||||
return s && r && q(e, a, r), r;
|
||||
};
|
||||
const l = window.Vaadin.devTools;
|
||||
let d = class extends O {
|
||||
constructor() {
|
||||
super(...arguments), this.toggledFeaturesThatAreRequiresServerRestart = [];
|
||||
}
|
||||
render() {
|
||||
return g` <style>
|
||||
${S}
|
||||
</style>
|
||||
${p.featureFlags.map(
|
||||
(t) => g`
|
||||
<copilot-toggle-button
|
||||
.title="${t.title}"
|
||||
?checked=${t.enabled}
|
||||
@on-change=${(e) => this.toggleFeatureFlag(e, t)}>
|
||||
</copilot-toggle-button>
|
||||
<a class="ahreflike" href="${t.moreInfoLink}" title="Learn more" target="_blank"
|
||||
>learn more ${h.share}</a
|
||||
>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
toggleFeatureFlag(t, e) {
|
||||
const a = t.target.checked;
|
||||
m("use-feature", { source: "toggle", enabled: a, id: e.id }), l.frontendConnection ? (l.frontendConnection.send("setFeature", { featureId: e.id, enabled: a }), e.requiresServerRestart && p.toggleServerRequiringFeatureFlag(e), R({
|
||||
type: $.INFORMATION,
|
||||
message: `“${e.title}” ${a ? "enabled" : "disabled"}`,
|
||||
details: e.requiresServerRestart ? b() : void 0,
|
||||
dismissId: `feature${e.id}${a ? "Enabled" : "Disabled"}`
|
||||
}), y()) : l.log("error", `Unable to toggle feature ${e.title}: No server connection available`);
|
||||
}
|
||||
};
|
||||
o([
|
||||
v()
|
||||
], d.prototype, "toggledFeaturesThatAreRequiresServerRestart", 2);
|
||||
d = o([
|
||||
f("copilot-features-panel")
|
||||
], d);
|
||||
let c = class extends F {
|
||||
constructor() {
|
||||
super(...arguments), this.serverRestarting = !1;
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
if (p.serverRestartRequiringToggledFeatureFlags.length === 0)
|
||||
return u;
|
||||
if (!x())
|
||||
return u;
|
||||
const t = this.serverRestarting ? "Restarting..." : "Click to restart server";
|
||||
return g`
|
||||
<style>
|
||||
.fade-in-out {
|
||||
animation: fadeInOut 2s ease-in-out infinite;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.fade-in-out:hover {
|
||||
animation-play-state: paused;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
${w}
|
||||
</style>
|
||||
<button
|
||||
?disabled="${this.serverRestarting}"
|
||||
id="restart-server-btn"
|
||||
class="icon ${this.serverRestarting ? "" : "fade-in-out"}"
|
||||
@click=${() => {
|
||||
this.serverRestarting = !0, T();
|
||||
}}>
|
||||
<span>${h.refresh}</span>
|
||||
</button>
|
||||
<vaadin-tooltip for="restart-server-btn" text=${t}></vaadin-tooltip>
|
||||
`;
|
||||
}
|
||||
};
|
||||
o([
|
||||
v()
|
||||
], c.prototype, "serverRestarting", 2);
|
||||
c = o([
|
||||
f("copilot-features-actions")
|
||||
], c);
|
||||
const I = {
|
||||
header: "Features",
|
||||
expanded: !1,
|
||||
panelOrder: 35,
|
||||
panel: "right",
|
||||
floating: !1,
|
||||
tag: "copilot-features-panel",
|
||||
helpUrl: "https://vaadin.com/docs/latest/flow/configuration/feature-flags",
|
||||
actionsTag: "copilot-features-actions"
|
||||
}, P = {
|
||||
init(t) {
|
||||
t.addPanel(I);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(P);
|
||||
export {
|
||||
c as CopilotFeaturesActions,
|
||||
d as CopilotFeaturesPanel
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
import { D as c, j as d, b as h, w as y, a6 as u, s as v, P as f, H as m, r as k } from "./copilot-BvIxHaRg.js";
|
||||
import { r as s } from "./state-BGGS46O3.js";
|
||||
import { e as w } from "./query-BykXNUlT.js";
|
||||
import { B as $ } from "./base-panel-C5as2IDv.js";
|
||||
import { i as x } from "./icons-DpjjuYvb.js";
|
||||
const A = "copilot-feedback-panel{display:flex;flex-direction:column;font:var(--copilot-font-xs);gap:var(--space-200);padding:var(--space-150)}copilot-feedback-panel>p{margin:0}copilot-feedback-panel .dialog-footer{display:flex;gap:var(--space-100)}copilot-feedback-panel :is(vaadin-select,vaadin-email-field)::part(input-field){padding-block:0}copilot-feedback-panel :is(vaadin-select)::part(input-field){padding-inline-end:0}copilot-feedback-panel vaadin-select::part(toggle-button){align-items:center;display:flex;height:var(--copilot-size-md);justify-content:center;width:var(--copilot-size-md)}copilot-feedback-panel vaadin-text-area textarea{line-height:var(--copilot-line-height-sm)}copilot-feedback-panel vaadin-text-area:hover::part(input-field){background:none}copilot-feedback-panel>*::part(helper-text){line-height:var(--copilot-line-height-sm);margin:0}";
|
||||
var P = Object.defineProperty, F = Object.getOwnPropertyDescriptor, o = (e, t, n, l) => {
|
||||
for (var a = l > 1 ? void 0 : l ? F(t, n) : t, p = e.length - 1, r; p >= 0; p--)
|
||||
(r = e[p]) && (a = (l ? r(t, n, a) : r(a)) || a);
|
||||
return l && a && P(t, n, a), a;
|
||||
};
|
||||
const T = "https://github.com/vaadin", b = "https://github.com/vaadin/copilot/issues/new", D = "?template=feature_request.md&title=%5BFEATURE%5D", E = "A short, concise description of the bug and why you consider it a bug. Any details like exceptions and logs can be helpful as well.", C = "Please provide as many details as possible, this will help us deliver a fix as soon as possible.%0AThank you!%0A%0A%23%23%23 Description of the Bug%0A%0A{description}%0A%0A%23%23%23 Expected Behavior%0A%0AA description of what you would expect to happen. (Sometimes it is clear what the expected outcome is if something does not work, other times, it is not super clear.)%0A%0A%23%23%23 Minimal Reproducible Example%0A%0AWe would appreciate the minimum code with which we can reproduce the issue.%0A%0A%23%23%23 Versions%0A{versionsInfo}";
|
||||
let i = class extends $ {
|
||||
constructor() {
|
||||
super(), this.description = "", this.types = [
|
||||
{
|
||||
label: "Generic feedback",
|
||||
value: "feedback",
|
||||
ghTitle: ""
|
||||
},
|
||||
{
|
||||
label: "Report a bug",
|
||||
value: "bug",
|
||||
ghTitle: "[BUG]"
|
||||
},
|
||||
{
|
||||
label: "Ask a question",
|
||||
value: "question",
|
||||
ghTitle: "[QUESTION]"
|
||||
},
|
||||
{
|
||||
label: "Share an idea",
|
||||
value: "idea",
|
||||
ghTitle: "[FEATURE]"
|
||||
}
|
||||
], this.type = this.types[0].value, this.topics = [
|
||||
{
|
||||
label: "Generic",
|
||||
value: "platform"
|
||||
},
|
||||
{
|
||||
label: "Flow",
|
||||
value: "flow"
|
||||
},
|
||||
{
|
||||
label: "Hilla",
|
||||
value: "hilla"
|
||||
},
|
||||
{
|
||||
label: "Copilot",
|
||||
value: "copilot"
|
||||
}
|
||||
], this.topic = this.topics[0].value;
|
||||
}
|
||||
render() {
|
||||
return c`<style>
|
||||
${A}</style
|
||||
>${this.renderContent()}${this.renderFooter()}`;
|
||||
}
|
||||
renderContent() {
|
||||
return this.message === void 0 ? c`
|
||||
<p>
|
||||
Your insights are incredibly valuable to us. Whether you’ve encountered a hiccup, have questions, or ideas
|
||||
to make our platform better, we're all ears! If you wish, leave your email, and we’ll get back to you. You
|
||||
can even share your code snippet with us for a clearer picture.
|
||||
</p>
|
||||
<vaadin-select
|
||||
.items="${this.types}"
|
||||
.value="${this.type}"
|
||||
overlay-class="alwaysVisible"
|
||||
@value-changed=${(e) => {
|
||||
this.type = e.detail.value;
|
||||
}}>
|
||||
</vaadin-select>
|
||||
<vaadin-select
|
||||
label="Feedback Topic"
|
||||
overlay-class="alwaysVisible"
|
||||
.items=${this.topics}
|
||||
.value="${this.topic}"
|
||||
.hidden=${this.type !== "feedback"}
|
||||
@value-changed=${(e) => {
|
||||
this.topic = e.detail.value;
|
||||
}}>
|
||||
</vaadin-select>
|
||||
<vaadin-text-area
|
||||
.value="${this.description}"
|
||||
@keydown=${this.keyDown}
|
||||
@focus=${() => {
|
||||
this.descriptionField.invalid = !1, this.descriptionField.placeholder = "";
|
||||
}}
|
||||
@value-changed=${(e) => {
|
||||
this.description = e.detail.value;
|
||||
}}
|
||||
label="Tell Us More"
|
||||
helper-text="Describe what you're experiencing, wondering about, or envisioning. The more you share, the better we can understand and act on your feedback"></vaadin-text-area>
|
||||
<vaadin-text-field
|
||||
@keydown=${this.keyDown}
|
||||
@value-changed=${(e) => {
|
||||
this.email = e.detail.value;
|
||||
}}
|
||||
.required=${this.type === "question"}
|
||||
id="email"
|
||||
value="${d.userInfo?.email}"
|
||||
label="Your Email${this.type === "question" ? "" : " (Optional)"}"
|
||||
helper-text="Leave your email if you’d like us to follow up, we’d love to keep the conversation going."></vaadin-text-field>
|
||||
` : c`<p>${this.message}</p>`;
|
||||
}
|
||||
renderFooter() {
|
||||
return this.message === void 0 ? c`
|
||||
<div class="dialog-footer">
|
||||
<button
|
||||
style="margin-inline-end: auto"
|
||||
@click="${() => {
|
||||
d.active ? h.emit("system-info-with-callback", {
|
||||
callback: (e) => this.openGithub(e, this),
|
||||
notify: !1
|
||||
}) : this.openGithub(null, this);
|
||||
}}">
|
||||
<span class="prefix">${x.github}</span>
|
||||
Create GitHub Issue
|
||||
</button>
|
||||
<button @click="${this.close}">Cancel</button>
|
||||
<button class="primary" @click="${this.submit}" .disabled=${this.type === "question" && !this.email}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
` : c` <div class="footer">
|
||||
<vaadin-button @click="${this.close}">Close</vaadin-button>
|
||||
</div>`;
|
||||
}
|
||||
close() {
|
||||
y.updatePanel("copilot-feedback-panel", {
|
||||
floating: !1
|
||||
});
|
||||
}
|
||||
submit() {
|
||||
if (u("feedback", { github: !1, type: this.type, topic: this.topic }), this.description.trim() === "") {
|
||||
this.descriptionField.invalid = !0, this.descriptionField.placeholder = "Please tell us more before sending", this.descriptionField.value = "";
|
||||
return;
|
||||
}
|
||||
const e = {
|
||||
description: this.description,
|
||||
email: this.email,
|
||||
type: this.type,
|
||||
topic: this.topic
|
||||
};
|
||||
d.active ? h.emit("system-info-with-callback", {
|
||||
callback: (t) => v(`${f}feedback`, { ...e, versions: t }),
|
||||
notify: !1
|
||||
}) : v(`${f}feedback`, { ...e, versions: {} }), this.parentNode?.style.setProperty("--section-height", "150px"), this.message = "Thank you for sharing feedback.";
|
||||
}
|
||||
keyDown(e) {
|
||||
(e.key === "Backspace" || e.key === "Delete") && e.stopPropagation();
|
||||
}
|
||||
openGithub(e, t) {
|
||||
if (u("feedback", { github: !0, type: this.type, topic: this.topic }), this.type === "idea") {
|
||||
window.open(`${b}${D}`);
|
||||
return;
|
||||
}
|
||||
if (this.type === "feedback") {
|
||||
window.open(`${T}/${this.topic}/issues/new`);
|
||||
return;
|
||||
}
|
||||
const n = e ? e.replace(/\n/g, "%0A") : "Activate Copilot to include version info.", l = `${t.types.find((r) => r.value === this.type)?.ghTitle}`, a = t.description !== "" ? t.description : E, p = C.replace("{description}", a).replace("{versionsInfo}", n);
|
||||
window.open(`${b}?title=${l}&body=${p}`, "_blank")?.focus();
|
||||
}
|
||||
};
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "description", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "type", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "topic", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "email", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "message", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "types", 2);
|
||||
o([
|
||||
s()
|
||||
], i.prototype, "topics", 2);
|
||||
o([
|
||||
w("vaadin-text-area")
|
||||
], i.prototype, "descriptionField", 2);
|
||||
i = o([
|
||||
k("copilot-feedback-panel")
|
||||
], i);
|
||||
const g = m({
|
||||
header: "Help Us Improve!",
|
||||
tag: "copilot-feedback-panel",
|
||||
width: 500,
|
||||
height: 550,
|
||||
floatingPosition: {
|
||||
top: 100,
|
||||
left: 100
|
||||
},
|
||||
individual: !0
|
||||
}), U = {
|
||||
init(e) {
|
||||
e.addPanel(g);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(U);
|
||||
y.addPanel(g);
|
||||
export {
|
||||
i as CopilotFeedbackPanel
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,202 @@
|
||||
import { j as p, J as c, w as y, D as s, K as g, E as k, ar as u, T as $, _ as I, a7 as V, a8 as S, M as C, b as D, r as b } from "./copilot-BvIxHaRg.js";
|
||||
import { B as E } from "./base-panel-C5as2IDv.js";
|
||||
import { i as d } from "./icons-DpjjuYvb.js";
|
||||
import { c as P } from "./index-DjQvWJdw.js";
|
||||
const A = 'copilot-info-panel{--dev-tools-red-color: red;--dev-tools-grey-color: gray;--dev-tools-green-color: green;position:relative}copilot-info-panel dl{margin:0;width:100%}copilot-info-panel dl>div{align-items:center;display:flex;gap:var(--space-50);height:var(--copilot-size-md);padding:0 var(--space-150);position:relative}copilot-info-panel dl>div:after{border-bottom:1px solid var(--divider-secondary-color);content:"";inset:auto var(--space-150) 0;position:absolute}copilot-info-panel dl dt{color:var(--vaadin-text-color-secondary)}copilot-info-panel dl dd{align-items:center;display:flex;font-weight:var(--copilot-font-weight-medium);gap:var(--space-50);margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}copilot-info-panel dl dd span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}copilot-info-panel dl dd span.icon{display:inline-flex;vertical-align:bottom}copilot-info-panel dd.live-reload-status>span{overflow:hidden;text-overflow:ellipsis;display:block;color:var(--status-color)}copilot-info-panel dd span.hidden{display:none}copilot-info-panel code{white-space:nowrap;-webkit-user-select:all;user-select:all}copilot-info-panel .checks{display:inline-grid;grid-template-columns:auto 1fr;gap:var(--space-50)}copilot-info-panel span.hint{font-size:var(--copilot-font-size-xs);background:var(--gray-50);padding:var(--space-75);border-radius:var(--vaadin-radius-m)}';
|
||||
var T = Object.getOwnPropertyDescriptor, h = (e, t, i, o) => {
|
||||
for (var a = o > 1 ? void 0 : o ? T(t, i) : t, n = e.length - 1, l; n >= 0; n--)
|
||||
(l = e[n]) && (a = l(a) || a);
|
||||
return a;
|
||||
};
|
||||
let m = class extends E {
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.onEventBus("system-info-with-callback", (e) => {
|
||||
e.detail.callback(this.getInfoForClipboard(e.detail.notify));
|
||||
}), this.reaction(
|
||||
() => p.idePluginState,
|
||||
() => {
|
||||
this.requestUpdate("serverInfo");
|
||||
}
|
||||
);
|
||||
}
|
||||
getIndex(e) {
|
||||
return c.serverVersions.findIndex((t) => t.name === e);
|
||||
}
|
||||
render() {
|
||||
const e = p.newVaadinVersionState?.versions !== void 0 && p.newVaadinVersionState.versions.length > 0, t = [];
|
||||
p.userInfo?.vaadiner && t.push({
|
||||
name: "Vaadin Employee",
|
||||
version: "true"
|
||||
});
|
||||
const i = [
|
||||
...c.serverVersions,
|
||||
...t,
|
||||
...c.clientVersions
|
||||
].map((a) => {
|
||||
const n = { ...a };
|
||||
return n.name === "Vaadin" && (n.more = s` <button
|
||||
aria-label="Edit Vaadin Version"
|
||||
class="icon relative"
|
||||
id="new-vaadin-version-btn"
|
||||
title="Edit Vaadin Version"
|
||||
@click="${(l) => {
|
||||
l.stopPropagation(), y.updatePanel("copilot-vaadin-versions", { floating: !0 });
|
||||
}}">
|
||||
${d.editSquare}
|
||||
${e ? s`<span aria-hidden="true" class="absolute bg-error end-0 h-75 rounded-full top-0 w-75"></span>` : ""}
|
||||
</button>`), n;
|
||||
});
|
||||
let o = this.getIndex("Spring") + 1;
|
||||
return o === 0 && (o = i.length), c.springSecurityEnabled && (i.splice(o, 0, { name: "Spring Security", version: "true" }), o++), c.springJpaDataEnabled && (i.splice(o, 0, { name: "Spring Data JPA", version: "true" }), o++), s` <style>
|
||||
${A}
|
||||
</style>
|
||||
<div class="flex flex-col gap-150 items-start">
|
||||
<dl>
|
||||
${i.map(
|
||||
(a) => s`
|
||||
<div>
|
||||
<dt>${a.name}</dt>
|
||||
<dd title="${a.version}">
|
||||
<span> ${this.renderValue(a.version)} </span>
|
||||
${a.more}
|
||||
</dd>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.renderDevWorkflowSection()} ${this.renderDevelopmentWorkflowButton()}
|
||||
</dl>
|
||||
</div>`;
|
||||
}
|
||||
renderDevWorkflowSection() {
|
||||
const e = g(), t = this.getIdePluginLabelText(p.idePluginState), i = this.getHotswapAgentLabelText(e);
|
||||
return s`
|
||||
<div>
|
||||
<dt>Java Hotswap</dt>
|
||||
<dd>
|
||||
${f(e === "success", e === "success" ? "Enabled" : "Disabled")} ${i}
|
||||
</dd>
|
||||
</div>
|
||||
${u() !== "unsupported" ? s` <div>
|
||||
<dt>IDE Plugin</dt>
|
||||
<dd>
|
||||
${f(
|
||||
u() === "success",
|
||||
u() === "success" ? "Installed" : "Not Installed"
|
||||
)}
|
||||
${t}
|
||||
</dd>
|
||||
</div>` : k}
|
||||
`;
|
||||
}
|
||||
renderDevelopmentWorkflowButton() {
|
||||
const e = $();
|
||||
let t = "", i = null, o = "";
|
||||
return e.status === "success" ? (t = "success", i = d.check, o = "IDE Plugin and Java Hotswap are in use.") : e.status === "warning" ? (t = "warning", i = d.lightning, o = "Improve Development Workflow") : e.status === "error" && (t = "error", i = d.alertCircle, o = "Fix Development Workflow"), s`
|
||||
<div>
|
||||
<dt>Development Workflow</dt>
|
||||
<dd>
|
||||
<span class="${t}-text icon" id="development-status-value">${i}</span>
|
||||
<vaadin-tooltip for="development-status-value" text="${o}"></vaadin-tooltip>
|
||||
<button
|
||||
id="development-workflow-status-detail"
|
||||
class="link-button"
|
||||
@click=${() => {
|
||||
I();
|
||||
}}>
|
||||
Show details
|
||||
</button>
|
||||
</dd>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
getHotswapAgentLabelText(e) {
|
||||
return e === "success" ? "Java Hotswap is enabled" : e === "error" ? "Hotswap is partially enabled" : "Hotswap is disabled";
|
||||
}
|
||||
getIdePluginLabelText(e) {
|
||||
if (u() !== "success")
|
||||
return "Not installed";
|
||||
if (e?.version) {
|
||||
let t = null;
|
||||
return e?.ide && (e?.ide === "intellij" ? t = "IntelliJ" : e?.ide === "vscode" ? t = "VS Code" : e?.ide === "eclipse" && (t = "Eclipse")), t ? `${e?.version} ${t}` : e?.version;
|
||||
}
|
||||
return "Not installed";
|
||||
}
|
||||
renderValue(e) {
|
||||
return e === "false" ? f(!1, "False") : e === "true" ? f(!0, "True") : e;
|
||||
}
|
||||
getInfoForClipboard(e) {
|
||||
const t = this.renderRoot.querySelectorAll(".items-start dt"), a = Array.from(t).map((n) => ({
|
||||
key: n.textContent.trim(),
|
||||
value: n.nextElementSibling.textContent.trim()
|
||||
})).filter((n) => n.key !== "Live reload").filter((n) => !n.key.startsWith("Vaadin Emplo")).filter((n) => n.key !== "Development Workflow").map((n) => {
|
||||
const { key: l } = n;
|
||||
let { value: r } = n;
|
||||
if (l === "IDE Plugin")
|
||||
r = this.getIdePluginLabelText(p.idePluginState) ?? "false";
|
||||
else if (l === "Java Hotswap") {
|
||||
const x = c.jdkInfo?.jrebel, v = g();
|
||||
x && v === "success" ? r = "JRebel is in use" : r = this.getHotswapAgentLabelText(v);
|
||||
} else l === "Vaadin" && r.indexOf(`
|
||||
`) !== -1 && (r = r.substring(0, r.indexOf(`
|
||||
`)));
|
||||
return `${l}: ${r}`;
|
||||
}).join(`
|
||||
`);
|
||||
return e && V({
|
||||
type: S.INFORMATION,
|
||||
message: "Environment information copied to clipboard",
|
||||
dismissId: "versionInfoCopied"
|
||||
}), a.trim();
|
||||
}
|
||||
};
|
||||
m = h([
|
||||
b("copilot-info-panel")
|
||||
], m);
|
||||
let w = class extends C {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.style.display = "flex";
|
||||
}
|
||||
render() {
|
||||
return s` <button
|
||||
@click=${() => {
|
||||
D.emit("system-info-with-callback", {
|
||||
callback: P,
|
||||
notify: !0
|
||||
});
|
||||
}}
|
||||
aria-label="Copy to Clipboard"
|
||||
class="icon"
|
||||
title="Copy to Clipboard">
|
||||
<span>${d.copy}</span>
|
||||
</button>`;
|
||||
}
|
||||
};
|
||||
w = h([
|
||||
b("copilot-info-actions")
|
||||
], w);
|
||||
const H = {
|
||||
header: "Info",
|
||||
expanded: !1,
|
||||
panelOrder: 15,
|
||||
panel: "right",
|
||||
floating: !1,
|
||||
tag: "copilot-info-panel",
|
||||
actionsTag: "copilot-info-actions",
|
||||
eager: !0
|
||||
// Render even when collapsed as error handling depends on this
|
||||
}, J = {
|
||||
init(e) {
|
||||
e.addPanel(H);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(J);
|
||||
function f(e, t) {
|
||||
return e ? s`<span aria-label=${t} class="icon success-text" title=${t}>${d.check}</span>` : s`<span aria-label=${t} class="icon error-text" title=${t}>${d.x}</span>`;
|
||||
}
|
||||
export {
|
||||
w as Actions,
|
||||
m as CopilotInfoPanel
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
import { j as R, ao as L, ap as f, ab as c, D as l, a8 as p, aq as S, a6 as M, M as D, b as I, L as k, m as q, w as y, r as w } from "./copilot-BvIxHaRg.js";
|
||||
import { r as v } from "./state-BGGS46O3.js";
|
||||
import { B as P } from "./base-panel-C5as2IDv.js";
|
||||
import { i as r } from "./icons-DpjjuYvb.js";
|
||||
const A = 'copilot-log-panel ul{list-style-type:none;margin:0;padding:0}copilot-log-panel ul li{align-items:start;display:flex;gap:var(--space-50);padding:var(--space-100) var(--space-50);position:relative}copilot-log-panel ul li:before{border-bottom:1px dashed var(--divider-primary-color);content:"";inset:auto 0 0 calc(var(--copilot-size-md) + var(--space-100));position:absolute}copilot-log-panel ul li span.icon{display:flex;flex-shrink:0;justify-content:center;width:var(--copilot-size-md)}copilot-log-panel ul li.information span.icon{color:var(--blue-color)}copilot-log-panel ul li.warning span.icon{color:var(--warning-color)}copilot-log-panel ul li.error span.icon{color:var(--error-color)}copilot-log-panel ul li .message{display:flex;flex-direction:column;flex-grow:1;overflow:hidden}copilot-log-panel ul li:not(.expanded) span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}copilot-log-panel ul li button svg{transition:transform .15s cubic-bezier(.2,0,0,1)}copilot-log-panel ul li button[aria-expanded=true] svg{transform:rotate(90deg)}copilot-log-panel ul li code{margin-top:var(--space-50)}copilot-log-panel ul li.expanded .secondary{margin-top:var(--space-100)}copilot-log-panel .secondary a{display:block;margin-bottom:var(--space-50)}', C = () => {
|
||||
const e = { hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3 };
|
||||
let t;
|
||||
const a = navigator.language ?? "", s = a.indexOf("@"), o = s === -1 ? a : a.slice(0, s);
|
||||
try {
|
||||
t = new Intl.DateTimeFormat(Intl.getCanonicalLocales(o), e);
|
||||
} catch (i) {
|
||||
console.error("Failed to create date time formatter for ", o, i), t = new Intl.DateTimeFormat("en-US", e);
|
||||
}
|
||||
return t;
|
||||
}, _ = C();
|
||||
var b = Object.defineProperty, B = Object.getOwnPropertyDescriptor, u = (e, t, a, s) => {
|
||||
for (var o = s > 1 ? void 0 : s ? B(t, a) : t, i = e.length - 1, n; i >= 0; i--)
|
||||
(n = e[i]) && (o = (s ? n(t, a, o) : n(o)) || o);
|
||||
return s && o && b(t, a, o), o;
|
||||
};
|
||||
class F {
|
||||
constructor() {
|
||||
this.showTimestamps = !1, q(this);
|
||||
}
|
||||
toggleShowTimestamps() {
|
||||
this.showTimestamps = !this.showTimestamps;
|
||||
}
|
||||
}
|
||||
const h = new F();
|
||||
let d = class extends P {
|
||||
constructor() {
|
||||
super(...arguments), this.unreadErrors = !1, this.messages = [], this.nextMessageId = 1, this.transitionDuration = 0, this.errorHandlersAdded = !1;
|
||||
}
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback(), this.onCommand("log", (e) => {
|
||||
this.handleLogEventData({ type: e.data.type, message: e.data.message });
|
||||
}), this.onEventBus("log", (e) => this.handleLogEvent(e)), this.onEventBus("update-log", (e) => this.updateLog(e.detail)), this.onEventBus("notification-shown", (e) => this.handleNotification(e)), this.onEventBus("clear-log", () => this.clear()), this.reaction(
|
||||
() => R.sectionPanelResizing,
|
||||
() => {
|
||||
this.requestUpdate();
|
||||
}
|
||||
), this.transitionDuration = parseInt(
|
||||
window.getComputedStyle(this).getPropertyValue("--dev-tools-transition-duration"),
|
||||
10
|
||||
), !this.errorHandlersAdded) {
|
||||
const e = (t) => {
|
||||
k(() => {
|
||||
y.attentionRequiredPanelTag = "copilot-log-panel";
|
||||
}), this.log(p.ERROR, t.message, !!t.internal, t.details, t.link);
|
||||
};
|
||||
L((t) => {
|
||||
e(t);
|
||||
}), f.forEach((t) => {
|
||||
e(t);
|
||||
}), f.length = 0, this.errorHandlersAdded = !0;
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
this.messages = [];
|
||||
}
|
||||
handleNotification(e) {
|
||||
this.log(e.detail.type, e.detail.message, !0, e.detail.details, e.detail.link);
|
||||
}
|
||||
handleLogEvent(e) {
|
||||
this.handleLogEventData(e.detail);
|
||||
}
|
||||
handleLogEventData(e) {
|
||||
this.log(
|
||||
e.type,
|
||||
e.message,
|
||||
!!e.internal,
|
||||
e.details,
|
||||
e.link,
|
||||
c(e.expandedMessage),
|
||||
c(e.expandedDetails),
|
||||
e.id
|
||||
);
|
||||
}
|
||||
activate() {
|
||||
this.unreadErrors = !1, this.updateComplete.then(() => {
|
||||
const e = this.renderRoot.querySelector(".message:last-child");
|
||||
e && e.scrollIntoView();
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return l`
|
||||
<style>
|
||||
${A}
|
||||
</style>
|
||||
<ul>
|
||||
${this.messages.map((e) => this.renderMessage(e))}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
renderMessage(e) {
|
||||
let t, a;
|
||||
return e.type === p.ERROR ? (a = r.alertTriangle, t = "Error") : e.type === p.WARNING ? (a = r.warning, t = "Warning") : (a = r.info, t = "Info"), l`
|
||||
<li
|
||||
class="${e.type} ${e.expanded ? "expanded" : ""} ${e.details || e.link ? "has-details" : ""}"
|
||||
data-id="${e.id}">
|
||||
<span aria-label="${t}" class="icon" title="${t}">${a}</span>
|
||||
<span class="message" @click=${() => this.toggleExpanded(e)}>
|
||||
<span class="timestamp" ?hidden=${!h.showTimestamps}>${N(e.timestamp)}</span>
|
||||
<span class="primary">
|
||||
${e.expanded && e.expandedMessage ? e.expandedMessage : e.message}
|
||||
</span>
|
||||
${e.expanded ? l` <span class="secondary"> ${e.expandedDetails ?? e.details} </span>` : l` <span class="secondary" ?hidden="${!e.details && !e.link}">
|
||||
${c(e.details)}
|
||||
${e.link ? l` <a href="${e.link}" target="_blank">Learn more</a>` : ""}
|
||||
</span>`}
|
||||
</span>
|
||||
<!-- TODO: a11y, button needs aria-controls with unique ids -->
|
||||
<button
|
||||
aria-controls="content"
|
||||
aria-expanded="${e.expanded}"
|
||||
aria-label="Expand details"
|
||||
class="icon"
|
||||
@click=${() => this.toggleExpanded(e)}
|
||||
?hidden=${!this.canBeExpanded(e)}>
|
||||
<span>${r.chevronRight}</span>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
log(e, t, a, s, o, i, n, E) {
|
||||
const T = this.nextMessageId;
|
||||
this.nextMessageId += 1, n || (n = t);
|
||||
const g = {
|
||||
id: T,
|
||||
type: e,
|
||||
message: t,
|
||||
details: s,
|
||||
link: o,
|
||||
dontShowAgain: !1,
|
||||
deleted: !1,
|
||||
expanded: !1,
|
||||
expandedMessage: i,
|
||||
expandedDetails: n,
|
||||
timestamp: /* @__PURE__ */ new Date(),
|
||||
internal: a,
|
||||
userId: E
|
||||
};
|
||||
for (this.messages.push(g); this.messages.length > d.MAX_LOG_ROWS; )
|
||||
this.messages.shift();
|
||||
return this.requestUpdate(), this.updateComplete.then(() => {
|
||||
const m = this.renderRoot.querySelector(".message:last-child");
|
||||
m ? (setTimeout(() => m.scrollIntoView({ behavior: "smooth" }), this.transitionDuration), this.unreadErrors = !1) : e === p.ERROR && (this.unreadErrors = !0);
|
||||
}), g;
|
||||
}
|
||||
updateLog(e) {
|
||||
let t = this.messages.find((a) => a.userId === e.id);
|
||||
t || (t = this.log(p.INFORMATION, "<Log message to update was not found>", !1)), Object.assign(t, e), S(t.expandedDetails) && (t.expandedDetails = c(t.expandedDetails)), this.requestUpdate();
|
||||
}
|
||||
updated() {
|
||||
const e = this.querySelector(".row:last-child");
|
||||
e && this.isTooLong(e.querySelector(".firstrowmessage")) && e.querySelector("button.expand")?.removeAttribute("hidden");
|
||||
}
|
||||
toggleExpanded(e) {
|
||||
this.canBeExpanded(e) && (e.expanded = !e.expanded, this.requestUpdate()), M("use-log", { source: "toggleExpanded" });
|
||||
}
|
||||
canBeExpanded(e) {
|
||||
if (e.expandedMessage || e.expanded)
|
||||
return !0;
|
||||
const t = this.querySelector(`[data\\-id="${e.id}"]`)?.querySelector(
|
||||
".firstrowmessage"
|
||||
);
|
||||
return this.isTooLong(t);
|
||||
}
|
||||
isTooLong(e) {
|
||||
return e && e.offsetWidth < e.scrollWidth;
|
||||
}
|
||||
};
|
||||
d.MAX_LOG_ROWS = 1e3;
|
||||
u([
|
||||
v()
|
||||
], d.prototype, "unreadErrors", 2);
|
||||
u([
|
||||
v()
|
||||
], d.prototype, "messages", 2);
|
||||
d = u([
|
||||
w("copilot-log-panel")
|
||||
], d);
|
||||
let x = class extends D {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
return l`
|
||||
<style>
|
||||
copilot-log-panel-actions {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
<button
|
||||
aria-label="Clear log"
|
||||
class="icon"
|
||||
title="Clear log"
|
||||
@click=${() => {
|
||||
I.emit("clear-log", {});
|
||||
}}>
|
||||
<span>${r.delete}</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Toggle timestamps"
|
||||
class="icon"
|
||||
title="Toggle timestamps"
|
||||
@click=${() => {
|
||||
h.toggleShowTimestamps();
|
||||
}}>
|
||||
<span class="${h.showTimestamps ? "on" : "off"}"> ${r.clock} </span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
};
|
||||
x = u([
|
||||
w("copilot-log-panel-actions")
|
||||
], x);
|
||||
const $ = {
|
||||
header: "Log",
|
||||
expanded: !0,
|
||||
panelOrder: 0,
|
||||
panel: "bottom",
|
||||
floating: !1,
|
||||
tag: "copilot-log-panel",
|
||||
actionsTag: "copilot-log-panel-actions",
|
||||
individual: !0
|
||||
}, U = {
|
||||
init(e) {
|
||||
e.addPanel($);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(U);
|
||||
y.addPanel($);
|
||||
function N(e) {
|
||||
return _.format(e);
|
||||
}
|
||||
export {
|
||||
x as Actions,
|
||||
d as CopilotLogPanel
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import { r as f, b as n, E as v, D as p, w as b, a3 as g, $ as s, H as $ } from "./copilot-BvIxHaRg.js";
|
||||
import { B as m } from "./base-panel-C5as2IDv.js";
|
||||
import { i as e } from "./icons-DpjjuYvb.js";
|
||||
const y = 'copilot-shortcuts-panel{display:flex;flex-direction:column;padding:var(--space-150)}copilot-shortcuts-panel h3{font:var(--copilot-font-xs-semibold);margin-bottom:var(--space-100);margin-top:0}copilot-shortcuts-panel h3:not(:first-of-type){margin-top:var(--space-200)}copilot-shortcuts-panel ul{display:flex;flex-direction:column;list-style:none;margin:0;padding:0}copilot-shortcuts-panel ul li{display:flex;align-items:center;gap:var(--space-50);position:relative}copilot-shortcuts-panel ul li:not(:last-of-type):before{border-bottom:1px dashed var(--border-color);content:"";inset:auto 0 0 calc(var(--copilot-size-md) + var(--space-50));position:absolute}copilot-shortcuts-panel ul li span:has(svg){align-items:center;display:flex;height:var(--copilot-size-md);justify-content:center;width:var(--copilot-size-md)}copilot-shortcuts-panel .kbds{margin-inline-start:auto}copilot-shortcuts-panel kbd{align-items:center;border:1px solid var(--border-color);border-radius:var(--vaadin-radius-m);box-sizing:border-box;display:inline-flex;font-family:var(--copilot-font-family);font-size:var(--copilot-font-size-xs);line-height:var(--copilot-line-height-sm);padding:0 var(--space-50)}', u = window.Vaadin.copilot.tree;
|
||||
if (!u)
|
||||
throw new Error("Tried to access copilot tree before it was initialized.");
|
||||
var x = Object.getOwnPropertyDescriptor, P = (o, i, h, r) => {
|
||||
for (var a = r > 1 ? void 0 : r ? x(i, h) : i, l = o.length - 1, c; l >= 0; l--)
|
||||
(c = o[l]) && (a = c(a) || a);
|
||||
return a;
|
||||
};
|
||||
let d = class extends m {
|
||||
constructor() {
|
||||
super(), this.onKeyPressedEvent = (o) => {
|
||||
o.detail.event.defaultPrevented || this.close();
|
||||
}, this.onTreeUpdated = () => {
|
||||
this.requestUpdate();
|
||||
};
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), n.on("copilot-tree-created", this.onTreeUpdated), n.on("escape-key-pressed", this.onKeyPressedEvent);
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(), n.off("copilot-tree-created", this.onTreeUpdated), n.off("escape-key-pressed", this.onKeyPressedEvent);
|
||||
}
|
||||
render() {
|
||||
const o = u.hasFlowComponents();
|
||||
return p`<style>
|
||||
${y}
|
||||
</style>
|
||||
<h3>Global</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span>${e.vaadin}</span>
|
||||
<span>Copilot</span>
|
||||
${t(s.toggleCopilot)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.flipBack}</span>
|
||||
<span>Undo</span>
|
||||
${t(s.undo)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.flipForward}</span>
|
||||
<span>Redo</span>
|
||||
${t(s.redo)}
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Selected component</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span>${e.terminal}</span>
|
||||
<span>Open AI popover</span>
|
||||
${t(s.openAiPopover)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.fileCodeAlt}</span>
|
||||
<span>Go to source</span>
|
||||
${t(s.goToSource)}
|
||||
</li>
|
||||
${o ? p`<li>
|
||||
<span>${e.code}</span>
|
||||
<span>Go to attach source</span>
|
||||
${t(s.goToAttachSource)}
|
||||
</li>` : v}
|
||||
<li>
|
||||
<span>${e.copy}</span>
|
||||
<span>Copy</span>
|
||||
${t(s.copy)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.clipboard}</span>
|
||||
<span>Paste</span>
|
||||
${t(s.paste)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.copyAlt}</span>
|
||||
<span>Duplicate</span>
|
||||
${t(s.duplicate)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userUp}</span>
|
||||
<span>Select parent</span>
|
||||
${t(s.selectParent)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userLeft}</span>
|
||||
<span>Select previous sibling</span>
|
||||
${t(s.selectPreviousSibling)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userRight}</span>
|
||||
<span>Select first child / next sibling</span>
|
||||
${t(s.selectNextSibling)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.delete}</span>
|
||||
<span>Delete</span>
|
||||
${t(s.delete)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.zap}</span>
|
||||
<span>Quick add from palette</span>
|
||||
${t("<kbd>A ... Z</kbd>")}
|
||||
</li>
|
||||
</ul>`;
|
||||
}
|
||||
/**
|
||||
* Closes the panel. Used from shortcuts
|
||||
*/
|
||||
close() {
|
||||
b.updatePanel("copilot-shortcuts-panel", {
|
||||
floating: !1
|
||||
});
|
||||
}
|
||||
};
|
||||
d = P([
|
||||
f("copilot-shortcuts-panel")
|
||||
], d);
|
||||
function t(o) {
|
||||
return p`<span class="kbds">${g(o)}</span>`;
|
||||
}
|
||||
const w = $({
|
||||
header: "Keyboard Shortcuts",
|
||||
tag: "copilot-shortcuts-panel",
|
||||
width: 400,
|
||||
height: 550,
|
||||
floatingPosition: {
|
||||
top: 50,
|
||||
left: 50
|
||||
}
|
||||
}), k = {
|
||||
init(o) {
|
||||
o.addPanel(w);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(k);
|
||||
102
backend/src/main/frontend/generated/jar-resources/copilot/figma-public/figma-api.d.ts
vendored
Normal file
102
backend/src/main/frontend/generated/jar-resources/copilot/figma-public/figma-api.d.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NodeType, StackAlign, StackCounterAlign, StackJustify, StackMode, StackSize } from 'fig-kiwi/fig-kiwi';
|
||||
import { ComponentDefinition } from '../shared/flow-utils';
|
||||
export type SwappedInstance = {
|
||||
name: string | undefined;
|
||||
symbolDescription: string | undefined;
|
||||
};
|
||||
export type PropertyValue = SwappedInstance | boolean | number | string;
|
||||
export type FigmaNode = {
|
||||
type: NodeType | undefined;
|
||||
name: string | undefined;
|
||||
symbolDescription: string | undefined;
|
||||
parent: FigmaNode | undefined;
|
||||
children: FigmaNode[];
|
||||
htmlTag: string;
|
||||
reactTag: string;
|
||||
vaadinComponent: boolean;
|
||||
vaadinLayout: boolean;
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
x: number | undefined;
|
||||
y: number | undefined;
|
||||
classNames: string[];
|
||||
styles: Record<string, string>;
|
||||
properties: Record<string, PropertyValue>;
|
||||
relativePosition: boolean;
|
||||
stackMode: StackMode | undefined;
|
||||
stackSpacing: number | undefined;
|
||||
stackPrimaryAlignItems: StackJustify | undefined;
|
||||
stackCounterAlignItems: StackAlign | undefined;
|
||||
stackPrimarySizing: StackSize | undefined;
|
||||
stackCounterSizing: StackSize | undefined;
|
||||
stackChildAlignSelf: StackCounterAlign | undefined;
|
||||
stackChildPrimaryGrow: number | undefined;
|
||||
stackHorizontalPadding: number | undefined;
|
||||
stackVerticalPadding: number | undefined;
|
||||
stackPadding: number | undefined;
|
||||
stackPaddingBottom: number | undefined;
|
||||
stackPaddingRight: number | undefined;
|
||||
_innerHTML: string | undefined;
|
||||
};
|
||||
export type Importer = (node: FigmaNode, metadata: ImportMetadata) => ComponentDefinition | undefined;
|
||||
export type ImportMetadata = {
|
||||
target: 'java' | 'react';
|
||||
};
|
||||
/**
|
||||
* Registers a custom importer function that can be used to convert Figma nodes into Vaadin components.
|
||||
* <p>
|
||||
* For example if you have a figma component called "AcmeCard" with a marker property `type=AcmeCard` and with two properties for customizing it: title and content,
|
||||
* you can register an importer like this:
|
||||
*
|
||||
* ```typescript
|
||||
* import type { ComponentDefinition, FigmaNode } from 'Frontend/generated/jar-resources/copilot.js';
|
||||
* import { registerImporter } from 'Frontend/generated/jar-resources/copilot.js';
|
||||
*
|
||||
* function acmeCardImporter(node: FigmaNode): ComponentDefinition | undefined {
|
||||
* if (node.properties.type === 'AcmeCard') {
|
||||
* return {
|
||||
* tag: 'AcmeCard',
|
||||
* props: {
|
||||
* cardTitle: node.properties.title,
|
||||
* cardText: node.properties.content,
|
||||
* },
|
||||
* children: [],
|
||||
* javaClass: 'my.project.components.AcmeCard',
|
||||
* reactImports: {
|
||||
* AcmeCard: 'Frontend/components/AcmeCard',
|
||||
* },
|
||||
* };
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* registerImporter(acmeCardImporter);
|
||||
* ```
|
||||
* If you only want to support either Java or React, you can omit the `javaClass` or `reactImports` property respectively.
|
||||
*
|
||||
* The above content should be placed in a file that is imported only in development mode, for example in `src/main/frontend/figma-importer.ts`.
|
||||
* In `index.tsx` you can then place
|
||||
* ```typescript
|
||||
* // @ts-ignore
|
||||
* if (import.meta.env.DEV) {
|
||||
* import('./figma-importer');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Registered importers will be used before the built in importers, so you can override the built-in importers if needed.
|
||||
*
|
||||
* This method is experimental and may change in the future.
|
||||
*
|
||||
* @param importer the importer to register
|
||||
*/
|
||||
export declare function registerImporter(importer: Importer): void;
|
||||
export declare function _registerInternalImporter(importer: Importer): void;
|
||||
export declare function _getImporters(): Importer[];
|
||||
export declare function _getIcon(node: FigmaNode, enablerKey: string, iconKey: string, slot?: string | undefined): ComponentDefinition | undefined;
|
||||
export declare function renderNodesAs(htmlTag: string, nodes: Array<FigmaNode | undefined>, metadata: ImportMetadata): ComponentDefinition[];
|
||||
export declare function renderNodeAs(htmlTag: string, node: FigmaNode, metadata: ImportMetadata, customProperties?: Record<string, string>): ComponentDefinition | undefined;
|
||||
export declare function renderNodes(childNodes: FigmaNode[], metadata: ImportMetadata): ComponentDefinition[];
|
||||
export declare function renderNode(node: FigmaNode, metadata: ImportMetadata, customProperties?: Record<string, string>): ComponentDefinition | undefined;
|
||||
export declare function findChild(node: FigmaNode, matcher: (node: FigmaNode) => boolean): FigmaNode | undefined;
|
||||
export declare function findFirstChild(node: FigmaNode, name: string): FigmaNode | undefined;
|
||||
export declare function findAllChildren(node: FigmaNode, matcher: (node: FigmaNode) => boolean): FigmaNode[];
|
||||
export declare function createChildrenDefinitions(node: FigmaNode, metadata: ImportMetadata, matcher: (n: FigmaNode) => boolean): ComponentDefinition[];
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
import { a as D } from "./copilot-BvIxHaRg.js";
|
||||
var g, b;
|
||||
function x() {
|
||||
return b || (b = 1, g = function() {
|
||||
var a = document.getSelection();
|
||||
if (!a.rangeCount)
|
||||
return function() {
|
||||
};
|
||||
for (var o = document.activeElement, s = [], i = 0; i < a.rangeCount; i++)
|
||||
s.push(a.getRangeAt(i));
|
||||
switch (o.tagName.toUpperCase()) {
|
||||
// .toUpperCase handles XHTML
|
||||
case "INPUT":
|
||||
case "TEXTAREA":
|
||||
o.blur();
|
||||
break;
|
||||
default:
|
||||
o = null;
|
||||
break;
|
||||
}
|
||||
return a.removeAllRanges(), function() {
|
||||
a.type === "Caret" && a.removeAllRanges(), a.rangeCount || s.forEach(function(d) {
|
||||
a.addRange(d);
|
||||
}), o && o.focus();
|
||||
};
|
||||
}), g;
|
||||
}
|
||||
var m, C;
|
||||
function E() {
|
||||
if (C) return m;
|
||||
C = 1;
|
||||
var a = x(), o = {
|
||||
"text/plain": "Text",
|
||||
"text/html": "Url",
|
||||
default: "Text"
|
||||
}, s = "Copy to clipboard: #{key}, Enter";
|
||||
function i(n) {
|
||||
var t = (/mac os x/i.test(navigator.userAgent) ? "⌘" : "Ctrl") + "+C";
|
||||
return n.replace(/#{\s*key\s*}/g, t);
|
||||
}
|
||||
function d(n, t) {
|
||||
var c, y, v, u, l, e, f = !1;
|
||||
t || (t = {}), c = t.debug || !1;
|
||||
try {
|
||||
v = a(), u = document.createRange(), l = document.getSelection(), e = document.createElement("span"), e.textContent = n, e.ariaHidden = "true", e.style.all = "unset", e.style.position = "fixed", e.style.top = 0, e.style.clip = "rect(0, 0, 0, 0)", e.style.whiteSpace = "pre", e.style.webkitUserSelect = "text", e.style.MozUserSelect = "text", e.style.msUserSelect = "text", e.style.userSelect = "text", e.addEventListener("copy", function(r) {
|
||||
if (r.stopPropagation(), t.format)
|
||||
if (r.preventDefault(), typeof r.clipboardData > "u") {
|
||||
c && console.warn("unable to use e.clipboardData"), c && console.warn("trying IE specific stuff"), window.clipboardData.clearData();
|
||||
var p = o[t.format] || o.default;
|
||||
window.clipboardData.setData(p, n);
|
||||
} else
|
||||
r.clipboardData.clearData(), r.clipboardData.setData(t.format, n);
|
||||
t.onCopy && (r.preventDefault(), t.onCopy(r.clipboardData));
|
||||
}), document.body.appendChild(e), u.selectNodeContents(e), l.addRange(u);
|
||||
var w = document.execCommand("copy");
|
||||
if (!w)
|
||||
throw new Error("copy command was unsuccessful");
|
||||
f = !0;
|
||||
} catch (r) {
|
||||
c && console.error("unable to copy using execCommand: ", r), c && console.warn("trying IE specific stuff");
|
||||
try {
|
||||
window.clipboardData.setData(t.format || "text", n), t.onCopy && t.onCopy(window.clipboardData), f = !0;
|
||||
} catch (p) {
|
||||
c && console.error("unable to copy using clipboardData: ", p), c && console.error("falling back to prompt"), y = i("message" in t ? t.message : s), window.prompt(y, n);
|
||||
}
|
||||
} finally {
|
||||
l && (typeof l.removeRange == "function" ? l.removeRange(u) : l.removeAllRanges()), e && document.body.removeChild(e), v();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
return m = d, m;
|
||||
}
|
||||
var h = E();
|
||||
const T = /* @__PURE__ */ D(h);
|
||||
export {
|
||||
T as c
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const c = (r, t, e) => (e.configurable = !0, e.enumerable = !0, Reflect.decorate && typeof t != "object" && Object.defineProperty(r, t, e), e);
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
function f(r, t) {
|
||||
return (e, o, l) => {
|
||||
const n = (u) => u.renderRoot?.querySelector(r) ?? null;
|
||||
return c(e, o, { get() {
|
||||
return n(this);
|
||||
} });
|
||||
};
|
||||
}
|
||||
export {
|
||||
f as e
|
||||
};
|
||||
117
backend/src/main/frontend/generated/jar-resources/copilot/shared/copilot-plugin-support.d.ts
vendored
Normal file
117
backend/src/main/frontend/generated/jar-resources/copilot/shared/copilot-plugin-support.d.ts
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
import { IObservableValue } from 'mobx';
|
||||
import { TemplateResult } from 'lit';
|
||||
/**
|
||||
* Plugin API for the dev tools window.
|
||||
*/
|
||||
export interface CopilotInterface {
|
||||
send(command: string, data: any): void;
|
||||
addPanel(panel: PanelConfiguration): void;
|
||||
}
|
||||
export interface MessageHandler {
|
||||
handleMessage(message: ServerMessage): boolean;
|
||||
}
|
||||
export interface ServerMessage {
|
||||
/**
|
||||
* The command
|
||||
*/
|
||||
command: string;
|
||||
/**
|
||||
* The data for the command
|
||||
*/
|
||||
data: any;
|
||||
}
|
||||
export type Framework = 'flow' | 'hilla-lit' | 'hilla-react';
|
||||
export interface CopilotPlugin {
|
||||
/**
|
||||
* Called once to initialize the plugin.
|
||||
*
|
||||
* @param copilotInterface provides methods to interact with the dev tools
|
||||
*/
|
||||
init(copilotInterface: CopilotInterface): void;
|
||||
}
|
||||
export declare enum MessageType {
|
||||
INFORMATION = "information",
|
||||
WARNING = "warning",
|
||||
ERROR = "error"
|
||||
}
|
||||
export interface Message {
|
||||
id: number;
|
||||
type: MessageType;
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
details?: IObservableValue<TemplateResult> | string;
|
||||
link?: string;
|
||||
persistentId?: string;
|
||||
dontShowAgain: boolean;
|
||||
deleted: boolean;
|
||||
}
|
||||
/**
|
||||
* Known Copilot panel tags used throughout the application.
|
||||
* This object serves as a registry of all panel identifiers.
|
||||
*/
|
||||
export declare const CopilotPanelTags: {
|
||||
readonly FEATURES: "copilot-features-panel";
|
||||
readonly FEEDBACK: "copilot-feedback-panel";
|
||||
readonly SHORTCUTS: "copilot-shortcuts-panel";
|
||||
readonly INFO: "copilot-info-panel";
|
||||
readonly LOG: "copilot-log-panel";
|
||||
readonly DEVELOPMENT_SETUP: "copilot-development-setup-user-guide";
|
||||
readonly A11Y_CHECKER: "copilot-a11y-checker";
|
||||
readonly BACKEND_AND_DATA: "copilot-backend-and-data-panel";
|
||||
readonly COMPONENT_PROPERTIES: "copilot-component-properties";
|
||||
readonly CONNECT_TO_SERVICE: "copilot-connect-to-service";
|
||||
readonly DOCS: "copilot-docs";
|
||||
readonly EDIT_COMPONENT: "copilot-edit-component";
|
||||
readonly I18N: "copilot-i18n-panel";
|
||||
readonly NEW_ROUTE: "copilot-new-route";
|
||||
readonly OUTLINE: "copilot-outline-panel";
|
||||
readonly PALETTE: "copilot-palette";
|
||||
readonly ROUTES: "copilot-routes-panel";
|
||||
readonly SPRING_SECURITY: "copilot-spring-security";
|
||||
readonly TEST_BENCH_TEST_GENERATOR: "copilot-test-bench-test-generator-panel";
|
||||
readonly THEME_EDITOR: "copilot-theme-editor-panel";
|
||||
readonly UI_SERVICES: "copilot-ui-services-panel";
|
||||
readonly UI_TEST_GENERATOR: "copilot-ui-test-generator-panel";
|
||||
readonly VAADIN_VERSIONS: "copilot-vaadin-versions";
|
||||
readonly TEST_LOG_PANEL: "test-log-panel";
|
||||
readonly TEST_PLUGIN_PANEL: "test-plugin-panel";
|
||||
readonly TEST_FOO_PANEL: "foo-panel";
|
||||
readonly TEST_FOO_PANEL_2: "foo-panel-2";
|
||||
readonly TEST_FOO_PANEL_BOTTOM: "foo-panel-bottom";
|
||||
readonly TEST_FOO_PANEL_LEFT: "foo-panel-left";
|
||||
readonly TEST_FOO_PANEL_RIGHT: "foo-panel-right";
|
||||
};
|
||||
/**
|
||||
* Type representing all known Copilot panel tags.
|
||||
* String literals matching these values are automatically compatible.
|
||||
*/
|
||||
export type CopilotPanelTag = (typeof CopilotPanelTags)[keyof typeof CopilotPanelTags];
|
||||
export interface PanelConfiguration {
|
||||
header: string;
|
||||
expanded: boolean;
|
||||
expandable?: boolean;
|
||||
panel?: 'bottom' | 'left' | 'right';
|
||||
panelOrder: number;
|
||||
tag: CopilotPanelTag;
|
||||
actionsTag?: string;
|
||||
floating: boolean;
|
||||
height?: number;
|
||||
width?: number;
|
||||
floatingPosition?: FloatingPosition;
|
||||
showWhileDragging?: boolean;
|
||||
helpUrl?: string;
|
||||
/**
|
||||
* These panels can be visible regardless of copilot activation status
|
||||
*/
|
||||
individual?: boolean;
|
||||
/**
|
||||
* A panel is rendered the first time when it is expanded unless eager is set to true, which causes it be always be rendered
|
||||
*/
|
||||
eager?: boolean;
|
||||
}
|
||||
export interface FloatingPosition {
|
||||
top?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
}
|
||||
41
backend/src/main/frontend/generated/jar-resources/copilot/shared/flow-utils.d.ts
vendored
Normal file
41
backend/src/main/frontend/generated/jar-resources/copilot/shared/flow-utils.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import { FiberNode, Source } from 'react-devtools-inline';
|
||||
import { CopilotTreeNode } from './copilot-tree';
|
||||
import { JavaSource } from '../show-in-ide';
|
||||
export type FlowComponentReference = {
|
||||
nodeId: number;
|
||||
uiId: number;
|
||||
};
|
||||
export type FlowComponentInfo = FlowComponentReference & {
|
||||
element: HTMLElement;
|
||||
javaClass?: string;
|
||||
hiddenByServer: boolean;
|
||||
styles: Record<string, string>;
|
||||
};
|
||||
export type ComponentDefinitionProperties = Record<string, any[] | Record<string, any> | boolean | number | string | null>;
|
||||
export type ComponentDefinition = {
|
||||
tag?: string;
|
||||
className?: string;
|
||||
props?: ComponentDefinitionProperties;
|
||||
children?: Array<ComponentDefinition | string>;
|
||||
reactImports?: Record<string, string>;
|
||||
javaClass?: string;
|
||||
metadata?: any;
|
||||
};
|
||||
export declare function isFlowComponentInfo(info: FlowComponentInfo | JavaSource | Source | undefined): info is FlowComponentInfo;
|
||||
export declare function isFlowComponent(element: HTMLElement): boolean;
|
||||
export declare function getJavaClassName(component: FlowComponentInfo): string | undefined;
|
||||
export declare function getFlowComponent(element: HTMLElement): FlowComponentInfo | undefined;
|
||||
export declare const fetchComponentDefinition: (flowComponent: FlowComponentInfo) => Promise<ComponentDefinition>;
|
||||
export declare function getUIId(): string | undefined;
|
||||
export declare function getFlowComponentId(flowComponent: FlowComponentInfo): FlowComponentReference;
|
||||
export declare function isServerRouteContainer(fiber?: FiberNode): boolean;
|
||||
export declare const isEditableComponentText: (node: CopilotTreeNode | undefined, propertyToCheck: string) => Promise<{
|
||||
canBeEdited: boolean;
|
||||
isTranslation: boolean;
|
||||
}> | {
|
||||
canBeEdited: boolean;
|
||||
isTranslation: boolean;
|
||||
};
|
||||
export declare function isServerRouteContainerElement(element: HTMLElement): boolean;
|
||||
export declare function getSimpleName(className: string): string;
|
||||
export declare function getPackageName(className: string): string;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { aw as u, ax as l } from "./copilot-BvIxHaRg.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const p = { attribute: !0, type: String, converter: l, reflect: !1, hasChanged: u }, d = (t = p, o, e) => {
|
||||
const { kind: s, metadata: i } = e;
|
||||
let r = globalThis.litPropertyMetadata.get(i);
|
||||
if (r === void 0 && globalThis.litPropertyMetadata.set(i, r = /* @__PURE__ */ new Map()), s === "setter" && ((t = Object.create(t)).wrapped = !0), r.set(e.name, t), s === "accessor") {
|
||||
const { name: a } = e;
|
||||
return { set(n) {
|
||||
const c = o.get.call(this);
|
||||
o.set.call(this, n), this.requestUpdate(a, c, t);
|
||||
}, init(n) {
|
||||
return n !== void 0 && this.C(a, void 0, t, n), n;
|
||||
} };
|
||||
}
|
||||
if (s === "setter") {
|
||||
const { name: a } = e;
|
||||
return function(n) {
|
||||
const c = this[a];
|
||||
o.call(this, n), this.requestUpdate(a, c, t);
|
||||
};
|
||||
}
|
||||
throw Error("Unsupported decorator location: " + s);
|
||||
};
|
||||
function h(t) {
|
||||
return (o, e) => typeof e == "object" ? d(t, o, e) : ((s, i, r) => {
|
||||
const a = i.hasOwnProperty(r);
|
||||
return i.constructor.createProperty(r, s), a ? Object.getOwnPropertyDescriptor(i, r) : void 0;
|
||||
})(t, o, e);
|
||||
}
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
function b(t) {
|
||||
return h({ ...t, state: !0, attribute: !1 });
|
||||
}
|
||||
export {
|
||||
h as n,
|
||||
b as r
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import dateFnsFormat from 'date-fns/format';
|
||||
import dateFnsParse from 'date-fns/parse';
|
||||
import dateFnsIsValid from 'date-fns/isValid';
|
||||
import { extractDateParts, parseDate as _parseDate } from '@vaadin/date-picker/src/vaadin-date-picker-helper.js';
|
||||
|
||||
window.Vaadin.Flow.datepickerConnector = {};
|
||||
window.Vaadin.Flow.datepickerConnector.initLazy = (datepicker) => {
|
||||
// Check whether the connector was already initialized for the datepicker
|
||||
if (datepicker.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
datepicker.$connector = {};
|
||||
|
||||
const createLocaleBasedDateFormat = function (locale) {
|
||||
try {
|
||||
// Check whether the locale is supported or not
|
||||
new Date().toLocaleDateString(locale);
|
||||
} catch (e) {
|
||||
console.warn('The locale is not supported, using default format setting (ISO 8601).');
|
||||
return 'yyyy-MM-dd';
|
||||
}
|
||||
|
||||
// format test date and convert to date-fns pattern
|
||||
const testDate = new Date(Date.UTC(1234, 4, 6));
|
||||
let pattern = testDate.toLocaleDateString(locale, { timeZone: 'UTC' });
|
||||
pattern = pattern
|
||||
// escape date-fns pattern letters by enclosing them in single quotes
|
||||
.replace(/([a-zA-Z]+)/g, "'$1'")
|
||||
// insert date placeholder
|
||||
.replace('06', 'dd')
|
||||
.replace('6', 'd')
|
||||
// insert month placeholder
|
||||
.replace('05', 'MM')
|
||||
.replace('5', 'M')
|
||||
// insert year placeholder
|
||||
.replace('1234', 'yyyy');
|
||||
const isValidPattern = pattern.includes('d') && pattern.includes('M') && pattern.includes('y');
|
||||
if (!isValidPattern) {
|
||||
console.warn('The locale is not supported, using default format setting (ISO 8601).');
|
||||
return 'yyyy-MM-dd';
|
||||
}
|
||||
|
||||
return pattern;
|
||||
};
|
||||
|
||||
function createFormatterAndParser(formats) {
|
||||
if (!formats || formats.length === 0) {
|
||||
throw new Error('Array of custom date formats is null or empty');
|
||||
}
|
||||
|
||||
function getShortYearFormat(format) {
|
||||
if (format.includes('yyyy') && !format.includes('yyyyy')) {
|
||||
return format.replace('yyyy', 'yy');
|
||||
}
|
||||
if (format.includes('YYYY') && !format.includes('YYYYY')) {
|
||||
return format.replace('YYYY', 'YY');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isFormatWithYear(format) {
|
||||
return format.includes('y') || format.includes('Y');
|
||||
}
|
||||
|
||||
function isShortYearFormat(format) {
|
||||
// Format is long if it includes a four-digit year.
|
||||
return !format.includes('yyyy') && !format.includes('YYYY');
|
||||
}
|
||||
|
||||
function getExtendedFormats(formats) {
|
||||
return formats.reduce((acc, format) => {
|
||||
// We first try to match the date with the shorter version,
|
||||
// as short years are supported with the long date format.
|
||||
if (isFormatWithYear(format) && !isShortYearFormat(format)) {
|
||||
acc.push(getShortYearFormat(format));
|
||||
}
|
||||
acc.push(format);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function correctFullYear(date) {
|
||||
// The last parsed date check handles the case where a four-digit year is parsed, then formatted
|
||||
// as a two-digit year, and then parsed again. In this case we want to keep the century of the
|
||||
// originally parsed year, instead of using the century of the reference date.
|
||||
|
||||
// Do not apply any correction if the previous parse attempt was failed.
|
||||
if (datepicker.$connector._lastParseStatus === 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update century if the last parsed date is the same except the century.
|
||||
if (datepicker.$connector._lastParseStatus === 'successful') {
|
||||
if (
|
||||
datepicker.$connector._lastParsedDate.day === date.getDate() &&
|
||||
datepicker.$connector._lastParsedDate.month === date.getMonth() &&
|
||||
datepicker.$connector._lastParsedDate.year % 100 === date.getFullYear() % 100
|
||||
) {
|
||||
date.setFullYear(datepicker.$connector._lastParsedDate.year);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update century if this is the first parse after overlay open.
|
||||
const currentValue = _parseDate(datepicker.value);
|
||||
if (
|
||||
dateFnsIsValid(currentValue) &&
|
||||
currentValue.getDate() === date.getDate() &&
|
||||
currentValue.getMonth() === date.getMonth() &&
|
||||
currentValue.getFullYear() % 100 === date.getFullYear() % 100
|
||||
) {
|
||||
date.setFullYear(currentValue.getFullYear());
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateParts) {
|
||||
const format = formats[0];
|
||||
const date = _parseDate(`${dateParts.year}-${dateParts.month + 1}-${dateParts.day}`);
|
||||
|
||||
return dateFnsFormat(date, format);
|
||||
}
|
||||
|
||||
function doParseDate(dateString, format, referenceDate) {
|
||||
// When format does not contain a year, then current year should be used.
|
||||
const refDate = isFormatWithYear(format) ? referenceDate : new Date();
|
||||
const date = dateFnsParse(dateString, format, refDate);
|
||||
if (dateFnsIsValid(date)) {
|
||||
if (isFormatWithYear(format) && isShortYearFormat(format)) {
|
||||
correctFullYear(date);
|
||||
}
|
||||
return {
|
||||
day: date.getDate(),
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseDate(dateString) {
|
||||
const referenceDate = _getReferenceDate();
|
||||
for (let format of getExtendedFormats(formats)) {
|
||||
const parsedDate = doParseDate(dateString, format, referenceDate);
|
||||
if (parsedDate) {
|
||||
datepicker.$connector._lastParseStatus = 'successful';
|
||||
datepicker.$connector._lastParsedDate = parsedDate;
|
||||
return parsedDate;
|
||||
}
|
||||
}
|
||||
datepicker.$connector._lastParseStatus = 'error';
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
formatDate: formatDate,
|
||||
parseDate: parseDate
|
||||
};
|
||||
}
|
||||
|
||||
function _getReferenceDate() {
|
||||
const { referenceDate } = datepicker.i18n;
|
||||
return referenceDate ? new Date(referenceDate.year, referenceDate.month, referenceDate.day) : new Date();
|
||||
}
|
||||
|
||||
datepicker.$connector.updateI18n = (locale, i18n) => {
|
||||
// Either use custom formats specified in I18N, or create format from locale
|
||||
const hasCustomFormats = i18n && i18n.dateFormats && i18n.dateFormats.length > 0;
|
||||
if (i18n && i18n.referenceDate) {
|
||||
i18n.referenceDate = extractDateParts(new Date(i18n.referenceDate));
|
||||
}
|
||||
const usedFormats = hasCustomFormats ? i18n.dateFormats : [createLocaleBasedDateFormat(locale)];
|
||||
const formatterAndParser = createFormatterAndParser(usedFormats);
|
||||
|
||||
// Merge current web component I18N settings with new I18N settings and the formatting and parsing functions
|
||||
datepicker.i18n = Object.assign({}, datepicker.i18n, i18n, formatterAndParser);
|
||||
};
|
||||
|
||||
datepicker.addEventListener('opened-changed', () => (datepicker.$connector._lastParseStatus = undefined));
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.composedPath().find((node) => node.hasAttribute && node.hasAttribute('disableonclick'));
|
||||
if (target) {
|
||||
target.disabled = true;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.Flow = window.Vaadin.Flow || {};
|
||||
window.Vaadin.Flow.dndConnector = {
|
||||
__ondragenterListener: function (event) {
|
||||
// TODO filter by data type
|
||||
// TODO prevent dropping on itself (by default)
|
||||
const effect = event.currentTarget['__dropEffect'];
|
||||
if (!event.currentTarget.hasAttribute('disabled')) {
|
||||
if (effect) {
|
||||
event.dataTransfer.dropEffect = effect;
|
||||
}
|
||||
|
||||
if (effect !== 'none') {
|
||||
/* #7108: if drag moves on top of drop target's children, first another ondragenter event
|
||||
* is fired and then a ondragleave event. This happens again once the drag
|
||||
* moves on top of another children, or back on top of the drop target element.
|
||||
* Thus need to "cancel" the following ondragleave, to not remove class name.
|
||||
* Drop event will happen even when dropped to a child element. */
|
||||
if (event.currentTarget.classList.contains('v-drag-over-target')) {
|
||||
event.currentTarget['__skip-leave'] = true;
|
||||
} else {
|
||||
event.currentTarget.classList.add('v-drag-over-target');
|
||||
}
|
||||
// enables browser specific pseudo classes (at least FF)
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // don't let parents know
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
__ondragoverListener: function (event) {
|
||||
// TODO filter by data type
|
||||
// TODO filter by effectAllowed != dropEffect due to Safari & IE11 ?
|
||||
if (!event.currentTarget.hasAttribute('disabled')) {
|
||||
const effect = event.currentTarget['__dropEffect'];
|
||||
if (effect) {
|
||||
event.dataTransfer.dropEffect = effect;
|
||||
}
|
||||
// allows the drop && don't let parents know
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
__ondragleaveListener: function (event) {
|
||||
if (event.currentTarget['__skip-leave']) {
|
||||
event.currentTarget['__skip-leave'] = false;
|
||||
} else {
|
||||
event.currentTarget.classList.remove('v-drag-over-target');
|
||||
}
|
||||
// #7109 need to stop or any parent drop target might not get highlighted,
|
||||
// as ondragenter for it is fired before the child gets dragleave.
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
__ondropListener: function (event) {
|
||||
const effect = event.currentTarget['__dropEffect'];
|
||||
if (effect) {
|
||||
event.dataTransfer.dropEffect = effect;
|
||||
}
|
||||
event.currentTarget.classList.remove('v-drag-over-target');
|
||||
// prevent browser handling && don't let parents know
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
updateDropTarget: function (element) {
|
||||
if (element['__active']) {
|
||||
element.addEventListener('dragenter', this.__ondragenterListener, false);
|
||||
element.addEventListener('dragover', this.__ondragoverListener, false);
|
||||
element.addEventListener('dragleave', this.__ondragleaveListener, false);
|
||||
element.addEventListener('drop', this.__ondropListener, false);
|
||||
} else {
|
||||
element.removeEventListener('dragenter', this.__ondragenterListener, false);
|
||||
element.removeEventListener('dragover', this.__ondragoverListener, false);
|
||||
element.removeEventListener('dragleave', this.__ondragleaveListener, false);
|
||||
element.removeEventListener('drop', this.__ondropListener, false);
|
||||
element.classList.remove('v-drag-over-target');
|
||||
}
|
||||
},
|
||||
|
||||
/** DRAG SOURCE METHODS: */
|
||||
|
||||
__dragstartListener: function (event) {
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.setData('text/plain', '');
|
||||
if (event.currentTarget.hasAttribute('disabled')) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
if (event.currentTarget['__effectAllowed']) {
|
||||
event.dataTransfer.effectAllowed = event.currentTarget['__effectAllowed'];
|
||||
}
|
||||
event.currentTarget.classList.add('v-dragged');
|
||||
}
|
||||
if (event.currentTarget.__dragImage) {
|
||||
if (event.currentTarget.__dragImage.style.display === 'none') {
|
||||
event.currentTarget.__dragImage.style.display = 'block';
|
||||
event.currentTarget.classList.add('shown');
|
||||
}
|
||||
event.dataTransfer.setDragImage(
|
||||
event.currentTarget.__dragImage,
|
||||
event.currentTarget.__dragImageOffsetX,
|
||||
event.currentTarget.__dragImageOffsetY
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
__dragendListener: function (event) {
|
||||
event.currentTarget.classList.remove('v-dragged');
|
||||
if (event.currentTarget.classList.contains('shown')) {
|
||||
event.currentTarget.classList.remove('shown');
|
||||
event.currentTarget.__dragImage.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
updateDragSource: function (element) {
|
||||
if (element['draggable']) {
|
||||
element.addEventListener('dragstart', this.__dragstartListener, false);
|
||||
element.addEventListener('dragend', this.__dragendListener, false);
|
||||
} else {
|
||||
element.removeEventListener('dragstart', this.__dragstartListener, false);
|
||||
element.removeEventListener('dragend', this.__dragendListener, false);
|
||||
}
|
||||
},
|
||||
|
||||
setDragImage: function (dragImage, offsetX, offsetY, dragSource) {
|
||||
dragSource.__dragImage = dragImage;
|
||||
dragSource.__dragImageOffsetX = offsetX;
|
||||
dragSource.__dragImageOffsetY = offsetY;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { noChange } from 'lit';
|
||||
import { directive, PartType } from 'lit/directive.js';
|
||||
import { AsyncDirective } from 'lit/async-directive.js';
|
||||
|
||||
class FlowComponentDirective extends AsyncDirective {
|
||||
constructor(partInfo) {
|
||||
super(partInfo);
|
||||
if (partInfo.type !== PartType.CHILD) {
|
||||
throw new Error(`${this.constructor.directiveName}() can only be used in child bindings`);
|
||||
}
|
||||
}
|
||||
|
||||
update(part, [appid, nodeid]) {
|
||||
this.updateContent(part, appid, nodeid);
|
||||
return noChange;
|
||||
}
|
||||
|
||||
updateContent(part, appid, nodeid) {
|
||||
const { parentNode, startNode } = part;
|
||||
this.__parentNode = parentNode;
|
||||
|
||||
const hasNewNodeId = nodeid !== undefined && nodeid !== null;
|
||||
const newNode = hasNewNodeId ? this.getNewNode(appid, nodeid) : null;
|
||||
const oldNode = this.getOldNode(part);
|
||||
|
||||
clearTimeout(this.__parentNode.__nodeRetryTimeout);
|
||||
|
||||
if (hasNewNodeId && !newNode) {
|
||||
// If the node is not found, try again later.
|
||||
this.__parentNode.__nodeRetryTimeout = setTimeout(() => this.updateContent(part, appid, nodeid));
|
||||
} else if (oldNode === newNode) {
|
||||
return;
|
||||
} else if (oldNode && newNode) {
|
||||
parentNode.replaceChild(newNode, oldNode);
|
||||
} else if (oldNode) {
|
||||
parentNode.removeChild(oldNode);
|
||||
} else if (newNode) {
|
||||
startNode.after(newNode);
|
||||
}
|
||||
}
|
||||
|
||||
getNewNode(appid, nodeid) {
|
||||
return window.Vaadin.Flow.clients[appid].getByNodeId(nodeid);
|
||||
}
|
||||
|
||||
getOldNode(part) {
|
||||
const { startNode, endNode } = part;
|
||||
if (startNode.nextSibling === endNode) {
|
||||
return;
|
||||
}
|
||||
return startNode.nextSibling;
|
||||
}
|
||||
|
||||
disconnected() {
|
||||
clearTimeout(this.__parentNode.__nodeRetryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given flow component node.
|
||||
*
|
||||
* WARNING: This directive is not intended for public use.
|
||||
*
|
||||
* @param {string} appid
|
||||
* @param {number} nodeid
|
||||
* @private
|
||||
*/
|
||||
export const flowComponentDirective = directive(FlowComponentDirective);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { flowComponentDirective } from './flow-component-directive.js';
|
||||
import { render, html as litHtml } from 'lit';
|
||||
|
||||
/**
|
||||
* Returns the requested node in a form suitable for Lit template interpolation.
|
||||
* @param {string} appid
|
||||
* @param {number} nodeid
|
||||
* @returns {any} a Lit directive
|
||||
*/
|
||||
function getNode(appid, nodeid) {
|
||||
return flowComponentDirective(appid, nodeid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the nodes defined by the given node ids as the child nodes of the
|
||||
* given root element.
|
||||
* @param {string} appid
|
||||
* @param {number[]} nodeIds
|
||||
* @param {Element} root
|
||||
*/
|
||||
function setChildNodes(appid, nodeIds, root) {
|
||||
render(litHtml`${nodeIds.map((id) => flowComponentDirective(appid, id))}`, root);
|
||||
}
|
||||
|
||||
/**
|
||||
* SimpleElementBindingStrategy::addChildren uses insertBefore to add child
|
||||
* elements to the container. When the children are manually placed under
|
||||
* another element, the call to insertBefore can occasionally fail due to
|
||||
* an invalid reference node.
|
||||
*
|
||||
* This is a temporary workaround which patches the container's native API
|
||||
* to not fail when called with invalid arguments.
|
||||
*/
|
||||
function patchVirtualContainer(container) {
|
||||
const originalInsertBefore = container.insertBefore;
|
||||
|
||||
container.insertBefore = function (newNode, referenceNode) {
|
||||
if (referenceNode && referenceNode.parentNode === this) {
|
||||
return originalInsertBefore.call(this, newNode, referenceNode);
|
||||
} else {
|
||||
return originalInsertBefore.call(this, newNode, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.Vaadin ||= {};
|
||||
window.Vaadin.FlowComponentHost ||= { patchVirtualContainer, getNode, setChildNodes };
|
||||
@@ -0,0 +1,776 @@
|
||||
// @ts-nocheck
|
||||
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
||||
import { timeOut, animationFrame } from '@vaadin/component-base/src/async.js';
|
||||
import { Grid } from '@vaadin/grid/src/vaadin-grid.js';
|
||||
import { isFocusable } from '@vaadin/grid/src/vaadin-grid-active-item-mixin.js';
|
||||
import { GridFlowSelectionColumn } from './vaadin-grid-flow-selection-column.js';
|
||||
|
||||
window.Vaadin.Flow.gridConnector = {};
|
||||
window.Vaadin.Flow.gridConnector.initLazy = (grid) => {
|
||||
// Check whether the connector was already initialized for the grid
|
||||
if (grid.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataProviderController = grid._dataProviderController;
|
||||
|
||||
let cache = {};
|
||||
|
||||
const rootRequestDelay = 150;
|
||||
let rootRequestDebouncer;
|
||||
|
||||
let lastRequestedRange = [0, 0];
|
||||
|
||||
const validSelectionModes = ['SINGLE', 'NONE', 'MULTI'];
|
||||
let selectedKeys = {};
|
||||
let selectionMode = 'SINGLE';
|
||||
|
||||
let sorterDirectionsSetFromServer = false;
|
||||
|
||||
grid.size = 0; // To avoid NaN here and there before we get proper data
|
||||
grid.itemIdPath = 'key';
|
||||
|
||||
grid.$connector = {};
|
||||
|
||||
grid.$connector.hasRootRequestQueue = () => {
|
||||
const { pendingRequests } = dataProviderController.rootCache;
|
||||
return Object.keys(pendingRequests).length > 0 || !!rootRequestDebouncer?.isActive();
|
||||
};
|
||||
|
||||
grid.$connector.doSelection = function (items, userOriginated) {
|
||||
if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) {
|
||||
return;
|
||||
}
|
||||
if (selectionMode === 'SINGLE') {
|
||||
selectedKeys = {};
|
||||
}
|
||||
|
||||
let selectedItemsChanged = false;
|
||||
items.forEach((item) => {
|
||||
const selectable = !userOriginated || grid.isItemSelectable(item);
|
||||
selectedItemsChanged = selectedItemsChanged || selectable;
|
||||
if (item && selectable) {
|
||||
selectedKeys[item.key] = item;
|
||||
item.selected = true;
|
||||
if (userOriginated) {
|
||||
grid.$server.select(item.key);
|
||||
}
|
||||
}
|
||||
|
||||
// FYI: In single selection mode, the server can send items = [null]
|
||||
// which means a "Deselect All" command.
|
||||
const isSelectedItemDifferentOrNull = !grid.activeItem || !item || item.key != grid.activeItem.key;
|
||||
if (!userOriginated && selectionMode === 'SINGLE' && isSelectedItemDifferentOrNull) {
|
||||
grid.activeItem = item;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedItemsChanged) {
|
||||
grid.selectedItems = Object.values(selectedKeys);
|
||||
}
|
||||
};
|
||||
|
||||
grid.$connector.doDeselection = function (items, userOriginated) {
|
||||
if (selectionMode === 'NONE' || !items.length || (userOriginated && grid.hasAttribute('disabled'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedSelectedItems = grid.selectedItems.slice();
|
||||
while (items.length) {
|
||||
const itemToDeselect = items.shift();
|
||||
const selectable = !userOriginated || grid.isItemSelectable(itemToDeselect);
|
||||
if (!selectable) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < updatedSelectedItems.length; i++) {
|
||||
const selectedItem = updatedSelectedItems[i];
|
||||
if (itemToDeselect?.key === selectedItem.key) {
|
||||
updatedSelectedItems.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (itemToDeselect) {
|
||||
delete selectedKeys[itemToDeselect.key];
|
||||
delete itemToDeselect.selected;
|
||||
if (userOriginated) {
|
||||
grid.$server.deselect(itemToDeselect.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
grid.selectedItems = updatedSelectedItems;
|
||||
};
|
||||
|
||||
grid.__activeItemChanged = function (newVal, oldVal) {
|
||||
if (selectionMode != 'SINGLE') {
|
||||
return;
|
||||
}
|
||||
if (!newVal) {
|
||||
if (oldVal && selectedKeys[oldVal.key]) {
|
||||
if (grid.__deselectDisallowed) {
|
||||
grid.activeItem = oldVal;
|
||||
} else {
|
||||
// The item instance may have changed since the item was stored as active item
|
||||
// and information such as whether the item may be selected or deselected may
|
||||
// be stale. Use data provider controller to get updated instance from grid
|
||||
// cache.
|
||||
oldVal = dataProviderController.getItemContext(oldVal).item;
|
||||
grid.$connector.doDeselection([oldVal], true);
|
||||
}
|
||||
}
|
||||
} else if (!selectedKeys[newVal.key]) {
|
||||
grid.$connector.doSelection([newVal], true);
|
||||
}
|
||||
};
|
||||
grid._createPropertyObserver('activeItem', '__activeItemChanged', true);
|
||||
|
||||
grid.__activeItemChangedDetails = function (newVal, oldVal) {
|
||||
if (grid.__disallowDetailsOnClick) {
|
||||
return;
|
||||
}
|
||||
// when grid is attached, newVal is not set and oldVal is undefined
|
||||
// do nothing
|
||||
if (newVal == null && oldVal === undefined) {
|
||||
return;
|
||||
}
|
||||
if (newVal && !newVal.detailsOpened) {
|
||||
grid.$server.setDetailsVisible(newVal.key);
|
||||
} else {
|
||||
grid.$server.setDetailsVisible(null);
|
||||
}
|
||||
};
|
||||
grid._createPropertyObserver('activeItem', '__activeItemChangedDetails', true);
|
||||
|
||||
grid.$connector.debounceRootRequest = function (page) {
|
||||
const delay = grid._hasData ? rootRequestDelay : 0;
|
||||
|
||||
rootRequestDebouncer = Debouncer.debounce(rootRequestDebouncer, timeOut.after(delay), () => {
|
||||
grid.$connector.fetchPage((firstIndex, size) => grid.$server.setViewportRange(firstIndex, size), page);
|
||||
});
|
||||
};
|
||||
|
||||
grid.$connector.fetchPage = function (fetch, page) {
|
||||
// Adjust the requested page to be within the valid range in case
|
||||
// the grid size has changed while fetchPage was debounced.
|
||||
page = Math.min(page, Math.floor((grid.size - 1) / grid.pageSize));
|
||||
|
||||
// Determine what to fetch based on scroll position and not only
|
||||
// what grid asked for
|
||||
const visibleRows = grid._getRenderedRows();
|
||||
let start = visibleRows.length > 0 ? visibleRows[0].index : 0;
|
||||
let end = visibleRows.length > 0 ? visibleRows[visibleRows.length - 1].index : 0;
|
||||
|
||||
// The buffer size could be multiplied by some constant defined by the user,
|
||||
// if he needs to reduce the number of items sent to the Grid to improve performance
|
||||
// or to increase it to make Grid smoother when scrolling
|
||||
let buffer = end - start;
|
||||
start = Math.max(0, start - buffer);
|
||||
end = Math.min(end + buffer, grid.size);
|
||||
|
||||
let pageRange = [Math.floor(start / grid.pageSize), Math.floor(end / grid.pageSize)];
|
||||
|
||||
// When the viewport doesn't contain the requested page or it doesn't contain any items from
|
||||
// the requested level at all, it means that the scroll position has changed while fetchPage
|
||||
// was debounced. For example, it can happen if the user scrolls the grid to the bottom and
|
||||
// then immediately back to the top. In this case, the request for the last page will be left
|
||||
// hanging. To avoid this, as a workaround, we reset the range to only include the requested page
|
||||
// to make sure all hanging requests are resolved. After that, the grid requests the first page
|
||||
// or whatever in the viewport again.
|
||||
if (page < pageRange[0] || page > pageRange[1]) {
|
||||
pageRange = [page, page];
|
||||
}
|
||||
|
||||
if (lastRequestedRange[0] != pageRange[0] || lastRequestedRange[1] != pageRange[1]) {
|
||||
lastRequestedRange = pageRange;
|
||||
let pageCount = pageRange[1] - pageRange[0] + 1;
|
||||
fetch(pageRange[0] * grid.pageSize, pageCount * grid.pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
grid.dataProvider = function (params, callback) {
|
||||
if (params.pageSize != grid.pageSize) {
|
||||
throw 'Invalid pageSize';
|
||||
}
|
||||
|
||||
let page = params.page;
|
||||
|
||||
// size is controlled by the server (data communicator), so if the
|
||||
// size is zero, we know that there is no data to fetch.
|
||||
// This also prevents an empty grid getting stuck in a loading state.
|
||||
// The connector does not cache empty pages, so if the grid requests
|
||||
// data again, there would be no cache entry, causing a request to
|
||||
// the server. However, the data communicator will never respond,
|
||||
// as it assumes that the data is already cached.
|
||||
if (grid.size === 0) {
|
||||
callback([], 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache[page]) {
|
||||
callback(cache[page]);
|
||||
} else {
|
||||
grid.$connector.debounceRootRequest(page);
|
||||
}
|
||||
};
|
||||
|
||||
grid.$connector.setSorterDirections = function (directions) {
|
||||
sorterDirectionsSetFromServer = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const sorters = Array.from(grid.querySelectorAll('vaadin-grid-sorter'));
|
||||
|
||||
// Sorters for hidden columns are removed from DOM but stored in the web component.
|
||||
// We need to ensure that all the sorters are reset when using `grid.sort(null)`.
|
||||
grid._sorters.forEach((sorter) => {
|
||||
if (!sorters.includes(sorter)) {
|
||||
sorters.push(sorter);
|
||||
}
|
||||
});
|
||||
|
||||
sorters.forEach((sorter) => {
|
||||
sorter.direction = null;
|
||||
});
|
||||
|
||||
// Apply directions in correct order, depending on configured multi-sort priority.
|
||||
// For the default "prepend" mode, directions need to be applied in reverse, in
|
||||
// order for the sort indicators to match the order on the server. For "append"
|
||||
// just keep the order passed from the server.
|
||||
if (grid.multiSortPriority !== 'append') {
|
||||
directions = directions.reverse();
|
||||
}
|
||||
directions.forEach(({ column, direction }) => {
|
||||
sorters.forEach((sorter) => {
|
||||
if (sorter.getAttribute('path') === column) {
|
||||
sorter.direction = direction;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Manually trigger a re-render of the sorter priority indicators
|
||||
// in case some of the sorters were hidden while being updated above
|
||||
// and therefore didn't notify the grid about their direction change.
|
||||
grid.__applySorters();
|
||||
} finally {
|
||||
sorterDirectionsSetFromServer = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let preventUpdateVisibleRowsActive = 0;
|
||||
|
||||
function preventUpdateVisibleRows(callback) {
|
||||
try {
|
||||
preventUpdateVisibleRowsActive++;
|
||||
callback();
|
||||
} finally {
|
||||
preventUpdateVisibleRowsActive--;
|
||||
}
|
||||
}
|
||||
|
||||
grid.__updateVisibleRows = function (...args) {
|
||||
if (preventUpdateVisibleRowsActive === 0) {
|
||||
Object.getPrototypeOf(this).__updateVisibleRows.call(this, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
grid.__updateRow = function (row, ...args) {
|
||||
Object.getPrototypeOf(this).__updateRow.call(this, row, ...args);
|
||||
|
||||
// since no row can be selected when selection mode is NONE
|
||||
// if selectionMode is set to NONE, remove aria-selected attribute from the row
|
||||
if (selectionMode === validSelectionModes[1]) {
|
||||
// selectionMode === NONE
|
||||
row.removeAttribute('aria-selected');
|
||||
Array.from(row.children).forEach((cell) => cell.removeAttribute('aria-selected'));
|
||||
}
|
||||
};
|
||||
|
||||
const itemsUpdated = function (items) {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
throw 'Attempted to call itemsUpdated with an invalid value: ' + JSON.stringify(items);
|
||||
}
|
||||
let detailsOpenedItems = Array.from(grid.detailsOpenedItems);
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
const item = items[i];
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
if (item.detailsOpened) {
|
||||
if (grid._getItemIndexInArray(item, detailsOpenedItems) < 0) {
|
||||
detailsOpenedItems.push(item);
|
||||
}
|
||||
} else if (grid._getItemIndexInArray(item, detailsOpenedItems) >= 0) {
|
||||
detailsOpenedItems.splice(grid._getItemIndexInArray(item, detailsOpenedItems), 1);
|
||||
}
|
||||
}
|
||||
grid.detailsOpenedItems = detailsOpenedItems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the cache for the given page for grid or tree-grid.
|
||||
*
|
||||
* @param page index of the page to update
|
||||
*/
|
||||
const updateGridCache = function (page) {
|
||||
const { rootCache } = dataProviderController;
|
||||
|
||||
// Force update unless there's a callback waiting.
|
||||
if (cache[page] && rootCache.pendingRequests[page]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < grid.pageSize; i++) {
|
||||
const index = page * grid.pageSize + i;
|
||||
const item = cache[page]?.[i];
|
||||
rootCache.items[index] = item;
|
||||
}
|
||||
};
|
||||
|
||||
grid.$connector.set = function (startIndex, items) {
|
||||
items.forEach((item, i) => {
|
||||
const index = startIndex + i;
|
||||
const page = Math.floor(index / grid.pageSize);
|
||||
cache[page] ??= [];
|
||||
cache[page][index % grid.pageSize] = item;
|
||||
});
|
||||
|
||||
const firstPage = Math.floor(startIndex / grid.pageSize);
|
||||
const updatedPageCount = Math.ceil(items.length / grid.pageSize);
|
||||
for (let i = 0; i < updatedPageCount; i++) {
|
||||
updateGridCache(firstPage + i);
|
||||
}
|
||||
|
||||
preventUpdateVisibleRows(() => {
|
||||
grid.$connector.doSelection(items.filter((item) => item.selected));
|
||||
grid.$connector.doDeselection(items.filter((item) => !item.selected && selectedKeys[item.key]));
|
||||
itemsUpdated(items);
|
||||
});
|
||||
|
||||
grid.__updateVisibleRows(startIndex, startIndex + items.length - 1);
|
||||
};
|
||||
|
||||
const itemToCacheLocation = function (item) {
|
||||
for (let page in cache) {
|
||||
for (let index in cache[page]) {
|
||||
if (grid.getItemId(cache[page][index]) === grid.getItemId(item)) {
|
||||
return { page: page, index: index };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the given items for a non-hierarchical grid.
|
||||
*
|
||||
* @param updatedItems the updated items array
|
||||
*/
|
||||
grid.$connector.updateFlatData = function (updatedItems) {
|
||||
const updatedIndexes = [];
|
||||
|
||||
// update (flat) caches
|
||||
for (let i = 0; i < updatedItems.length; i++) {
|
||||
let cacheLocation = itemToCacheLocation(updatedItems[i]);
|
||||
if (cacheLocation) {
|
||||
// update connector cache
|
||||
cache[cacheLocation.page][cacheLocation.index] = updatedItems[i];
|
||||
|
||||
// update grid's cache
|
||||
const index = parseInt(cacheLocation.page) * grid.pageSize + parseInt(cacheLocation.index);
|
||||
const { rootCache } = dataProviderController;
|
||||
if (rootCache.items[index]) {
|
||||
rootCache.items[index] = updatedItems[i];
|
||||
}
|
||||
updatedIndexes.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
preventUpdateVisibleRows(() => {
|
||||
itemsUpdated(updatedItems);
|
||||
});
|
||||
|
||||
updatedIndexes.forEach((index) => grid.__updateVisibleRows(index, index));
|
||||
};
|
||||
|
||||
grid.$connector.clear = function (index, length) {
|
||||
if (!cache || Object.keys(cache).length === 0) {
|
||||
return;
|
||||
}
|
||||
if (index % grid.pageSize != 0) {
|
||||
throw 'Got cleared data for index ' + index + ' which is not aligned with the page size of ' + grid.pageSize;
|
||||
}
|
||||
|
||||
let firstPage = Math.floor(index / grid.pageSize);
|
||||
let updatedPageCount = Math.ceil(length / grid.pageSize);
|
||||
|
||||
for (let i = 0; i < updatedPageCount; i++) {
|
||||
let page = firstPage + i;
|
||||
let items = cache[page];
|
||||
if (items) {
|
||||
preventUpdateVisibleRows(() => {
|
||||
grid.$connector.doDeselection(items.filter((item) => selectedKeys[item.key]));
|
||||
items.forEach((item) => grid.closeItemDetails(item));
|
||||
});
|
||||
delete cache[page];
|
||||
updateGridCache(page);
|
||||
}
|
||||
}
|
||||
|
||||
grid.__updateVisibleRows(index, index + length - 1);
|
||||
};
|
||||
|
||||
grid.$connector.reset = function () {
|
||||
cache = {};
|
||||
dataProviderController.clearCache();
|
||||
lastRequestedRange = [-1, -1];
|
||||
rootRequestDebouncer?.cancel();
|
||||
grid.__updateVisibleRows();
|
||||
};
|
||||
|
||||
grid.$connector.updateSize = (newSize) => (grid.size = newSize);
|
||||
|
||||
grid.$connector.updateUniqueItemIdPath = (path) => (grid.itemIdPath = path);
|
||||
|
||||
grid.$connector.confirm = function (id) {
|
||||
// We're done applying changes from this batch, resolve pending
|
||||
// callbacks
|
||||
const { pendingRequests } = dataProviderController.rootCache;
|
||||
Object.entries(pendingRequests).forEach(([page, callback]) => {
|
||||
const lastAvailablePage = grid.size ? Math.ceil(grid.size / grid.pageSize) - 1 : 0;
|
||||
// It's possible that the lastRequestedRange includes a page that's beyond lastAvailablePage if the grid's size got reduced during an ongoing data request
|
||||
const lastRequestedRangeEnd = Math.min(lastRequestedRange[1], lastAvailablePage);
|
||||
// Resolve if we have data or if we don't expect to get data
|
||||
if (cache[page]) {
|
||||
// Cached data is available, resolve the callback
|
||||
callback(cache[page]);
|
||||
} else if (page < lastRequestedRange[0] || +page > lastRequestedRangeEnd) {
|
||||
// No cached data, resolve the callback with an empty array
|
||||
callback(new Array(grid.pageSize));
|
||||
// Request grid for content update
|
||||
grid.requestContentUpdate();
|
||||
} else if (callback && grid.size === 0) {
|
||||
// The grid has 0 items => resolve the callback with an empty array
|
||||
callback([]);
|
||||
}
|
||||
});
|
||||
|
||||
// If all pending requests have already been resolved (which can happen
|
||||
// for example if the server sent preloaded data while the grid had
|
||||
// already made its own requests), cancel the request debouncer to
|
||||
// prevent further unnecessary calls.
|
||||
if (Object.keys(pendingRequests).length === 0) {
|
||||
rootRequestDebouncer?.cancel();
|
||||
lastRequestedRange = [-1, -1];
|
||||
}
|
||||
|
||||
// Let server know we're done
|
||||
grid.$server.confirmUpdate(id);
|
||||
};
|
||||
|
||||
grid.$connector.setSelectionMode = function (mode) {
|
||||
if ((typeof mode === 'string' || mode instanceof String) && validSelectionModes.indexOf(mode) >= 0) {
|
||||
selectionMode = mode;
|
||||
selectedKeys = {};
|
||||
grid.selectedItems = [];
|
||||
grid.$connector.updateMultiSelectable();
|
||||
} else {
|
||||
throw 'Attempted to set an invalid selection mode';
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Manage aria-multiselectable attribute depending on the selection mode.
|
||||
* see more: https://github.com/vaadin/web-components/issues/1536
|
||||
* or: https://www.w3.org/TR/wai-aria-1.1/#aria-multiselectable
|
||||
* For selection mode SINGLE, set the aria-multiselectable attribute to false
|
||||
*/
|
||||
grid.$connector.updateMultiSelectable = function () {
|
||||
if (!grid.$) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionMode === validSelectionModes[0]) {
|
||||
grid.$.table.setAttribute('aria-multiselectable', false);
|
||||
// For selection mode NONE, remove the aria-multiselectable attribute
|
||||
} else if (selectionMode === validSelectionModes[1]) {
|
||||
grid.$.table.removeAttribute('aria-multiselectable');
|
||||
// For selection mode MULTI, set aria-multiselectable to true
|
||||
} else {
|
||||
grid.$.table.setAttribute('aria-multiselectable', true);
|
||||
}
|
||||
};
|
||||
|
||||
// Have the multi-selectable state updated on attach
|
||||
grid._createPropertyObserver('isAttached', () => grid.$connector.updateMultiSelectable());
|
||||
|
||||
const singleTimeRenderer = (renderer) => {
|
||||
return (root) => {
|
||||
if (renderer) {
|
||||
renderer(root);
|
||||
renderer = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
grid.$connector.setHeaderRenderer = function (column, options) {
|
||||
const { content, showSorter, sorterPath } = options;
|
||||
|
||||
if (content === null) {
|
||||
column.headerRenderer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
column.headerRenderer = singleTimeRenderer((root) => {
|
||||
// Clear previous contents
|
||||
root.innerHTML = '';
|
||||
// Render sorter
|
||||
let contentRoot = root;
|
||||
if (showSorter) {
|
||||
const sorter = document.createElement('vaadin-grid-sorter');
|
||||
sorter.setAttribute('path', sorterPath);
|
||||
const ariaLabel = content instanceof Node ? content.textContent : content;
|
||||
if (ariaLabel) {
|
||||
sorter.setAttribute('aria-label', `Sort by ${ariaLabel}`);
|
||||
}
|
||||
root.appendChild(sorter);
|
||||
|
||||
// Use sorter as content root
|
||||
contentRoot = sorter;
|
||||
}
|
||||
// Add content
|
||||
if (content instanceof Node) {
|
||||
contentRoot.appendChild(content);
|
||||
} else {
|
||||
contentRoot.textContent = content;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// This method is overridden to prevent the grid web component from
|
||||
// automatically excluding columns from sorting when they get hidden.
|
||||
// In Flow, it's the developer's responsibility to remove the column
|
||||
// from the backend sort order when the column gets hidden.
|
||||
grid._getActiveSorters = function () {
|
||||
return this._sorters.filter((sorter) => sorter.direction);
|
||||
};
|
||||
|
||||
grid.__applySorters = function (...args) {
|
||||
const sorters = grid._mapSorters();
|
||||
const sortersChanged = JSON.stringify(grid._previousSorters) !== JSON.stringify(sorters);
|
||||
|
||||
// Update the _previousSorters in vaadin-grid-sort-mixin so that the __applySorters
|
||||
// method in the mixin will skip calling clearCache().
|
||||
//
|
||||
// In Flow Grid's case, we never want to clear the cache eagerly when the sorter elements
|
||||
// change due to one of the following reasons:
|
||||
//
|
||||
// 1. Sorted by user: The items in the new sort order need to be fetched from the server,
|
||||
// and we want to avoid a heavy re-render before the updated items have actually been fetched.
|
||||
//
|
||||
// 2. Sorted programmatically on the server: The items in the new sort order have already
|
||||
// been fetched and applied to the grid. The sorter element states are updated programmatically
|
||||
// to reflect the new sort order, but there's no need to re-render the grid rows.
|
||||
grid._previousSorters = sorters;
|
||||
|
||||
// Call the original __applySorters method in vaadin-grid-sort-mixin
|
||||
Object.getPrototypeOf(this).__applySorters.call(this, ...args);
|
||||
|
||||
if (sortersChanged && !sorterDirectionsSetFromServer) {
|
||||
grid.$server.sortersChanged(sorters);
|
||||
}
|
||||
};
|
||||
|
||||
grid.$connector.setFooterRenderer = function (column, options) {
|
||||
const { content } = options;
|
||||
|
||||
if (content === null) {
|
||||
column.footerRenderer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
column.footerRenderer = singleTimeRenderer((root) => {
|
||||
// Clear previous contents
|
||||
root.innerHTML = '';
|
||||
// Add content
|
||||
if (content instanceof Node) {
|
||||
root.appendChild(content);
|
||||
} else {
|
||||
root.textContent = content;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
grid.addEventListener('vaadin-context-menu-before-open', function (e) {
|
||||
const { key, columnId } = e.detail;
|
||||
grid.$server.updateContextMenuTargetItem(key, columnId);
|
||||
});
|
||||
|
||||
grid.getContextMenuBeforeOpenDetail = function (event) {
|
||||
// For `contextmenu` events, we need to access the source event,
|
||||
// when using open on click we just use the click event itself
|
||||
const sourceEvent = event.detail.sourceEvent || event;
|
||||
const eventContext = grid.getEventContext(sourceEvent);
|
||||
const key = eventContext.item?.key || '';
|
||||
const columnId = eventContext.column?.id || '';
|
||||
return { key, columnId };
|
||||
};
|
||||
|
||||
grid.preventContextMenu = function (event) {
|
||||
const isLeftClick = event.type === 'click';
|
||||
const { column } = grid.getEventContext(event);
|
||||
|
||||
return isLeftClick && column instanceof GridFlowSelectionColumn;
|
||||
};
|
||||
|
||||
grid.addEventListener('click', (e) => _fireClickEvent(e, 'item-click'));
|
||||
grid.addEventListener('dblclick', (e) => _fireClickEvent(e, 'item-double-click'));
|
||||
|
||||
grid.addEventListener('column-resize', (e) => {
|
||||
const cols = grid._getColumnsInOrder().filter((col) => !col.hidden);
|
||||
|
||||
cols.forEach((col) => {
|
||||
col.dispatchEvent(new CustomEvent('column-drag-resize'));
|
||||
});
|
||||
|
||||
grid.dispatchEvent(
|
||||
new CustomEvent('column-drag-resize', {
|
||||
detail: {
|
||||
resizedColumnKey: e.detail.resizedColumn._flowId
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
grid.addEventListener('column-reorder', (e) => {
|
||||
const columns = grid._columnTree
|
||||
.slice(0)
|
||||
.pop()
|
||||
.filter((c) => c._flowId)
|
||||
.sort((b, a) => b._order - a._order)
|
||||
.map((c) => c._flowId);
|
||||
|
||||
grid.dispatchEvent(
|
||||
new CustomEvent('column-reorder-all-columns', {
|
||||
detail: { columns }
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
grid.addEventListener('cell-focus', (e) => {
|
||||
const eventContext = grid.getEventContext(e);
|
||||
const expectedSectionValues = ['header', 'body', 'footer'];
|
||||
|
||||
if (expectedSectionValues.indexOf(eventContext.section) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.dispatchEvent(
|
||||
new CustomEvent('grid-cell-focus', {
|
||||
detail: {
|
||||
itemKey: eventContext.item ? eventContext.item.key : null,
|
||||
|
||||
internalColumnId: eventContext.column ? eventContext.column._flowId : null,
|
||||
|
||||
section: eventContext.section
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
function _fireClickEvent(event, eventName) {
|
||||
// Click event was handled by the component inside grid, do nothing.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = event.composedPath();
|
||||
const idx = path.findIndex((node) => node.localName === 'td' || node.localName === 'th');
|
||||
const cell = path[idx];
|
||||
const content = path.slice(0, idx);
|
||||
|
||||
// Do not fire item click event if cell content contains focusable elements.
|
||||
// Use this instead of event.target to detect cases like icon inside button.
|
||||
// See https://github.com/vaadin/flow-components/issues/4065
|
||||
if (
|
||||
content.some((node) => {
|
||||
// Ignore focus buttons that the component renders into cells in focus button mode on MacOS
|
||||
const focusable = cell?._focusButton !== node && isFocusable(node);
|
||||
return focusable || node instanceof HTMLLabelElement;
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventContext = grid.getEventContext(event);
|
||||
const section = eventContext.section;
|
||||
|
||||
if (eventContext.item && section !== 'details') {
|
||||
event.itemKey = eventContext.item.key;
|
||||
// if you have a details-renderer, getEventContext().column is undefined
|
||||
if (eventContext.column) {
|
||||
event.internalColumnId = eventContext.column._flowId;
|
||||
}
|
||||
grid.dispatchEvent(new CustomEvent(eventName, { detail: event }));
|
||||
}
|
||||
}
|
||||
|
||||
grid.cellPartNameGenerator = function (column, rowData) {
|
||||
const part = rowData.item.part;
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
return (part.row || '') + ' ' + ((column && part[column._flowId]) || '');
|
||||
};
|
||||
|
||||
grid.dropFilter = (rowData) => rowData.item && !rowData.item.dropDisabled;
|
||||
|
||||
grid.dragFilter = (rowData) => rowData.item && !rowData.item.dragDisabled;
|
||||
|
||||
grid.addEventListener('grid-dragstart', (e) => {
|
||||
if (grid._isSelected(e.detail.draggedItems[0])) {
|
||||
// Dragging selected (possibly multiple) items
|
||||
if (grid.__selectionDragData) {
|
||||
Object.keys(grid.__selectionDragData).forEach((type) => {
|
||||
e.detail.setDragData(type, grid.__selectionDragData[type]);
|
||||
});
|
||||
} else {
|
||||
(grid.__dragDataTypes || []).forEach((type) => {
|
||||
e.detail.setDragData(type, e.detail.draggedItems.map((item) => item.dragData[type]).join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
if (grid.__selectionDraggedItemsCount > 1) {
|
||||
e.detail.setDraggedItemsCount(grid.__selectionDraggedItemsCount);
|
||||
}
|
||||
} else {
|
||||
// Dragging just one (non-selected) item
|
||||
(grid.__dragDataTypes || []).forEach((type) => {
|
||||
e.detail.setDragData(type, e.detail.draggedItems[0].dragData[type]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
grid.isItemSelectable = (item) => {
|
||||
// If there is no selectable data, assume the item is selectable
|
||||
return item?.selectable === undefined || item.selectable;
|
||||
};
|
||||
|
||||
function isRowFullyInViewport(row) {
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const tableRect = grid.$.table.getBoundingClientRect();
|
||||
const headerRect = grid.$.header.getBoundingClientRect();
|
||||
const footerRect = grid.$.footer.getBoundingClientRect();
|
||||
return rowRect.top >= tableRect.top + headerRect.height && rowRect.bottom <= tableRect.bottom - footerRect.height;
|
||||
}
|
||||
|
||||
grid.$connector.scrollToItem = function (itemKey, ...args) {
|
||||
const targetRow = grid._getRenderedRows().find((row) => {
|
||||
const { item } = grid.__getRowModel(row);
|
||||
return grid.getItemId(item) === itemKey;
|
||||
});
|
||||
if (targetRow && isRowFullyInViewport(targetRow)) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.scrollToIndex(...args);
|
||||
};
|
||||
};
|
||||
2
backend/src/main/frontend/generated/jar-resources/index.d.ts
vendored
Normal file
2
backend/src/main/frontend/generated/jar-resources/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './copilot'
|
||||
export {}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Flow';
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/main/frontend/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC","sourcesContent":["export * from './Flow';\n"]}
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable max-params */
|
||||
import { html, render } from 'lit';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
|
||||
type RenderRoot = HTMLElement & { __litRenderer?: Renderer; _$litPart$?: any };
|
||||
|
||||
type ItemModel = { item: any; index: number };
|
||||
|
||||
type Renderer = ((root: RenderRoot, rendererOwner: HTMLElement, model: ItemModel) => void) & { __rendererId?: string };
|
||||
|
||||
type Component = HTMLElement & { [key: string]: Renderer | undefined };
|
||||
|
||||
const _window = window as any;
|
||||
_window.Vaadin = _window.Vaadin || {};
|
||||
|
||||
/**
|
||||
* Assigns the component a renderer function which uses Lit to render
|
||||
* the given template expression inside the render root element.
|
||||
*
|
||||
* @param component The host component to which the renderer runction is to be set
|
||||
* @param rendererName The name of the renderer function
|
||||
* @param templateExpression The content of the template literal passed to Lit for rendering.
|
||||
* @param returnChannel A channel to the server.
|
||||
* Calling it will end up invoking a handler in the server-side LitRenderer.
|
||||
* @param clientCallables A list of function names that can be called from within the template literal.
|
||||
* @param propertyNamespace LitRenderer-specific namespace for properties.
|
||||
* Needed to avoid property name collisions between renderers.
|
||||
*/
|
||||
_window.Vaadin.setLitRenderer = (
|
||||
component: Component,
|
||||
rendererName: string,
|
||||
templateExpression: string,
|
||||
returnChannel: (name: string, itemKey: string, args: any[]) => void,
|
||||
clientCallables: string[],
|
||||
propertyNamespace: string,
|
||||
appId: string
|
||||
) => {
|
||||
const callablesCreator = (itemKey: string) => {
|
||||
return clientCallables.map((clientCallable) => (...args: any[]) => {
|
||||
if (itemKey !== undefined) {
|
||||
returnChannel(clientCallable, itemKey, args[0] instanceof Event ? [] : [...args]);
|
||||
}
|
||||
});
|
||||
};
|
||||
const fnArgs = [
|
||||
'html',
|
||||
'root',
|
||||
'live',
|
||||
'appId',
|
||||
'itemKey',
|
||||
'model',
|
||||
'item',
|
||||
'index',
|
||||
...clientCallables,
|
||||
`return html\`${templateExpression}\``
|
||||
];
|
||||
const htmlGenerator = new Function(...fnArgs);
|
||||
const renderFunction = (root: RenderRoot, model: ItemModel, itemKey: string) => {
|
||||
const { item, index } = model;
|
||||
render(htmlGenerator(html, root, live, appId, itemKey, model, item, index, ...callablesCreator(itemKey)), root);
|
||||
};
|
||||
|
||||
const renderer: Renderer = (root, _, model) => {
|
||||
const { item } = model;
|
||||
// Clean up the root element of any existing content
|
||||
// (and Lit's _$litPart$ property) from other renderers
|
||||
// TODO: Remove once https://github.com/vaadin/web-components/issues/2235 is done
|
||||
if (root.__litRenderer !== renderer) {
|
||||
root.innerHTML = '';
|
||||
delete root._$litPart$;
|
||||
root.__litRenderer = renderer;
|
||||
}
|
||||
|
||||
// Map a new item that only includes the properties defined by
|
||||
// this specific LitRenderer instance. The renderer instance specific
|
||||
// "propertyNamespace" prefix is stripped from the property name at this point:
|
||||
//
|
||||
// item: { key: "2", lr_3769df5394a74ef3_lastName: "Tyler"}
|
||||
// ->
|
||||
// mappedItem: { lastName: "Tyler" }
|
||||
const mappedItem: { [key: string]: any } = {};
|
||||
for (const key in item) {
|
||||
if (key.startsWith(propertyNamespace)) {
|
||||
mappedItem[key.replace(propertyNamespace, '')] = item[key];
|
||||
}
|
||||
}
|
||||
|
||||
renderFunction(root, { ...model, item: mappedItem }, item.key);
|
||||
};
|
||||
|
||||
renderer.__rendererId = propertyNamespace;
|
||||
component[rendererName] = renderer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the renderer function with the given name from the component
|
||||
* if the propertyNamespace matches the renderer's id.
|
||||
*
|
||||
* @param component The host component whose renderer function is to be removed
|
||||
* @param rendererName The name of the renderer function
|
||||
* @param rendererId The rendererId of the function to be removed
|
||||
*/
|
||||
_window.Vaadin.unsetLitRenderer = (component: Component, rendererName: string, rendererId: string) => {
|
||||
// The check for __rendererId property is necessary since the renderer function
|
||||
// may get overridden by another renderer, for example, by one coming from
|
||||
// vaadin-template-renderer. We don't want LitRenderer registration cleanup to
|
||||
// unintentionally remove the new renderer.
|
||||
if (component[rendererName]?.__rendererId === rendererId) {
|
||||
component[rendererName] = undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2000-2026 Vaadin Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
import './contextMenuConnector.js';
|
||||
|
||||
/**
|
||||
* Initializes the connector for a menu bar element.
|
||||
*
|
||||
* @param {HTMLElement} menubar
|
||||
* @param {string} appId
|
||||
*/
|
||||
function initLazy(menubar, appId) {
|
||||
if (menubar.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((records) => {
|
||||
const hasChangedAttributes = records.some((entry) => {
|
||||
const oldValue = entry.oldValue;
|
||||
const newValue = entry.target.getAttribute(entry.attributeName);
|
||||
return oldValue !== newValue;
|
||||
});
|
||||
|
||||
if (hasChangedAttributes) {
|
||||
menubar.$connector.generateItems();
|
||||
}
|
||||
});
|
||||
|
||||
menubar.$connector = {
|
||||
/**
|
||||
* Generates and assigns the items to the menu bar.
|
||||
*
|
||||
* When the method is called without providing a node id,
|
||||
* the previously generated items tree will be used.
|
||||
* That can be useful if you only want to sync the disabled and hidden properties of root items.
|
||||
*
|
||||
* @param {number | undefined} nodeId
|
||||
*/
|
||||
generateItems(nodeId) {
|
||||
if (!menubar.shadowRoot) {
|
||||
// workaround for https://github.com/vaadin/flow/issues/5722
|
||||
setTimeout(() => menubar.$connector.generateItems(nodeId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!menubar._container) {
|
||||
// Menu-bar defers first buttons render to avoid re-layout
|
||||
// See https://github.com/vaadin/web-components/issues/7271
|
||||
queueMicrotask(() => menubar.$connector.generateItems(nodeId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeId) {
|
||||
menubar.__generatedItems = window.Vaadin.Flow.contextMenuConnector.generateItemsTree(appId, nodeId);
|
||||
}
|
||||
|
||||
let items = menubar.__generatedItems || [];
|
||||
|
||||
items.forEach((item) => {
|
||||
// Propagate disabled state from items to parent buttons
|
||||
item.disabled = item.component.disabled;
|
||||
|
||||
// Saving item to component because `_item` can be reassigned to a new value
|
||||
// when the component goes to the overflow menu
|
||||
item.component._rootItem = item;
|
||||
});
|
||||
|
||||
// Observe for hidden and disabled attributes in case they are changed by Flow.
|
||||
// When a change occurs, the observer will re-generate items on top of the existing tree
|
||||
// to sync the new attribute values with the corresponding properties in the items array.
|
||||
items.forEach((item) => {
|
||||
observer.observe(item.component, {
|
||||
attributeFilter: ['hidden', 'disabled'],
|
||||
attributeOldValue: true
|
||||
});
|
||||
});
|
||||
|
||||
// Remove hidden items entirely from the array. Just hiding them
|
||||
// could cause the overflow button to be rendered without items.
|
||||
//
|
||||
// The items-prop needs to be set even when all items are visible
|
||||
// to update the disabled state and re-render buttons.
|
||||
items = items.filter((item) => !item.component.hidden);
|
||||
|
||||
menubar.items = items;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setClassName(component) {
|
||||
const item = component._rootItem || component._item;
|
||||
|
||||
if (item) {
|
||||
item.className = component.className;
|
||||
}
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.menubarConnector = { initLazy, setClassName };
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2000-2026 Vaadin Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a DateTimeFormat with the given locale, or throws if invalid.
|
||||
*/
|
||||
function createDateTimeFormatter(locale) {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function getFormatter(locale) {
|
||||
// Try creating formatter with progressive fallbacks
|
||||
const localeParts = locale?.split('-');
|
||||
const fallbackLocales = [
|
||||
locale, // Full locale (e.g., "de-DE-hw")
|
||||
localeParts?.slice(0, 2).join('-'), // Base locale without variant (e.g., "de-DE")
|
||||
localeParts?.[0] // Language only (e.g., "de")
|
||||
];
|
||||
|
||||
for (const fallbackLocale of fallbackLocales) {
|
||||
try {
|
||||
return createDateTimeFormatter(fallbackLocale);
|
||||
} catch (e) {
|
||||
// Continue to next fallback
|
||||
}
|
||||
}
|
||||
|
||||
return createDateTimeFormatter(undefined); // Default locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given items to a new array of items with formatted time.
|
||||
*/
|
||||
function formatItems(items, locale) {
|
||||
const formatter = getFormatter(locale);
|
||||
|
||||
return items.map((item) =>
|
||||
item.time
|
||||
? Object.assign(item, {
|
||||
time: formatter.format(new Date(item.time))
|
||||
})
|
||||
: item
|
||||
);
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.messageListConnector = {
|
||||
/**
|
||||
* Fully replaces the items in the list with the given items.
|
||||
*/
|
||||
setItems(list, items, locale) {
|
||||
list.items = formatItems(items, locale);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the text of the item at the given index to the given text.
|
||||
*/
|
||||
setItemText(list, text, index) {
|
||||
list.items[index].text = text;
|
||||
list.items = [...list.items];
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends the given text to the text of the item at the given index.
|
||||
*/
|
||||
appendItemText(list, appendedText, index) {
|
||||
const currentText = list.items[index].text || '';
|
||||
this.setItemText(list, currentText + appendedText, index);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds the given items to the end of the list.
|
||||
*/
|
||||
addItems(list, newItems, locale) {
|
||||
list.items = [...(list.items || []), ...formatItems(newItems, locale)];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
window.Vaadin.Flow.selectConnector = {};
|
||||
window.Vaadin.Flow.selectConnector.initLazy = (select) => {
|
||||
// do not init this connector twice for the given select
|
||||
if (select.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
select.$connector = {};
|
||||
|
||||
select.renderer = (root) => {
|
||||
const listBox = select.querySelector('vaadin-select-list-box');
|
||||
if (listBox) {
|
||||
if (root.firstChild) {
|
||||
root.removeChild(root.firstChild);
|
||||
}
|
||||
root.appendChild(listBox);
|
||||
}
|
||||
};
|
||||
};
|
||||
175
backend/src/main/frontend/generated/jar-resources/theme-util.js
Normal file
175
backend/src/main/frontend/generated/jar-resources/theme-util.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import stripCssComments from 'strip-css-comments';
|
||||
|
||||
// Safari 15 - 16.3, polyfilled
|
||||
const polyfilledSafari = CSSStyleSheet.toString().includes('document.createElement');
|
||||
|
||||
const createLinkReferences = (css, target) => {
|
||||
// Unresolved urls are written as '@import url(text);' or '@import "text";' to the css
|
||||
// media query can be present on @media tag or on @import directive after url
|
||||
// Note that with Vite production build there is no space between @import and "text"
|
||||
// [0] is the full match
|
||||
// [1] matches the media query
|
||||
// [2] matches the url
|
||||
// [3] matches the quote char surrounding in '@import "..."'
|
||||
// [4] matches the url in '@import "..."'
|
||||
// [5] matches media query on @import statement
|
||||
const importMatcher =
|
||||
/(?:@media\s(.+?))?(?:\s{)?\@import\s*(?:url\(\s*['"]?(.+?)['"]?\s*\)|(["'])((?:\\.|[^\\])*?)\3)([^;]*);(?:})?/g;
|
||||
|
||||
// Only cleanup if comment exist
|
||||
if (/\/\*(.|[\r\n])*?\*\//gm.exec(css) != null) {
|
||||
// clean up comments
|
||||
css = stripCssComments(css);
|
||||
}
|
||||
|
||||
var match;
|
||||
var styleCss = css;
|
||||
|
||||
// For each external url import add a link reference
|
||||
while ((match = importMatcher.exec(css)) !== null) {
|
||||
styleCss = styleCss.replace(match[0], '');
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = match[2] || match[4];
|
||||
const media = match[1] || match[5];
|
||||
if (media) {
|
||||
link.media = media;
|
||||
}
|
||||
// For target document append to head else append to target
|
||||
if (target === document) {
|
||||
document.head.appendChild(link);
|
||||
} else {
|
||||
target.appendChild(link);
|
||||
}
|
||||
}
|
||||
return styleCss;
|
||||
};
|
||||
|
||||
const addAdoptedStyleSafariPolyfill = (sheet, target, first) => {
|
||||
if (first) {
|
||||
target.adoptedStyleSheets = [sheet, ...target.adoptedStyleSheets];
|
||||
} else {
|
||||
target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet];
|
||||
}
|
||||
return () => {
|
||||
target.adoptedStyleSheets = target.adoptedStyleSheets.filter((ss) => ss !== sheet);
|
||||
};
|
||||
};
|
||||
|
||||
const addAdoptedStyle = (cssText, target, first) => {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(cssText);
|
||||
if (polyfilledSafari) {
|
||||
return addAdoptedStyleSafariPolyfill(sheet, target, first);
|
||||
}
|
||||
if (first) {
|
||||
target.adoptedStyleSheets.splice(0, 0, sheet);
|
||||
} else {
|
||||
target.adoptedStyleSheets.push(sheet);
|
||||
}
|
||||
return () => {
|
||||
target.adoptedStyleSheets.splice(target.adoptedStyleSheets.indexOf(sheet), 1);
|
||||
};
|
||||
};
|
||||
|
||||
const addStyleTag = (cssText, referenceComment) => {
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.textContent = cssText;
|
||||
|
||||
let beforeThis = undefined;
|
||||
if (referenceComment) {
|
||||
const comments = Array.from(document.head.childNodes).filter((elem) => elem.nodeType === Node.COMMENT_NODE);
|
||||
const container = comments.find((comment) => comment.data.trim() === referenceComment);
|
||||
if (container) {
|
||||
beforeThis = container;
|
||||
}
|
||||
}
|
||||
document.head.insertBefore(styleTag, beforeThis);
|
||||
return () => {
|
||||
styleTag.remove();
|
||||
};
|
||||
};
|
||||
|
||||
// target: Document | ShadowRoot
|
||||
export const injectGlobalCss = (css, referenceComment, target, first) => {
|
||||
if (target === document) {
|
||||
const hash = getHash(css);
|
||||
if (window.Vaadin.theme.injectedGlobalCss.indexOf(hash) !== -1) {
|
||||
return;
|
||||
}
|
||||
window.Vaadin.theme.injectedGlobalCss.push(hash);
|
||||
}
|
||||
const cssText = createLinkReferences(css, target);
|
||||
|
||||
// We avoid mixing style tags and adoptedStyleSheets to make override order clear
|
||||
if (target === document) {
|
||||
return addStyleTag(cssText, referenceComment);
|
||||
}
|
||||
|
||||
return addAdoptedStyle(cssText, target, first);
|
||||
};
|
||||
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.theme = window.Vaadin.theme || {};
|
||||
window.Vaadin.theme.injectedGlobalCss = [];
|
||||
|
||||
const webcomponentGlobalCss = {
|
||||
css: [],
|
||||
importers: []
|
||||
};
|
||||
|
||||
export const injectGlobalWebcomponentCss = (css) => {
|
||||
webcomponentGlobalCss.css.push(css);
|
||||
webcomponentGlobalCss.importers.forEach((registrar) => {
|
||||
registrar(css);
|
||||
});
|
||||
};
|
||||
|
||||
export const webcomponentGlobalCssInjector = (registrar) => {
|
||||
const registeredCss = [];
|
||||
const wrapper = (css) => {
|
||||
const hash = getHash(css);
|
||||
if (!registeredCss.includes(hash)) {
|
||||
registeredCss.push(hash);
|
||||
registrar(css);
|
||||
}
|
||||
};
|
||||
webcomponentGlobalCss.importers.push(wrapper);
|
||||
webcomponentGlobalCss.css.forEach(wrapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a 32 bit FNV-1a hash
|
||||
* Found here: https://gist.github.com/vaiorabbit/5657561
|
||||
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
|
||||
*
|
||||
* @param {string} str the input value
|
||||
* @returns {string} 32 bit (as 8 byte hex string)
|
||||
*/
|
||||
function hashFnv32a(str) {
|
||||
/*jshint bitwise:false */
|
||||
let i,
|
||||
l,
|
||||
hval = 0x811c9dc5;
|
||||
|
||||
for (i = 0, l = str.length; i < l; i++) {
|
||||
hval ^= str.charCodeAt(i);
|
||||
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
|
||||
}
|
||||
|
||||
// Convert to 8 digit hex string
|
||||
return ('0000000' + (hval >>> 0).toString(16)).substr(-8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a 64 bit hash for the given input.
|
||||
* Double hash is used to significantly lower the collision probability.
|
||||
*
|
||||
* @param {string} input value to get hash for
|
||||
* @returns {string} 64 bit (as 16 byte hex string)
|
||||
*/
|
||||
function getHash(input) {
|
||||
let h1 = hashFnv32a(input); // returns 32 bit (as 8 byte hex string)
|
||||
return h1 + hashFnv32a(h1 + input);
|
||||
}
|
||||
23
backend/src/main/frontend/generated/jar-resources/tooltip.ts
Normal file
23
backend/src/main/frontend/generated/jar-resources/tooltip.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Tooltip } from '@vaadin/tooltip/src/vaadin-tooltip.js';
|
||||
|
||||
const _window = window as any;
|
||||
_window.Vaadin ||= {};
|
||||
_window.Vaadin.Flow ||= {};
|
||||
_window.Vaadin.Flow.tooltip ||= {};
|
||||
|
||||
Object.assign(_window.Vaadin.Flow.tooltip, {
|
||||
setDefaultHideDelay: (hideDelay: number) => Tooltip.setDefaultHideDelay(hideDelay),
|
||||
setDefaultFocusDelay: (focusDelay: number) => Tooltip.setDefaultFocusDelay(focusDelay),
|
||||
setDefaultHoverDelay: (hoverDelay: number) => Tooltip.setDefaultHoverDelay(hoverDelay)
|
||||
});
|
||||
|
||||
const { defaultHideDelay, defaultFocusDelay, defaultHoverDelay } = _window.Vaadin.Flow.tooltip;
|
||||
if (defaultHideDelay) {
|
||||
Tooltip.setDefaultHideDelay(defaultHideDelay);
|
||||
}
|
||||
if (defaultFocusDelay) {
|
||||
Tooltip.setDefaultFocusDelay(defaultFocusDelay);
|
||||
}
|
||||
if (defaultHoverDelay) {
|
||||
Tooltip.setDefaultHoverDelay(defaultHoverDelay);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-nocheck
|
||||
import './gridConnector.ts';
|
||||
|
||||
/**
|
||||
* treeGridConnector is a communication layer between TreeGrid's flow component
|
||||
* (server-side) and web component (client-side).
|
||||
*
|
||||
* TreeGrid does not rely on the web component's built-in features for handling
|
||||
* hierarchical data. Instead, the hierarchy is fully managed on the server side
|
||||
* and sent to the client as a flattened structure. This approach simplifies the
|
||||
* client-side implementation and improves performance by avoiding recursive
|
||||
* requests to the data provider.
|
||||
*
|
||||
* While the data is transferred as a flat list, the connector makes it appear as
|
||||
* a hierarchy by overriding the web component's methods to add indentation based
|
||||
* on information from server-provided fields `item.level`, `item.expanded`, etc.
|
||||
*
|
||||
* The connector overrides the web component's default `scrollToIndex(...indexes)`
|
||||
* implementation, as it by default assumes that the hierarchy is managed on the
|
||||
* client side. Instead, it uses the server-side method to resolve the hierarchical
|
||||
* path and preload the viewport range, all in a single round-trip. As a result,
|
||||
* required data is already loaded on the client-side by the time the scrolling
|
||||
* begins, which allows the scrollToIndex operation to be executed faster.
|
||||
*
|
||||
* The server estimates the viewport range for `scrollToIndex` based on the `padding`
|
||||
* parameter of $server.setViewportRangeByIndexPath, which defines how many items to
|
||||
* include above and below the target item in the range.
|
||||
*/
|
||||
window.Vaadin.Flow.treeGridConnector = {};
|
||||
window.Vaadin.Flow.treeGridConnector.initLazy = function (grid) {
|
||||
if (grid.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.gridConnector.initLazy(grid);
|
||||
|
||||
function getViewportRange() {
|
||||
const renderedRows = grid._getRenderedRows();
|
||||
return [renderedRows[0]?.index ?? 0, renderedRows[renderedRows.length - 1]?.index ?? 0];
|
||||
}
|
||||
|
||||
grid._dataProviderController._shouldLoadCachePage = function (cache, page) {
|
||||
// `$server.setViewportRangeByIndexPath` sends a preloaded viewport range based on
|
||||
// the provided index path and `padding` parameter. Applying the new range clears
|
||||
// the old range, which is still visible because the actual scroll happens only
|
||||
// after all connector calls in that update are processed. This check prevents
|
||||
// the old range from being unnecessarily re-requested while the new range is
|
||||
// still being processed, which could cause flickering.
|
||||
return !grid.__pendingScrollToIndexes;
|
||||
};
|
||||
|
||||
grid.scrollToIndex = async function (...indexes) {
|
||||
grid.__pendingScrollToIndexes = indexes;
|
||||
|
||||
if (!grid.clientHeight || !grid._columnTree || grid._dataProviderController.isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [start, end] = getViewportRange();
|
||||
const padding = Math.floor((end - start) * 1.5);
|
||||
const flatIndex = await grid.$server.setViewportRangeByIndexPath(indexes, padding);
|
||||
grid._scrollToFlatIndex(flatIndex);
|
||||
|
||||
delete grid.__pendingScrollToIndexes;
|
||||
|
||||
return flatIndex;
|
||||
};
|
||||
|
||||
grid.__getRowLevel = function (row) {
|
||||
return row._item?.level ?? 0;
|
||||
};
|
||||
|
||||
grid._isExpanded = function (item) {
|
||||
return !!item?.expanded;
|
||||
};
|
||||
|
||||
grid.expandItem = function (item) {
|
||||
if (item !== undefined) {
|
||||
grid.$server.updateExpandedState(grid.getItemId(item), true);
|
||||
}
|
||||
};
|
||||
|
||||
grid.collapseItem = function (item) {
|
||||
if (item !== undefined) {
|
||||
grid.$server.updateExpandedState(grid.getItemId(item), false);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2000-2026 Vaadin Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
import { css } from 'lit';
|
||||
import { TextField } from '@vaadin/text-field/src/vaadin-text-field.js';
|
||||
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
||||
|
||||
class BigDecimalField extends TextField {
|
||||
static get is() {
|
||||
return 'vaadin-big-decimal-field';
|
||||
}
|
||||
|
||||
static get lumoInjector() {
|
||||
return { ...super.lumoInjector, is: 'vaadin-text-field' };
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
css`
|
||||
:host([dir='rtl']) [part='input-field'] {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
:host([dir='rtl']) [part='input-field'] ::slotted(input) {
|
||||
--_lumo-text-field-overflow-mask-image: linear-gradient(to left, transparent, #000 1.25em) !important;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_decimalSeparator: {
|
||||
type: String,
|
||||
value: '.',
|
||||
sync: true,
|
||||
observer: '__decimalSeparatorChanged'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.inputElement.setAttribute('inputmode', 'decimal');
|
||||
}
|
||||
|
||||
__decimalSeparatorChanged(separator, oldSeparator) {
|
||||
this.allowedCharPattern = '[-+\\d' + separator + ']';
|
||||
|
||||
if (this.value && oldSeparator) {
|
||||
this.value = this.value.split(oldSeparator).join(separator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineCustomElement(BigDecimalField);
|
||||
25
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/License.d.ts
vendored
Normal file
25
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/License.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ServerMessage } from './vaadin-dev-tools';
|
||||
export interface Product {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
export interface PreTrial {
|
||||
trialName?: String;
|
||||
trialState: String;
|
||||
daysRemaining?: number;
|
||||
daysRemainingUntilRenewal?: number;
|
||||
}
|
||||
export interface ProductAndMessage {
|
||||
message: string;
|
||||
messageHtml?: string;
|
||||
product: Product;
|
||||
preTrial?: PreTrial;
|
||||
}
|
||||
export declare const findAll: (element: Element | ShadowRoot | Document, tags: string[]) => Element[];
|
||||
export declare const licenseCheckOk: (data: Product) => void;
|
||||
export declare const licenseCheckFailed: (data: ProductAndMessage) => void;
|
||||
export declare const licenseCheckNoKey: (data: ProductAndMessage) => void;
|
||||
export declare const handleLicenseMessage: (message: ServerMessage, bodyShadowRoot: ShadowRoot | null) => boolean;
|
||||
export declare const startPreTrial: () => void;
|
||||
export declare const tryAcquireLicense: () => void;
|
||||
export declare const licenseInit: () => void;
|
||||
15
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/connection.d.ts
vendored
Normal file
15
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/connection.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export declare enum ConnectionStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
UNAVAILABLE = "unavailable",
|
||||
ERROR = "error"
|
||||
}
|
||||
export declare abstract class Connection {
|
||||
static HEARTBEAT_INTERVAL: number;
|
||||
status: ConnectionStatus;
|
||||
onHandshake(): void;
|
||||
onConnectionError(_: string): void;
|
||||
onStatusChange(_: ConnectionStatus): void;
|
||||
setActive(yes: boolean): void;
|
||||
setStatus(status: ConnectionStatus): void;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Connection } from './connection.js';
|
||||
export declare class LiveReloadConnection extends Connection {
|
||||
webSocket?: WebSocket;
|
||||
constructor(url: string);
|
||||
onReload(_strategy: string): void;
|
||||
handleMessage(msg: any): void;
|
||||
handleError(msg: any): void;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ProductAndMessage } from './License';
|
||||
export declare const showPreTrialSplashScreen: (shadowRoot: ShadowRoot | null, message: ProductAndMessage) => void;
|
||||
export declare const preTrialStartFailed: (expired: boolean, shadowRoot: ShadowRoot | null) => void;
|
||||
export declare const updateLicenseDownloadStatus: (action: "started" | "failed" | "completed", shadowRoot: ShadowRoot | null) => void;
|
||||
105
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/vaadin-dev-tools.d.ts
vendored
Normal file
105
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/vaadin-dev-tools.d.ts
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { Product } from './License';
|
||||
import { ConnectionStatus } from './connection';
|
||||
/**
|
||||
* Plugin API for the dev tools window.
|
||||
*/
|
||||
export interface DevToolsInterface {
|
||||
send(command: string, data: any): void;
|
||||
}
|
||||
export interface MessageHandler {
|
||||
handleMessage(message: ServerMessage): boolean;
|
||||
}
|
||||
export interface ServerMessage {
|
||||
/**
|
||||
* The command
|
||||
*/
|
||||
command: string;
|
||||
/**
|
||||
* the data for the command
|
||||
*/
|
||||
data: any;
|
||||
}
|
||||
/**
|
||||
* To create and register a plugin, use e.g.
|
||||
* @example
|
||||
* export class MyTab extends LitElement implements MessageHandler {
|
||||
* render() {
|
||||
* return html`<div>Here I am</div>`;
|
||||
* }
|
||||
* }
|
||||
* customElements.define('my-tab', MyTab);
|
||||
*
|
||||
* const plugin: DevToolsPlugin = {
|
||||
* init: function (devToolsInterface: DevToolsInterface): void {
|
||||
* devToolsInterface.addTab('Tab title', 'my-tab')
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* (window as any).Vaadin.devToolsPlugins.push(plugin);
|
||||
*/
|
||||
export interface DevToolsPlugin {
|
||||
/**
|
||||
* Called once to initialize the plugin.
|
||||
*
|
||||
* @param devToolsInterface provides methods to interact with the dev tools
|
||||
*/
|
||||
init(devToolsInterface: DevToolsInterface): void;
|
||||
}
|
||||
export declare enum MessageType {
|
||||
LOG = "log",
|
||||
INFORMATION = "information",
|
||||
WARNING = "warning",
|
||||
ERROR = "error"
|
||||
}
|
||||
type DevToolsConf = {
|
||||
enable: boolean;
|
||||
url: string;
|
||||
contextRelativePath: string;
|
||||
backend?: string;
|
||||
liveReloadPort?: number;
|
||||
token?: string;
|
||||
usageStatisticsEnabled?: boolean;
|
||||
};
|
||||
export declare class VaadinDevTools extends LitElement {
|
||||
unhandledMessages: ServerMessage[];
|
||||
conf: DevToolsConf;
|
||||
bodyShadowRoot: ShadowRoot | null;
|
||||
static get styles(): import("lit").CSSResult[];
|
||||
static DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE: string;
|
||||
static ACTIVE_KEY_IN_SESSION_STORAGE: string;
|
||||
static TRIGGERED_KEY_IN_SESSION_STORAGE: string;
|
||||
static TRIGGERED_COUNT_KEY_IN_SESSION_STORAGE: string;
|
||||
static AUTO_DEMOTE_NOTIFICATION_DELAY: number;
|
||||
static HOTSWAP_AGENT: string;
|
||||
static JREBEL: string;
|
||||
static SPRING_BOOT_DEVTOOLS: string;
|
||||
static BACKEND_DISPLAY_NAME: Record<string, string>;
|
||||
static get isActive(): boolean;
|
||||
frontendStatus: ConnectionStatus;
|
||||
javaStatus: ConnectionStatus;
|
||||
private root;
|
||||
componentPickActive: boolean;
|
||||
private javaConnection?;
|
||||
private frontendConnection?;
|
||||
private nextMessageId;
|
||||
private transitionDuration;
|
||||
elementTelemetry(): void;
|
||||
openWebSocketConnection(): void;
|
||||
removeOldLinks(path: string): void;
|
||||
tabHandleMessage(tabElement: HTMLElement, message: ServerMessage): boolean;
|
||||
handleFrontendMessage(message: ServerMessage): void;
|
||||
handleHmrMessage(message: ServerMessage): boolean;
|
||||
getDedicatedWebSocketUrl(): string | undefined;
|
||||
getSpringBootWebSocketUrl(location: any): string;
|
||||
connectedCallback(): void;
|
||||
initPlugin(plugin: DevToolsPlugin): Promise<void>;
|
||||
format(o: any): string;
|
||||
checkLicense(productInfo: Product): void;
|
||||
startPreTrial(): void;
|
||||
downloadLicense(productInfo: Product): void;
|
||||
setActive(yes: boolean): void;
|
||||
render(): import("lit-html").TemplateResult<1>;
|
||||
setJavaLiveReloadActive(active: boolean): void;
|
||||
}
|
||||
export {};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/websocket-connection.d.ts
vendored
Normal file
13
backend/src/main/frontend/generated/jar-resources/vaadin-dev-tools/websocket-connection.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Connection } from './connection';
|
||||
export declare class WebSocketConnection extends Connection {
|
||||
static HEARTBEAT_INTERVAL: number;
|
||||
socket?: any;
|
||||
canSend: boolean;
|
||||
constructor(url: string);
|
||||
onReload(_strategy: string): void;
|
||||
onUpdate(_path: string, _content: string): void;
|
||||
onMessage(_message: any): void;
|
||||
handleMessage(msg: any): void;
|
||||
handleError(msg: any): void;
|
||||
send(command: string, data: any): void;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import '@vaadin/grid/src/vaadin-grid-column.js';
|
||||
import { GridColumn } from '@vaadin/grid/src/vaadin-grid-column.js';
|
||||
import { GridSelectionColumnBaseMixin } from '@vaadin/grid/src/vaadin-grid-selection-column-base-mixin.js';
|
||||
|
||||
export class GridFlowSelectionColumn extends GridSelectionColumnBaseMixin(GridColumn) {
|
||||
static get is() {
|
||||
return 'vaadin-grid-flow-selection-column';
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* Override property to enable auto-width
|
||||
*/
|
||||
autoWidth: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* Override property to set custom width
|
||||
*/
|
||||
width: {
|
||||
type: String,
|
||||
value: '56px'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override method from `GridSelectionColumnBaseMixin` to add ID to select all
|
||||
* checkbox
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_defaultHeaderRenderer(root, _column) {
|
||||
super._defaultHeaderRenderer(root, _column);
|
||||
const checkbox = root.firstElementChild;
|
||||
if (checkbox) {
|
||||
checkbox.id = 'selectAllCheckbox';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override a method from `GridSelectionColumnBaseMixin` to handle the user
|
||||
* selecting all items.
|
||||
*
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
_selectAll() {
|
||||
this.selectAll = true;
|
||||
this.$server.selectAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override a method from `GridSelectionColumnBaseMixin` to handle the user
|
||||
* deselecting all items.
|
||||
*
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
_deselectAll() {
|
||||
this.selectAll = false;
|
||||
this.$server.deselectAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override a method from `GridSelectionColumnBaseMixin` to handle the user
|
||||
* selecting an item.
|
||||
*
|
||||
* @param {Object} item the item to select
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
_selectItem(item) {
|
||||
this.$server.setShiftKeyDown(this._shiftKeyDown);
|
||||
this._grid.$connector.doSelection([item], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override a method from `GridSelectionColumnBaseMixin` to handle the user
|
||||
* deselecting an item.
|
||||
*
|
||||
* @param {Object} item the item to deselect
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
_deselectItem(item) {
|
||||
this.$server.setShiftKeyDown(this._shiftKeyDown);
|
||||
this._grid.$connector.doDeselection([item], true);
|
||||
// Optimistically update select all state
|
||||
this.selectAll = false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(GridFlowSelectionColumn.is, GridFlowSelectionColumn);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Popover } from '@vaadin/popover/src/vaadin-popover.js';
|
||||
|
||||
const _window = window as any;
|
||||
_window.Vaadin ||= {};
|
||||
_window.Vaadin.Flow ||= {};
|
||||
_window.Vaadin.Flow.popover ||= {};
|
||||
|
||||
Object.assign(_window.Vaadin.Flow.popover, {
|
||||
setDefaultHideDelay: (hideDelay: number) => Popover.setDefaultHideDelay(hideDelay),
|
||||
setDefaultFocusDelay: (focusDelay: number) => Popover.setDefaultFocusDelay(focusDelay),
|
||||
setDefaultHoverDelay: (hoverDelay: number) => Popover.setDefaultHoverDelay(hoverDelay)
|
||||
});
|
||||
|
||||
const { defaultHideDelay, defaultFocusDelay, defaultHoverDelay } = _window.Vaadin.Flow.popover;
|
||||
|
||||
if (defaultHideDelay) {
|
||||
Popover.setDefaultHideDelay(defaultHideDelay);
|
||||
}
|
||||
|
||||
if (defaultFocusDelay) {
|
||||
Popover.setDefaultFocusDelay(defaultFocusDelay);
|
||||
}
|
||||
|
||||
if (defaultHoverDelay) {
|
||||
Popover.setDefaultHoverDelay(defaultHoverDelay);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// map from unicode eastern arabic number characters to arabic numbers
|
||||
const EASTERN_ARABIC_DIGIT_MAP = {
|
||||
'\\u0660': '0',
|
||||
'\\u0661': '1',
|
||||
'\\u0662': '2',
|
||||
'\\u0663': '3',
|
||||
'\\u0664': '4',
|
||||
'\\u0665': '5',
|
||||
'\\u0666': '6',
|
||||
'\\u0667': '7',
|
||||
'\\u0668': '8',
|
||||
'\\u0669': '9'
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes the given string so it can be safely used in a regexp.
|
||||
*
|
||||
* @param {string} string
|
||||
* @return {string}
|
||||
*/
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses eastern arabic number characters to arabic numbers (0-9)
|
||||
*
|
||||
* @param {string} digits
|
||||
* @return {string}
|
||||
*/
|
||||
function parseEasternArabicDigits(digits) {
|
||||
return digits.replace(/[\u0660-\u0669]/g, function (char) {
|
||||
const unicode = '\\u0' + char.charCodeAt(0).toString(16);
|
||||
return EASTERN_ARABIC_DIGIT_MAP[unicode];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @param {Date} testTime
|
||||
* @return {string | null}
|
||||
*/
|
||||
function getAmOrPmString(locale, testTime) {
|
||||
const testTimeString = testTime.toLocaleTimeString(locale);
|
||||
|
||||
// AM/PM string is anything from one letter in eastern arabic to standard two letters,
|
||||
// to having space in between, dots ...
|
||||
// cannot disqualify whitespace since some locales use a. m. / p. m.
|
||||
// TODO when more scripts support is added (than Arabic), need to exclude those numbers too
|
||||
const amOrPmRegExp = /[^\d\u0660-\u0669]/;
|
||||
|
||||
const matches =
|
||||
// In most locales, the time ends with AM/PM:
|
||||
testTimeString.match(new RegExp(`${amOrPmRegExp.source}+$`, 'g')) ||
|
||||
// In some locales, the time starts with AM/PM e.g in Chinese:
|
||||
testTimeString.match(new RegExp(`^${amOrPmRegExp.source}+`, 'g'));
|
||||
|
||||
return matches && matches[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string | null}
|
||||
*/
|
||||
export function getSeparator(locale) {
|
||||
let timeString = TEST_PM_TIME.toLocaleTimeString(locale);
|
||||
|
||||
// Since the next regex picks first non-number-whitespace,
|
||||
// need to discard possible PM from beginning (eg. chinese locale)
|
||||
const pmString = getPmString(locale);
|
||||
if (pmString && timeString.startsWith(pmString)) {
|
||||
timeString = timeString.replace(pmString, '');
|
||||
}
|
||||
|
||||
const matches = timeString.match(/[^\u0660-\u0669\s\d]/);
|
||||
return matches && matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for either an AM or PM token in the given time string
|
||||
* depending on what is provided in `amOrPmString`.
|
||||
*
|
||||
* The search is case and space insensitive.
|
||||
*
|
||||
* @example
|
||||
* `searchAmOrPmToken('1 P M', 'PM')` => `'P M'`
|
||||
*
|
||||
* @example
|
||||
* `searchAmOrPmToken('1 a.m.', 'A. M.')` => `a.m.`
|
||||
*
|
||||
* @param {string} timeString
|
||||
* @param {string} amOrPmString
|
||||
* @return {string | null}
|
||||
*/
|
||||
export function searchAmOrPmToken(timeString, amOrPmString) {
|
||||
if (!amOrPmString) return null;
|
||||
|
||||
// Create a regexp string for searching for AM/PM without space-sensitivity.
|
||||
const tokenRegExpString = amOrPmString.split(/\s*/).map(escapeRegExp).join('\\s*');
|
||||
|
||||
// Create a regexp without case-sensitivity.
|
||||
const tokenRegExp = new RegExp(tokenRegExpString, 'i');
|
||||
|
||||
// Match the regexp against the time string.
|
||||
const tokenMatches = timeString.match(tokenRegExp);
|
||||
if (tokenMatches) {
|
||||
return tokenMatches[0];
|
||||
}
|
||||
}
|
||||
|
||||
export const TEST_PM_TIME = new Date('August 19, 1975 23:15:30');
|
||||
|
||||
export const TEST_AM_TIME = new Date('August 19, 1975 05:15:30');
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string}
|
||||
*/
|
||||
export function getPmString(locale) {
|
||||
return getAmOrPmString(locale, TEST_PM_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @return {string}
|
||||
*/
|
||||
export function getAmString(locale) {
|
||||
return getAmOrPmString(locale, TEST_AM_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} digits
|
||||
* @return {number}
|
||||
*/
|
||||
export function parseDigitsIntoInteger(digits) {
|
||||
return parseInt(parseEasternArabicDigits(digits));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} milliseconds
|
||||
* @return {number}
|
||||
*/
|
||||
export function parseMillisecondsIntoInteger(milliseconds) {
|
||||
milliseconds = parseEasternArabicDigits(milliseconds);
|
||||
// digits are either .1 .01 or .001 so need to "shift"
|
||||
if (milliseconds.length === 1) {
|
||||
milliseconds += '00';
|
||||
} else if (milliseconds.length === 2) {
|
||||
milliseconds += '0';
|
||||
}
|
||||
return parseInt(milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} timeString
|
||||
* @param {number} milliseconds
|
||||
* @param {string} amString
|
||||
* @param {string} pmString
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatMilliseconds(timeString, milliseconds, amString, pmString) {
|
||||
// might need to inject milliseconds between seconds and AM/PM
|
||||
let cleanedTimeString = timeString;
|
||||
if (timeString.endsWith(amString)) {
|
||||
cleanedTimeString = timeString.replace(' ' + amString, '');
|
||||
} else if (timeString.endsWith(pmString)) {
|
||||
cleanedTimeString = timeString.replace(' ' + pmString, '');
|
||||
}
|
||||
if (milliseconds) {
|
||||
let millisecondsString = milliseconds < 10 ? '0' : '';
|
||||
millisecondsString += milliseconds < 100 ? '0' : '';
|
||||
millisecondsString += milliseconds;
|
||||
cleanedTimeString += '.' + millisecondsString;
|
||||
} else {
|
||||
cleanedTimeString += '.000';
|
||||
}
|
||||
if (timeString.endsWith(amString)) {
|
||||
cleanedTimeString = cleanedTimeString + ' ' + amString;
|
||||
} else if (timeString.endsWith(pmString)) {
|
||||
cleanedTimeString = cleanedTimeString + ' ' + pmString;
|
||||
}
|
||||
return cleanedTimeString;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
TEST_PM_TIME,
|
||||
formatMilliseconds,
|
||||
parseMillisecondsIntoInteger,
|
||||
parseDigitsIntoInteger,
|
||||
getAmString,
|
||||
getPmString,
|
||||
getSeparator,
|
||||
searchAmOrPmToken
|
||||
} from './helpers.js';
|
||||
import { parseISOTime } from '@vaadin/time-picker/src/vaadin-time-picker-helper.js';
|
||||
|
||||
// Execute callback when predicate returns true.
|
||||
// Try again later if predicate returns false.
|
||||
function when(predicate, callback, timeout = 0) {
|
||||
if (predicate()) {
|
||||
callback();
|
||||
} else {
|
||||
setTimeout(() => when(predicate, callback, 200), timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function parseISO(text) {
|
||||
// The default i18n parser of the web component is ISO 8601 compliant.
|
||||
const timeObject = parseISOTime(text);
|
||||
|
||||
// The web component returns an object with string values
|
||||
// while the connector expects number values.
|
||||
return {
|
||||
hours: parseInt(timeObject.hours || 0),
|
||||
minutes: parseInt(timeObject.minutes || 0),
|
||||
seconds: parseInt(timeObject.seconds || 0),
|
||||
milliseconds: parseInt(timeObject.milliseconds || 0)
|
||||
};
|
||||
}
|
||||
|
||||
window.Vaadin.Flow.timepickerConnector = {};
|
||||
window.Vaadin.Flow.timepickerConnector.initLazy = (timepicker) => {
|
||||
// Check whether the connector was already initialized for the timepicker
|
||||
if (timepicker.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
timepicker.$connector = {};
|
||||
|
||||
timepicker.$connector.setLocale = (locale) => {
|
||||
// capture previous value if any
|
||||
let previousValueObject;
|
||||
if (timepicker.value && timepicker.value !== '') {
|
||||
previousValueObject = parseISO(timepicker.value);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check whether the locale is supported by the browser or not
|
||||
TEST_PM_TIME.toLocaleTimeString(locale);
|
||||
} catch (e) {
|
||||
locale = 'en-US';
|
||||
// FIXME should do a callback for server to throw an exception ?
|
||||
throw new Error(
|
||||
'vaadin-time-picker: The locale ' + locale + ' is not supported, falling back to default locale setting(en-US).'
|
||||
);
|
||||
}
|
||||
|
||||
// 1. 24 or 12 hour clock, if latter then what are the am/pm strings ?
|
||||
const pmString = getPmString(locale);
|
||||
const amString = getAmString(locale);
|
||||
|
||||
// 2. What is the separator ?
|
||||
const separator = getSeparator(locale);
|
||||
|
||||
const includeSeconds = function () {
|
||||
return timepicker.step && timepicker.step < 60;
|
||||
};
|
||||
|
||||
const includeMilliSeconds = function () {
|
||||
return timepicker.step && timepicker.step < 1;
|
||||
};
|
||||
|
||||
let cachedTimeString;
|
||||
let cachedTimeObject;
|
||||
|
||||
timepicker.i18n = {
|
||||
formatTime(timeObject) {
|
||||
if (!timeObject) return;
|
||||
|
||||
const timeToBeFormatted = new Date();
|
||||
timeToBeFormatted.setHours(timeObject.hours);
|
||||
timeToBeFormatted.setMinutes(timeObject.minutes);
|
||||
timeToBeFormatted.setSeconds(timeObject.seconds !== undefined ? timeObject.seconds : 0);
|
||||
|
||||
// the web component expects the correct granularity used for the time string,
|
||||
// thus need to format the time object in correct granularity by passing the format options
|
||||
let localeTimeString = timeToBeFormatted.toLocaleTimeString(locale, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: includeSeconds() ? 'numeric' : undefined
|
||||
});
|
||||
|
||||
// milliseconds not part of the time format API
|
||||
if (includeMilliSeconds()) {
|
||||
localeTimeString = formatMilliseconds(localeTimeString, timeObject.milliseconds, amString, pmString);
|
||||
}
|
||||
|
||||
return localeTimeString;
|
||||
},
|
||||
|
||||
parseTime(timeString) {
|
||||
if (timeString && timeString === cachedTimeString && cachedTimeObject) {
|
||||
return cachedTimeObject;
|
||||
}
|
||||
|
||||
if (!timeString) {
|
||||
// when nothing is returned, the component shows the invalid state for the input
|
||||
return;
|
||||
}
|
||||
|
||||
const amToken = searchAmOrPmToken(timeString, amString);
|
||||
const pmToken = searchAmOrPmToken(timeString, pmString);
|
||||
|
||||
const numbersOnlyTimeString = timeString
|
||||
.replace(amToken || '', '')
|
||||
.replace(pmToken || '', '')
|
||||
.trim();
|
||||
|
||||
// A regexp that allows to find the numbers with optional separator and continuing searching after it.
|
||||
const numbersRegExp = new RegExp('([\\d\\u0660-\\u0669]){1,2}(?:' + separator + ')?', 'g');
|
||||
|
||||
let hours = numbersRegExp.exec(numbersOnlyTimeString);
|
||||
if (hours) {
|
||||
hours = parseDigitsIntoInteger(hours[0].replace(separator, ''));
|
||||
// handle 12 am -> 0
|
||||
// do not do anything if am & pm are not used or if those are the same,
|
||||
// as with locale bg-BG there is always ч. at the end of the time
|
||||
if (amToken !== pmToken) {
|
||||
if (hours === 12 && amToken) {
|
||||
hours = 0;
|
||||
}
|
||||
if (hours !== 12 && pmToken) {
|
||||
hours += 12;
|
||||
}
|
||||
}
|
||||
const minutes = numbersRegExp.exec(numbersOnlyTimeString);
|
||||
const seconds = minutes && numbersRegExp.exec(numbersOnlyTimeString);
|
||||
// detecting milliseconds from input, expects am/pm removed from end, eg. .0 or .00 or .000
|
||||
const millisecondRegExp = /[[\.][\d\u0660-\u0669]{1,3}$/;
|
||||
// reset to end or things can explode
|
||||
let milliseconds = seconds && includeMilliSeconds() && millisecondRegExp.exec(numbersOnlyTimeString);
|
||||
// handle case where last numbers are seconds and . is the separator (invalid regexp match)
|
||||
if (milliseconds && milliseconds['index'] <= seconds['index']) {
|
||||
milliseconds = undefined;
|
||||
}
|
||||
// hours is a number at this point, others are either arrays or null
|
||||
// the string in [0] from the arrays includes the separator too
|
||||
cachedTimeObject = hours !== undefined && {
|
||||
hours: hours,
|
||||
minutes: minutes ? parseDigitsIntoInteger(minutes[0].replace(separator, '')) : 0,
|
||||
seconds: seconds ? parseDigitsIntoInteger(seconds[0].replace(separator, '')) : 0,
|
||||
milliseconds:
|
||||
minutes && seconds && milliseconds ? parseMillisecondsIntoInteger(milliseconds[0].replace('.', '')) : 0
|
||||
};
|
||||
cachedTimeString = timeString;
|
||||
return cachedTimeObject;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (previousValueObject) {
|
||||
when(
|
||||
() => timepicker.$,
|
||||
() => {
|
||||
const newValue = timepicker.i18n.formatTime(previousValueObject);
|
||||
// FIXME works but uses private API, needs fixes in web component
|
||||
if (timepicker.inputElement.value !== newValue) {
|
||||
timepicker.inputElement.value = newValue;
|
||||
timepicker.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
||||
import { timeOut } from '@vaadin/component-base/src/async.js';
|
||||
|
||||
window.Vaadin.Flow.virtualListConnector = {
|
||||
initLazy: function (list) {
|
||||
// Check whether the connector was already initialized for the virtual list
|
||||
if (list.$connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extraItemsBuffer = 20;
|
||||
|
||||
let lastRequestedRange = [0, 0];
|
||||
|
||||
list.$connector = {};
|
||||
list.$connector.placeholderItem = { __placeholder: true };
|
||||
|
||||
list.itemAccessibleNameGenerator = (item) => item && item.accessibleName;
|
||||
|
||||
const updateRequestedItem = function () {
|
||||
/*
|
||||
* TODO virtual list seems to do a small index adjustment after scrolling
|
||||
* has stopped. This causes a redundant request to be sent to make a
|
||||
* corresponding minimal change to the buffer. We should avoid these
|
||||
* requests by making the logic skip doing a request if the available
|
||||
* buffer is within some tolerance compared to the requested buffer.
|
||||
*/
|
||||
const visibleIndexes = [...list.children]
|
||||
.filter((el) => '__virtualListIndex' in el)
|
||||
.map((el) => el.__virtualListIndex);
|
||||
const firstNeededItem = Math.min(...visibleIndexes);
|
||||
const lastNeededItem = Math.max(...visibleIndexes);
|
||||
|
||||
let first = Math.max(0, firstNeededItem - extraItemsBuffer);
|
||||
let last = Math.min(lastNeededItem + extraItemsBuffer, list.items.length);
|
||||
|
||||
if (lastRequestedRange[0] != first || lastRequestedRange[1] != last) {
|
||||
lastRequestedRange = [first, last];
|
||||
const count = 1 + last - first;
|
||||
list.$server.setViewportRange(first, count);
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleUpdateRequest = function () {
|
||||
list.__requestDebounce = Debouncer.debounce(list.__requestDebounce, timeOut.after(50), updateRequestedItem);
|
||||
};
|
||||
|
||||
requestAnimationFrame(() => updateRequestedItem);
|
||||
|
||||
// Add an observer function that will invoke on virtualList.renderer property
|
||||
// change and then patches it with a wrapper renderer
|
||||
list.patchVirtualListRenderer = function () {
|
||||
if (!list.renderer || list.renderer.__virtualListConnectorPatched) {
|
||||
// The list either doesn't have a renderer yet or it's already been patched
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRenderer = list.renderer;
|
||||
|
||||
const renderer = (root, list, model) => {
|
||||
root.__virtualListIndex = model.index;
|
||||
|
||||
if (model.item === undefined) {
|
||||
if (list.$connector.placeholderElement) {
|
||||
// ComponentRenderer
|
||||
if (!root.__hasComponentRendererPlaceholder) {
|
||||
// The root was previously rendered by the ComponentRenderer. Clear and add a placeholder.
|
||||
root.innerHTML = '';
|
||||
delete root._$litPart$;
|
||||
root.appendChild(list.$connector.placeholderElement.cloneNode(true));
|
||||
root.__hasComponentRendererPlaceholder = true;
|
||||
}
|
||||
} else {
|
||||
// LitRenderer
|
||||
originalRenderer.call(list, root, list, {
|
||||
...model,
|
||||
item: list.$connector.placeholderItem
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (root.__hasComponentRendererPlaceholder) {
|
||||
// The root was previously populated with a placeholder. Clear it.
|
||||
root.innerHTML = '';
|
||||
root.__hasComponentRendererPlaceholder = false;
|
||||
}
|
||||
|
||||
originalRenderer.call(list, root, list, model);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if we need to do anything once things have settled down.
|
||||
* This method is called multiple times in sequence for the same user
|
||||
* action, but we only want to do the check once.
|
||||
*/
|
||||
scheduleUpdateRequest();
|
||||
};
|
||||
renderer.__virtualListConnectorPatched = true;
|
||||
renderer.__rendererId = originalRenderer.__rendererId;
|
||||
|
||||
list.renderer = renderer;
|
||||
};
|
||||
|
||||
list._createPropertyObserver('renderer', 'patchVirtualListRenderer', true);
|
||||
list.patchVirtualListRenderer();
|
||||
|
||||
list.items = [];
|
||||
|
||||
list.$connector.set = function (index, items) {
|
||||
list.items.splice(index, items.length, ...items);
|
||||
list.items = [...list.items];
|
||||
};
|
||||
|
||||
list.$connector.clear = function (index, length) {
|
||||
// How many items, starting from "index", should be set as undefined
|
||||
const clearCount = Math.min(length, list.items.length - index);
|
||||
list.$connector.set(index, [...Array(clearCount)]);
|
||||
};
|
||||
|
||||
list.$connector.updateData = function (items) {
|
||||
const updatedItemsMap = items.reduce((map, item) => {
|
||||
map[item.key] = item;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
list.items = list.items.map((item) => {
|
||||
// Items can be undefined if they are outside the viewport
|
||||
if (!item) {
|
||||
return item;
|
||||
}
|
||||
// Replace existing item with updated item,
|
||||
// return existing item as fallback if it was not updated
|
||||
return updatedItemsMap[item.key] || item;
|
||||
});
|
||||
};
|
||||
|
||||
list.$connector.updateSize = function (newSize) {
|
||||
const delta = newSize - list.items.length;
|
||||
if (delta > 0) {
|
||||
list.items = [...list.items, ...Array(delta)];
|
||||
} else if (delta < 0) {
|
||||
list.items = list.items.slice(0, newSize);
|
||||
}
|
||||
};
|
||||
|
||||
list.$connector.setPlaceholderItem = function (placeholderItem = {}, appId) {
|
||||
placeholderItem.__placeholder = true;
|
||||
list.$connector.placeholderItem = placeholderItem;
|
||||
const nodeId = Object.entries(placeholderItem).find(([key]) => key.endsWith('_nodeid'));
|
||||
list.$connector.placeholderElement = nodeId ? Vaadin.Flow.clients[appId].getByNodeId(nodeId[1]) : null;
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createElement as reactCreateElement } from 'react';
|
||||
|
||||
export const createElement = reactCreateElement;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { JSXSource, Fragment as reactFragment, jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
|
||||
|
||||
export const Fragment = reactFragment;
|
||||
|
||||
export function jsxDEV(
|
||||
type: React.ElementType,
|
||||
props: unknown,
|
||||
key: React.Key | undefined,
|
||||
isStatic: boolean,
|
||||
source?: JSXSource,
|
||||
self?: unknown
|
||||
): React.ReactElement {
|
||||
const realFreeze = Object.freeze;
|
||||
try {
|
||||
(Object as any).freeze = undefined; // prevent React from freezing the element
|
||||
|
||||
const reactElement: any = reactJsxDEV(type, props, key, isStatic, source, self);
|
||||
if (source && !reactElement._source) {
|
||||
// When running with React 19, put the source information on the _debugInfo array,
|
||||
// which will be transferred to the fiber node by React
|
||||
reactElement._debugInfo ??= [];
|
||||
reactElement._debugInfo.source = source;
|
||||
}
|
||||
realFreeze(reactElement);
|
||||
return reactElement;
|
||||
} finally {
|
||||
(Object as any).freeze = realFreeze;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Fragment as reactFragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
|
||||
|
||||
export const Fragment = reactFragment;
|
||||
export const jsx = reactJsx;
|
||||
export const jsxs = reactJsxs;
|
||||
|
||||
throw new Error('Do not use this transform for production builds. It is only meant for development.');
|
||||
1
backend/src/main/frontend/generated/layouts.json
Normal file
1
backend/src/main/frontend/generated/layouts.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
19
backend/src/main/frontend/generated/routes.tsx
Normal file
19
backend/src/main/frontend/generated/routes.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/******************************************************************************
|
||||
* This file is auto-generated by Vaadin.
|
||||
* It configures React Router automatically by adding server-side (Flow) routes,
|
||||
* which is enough for Vaadin Flow applications.
|
||||
* Once any `.tsx` or `.jsx` React routes are added into
|
||||
* `src/main/frontend/views/` directory, this route configuration is
|
||||
* re-generated automatically by Vaadin.
|
||||
******************************************************************************/
|
||||
import { createBrowserRouter, RouteObject } from 'react-router';
|
||||
import { serverSideRoutes } from 'Frontend/generated/flow/Flow';
|
||||
|
||||
function build() {
|
||||
const routes = [...serverSideRoutes] as RouteObject[];
|
||||
return {
|
||||
router: createBrowserRouter([...routes], { basename: new URL(document.baseURI).pathname }),
|
||||
routes
|
||||
};
|
||||
}
|
||||
export const { router, routes } = build()
|
||||
21
backend/src/main/frontend/generated/vaadin-featureflags.js
Normal file
21
backend/src/main/frontend/generated/vaadin-featureflags.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @ts-nocheck
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.featureFlags = window.Vaadin.featureFlags || {};
|
||||
if (Object.keys(window.Vaadin.featureFlags).length === 0) {
|
||||
window.Vaadin.featureFlags.collaborationEngineBackend = false;
|
||||
window.Vaadin.featureFlags.flowFullstackSignals = false;
|
||||
window.Vaadin.featureFlags.accessibleDisabledButtons = false;
|
||||
window.Vaadin.featureFlags.themeComponentStyles = false;
|
||||
window.Vaadin.featureFlags.copilotExperimentalFeatures = false;
|
||||
window.Vaadin.featureFlags.tailwindCss = false;
|
||||
window.Vaadin.featureFlags.fullstackSignals = false;
|
||||
window.Vaadin.featureFlags.masterDetailLayoutComponent = false;
|
||||
window.Vaadin.featureFlags.layoutComponentImprovements = false;
|
||||
window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout = false;
|
||||
};
|
||||
if (window.Vaadin.featureFlagsUpdaters) {
|
||||
const activator = (id) => window.Vaadin.featureFlags[id] = true;
|
||||
window.Vaadin.featureFlagsUpdaters.forEach(updater => updater(activator));
|
||||
delete window.Vaadin.featureFlagsUpdaters;
|
||||
}
|
||||
export {};
|
||||
18
backend/src/main/frontend/generated/vaadin-react.tsx
Normal file
18
backend/src/main/frontend/generated/vaadin-react.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { routes } from "Frontend/generated/routes.js";
|
||||
import { registerGlobalClickHandler } from "Frontend/generated/flow/Flow.js";
|
||||
|
||||
(window as any).Vaadin ??= {};
|
||||
(window as any).Vaadin.routesConfig = routes;
|
||||
registerGlobalClickHandler();
|
||||
|
||||
export { routes as forHMROnly };
|
||||
|
||||
// @ts-ignore
|
||||
if (import.meta.hot) {
|
||||
// @ts-ignore
|
||||
import.meta.hot.accept((module) => {
|
||||
if (module?.forHMROnly) {
|
||||
(window as any).Vaadin.routesConfig = module.forHMROnly;
|
||||
}
|
||||
});
|
||||
}
|
||||
9
backend/src/main/frontend/generated/vaadin.ts
Normal file
9
backend/src/main/frontend/generated/vaadin.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import './vaadin-featureflags.js';
|
||||
|
||||
import './index';
|
||||
|
||||
import './vaadin-react.js';
|
||||
import './app-shell-imports.js';
|
||||
import './css.generated.js';
|
||||
import { applyCss } from './css.generated.js';
|
||||
applyCss(document);
|
||||
23
backend/src/main/frontend/index.html
Normal file
23
backend/src/main/frontend/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<style>
|
||||
html, body, #outlet {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.assecutor.hha.backend;
|
||||
|
||||
import com.vaadin.flow.component.dependency.StyleSheet;
|
||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
||||
import com.vaadin.flow.theme.aura.Aura;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@StyleSheet(Aura.STYLESHEET)
|
||||
@StyleSheet("styles.css")
|
||||
public class HhaBackendApplication implements AppShellConfigurator {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HhaBackendApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.assecutor.hha.backend.api;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class StatusController {
|
||||
|
||||
@GetMapping("/status")
|
||||
public BackendStatus status() {
|
||||
return new BackendStatus(
|
||||
"hha-backend",
|
||||
"UP",
|
||||
"HHA Backoffice",
|
||||
"Vaadin UI und REST API verfuegbar",
|
||||
List.of("app", "backend", "vaadin-ui"),
|
||||
Instant.now(),
|
||||
Runtime.version().toString()
|
||||
);
|
||||
}
|
||||
|
||||
public record BackendStatus(
|
||||
String application,
|
||||
String status,
|
||||
String ui,
|
||||
String message,
|
||||
List<String> modules,
|
||||
Instant timestamp,
|
||||
String javaVersion
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package de.assecutor.hha.backend.views;
|
||||
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
@PageTitle("Dashboard | HHA Backoffice")
|
||||
@Route(value = "", layout = MainLayout.class)
|
||||
public class DashboardView extends Div {
|
||||
|
||||
public DashboardView() {
|
||||
addClassName("content-view");
|
||||
add(
|
||||
createHero(),
|
||||
createSectionTitle("Module im Repository"),
|
||||
createModuleGrid(),
|
||||
createSectionTitle("Direkt verfuegbare Endpunkte"),
|
||||
createEndpointGrid()
|
||||
);
|
||||
}
|
||||
|
||||
private Div createHero() {
|
||||
Span eyebrow = new Span("Startpunkt");
|
||||
eyebrow.addClassName("eyebrow");
|
||||
|
||||
H2 title = new H2("Vaadin-Weboberflaeche und Spring-Boot-Backend sind jetzt Teil desselben Repositories.");
|
||||
title.addClassName("hero-title");
|
||||
|
||||
Paragraph copy = new Paragraph(
|
||||
"Das Web-Modul lebt isoliert unter /backend, kann separat gebaut werden und bildet die Grundlage fuer "
|
||||
+ "ein Backoffice, Monitoring-Ansichten oder spaetere APIs fuer die Flutter-App in /app.");
|
||||
copy.addClassName("hero-copy");
|
||||
|
||||
Anchor apiLink = createExternalLink("/api/status", "API Status");
|
||||
Anchor healthLink = createExternalLink("/actuator/health", "Actuator Health");
|
||||
|
||||
Div actions = new Div(apiLink, healthLink);
|
||||
actions.addClassName("action-row");
|
||||
|
||||
Div hero = new Div(eyebrow, title, copy, actions);
|
||||
hero.addClassName("hero-card");
|
||||
return hero;
|
||||
}
|
||||
|
||||
private Div createModuleGrid() {
|
||||
Div grid = new Div(
|
||||
createInfoCard("Flutter App", "Mobile Client",
|
||||
"Die bestehende Flutter-Anwendung liegt jetzt gekapselt im Modul /app."),
|
||||
createInfoCard("Spring Boot", "Backend Runtime",
|
||||
"Boot 4.0.3 liefert den Server, REST-Endpunkte und die Anwendungskonfiguration."),
|
||||
createInfoCard("Vaadin", "Web UI",
|
||||
"Vaadin 25.0.7 stellt die serverseitige Admin- und Backoffice-Oberflaeche bereit.")
|
||||
);
|
||||
grid.addClassName("card-grid");
|
||||
return grid;
|
||||
}
|
||||
|
||||
private Div createEndpointGrid() {
|
||||
Div grid = new Div(
|
||||
createEndpointCard("/api/status", "JSON-Status fuer externe Clients"),
|
||||
createEndpointCard("/actuator/health", "Health-Endpunkt fuer Betrieb und Monitoring"),
|
||||
createEndpointCard("/", "Vaadin-Startseite fuer das Web-Backoffice")
|
||||
);
|
||||
grid.addClassName("card-grid");
|
||||
return grid;
|
||||
}
|
||||
|
||||
private Div createSectionTitle(String text) {
|
||||
H3 title = new H3(text);
|
||||
title.addClassName("section-title");
|
||||
|
||||
Div wrapper = new Div(title);
|
||||
wrapper.addClassName("section-header");
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private Div createInfoCard(String label, String title, String body) {
|
||||
Span chip = new Span(label);
|
||||
chip.addClassName("pill");
|
||||
|
||||
H3 heading = new H3(title);
|
||||
heading.addClassName("card-title");
|
||||
|
||||
Paragraph copy = new Paragraph(body);
|
||||
copy.addClassName("card-copy");
|
||||
|
||||
Div card = new Div(chip, heading, copy);
|
||||
card.addClassName("info-card");
|
||||
return card;
|
||||
}
|
||||
|
||||
private Div createEndpointCard(String path, String description) {
|
||||
Anchor link = createExternalLink(path, path);
|
||||
link.addClassName("endpoint-link");
|
||||
|
||||
Paragraph copy = new Paragraph(description);
|
||||
copy.addClassName("card-copy");
|
||||
|
||||
Div card = new Div(link, copy);
|
||||
card.addClassName("info-card");
|
||||
return card;
|
||||
}
|
||||
|
||||
private Anchor createExternalLink(String href, String text) {
|
||||
Anchor anchor = new Anchor(href, text);
|
||||
anchor.getElement().setAttribute("router-ignore", true);
|
||||
anchor.setTarget("_blank");
|
||||
anchor.addClassName("action-link");
|
||||
return anchor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package de.assecutor.hha.backend.views;
|
||||
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Pre;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
@PageTitle("Integration | HHA Backoffice")
|
||||
@Route(value = "integration", layout = MainLayout.class)
|
||||
public class IntegrationView extends Div {
|
||||
|
||||
public IntegrationView() {
|
||||
addClassName("content-view");
|
||||
add(
|
||||
createIntro(),
|
||||
createRepositoryCard(),
|
||||
createNextStepsCard()
|
||||
);
|
||||
}
|
||||
|
||||
private Div createIntro() {
|
||||
Span eyebrow = new Span("Struktur");
|
||||
eyebrow.addClassName("eyebrow");
|
||||
|
||||
H2 title = new H2("Das Repository ist jetzt fuer Mobile und Web vorbereitet.");
|
||||
title.addClassName("hero-title");
|
||||
|
||||
Paragraph copy = new Paragraph(
|
||||
"Das Flutter-Projekt lebt jetzt in /app. Das Web-Backoffice ist bewusst entkoppelt in /backend angelegt, "
|
||||
+ "damit Build, Runtime und Deployment separat steuerbar bleiben.");
|
||||
copy.addClassName("hero-copy");
|
||||
|
||||
Div intro = new Div(eyebrow, title, copy);
|
||||
intro.addClassName("hero-card");
|
||||
return intro;
|
||||
}
|
||||
|
||||
private Div createRepositoryCard() {
|
||||
H3 title = new H3("Projektlayout");
|
||||
title.addClassName("card-title");
|
||||
|
||||
Pre layout = new Pre("""
|
||||
HHA/
|
||||
|- app/
|
||||
| |- lib/ Flutter-App
|
||||
| |- android/ Flutter Android-Projekt
|
||||
| `- pubspec.yaml
|
||||
|- backend/
|
||||
| |- src/main/java/ Spring Boot + Vaadin
|
||||
| |- src/main/resources/
|
||||
| |- src/test/java/
|
||||
| |- pom.xml
|
||||
| |- mvnw
|
||||
| `- .mvn/
|
||||
`- README.md
|
||||
""");
|
||||
layout.addClassName("repo-tree");
|
||||
|
||||
Div card = new Div(title, layout);
|
||||
card.addClassName("wide-card");
|
||||
return card;
|
||||
}
|
||||
|
||||
private Div createNextStepsCard() {
|
||||
H3 title = new H3("Naechste sinnvolle Schritte");
|
||||
title.addClassName("card-title");
|
||||
|
||||
Paragraph first = new Paragraph("1. Fachliche REST-Endpunkte unter /api aufbauen und von Flutter konsumieren.");
|
||||
Paragraph second = new Paragraph("2. Vaadin-Views fuer Backoffice, Disposition oder Administration anlegen.");
|
||||
Paragraph third = new Paragraph("3. Build und Deployment fuer Mobile und Backend getrennt in CI/CD ausrollen.");
|
||||
|
||||
Anchor statusLink = createExternalLink("/api/status", "Status-Endpunkt pruefen");
|
||||
Anchor healthLink = createExternalLink("/actuator/health", "Health-Check pruefen");
|
||||
|
||||
Div actions = new Div(statusLink, healthLink);
|
||||
actions.addClassName("action-row");
|
||||
|
||||
Div card = new Div(title, first, second, third, actions);
|
||||
card.addClassName("wide-card");
|
||||
return card;
|
||||
}
|
||||
|
||||
private Anchor createExternalLink(String href, String text) {
|
||||
Anchor anchor = new Anchor(href, text);
|
||||
anchor.getElement().setAttribute("router-ignore", true);
|
||||
anchor.setTarget("_blank");
|
||||
anchor.addClassName("action-link");
|
||||
return anchor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.assecutor.hha.backend.views;
|
||||
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.sidenav.SideNav;
|
||||
import com.vaadin.flow.component.sidenav.SideNavItem;
|
||||
|
||||
public class MainLayout extends AppLayout {
|
||||
|
||||
public MainLayout() {
|
||||
addClassName("app-shell");
|
||||
setPrimarySection(Section.DRAWER);
|
||||
addToNavbar(createHeader());
|
||||
addToDrawer(createDrawerContent());
|
||||
}
|
||||
|
||||
private Div createHeader() {
|
||||
DrawerToggle toggle = new DrawerToggle();
|
||||
|
||||
H1 title = new H1("HHA Backoffice");
|
||||
title.addClassName("app-title");
|
||||
|
||||
Paragraph subtitle = new Paragraph("Spring Boot 4 und Vaadin 25 im selben Repository wie die Flutter-App.");
|
||||
subtitle.addClassName("app-subtitle");
|
||||
|
||||
Div brandBlock = new Div(title, subtitle);
|
||||
brandBlock.addClassName("brand-block");
|
||||
|
||||
Div header = new Div(toggle, brandBlock);
|
||||
header.addClassName("app-header");
|
||||
return header;
|
||||
}
|
||||
|
||||
private VerticalLayout createDrawerContent() {
|
||||
Span eyebrow = new Span("Shared Repository");
|
||||
eyebrow.addClassName("drawer-eyebrow");
|
||||
|
||||
H1 heading = new H1("Web- und Mobile-Stack");
|
||||
heading.addClassName("drawer-title");
|
||||
|
||||
Paragraph copy = new Paragraph(
|
||||
"Flutter lebt jetzt sauber im Modul /app. Das neue Backend liefert die Web-Oberflaeche und spaeter die API.");
|
||||
copy.addClassName("drawer-copy");
|
||||
|
||||
Div intro = new Div(eyebrow, heading, copy);
|
||||
intro.addClassName("drawer-intro");
|
||||
|
||||
SideNav navigation = new SideNav();
|
||||
navigation.addItem(new SideNavItem("Dashboard", DashboardView.class, VaadinIcon.DASHBOARD.create()));
|
||||
navigation.addItem(new SideNavItem("Integration", IntegrationView.class, VaadinIcon.CONNECT.create()));
|
||||
navigation.addClassName("drawer-nav");
|
||||
|
||||
Paragraph footer = new Paragraph("Module: /app und /backend");
|
||||
footer.addClassName("drawer-footer");
|
||||
|
||||
VerticalLayout drawer = new VerticalLayout(intro, navigation, footer);
|
||||
drawer.setPadding(false);
|
||||
drawer.setSpacing(false);
|
||||
drawer.setSizeFull();
|
||||
drawer.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||
drawer.addClassName("drawer-content");
|
||||
return drawer;
|
||||
}
|
||||
}
|
||||
205
backend/src/main/resources/META-INF/resources/styles.css
Normal file
205
backend/src/main/resources/META-INF/resources/styles.css
Normal file
@@ -0,0 +1,205 @@
|
||||
html {
|
||||
--aura-accent-color: #e3001b;
|
||||
--aura-background-color-light: #f4efe8;
|
||||
--aura-font-family: "IBM Plex Sans", "Aptos", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(227, 0, 27, 0.12), transparent 32%),
|
||||
linear-gradient(180deg, #f7f2eb 0%, #efe5d7 100%);
|
||||
color: #241d18;
|
||||
}
|
||||
|
||||
vaadin-app-layout::part(navbar) {
|
||||
background: rgba(255, 248, 241, 0.88);
|
||||
backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid rgba(36, 29, 24, 0.12);
|
||||
}
|
||||
|
||||
vaadin-app-layout::part(drawer) {
|
||||
background: linear-gradient(180deg, #fff7f1 0%, #f3e6d8 100%);
|
||||
border-right: 1px solid rgba(36, 29, 24, 0.1);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.app-title,
|
||||
.drawer-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-subtitle,
|
||||
.drawer-copy,
|
||||
.drawer-footer,
|
||||
.hero-copy,
|
||||
.card-copy {
|
||||
color: rgba(36, 29, 24, 0.76);
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.drawer-intro {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border: 1px solid rgba(36, 29, 24, 0.1);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow: 0 18px 45px rgba(118, 92, 63, 0.08);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.drawer-eyebrow,
|
||||
.eyebrow,
|
||||
.pill {
|
||||
align-items: center;
|
||||
background: rgba(227, 0, 27, 0.1);
|
||||
border-radius: 999px;
|
||||
color: #970015;
|
||||
display: inline-flex;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.35rem 0.7rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.drawer-nav {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.content-view {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
margin: 0 auto;
|
||||
max-width: 1100px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.info-card,
|
||||
.wide-card {
|
||||
background: rgba(255, 252, 248, 0.82);
|
||||
border: 1px solid rgba(36, 29, 24, 0.1);
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 22px 55px rgba(118, 92, 63, 0.1);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.wide-card {
|
||||
padding: 1.4rem;
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.card-title,
|
||||
.section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(1.8rem, 2vw + 1rem, 2.8rem);
|
||||
line-height: 1.1;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 300px;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
align-items: center;
|
||||
background: #241d18;
|
||||
border-radius: 999px;
|
||||
color: #fff8f1;
|
||||
display: inline-flex;
|
||||
font-weight: 700;
|
||||
padding: 0.8rem 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
background: #e3001b;
|
||||
}
|
||||
|
||||
.endpoint-link {
|
||||
font-family: "SF Mono", "Cascadia Code", monospace;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.repo-tree {
|
||||
background: #1f1b18;
|
||||
border-radius: 1rem;
|
||||
color: #fff4e7;
|
||||
line-height: 1.5;
|
||||
margin: 1rem 0 0;
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.content-view {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.info-card,
|
||||
.wide-card {
|
||||
border-radius: 1.15rem;
|
||||
}
|
||||
}
|
||||
10
backend/src/main/resources/application.properties
Normal file
10
backend/src/main/resources/application.properties
Normal file
@@ -0,0 +1,10 @@
|
||||
spring.application.name=hha-backend
|
||||
vaadin.launch-browser=false
|
||||
|
||||
management.endpoints.web.exposure.include=health,info
|
||||
management.endpoint.health.show-details=always
|
||||
|
||||
info.app.name=HHA Backend
|
||||
info.app.description=Spring Boot und Vaadin Backoffice im gemeinsamen Repository mit der Flutter-App
|
||||
info.app.ui=Vaadin 25
|
||||
info.app.mobile=Flutter im Modul app
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.assecutor.hha.backend;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
class HhaBackendApplicationTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void statusEndpointReturnsOperationalPayload() throws Exception {
|
||||
mockMvc.perform(get("/api/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.application").value("hha-backend"))
|
||||
.andExpect(jsonPath("$.status").value("UP"))
|
||||
.andExpect(jsonPath("$.ui").value("HHA Backoffice"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user