Compare commits
60 Commits
6f31eee9b0
...
feature/TT
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2126efbc8d | ||
|
|
976d979f5c | ||
|
|
17e8998248 | ||
|
|
1cd4728eb4 | ||
|
|
3e0a0532c0 | ||
|
|
c9163753ac | ||
|
|
d05031f35e | ||
|
|
71b35a44a7 | ||
|
|
a58f9d9635 | ||
|
|
736630adec | ||
|
|
030f2d2155 | ||
|
|
0d2334da48 | ||
|
|
f24fef9b16 | ||
|
|
13d8970b7a | ||
|
|
eed815c646 | ||
|
|
5bacd33ea0 | ||
|
|
9bf604719a | ||
|
|
8dcd02c593 | ||
|
|
9efdacea9e | ||
|
|
587e2dbdf2 | ||
|
|
4f3a5d4d66 | ||
|
|
590babea9e | ||
|
|
4e9fd80296 | ||
|
|
eb60e5af16 | ||
|
|
eb02ef4490 | ||
|
|
3c705b6ad8 | ||
|
|
464d644c4f | ||
|
|
c04e4f85fc | ||
|
|
d3c41b3e6c | ||
|
|
b1202577a8 | ||
|
|
39af511bbd | ||
|
|
32dfb0d6b4 | ||
|
|
e70a559f1d | ||
|
|
bbf3e4a20e | ||
|
|
64abe911ee | ||
|
|
d3265d364c | ||
|
|
8be2920952 | ||
|
|
98db0678b4 | ||
|
|
f88274be3d | ||
|
|
6d56458bb9 | ||
|
|
daf43b7ce2 | ||
| 6b72da2d1e | |||
|
|
0361bd7bed | ||
| 8435b976d6 | |||
| ac3993545e | |||
| c62127e24d | |||
| f36fd63a04 | |||
| 75ddbecc79 | |||
| 7de22a723e | |||
| a64d56bbd9 | |||
| 7446839a7b | |||
|
|
2c7dd8c8f2 | ||
|
|
44672876be | ||
|
|
e828e83991 | ||
|
|
6b57a0c5e5 | ||
|
|
0f005ca130 | ||
|
|
acb96dc1c6 | ||
|
|
171f71209b | ||
|
|
94931bd7e0 | ||
|
|
9e4b0de915 |
61
.gitea/workflows/buildAndDeply.yaml
Normal file
61
.gitea/workflows/buildAndDeply.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Build-und-Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ci-java21-node20:latest
|
||||
|
||||
steps:
|
||||
- name: Map gitea into /etc/hosts (ohne ip)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gw_hex="$(awk '$2=="00000000" {print $3; exit}' /proc/net/route || true)"
|
||||
if [ -n "${gw_hex:-}" ]; then
|
||||
gw="$(printf "%d.%d.%d.%d" 0x${gw_hex:6:2} 0x${gw_hex:4:2} 0x${gw_hex:2:2} 0x${gw_hex:0:2})"
|
||||
else
|
||||
gw="172.17.0.1"
|
||||
fi
|
||||
echo "$gw gitea" >> /etc/hosts
|
||||
getent hosts gitea || true
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🧰 Gradle vorbereiten & Version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
test -f ./gradlew || { echo "gradlew fehlt – bitte lokal 'gradle wrapper' ausführen und committen."; exit 1; }
|
||||
chmod +x ./gradlew
|
||||
./gradlew --version
|
||||
|
||||
- name: 🏗️ Build Backend + Vaadin (Production)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
# Vaadin im Production-Mode bündeln
|
||||
./gradlew clean build -x test --no-daemon -Pvaadin.productionMode
|
||||
ls -al build/libs
|
||||
JAR="$(ls -1 build/libs/*.jar | head -n1)"
|
||||
echo "Gebautes JAR: $JAR"
|
||||
# kurze Sichtprüfung, dass Frontend drin ist
|
||||
jar tf "$JAR" | grep -E 'VAADIN|static/|index\.html' | head || true
|
||||
|
||||
- name: 📦 Nach /shared_jar_data deployen (fester Name + Archiv)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
ts="$(date +'%Y%m%d-%H%M%S')"
|
||||
mkdir -p /shared_jar_data /shared_jar_data/archive
|
||||
SRC="$(ls -1 build/libs/*.jar | head -n1)"
|
||||
# WICHTIG: Fester Name, den docker-compose startet
|
||||
cp -f "$SRC" "/shared_jar_data/MyTimeTracker-0.0.1-SNAPSHOT.jar"
|
||||
# Zusätzlich archivieren
|
||||
cp -f "$SRC" "/shared_jar_data/archive/MyTimeTracker-${ts}.jar"
|
||||
ls -al /shared_jar_data | tail -n +1
|
||||
14
.gitea/workflows/restart.yaml
Normal file
14
.gitea/workflows/restart.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Docker Restart Manuell
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
restart-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Docker Compose Restart
|
||||
run: |
|
||||
cd /home/docker/apps/mytimetracker
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -91,4 +91,6 @@ coverage/
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.tmp
|
||||
*.tmpdata/*.mv.db
|
||||
src/main/frontend/generated/
|
||||
/data/*.mv.db
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,14 +1,5 @@
|
||||
# 1. Verwende Java 17 JDK als Base Image
|
||||
FROM eclipse-temurin:17-jdk-alpine
|
||||
|
||||
# 2. Arbeitsverzeichnis
|
||||
WORKDIR /app
|
||||
|
||||
# 3. Kopiere das JAR (Pfad siehe unten)
|
||||
COPY build/libs/MyTimeTracker-0.0.1-SNAPSHOT.jar app.jar
|
||||
|
||||
# 4. Port freigeben
|
||||
COPY --chown=appuser:appuser build/libs/MyTimeTracker-0.0.1-SNAPSHOT.jar app.jar
|
||||
EXPOSE 8080
|
||||
|
||||
# 5. Startbefehl
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@@ -10,7 +10,7 @@ version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ configurations {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://maven.vaadin.com/vaadin-addons' }
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -32,6 +33,7 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'com.vaadin:vaadin-spring-boot-starter'
|
||||
implementation("org.vaadin.crudui:crudui:7.2.0")
|
||||
|
||||
runtimeOnly 'com.mysql:mysql-connector-j:8.3.0'
|
||||
|
||||
|
||||
13
compose.yml
13
compose.yml
@@ -18,15 +18,18 @@ services:
|
||||
- traefik
|
||||
|
||||
app:
|
||||
build: .
|
||||
image: mytimetracker-app
|
||||
image: eclipse-temurin:17-jdk-alpine
|
||||
container_name: mytimetracker-app
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8400:8080" # 8400 auf dem Host, 8080 im Container
|
||||
working_dir: /app
|
||||
command: java -jar /app/MyTimeTracker-0.0.1-SNAPSHOT.jar
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
volumes:
|
||||
- shared_jar_data:/app:ro # ⬅️ Direkt ins /app mounten
|
||||
ports:
|
||||
- "8400:8080"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.mytimetracker.rule=Host(`timetracker.nilzbu.de`)"
|
||||
@@ -40,6 +43,8 @@ services:
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
shared_jar_data:
|
||||
external: true
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -280,7 +280,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -482,7 +482,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -684,7 +684,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -886,7 +886,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'MyTimeTracker'
|
||||
|
||||
@@ -1,684 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2025 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;
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
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;
|
||||
navigate(navigateArgs.to, navigateArgs.opts);
|
||||
setNavigateQueueLength(navigateQueue.length);
|
||||
};
|
||||
blockingNavigate();
|
||||
}, [navigate, setNavigateQueueLength]);
|
||||
|
||||
const dequeueNavigationAfterCurrentTask = useCallback(() => {
|
||||
queueMicrotask(dequeueNavigation);
|
||||
}, [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;
|
||||
}
|
||||
|
||||
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;
|
||||
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(() => {
|
||||
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;
|
||||
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();
|
||||
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();
|
||||
navigated.current = false;
|
||||
},
|
||||
redirect,
|
||||
continue() {
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
}
|
||||
},
|
||||
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();
|
||||
blockingPromise.resolve();
|
||||
} else {
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// permitted navigation: proceed with the blocker
|
||||
blocker.proceed();
|
||||
blockingPromise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2025 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: '24.8.1'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'Frontend/generated/jar-resources/flow-component-renderer.js';
|
||||
import '@vaadin/polymer-legacy-adapter/style-modules.js';
|
||||
import '@vaadin/number-field/theme/lumo/vaadin-number-field.js';
|
||||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box.js';
|
||||
import 'Frontend/generated/jar-resources/comboBoxConnector.js';
|
||||
import 'Frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-column.js';
|
||||
import '@vaadin/text-field/theme/lumo/vaadin-text-field.js';
|
||||
import '@vaadin/date-picker/theme/lumo/vaadin-date-picker.js';
|
||||
import 'Frontend/generated/jar-resources/datepickerConnector.js';
|
||||
import '@vaadin/text-area/theme/lumo/vaadin-text-area.js';
|
||||
import '@vaadin/password-field/theme/lumo/vaadin-password-field.js';
|
||||
import '@vaadin/vertical-layout/theme/lumo/vaadin-vertical-layout.js';
|
||||
import '@vaadin/app-layout/theme/lumo/vaadin-app-layout.js';
|
||||
import '@vaadin/tooltip/theme/lumo/vaadin-tooltip.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tab.js';
|
||||
import '@vaadin/context-menu/theme/lumo/vaadin-context-menu.js';
|
||||
import 'Frontend/generated/jar-resources/contextMenuConnector.js';
|
||||
import 'Frontend/generated/jar-resources/contextMenuTargetConnector.js';
|
||||
import '@vaadin/horizontal-layout/theme/lumo/vaadin-horizontal-layout.js';
|
||||
import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tabs.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-sorter.js';
|
||||
import '@vaadin/checkbox/theme/lumo/vaadin-checkbox.js';
|
||||
import 'Frontend/generated/jar-resources/gridConnector.ts';
|
||||
import '@vaadin/button/theme/lumo/vaadin-button.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-column-group.js';
|
||||
import 'Frontend/generated/jar-resources/lit-renderer.ts';
|
||||
import '@vaadin/time-picker/theme/lumo/vaadin-time-picker.js';
|
||||
import 'Frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js';
|
||||
import '@vaadin/notification/theme/lumo/vaadin-notification.js';
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'Frontend/generated/jar-resources/flow-component-renderer.js';
|
||||
import '@vaadin/polymer-legacy-adapter/style-modules.js';
|
||||
import '@vaadin/number-field/theme/lumo/vaadin-number-field.js';
|
||||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box.js';
|
||||
import 'Frontend/generated/jar-resources/comboBoxConnector.js';
|
||||
import 'Frontend/generated/jar-resources/vaadin-grid-flow-selection-column.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-column.js';
|
||||
import '@vaadin/date-picker/theme/lumo/vaadin-date-picker.js';
|
||||
import 'Frontend/generated/jar-resources/datepickerConnector.js';
|
||||
import '@vaadin/text-area/theme/lumo/vaadin-text-area.js';
|
||||
import '@vaadin/vertical-layout/theme/lumo/vaadin-vertical-layout.js';
|
||||
import '@vaadin/app-layout/theme/lumo/vaadin-app-layout.js';
|
||||
import '@vaadin/tooltip/theme/lumo/vaadin-tooltip.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tab.js';
|
||||
import '@vaadin/context-menu/theme/lumo/vaadin-context-menu.js';
|
||||
import 'Frontend/generated/jar-resources/contextMenuConnector.js';
|
||||
import 'Frontend/generated/jar-resources/contextMenuTargetConnector.js';
|
||||
import '@vaadin/horizontal-layout/theme/lumo/vaadin-horizontal-layout.js';
|
||||
import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tabs.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-sorter.js';
|
||||
import '@vaadin/checkbox/theme/lumo/vaadin-checkbox.js';
|
||||
import 'Frontend/generated/jar-resources/gridConnector.ts';
|
||||
import '@vaadin/button/theme/lumo/vaadin-button.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/grid/theme/lumo/vaadin-grid-column-group.js';
|
||||
import 'Frontend/generated/jar-resources/lit-renderer.ts';
|
||||
import '@vaadin/time-picker/theme/lumo/vaadin-time-picker.js';
|
||||
import 'Frontend/generated/jar-resources/vaadin-time-picker/timepickerConnector.js';
|
||||
import '@vaadin/notification/theme/lumo/vaadin-notification.js';
|
||||
@@ -1 +0,0 @@
|
||||
export {}
|
||||
@@ -1,42 +0,0 @@
|
||||
import '@vaadin/polymer-legacy-adapter/style-modules.js';
|
||||
import '@vaadin/login/theme/lumo/vaadin-login-form.js';
|
||||
import '@vaadin/vertical-layout/theme/lumo/vaadin-vertical-layout.js';
|
||||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box.js';
|
||||
import 'Frontend/generated/jar-resources/flow-component-renderer.js';
|
||||
import 'Frontend/generated/jar-resources/comboBoxConnector.js';
|
||||
import '@vaadin/app-layout/theme/lumo/vaadin-app-layout.js';
|
||||
import '@vaadin/tooltip/theme/lumo/vaadin-tooltip.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tab.js';
|
||||
import '@vaadin/horizontal-layout/theme/lumo/vaadin-horizontal-layout.js';
|
||||
import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tabs.js';
|
||||
import '@vaadin/button/theme/lumo/vaadin-button.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||
import '@vaadin/vaadin-lumo-styles/color-global.js';
|
||||
import '@vaadin/vaadin-lumo-styles/typography-global.js';
|
||||
import '@vaadin/vaadin-lumo-styles/sizing.js';
|
||||
import '@vaadin/vaadin-lumo-styles/spacing.js';
|
||||
import '@vaadin/vaadin-lumo-styles/style.js';
|
||||
import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js';
|
||||
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||
|
||||
const loadOnDemand = (key) => {
|
||||
const pending = [];
|
||||
if (key === 'bdac0ff193c2b1c5a8ca68adcb7a82390eba20425e77efae19dff192803f620a') {
|
||||
pending.push(import('./chunks/chunk-7fe4f81785dc07525e79bd982a7f6ea301bc5f05b6211e6fb8dda240b04e25c0.js'));
|
||||
}
|
||||
if (key === '64c3805e387d62a38c42d3f30bcd0fc30b54a7b060f9d022fb9506aa661b88a9') {
|
||||
pending.push(import('./chunks/chunk-331a5131a204ee57aa89c6af6e3a559fbd60a4896c0f4a8ee5484ffc9183fc26.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;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';
|
||||
|
||||
import '@vaadin/polymer-legacy-adapter/style-modules.js';
|
||||
import '@vaadin/login/theme/lumo/vaadin-login-form.js';
|
||||
import '@vaadin/vertical-layout/theme/lumo/vaadin-vertical-layout.js';
|
||||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box.js';
|
||||
import 'Frontend/generated/jar-resources/flow-component-renderer.js';
|
||||
import 'Frontend/generated/jar-resources/comboBoxConnector.js';
|
||||
import '@vaadin/app-layout/theme/lumo/vaadin-app-layout.js';
|
||||
import '@vaadin/tooltip/theme/lumo/vaadin-tooltip.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tab.js';
|
||||
import '@vaadin/horizontal-layout/theme/lumo/vaadin-horizontal-layout.js';
|
||||
import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
|
||||
import '@vaadin/tabs/theme/lumo/vaadin-tabs.js';
|
||||
import '@vaadin/button/theme/lumo/vaadin-button.js';
|
||||
import 'Frontend/generated/jar-resources/disableOnClickFunctions.js';
|
||||
import '@vaadin/common-frontend/ConnectionIndicator.js';
|
||||
import '@vaadin/vaadin-lumo-styles/sizing.js';
|
||||
import '@vaadin/vaadin-lumo-styles/spacing.js';
|
||||
import '@vaadin/vaadin-lumo-styles/style.js';
|
||||
import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js';
|
||||
import 'Frontend/generated/jar-resources/ReactRouterOutletElement.tsx';
|
||||
|
||||
const loadOnDemand = (key) => {
|
||||
const pending = [];
|
||||
if (key === 'bdac0ff193c2b1c5a8ca68adcb7a82390eba20425e77efae19dff192803f620a') {
|
||||
pending.push(import('./chunks/chunk-7fe4f81785dc07525e79bd982a7f6ea301bc5f05b6211e6fb8dda240b04e25c0.js'));
|
||||
}
|
||||
if (key === '64c3805e387d62a38c42d3f30bcd0fc30b54a7b060f9d022fb9506aa661b88a9') {
|
||||
pending.push(import('./chunks/chunk-331a5131a204ee57aa89c6af6e3a559fbd60a4896c0f4a8ee5484ffc9183fc26.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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/******************************************************************************
|
||||
* 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));
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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 addConnectionIndicator;
|
||||
private offlineStubAction;
|
||||
private isFlowClientLoaded;
|
||||
}
|
||||
export {};
|
||||
@@ -1,394 +0,0 @@
|
||||
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));
|
||||
}
|
||||
/**
|
||||
* Client API for flow UI operations.
|
||||
*/
|
||||
export class Flow {
|
||||
constructor(config) {
|
||||
this.response = undefined;
|
||||
this.pathname = '';
|
||||
// flag used to inform Testbench whether a server route is in progress
|
||||
this.isActive = false;
|
||||
this.baseRegex = /^\//;
|
||||
this.navigation = '';
|
||||
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
|
||||
}
|
||||
};
|
||||
// Regular expression used to remove the app-context
|
||||
const elm = document.head.querySelector('base');
|
||||
this.baseRegex = new RegExp(`^${
|
||||
// IE11 does not support document.baseURI
|
||||
(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) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (_e.target.hasAttribute('router-link')) {
|
||||
this.navigation = 'link';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
}
|
||||
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) => {
|
||||
var _a;
|
||||
resolve(cmd && cancel ? cmd.prevent() : (_a = cmd === null || cmd === void 0 ? void 0 : cmd.continue) === null || _a === void 0 ? void 0 : _a.call(cmd));
|
||||
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) => {
|
||||
var _a;
|
||||
if (cmd && cancel) {
|
||||
resolve(cmd.prevent());
|
||||
}
|
||||
else if (cmd && cmd.redirect && redirectContext) {
|
||||
resolve(cmd.redirect(redirectContext.pathname));
|
||||
}
|
||||
else {
|
||||
(_a = cmd === null || cmd === void 0 ? void 0 : cmd.continue) === null || _a === void 0 ? void 0 : _a.call(cmd);
|
||||
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;
|
||||
const requestPath = `?v-r=init&location=${encodeURIComponent(this.getFlowRoutePath(location))}&query=${encodeURIComponent(this.getFlowRouteQuery(location))}`;
|
||||
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();
|
||||
});
|
||||
}
|
||||
// Create shared connection state store and connection indicator
|
||||
addConnectionIndicator() {
|
||||
// add connection indicator to DOM
|
||||
ConnectionIndicator.create();
|
||||
// Listen to browser online/offline events and update the loading indicator accordingly.
|
||||
// Note: if flow-client is loaded, it instead handles the state transitions.
|
||||
$wnd.addEventListener('online', () => {
|
||||
if (!this.isFlowClientLoaded()) {
|
||||
// Send an HTTP HEAD request for sw.js to verify server reachability.
|
||||
// We do not expect sw.js to be cached, so the request goes to the
|
||||
// server rather than being served from local cache.
|
||||
// Require network-level failure to revert the state to CONNECTION_LOST
|
||||
// (HTTP error code is ok since it still verifies server's presence).
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.RECONNECTING;
|
||||
const http = new XMLHttpRequest();
|
||||
http.open('HEAD', 'sw.js');
|
||||
http.onload = () => {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTED;
|
||||
};
|
||||
http.onerror = () => {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||
};
|
||||
// Postpone request to reduce potential net::ERR_INTERNET_DISCONNECTED
|
||||
// errors that sometimes occurs even if browser says it is online
|
||||
setTimeout(() => http.send(), 50);
|
||||
}
|
||||
});
|
||||
$wnd.addEventListener('offline', () => {
|
||||
if (!this.isFlowClientLoaded()) {
|
||||
$wnd.Vaadin.connectionState.state = ConnectionState.CONNECTION_LOST;
|
||||
}
|
||||
});
|
||||
}
|
||||
async offlineStubAction() {
|
||||
const offlineStub = document.createElement('iframe');
|
||||
const offlineStubPath = './offline-stub.html';
|
||||
offlineStub.setAttribute('src', offlineStubPath);
|
||||
offlineStub.setAttribute('style', 'width: 100%; height: 100%; border: 0');
|
||||
this.response = undefined;
|
||||
let onlineListener;
|
||||
const removeOfflineStubAndOnlineListener = () => {
|
||||
if (onlineListener !== undefined) {
|
||||
$wnd.Vaadin.connectionState.removeStateChangeListener(onlineListener);
|
||||
onlineListener = undefined;
|
||||
}
|
||||
};
|
||||
offlineStub.onBeforeEnter = (ctx, _cmds, router) => {
|
||||
onlineListener = () => {
|
||||
if ($wnd.Vaadin.connectionState.online) {
|
||||
removeOfflineStubAndOnlineListener();
|
||||
router.render(ctx, false);
|
||||
}
|
||||
};
|
||||
$wnd.Vaadin.connectionState.addStateChangeListener(onlineListener);
|
||||
};
|
||||
offlineStub.onBeforeLeave = (_ctx, _cmds, _router) => {
|
||||
removeOfflineStubAndOnlineListener();
|
||||
};
|
||||
return offlineStub;
|
||||
}
|
||||
isFlowClientLoaded() {
|
||||
return this.response !== undefined;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=Flow.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
export const init: (appInitResponse: any) => void;
|
||||
@@ -1,291 +0,0 @@
|
||||
/* 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;
|
||||
|
||||
if (!window.name) {
|
||||
window.name = appId + '-' + Math.random();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
window.Vaadin.Flow.getBrowserDetailsParameters = function () {
|
||||
var params = {};
|
||||
|
||||
/* Screen height and width */
|
||||
params['v-sh'] = window.screen.height;
|
||||
params['v-sw'] = window.screen.width;
|
||||
/* Browser window dimensions */
|
||||
params['v-wh'] = window.innerHeight;
|
||||
params['v-ww'] = window.innerWidth;
|
||||
/* Body element dimensions */
|
||||
params['v-bh'] = document.body.clientHeight;
|
||||
params['v-bw'] = document.body.clientWidth;
|
||||
|
||||
/* Current time */
|
||||
var date = new Date();
|
||||
params['v-curdate'] = date.getTime();
|
||||
|
||||
/* Current timezone offset (including DST shift) */
|
||||
var 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 */
|
||||
var dstDiff = 0;
|
||||
var rawTzo = tzo1;
|
||||
for (var m = 12; m > 0; m--) {
|
||||
date.setUTCMonth(m);
|
||||
var 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 (window.name) {
|
||||
params['v-wn'] = window.name;
|
||||
}
|
||||
|
||||
/* Detect touch device support */
|
||||
var supportsTouch = false;
|
||||
try {
|
||||
document.createEvent('TouchEvent');
|
||||
supportsTouch = true;
|
||||
} catch (e) {
|
||||
/* Chrome and IE10 touch detection */
|
||||
supportsTouch = 'ontouchstart' in window || typeof navigator.msMaxTouchPoints !== 'undefined';
|
||||
}
|
||||
params['v-td'] = supportsTouch;
|
||||
|
||||
/* Device Pixel Ratio */
|
||||
params['v-pr'] = window.devicePixelRatio;
|
||||
|
||||
if (navigator.platform) {
|
||||
params['v-np'] = navigator.platform;
|
||||
}
|
||||
|
||||
/* Stringify each value (they are parsed on the server side) */
|
||||
Object.keys(params).forEach(function (key) {
|
||||
var value = params[key];
|
||||
if (typeof value !== 'undefined') {
|
||||
params[key] = value.toString();
|
||||
}
|
||||
});
|
||||
return params;
|
||||
};
|
||||
}
|
||||
|
||||
log('Flow bootstrap loaded');
|
||||
if (appInitResponse.appConfig.productionMode && typeof window.__gwtStatsEvent != 'function') {
|
||||
window.Vaadin.Flow.gwtStatsEvents = [];
|
||||
window.__gwtStatsEvent = function (event) {
|
||||
window.Vaadin.Flow.gwtStatsEvents.push(event);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
var config = appInitResponse.appConfig;
|
||||
var mode = appInitResponse.appConfig.productionMode;
|
||||
window.Vaadin.Flow.initApplication(config.appId, config);
|
||||
};
|
||||
|
||||
export { init };
|
||||
@@ -1 +0,0 @@
|
||||
export const init: () => void;
|
||||
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
@@ -1,277 +0,0 @@
|
||||
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.setRequestedRange(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;
|
||||
this._filterDebouncer = Debouncer.debounce(this._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.
|
||||
this._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 (this._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 () {
|
||||
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;
|
||||
@@ -1,122 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
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 };
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
export { _registerImporter } from './copilot/figma-public/figma-api';
|
||||
export type { FigmaNode } from './copilot/figma-public/figma-api';
|
||||
export type { ComponentDefinition, ComponentDefinitionProperties } from './copilot/shared/flow-utils';
|
||||
@@ -1,4 +0,0 @@
|
||||
import { as as o } from "./copilot/copilot-CP3-W7yE.js";
|
||||
export {
|
||||
o as _registerImporter
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import { v as t, j as n, b as a } from "./copilot-CP3-W7yE.js";
|
||||
class i extends t {
|
||||
constructor() {
|
||||
super(...arguments), this.eventBusRemovers = [], this.messageHandlers = {}, this.handleESC = (e) => {
|
||||
n.active && e.key === "Escape" && typeof this.close == "function" && this.close();
|
||||
};
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
onEventBus(e, s) {
|
||||
this.eventBusRemovers.push(a.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 {
|
||||
i as B
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,70 +0,0 @@
|
||||
import { j as d, F as r, a2 as c, am as g, af as f, an as u, U as h, M as m, ao as v, u as b } from "./copilot-CP3-W7yE.js";
|
||||
import { B as $ } from "./base-panel-Ckfoxxex.js";
|
||||
import { i as w } from "./icons-DVw-r69H.js";
|
||||
const x = "copilot-features-panel{padding:var(--space-100);font:var(--font-xsmall);display:grid;grid-template-columns:auto 1fr;gap:var(--space-50);height:auto}copilot-features-panel a{display:flex;align-items:center;gap:var(--space-50);white-space:nowrap}copilot-features-panel a svg{height:12px;width:12px;min-height:12px;min-width:12px}";
|
||||
var F = Object.getOwnPropertyDescriptor, y = (e, a, o, s) => {
|
||||
for (var t = s > 1 ? void 0 : s ? F(a, o) : a, l = e.length - 1, i; l >= 0; l--)
|
||||
(i = e[l]) && (t = i(t) || t);
|
||||
return t;
|
||||
};
|
||||
const n = window.Vaadin.devTools;
|
||||
let p = class extends $ {
|
||||
render() {
|
||||
return r` <style>
|
||||
${x}
|
||||
</style>
|
||||
${d.featureFlags.map(
|
||||
(e) => r`
|
||||
<copilot-toggle-button
|
||||
.title="${e.title}"
|
||||
?checked=${e.enabled}
|
||||
@on-change=${(a) => this.toggleFeatureFlag(a, e)}>
|
||||
</copilot-toggle-button>
|
||||
<a class="ahreflike" href="${e.moreInfoLink}" title="Learn more" target="_blank"
|
||||
>learn more ${w.share}</a
|
||||
>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
toggleFeatureFlag(e, a) {
|
||||
const o = e.target.checked;
|
||||
if (c("use-feature", { source: "toggle", enabled: o, id: a.id }), n.frontendConnection) {
|
||||
n.frontendConnection.send("setFeature", { featureId: a.id, enabled: o });
|
||||
let s;
|
||||
if (a.requiresServerRestart) {
|
||||
const t = "This feature requires a server restart";
|
||||
g() ? s = f(
|
||||
r`${t} <br />
|
||||
${u()}`
|
||||
) : s = t;
|
||||
}
|
||||
h({
|
||||
type: m.INFORMATION,
|
||||
message: `“${a.title}” ${o ? "enabled" : "disabled"}`,
|
||||
details: s,
|
||||
dismissId: `feature${a.id}${o ? "Enabled" : "Disabled"}`
|
||||
}), v();
|
||||
} else
|
||||
n.log("error", `Unable to toggle feature ${a.title}: No server connection available`);
|
||||
}
|
||||
};
|
||||
p = y([
|
||||
b("copilot-features-panel")
|
||||
], p);
|
||||
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"
|
||||
}, O = {
|
||||
init(e) {
|
||||
e.addPanel(I);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(O);
|
||||
export {
|
||||
p as CopilotFeaturesPanel
|
||||
};
|
||||
@@ -1,217 +0,0 @@
|
||||
import { F as c, j as d, b as h, x as g, a2 as u, s as v, P as f, H as m, u as k } from "./copilot-CP3-W7yE.js";
|
||||
import { r as s } from "./state-C3WY-pqX.js";
|
||||
import { m as w, e as $ } from "./overlay-monkeypatch-DEOgVcvl.js";
|
||||
import { B as x } from "./base-panel-Ckfoxxex.js";
|
||||
import { i as A } from "./icons-DVw-r69H.js";
|
||||
const P = "copilot-feedback-panel{display:flex;flex-direction:column;font:var(--font-xsmall);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-text-area,vaadin-email-field){padding:0}copilot-feedback-panel :is(vaadin-select,vaadin-text-area,vaadin-email-field)::part(input-field),copilot-feedback-panel vaadin-select-value-button{padding:0}copilot-feedback-panel vaadin-select::part(toggle-button){align-items:center;display:flex;height:var(--size-m);justify-content:center;width:var(--size-m)}copilot-feedback-panel vaadin-text-area textarea{line-height:var(--line-height-1);padding:calc((var(--size-m) - var(--line-height-1)) / 2) var(--space-100)}copilot-feedback-panel vaadin-text-area:hover::part(input-field){background:none}copilot-feedback-panel vaadin-email-field input{padding:0 var(--space-100)}copilot-feedback-panel>*::part(label){font-weight:var(--font-weight-medium);line-height:var(--line-height-1);margin:0;padding:0 var(--space-150) var(--space-50) 0}copilot-feedback-panel>*::part(helper-text){line-height:var(--line-height-1);margin:0}";
|
||||
var F = Object.defineProperty, T = Object.getOwnPropertyDescriptor, o = (e, t, n, l) => {
|
||||
for (var a = l > 1 ? void 0 : l ? T(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 && F(t, n, a), a;
|
||||
};
|
||||
const D = "https://github.com/vaadin", b = "https://github.com/vaadin/copilot/issues/new", E = "?template=feature_request.md&title=%5BFEATURE%5D", U = "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 x {
|
||||
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>
|
||||
${P}</style
|
||||
>${this.renderContent()}${this.renderFooter()}`;
|
||||
}
|
||||
firstUpdated() {
|
||||
w(this);
|
||||
}
|
||||
renderContent() {
|
||||
return this.message === void 0 ? c`
|
||||
<p>
|
||||
Your insights are incredibly valuable to us. Whether you’ve encountered a hiccup, have questions, or ideas
|
||||
to make our platform better, we're all ears! If you wish, leave your email, and we’ll get back to you. You
|
||||
can even share your code snippet with us for a clearer picture.
|
||||
</p>
|
||||
<vaadin-select
|
||||
.items="${this.types}"
|
||||
.value="${this.type}"
|
||||
overlay-class="alwaysVisible"
|
||||
@value-changed=${(e) => {
|
||||
this.type = e.detail.value;
|
||||
}}>
|
||||
</vaadin-select>
|
||||
<vaadin-select
|
||||
label="Feedback Topic"
|
||||
overlay-class="alwaysVisible"
|
||||
.items=${this.topics}
|
||||
.value="${this.topic}"
|
||||
.hidden=${this.type !== "feedback"}
|
||||
@value-changed=${(e) => {
|
||||
this.topic = e.detail.value;
|
||||
}}>
|
||||
</vaadin-select>
|
||||
<vaadin-text-area
|
||||
.value="${this.description}"
|
||||
@keydown=${this.keyDown}
|
||||
@focus=${() => {
|
||||
this.descriptionField.invalid = !1, this.descriptionField.placeholder = "";
|
||||
}}
|
||||
@value-changed=${(e) => {
|
||||
this.description = e.detail.value;
|
||||
}}
|
||||
label="Tell Us More"
|
||||
helper-text="Describe what you're experiencing, wondering about, or envisioning. The more you share, the better we can understand and act on your feedback"></vaadin-text-area>
|
||||
<vaadin-text-field
|
||||
@keydown=${this.keyDown}
|
||||
@value-changed=${(e) => {
|
||||
this.email = e.detail.value;
|
||||
}}
|
||||
.required=${this.type === "question"}
|
||||
id="email"
|
||||
value="${d.userInfo?.email}"
|
||||
label="Your Email${this.type === "question" ? "" : " (Optional)"}"
|
||||
helper-text="Leave your email if you’d like us to follow up, we’d love to keep the conversation going."></vaadin-text-field>
|
||||
` : c`<p>${this.message}</p>`;
|
||||
}
|
||||
renderFooter() {
|
||||
return this.message === void 0 ? c`
|
||||
<div class="dialog-footer">
|
||||
<button
|
||||
style="margin-inline-end: auto"
|
||||
@click="${() => {
|
||||
d.active ? h.emit("system-info-with-callback", {
|
||||
callback: (e) => this.openGithub(e, this),
|
||||
notify: !1
|
||||
}) : this.openGithub(null, this);
|
||||
}}">
|
||||
<span class="prefix">${A.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() {
|
||||
g.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}${E}`);
|
||||
return;
|
||||
}
|
||||
if (this.type === "feedback") {
|
||||
window.open(`${D}/${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 : U, 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([
|
||||
$("vaadin-text-area")
|
||||
], i.prototype, "descriptionField", 2);
|
||||
i = o([
|
||||
k("copilot-feedback-panel")
|
||||
], i);
|
||||
const y = m({
|
||||
header: "Help Us Improve!",
|
||||
tag: "copilot-feedback-panel",
|
||||
width: 500,
|
||||
height: 550,
|
||||
floatingPosition: {
|
||||
top: 100,
|
||||
left: 100
|
||||
},
|
||||
individual: !0
|
||||
}), q = {
|
||||
init(e) {
|
||||
e.addPanel(y);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(q);
|
||||
g.addPanel(y);
|
||||
export {
|
||||
i as CopilotFeedbackPanel
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,205 +0,0 @@
|
||||
import { ak as I, J as g, a2 as $, L as k, j as p, x as S, F as a, E, al as c, Q as C, W as V, U as P, M as D, v as A, b as H, u as w } from "./copilot-CP3-W7yE.js";
|
||||
import { r as x } from "./state-C3WY-pqX.js";
|
||||
import { B as T } from "./base-panel-Ckfoxxex.js";
|
||||
import { i as d } from "./icons-DVw-r69H.js";
|
||||
import { e as h, c as O } from "./early-project-state-DgrvrTky.js";
|
||||
const j = '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(--size-m);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(--secondary-text-color)}copilot-info-panel dl dd{align-items:center;display:flex;font-weight:var(--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(--font-size-0);background:var(--gray-50);padding:var(--space-75);border-radius:var(--radius-2)}';
|
||||
var J = Object.defineProperty, N = Object.getOwnPropertyDescriptor, v = (e, t, n, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? N(t, n) : t, s = e.length - 1, l; s >= 0; s--)
|
||||
(l = e[s]) && (o = (i ? l(t, n, o) : l(o)) || o);
|
||||
return i && o && J(t, n, o), o;
|
||||
};
|
||||
let u = class extends T {
|
||||
constructor() {
|
||||
super(...arguments), this.serverInfo = [], this.clientInfo = [{ name: "Browser", version: navigator.userAgent }], this.handleServerInfoEvent = (e) => {
|
||||
const t = JSON.parse(e.data.info);
|
||||
this.serverInfo = t.versions, I().then((n) => {
|
||||
n && (this.clientInfo.unshift({ name: "Vaadin Employee", version: "true", more: void 0 }), this.requestUpdate("clientInfo"));
|
||||
}), g() === "success" && $("hotswap-active", { value: k() });
|
||||
};
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.onCommand("copilot-info", this.handleServerInfoEvent), 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 this.serverInfo.findIndex((t) => t.name === e);
|
||||
}
|
||||
render() {
|
||||
const e = p.newVaadinVersionState?.versions !== void 0 && p.newVaadinVersionState.versions.length > 0, t = [...this.serverInfo, ...this.clientInfo];
|
||||
let n = this.getIndex("Spring") + 1;
|
||||
n === 0 && (n = t.length), h.springSecurityEnabled && (t.splice(n, 0, { name: "Spring Security", version: "true" }), n++), h.springJpaDataEnabled && (t.splice(n, 0, { name: "Spring Data JPA", version: "true" }), n++);
|
||||
const i = t.find((o) => o.name === "Vaadin");
|
||||
return i && (i.more = a` <button
|
||||
aria-label="Edit Vaadin Version"
|
||||
class="icon relative"
|
||||
id="new-vaadin-version-btn"
|
||||
title="Edit Vaadin Version"
|
||||
@click="${(o) => {
|
||||
o.stopPropagation(), S.updatePanel("copilot-vaadin-versions", { floating: !0 });
|
||||
}}">
|
||||
${d.editAlt}
|
||||
${e ? a`<span aria-hidden="true" class="absolute bg-error end-0 h-75 rounded-full top-0 w-75"></span>` : ""}
|
||||
</button>`), a` <style>
|
||||
${j}
|
||||
</style>
|
||||
<div class="flex flex-col gap-150 items-start">
|
||||
<dl>
|
||||
${t.map(
|
||||
(o) => a`
|
||||
<div>
|
||||
<dt>${o.name}</dt>
|
||||
<dd title="${o.version}">
|
||||
<span> ${this.renderValue(o.version)} </span>
|
||||
${o.more}
|
||||
</dd>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.renderDevWorkflowSection()}
|
||||
</dl>
|
||||
${this.renderDevelopmentWorkflowButton()}
|
||||
</div>`;
|
||||
}
|
||||
renderDevWorkflowSection() {
|
||||
const e = g(), t = this.getIdePluginLabelText(p.idePluginState), n = this.getHotswapAgentLabelText(e);
|
||||
return a`
|
||||
<div>
|
||||
<dt>Java Hotswap</dt>
|
||||
<dd>
|
||||
${f(e === "success", e === "success" ? "Enabled" : "Disabled")} ${n}
|
||||
</dd>
|
||||
</div>
|
||||
${c() !== "unsupported" ? a` <div>
|
||||
<dt>IDE Plugin</dt>
|
||||
<dd>
|
||||
${f(
|
||||
c() === "success",
|
||||
c() === "success" ? "Installed" : "Not Installed"
|
||||
)}
|
||||
${t}
|
||||
</dd>
|
||||
</div>` : E}
|
||||
`;
|
||||
}
|
||||
renderDevelopmentWorkflowButton() {
|
||||
const e = C();
|
||||
let t = "", n = null, i = "";
|
||||
return e.status === "success" ? (t = "success", n = d.check, i = "Details") : e.status === "warning" ? (t = "warning", n = d.lightning, i = "Improve Development Workflow") : e.status === "error" && (t = "error", n = d.alertCircle, i = "Fix Development Workflow"), a`
|
||||
<button
|
||||
class="mx-50"
|
||||
id="development-workflow-guide"
|
||||
@click="${() => {
|
||||
V();
|
||||
}}">
|
||||
<span class="prefix ${t}-text"> ${n} </span>
|
||||
${i}
|
||||
<span class="suffix">
|
||||
<span class="bg-${t} end-0 h-75 rounded-full top-0 w-75"></span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
getHotswapAgentLabelText(e) {
|
||||
return e === "success" ? "Java Hotswap is enabled" : e === "error" ? "Hotswap is partially enabled" : "Hotswap is disabled";
|
||||
}
|
||||
getIdePluginLabelText(e) {
|
||||
if (c() !== "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"), o = Array.from(t).map((s) => ({
|
||||
key: s.textContent.trim(),
|
||||
value: s.nextElementSibling.textContent.trim()
|
||||
})).filter((s) => s.key !== "Live reload").filter((s) => !s.key.startsWith("Vaadin Emplo")).map((s) => {
|
||||
const { key: l } = s;
|
||||
let { value: r } = s;
|
||||
if (l === "IDE Plugin")
|
||||
r = this.getIdePluginLabelText(p.idePluginState) ?? "false";
|
||||
else if (l === "Java Hotswap") {
|
||||
const y = p.jdkInfo?.jrebel, m = g();
|
||||
y && m === "success" ? r = "JRebel is in use" : r = this.getHotswapAgentLabelText(m);
|
||||
} else l === "Vaadin" && r.indexOf(`
|
||||
`) !== -1 && (r = r.substring(0, r.indexOf(`
|
||||
`)));
|
||||
return `${l}: ${r}`;
|
||||
}).join(`
|
||||
`);
|
||||
return e && P({
|
||||
type: D.INFORMATION,
|
||||
message: "Environment information copied to clipboard",
|
||||
dismissId: "versionInfoCopied"
|
||||
}), o.trim();
|
||||
}
|
||||
};
|
||||
v([
|
||||
x()
|
||||
], u.prototype, "serverInfo", 2);
|
||||
v([
|
||||
x()
|
||||
], u.prototype, "clientInfo", 2);
|
||||
u = v([
|
||||
w("copilot-info-panel")
|
||||
], u);
|
||||
let b = class extends A {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.style.display = "flex";
|
||||
}
|
||||
render() {
|
||||
return a` <button
|
||||
@click=${() => {
|
||||
H.emit("system-info-with-callback", {
|
||||
callback: O,
|
||||
notify: !0
|
||||
});
|
||||
}}
|
||||
aria-label="Copy to Clipboard"
|
||||
class="icon"
|
||||
title="Copy to Clipboard">
|
||||
<span>${d.copy}</span>
|
||||
</button>`;
|
||||
}
|
||||
};
|
||||
b = v([
|
||||
w("copilot-info-actions")
|
||||
], b);
|
||||
const B = {
|
||||
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
|
||||
}, W = {
|
||||
init(e) {
|
||||
e.addPanel(B);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(W);
|
||||
function f(e, t) {
|
||||
return e ? a`<span aria-label=${t} class="icon success-text" title=${t}>${d.check}</span>` : a`<span aria-label=${t} class="icon error-text" title=${t}>${d.x}</span>`;
|
||||
}
|
||||
export {
|
||||
b as Actions,
|
||||
u as CopilotInfoPanel
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,235 +0,0 @@
|
||||
import { j as R, ah as S, ai as x, a6 as c, F as n, M as p, aj as M, a2 as L, v as D, b as k, K as I, m as q, x as A, u as v } from "./copilot-CP3-W7yE.js";
|
||||
import { r as $ } from "./state-C3WY-pqX.js";
|
||||
import { B as P } from "./base-panel-Ckfoxxex.js";
|
||||
import { i as l } from "./icons-DVw-r69H.js";
|
||||
const B = '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(--size-m) + var(--space-100));position:absolute}copilot-log-panel ul li span.icon{display:flex;flex-shrink:0;justify-content:center;width:var(--size-m)}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)}';
|
||||
var C = Object.defineProperty, _ = Object.getOwnPropertyDescriptor, u = (e, t, a, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? _(t, a) : t, d = e.length - 1, s; d >= 0; d--)
|
||||
(s = e[d]) && (o = (i ? s(t, a, o) : s(o)) || o);
|
||||
return i && o && C(t, a, o), o;
|
||||
};
|
||||
class b {
|
||||
constructor() {
|
||||
this.showTimestamps = !1, q(this);
|
||||
}
|
||||
toggleShowTimestamps() {
|
||||
this.showTimestamps = !this.showTimestamps;
|
||||
}
|
||||
}
|
||||
const h = new b();
|
||||
let r = 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) => {
|
||||
I(() => {
|
||||
A.attentionRequiredPanelTag = "copilot-log-panel";
|
||||
}), this.log(p.ERROR, t.message, !!t.internal, t.details, t.link);
|
||||
};
|
||||
S((t) => {
|
||||
e(t);
|
||||
}), x.forEach((t) => {
|
||||
e(t);
|
||||
}), x.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 n`
|
||||
<style>
|
||||
${B}
|
||||
</style>
|
||||
<ul>
|
||||
${this.messages.map((e) => this.renderMessage(e))}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
renderMessage(e) {
|
||||
let t, a;
|
||||
return e.type === p.ERROR ? (a = l.alertTriangle, t = "Error") : e.type === p.WARNING ? (a = l.warning, t = "Warning") : (a = l.info, t = "Info"), n`
|
||||
<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 ? n` <span class="secondary"> ${e.expandedDetails ?? e.details} </span>` : n` <span class="secondary" ?hidden="${!e.details && !e.link}">
|
||||
${c(e.details)}
|
||||
${e.link ? n` <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>${l.chevronRight}</span>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
log(e, t, a, i, o, d, s, E) {
|
||||
const T = this.nextMessageId;
|
||||
this.nextMessageId += 1, s || (s = t);
|
||||
const m = {
|
||||
id: T,
|
||||
type: e,
|
||||
message: t,
|
||||
details: i,
|
||||
link: o,
|
||||
dontShowAgain: !1,
|
||||
deleted: !1,
|
||||
expanded: !1,
|
||||
expandedMessage: d,
|
||||
expandedDetails: s,
|
||||
timestamp: /* @__PURE__ */ new Date(),
|
||||
internal: a,
|
||||
userId: E
|
||||
};
|
||||
for (this.messages.push(m); this.messages.length > r.MAX_LOG_ROWS; )
|
||||
this.messages.shift();
|
||||
return this.requestUpdate(), this.updateComplete.then(() => {
|
||||
const f = this.renderRoot.querySelector(".message:last-child");
|
||||
f ? (setTimeout(() => f.scrollIntoView({ behavior: "smooth" }), this.transitionDuration), this.unreadErrors = !1) : e === p.ERROR && (this.unreadErrors = !0);
|
||||
}), m;
|
||||
}
|
||||
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), M(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()), L("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;
|
||||
}
|
||||
};
|
||||
r.MAX_LOG_ROWS = 1e3;
|
||||
u([
|
||||
$()
|
||||
], r.prototype, "unreadErrors", 2);
|
||||
u([
|
||||
$()
|
||||
], r.prototype, "messages", 2);
|
||||
r = u([
|
||||
v("copilot-log-panel")
|
||||
], r);
|
||||
let y = class extends D {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
render() {
|
||||
return n`
|
||||
<style>
|
||||
copilot-log-panel-actions {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
<button
|
||||
aria-label="Clear log"
|
||||
class="icon"
|
||||
title="Clear log"
|
||||
@click=${() => {
|
||||
k.emit("clear-log", {});
|
||||
}}>
|
||||
<span>${l.trash}</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Toggle timestamps"
|
||||
class="icon"
|
||||
title="Toggle timestamps"
|
||||
@click=${() => {
|
||||
h.toggleShowTimestamps();
|
||||
}}>
|
||||
<span class="${h.showTimestamps ? "on" : "off"}"> ${l.clock} </span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
};
|
||||
y = u([
|
||||
v("copilot-log-panel-actions")
|
||||
], y);
|
||||
const U = {
|
||||
header: "Log",
|
||||
expanded: !0,
|
||||
panelOrder: 0,
|
||||
panel: "bottom",
|
||||
floating: !1,
|
||||
tag: "copilot-log-panel",
|
||||
actionsTag: "copilot-log-panel-actions"
|
||||
}, F = {
|
||||
init(e) {
|
||||
e.addPanel(U);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(F);
|
||||
const w = { hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3 };
|
||||
let g;
|
||||
try {
|
||||
g = new Intl.DateTimeFormat(navigator.language, w);
|
||||
} catch (e) {
|
||||
console.error("Failed to create date time formatter for ", navigator.language, e), g = new Intl.DateTimeFormat("en-US", w);
|
||||
}
|
||||
function N(e) {
|
||||
return g.format(e);
|
||||
}
|
||||
export {
|
||||
y as Actions,
|
||||
r as CopilotLogPanel
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import { u as f, b as c, E as g, F as i, a0 as b, X as s, H as m } from "./copilot-CP3-W7yE.js";
|
||||
import { B as $ } from "./base-panel-Ckfoxxex.js";
|
||||
import { i as e } from "./icons-DVw-r69H.js";
|
||||
const v = 'copilot-shortcuts-panel{display:flex;flex-direction:column;padding:var(--space-150)}copilot-shortcuts-panel h3{font:var(--font-xsmall-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(--size-m) + var(--space-50));position:absolute}copilot-shortcuts-panel ul li span:has(svg){align-items:center;display:flex;height:var(--size-m);justify-content:center;width:var(--size-m)}copilot-shortcuts-panel .kbds{margin-inline-start:auto}copilot-shortcuts-panel kbd{align-items:center;border:1px solid var(--border-color);border-radius:var(--radius-2);box-sizing:border-box;display:inline-flex;font-family:var(--font-family);font-size:var(--font-size-1);line-height:var(--line-height-1);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 y = Object.getOwnPropertyDescriptor, w = (t, l, h, p) => {
|
||||
for (var o = p > 1 ? void 0 : p ? y(l, h) : l, n = t.length - 1, r; n >= 0; n--)
|
||||
(r = t[n]) && (o = r(o) || o);
|
||||
return o;
|
||||
};
|
||||
let d = class extends $ {
|
||||
constructor() {
|
||||
super(), this.onTreeUpdated = () => {
|
||||
this.requestUpdate();
|
||||
};
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), c.on("copilot-tree-created", this.onTreeUpdated);
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(), c.off("copilot-tree-created", this.onTreeUpdated);
|
||||
}
|
||||
render() {
|
||||
const t = u.hasFlowComponents();
|
||||
return i`<style>
|
||||
${v}
|
||||
</style>
|
||||
<h3>Global</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span>${e.vaadinLogo}</span>
|
||||
<span>Copilot</span>
|
||||
${a(s.toggleCopilot)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.terminal}</span>
|
||||
<span>Command window</span>
|
||||
${a(s.toggleCommandWindow)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.flipBack}</span>
|
||||
<span>Undo</span>
|
||||
${a(s.undo)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.flipForward}</span>
|
||||
<span>Redo</span>
|
||||
${a(s.redo)}
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Selected component</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span>${e.fileCodeAlt}</span>
|
||||
<span>Go to source</span>
|
||||
${a(s.goToSource)}
|
||||
</li>
|
||||
${t ? i`<li>
|
||||
<span>${e.code}</span>
|
||||
<span>Go to attach source</span>
|
||||
${a(s.goToAttachSource)}
|
||||
</li>` : g}
|
||||
<li>
|
||||
<span>${e.copy}</span>
|
||||
<span>Copy</span>
|
||||
${a(s.copy)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.clipboard}</span>
|
||||
<span>Paste</span>
|
||||
${a(s.paste)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.copyAlt}</span>
|
||||
<span>Duplicate</span>
|
||||
${a(s.duplicate)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userUp}</span>
|
||||
<span>Select parent</span>
|
||||
${a(s.selectParent)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userLeft}</span>
|
||||
<span>Select previous sibling</span>
|
||||
${a(s.selectPreviousSibling)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.userRight}</span>
|
||||
<span>Select first child / next sibling</span>
|
||||
${a(s.selectNextSibling)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.trash}</span>
|
||||
<span>Delete</span>
|
||||
${a(s.delete)}
|
||||
</li>
|
||||
<li>
|
||||
<span>${e.zap}</span>
|
||||
<span>Quick add from palette</span>
|
||||
${a("<kbd>A ... Z</kbd>")}
|
||||
</li>
|
||||
</ul>`;
|
||||
}
|
||||
};
|
||||
d = w([
|
||||
f("copilot-shortcuts-panel")
|
||||
], d);
|
||||
function a(t) {
|
||||
return i`<span class="kbds">${b(t)}</span>`;
|
||||
}
|
||||
const x = m({
|
||||
header: "Keyboard Shortcuts",
|
||||
tag: "copilot-shortcuts-panel",
|
||||
width: 400,
|
||||
height: 550,
|
||||
floatingPosition: {
|
||||
top: 50,
|
||||
left: 50
|
||||
}
|
||||
}), C = {
|
||||
init(t) {
|
||||
t.addPanel(x);
|
||||
}
|
||||
};
|
||||
window.Vaadin.copilot.plugins.push(C);
|
||||
@@ -1,80 +0,0 @@
|
||||
import { a as D } from "./copilot-CP3-W7yE.js";
|
||||
var y, v;
|
||||
function E() {
|
||||
return v || (v = 1, y = 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();
|
||||
};
|
||||
}), y;
|
||||
}
|
||||
var g, C;
|
||||
function h() {
|
||||
if (C) return g;
|
||||
C = 1;
|
||||
var a = E(), 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, m, b, u, l, e, f = !1;
|
||||
t || (t = {}), c = t.debug || !1;
|
||||
try {
|
||||
b = 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"), m = i("message" in t ? t.message : s), window.prompt(m, n);
|
||||
}
|
||||
} finally {
|
||||
l && (typeof l.removeRange == "function" ? l.removeRange(u) : l.removeAllRanges()), e && document.body.removeChild(e), b();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
return g = d, g;
|
||||
}
|
||||
var x = h();
|
||||
const S = /* @__PURE__ */ D(x), T = window.Vaadin.copilot._earlyProjectState;
|
||||
if (!T)
|
||||
throw new Error("Tried to access early project state before it was initialized.");
|
||||
export {
|
||||
S as c,
|
||||
T as e
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
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;
|
||||
File diff suppressed because one or more lines are too long
@@ -1,48 +0,0 @@
|
||||
import { P as c } from "./copilot-CP3-W7yE.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const d = (e, a, t) => (t.configurable = !0, t.enumerable = !0, Reflect.decorate && typeof a != "object" && Object.defineProperty(e, a, t), t);
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
function h(e, a) {
|
||||
return (t, o, v) => {
|
||||
const i = (l) => l.renderRoot?.querySelector(e) ?? null;
|
||||
return d(t, o, { get() {
|
||||
return i(this);
|
||||
} });
|
||||
};
|
||||
}
|
||||
function u(e) {
|
||||
e.querySelectorAll(
|
||||
"vaadin-context-menu, vaadin-menu-bar, vaadin-menu-bar-submenu, vaadin-select, vaadin-combo-box, vaadin-tooltip, vaadin-dialog, vaadin-multi-select-combo-box, vaadin-popover"
|
||||
).forEach((a) => {
|
||||
a?.$?.comboBox && (a = a.$.comboBox);
|
||||
let t = a.shadowRoot?.querySelector(
|
||||
`${a.localName}-overlay, ${a.localName}-submenu, vaadin-menu-bar-overlay`
|
||||
);
|
||||
t?.localName === "vaadin-menu-bar-submenu" && (t = t.shadowRoot.querySelector("vaadin-menu-bar-overlay")), t ? t._attachOverlay = n.bind(t) : a.$?.overlay && (a.$.overlay._attachOverlay = n.bind(a.$.overlay));
|
||||
});
|
||||
}
|
||||
function r() {
|
||||
return document.querySelector(`${c}main`).shadowRoot;
|
||||
}
|
||||
const m = () => Array.from(r().children).filter((a) => a._hasOverlayStackMixin && !a.hasAttribute("closing")).sort((a, t) => a.__zIndex - t.__zIndex || 0), s = (e) => e === m().pop();
|
||||
function n() {
|
||||
const e = this;
|
||||
e._placeholder = document.createComment("vaadin-overlay-placeholder"), e.parentNode.insertBefore(e._placeholder, e), r().appendChild(e), e.hasOwnProperty("_last") || Object.defineProperty(e, "_last", {
|
||||
// Only returns odd die sides
|
||||
get() {
|
||||
return s(this);
|
||||
}
|
||||
}), e.bringToFront(), requestAnimationFrame(() => u(e));
|
||||
}
|
||||
export {
|
||||
h as e,
|
||||
u as m
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
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;
|
||||
}
|
||||
export interface PanelConfiguration {
|
||||
header: string;
|
||||
expanded: boolean;
|
||||
expandable?: boolean;
|
||||
panel?: 'bottom' | 'left' | 'right';
|
||||
panelOrder: number;
|
||||
tag: string;
|
||||
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;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ap as u, aq as p } from "./copilot-CP3-W7yE.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const l = { attribute: !0, type: String, converter: p, reflect: !1, hasChanged: u }, d = (t = l, 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
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
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));
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.composedPath().find((node) => node.hasAttribute && node.hasAttribute('disableonclick'));
|
||||
if (target) {
|
||||
target.disabled = true;
|
||||
}
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
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);
|
||||
@@ -1,198 +0,0 @@
|
||||
import '@polymer/polymer/lib/elements/dom-if.js';
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { Debouncer } from '@polymer/polymer/lib/utils/debounce.js';
|
||||
import { idlePeriod } from '@polymer/polymer/lib/utils/async.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
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 };
|
||||
|
||||
class FlowComponentRenderer extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
animation: 1ms flow-component-renderer-appear;
|
||||
}
|
||||
|
||||
@keyframes flow-component-renderer-appear {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return 'flow-component-renderer';
|
||||
}
|
||||
static get properties() {
|
||||
return {
|
||||
nodeid: Number,
|
||||
appid: String
|
||||
};
|
||||
}
|
||||
static get observers() {
|
||||
return ['_attachRenderedComponentIfAble(appid, nodeid)'];
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.addEventListener('click', function (event) {
|
||||
if (this.firstChild && typeof this.firstChild.click === 'function' && event.target === this) {
|
||||
event.stopPropagation();
|
||||
this.firstChild.click();
|
||||
}
|
||||
});
|
||||
this.addEventListener('animationend', this._onAnimationEnd);
|
||||
}
|
||||
|
||||
_asyncAttachRenderedComponentIfAble() {
|
||||
this._debouncer = Debouncer.debounce(this._debouncer, idlePeriod, () => this._attachRenderedComponentIfAble());
|
||||
}
|
||||
|
||||
_attachRenderedComponentIfAble() {
|
||||
if (this.appid == null) {
|
||||
return;
|
||||
}
|
||||
if (this.nodeid == null) {
|
||||
if (this.firstChild) {
|
||||
this.removeChild(this.firstChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const renderedComponent = this._getRenderedComponent();
|
||||
if (this.firstChild) {
|
||||
if (!renderedComponent) {
|
||||
this._asyncAttachRenderedComponentIfAble();
|
||||
} else if (this.firstChild !== renderedComponent) {
|
||||
this.replaceChild(renderedComponent, this.firstChild);
|
||||
this._defineFocusTarget();
|
||||
this.onComponentRendered();
|
||||
} else {
|
||||
this._defineFocusTarget();
|
||||
this.onComponentRendered();
|
||||
}
|
||||
} else {
|
||||
if (renderedComponent) {
|
||||
this.appendChild(renderedComponent);
|
||||
this._defineFocusTarget();
|
||||
this.onComponentRendered();
|
||||
} else {
|
||||
this._asyncAttachRenderedComponentIfAble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderedComponent() {
|
||||
try {
|
||||
return window.Vaadin.Flow.clients[this.appid].getByNodeId(this.nodeid);
|
||||
} catch (error) {
|
||||
console.error('Could not get node %s from app %s', this.nodeid, this.appid);
|
||||
console.error(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onComponentRendered() {
|
||||
// subclasses can override this method to execute custom logic on resize
|
||||
}
|
||||
|
||||
/* Setting the `focus-target` attribute to the first focusable descendant
|
||||
starting from the firstChild necessary for the focus to be delegated
|
||||
within the flow-component-renderer when used inside a vaadin-grid cell */
|
||||
_defineFocusTarget() {
|
||||
var focusable = this._getFirstFocusableDescendant(this.firstChild);
|
||||
if (focusable !== null) {
|
||||
focusable.setAttribute('focus-target', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
_getFirstFocusableDescendant(node) {
|
||||
if (this._isFocusable(node)) {
|
||||
return node;
|
||||
}
|
||||
if (node.hasAttribute && (node.hasAttribute('disabled') || node.hasAttribute('hidden'))) {
|
||||
return null;
|
||||
}
|
||||
if (!node.children) {
|
||||
return null;
|
||||
}
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
var focusable = this._getFirstFocusableDescendant(node.children[i]);
|
||||
if (focusable !== null) {
|
||||
return focusable;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_isFocusable(node) {
|
||||
if (
|
||||
node.hasAttribute &&
|
||||
typeof node.hasAttribute === 'function' &&
|
||||
(node.hasAttribute('disabled') || node.hasAttribute('hidden'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.tabIndex === 0;
|
||||
}
|
||||
|
||||
_onAnimationEnd(e) {
|
||||
// ShadyCSS applies scoping suffixes to animation names
|
||||
// To ensure that child is attached once element is unhidden
|
||||
// for when it was filtered out from, eg, ComboBox
|
||||
// https://github.com/vaadin/vaadin-flow-components/issues/437
|
||||
if (e.animationName.indexOf('flow-component-renderer-appear') === 0) {
|
||||
this._attachRenderedComponentIfAble();
|
||||
}
|
||||
}
|
||||
}
|
||||
window.customElements.define(FlowComponentRenderer.is, FlowComponentRenderer);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
export * from './copilot'
|
||||
export {}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './Flow';
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/main/frontend/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC","sourcesContent":["export * from './Flow';\n"]}
|
||||
@@ -1,112 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2025 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 };
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2025 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps the given items to a new array of items with formatted time.
|
||||
*/
|
||||
function formatItems(items, locale) {
|
||||
const formatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
});
|
||||
|
||||
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)];
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2025 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 { TextField } from '@vaadin/text-field/src/vaadin-text-field.js';
|
||||
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
|
||||
|
||||
let memoizedTemplate;
|
||||
|
||||
class BigDecimalField extends TextField {
|
||||
static get template() {
|
||||
if (!memoizedTemplate) {
|
||||
memoizedTemplate = super.template.cloneNode(true);
|
||||
memoizedTemplate.innerHTML += `<style>
|
||||
:host {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
return memoizedTemplate;
|
||||
}
|
||||
|
||||
static get is() {
|
||||
return 'vaadin-big-decimal-field';
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_decimalSeparator: {
|
||||
type: String,
|
||||
value: '.',
|
||||
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);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ServerMessage } from "./vaadin-dev-tools";
|
||||
export interface Product {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
export interface ProductAndMessage {
|
||||
message: string;
|
||||
messageHtml?: string;
|
||||
product: Product;
|
||||
}
|
||||
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) => boolean;
|
||||
export declare const licenseInit: () => void;
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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;
|
||||
backend?: string;
|
||||
liveReloadPort: number;
|
||||
token?: string;
|
||||
};
|
||||
export declare class VaadinDevTools extends LitElement {
|
||||
unhandledMessages: ServerMessage[];
|
||||
conf: DevToolsConf;
|
||||
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;
|
||||
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;
|
||||
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
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import '@vaadin/grid/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);
|
||||
@@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
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.$.comboBox.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
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.setRequestedRange(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;
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createElement as reactCreateElement } from "react";
|
||||
|
||||
export const createElement = reactCreateElement;
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import {
|
||||
Fragment as reactFragment,
|
||||
jsx as reactJsx,
|
||||
jsxs as reactJsxs,
|
||||
} from "react/jsx-runtime";
|
||||
|
||||
export const Fragment = reactFragment;
|
||||
export const jsx = reactJsx;
|
||||
export const jsxs = reactJsxs;
|
||||
|
||||
throw new Error(
|
||||
"Do not use this transform for production builds. It is only meant for development."
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,19 +0,0 @@
|
||||
/******************************************************************************
|
||||
* 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()
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
|
||||
|
||||
if (!document['_vaadintheme_myTheme_componentCss']) {
|
||||
|
||||
document['_vaadintheme_myTheme_componentCss'] = true;
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept((module) => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'construct-style-sheets-polyfill';
|
||||
import { injectGlobalCss } from 'Frontend/generated/jar-resources/theme-util.js';
|
||||
import { webcomponentGlobalCssInjector } from 'Frontend/generated/jar-resources/theme-util.js';
|
||||
import './theme-myTheme.components.generated.js';
|
||||
let needsReloadOnChanges = false;
|
||||
import { typography } from '@vaadin/vaadin-lumo-styles/typography.js';
|
||||
import { color } from '@vaadin/vaadin-lumo-styles/color.js';
|
||||
import { spacing } from '@vaadin/vaadin-lumo-styles/spacing.js';
|
||||
import { badge } from '@vaadin/vaadin-lumo-styles/badge.js';
|
||||
import { utility } from '@vaadin/vaadin-lumo-styles/utility.js';
|
||||
import stylesCss from 'themes/myTheme/styles.css?inline';
|
||||
|
||||
let themeRemovers = new WeakMap();
|
||||
let targets = [];
|
||||
|
||||
export const applyTheme = (target) => {
|
||||
const removers = [];
|
||||
if (target !== document) {
|
||||
removers.push(injectGlobalCss(typography.cssText, '', target, true));
|
||||
removers.push(injectGlobalCss(color.cssText, '', target, true));
|
||||
removers.push(injectGlobalCss(spacing.cssText, '', target, true));
|
||||
removers.push(injectGlobalCss(badge.cssText, '', target, true));
|
||||
removers.push(injectGlobalCss(utility.cssText, '', target, true));
|
||||
removers.push(injectGlobalCss(stylesCss.toString(), '', target));
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (import.meta.hot) {
|
||||
targets.push(new WeakRef(target));
|
||||
themeRemovers.set(target, removers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept((module) => {
|
||||
|
||||
if (needsReloadOnChanges) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
targets.forEach(targetRef => {
|
||||
const target = targetRef.deref();
|
||||
if (target) {
|
||||
themeRemovers.get(target).forEach(remover => remover())
|
||||
module.applyTheme(target);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
import.meta.hot.on('vite:afterUpdate', (update) => {
|
||||
document.dispatchEvent(new CustomEvent('vaadin-theme-updated', { detail: update }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// When this file is imported, global styles are automatically applied
|
||||
|
||||
import '@vaadin/vaadin-lumo-styles/typography-global.js';
|
||||
import '@vaadin/vaadin-lumo-styles/color-global.js';
|
||||
import '@vaadin/vaadin-lumo-styles/badge-global.js';
|
||||
import '@vaadin/vaadin-lumo-styles/utility-global.js';
|
||||
import 'themes/myTheme/styles.css';
|
||||
|
||||
1
src/main/frontend/generated/theme.d.ts
vendored
1
src/main/frontend/generated/theme.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export declare const applyTheme: (target: Node) => void;
|
||||
@@ -1,2 +0,0 @@
|
||||
import {applyTheme as _applyTheme} from './theme-myTheme.generated.js';
|
||||
export const applyTheme = _applyTheme;
|
||||
@@ -1,22 +0,0 @@
|
||||
// @ts-nocheck
|
||||
window.Vaadin = window.Vaadin || {};
|
||||
window.Vaadin.featureFlags = window.Vaadin.featureFlags || {};
|
||||
if (Object.keys(window.Vaadin.featureFlags).length === 0) {
|
||||
window.Vaadin.featureFlags.exampleFeatureFlag = false;
|
||||
window.Vaadin.featureFlags.collaborationEngineBackend = false;
|
||||
window.Vaadin.featureFlags.formFillerAddon = false;
|
||||
window.Vaadin.featureFlags.fullstackSignals = false;
|
||||
window.Vaadin.featureFlags.flowFullstackSignals = false;
|
||||
window.Vaadin.featureFlags.copilotExperimentalFeatures = false;
|
||||
window.Vaadin.featureFlags.masterDetailLayoutComponent = false;
|
||||
window.Vaadin.featureFlags.react19 = false;
|
||||
window.Vaadin.featureFlags.accessibleDisabledButtons = 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 {};
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import './vaadin-featureflags.js';
|
||||
|
||||
import './index';
|
||||
|
||||
import './vaadin-react.js';
|
||||
import './theme-myTheme.global.generated.js';
|
||||
import { applyTheme } from './theme.js';
|
||||
applyTheme(document);
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
:root {
|
||||
:root {
|
||||
--lumo-font-size: 1rem;
|
||||
--lumo-font-size-xxxl: 3rem;
|
||||
--lumo-font-size-xxl: 2.25rem;
|
||||
@@ -9,11 +8,14 @@
|
||||
--lumo-font-size-s: 1rem;
|
||||
--lumo-font-size-xs: 0.875rem;
|
||||
--lumo-font-size-xxs: 0.8125rem;
|
||||
|
||||
--lumo-primary-text-color: rgb(9, 134, 24);
|
||||
--lumo-primary-color-50pct: rgba(9, 134, 24, 0.5);
|
||||
--lumo-primary-color-10pct: rgba(9, 134, 24, 0.1);
|
||||
--lumo-primary-color: hsl(127, 87%, 28%);
|
||||
--lumo-base-color: #dadcc79e;
|
||||
|
||||
--lumo-base-color: #dadcc7;
|
||||
|
||||
--lumo-tint-5pct: rgba(101, 105, 63, 0.05);
|
||||
--lumo-tint-10pct: rgba(101, 105, 63, 0.1);
|
||||
--lumo-tint-20pct: rgba(101, 105, 63, 0.2);
|
||||
@@ -24,9 +26,44 @@
|
||||
--lumo-tint-70pct: rgba(101, 105, 63, 0.7);
|
||||
--lumo-tint-80pct: rgba(101, 105, 63, 0.8);
|
||||
--lumo-tint-90pct: rgba(101, 105, 63, 0.9);
|
||||
--lumo-tint: #65693fed;
|
||||
--lumo-tint: #65693f;
|
||||
|
||||
--lumo-success-text-color: rgb(56, 204, 36);
|
||||
--lumo-success-color-50pct: rgba(56, 204, 36, 0.5);
|
||||
--lumo-success-color-10pct: rgba(56, 204, 36, 0.1);
|
||||
--lumo-success-color: hsl(113, 70%, 47%);
|
||||
}
|
||||
|
||||
/* Lumo-Tokens, die IntelliJ anmeckert – mit sinnvollen Defaults */
|
||||
--lumo-space-xs: 0.25rem;
|
||||
--lumo-border-radius-s: 0.25rem;
|
||||
--lumo-contrast-10pct: rgba(0, 0, 0, 0.06);
|
||||
--lumo-size-s: 2rem;
|
||||
}
|
||||
|
||||
/* Deckender Hintergrund + Rahmen für die Filter-Wrapper im Header */
|
||||
.filter-cell {
|
||||
background: var(--lumo-base-color, #fff);
|
||||
padding: var(--lumo-space-xs, 0.25rem);
|
||||
border-radius: var(--lumo-border-radius-s, 0.25rem);
|
||||
/* zarte Kontur; Fallback, falls IntelliJ die Var. nicht kennt */
|
||||
box-shadow: inset 0 0 0 1px var(--lumo-contrast-10pct, rgba(0,0,0,0.06));
|
||||
display: block;
|
||||
background-clip: padding-box; /* verhindert „Ausbluten“ */
|
||||
}
|
||||
|
||||
vaadin-grid::part(header-row),
|
||||
|
||||
vaadin-grid::part(header-cell) {
|
||||
background: var(--lumo-base-color, #fff);
|
||||
box-shadow: 0 1px 0 0 var(--lumo-contrast-10pct, rgba(0,0,0,0.06));
|
||||
min-height: var(--lumo-size-s, 2rem); /* gleichmäßige Höhe */
|
||||
}
|
||||
|
||||
.filter-cell :is(vaadin-date-picker,
|
||||
vaadin-combo-box,
|
||||
vaadin-text-field,
|
||||
vaadin-checkbox,
|
||||
vaadin-button) {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.nilzbu.mytimetracker.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum DayStatus {
|
||||
OFFICE("In office"),
|
||||
REMOTE("Remote work"),
|
||||
@@ -12,7 +15,4 @@ public enum DayStatus {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public class TimeEntry {
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private DayStatus status = DayStatus.REMOTE;
|
||||
|
||||
private String comment;
|
||||
|
||||
@@ -34,9 +34,11 @@ public class User {
|
||||
private Set<String> roles; // z.B. ROLE_USER, ROLE_ADMIN
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private boolean enabled = true;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private boolean locked = false;
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
|
||||
@@ -12,10 +12,6 @@ public interface TimeEntryRepository extends JpaRepository<TimeEntry, Long> {
|
||||
|
||||
List<TimeEntry> findAllByUserOrderByDateDesc(User user);
|
||||
|
||||
Optional<TimeEntry> findTopByUserOrderByDateDesc(User user);
|
||||
|
||||
boolean existsByUserAndDate(User user, LocalDate date);
|
||||
|
||||
Optional<TimeEntry> findByUserAndDate(User user, LocalDate date);
|
||||
|
||||
List<TimeEntry> findByUserAndDateBetween(User user, LocalDate start, LocalDate end);
|
||||
|
||||
@@ -12,10 +12,7 @@ public class SecurityConfig extends VaadinWebSecurity {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// Standard-Konfiguration von Vaadin-Security erweitern
|
||||
super.configure(http);
|
||||
|
||||
// Login-View setzen
|
||||
setLoginView(http, LoginView.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ public class TimeEntryService {
|
||||
return repository.findAllByUserOrderByDateDesc(user);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForUser(User user) {
|
||||
return getEntriesForUser(user).size();
|
||||
}
|
||||
|
||||
public TimeEntry save(TimeEntry entry) {
|
||||
return repository.save(entry);
|
||||
}
|
||||
@@ -32,7 +36,7 @@ public class TimeEntryService {
|
||||
return 0;
|
||||
}
|
||||
long total = Duration.between(entry.getStartTime(), entry.getEndTime()).toMinutes();
|
||||
return (int)(total - entry.getPauseMinutes());
|
||||
return (int) (total - entry.getPauseMinutes());
|
||||
}
|
||||
|
||||
public long calculateDeviation(TimeEntry entry) {
|
||||
@@ -49,6 +53,10 @@ public class TimeEntryService {
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForMonth(User user, int year, int month) {
|
||||
return getEntriesForMonth(user, year, month).size();
|
||||
}
|
||||
|
||||
public List<TimeEntry> getEntriesForQuarter(User user, int year, int quarter) {
|
||||
int startMonth = (quarter - 1) * 3 + 1;
|
||||
LocalDate start = LocalDate.of(year, startMonth, 1);
|
||||
@@ -56,12 +64,20 @@ public class TimeEntryService {
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForQuarter(User user, int year, int month) {
|
||||
return getEntriesForQuarter(user, year, month).size();
|
||||
}
|
||||
|
||||
public List<TimeEntry> getEntriesForYear(User user, int year) {
|
||||
LocalDate start = LocalDate.of(year, 1, 1);
|
||||
LocalDate end = LocalDate.of(year, 12, 31);
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForYear(User user, int year) {
|
||||
return getEntriesForYear(user, year).size();
|
||||
}
|
||||
|
||||
public Optional<LocalDate> getEarliestEntryDate(User user) {
|
||||
return repository.findFirstByUserOrderByDateAsc(user)
|
||||
.map(TimeEntry::getDate);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package de.nilzbu.mytimetracker.ui.component;
|
||||
|
||||
import com.vaadin.flow.component.HasSize;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.Tag;
|
||||
import com.vaadin.flow.component.dependency.JsModule;
|
||||
import com.vaadin.flow.component.dependency.NpmPackage;
|
||||
import com.vaadin.flow.component.Tag;
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import elemental.json.Json;
|
||||
import elemental.json.JsonArray;
|
||||
import elemental.json.JsonObject;
|
||||
@@ -31,9 +30,15 @@ public class ChartJsComponent extends Div {
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
renderChart();
|
||||
}
|
||||
|
||||
private void renderChart() {
|
||||
getElement().executeJs(
|
||||
"""
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
this.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
new Chart(ctx, {
|
||||
@@ -54,47 +59,54 @@ public class ChartJsComponent extends Div {
|
||||
}
|
||||
});
|
||||
""",
|
||||
getChartTypeFromData(chartData),
|
||||
detectChartType(chartData),
|
||||
chartData,
|
||||
chartTitle
|
||||
);
|
||||
}
|
||||
|
||||
private String getChartTypeFromData(JsonObject data) {
|
||||
// Default zu bar wenn keine sinnvolle Bestimmung möglich ist
|
||||
/**
|
||||
* Bestimmt den Diagrammtyp basierend auf dem Dataset.
|
||||
*/
|
||||
private String detectChartType(JsonObject data) {
|
||||
try {
|
||||
JsonArray datasets = data.getArray("datasets");
|
||||
if (datasets.length() > 0) {
|
||||
JsonObject first = datasets.getObject(0);
|
||||
if (first.hasKey("fill") && !first.getBoolean("fill")) {
|
||||
JsonObject firstDataset = datasets.getObject(0);
|
||||
if (firstDataset.hasKey("fill") && !firstDataset.getBoolean("fill")) {
|
||||
return "line";
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "bar";
|
||||
}
|
||||
|
||||
// Balkendiagramm für Kategorien
|
||||
/**
|
||||
* Erstellt ein Balkendiagramm für Kategoriezählungen.
|
||||
*/
|
||||
public static JsonObject generateBarChartData(Map<String, Long> categoryCounts) {
|
||||
JsonObject data = Json.createObject();
|
||||
JsonArray labels = Json.createArray();
|
||||
JsonArray values = Json.createArray();
|
||||
JsonArray backgroundColors = Json.createArray();
|
||||
JsonArray colors = Json.createArray();
|
||||
|
||||
String[] defaultColors = {"#1e7e34", "#138496", "#ffc107", "#dc3545", "#6f42c1", "#20c997"};
|
||||
String[] defaultColors = {
|
||||
"#1e7e34", "#138496", "#ffc107", "#dc3545", "#6f42c1", "#20c997"
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
int index = 0;
|
||||
for (Map.Entry<String, Long> entry : categoryCounts.entrySet()) {
|
||||
labels.set(i, Json.create(entry.getKey()));
|
||||
values.set(i, Json.create(entry.getValue()));
|
||||
backgroundColors.set(i, Json.create(defaultColors[i % defaultColors.length]));
|
||||
i++;
|
||||
labels.set(index, Json.create(entry.getKey()));
|
||||
values.set(index, Json.create(entry.getValue()));
|
||||
colors.set(index, Json.create(defaultColors[index % defaultColors.length]));
|
||||
index++;
|
||||
}
|
||||
|
||||
JsonObject dataset = Json.createObject();
|
||||
dataset.put("label", "Einträge nach Kategorie");
|
||||
dataset.put("label", "Entries by Category");
|
||||
dataset.put("data", values);
|
||||
dataset.put("backgroundColor", backgroundColors);
|
||||
dataset.put("backgroundColor", colors);
|
||||
|
||||
JsonArray datasets = Json.createArray();
|
||||
datasets.set(0, dataset);
|
||||
@@ -105,19 +117,21 @@ public class ChartJsComponent extends Div {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Liniendiagramm für Überstunden-Saldo (in Stunden)
|
||||
public static JsonObject generateLineChartData(List<LocalDate> dates, List<Double> saldoValues) {
|
||||
/**
|
||||
* Erstellt ein Liniendiagramm zur Darstellung des Überzeit-Saldos.
|
||||
*/
|
||||
public static JsonObject generateLineChartData(List<LocalDate> dates, List<Double> balanceValues) {
|
||||
JsonObject data = Json.createObject();
|
||||
JsonArray labels = Json.createArray();
|
||||
JsonArray values = Json.createArray();
|
||||
|
||||
for (int i = 0; i < dates.size(); i++) {
|
||||
labels.set(i, Json.create(dates.get(i).toString()));
|
||||
values.set(i, Json.create(saldoValues.get(i)));
|
||||
values.set(i, Json.create(balanceValues.get(i)));
|
||||
}
|
||||
|
||||
JsonObject dataset = Json.createObject();
|
||||
dataset.put("label", "Überstunden-Saldo (h)");
|
||||
dataset.put("label", "Overtime Balance (hours)");
|
||||
dataset.put("data", values);
|
||||
dataset.put("borderColor", "rgb(54, 162, 235)");
|
||||
dataset.put("tension", 0.1);
|
||||
|
||||
@@ -8,9 +8,9 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.router.RouterLink;
|
||||
import com.vaadin.flow.server.VaadinServletRequest;
|
||||
import com.vaadin.flow.server.VaadinServletResponse;
|
||||
import de.nilzbu.mytimetracker.ui.view.MainView;
|
||||
import de.nilzbu.mytimetracker.ui.view.TimeEntryView;
|
||||
import de.nilzbu.mytimetracker.ui.view.UserAdminView;
|
||||
import de.nilzbu.mytimetracker.ui.view.DashboardOverView;
|
||||
import de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView;
|
||||
import de.nilzbu.mytimetracker.ui.view.UserManagementView;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -19,44 +19,46 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
|
||||
@PermitAll
|
||||
|
||||
public class MainLayout extends AppLayout {
|
||||
|
||||
public MainLayout() {
|
||||
createHeader();
|
||||
buildHeader();
|
||||
}
|
||||
|
||||
private void createHeader() {
|
||||
H1 logo = new H1("Time Tracker");
|
||||
logo.getStyle()
|
||||
private void buildHeader() {
|
||||
H1 title = new H1("Time Tracker");
|
||||
title.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-l)")
|
||||
.set("margin", "0");
|
||||
|
||||
RouterLink homeLink = new RouterLink("Dashboard", MainView.class);
|
||||
RouterLink dashboardLink = new RouterLink("Dashboard", DashboardOverView.class);
|
||||
RouterLink bookingsLink = new RouterLink("Bookings", TimeEntryView.class);
|
||||
RouterLink userAdminView = new RouterLink("Admin", UserAdminView.class);
|
||||
homeLink.getStyle().set("margin-left", "2em");
|
||||
RouterLink adminLink = new RouterLink("Admin", UserManagementView.class);
|
||||
|
||||
dashboardLink.getStyle().set("margin-left", "2em");
|
||||
bookingsLink.getStyle().set("margin-left", "2em");
|
||||
userAdminView.getStyle().set("margin-left", "2em");
|
||||
adminLink.getStyle().set("margin-left", "2em");
|
||||
|
||||
Button logoutButton = new Button("Logout", event -> handleLogout());
|
||||
Button logoutButton = new Button("Logout", event -> performLogout());
|
||||
|
||||
HorizontalLayout header = new HorizontalLayout(logo, homeLink, bookingsLink, userAdminView, logoutButton);
|
||||
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
header.setWidthFull();
|
||||
header.setPadding(true);
|
||||
header.setSpacing(true);
|
||||
HorizontalLayout headerLayout = new HorizontalLayout(
|
||||
title, dashboardLink, bookingsLink, adminLink, logoutButton
|
||||
);
|
||||
headerLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
headerLayout.setWidthFull();
|
||||
headerLayout.setPadding(true);
|
||||
headerLayout.setSpacing(true);
|
||||
|
||||
addToNavbar(header);
|
||||
addToNavbar(headerLayout);
|
||||
}
|
||||
|
||||
private void handleLogout() {
|
||||
private void performLogout() {
|
||||
HttpServletRequest request = VaadinServletRequest.getCurrent().getHttpServletRequest();
|
||||
HttpServletResponse response = VaadinServletResponse.getCurrent().getHttpServletResponse();
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (auth != null) {
|
||||
new SecurityContextLogoutHandler().logout(request, response, auth);
|
||||
if (authentication != null) {
|
||||
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||
}
|
||||
|
||||
getUI().ifPresent(ui -> ui.getPage().setLocation("/login"));
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package de.nilzbu.mytimetracker.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.component.ChartJsComponent;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import de.nilzbu.mytimetracker.ui.widget.KeyFigureWidget;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@PermitAll
|
||||
@PageTitle("Dashboard")
|
||||
@Route(value = "", layout = MainLayout.class)
|
||||
public class DashboardOverView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
private User currentUser;
|
||||
|
||||
private final ComboBox<Integer> yearSelector = new ComboBox<>("Year");
|
||||
private final ComboBox<Integer> monthSelector = new ComboBox<>("Month");
|
||||
private final ComboBox<Integer> quarterSelector = new ComboBox<>("Quarter");
|
||||
|
||||
private final Div filterContainer = new Div();
|
||||
private final Div contentContainer = new Div();
|
||||
|
||||
private final BiFunction<List<TimeEntry>, DayStatus, String> calculateDaysWithDayStatus =
|
||||
(scopedEntries, status) -> "%d".formatted(scopedEntries.stream().filter(entry -> entry.getStatus().equals(status)).count());
|
||||
|
||||
public DashboardOverView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
configureLayout();
|
||||
initializeCurrentUser();
|
||||
createFilterSelectors();
|
||||
renderTabsAndContent();
|
||||
}
|
||||
|
||||
private void configureLayout() {
|
||||
setPadding(false);
|
||||
setSpacing(false);
|
||||
setMargin(false);
|
||||
|
||||
contentContainer.setSizeFull();
|
||||
setFlexGrow(1, contentContainer);
|
||||
}
|
||||
|
||||
private void initializeCurrentUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
this.currentUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new RuntimeException("User not found: " + username));
|
||||
}
|
||||
|
||||
private void createFilterSelectors() {
|
||||
int currentYear = LocalDate.now().getYear();
|
||||
int currentMonth = LocalDate.now().getMonthValue();
|
||||
int currentQuarter = (currentMonth - 1) / 3 + 1;
|
||||
|
||||
yearSelector.setItems(IntStream.rangeClosed(2020, currentYear).boxed().toList());
|
||||
yearSelector.setValue(currentYear);
|
||||
|
||||
monthSelector.setItems(IntStream.rangeClosed(1, 12).boxed().toList());
|
||||
monthSelector.setValue(currentMonth);
|
||||
|
||||
quarterSelector.setItems(1, 2, 3, 4);
|
||||
quarterSelector.setValue(currentQuarter);
|
||||
}
|
||||
|
||||
private void renderTabsAndContent() {
|
||||
Tabs scopeTabs = new Tabs(
|
||||
new Tab("All"),
|
||||
new Tab("Year"),
|
||||
new Tab("Quarter"),
|
||||
new Tab("Month")
|
||||
);
|
||||
|
||||
scopeTabs.addSelectedChangeListener(e ->
|
||||
updateContent(scopeTabs.getSelectedTab().getLabel())
|
||||
);
|
||||
|
||||
add(new H2("Dashboard"), scopeTabs, filterContainer, contentContainer);
|
||||
updateContent("All");
|
||||
}
|
||||
|
||||
private void updateContent(String scope) {
|
||||
contentContainer.removeAll();
|
||||
filterContainer.removeAll();
|
||||
|
||||
yearSelector.setVisible(false);
|
||||
monthSelector.setVisible(false);
|
||||
quarterSelector.setVisible(false);
|
||||
|
||||
List<TimeEntry> entries = switch (scope) {
|
||||
case "Month" -> {
|
||||
configureFilterScope(yearSelector, monthSelector);
|
||||
yield timeEntryService.getEntriesForMonth(currentUser, yearSelector.getValue(), monthSelector.getValue());
|
||||
}
|
||||
case "Quarter" -> {
|
||||
configureFilterScope(yearSelector, quarterSelector);
|
||||
yield timeEntryService.getEntriesForQuarter(currentUser, yearSelector.getValue(), quarterSelector.getValue());
|
||||
}
|
||||
case "Year" -> {
|
||||
configureFilterScope(yearSelector, null);
|
||||
yield timeEntryService.getEntriesForYear(currentUser, yearSelector.getValue());
|
||||
}
|
||||
default -> timeEntryService.getEntriesForUser(currentUser);
|
||||
};
|
||||
|
||||
renderKeyFigures(entries);
|
||||
renderCharts(entries);
|
||||
}
|
||||
|
||||
private void configureFilterScope(ComboBox<?>... selectors) {
|
||||
for (ComboBox<?> selector : selectors) {
|
||||
if (selector == null) continue;
|
||||
filterContainer.add(selector);
|
||||
selector.setVisible(true);
|
||||
}
|
||||
|
||||
yearSelector.addValueChangeListener(e -> updateContent("Year"));
|
||||
monthSelector.addValueChangeListener(e -> updateContent("Month"));
|
||||
quarterSelector.addValueChangeListener(e -> updateContent("Quarter"));
|
||||
}
|
||||
|
||||
private void renderCharts(List<TimeEntry> scopedEntries) {
|
||||
List<TimeEntry> allEntries = timeEntryService.getEntriesForUser(currentUser);
|
||||
|
||||
HorizontalLayout chartLayout = new HorizontalLayout();
|
||||
chartLayout.setPadding(false);
|
||||
chartLayout.setSpacing(true);
|
||||
|
||||
Component categoryChart = createCategoryBarChart(scopedEntries);
|
||||
categoryChart.getStyle().setBackgroundColor("lightgray");
|
||||
categoryChart.getStyle().setBorder("1px solid black" );
|
||||
categoryChart.getStyle().setMargin("10px");
|
||||
|
||||
Component overtimeChart = createOvertimeLineChart(allEntries, scopedEntries);
|
||||
overtimeChart.getStyle().setBackgroundColor("lightgray");
|
||||
overtimeChart.getStyle().setBorder("1px solid black" );
|
||||
overtimeChart.getStyle().setMargin("10px");
|
||||
|
||||
chartLayout.add(categoryChart, overtimeChart);
|
||||
chartLayout.setFlexGrow(1, categoryChart);
|
||||
chartLayout.setFlexGrow(1, overtimeChart);
|
||||
|
||||
contentContainer.add(chartLayout);
|
||||
}
|
||||
|
||||
private void renderKeyFigures(List<TimeEntry> scopedEntries) {
|
||||
HorizontalLayout keyFigureLayout = new HorizontalLayout();
|
||||
|
||||
KeyFigureWidget workingDays = new KeyFigureWidget("Working Days", "" + scopedEntries.size());
|
||||
|
||||
KeyFigureWidget remoteDays = new KeyFigureWidget(
|
||||
"Remote Days", calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.REMOTE)
|
||||
);
|
||||
|
||||
KeyFigureWidget officeDays = new KeyFigureWidget(
|
||||
"Office Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.OFFICE)
|
||||
);
|
||||
|
||||
KeyFigureWidget vacationDays = new KeyFigureWidget(
|
||||
"Vacation Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.VACATION)
|
||||
);
|
||||
|
||||
KeyFigureWidget sickDays = new KeyFigureWidget(
|
||||
"Sick Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.SICK)
|
||||
);
|
||||
|
||||
KeyFigureWidget timeBalance = new KeyFigureWidget(
|
||||
"Time Balance",
|
||||
getTimeBalance(scopedEntries)
|
||||
);
|
||||
|
||||
keyFigureLayout.getStyle().setBackgroundColor("lightgray");
|
||||
keyFigureLayout.getStyle().setBorder("1px solid black" );
|
||||
keyFigureLayout.getStyle().setMargin("10px");
|
||||
|
||||
keyFigureLayout.add(timeBalance, workingDays, remoteDays, officeDays, vacationDays, sickDays);
|
||||
|
||||
contentContainer.add(keyFigureLayout);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getTimeBalance(List<TimeEntry> scopedEntries) {
|
||||
Integer minutes = scopedEntries.stream()
|
||||
.map(entry -> timeEntryService.calculateNetWorkMinutes(entry) - entry.getTargetMinutes())
|
||||
.reduce(0, Integer::sum);
|
||||
|
||||
double hours = minutes / 60.0;
|
||||
return "%.2f h".formatted(hours);
|
||||
}
|
||||
|
||||
private Component createCategoryBarChart(List<TimeEntry> entries) {
|
||||
Map<String, Long> statusCount = entries.stream()
|
||||
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
|
||||
|
||||
return new ChartJsComponent(
|
||||
ChartJsComponent.generateBarChartData(statusCount),
|
||||
"Entries by Category");
|
||||
}
|
||||
|
||||
private Component createOvertimeLineChart(List<TimeEntry> allEntries, List<TimeEntry> scopedEntries) {
|
||||
Map<LocalDate, Double> dailySaldo = new TreeMap<>();
|
||||
int accumulatedMinutes = 0;
|
||||
|
||||
List<TimeEntry> sortedEntries = allEntries.stream()
|
||||
.sorted(Comparator.comparing(TimeEntry::getDate))
|
||||
.toList();
|
||||
|
||||
for (TimeEntry entry : sortedEntries) {
|
||||
accumulatedMinutes += timeEntryService.calculateNetWorkMinutes(entry) - entry.getTargetMinutes();
|
||||
dailySaldo.put(entry.getDate(), accumulatedMinutes / 60.0);
|
||||
}
|
||||
|
||||
List<LocalDate> scopedDates = scopedEntries.stream()
|
||||
.map(TimeEntry::getDate)
|
||||
.sorted()
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
List<Double> saldoValues = scopedDates.stream()
|
||||
.map(date -> dailySaldo.getOrDefault(date, 0.0))
|
||||
.toList();
|
||||
|
||||
return new ChartJsComponent(
|
||||
ChartJsComponent.generateLineChartData(scopedDates, saldoValues),
|
||||
"Overtime Balance Over Time (in hours)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package de.nilzbu.mytimetracker.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.component.ChartJsComponent;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@PermitAll
|
||||
@PageTitle("Startseite")
|
||||
@Route(value = "", layout = MainLayout.class)
|
||||
public class MainView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private User currentUser;
|
||||
private final ComboBox<Integer> jahrAuswahl = new ComboBox<>("Jahr");
|
||||
private final ComboBox<Integer> monatAuswahl = new ComboBox<>("Monat");
|
||||
private final ComboBox<Integer> quartalAuswahl = new ComboBox<>("Quartal");
|
||||
|
||||
private final Div filterContainer = new Div();
|
||||
private final Div content = new Div();
|
||||
|
||||
public MainView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(false);
|
||||
setSpacing(false);
|
||||
setMargin(false);
|
||||
|
||||
initializeCurrentUser();
|
||||
configureFilter();
|
||||
|
||||
Tabs tabs = new Tabs(new Tab("Gesamt"), new Tab("Jahr"), new Tab("Quartal"), new Tab("Monat"));
|
||||
tabs.addSelectedChangeListener(e -> updateContent(tabs.getSelectedTab().getLabel()));
|
||||
|
||||
add(new H2("Dashboard"), tabs, filterContainer, content);
|
||||
|
||||
content.setSizeFull();
|
||||
content.getStyle().set("overflow", "auto");
|
||||
setFlexGrow(1, content);
|
||||
|
||||
updateContent("Gesamt");
|
||||
}
|
||||
|
||||
private void initializeCurrentUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
this.currentUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new RuntimeException("Benutzer nicht gefunden"));
|
||||
}
|
||||
|
||||
private void configureFilter() {
|
||||
jahrAuswahl.setItems(IntStream.rangeClosed(2020, LocalDate.now().getYear()).boxed().toList());
|
||||
jahrAuswahl.setValue(LocalDate.now().getYear());
|
||||
|
||||
monatAuswahl.setItems(IntStream.rangeClosed(1, 12).boxed().toList());
|
||||
monatAuswahl.setValue(LocalDate.now().getMonthValue());
|
||||
|
||||
quartalAuswahl.setItems(1, 2, 3, 4);
|
||||
quartalAuswahl.setValue((LocalDate.now().getMonthValue() - 1) / 3 + 1);
|
||||
}
|
||||
|
||||
private void updateContent(String scope) {
|
||||
content.removeAll();
|
||||
filterContainer.removeAll();
|
||||
|
||||
jahrAuswahl.setVisible(false);
|
||||
monatAuswahl.setVisible(false);
|
||||
quartalAuswahl.setVisible(false);
|
||||
|
||||
List<TimeEntry> entries;
|
||||
|
||||
switch (scope) {
|
||||
case "Monat" -> {
|
||||
filterContainer.add(jahrAuswahl, monatAuswahl);
|
||||
jahrAuswahl.setVisible(true);
|
||||
monatAuswahl.setVisible(true);
|
||||
|
||||
jahrAuswahl.addValueChangeListener(e -> updateContent("Monat"));
|
||||
monatAuswahl.addValueChangeListener(e -> updateContent("Monat"));
|
||||
|
||||
entries = timeEntryService.getEntriesForMonth(currentUser, jahrAuswahl.getValue(), monatAuswahl.getValue());
|
||||
}
|
||||
case "Quartal" -> {
|
||||
filterContainer.add(jahrAuswahl, quartalAuswahl);
|
||||
jahrAuswahl.setVisible(true);
|
||||
quartalAuswahl.setVisible(true);
|
||||
|
||||
jahrAuswahl.addValueChangeListener(e -> updateContent("Quartal"));
|
||||
quartalAuswahl.addValueChangeListener(e -> updateContent("Quartal"));
|
||||
|
||||
entries = timeEntryService.getEntriesForQuarter(currentUser, jahrAuswahl.getValue(), quartalAuswahl.getValue());
|
||||
}
|
||||
case "Jahr" -> {
|
||||
filterContainer.add(jahrAuswahl);
|
||||
jahrAuswahl.setVisible(true);
|
||||
|
||||
jahrAuswahl.addValueChangeListener(e -> updateContent("Jahr"));
|
||||
|
||||
entries = timeEntryService.getEntriesForYear(currentUser, jahrAuswahl.getValue());
|
||||
}
|
||||
default -> {
|
||||
entries = timeEntryService.getEntriesForUser(currentUser);
|
||||
}
|
||||
}
|
||||
|
||||
renderCharts(entries);
|
||||
}
|
||||
|
||||
private void renderCharts(List<TimeEntry> scopedEntries) {
|
||||
List<TimeEntry> allEntries = timeEntryService.getEntriesForUser(currentUser);
|
||||
|
||||
VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setPadding(false);
|
||||
layout.setSpacing(true);
|
||||
|
||||
Component first = createCategoryBarChart(scopedEntries);
|
||||
Component second = createOvertimeLineChart(allEntries, scopedEntries);
|
||||
|
||||
first.getStyle().set("minHeight", "45vh");
|
||||
second.getStyle().set("minHeight", "50vh");
|
||||
|
||||
layout.add(first, second);
|
||||
layout.setFlexGrow(1, first);
|
||||
layout.setFlexGrow(1, second);
|
||||
|
||||
content.add(layout);
|
||||
}
|
||||
|
||||
private Component createCategoryBarChart(List<TimeEntry> entries) {
|
||||
Map<String, Long> categoryCounts = entries.stream()
|
||||
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
|
||||
|
||||
return new ChartJsComponent(
|
||||
ChartJsComponent.generateBarChartData(categoryCounts),
|
||||
"Einträge nach Kategorie");
|
||||
}
|
||||
|
||||
private Component createOvertimeLineChart(List<TimeEntry> allEntries, List<TimeEntry> scopeEntries) {
|
||||
Map<LocalDate, Double> saldoPerDay = new TreeMap<>();
|
||||
int accumulatedMinutes = 0;
|
||||
|
||||
List<TimeEntry> sorted = allEntries.stream()
|
||||
.sorted(Comparator.comparing(TimeEntry::getDate))
|
||||
.toList();
|
||||
|
||||
for (TimeEntry entry : sorted) {
|
||||
accumulatedMinutes += timeEntryService.calculateNetWorkMinutes(entry) - entry.getTargetMinutes();
|
||||
saldoPerDay.put(entry.getDate(), accumulatedMinutes / 60.0); // Umwandlung in Stunden
|
||||
}
|
||||
|
||||
List<LocalDate> datesInScope = scopeEntries.stream()
|
||||
.map(TimeEntry::getDate)
|
||||
.sorted()
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
List<Double> values = datesInScope.stream()
|
||||
.map(date -> saldoPerDay.getOrDefault(date, 0.0))
|
||||
.toList();
|
||||
|
||||
return new ChartJsComponent(
|
||||
ChartJsComponent.generateLineChartData(datesInScope, values),
|
||||
"Überstunden-Saldo im Verlauf (in Stunden)");
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package de.nilzbu.mytimetracker.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.timepicker.TimePicker;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Route(value = "time-entry", layout = MainLayout.class)
|
||||
@PageTitle("Time Entry")
|
||||
@PermitAll
|
||||
public class TimeEntryView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final DatePicker datePicker = new DatePicker("Date");
|
||||
private final TimePicker startTimePicker = new TimePicker("Start Time");
|
||||
private final TimePicker endTimePicker = new TimePicker("End Time");
|
||||
private final NumberField breakMinutesField = new NumberField("Break (minutes)");
|
||||
private final NumberField targetMinutesField = new NumberField("Target Time (minutes)");
|
||||
private final TextArea commentField = new TextArea("Comment");
|
||||
private final ComboBox<DayStatus> statusComboBox = new ComboBox<>("Status");
|
||||
|
||||
private final Button saveButton = new Button("Save");
|
||||
private final Button updateButton = new Button("Update");
|
||||
private final Button deleteButton = new Button("Delete");
|
||||
|
||||
private final Grid<TimeEntry> entryGrid = new Grid<>(TimeEntry.class, false);
|
||||
|
||||
private TimeEntry selectedEntry = null;
|
||||
private User currentUser;
|
||||
|
||||
public TimeEntryView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
initializeCurrentUser();
|
||||
configureFields();
|
||||
configureButtons();
|
||||
configureGrid();
|
||||
|
||||
datePicker.addValueChangeListener(e -> checkIfDateAlreadyExists());
|
||||
|
||||
add(createFormLayout(), createButtonLayout(), entryGrid);
|
||||
refreshGrid();
|
||||
checkIfDateAlreadyExists(); // Initiale Prüfung
|
||||
}
|
||||
|
||||
private void initializeCurrentUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
this.currentUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
|
||||
private void configureFields() {
|
||||
datePicker.setValue(LocalDate.now());
|
||||
breakMinutesField.setValue(30.0);
|
||||
targetMinutesField.setValue(480.0);
|
||||
|
||||
statusComboBox.setItems(DayStatus.values());
|
||||
statusComboBox.setValue(DayStatus.REMOTE);
|
||||
}
|
||||
|
||||
private HorizontalLayout createFormLayout() {
|
||||
return new HorizontalLayout(
|
||||
datePicker,
|
||||
startTimePicker,
|
||||
endTimePicker,
|
||||
breakMinutesField,
|
||||
targetMinutesField,
|
||||
commentField,
|
||||
statusComboBox
|
||||
);
|
||||
}
|
||||
|
||||
private HorizontalLayout createButtonLayout() {
|
||||
updateButton.setEnabled(false);
|
||||
deleteButton.setEnabled(false);
|
||||
return new HorizontalLayout(saveButton, updateButton, deleteButton);
|
||||
}
|
||||
|
||||
private void configureButtons() {
|
||||
saveButton.addClickListener(e -> {
|
||||
if (!isDateAvailable(datePicker.getValue())) {
|
||||
Notification.show("An entry already exists for this date.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeEntry entry = TimeEntry.builder()
|
||||
.user(currentUser)
|
||||
.date(datePicker.getValue())
|
||||
.startTime(startTimePicker.getValue())
|
||||
.endTime(endTimePicker.getValue())
|
||||
.pauseMinutes(breakMinutesField.getValue().intValue())
|
||||
.targetMinutes(targetMinutesField.getValue().intValue())
|
||||
.status(statusComboBox.getValue())
|
||||
.comment(commentField.getValue())
|
||||
.build();
|
||||
|
||||
timeEntryService.save(entry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry saved");
|
||||
});
|
||||
|
||||
updateButton.addClickListener(e -> {
|
||||
if (selectedEntry == null) return;
|
||||
|
||||
selectedEntry.setDate(datePicker.getValue());
|
||||
selectedEntry.setStartTime(startTimePicker.getValue());
|
||||
selectedEntry.setEndTime(endTimePicker.getValue());
|
||||
selectedEntry.setPauseMinutes(breakMinutesField.getValue().intValue());
|
||||
selectedEntry.setTargetMinutes(targetMinutesField.getValue().intValue());
|
||||
selectedEntry.setStatus(statusComboBox.getValue());
|
||||
selectedEntry.setComment(commentField.getValue());
|
||||
|
||||
timeEntryService.save(selectedEntry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry updated");
|
||||
});
|
||||
|
||||
deleteButton.addClickListener(e -> {
|
||||
if (selectedEntry == null) return;
|
||||
|
||||
timeEntryService.delete(selectedEntry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry deleted");
|
||||
});
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
entryGrid.addColumn(TimeEntry::getDate).setHeader("Date");
|
||||
entryGrid.addColumn(TimeEntry::getStartTime).setHeader("Start");
|
||||
entryGrid.addColumn(TimeEntry::getEndTime).setHeader("End");
|
||||
entryGrid.addColumn(TimeEntry::getPauseMinutes).setHeader("Break (min)");
|
||||
entryGrid.addColumn(TimeEntry::getTargetMinutes).setHeader("Target (min)");
|
||||
entryGrid.addColumn(timeEntryService::calculateNetWorkMinutes).setHeader("Actual (min)");
|
||||
entryGrid.addColumn(TimeEntry::getStatus).setHeader("Status");
|
||||
entryGrid.addColumn(TimeEntry::getComment).setHeader("Comment");
|
||||
entryGrid.addColumn(timeEntryService::calculateDeviation).setHeader("Deviation (min)");
|
||||
|
||||
entryGrid.asSingleSelect().addValueChangeListener(event -> {
|
||||
selectedEntry = event.getValue();
|
||||
if (selectedEntry != null) {
|
||||
datePicker.setValue(selectedEntry.getDate());
|
||||
startTimePicker.setValue(selectedEntry.getStartTime());
|
||||
endTimePicker.setValue(selectedEntry.getEndTime());
|
||||
breakMinutesField.setValue((double) selectedEntry.getPauseMinutes());
|
||||
targetMinutesField.setValue((double) selectedEntry.getTargetMinutes());
|
||||
statusComboBox.setValue(selectedEntry.getStatus());
|
||||
commentField.setValue(selectedEntry.getComment() != null ? selectedEntry.getComment() : "");
|
||||
|
||||
updateButton.setEnabled(true);
|
||||
deleteButton.setEnabled(true);
|
||||
saveButton.setEnabled(false);
|
||||
} else {
|
||||
updateButton.setEnabled(false);
|
||||
deleteButton.setEnabled(false);
|
||||
checkIfDateAlreadyExists();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshGrid() {
|
||||
entryGrid.setItems(timeEntryService.getEntriesForUser(currentUser));
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
selectedEntry = null;
|
||||
datePicker.setValue(LocalDate.now());
|
||||
startTimePicker.clear();
|
||||
endTimePicker.clear();
|
||||
breakMinutesField.setValue(30.0);
|
||||
targetMinutesField.setValue(480.0);
|
||||
statusComboBox.setValue(DayStatus.REMOTE);
|
||||
commentField.clear();
|
||||
updateButton.setEnabled(false);
|
||||
deleteButton.setEnabled(false);
|
||||
checkIfDateAlreadyExists();
|
||||
}
|
||||
|
||||
private void checkIfDateAlreadyExists() {
|
||||
LocalDate date = datePicker.getValue();
|
||||
if (!isDateAvailable(date)) {
|
||||
saveButton.setEnabled(false);
|
||||
} else {
|
||||
saveButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDateAvailable(LocalDate date) {
|
||||
return timeEntryService.getEntriesForUser(currentUser).stream()
|
||||
.noneMatch(entry -> entry.getDate().equals(date));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user