first commit

This commit is contained in:
2026-03-24 15:03:35 +01:00
commit cdba16ebe8
162 changed files with 194406 additions and 0 deletions

34
backend/.gitignore vendored Normal file
View 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/

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

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1 @@
export declare const applyCss: (target: Node) => void;

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

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

View File

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

View File

@@ -0,0 +1 @@
export {}

View File

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

View File

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

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

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

View 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

View File

@@ -0,0 +1 @@
export const init: (appInitResponse: any) => void;

View File

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

View File

@@ -0,0 +1 @@
export const init: () => void;

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
// Full cdn version: 25.0.7-undefined

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

View File

@@ -0,0 +1,5 @@
import { aB as a, aA as i } from "./copilot/copilot-BvIxHaRg.js";
export {
a as createChildrenDefinitions,
i as registerImporter
};

View File

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

View File

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

View File

@@ -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 youve encountered a hiccup, have questions, or ideas
to make our platform better, we're all ears! If you wish, leave your email, and well 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 youd like us to follow up, wed 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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
document.addEventListener('click', (event) => {
const target = event.composedPath().find((node) => node.hasAttribute && node.hasAttribute('disableonclick'));
if (target) {
target.disabled = true;
}
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export * from './copilot'
export {}

View File

@@ -0,0 +1,2 @@
export * from './Flow';
//# sourceMappingURL=index.js.map

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
import { createElement as reactCreateElement } from 'react';
export const createElement = reactCreateElement;

View File

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

View File

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

View File

@@ -0,0 +1 @@
[]

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

View File

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