(props.initialPayload || {});
30 | const scopeType = definitionId?.startsWith('PRC') ? 'process' : 'case';
31 | const queryClient = useQueryClient();
32 |
33 | if (!definitionId) {
34 | throw new Error('Definition ID is missing');
35 | }
36 |
37 | const navigate = useNavigate();
38 |
39 | // Query to fetch the form definition
40 | const {
41 | data: fetchedFormDefinition,
42 | isPending,
43 | error,
44 | isSuccess,
45 | } = useQuery({
46 | queryKey: ['form', definitionId],
47 | queryFn: () => getFormByType(formType, scopeType, definitionId),
48 | enabled: !!definitionId,
49 | retry: false,
50 | });
51 |
52 | // Form submission mutation
53 | const { mutate: submit } = useMutation({
54 | mutationFn: startInstance,
55 | mutationKey: ['outcome'],
56 | onError: (error: FlyableApiError) => {
57 | enqueueSnackbar(error.message || 'Could not start instance', { variant: 'error' });
58 | },
59 | onSuccess: () => {
60 | enqueueSnackbar('Instance started successfully!');
61 | queryClient.invalidateQueries({ queryKey: ['instances', scopeType] });
62 | navigate('/');
63 | },
64 | });
65 |
66 | // If we're still waiting for the form definition, show a loading indicator, otherwise render the form or an error
67 | if (isPending) {
68 | return ;
69 | } else if (isSuccess) {
70 | return renderForm(fetchedFormDefinition);
71 | } else if (error) {
72 | return renderForm(defaultForm);
73 | }
74 |
75 | // Renders the form with the given layout. If the form definition has no outcomes, add the default outcome
76 | function renderForm(formLayout: FormLayout) {
77 | // Check if the form definition has outcomes, if not add the default outcomes
78 | if (!formLayout.outcomes || formLayout.outcomes.length === 0) {
79 | formLayout.outcomes = [defaultOutcome];
80 | }
81 |
82 | return (
83 | <>
84 |
85 |
95 | >
96 | );
97 | }
98 |
99 | return ;
100 | };
101 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/form/workForm.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from '@flowable/forms';
2 | import { getWorkFormByInstanceId, getWorkFormDataByInstanceId } from '../api/flyableApi';
3 | import type { FormLayout, Payload } from '@flowable/forms/flowable-forms/src/flw/Model';
4 | import { useParams } from 'react-router-dom';
5 | import { useQuery } from '@tanstack/react-query';
6 | import FormSkeleton from './formSkeleton';
7 |
8 | /**
9 | * WorkForm component for rendering a work form based on the instance ID (case or process).
10 | */
11 | export const WorkForm = () => {
12 | const { instanceId } = useParams();
13 | if (!instanceId) {
14 | throw new Error('Instance ID is missing');
15 | }
16 |
17 | // Query to fetch the form definition
18 | const { data: fetchedFormDefinition, isPending: isLoadingFormDefinition } = useQuery({
19 | queryKey: ['form', instanceId],
20 | queryFn: () => getWorkFormByInstanceId(instanceId),
21 | enabled: !!instanceId,
22 | refetchOnWindowFocus: false,
23 | retry: false,
24 | });
25 |
26 | // Query to fetch the form data (variables)
27 | const { data: workformData, isPending: isLoadingWorkformData } = useQuery({
28 | queryKey: ['workform-data', instanceId],
29 | queryFn: () => getWorkFormDataByInstanceId(instanceId),
30 | enabled: !!instanceId,
31 | retry: false,
32 | });
33 |
34 | if (isLoadingFormDefinition || isLoadingWorkformData) {
35 | return ;
36 | }
37 |
38 | if (fetchedFormDefinition && workformData) {
39 | return renderForm(fetchedFormDefinition);
40 | }
41 |
42 | /**
43 | * Renders the form with the given layout.
44 | * @param {FormLayout} formLayout - Layout of the form to render.
45 | */
46 | function renderForm(formLayout: FormLayout) {
47 | return (
48 | <>
49 |
50 |
51 |
52 | >
53 | );
54 | }
55 |
56 | return ;
57 | };
58 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import './style/index.css';
3 | import App from './app';
4 |
5 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
6 | root.render();
7 |
8 | // @ts-expect-error - This is a global variable that is read by the App/Case View
9 | global.flowable = { endpoints: { baseUrl: process.env.REACT_APP_BASE_URL || 'http://localhost:8090' } };
10 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/layout/backNavigation.tsx:
--------------------------------------------------------------------------------
1 | import { Box, IconButton } from '@mui/material';
2 | import { ArrowBack } from '@mui/icons-material';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | /**
6 | * Simple component for rendering a back navigation button.
7 | * Just goes back in the history when clicked.
8 | */
9 | export const BackNavigation = () => {
10 | const navigate = useNavigate();
11 |
12 | return (
13 |
14 | navigate(-1)}>
15 |
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | import { AppBar, Toolbar, Typography } from '@mui/material';
2 |
3 | /**
4 | * Footer of the application.
5 | */
6 | export const Footer = () => (
7 |
8 |
9 | © {new Date().getFullYear()} Flowable AG
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/layout/header.tsx:
--------------------------------------------------------------------------------
1 | import { AppBar, Box, Toolbar, Typography } from '@mui/material';
2 | import { Link } from 'react-router-dom';
3 | import React from 'react';
4 | import { HeaderMenu } from './headerMenu';
5 |
6 | /**
7 | * Header of the Flyable Booking Portal with navigation and user information.
8 | */
9 | export const Header = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | Flyable Portal
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/layout/headerMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Avatar, Box, Menu, MenuItem, Typography } from '@mui/material';
3 | import LogoutIcon from '@mui/icons-material/Logout';
4 | import { useAuthStore } from '../store/authStore';
5 | import { useNavigate } from 'react-router-dom';
6 |
7 | /**
8 | * Component for displaying a user menu in the header.
9 | * Also contains logout logic.
10 | */
11 | export const HeaderMenu = () => {
12 | const { userInfos, clearJwt, clearUserInfos } = useAuthStore();
13 | const [anchorEl, setAnchorEl] = useState(null);
14 | const navigate = useNavigate();
15 |
16 | const handleMenuOpen = (event: React.MouseEvent) => {
17 | setAnchorEl(event.currentTarget);
18 | };
19 |
20 | const handleMenuClose = () => {
21 | setAnchorEl(null);
22 | };
23 |
24 | const handleLogout = () => {
25 | clearJwt();
26 | clearUserInfos();
27 | handleMenuClose();
28 | navigate('/login');
29 | };
30 |
31 | return (
32 | <>
33 |
34 |
35 |
36 | {`${userInfos.firstName} ${userInfos.lastName}`}
37 | {userInfos.username}
38 |
39 |
40 |
46 | >
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/layout/mainContent.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Container, Fab, Paper } from '@mui/material';
2 | import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
3 | import { useLocation, useNavigate } from 'react-router-dom';
4 | import { Add } from '@mui/icons-material';
5 | import BookingPortalRoutes from '../routes/bookingPortalRoutes';
6 | import '../style/flwFormsStyle.css';
7 | import { BackNavigation } from './backNavigation';
8 |
9 | /**
10 | * This component renders all the meat and bones of the application.
11 | * It includes an error boundary handling and a floating action button for navigation.
12 | */
13 | export const MainContent = () => {
14 | const onReset = () => {};
15 |
16 | const navigate = useNavigate();
17 | const isHome = useLocation().pathname === '/';
18 |
19 | const fallbackRender = (fallbackProps: FallbackProps) => (
20 |
21 | Something went wrong!
22 | {fallbackProps.error.message}
23 |
24 | );
25 |
26 | return (
27 |
37 |
38 |
39 | {!isHome && }
40 |
41 |
42 |
43 |
44 |
45 | navigate('/process/')} sx={{ position: 'fixed', right: 20, bottom: 100 }}>
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/apps/appDefinitionList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getAppDefinitions } from '../../api/flyableApi';
3 | import ItemList from '../../shared/itemList';
4 | import { AppDefinitionRepresentation } from '@flowable/forms/flowable-work-api/work/workApiModel';
5 |
6 | /**
7 | * Shows all the apps in Flowable Work.
8 | */
9 | const AppDefinitionList = () => (
10 |
11 | queryKey={['apps']}
12 | queryFn={getAppDefinitions}
13 | title="Select an app"
14 | getDataFn={data => data}
15 | getItemKey={item => item.id}
16 | getItemText={item => item.label}
17 | getItemLink={item => `/apps/${item.key}/`}
18 | emptyMessage="No app definitions found"
19 | errorMessage="Could not fetch process definitions. Check if Flowable Work is running."
20 | />
21 | );
22 |
23 | export default AppDefinitionList;
24 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/apps/appView.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router-dom';
2 | import { FlowableFlowApp } from '@flowable/work-views';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { getAppDefinitionByKey } from '../../api/flyableApi';
5 | import { CircularProgress } from '@mui/material';
6 | import '@flowable/work-views/dist/index.css';
7 |
8 | /**
9 | * Shows the view of a single app through the Flowable "App View" component.
10 | * FIXME: This is a work in progress as there are still some styling and auth issues, so it is currently not shown.
11 | */
12 | export const AppView = () => {
13 | const { appKey } = useParams();
14 | if (!appKey) {
15 | throw new Error('App key is required');
16 | }
17 |
18 | const { data: appDefinition } = useQuery({
19 | queryKey: ['appDefinition', appKey],
20 | queryFn: () => getAppDefinitionByKey(appKey),
21 | });
22 |
23 | if (!appDefinition) {
24 | return ;
25 | }
26 |
27 | return (
28 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/myFlights.tsx:
--------------------------------------------------------------------------------
1 | import { getTicketProcesses } from '../../api/flyableApi';
2 | import React from 'react';
3 | import ItemList from '../../shared/itemList';
4 | import { TICKET_PROCESS_KEY } from '../../util/modelKeys';
5 | import { ProcessInstanceResponse } from '@flowable/forms/flowable-work-api/process/processApiModel';
6 |
7 | /**
8 | * Shows a list of all "ticket booking" process instances that the user has started.
9 | * Since we are using the Flowable REST API, we do not have to take care of filtering/permissions.
10 | */
11 |
12 | const MyFlights = () => (
13 |
14 | queryKey={['processInstances', TICKET_PROCESS_KEY]}
15 | title="Open Process Instances"
16 | queryFn={getTicketProcesses}
17 | getDataFn={data => data?.data}
18 | getItemKey={item => item.id}
19 | getItemText={item => item.name || item.id}
20 | getItemLink={item => `/my-flights/${item.id}`}
21 | emptyMessage="No process instances found"
22 | errorMessage="Could not fetch process instances."
23 | />
24 | );
25 |
26 | export default MyFlights;
27 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/bookFlightWizard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useState } from 'react';
3 | import { Box } from '@mui/material';
4 | import { useFlightStore } from '../../../store/flightStore';
5 | import { useNavigate } from 'react-router-dom';
6 | import { useMutation } from '@tanstack/react-query';
7 | import { enqueueSnackbar } from 'notistack';
8 | import { createTicketProcess } from '../../../api/flyableApi';
9 | import WizardSuccessMessage from './wizardSuccessMessage';
10 | import WizardControlButtons from './wizardControlButtons';
11 | import StepContent from './stepContent';
12 | import WizardStepper from './wizardStepper';
13 | import './style/step-styles.css';
14 |
15 | /**
16 | * Main component for a simple ticket booking wizard.
17 | * This component serves as an example on how to gather data which then creates a process in Flowable.
18 | * It demonstrates how scenarios where Flowable forms may not be suitable can be handled.
19 | */
20 | export const BookFlightWizard = () => {
21 | const [activeStep, setActiveStep] = useState(0);
22 | const [newProcessInstanceId, setNewProcessInstanceId] = useState('');
23 | const { initializeDummyData } = useFlightStore();
24 | const flightStore = useFlightStore.getState();
25 | const navigate = useNavigate();
26 |
27 | const steps = ['Flight Details', 'Choose Flight', 'Passenger Information', 'Payment', 'Review & Submit'];
28 |
29 | const { mutate: startInstanceMutation } = useMutation({
30 | mutationKey: ['startTicketInstance'],
31 | mutationFn: () => {
32 | const { flightDetails, passengerInfo, selectedFlight, billingAddress, optionalServices, bookingReference } = flightStore;
33 | const payload = {
34 | flightDetails,
35 | passengerInfo,
36 | selectedFlight,
37 | billingAddress,
38 | optionalServices,
39 | bookingReference,
40 | };
41 | return createTicketProcess(payload);
42 | },
43 | onSuccess: data => {
44 | setNewProcessInstanceId(data.id);
45 | enqueueSnackbar('Ticket created successfully!', { variant: 'success' });
46 | },
47 | onError: error => {
48 | enqueueSnackbar('Error creating ticket: ' + error.message, { variant: 'error' });
49 | },
50 | });
51 |
52 | const handleNext = () => {
53 | if (activeStep === steps.length - 1) {
54 | startInstanceMutation();
55 | } else {
56 | setActiveStep(prevActiveStep => prevActiveStep + 1);
57 | }
58 | };
59 |
60 | const handleBack = () => {
61 | setActiveStep(prevActiveStep => prevActiveStep - 1);
62 | };
63 |
64 | return (
65 | <>
66 |
72 |
73 | {newProcessInstanceId ? (
74 |
75 | ) : (
76 | <>
77 |
78 | {activeStep !== steps.length && }
79 | >
80 | )}
81 |
82 | {!newProcessInstanceId && }
83 |
84 | >
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/stepContent.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import FlightDetailsStep from './steps/flightDetailsStep';
3 | import ChooseFlightStep from './steps/chooseFlightStep';
4 | import PassengerInfoStep from './steps/passengerInfoStep';
5 | import PaymentStep from './steps/paymentStep';
6 | import ReviewStep from './steps/reviewStep';
7 |
8 | /**
9 | * Wrapper for the content of the Ticket Booking Wizard.
10 | */
11 | const StepContent = ({ step }: { step: number }) => {
12 | switch (step) {
13 | case 0:
14 | return ;
15 | case 1:
16 | return ;
17 | case 2:
18 | return ;
19 | case 3:
20 | return ;
21 | case 4:
22 | return ;
23 | default:
24 | return Unknown step;
25 | }
26 | };
27 |
28 | export default StepContent;
29 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/steps/chooseFlightStep.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Box, Card, CardActions, CardContent, FormControlLabel, Radio, RadioGroup, Typography } from '@mui/material';
3 | import { useFlightStore } from '../../../../store/flightStore';
4 |
5 | /**
6 | * Booking Wizard - Page for the Ticket Booking Wizard to choose a flight.
7 | */
8 | const randomMinutes = Math.floor(Math.random() * (8 * 60 - 40 + 1)) + 40;
9 | const generateRandomFlight = () => {
10 | const hours = Math.floor(randomMinutes / 60);
11 | const minutes = randomMinutes % 60;
12 | const duration = `${hours}h ${minutes}m`;
13 |
14 | const randomDepartureHour = Math.floor(Math.random() * 24);
15 | const randomDepartureMinute = Math.floor(Math.random() * 60);
16 | const departureTime = `${randomDepartureHour.toString().padStart(2, '0')}:${randomDepartureMinute.toString().padStart(2, '0')} ${randomDepartureHour < 12 ? 'AM' : 'PM'}`;
17 |
18 | const randomPrice = Math.floor(Math.random() * (800 - 200 + 1)) + 200;
19 |
20 | return {
21 | id: 1,
22 | departureTime,
23 | duration,
24 | airline: 'Flyable',
25 | price: `$${randomPrice}`,
26 | };
27 | };
28 |
29 | const ChooseFlightStep = () => {
30 | const { selectedFlight, setSelectedFlight } = useFlightStore();
31 | const [dummyFlight] = React.useState(generateRandomFlight);
32 |
33 | const handleFlightSelect = (event: React.ChangeEvent) => {
34 | setSelectedFlight(parseInt(event.target.value));
35 | };
36 |
37 | return (
38 |
39 | Choose Flight
40 |
41 |
42 |
43 | {dummyFlight.airline}
44 | Departure: {dummyFlight.departureTime}
45 | Duration: {dummyFlight.duration}
46 | Price: {dummyFlight.price}
47 |
48 |
49 | } label="Select" />
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default ChooseFlightStep;
58 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/steps/flightDetailsStep.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Box, FormControl, FormControlLabel, InputLabel, MenuItem, Radio, RadioGroup, Select, TextField, Typography } from '@mui/material';
3 | import DatePicker from 'react-datepicker';
4 | import 'react-datepicker/dist/react-datepicker.css';
5 | import { useFlightStore } from '../../../../store/flightStore';
6 |
7 | /**
8 | * Booking Wizard - Page for the Ticket Booking Wizard to select flight details.
9 | * This step includes selecting the origin, destination, departure date, return date, and travel class.
10 | */
11 | const FlightDetailsStep = () => {
12 | const { flightDetails, setFlightDetails } = useFlightStore();
13 |
14 | const handleInputChange = (field: string, value: any) => {
15 | setFlightDetails({ ...flightDetails, [field]: value });
16 | };
17 |
18 | return (
19 |
20 | Select Flight Details
21 |
22 | Origin
23 |
27 |
28 |
29 | Destination
30 |
34 |
35 | handleInputChange('departureDate', date)} customInput={} />
36 |
37 | handleInputChange('returnDate', date)} customInput={} />
38 |
39 |
40 | Travel Class
41 | handleInputChange('travelClass', e.target.value)}>
42 | } label="Economy" />
43 | } label="Business" />
44 | } label="First Class" />
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default FlightDetailsStep;
52 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/steps/passengerInfoStep.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Box, FormControl, InputLabel, MenuItem, Select, TextField, Typography } from '@mui/material';
3 | import DatePicker from 'react-datepicker';
4 | import 'react-datepicker/dist/react-datepicker.css';
5 | import countries from 'i18n-iso-countries';
6 | import enLocale from 'i18n-iso-countries/langs/en.json';
7 | import { useFlightStore } from '../../../../store/flightStore';
8 |
9 | countries.registerLocale(enLocale);
10 |
11 | const countryList = Object.entries(countries.getNames('en', { select: 'official' }));
12 |
13 | /**
14 | * Booking Wizard - Page for the Ticket Booking Wizard to enter passenger information.
15 | */
16 | const PassengerInfoStep = () => {
17 | const { passengerInfo, setPassengerInfo } = useFlightStore();
18 |
19 | const handleInputChange = (field: string, value: any) => {
20 | setPassengerInfo({ ...passengerInfo, [field]: value });
21 | };
22 |
23 | return (
24 |
25 | Enter Passenger Information
26 | handleInputChange('firstName', e.target.value)} fullWidth margin="normal" />
27 | handleInputChange('lastName', e.target.value)} fullWidth margin="normal" />
28 | handleInputChange('dateOfBirth', date)} customInput={} />
29 |
30 | Gender
31 |
36 |
37 | handleInputChange('passportNumber', e.target.value)} fullWidth margin="normal" />
38 |
39 | Nationality
40 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default PassengerInfoStep;
53 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/steps/paymentStep.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Box, TextField, Typography } from '@mui/material';
3 | import { useFlightStore } from '../../../../store/flightStore';
4 |
5 | /**
6 | * Booking Wizard - Page for the Ticket Booking Wizard to enter payment information.
7 | */
8 | const PaymentStep = () => {
9 | const { billingAddress, setBillingAddress } = useFlightStore();
10 |
11 | const handleAddressChange = (field: string, value: any) => {
12 | setBillingAddress({ ...billingAddress, [field]: value });
13 | };
14 |
15 | return (
16 |
17 | Payment
18 | Billing Address
19 | handleAddressChange('address', e.target.value)} fullWidth margin="normal" />
20 | handleAddressChange('city', e.target.value)} fullWidth margin="normal" />
21 | handleAddressChange('state', e.target.value)} fullWidth margin="normal" />
22 | handleAddressChange('zip', e.target.value)} fullWidth margin="normal" />
23 |
24 | We currently accept payments exclusively by invoice.
25 |
26 |
27 | );
28 | };
29 |
30 | export default PaymentStep;
31 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/steps/reviewStep.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Box, Paper, Typography } from '@mui/material';
3 | import { useFlightStore } from '../../../../store/flightStore';
4 |
5 | /**
6 | * Booking Wizard - Page for the Ticket Booking Wizard to review and confirm the entered data.
7 | */
8 | const ReviewStep = () => {
9 | const { flightDetails, selectedFlight, passengerInfo, billingAddress } = useFlightStore();
10 |
11 | return (
12 |
13 |
14 | Review & Confirm
15 |
16 |
17 | Flight Details
18 | Origin: {flightDetails.origin}
19 | Destination: {flightDetails.destination}
20 | Departure Date: {flightDetails.departureDate?.toLocaleDateString()}
21 | Return Date: {flightDetails.returnDate?.toLocaleDateString()}
22 | Travel Class: {flightDetails.travelClass}
23 |
24 |
25 | Selected Flight
26 | Flight ID: {selectedFlight}
27 |
28 |
29 | Passenger Information
30 |
31 | Full Name: {passengerInfo.firstName} {passengerInfo.lastName}
32 |
33 | Date of Birth: {passengerInfo.dateOfBirth?.toLocaleDateString()}
34 | Gender: {passengerInfo.gender}
35 | Passport Number: {passengerInfo.passportNumber}
36 | Nationality: {passengerInfo.nationality}
37 |
38 |
39 | Billing Address
40 | Address: {billingAddress.address}
41 | City: {billingAddress.city}
42 | State: {billingAddress.state}
43 | ZIP Code: {billingAddress.zip}
44 |
45 |
46 | );
47 | };
48 |
49 | export default ReviewStep;
50 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/style/step-styles.css:
--------------------------------------------------------------------------------
1 | /* Render Flowable forms datepicker on top */
2 | .react-datepicker-popper {
3 | z-index: 999999;
4 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/wizardControlButtons.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button } from '@mui/material';
2 | import * as React from 'react';
3 |
4 | /**
5 | * Controls for the ticket booking wizard
6 | */
7 | const WizardControlButtons = ({ activeStep, stepsLength, handleBack, handleNext, initializeDummyData }: any) => (
8 |
15 |
18 |
19 |
22 |
23 | );
24 |
25 | export default WizardControlButtons;
26 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/wizardStepper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Step, StepLabel, Stepper } from '@mui/material';
3 |
4 | const steps = ['Flight Details', 'Choose Flight', 'Passenger Information', 'Payment', 'Review & Submit'];
5 |
6 | /**
7 | * Simple wrapper for the stepper for the Ticket Booking Wizard
8 | */
9 | const WizardStepper = ({ activeStep }: { activeStep: number }) => (
10 |
11 | {steps.map(label => (
12 |
13 | {label}
14 |
15 | ))}
16 |
17 | );
18 |
19 | export default WizardStepper;
20 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/book-flight/wizard/wizardSuccessMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Button, Stack, Typography } from '@mui/material';
3 |
4 | /**
5 | * Success message for the Ticket Booking Wizard.
6 | * Allows to navigate to the ticket details or the home page.
7 | */
8 | const WizardSuccessMessage = ({ instanceId, navigate }: { instanceId: string; navigate: any }) => (
9 |
10 |
11 | Success!
12 |
13 |
14 | Your ticket has been successfully created.
15 |
16 | Your reference number is: {instanceId}
17 |
18 |
19 |
22 |
25 |
26 |
27 | );
28 |
29 | export default WizardSuccessMessage;
30 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/generic-instances/definitionList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getDefinitions } from '../../api/flyableApi';
3 | import ItemList from '../../shared/itemList';
4 | import { ProcessDefinitionResponse } from '@flowable/forms/flowable-work-api/process/processApiModel';
5 |
6 | type DefinitionListProps = {
7 | scopeType: 'process' | 'case';
8 | };
9 |
10 | /**
11 | * Shows a list of definitions (cases or processes) and allows the user to start a new instance.
12 | */
13 | const DefinitionList = (props: DefinitionListProps) => {
14 | const { scopeType } = props;
15 |
16 | return (
17 |
18 | queryKey={['definitions', scopeType]}
19 | title={`Start a new ${scopeType}`}
20 | queryFn={() => getDefinitions(scopeType, true)}
21 | getDataFn={(data: any) => data['data']}
22 | getItemKey={item => item.id}
23 | getItemText={item => item.name || item.id}
24 | getItemSecondaryText={item => `Key: ${item.key}`}
25 | getItemLink={item => `/${scopeType}/${item.id}`}
26 | emptyMessage="No definitions found"
27 | errorMessage="Could not fetch definitions. Check if Flowable Work is running."
28 | />
29 | );
30 | };
31 |
32 | export default DefinitionList;
33 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/home/home.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Container, Typography } from '@mui/material';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | /**
5 | * Home screen of the Flyable Portal
6 | * @constructor
7 | */
8 | const Home = () => {
9 | const navigate = useNavigate();
10 |
11 | return (
12 |
13 |
14 |
15 | Welcome to Flya
16 | ble
17 |
18 |
19 | Your journey starts here.
20 |
21 |
22 |
25 |
28 |
31 |
32 | {/**/}
35 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Home;
45 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/portal/login/loginForm.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { Alert, Box, Button, CircularProgress, Container, TextField, Typography } from '@mui/material';
4 | import { useMutation } from '@tanstack/react-query';
5 | import { login } from '../../api/authApi';
6 | import { useAuthStore } from '../../store/authStore';
7 |
8 | /**
9 | * Simple login form component.
10 | * Sends a request to /flyable-api/auth/login on submit.
11 | * It will receive a JWT token and store it in the local storage.
12 | */
13 | const LoginForm = () => {
14 | const [username, setUsername] = useState('');
15 | const [password, setPassword] = useState('');
16 | const [error, setError] = useState('');
17 | const navigate = useNavigate();
18 |
19 | const authStore = useAuthStore();
20 |
21 | // Clear JWT and user infos on mount
22 | useEffect(() => {
23 | authStore.clearJwt();
24 | authStore.clearUserInfos();
25 | }, []);
26 |
27 | const { mutate: loginMutation, isPending: isLogggingIn } = useMutation({
28 | mutationKey: ['login'],
29 | mutationFn: login,
30 | onSuccess: data => {
31 | authStore.setJwt(data.jwt);
32 | authStore.setUserInfos(data.user);
33 | navigate('/'); // Redirect to home page on successful login
34 | },
35 | onError: error => {
36 | setError(error.message);
37 | },
38 | });
39 |
40 | const handleLogin = (e: React.FormEvent) => {
41 | e.preventDefault();
42 | loginMutation({ username, password });
43 | };
44 |
45 | if (isLogggingIn) {
46 | return ;
47 | }
48 |
49 | return (
50 |
51 |
52 |
53 | Login
54 |
55 |
56 | setUsername(e.target.value)} />
57 | setPassword(e.target.value)}
68 | />
69 | {error && {error}}
70 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default LoginForm;
80 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/routes/bookingPortalRoutes.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from 'react-router-dom';
2 | import Home from '../portal/home/home';
3 | import { BookFlightWizard } from '../portal/book-flight/wizard/bookFlightWizard';
4 | import DefinitionsList from '../portal/generic-instances/definitionList';
5 | import { SubmittableForm } from '../form/submittableForm';
6 | import MyFlights from '../portal/book-flight/myFlights';
7 | import { WorkForm } from '../form/workForm';
8 | import AppDefinitionList from '../portal/apps/appDefinitionList';
9 | import { AppView } from '../portal/apps/appView';
10 | import LoginForm from '../portal/login/loginForm';
11 | import { PrivateRoute } from './privateRoute';
12 | import React from 'react';
13 |
14 | /**
15 | * Defines the React Router routes of the Flyable Portal.
16 | */
17 | const routeConfig = [
18 | { path: '/', element: , private: true },
19 | { path: 'book-flight', element: , private: true },
20 | { path: 'process', element: , private: true },
21 | { path: 'process/:definitionId', element: , private: true },
22 | { path: 'case', element: , private: true },
23 | { path: 'case/:definitionId', element: , private: true },
24 | { path: 'my-flights', element: , private: true },
25 | { path: 'my-flights/:instanceId', element: , private: true },
26 | { path: 'apps', element: , private: true },
27 | { path: 'apps/:appKey', element: , private: true },
28 | { path: '/login', element: , private: false },
29 | ];
30 |
31 | const routes = () => (
32 |
33 | {routeConfig.map(({ path, element, private: isPrivate }) => (
34 | {element} : element} />
35 | ))}
36 |
37 | );
38 |
39 | export default routes;
40 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/routes/privateRoute.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { useAuthStore } from '../store/authStore';
3 |
4 | /**
5 | * A simple private route component that checks if the user is authenticated.
6 | * This is just a simple check to see if the user is authenticated by checking if the JWT is present.
7 | * In a real-world application, you would also want to check if the JWT is expired and refresh it if necessary.
8 | */
9 | export const PrivateRoute: React.FC<{ children: React.ReactElement }> = ({ children }) => {
10 | const authStore = useAuthStore();
11 | const isAuthenticated = authStore.jwt;
12 | return isAuthenticated ? children : ;
13 | };
14 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/shared/itemList.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Divider, List, ListItemText, Typography } from '@mui/material';
2 | import ListItemButton from '@mui/material/ListItemButton';
3 | import { useNavigate } from 'react-router-dom';
4 | import { useQuery } from '@tanstack/react-query';
5 | import { enqueueSnackbar } from 'notistack';
6 | import FormSkeleton from '../form/formSkeleton';
7 |
8 | type ItemListProps = {
9 | queryKey: string[];
10 | queryFn: () => Promise;
11 | title: string;
12 | getDataFn: (data: any) => T[];
13 | getItemKey: (item: T) => string;
14 | getItemText: (item: T) => string;
15 | getItemSecondaryText?: (item: T) => string;
16 | getItemLink: (item: T) => string;
17 | emptyMessage: string;
18 | errorMessage: string;
19 | };
20 |
21 | /**
22 | * Generic list component that fetches data and renders a list of items.
23 | */
24 | const ItemList = (props: ItemListProps) => {
25 | const { queryKey, queryFn, title, getItemKey, getItemText, getItemSecondaryText, getItemLink, emptyMessage, errorMessage, getDataFn } = props;
26 |
27 | const {
28 | data: allData,
29 | error,
30 | isPending,
31 | isError,
32 | isLoadingError,
33 | } = useQuery({
34 | queryKey,
35 | queryFn,
36 | retry: true,
37 | });
38 |
39 | const navigate = useNavigate();
40 |
41 | if (isError && error.message) enqueueSnackbar(error?.message);
42 | if (isLoadingError) return {errorMessage};
43 | if (isPending) return ;
44 | const data = getDataFn(allData);
45 | if (!data || data.length === 0) return {emptyMessage};
46 |
47 | return (
48 | <>
49 | {title}
50 |
51 | {data.map((item: any) => (
52 |
53 |
navigate(getItemLink(item))}>
54 |
55 |
56 |
57 |
58 | ))}
59 |
60 | >
61 | );
62 | };
63 |
64 | export default ItemList;
65 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/store/authStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist } from 'zustand/middleware';
3 | import { ANONYMOUS_USER, User } from '../api/authApi';
4 |
5 | type AuthState = {
6 | jwt: string | null;
7 | setJwt: (jwt: string) => void;
8 | clearJwt: () => void;
9 | userInfos: User;
10 | setUserInfos: (user: User) => void;
11 | clearUserInfos: () => void;
12 | };
13 |
14 | /**
15 | * A simple store to manage the authentication state.
16 | */
17 | export const useAuthStore = create()(
18 | persist(
19 | set => ({
20 | jwt: null,
21 | setJwt: (jwt: string) => set({ jwt }),
22 | clearJwt: () => set({ jwt: null }),
23 | userInfos: ANONYMOUS_USER,
24 | setUserInfos: (user: User) => set({ userInfos: user }),
25 | clearUserInfos: () => set({ userInfos: ANONYMOUS_USER }),
26 | }),
27 | {
28 | name: 'auth-storage',
29 | }
30 | )
31 | );
32 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/store/flightStore.ts:
--------------------------------------------------------------------------------
1 | import { create, StateCreator } from 'zustand';
2 | import { devtools } from 'zustand/middleware';
3 |
4 | // Simple zustand store and types for the flight booking wizard as an alternative to using state.
5 | type FlightDetails = {
6 | origin: string;
7 | destination: string;
8 | departureDate: Date | null;
9 | returnDate: Date | null;
10 | travelClass: string;
11 | };
12 |
13 | type PassengerInfo = {
14 | firstName: string;
15 | lastName: string;
16 | dateOfBirth: Date | null;
17 | gender: string;
18 | passportNumber: string;
19 | nationality: string;
20 | };
21 |
22 | type BillingAddress = {
23 | address: string;
24 | city: string;
25 | state: string;
26 | zip: string;
27 | };
28 |
29 | type FlightState = {
30 | flightDetails: FlightDetails;
31 | passengerInfo: PassengerInfo;
32 | selectedFlight: number | null;
33 | billingAddress: BillingAddress;
34 | optionalServices: Record;
35 | totalPrice: string;
36 | bookingReference: string;
37 | setFlightDetails: (details: FlightDetails) => void;
38 | setPassengerInfo: (info: PassengerInfo) => void;
39 | setSelectedFlight: (flight: number | null) => void;
40 | setBillingAddress: (address: BillingAddress) => void;
41 | setOptionalServices: (services: Record) => void;
42 | setTotalPrice: (price: string) => void;
43 | setBookingReference: (reference: string) => void;
44 | initializeDummyData: () => void;
45 | };
46 |
47 | const flightStore: StateCreator = set => ({
48 | flightDetails: {
49 | origin: '',
50 | destination: '',
51 | departureDate: null,
52 | returnDate: null,
53 | travelClass: '',
54 | },
55 | passengerInfo: {
56 | firstName: '',
57 | lastName: '',
58 | dateOfBirth: null,
59 | gender: '',
60 | passportNumber: '',
61 | nationality: '',
62 | },
63 | selectedFlight: null,
64 | billingAddress: {
65 | address: '',
66 | city: '',
67 | state: '',
68 | zip: '',
69 | },
70 | optionalServices: {},
71 | totalPrice: '',
72 | bookingReference: '',
73 | setFlightDetails: (details: FlightDetails) => set({ flightDetails: details }),
74 | setPassengerInfo: (info: PassengerInfo) => set({ passengerInfo: info }),
75 | setSelectedFlight: (flight: number | null) => set({ selectedFlight: flight }),
76 | setBillingAddress: (address: BillingAddress) => set({ billingAddress: address }),
77 | setOptionalServices: (services: Record) => set({ optionalServices: services }),
78 | setTotalPrice: (price: string) => set({ totalPrice: price }),
79 | setBookingReference: (reference: string) => set({ bookingReference: reference }),
80 | initializeDummyData: () =>
81 | set({
82 | flightDetails: {
83 | origin: 'ZRH',
84 | destination: 'MAD',
85 | departureDate: new Date(),
86 | returnDate: new Date(),
87 | travelClass: 'economy',
88 | },
89 | passengerInfo: {
90 | firstName: 'John',
91 | lastName: 'Doe',
92 | dateOfBirth: new Date('1990-01-01'),
93 | gender: 'male',
94 | passportNumber: '123456789',
95 | nationality: 'US',
96 | },
97 | selectedFlight: 1,
98 | billingAddress: {
99 | address: '123 Main St',
100 | city: 'New York',
101 | state: 'NY',
102 | zip: '10001',
103 | },
104 | optionalServices: {},
105 | totalPrice: '500',
106 | bookingReference: 'ABC123',
107 | }),
108 | });
109 |
110 | export const useFlightStore = create()(devtools(flightStore));
111 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/store/overlayStore.ts:
--------------------------------------------------------------------------------
1 | import { create, StateCreator } from 'zustand';
2 | import { devtools } from 'zustand/middleware';
3 |
4 | type OverlayState = {
5 | isFlowableUnavailable: boolean;
6 | setFlowableUnavailable: (value: boolean) => void;
7 | };
8 |
9 | const overlayStore: StateCreator = set => ({
10 | isFlowableUnavailable: false,
11 | setFlowableUnavailable: (value: boolean) => set({ isFlowableUnavailable: value }),
12 | });
13 |
14 | export const useOverlayStore = create()(devtools(overlayStore));
15 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/style/app.css:
--------------------------------------------------------------------------------
1 | @media (prefers-reduced-motion: no-preference) {
2 | .App-logo {
3 | animation: App-logo-spin infinite 20s linear;
4 | }
5 | }
6 |
7 | .App-header {
8 | background-color: #282c34;
9 | min-height: 100vh;
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | justify-content: center;
14 | font-size: calc(10px + 2vmin);
15 | color: white;
16 | }
17 |
18 | .App-link {
19 | color: #61dafb;
20 | }
21 |
22 | @keyframes App-logo-spin {
23 | from {
24 | transform: rotate(0deg);
25 | }
26 | to {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
31 | #root {
32 | background-color: #f3f3f3;
33 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/style/flwFormsStyle.css:
--------------------------------------------------------------------------------
1 | /*
2 | These styles customize Flowable Forms to match your project's design.
3 | */
4 |
5 | .flw__panel__label {
6 | font-size: 20px;
7 | font-weight: 500;
8 | }
9 |
10 | .flw__container--showBorder__content-wrapper {
11 | box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 1px 2px rgb(0 0 0 / 25%);
12 | margin-bottom: 20px;
13 | border-radius: 5px;
14 | }
15 |
16 | /* Base label styling */
17 | .flw__label {
18 | position: absolute;
19 | top: 6px;
20 | left: 12px;
21 | font-size: 12px;
22 | color: #666;
23 | display: flex;
24 | align-items: center;
25 | pointer-events: none;
26 | padding: 0 4px;
27 | transform: translateY(-50%);
28 | z-index: 1;
29 | }
30 |
31 | .flw__label > span {
32 | padding: 0 5px;
33 | background: var(--flw-forms-background-color);
34 | }
35 |
36 | .flw__component {
37 | margin-bottom: 15px;
38 | }
39 |
40 | .flw__component__input {
41 | padding: 12px 10px;
42 | font-size: 16px;
43 | border: 1px solid #ccc;
44 | border-radius: 4px;
45 | background-color: transparent;
46 | transition: border-color 0.3s ease-in-out;
47 | }
48 |
49 | .flw__component__input:focus {
50 | border-color: red;
51 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/style/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/src/util/modelKeys.ts:
--------------------------------------------------------------------------------
1 | // Hard coded key to identify the ticket process
2 | export const TICKET_PROCESS_KEY = 'TRA-P001';
3 |
--------------------------------------------------------------------------------
/flyable-booking-portal/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/FlyableBookingPortalApplication.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /**
7 | * Main entry point for the Flyable Booking Portal application.
8 | */
9 | @SpringBootApplication
10 | public class FlyableBookingPortalApplication {
11 |
12 | public static void main(String[] args) {
13 | SpringApplication.run(FlyableBookingPortalApplication.class, args);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/bootstrap/FlyableDemoBootstrapper.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.bootstrap;
2 |
3 | import com.flowable.training.flyable.service.FlowableAdminService;
4 | import com.flowable.training.flyable.util.FlowableUtil;
5 | import org.apache.commons.logging.Log;
6 | import org.apache.commons.logging.LogFactory;
7 | import org.springframework.boot.CommandLineRunner;
8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
9 | import org.springframework.core.io.Resource;
10 | import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
11 | import org.springframework.stereotype.Component;
12 |
13 | import java.io.InputStream;
14 | import java.util.zip.ZipInputStream;
15 |
16 | @Component
17 | @ConditionalOnExpression("${flyable.deploy-demo-apps:false}")
18 | public class FlyableDemoBootstrapper implements CommandLineRunner {
19 |
20 | private final FlowableAdminService adminRepositoryService;
21 | protected final Log logger = LogFactory.getLog(this.getClass());
22 |
23 | public FlyableDemoBootstrapper(FlowableAdminService adminRepositoryService) {
24 | this.adminRepositoryService = adminRepositoryService;
25 | }
26 |
27 | @Override
28 | public void run(String... args) throws Exception {
29 | // First check if Flowable is even running, if not, show an error message and skip
30 | if(!adminRepositoryService.flowableIsRunningAndHealthy()) {
31 | logger.error("Flowable is not running or not healthy, skipping deployment of demo apps.");
32 | return;
33 | }
34 |
35 | // Find all zip or bar files in the apps folder and deploy them if they don't exist yet in Work
36 | PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
37 | Resource[] resources = resolver.getResources("classpath:apps/*.{zip,bar}");
38 |
39 | for (Resource resource : resources) {
40 | try (InputStream is = resource.getInputStream();
41 | ZipInputStream zis = new ZipInputStream(is)) {
42 | String appKey = FlowableUtil.getAppKeyFromAppZipInputStream(zis);
43 | var definitions = adminRepositoryService.getAppDefinitionsByKey(appKey);
44 | if(definitions.getSize() == 0) {
45 | adminRepositoryService.deployApp(resource);
46 | } else {
47 | logger.info("App with key " + appKey + " already exists, skipping deployment.");
48 | }
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/configuration/FlyableConfigurationProperties.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.configuration;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 | import org.springframework.context.annotation.Configuration;
5 |
6 | /**
7 | * Configuration properties for the Flyable application.
8 | */
9 | @ConfigurationProperties(prefix = "flyable")
10 | @Configuration
11 | public class FlyableConfigurationProperties {
12 |
13 | /**
14 | * The URL of Flowable Work.
15 | */
16 | private String flowableUrl;
17 |
18 | /**
19 | * The secret used to sign the JWT tokens.
20 | */
21 | private String jwtSecret;
22 |
23 | /**
24 | * The seed for the faker library, used to generate users.
25 | */
26 | private int fakerSeed = 12345;
27 |
28 | /**
29 | * The technical user for Flowable Work.
30 | * Is only used for API calls that need a technical user (e.g. to fetch the theme).
31 | */
32 | private String flowableAdminUser = "admin";
33 |
34 | /**
35 | * The password for the technical user for Flowable Work.
36 | */
37 | private String flowableAdminPassword = "test";
38 |
39 | /**
40 | * Whether the demo apps (e.g. booking) are deployed.
41 | */
42 | private boolean deployDemoApps = true;
43 |
44 | public String getFlowableUrl() {
45 | return flowableUrl;
46 | }
47 | public void setFlowableUrl(String flowableUrl) {
48 | this.flowableUrl = flowableUrl;
49 | }
50 |
51 | public String getJwtSecret() {
52 | return jwtSecret;
53 | }
54 |
55 | public void setJwtSecret(String jwtSecret) {
56 | this.jwtSecret = jwtSecret;
57 | }
58 |
59 | public int getFakerSeed() {
60 | return fakerSeed;
61 | }
62 |
63 | public void setFakerSeed(int fakerSeed) {
64 | this.fakerSeed = fakerSeed;
65 | }
66 |
67 | public String getFlowableAdminUser() {
68 | return flowableAdminUser;
69 | }
70 |
71 | public void setFlowableAdminUser(String flowableAdminUser) {
72 | this.flowableAdminUser = flowableAdminUser;
73 | }
74 |
75 | public String getFlowableAdminPassword() {
76 | return flowableAdminPassword;
77 | }
78 |
79 | public void setFlowableAdminPassword(String flowableAdminPassword) {
80 | this.flowableAdminPassword = flowableAdminPassword;
81 | }
82 |
83 | public boolean isDeployDemoApps() {
84 | return deployDemoApps;
85 | }
86 |
87 | public void setDeployDemoApps(boolean deployDemoApps) {
88 | this.deployDemoApps = deployDemoApps;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/configuration/RestClientConfig.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.configuration;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.context.annotation.Primary;
7 | import org.springframework.http.HttpHeaders;
8 | import org.springframework.security.core.Authentication;
9 | import org.springframework.security.core.context.SecurityContextHolder;
10 | import org.springframework.web.client.RestClient;
11 |
12 | import java.nio.charset.StandardCharsets;
13 | import java.util.Base64;
14 |
15 | /**
16 | * Configuration for the REST clients used in the Flyable application.
17 | */
18 | @Configuration
19 | public class RestClientConfig {
20 |
21 |
22 | @Value("${flyable.flowable-url}")
23 | private String flowableApiUrl;
24 |
25 | @Value("${flyable.flowable-admin-user}")
26 | private String adminUser;
27 |
28 | @Value("${flyable.flowable-admin-password}")
29 | private String adminPassword;
30 |
31 | /**
32 | * Creates a REST client that uses the current user's credentials.
33 | * Since we're just using a "mock" implementation of JWT here, you would probably solve this
34 | * more elegantly in a productive setup.
35 | *
36 | * @return The REST client
37 | */
38 | @Primary
39 | @Bean
40 | public RestClient restClient() {
41 | return RestClient.builder()
42 | .baseUrl(flowableApiUrl)
43 | .requestInterceptor((request, body, execution) -> {
44 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
45 | if (authentication != null && authentication.isAuthenticated()) {
46 | String username = authentication.getName();
47 | // We hope it's clear that you don't do this in production :)
48 | // Usually, we would make use of OAuth2 or a system user
49 | String password = username.equals("admin") ? "test" : "training";
50 |
51 | String auth = username + ":" + password;
52 | byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));
53 | String authHeader = "Basic " + new String(encodedAuth);
54 |
55 | request.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
56 | }
57 | return execution.execute(request, body);
58 | })
59 | .build();
60 | }
61 |
62 | /**
63 | * Creates a REST client that uses the admin user's credentials.
64 | *
65 | * @return The REST client
66 | */
67 | @Bean
68 | public RestClient adminRestClient() {
69 | return RestClient.builder()
70 | .baseUrl(flowableApiUrl)
71 | .requestInterceptor((request, body, execution) -> {
72 | String auth = adminUser + ":" + adminPassword;
73 | byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(StandardCharsets.UTF_8));
74 | String authHeader = "Basic " + new String(encodedAuth);
75 | request.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
76 | return execution.execute(request, body);
77 | })
78 | .build();
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/exception/FlowableServiceException.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.exception;
2 |
3 | /**
4 | * Exception thrown when an error/bad status code occurs in Flowable Work.
5 | */
6 | public class FlowableServiceException extends RuntimeException {
7 | private final int statusCode;
8 |
9 | public FlowableServiceException(String message, Throwable cause, int statusCode) {
10 | super(message, cause);
11 | this.statusCode = statusCode;
12 | }
13 |
14 | public int getStatusCode() {
15 | return statusCode;
16 | }
17 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/FlowableDataResponse.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable;
2 |
3 | import java.util.List;
4 |
5 | /**
6 | * Response from most Flowable APIs will be paginated.
7 | * This class represents the response from Flowable APIs.
8 | * @param The type of data in the response.
9 | */
10 | public class FlowableDataResponse {
11 |
12 | List data;
13 | long total;
14 | int start;
15 | String sort;
16 | String order;
17 | int size;
18 |
19 | public List getData() {
20 | return data;
21 | }
22 |
23 | public FlowableDataResponse setData(List data) {
24 | this.data = data;
25 | return this;
26 | }
27 |
28 | public long getTotal() {
29 | return total;
30 | }
31 |
32 | public void setTotal(long total) {
33 | this.total = total;
34 | }
35 |
36 | public int getStart() {
37 | return start;
38 | }
39 |
40 | public void setStart(int start) {
41 | this.start = start;
42 | }
43 |
44 | public String getSort() {
45 | return sort;
46 | }
47 |
48 | public void setSort(String sort) {
49 | this.sort = sort;
50 | }
51 |
52 | public String getOrder() {
53 | return order;
54 | }
55 |
56 | public void setOrder(String order) {
57 | this.order = order;
58 | }
59 |
60 | public int getSize() {
61 | return size;
62 | }
63 |
64 | public void setSize(int size) {
65 | this.size = size;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/FlowableExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.ResponseEntity;
5 | import org.springframework.web.bind.annotation.ControllerAdvice;
6 | import org.springframework.web.bind.annotation.ExceptionHandler;
7 | import org.springframework.web.client.ResourceAccessException;
8 |
9 | import com.flowable.training.flyable.exception.FlowableServiceException;
10 | import com.flowable.training.flyable.rest.flowable.dto.response.ErrorResponse;
11 |
12 | /**
13 | * General exception handler for Flowable API.
14 | * If the exception is a FlowableServiceException, it will return a custom error response.
15 | */
16 | @ControllerAdvice
17 | public class FlowableExceptionHandler {
18 |
19 | @ExceptionHandler(FlowableServiceException.class)
20 | public ResponseEntity handleFlowableServiceException(FlowableServiceException ex) {
21 | if(ex.getCause() instanceof ResourceAccessException) {
22 | // Probably, Flowable is down
23 | return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body( new ErrorResponse("Flowable Work is not available", "FLOWABLE_UNAVAILABLE"));
24 | } else {
25 | // Generic errors
26 | return ResponseEntity.status(ex.getStatusCode()).body( new ErrorResponse(ex.getMessage(), "FLOWABLE_ERROR"));
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/FlowableThemeController.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable;
2 |
3 | import org.springframework.http.ResponseEntity;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RestController;
7 |
8 | import com.flowable.training.flyable.service.FlowableWorkThemeService;
9 |
10 | /**
11 | * Controller to fetch the theme from Flowable Work.
12 | * Requires a technical user to fetch.
13 | */
14 | @RestController
15 | @RequestMapping("/flyable-api")
16 | public class FlowableThemeController {
17 |
18 | private final FlowableWorkThemeService flowableWorkThemeService;
19 |
20 | public FlowableThemeController(FlowableWorkThemeService flowableWorkThemeService) {
21 | this.flowableWorkThemeService = flowableWorkThemeService;
22 | }
23 |
24 | @GetMapping("/theme")
25 | public ResponseEntity getTheme() {
26 | String flyableTheme = flowableWorkThemeService.getTheme();
27 | return ResponseEntity.ok(flyableTheme);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/request/ActuatorHealth.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.request;
2 |
3 | public record ActuatorHealth(
4 | String status
5 | ) {
6 | }
7 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/request/CreateFlowableInstanceRequest.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.request;
2 |
3 | import java.util.Map;
4 |
5 | /**
6 | * Request to create a new Flowable instance (case or process).
7 | * @param definitionId The ID of the definition to create an instance of.
8 | * @param values Payload to create the instance.
9 | */
10 | public record CreateFlowableInstanceRequest(
11 | String definitionId,
12 | Map values
13 | ) {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/ErrorResponse.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | /**
4 | * Error response from Flowable API.
5 | * @param message
6 | * @param reason
7 | */
8 | public record ErrorResponse(
9 | String message,
10 | String reason
11 | ) {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/FlowableAppDefinitionDto.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | /**
4 | * Representation of a Flowable app definition.
5 | * @param id The ID of the definition.
6 | * @param key The key of the definition.
7 | * @param name The name of the definition.
8 | * @param deploymentId The deployment ID of the definition.
9 | */
10 | public record FlowableAppDefinitionDto(
11 | String id,
12 | String key,
13 | String name,
14 | String deploymentId
15 | ) {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/FlowableAppPageDto.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | /**
4 | * Representation of a FlowApp page.
5 | * @param id The ID of the page.
6 | * @param key The key of the page.
7 | * @param label The label of the page.
8 | * @param order The order of the page (index).
9 | * @param formKey The form key associated with the page.
10 | * @param urlSegment The URL segment identifying the page in Flowable Work.
11 | */
12 | public record FlowableAppPageDto(
13 | String id,
14 | String key,
15 | String label,
16 | int order,
17 | String formKey,
18 | String urlSegment
19 | ) {
20 |
21 | }
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/FlowableDefinitionDto.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | /**
4 | * Representation of a Flowable definition (case or process).
5 | * @param id The ID of the definition.
6 | * @param key The key of the definition.
7 | * @param name The name of the definition.
8 | * @param version The version of the definition.
9 | * @param deploymentId The deployment ID of the definition.
10 | */
11 | public record FlowableDefinitionDto(
12 | String id,
13 | String key,
14 | String name,
15 | int version,
16 | String deploymentId
17 | ) {
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/FlowableInstanceDto.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | import java.util.Map;
4 |
5 | /**
6 | * Representation of a Flowable instance (mainly process).
7 | * @param id The ID of the instance.
8 | * @param name The name of the instance.
9 | * @param processDefinitionId The ID of the process definition.
10 | * @param processVariables The variables of the instance.
11 | */
12 | public record FlowableInstanceDto(
13 | String id,
14 | String name,
15 | String processDefinitionId,
16 | Map processVariables
17 | ) { }
18 |
--------------------------------------------------------------------------------
/flyable-booking-portal/src/main/java/com/flowable/training/flyable/rest/flowable/dto/response/FormDefinitionDto.java:
--------------------------------------------------------------------------------
1 | package com.flowable.training.flyable.rest.flowable.dto.response;
2 |
3 | import java.util.List;
4 | import java.util.Map;
5 |
6 | /**
7 | * Mapped representation of a Flowable Form Definition
8 | * @param id The ID of the form definition.
9 | * @param key The key of the form definition.
10 | * @param name The name of the form definition.
11 | * @param version The version of the form definition.
12 | * @param metadata The metadata of the form definition.
13 | * @param rows The rows (actual configuration) of the form definition.
14 | */
15 | public record FormDefinitionDto(
16 | String id,
17 | String key,
18 | String name,
19 | int version,
20 | Map metadata,
21 | List