33 Commits
Author SHA1 Message Date
pierpaolo.mammi 2289f6e5be fix xdebug (taken from elixforms-web-services) 2026-07-24 15:05:17 +02:00
pierpaolo.mammi 7a015f2b70 move docker compose 2026-07-23 23:07:07 +02:00
pierpaolo.mammi 1cf674a31c switch to Guzzle module for HTTP calls 2026-07-22 13:05:17 +02:00
pierpaolo.mammi 01b22f9c2d switch to php composer 2026-07-22 12:55:08 +02:00
pierpaolo.mammi 151bc70775 refactor solution to PHP through AI 2026-07-20 14:27:17 +02:00
pierpaolo.mammi a77f6f19cf fix vite config for correct build paths 2026-07-17 16:36:55 +02:00
pierpaolo.mammi 746c43c7b4 exclude config.json and add template 2026-07-17 16:12:37 +02:00
pierpaolo.mammi 85b51dfbc4 adjust recupero contratti component configuration 2026-07-17 16:08:03 +02:00
pierpaolo.mammi 1aacb21b1b make userDisplayName optional 2026-07-17 16:08:03 +02:00
pierpaolo.mammi 1c02e3d1b2 apply custom elixforms styling 2026-07-17 16:08:03 +02:00
pierpaolo.mammi bad919a98a apply custom elixforms styling 2026-07-17 15:19:55 +02:00
pierpaolo.mammi fefb45505c move magic values to configuration file 2026-07-17 13:46:12 +02:00
pierpaolo.mammi c3d4c2a660 switch to bootstrap-italia styling 2026-07-17 13:34:40 +02:00
pierpaolo.mammi 1943252422 first update to bootstrap italia styles 2026-07-17 10:40:42 +02:00
pierpaolo.mammi 4cd22396d9 switch to bootstrap italia through AI 2026-07-17 09:53:46 +02:00
pierpaolo.mammi 99a64f1157 move submit button to the right 2026-07-17 09:40:44 +02:00
pierpaolo.mammi beee00bee3 add custom styling to common component 2026-07-16 16:09:33 +02:00
pierpaolo.mammi e404bf0954 fix autocomplete results box positioning 2026-07-16 15:36:14 +02:00
pierpaolo.mammi 50681d07c5 cleanup call to contratti API 2026-07-16 15:32:48 +02:00
pierpaolo.mammi 6b5aa556fe move field names in a single place 2026-07-14 16:20:08 +02:00
pierpaolo.mammi 2c8a24032f add missing values to mock server response 2026-07-14 16:12:40 +02:00
pierpaolo.mammi dcf28d46b5 temporary implementation to call mock server 2026-07-14 16:12:34 +02:00
pierpaolo.mammi 296c481516 fix wrong component parameter name 2026-07-14 16:12:02 +02:00
pierpaolo.mammi faedc6203a update npm packages and hopefully fix debugging 2026-07-14 16:11:22 +02:00
pierpaolo.mammi 99e868bfb4 try to restore debug functionality 2026-07-14 15:05:09 +02:00
pierpaolo.mammi 5b756cb9cd move gitignore to root 2026-07-14 15:04:43 +02:00
pierpaolo.mammi 550978a852 add basic mock server for api testing 2026-07-14 15:04:22 +02:00
administrator 0de10445f6 add new custom page: recupero-proposta-cct-da-contratti 2026-07-13 18:48:54 +02:00
administrator 8412a0ab41 update skills and code based on official documentation 2026-07-10 09:36:01 +02:00
administrator 1daa5c7409 fix form state for dropdown controls
minor cleanups
2026-07-06 13:32:37 +02:00
administrator 158892bd3e refactor: move template rendering to own properties and functions 2026-07-06 11:41:27 +02:00
administrator 4af653e08a refactor: remove unneeded concrete common class
custom form fields creation logic as overridable function
2026-07-06 11:37:04 +02:00
administrator e50099a0c7 start of ai-based development 2026-07-06 10:44:32 +02:00
74 changed files with 12062 additions and 1184 deletions
+4
View File
@@ -0,0 +1,4 @@
# Regole e Convenzioni del Progetto elixForms Custom Pages
## Gestione ID dei form element
Gli ID dei campi del form (es. nel Dropdown, Checkbox, TextField) che vengono creati con i vari metodi `create...` in `ElixFormsComponentAbstract.tsx` non devono mai essere cambiati o manipolati (es. aggiungendo suffissi come `_dropdown`), poiché vengono ricevuti e inviati da una pagina iniziale che si basa strettamente sul loro nome e ID esatto. In caso di conflitti di ID (es. dovuti a input nascosti), il suffisso va applicato agli altri elementi (come l'input hidden) e MAI all'elemento principale visibile.
+168
View File
@@ -0,0 +1,168 @@
---
name: elixforms-common-library
description: >
Documentazione della libreria condivisa common/ del progetto elixForms.
Descrive i componenti ElixForms, il pattern di form field factory,
le interfacce TypeScript, i tipi, e l'helper per i query parameter.
Attiva questa skill quando lavori sui componenti condivisi, crei nuovi tipi di campo,
modifichi la gestione dello stato del form, o integri con la piattaforma elixForms.
---
# Libreria Common elixForms
## Panoramica
La cartella `react/common/` contiene la libreria condivisa di componenti React/TypeScript usata da tutte le pagine custom. È basata su **Bootstrap Italia + HTML nativo** e implementa un sistema di form con campi dinamici.
## Architettura dei Componenti
```
ElixFormsComponentAbstract (abstract class, extends React.Component)
└── ElixFormsComponent (classe concreta, exported as ElixFormsReact)
└── Usata nelle pagine come `new ElixForms.ElixFormsReact(props)`
```
### Gerarchia delle classi
1. **`ElixFormsComponentAbstract`** (`ElixFormsComponentAbstract.tsx`)
- Classe astratta che estende `React.Component<IElixFormsComponentProperties, IElixFormsComponentFormState>`
- Contiene tutta la logica di creazione dei campi form (factory methods)
- Gestisce lo state del form (`formData: { [key: string]: any }`)
- Implementa `render()` che genera `<form>` con action POST verso elixForms
2. **`ElixFormsComponent`** (`ElixFormsComponent.tsx`)
- Classe concreta che estende `ElixFormsComponentAbstract`
- Esportata come `ElixFormsReact` per uso nelle pagine
- Attualmente vuota (la logica è nell'abstract)
## Interfacce TypeScript
### `IElixFormsComponentProperties` (Props)
```typescript
interface IElixFormsComponentProperties {
description: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
additionalFieldsJson?: string; // JSON schema per campi dinamici
customFormFieldsConfiguration?: ( // Callback con factory per campi custom
formFieldFactory: IElixFormsComponentCustomFormFieldFactory
) => JSX.Element;
}
```
### `IElixFormsComponentFormState` (State)
```typescript
interface IElixFormsComponentFormState {
formData: { [key: string]: any };
}
```
### `IElixFormsComponentCustomFormFieldFactory` (Form Field Factory)
```typescript
type IElixFormsComponentCustomFormFieldFactory = {
createBooleanInput(name: string, label: string, required?: boolean): JSX.Element;
createCheckboxInput(name: string, label: string, options: ElixFormsCheckboxOption[]): JSX.Element;
createDropdownInput(name: string, label: string, options: ElixFormsDropdownOption[], required?: boolean): JSX.Element;
createHiddenInput(name: string): JSX.Element;
createNumberInput(name: string, label: string, required?: boolean): JSX.Element;
createRadioInput(name: string, label: string, options: ElixFormsRadioOption[], required?: boolean): JSX.Element;
createTextInput(name: string, label: string, required?: boolean): JSX.Element;
createTextAreaInput(name: string, label: string, required?: boolean): JSX.Element;
};
```
### Tipi per le Opzioni (`ElixFormsTypes.tsx`)
```typescript
type ElixFormsCheckboxOption = { value: number; label: string; required?: boolean };
type ElixFormsDropdownOption = { value: number; label: string };
type ElixFormsRadioOption = { value: number; label: string };
```
## Form Field Factory Pattern
Le pagine usano il pattern **Custom Form Field Factory** per dichiarare i campi del form:
```jsx
// In App.jsx della pagina
var myElixFormsReact = new ElixForms.ElixFormsReact({
// ...props...
customFormFieldsConfiguration: (formFieldFactory) => (
<>
{formFieldFactory.createDropdownInput("COL0015", "Goals", [...options], true)}
{formFieldFactory.createTextInput("COL0002", "Campo STRING", true)}
{formFieldFactory.createTextAreaInput("COL0003", "Campo TEXTAREA", true)}
{formFieldFactory.createBooleanInput("COL0004", "Campo BOOLEAN", true)}
{formFieldFactory.createRadioInput("COL0005", "Campo RADIO", [...options], true)}
{formFieldFactory.createCheckboxInput("COL0006", "Campo CHECKBOX", [...options])}
</>
)
});
```
### Tipi di Campo Supportati
| Metodo Factory | Componente Bootstrap Italia | HTML Output |
|---|---|---|
| `createTextInput` | `TextField` | Text input |
| `createTextAreaInput` | `TextField` (multiline) | Textarea |
| `createNumberInput` | `TextField` (type=number) | Number input |
| `createBooleanInput` | `ChoiceGroup` (Sì/No) | Radio buttons |
| `createRadioInput` | `ChoiceGroup` | Radio buttons |
| `createCheckboxInput` | `Checkbox` + hidden input | Checkboxes |
| `createDropdownInput` | `Dropdown` | Select dropdown |
| `createHiddenInput` | Native `<input type="text">` | Hidden field |
## ElixFormsElement (Wrapper Layout)
`ElixFormsElement` è una classe che incapsula label + input in un layout a due colonne compatibile con il design system elixForms:
```tsx
class ElixFormsElement {
constructor(labelElement: ReactElement, inputElement: ReactElement, separator?: string);
render(): JSX.Element;
}
```
Genera markup con classi CSS specifiche di elixForms:
- `.iuFieldContainer` → container del campo
- `.attrDisplay_left`, `.attrDisplay_label` → colonna label
- `.attrDisplay_right`, `.attrDisplay_input` → colonna input
## QueryParamHelper
Utility statica per leggere parametri dalla URL corrente:
```typescript
class QueryParamHelper {
static getCheckedFromQuery(paramName: string): number[]; // Per checkbox (valori separati da virgola)
static getOptionFromQuery(paramName: string): number | undefined; // Per radio/dropdown
static getBooleanFromQuery(paramName: string): boolean | undefined;// Per boolean
static getDecodedTextFromQuery(paramName: string): string | undefined; // Per testo (URI decoded)
}
```
Usato per pre-popolare i campi form con i valori passati via query string dalla piattaforma elixForms.
## Campi Obbligatori elixForms
Il componente abstract gestisce automaticamente questi parametri come hidden inputs:
- `RWE2_MODULE_ID`
- `RWE2_REQUEST_ID`
- `custom-workflow-back-url`
- `custom-workflow-generic-id`
- `custom-workflow-current-tabrel-genid`
- `custom-workflow-source-field`
- `crc`
- `MODULE_TESTMODE_KEY`
- `ELANG`
## Note Importanti
- Lo **state management** attuale usa `React.Component` class-based con `this.state` e `this.setState`
- I checkbox gestiscono un `Map<string, number[]>` interno per tracciare i valori selezionati
- Il form fa POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
- L'encoding charset è `ISO-8859-1` per compatibilità con il backend
- La libreria usa **SCSS modules** per gli stili (`ElixFormsComponent.module.scss`)
- Il package `common` è di tipo `commonjs` (diverso dalle pagine che sono `module`)
+229
View File
@@ -0,0 +1,229 @@
---
name: elixforms-create-new-page
description: >
Guida step-by-step per creare una nuova pagina custom nel progetto elixForms.
Copre la struttura dei file, la configurazione Vite per single-bundle,
il setup TypeScript, l'importazione dei componenti common, e le convenzioni.
Attiva questa skill quando devi creare una nuova pagina, duplicare una pagina esistente,
o scaffoldare un nuovo micro-progetto nel monorepo.
---
# Creare una Nuova Pagina elixForms Custom
## Prerequisiti
- Node.js installato
- Dipendenze root installate: `cd react && npm install`
## Step 1: Scaffolding con Vite
```powershell
cd react
npm create vite@latest <nome-pagina> -- --template react
cd <nome-pagina>
npm install vite-plugin-css-injected-by-js
npm install -D sass-embedded
```
## Step 2: Configurare package.json
```json
{
"name": "@elixforms/<nome-pagina>",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"vite-plugin-css-injected-by-js": "^5.0.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.1",
"sass-embedded": "^1.99.0",
"vite": "^8.0.10"
}
}
```
> **Nota**: React e ReactDOM NON vanno nel package.json della pagina.
> Sono nel `react/package.json` root e vengono risolti via hoisting di npm.
## Step 3: Configurare vite.config.js
```js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import path from "path";
export default defineConfig({
plugins: [
react(),
cssInjectedByJsPlugin()
],
build: {
outDir: 'dist',
cssCodeSplit: false,
rollupOptions: {
input: 'src/main.jsx', // o main.tsx se TypeScript
output: {
codeSplitting: false,
manualChunks: undefined,
entryFileNames: '<nome-pagina>.js',
assetFileNames: '[name].[ext]'
},
},
sourcemap: true,
},
resolve: {
alias: {
"@common": path.resolve(__dirname, "../common"),
}
},
server: {
fs: {
allow: [".."]
}
}
})
```
## Step 4: Configurare tsconfig.json
```json
{
"extends": "../tsconfig.base.json",
"references": [
{ "path": "../common" }
],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"allowJs": true,
"checkJs": true
},
"include": ["src/**/*"]
}
```
## Step 5: Creare il file di dichiarazioni globali
`src/global.d.ts`:
```typescript
declare module "*.css";
declare module "*.svg";
declare module "*.png";
```
## Step 6: Creare l'entry point
`src/main.jsx` (o `main.tsx`):
```jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Root element not found')
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
)
```
## Step 7: Creare il componente App
`src/App.jsx` (o `App.tsx`):
```jsx
import './App.css'
import * as ElixForms from '@common/src/ElixFormsComponent';
function App() {
var myElixFormsReact = new ElixForms.ElixFormsReact({
description: "Descrizione pagina",
isDarkTheme: false,
environmentMessage: "",
hasTeamsContext: false,
userDisplayName: "",
customFormFieldsConfiguration: (formFieldFactory) => (
<>
{/* Aggiungi i tuoi campi qui usando formFieldFactory */}
</>
)
});
var elixFormsReactRender = myElixFormsReact.render();
return <>
<div className="container_12" id="pageBody">
<div className="grid_12">
<div id="userConsole" className="fe-behaviour">
{/* Header, contenuto e footer della pagina */}
<div className="grid_12 console_main">
<div className="inside">
<div className="mainview">
<div className="container">
<h1>Titolo Pagina</h1>
{elixFormsReactRender}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>;
}
export default App
```
## Step 8: Configurare i CSS
`src/App.css` — Importa i CSS remoti del design system elixForms:
```css
@import url('https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/console_user.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/designitalia_adaptation.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/fonts.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style-bs.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style.css');
/* ... altri CSS necessari */
```
## Step 9: Configurare VS Code (opzionale)
Aggiungere configurazione di launch e task in `.vscode/launch.json` e `.vscode/tasks.json` seguendo il pattern esistente per `scelta-carriera`.
## Step 10: Build e Deploy
```powershell
npm run build
```
Output in `dist/`:
- `<nome-pagina>.js` — Singolo bundle JS pronto per il deploy
- Eventuali asset (immagini) referenziati
## Checklist Nuova Pagina
- [ ] Cartella creata in `react/<nome-pagina>/`
- [ ] `package.json` con `"name": "@elixforms/<nome-pagina>"`
- [ ] `vite.config.js` con `entryFileNames: '<nome-pagina>.js'`
- [ ] `tsconfig.json` estende `../tsconfig.base.json`
- [ ] Alias `@common` configurato in vite.config.js
- [ ] CSS remoti importati in App.css
- [ ] Entry point `main.jsx` con mount su `#root`
- [ ] `global.d.ts` con dichiarazioni per moduli CSS/SVG/PNG
- [ ] `npm install` eseguito nella cartella della pagina
- [ ] `npm run dev` funziona senza errori
- [ ] `npm run build` produce singolo bundle in `dist/`
+164
View File
@@ -0,0 +1,164 @@
---
name: elixforms-css-design-system
description: >
Strategia CSS e design system del progetto elixForms.
Documenta come vengono importati i CSS remoti della piattaforma,
il layout a griglia elixForms, le classi CSS specifiche della piattaforma,
gli stili SCSS modules nella libreria common, e la struttura visiva delle pagine.
Attiva questa skill quando lavori sugli stili, sul layout delle pagine,
o devi capire come funzionano le classi CSS del design system elixForms.
---
# CSS e Design System elixForms
## Strategia CSS
Il progetto utilizza **tre livelli di stili**:
1. **CSS remoti** — Design system dell'Università di Parma, caricati via `@import url(...)` da `console-unipr.elixforms.it`
2. **SCSS Modules** — Stili specifici dei componenti in `common/` (es. `ElixFormsComponent.module.scss`)
3. **CSS locali** — Stili specifici della pagina (es. `App.css`, `index.css`)
## CSS Remoti (Design System UniPR/elixForms)
Importati in `App.css` di ogni pagina:
```css
/* CSS principali del design system */
@import url('https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_overrides.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/console_user.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/designitalia_adaptation.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/fonts.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/isipcss_overrides.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/iulib.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/print.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/responsive.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style-bs.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/style.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/plugins/modaldisplay.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css');
@import url('https://console-unipr.elixforms.it/rwe2/isipcss/css/grid_fluid.css');
```
> **IMPORTANTE**: Questi CSS sono caricati a runtime dal CDN e **non vengono bundlati** nel JS.
> La pagina deve avere connessione internet per visualizzarsi correttamente.
## Layout della Pagina (Struttura HTML)
Le pagine seguono un layout standard elixForms con classi CSS specifiche:
```html
<div class="container_12" id="pageBody">
<div class="grid_12">
<div id="userConsole" class="fe-behaviour">
<!-- HEADER -->
<header>
<div class="neutral">
<div class="grid_12">
<div class="console_header">
<div class="leftDiv">
<h1>elixForms</h1>
<div class="logo"><!-- Logo UniPR --></div>
</div>
<div class="rightDiv">...</div>
</div>
</div>
</div>
</header>
<!-- CONTENUTO PRINCIPALE -->
<div class="grid_12 console_main">
<div class="inside">
<div class="fe-module-header-container">
<h2>Titolo Modulo</h2>
</div>
<div class="mainview">
<div class="container">
<!-- Contenuto custom della pagina -->
</div>
</div>
</div>
</div>
<!-- FOOTER -->
<footer class="it-footer">
<div class="it-footer-main">
<div class="container">
<div class="subfooter-top">powered by elixForms</div>
<div class="subfooter-bottom">versione X.XX.X</div>
</div>
</div>
</footer>
</div>
</div>
</div>
```
## Classi CSS del Design System
### Layout a Griglia
| Classe | Scopo |
|---|---|
| `container_12` | Container principale a 12 colonne |
| `grid_12` | Occupa tutte le 12 colonne |
| `clear` | Clearfix |
### Struttura Pagina
| Classe | Scopo |
|---|---|
| `fe-behaviour` | Container principale della console utente |
| `console_header` | Header della console |
| `console_main` | Area contenuto principale |
| `fe-module-header-container` | Header del modulo/form |
| `mainview` | Container del contenuto del form |
### Layout Campi Form (ElixFormsElement)
| Classe | Scopo |
|---|---|
| `iuFieldContainer` | Container di un campo form (label + input) |
| `attrDisplay_left` | Colonna sinistra (label) |
| `attrDisplay_right` | Colonna destra (input) |
| `attrDisplay_label` | Marcatore per la label |
| `attrDisplay_input` | Marcatore per l'input |
| `iuLabelContainer` | Container interno della label |
| `iuInputContainer` | Container interno dell'input |
| `iuTypeString` | Tipo di input stringa |
| `iuClearContainer` | Clearfix interno |
### Classi Input Bootstrap Italia Custom
| Classe | Componente |
|---|---|
| `isiportalPartialAdminFormFieldSingleLineText` | TextField singola riga |
| `isiportalPartialAdminFormFieldMultiLineText` | TextField multiriga |
| `isiportalPartialAdminFormFieldRadio` | ChoiceGroup radio |
| `isiportalPartialAdminFormFieldSelect` | Dropdown select |
| `isiportalPartialAdminCheckboxFieldItem` | Checkbox singolo |
## SCSS Modules (common/)
`ElixFormsComponent.module.scss`:
```scss
.elixFormsComponent {
overflow: hidden;
padding: 1em;
}
```
> **Nota**: Gli stili usano classi Bootstrap Italia e CSS del design system UniPR, senza dipendenze da FluentUI.
## Logo e Risorse Esterne
- **Logo UniPR**: `https://console-unipr.elixforms.it/elixFormsCustom/images/logo.png`
- Le immagini locali vanno in `src/assets/` e importate come moduli ES
- Le immagini vengono copiate nella `dist/` durante la build
## Note Importanti
1. Il CSS di `index.css` nella pagina `scelta-carriera` è **attualmente tutto commentato** — si usano solo i CSS remoti
2. Il CSS injection plugin (`vite-plugin-css-injected-by-js`) gestisce solo i CSS locali e SCSS, non gli `@import url(...)` che restano come riferimenti esterni
3. Per funzionare correttamente, la pagina deployata deve poter raggiungere `console-unipr.elixforms.it`
@@ -0,0 +1,32 @@
---
name: elixforms-custom-workflow-logic
description: Logica di funzionamento delle custom page richiamate dal Custom Workflow v2 di elixForms. Descrive il passaggio dei parametri, i campi obbligatori, e le regole di submit.
---
# Logica di Funzionamento Custom Workflow v2 in elixForms
Le **custom page** all'interno del progetto elixForms servono a gestire flussi esterni personalizzati (chiamati *Custom Workflow 2*).
Vengono lanciate tramite un pulsante presente su un modulo elixForms; la piattaforma reindirizza l'utente alla URL della custom page (sviluppata in questo progetto), passandole in query string diversi parametri di contesto. Al termine delle operazioni, l'utente viene reindirizzato nuovamente al modulo elixForms.
## Parametri e Query String
### Parametri Obbligatori
elixForms invia sempre alla custom page una serie di parametri obbligatori (es. `RWE2_MODULE_ID`, `RWE2_REQUEST_ID`, `custom-workflow-back-url`, `crc`, ecc.).
Questi parametri sono definiti nella classe `ElixFormsComponentAbstract` (metodo/variabile `mandatoryFormFieldNames`).
- **Regola:** Se alla custom page manca uno solo di questi parametri, deve presentare un **errore di autenticazione/accesso** (es. "Parametri obbligatori mancanti") in quanto il flusso risulta non valido (ad es. tentativo di accesso diretto bypassando elixForms). Non deve permettere l'utilizzo del form.
### Parametri Custom (Dati Modulo)
Oltre ai parametri obbligatori, elixForms può passare dati già compilati nel modulo (es. `email`, `COL0001`, ecc.).
- Questi dati vengono ricevuti tramite query string e possono essere utilizzati o visualizzati all'interno della custom page.
## Sottomissione del Form e Rientro in elixForms
Per tornare a elixForms, la custom page deve eseguire un submit `POST` contenente tutti i parametri necessari.
- **Form Action:** L'endpoint di destinazione (es. `https://[server]/rwe2/ComeBackToElixAndSave`).
- **Input Nascosti (Mandatory):** Il form *deve* contenere (come `input type="hidden"`) tutti i parametri obbligatori ricevuti originariamente dalla query string.
- **Input Custom / Dati Rientro:** Il form deve contenere tutti i campi aggiuntivi o le variabili calcolate che devono essere rimandate ad elixForms (es. un campo anagrafico). L'attributo `name` di questi input deve corrispondere esattamente all'identificativo atteso su elixForms (es. `name="COL0001"`).
## Gestione degli ID (Design System e Convenzioni)
Come da regole generali (vedi `AGENTS.md`):
- Gli `ID` dei componenti visibili (TextField, Checkbox, Dropdown) **non devono mai subire alterazioni** (non vanno aggiunti suffissi).
- Qualora ci sia necessità di inserire un input `hidden` per mantenere un valore che abbia lo stesso nome di un campo visibile (per rispettare la convenzione del `name`), l'attributo `ID` dell'input nascosto **deve** avere un suffisso (es. `id="MioCampo_hidden"`), mentre il componente visibile principale mantiene l'ID originale senza suffissi. Questo evita conflitti HTML a livello di ID mantenendo corretti i valori inviati (`name`).
+130
View File
@@ -0,0 +1,130 @@
---
name: elixforms-dev-workflow
description: >
Workflow di sviluppo, debug e deploy del progetto elixForms Custom Pages.
Copre i comandi per avviare il dev server, buildare, debuggare con VS Code,
e le configurazioni di launch/tasks.
Attiva questa skill quando devi eseguire comandi di sviluppo,
configurare il debug, o gestire il ciclo dev/build/deploy.
---
# Workflow di Sviluppo
## Comandi Principali
Tutti i comandi vanno eseguiti **dalla cartella della pagina** (es. `react/scelta-carriera/`):
```powershell
# Installare le dipendenze (prima volta o dopo modifiche a package.json)
cd react
npm install
cd scelta-carriera
npm install
# Avviare il dev server con HMR
npm run dev
# Build per produzione (singolo bundle JS)
npm run build
# Preview della build di produzione
npm run preview
# Lint del codice
npm run lint
```
## Configurazione VS Code
### Launch Configurations (`.vscode/launch.json`)
#### 1. "Scelta Carriere (BROWSER)" — Dev + Chrome Debug
- Avvia `npm run dev` nella cartella della pagina
- Apre automaticamente Chrome quando il server è pronto
- Pattern di rilevamento: `Local:\s+http://localhost:([0-9]+)/`
- WebRoot per sourcemaps: `${workspaceFolder}/react/scelta-carriera/src`
#### 2. "Scelta Carriere (DEBUG ONLY)" — Solo Dev Server
- Avvia `npm run dev` senza aprire il browser
- Utile per debug da terminale o browser già aperto
### Variabili d'Ambiente
- `NO_COLOR: "1"` — Disabilita colori ANSI nell'output (migliora leggibilità nel debug console)
### Skip Files nel Debug
```json
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
]
```
### Tasks (`.vscode/tasks.json`)
#### "Start scelta-carriera (Vite App)"
- Task background che avvia `npm run dev`
- Problem matcher configurato per output Vite:
- `beginsPattern`: "VITE"
- `endsPattern`: "ready in"
## Aggiungere una Nuova Pagina al Debug
Per aggiungere una nuova configurazione di debug per una pagina:
```json
// In .vscode/launch.json, aggiungere in "configurations":
{
"name": "<Nome Pagina> (BROWSER)",
"cwd": "${workspaceFolder}/react/<nome-pagina>",
"env": { "NO_COLOR": "1" },
"outputCapture": "console",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
],
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "Local:\\s+http://localhost:([0-9]+)/",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/react/<nome-pagina>/src"
}
}
```
## Flusso di Deploy
1. `npm run build` nella cartella della pagina
2. Output: `dist/<nome-pagina>.js` (+ sourcemap e asset)
3. Caricare il file JS sul server remoto `console-unipr.elixforms.it`
4. La pagina viene servita dalla piattaforma elixForms con i parametri query appropriati
## Testing Locale con Query Parameters
Per testare localmente con i parametri elixForms, aggiungere query params all'URL del dev server:
```
http://localhost:5173/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3
```
I valori verranno letti da `QueryParamHelper` e pre-popoleranno i campi del form.
## Struttura delle Dipendenze
```
react/ ← npm install qui per dipendenze condivise
├── node_modules/ ← react, react-dom
├── package.json
└── scelta-carriera/ ← npm install qui per devDependencies
├── node_modules/ ← vite, eslint, plugin, sass-embedded
└── package.json
```
> **Nota**: Le dipendenze runtime (React, ReactDOM) sono nel `package.json` root.
> Le dipendenze di sviluppo (Vite, ESLint) sono nel `package.json` della pagina.
> npm risolve le dipendenze risalendo la gerarchia delle cartelle (hoisting).
+190
View File
@@ -0,0 +1,190 @@
---
name: elixforms-php-common-library
description: >
Documentazione della libreria condivisa PHP common/ del progetto elixForms.
Descrive la classe base ElixFormsComponent, i metodi factory per i campi form,
ElixFormsElement per il layout, QueryParamHelper per i query parameter,
e il pattern di ereditarietà per creare pagine custom.
Attiva questa skill quando lavori sui componenti PHP condivisi, crei nuovi tipi di campo,
modifichi la classe base, o integri con la piattaforma elixForms in PHP.
---
# Libreria Common PHP elixForms
## Panoramica
La cartella `php/common/` contiene la libreria condivisa PHP usata da tutte le pagine custom PHP. È basata su **Bootstrap Italia + HTML nativo** e implementa un sistema di form con campi dinamici, analogo alla libreria React in `react/common/`.
## Architettura delle Classi
```
ElixFormsComponent (classe astratta, PHP 8.0+)
└── SceltaCarrieraPage (classe concreta, in scelta-carriera/index.php)
└── [AltrePageCustom] (classi concrete nelle sotto-cartelle)
```
### Namespace
Tutte le classi usano il namespace PSR-4:
```php
namespace ElixForms\Common;
```
Le pagine importano con:
```php
require_once __DIR__ . '/../../vendor/autoload.php';
use ElixForms\Common\ElixFormsComponent;
```
## File della Libreria
### `ElixFormsComponent.php` (Classe Base Astratta)
Equivalente di `ElixFormsComponentAbstract.tsx` in React.
#### Proprietà
```php
protected array $properties; // Equivalente di IElixFormsComponentProperties
```
Chiavi supportate in `$properties`:
| Chiave | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| `moduleName` | string | Sì | Nome del modulo nel breadcrumb |
| `cardTitle` | string | Sì | Titolo della card principale |
| `userDisplayName` | string | No | Nome utente nell'header |
| `headerTitle` | string | No | Titolo sotto il breadcrumb |
| `headerHeroImageSrc` | string | No | URL immagine hero |
| `cardDescription` | string | No | Descrizione nella card |
| `alertInfoTitle` | string | No | Titolo alert informativo |
| `alertInfoMessage` | string | No | Messaggio alert informativo |
| `instructionsTitle` | string | No | Titolo istruzioni |
| `instructionsMessage` | string | No | Messaggio istruzioni |
| `submitText` | string | No | Testo bottone submit (default: 'INVIA') |
| `additionalFieldsJson` | string | No | JSON schema per campi dinamici |
| `isDarkTheme` | bool | No | Tema scuro (riservato per uso futuro) |
#### Metodi Factory (Campi Form)
| Metodo | Tipo Campo | HTML Output |
|---|---|---|
| `createTextInput($name, $label, $required)` | Testo | `<input type="text">` |
| `createTextAreaInput($name, $label, $required)` | Textarea | `<textarea>` |
| `createNumberInput($name, $label, $required)` | Numerico | `<input type="number">` |
| `createBooleanInput($name, $label, $required)` | Booleano (Sì/No) | Radio buttons |
| `createRadioInput($name, $label, $options, $required)` | Radio | Radio buttons |
| `createCheckboxInput($name, $label, $options)` | Checkbox multipli | Checkboxes + hidden |
| `createDropdownInput($name, $label, $options, $required)` | Dropdown | `<select>` |
| `createHiddenInput($name)` | Nascosto | `<input type="hidden">` |
#### Metodi Estensibili (Override)
```php
// Campi custom del form — OBBLIGATORIO sovrascrivere
protected function createCustomFormFields(): string { return ''; }
// Contenuto extra prima del form
protected function renderExtraContentPre(): string { return ''; }
// Contenuto extra dopo il form
protected function renderExtraContentPost(): string { return ''; }
```
#### Metodo Principale
```php
public function render(): string
```
Genera l'HTML **completo** della pagina (`<!DOCTYPE html>` ... `</html>`), includendo:
- `<head>` con CSS Bootstrap Italia e design system UniPR
- Header con logo UniPR e nome utente
- Breadcrumb
- Card con alert, istruzioni, form e campi
- Footer "powered by elixForms"
- Script JS per checkbox
### `ElixFormsElement.php` (Wrapper Layout)
Genera il layout a due colonne per ogni campo form:
```php
$element = new ElixFormsElement($labelHtml, $inputHtml, $separator);
echo $element->render();
```
Output HTML:
```html
<div class="row mb-3 align-items-start">
<div class="col-12 col-md-4">
<div class="text-md-end pe-md-3 mt-2">{label}</div>
</div>
<div class="col-12 col-md-8">
{input}
</div>
</div>
```
### `QueryParamHelper.php` (Utility Query String)
Classe statica per leggere parametri dalla query string `$_GET`:
```php
QueryParamHelper::getCheckedFromQuery('COL0006'); // int[] (checkbox, valori separati da virgola)
QueryParamHelper::getOptionFromQuery('COL0015'); // ?int (radio/dropdown)
QueryParamHelper::getBooleanFromQuery('COL0004'); // ?bool (boolean)
QueryParamHelper::getDecodedTextFromQuery('COL0002'); // ?string (testo URL-decoded)
```
### `HttpClient.php` (Utility Richieste HTTP REST)
Adapter applicativo basato su Guzzle per effettuare chiamate HTTP server-to-server sicure verso Web Service esterni (es. endpoint contratti). Accetta un `GuzzleHttp\\ClientInterface` opzionale nel costruttore per consentire mocking e test senza traffico di rete:
```php
use ElixForms\Common\HttpClient;
$client = new HttpClient([
'X-Api-Key' => 'chiave-api-custom',
'Accept' => 'application/json'
]);
$client->setBasicAuth('username', 'password');
$client->setTimeout(10); // timeout in secondi
// Richiesta GET
$response = $client->get('http://api-endpoint/cerca', [
'parametro1' => 'valore1'
]);
```
**Best Practices gestite da HttpClient:**
- **Timeout**: Gestione dei timeout di connessione ed esecuzione per evitare blocchi infiniti del server Apache.
- **Sicurezza delle credenziali**: Permette di eseguire chiamate server-side mantenendo chiavi API e password nascoste al browser del client.
- **Error Handling**: Se il server remoto restituisce codici HTTP diversi da 20x (es. 404, 500), solleva una `RuntimeException` contenente il codice di stato generico senza includere nel messaggio d'errore l'eventuale payload della risposta (es. tag HTML dell'errore di Apache) per motivi di sicurezza ed integrità del JSON finale.
## Differenze rispetto alla versione React
| Aspetto | React | PHP |
|---|---|---|
| **Rendering** | Client-side (JSX → DOM) | Server-side (PHP → HTML) |
| **State** | `this.state` + `this.setState()` | Valori da `$_GET` (stateless) |
| **Checkbox sync** | React state + hidden input | JS client-side + hidden input |
| **Build** | Vite → single bundle JS | Nessuna build necessaria |
| **CSS** | Iniettato nel JS | `<link>` tags nell'HTML |
| **Output** | Singolo file `.js` | File `.php` serviti da Apache |
## Parametri Obbligatori elixForms
Gestiti automaticamente come hidden inputs dalla classe base:
- `RWE2_MODULE_ID`, `RWE2_REQUEST_ID`
- `custom-workflow-back-url`, `custom-workflow-generic-id`
- `custom-workflow-current-tabrel-genid`, `custom-workflow-source-field`
- `crc`, `MODULE_TESTMODE_KEY`, `ELANG`
## Note Importanti
- Il form fa POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
- L'encoding charset è `ISO-8859-1` per compatibilità con il backend
- Gli ID dei campi form **non devono mai essere manipolati** (vedi AGENTS.md)
- L'ID dell'hidden input dei checkbox e dei mandatory fields ha suffisso `_hidden`
- I checkbox usano un attributo `data-param` e uno script JS per sincronizzare il valore con l'hidden input
+258
View File
@@ -0,0 +1,258 @@
---
name: elixforms-php-create-new-page
description: >
Guida step-by-step per creare una nuova pagina custom PHP nel progetto elixForms.
Copre la struttura dei file, l'importazione della libreria common,
le convenzioni di naming, e la configurazione per il deploy su Apache.
Attiva questa skill quando devi creare una nuova pagina PHP,
duplicare una pagina esistente, o scaffoldare un nuovo micro-progetto PHP nel monorepo.
---
# Creare una Nuova Pagina PHP elixForms Custom
## Prerequisiti
- PHP 8.0+ installato
- Web server Apache configurato per servire la cartella `php/`
## Step 1: Creare la Cartella della Pagina
```powershell
cd php
mkdir <nome-pagina>
```
Il nome della cartella deve essere in **kebab-case** (es. `scelta-carriera`, `recupero-proposta`).
## Step 2: Creare il File index.php
Creare `php/<nome-pagina>/index.php`:
```php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../common/ElixFormsComponent.php';
use ElixForms\Common\ElixFormsComponent;
/**
* Pagina custom "<NomePagina>" per elixForms.
*/
class NomePaginaPage extends ElixFormsComponent
{
protected function createCustomFormFields(): string
{
$html = '';
// Aggiungi qui i campi del form usando i metodi factory:
// $html .= $this->createTextInput('COL0001', 'Campo Testo', true);
// $html .= $this->createDropdownInput('COL0002', 'Scelta', [...options], true);
return $html;
}
// Opzionale: contenuto extra prima del form
// protected function renderExtraContentPre(): string { return ''; }
// Opzionale: contenuto extra dopo il form
// protected function renderExtraContentPost(): string { return ''; }
}
// Entry point
$page = new NomePaginaPage([
'moduleName' => 'Nome Modulo',
'cardTitle' => 'Titolo Pagina',
'cardDescription' => 'Descrizione della pagina.',
'userDisplayName' => '', // Verrà popolato da elixForms
'headerTitle' => '',
'headerHeroImageSrc' => '',
'alertInfoTitle' => '',
'alertInfoMessage' => '',
'instructionsTitle' => '',
'instructionsMessage' => '',
'submitText' => 'INVIA',
]);
echo $page->render();
```
## Step 3: Definire i Campi Custom
Sovrascrivere `createCustomFormFields()` con i campi necessari:
```php
protected function createCustomFormFields(): string
{
$html = '';
// Dropdown
$html .= $this->createDropdownInput('COL0015', 'Scelta', [
['value' => 0, 'label' => 'Opzione A'],
['value' => 1, 'label' => 'Opzione B'],
['value' => 2, 'label' => 'Opzione C'],
], true);
// Testo
$html .= $this->createTextInput('COL0002', 'Nome', true);
// Textarea
$html .= $this->createTextAreaInput('COL0003', 'Note', false);
// Booleano (Sì/No)
$html .= $this->createBooleanInput('COL0004', 'Conferma', true);
// Radio
$html .= $this->createRadioInput('COL0005', 'Preferenza', [
['value' => 1, 'label' => 'Prima scelta'],
['value' => 2, 'label' => 'Seconda scelta'],
], true);
// Checkbox
$html .= $this->createCheckboxInput('COL0006', 'Opzioni Multiple', [
['value' => 1, 'label' => 'Opzione 1'],
['value' => 2, 'label' => 'Opzione 2'],
]);
// Numerico
$html .= $this->createNumberInput('COL0007', 'Quantità', true);
return $html;
}
```
## Step 4: Opzionale — Aggiungere File di Configurazione
Per pagine complesse, creare un file `config.json` nella cartella della pagina:
```json
{
"component": {
"moduleName": "Nome Modulo",
"cardTitle": "Titolo",
"cardDescription": "Descrizione",
"submitText": "CONFERMA"
},
"elixForms": {
"inputs": {
"codiceFiscale": "COL0009"
},
"outputs": {
"campo1": "COL0001",
"campo2": "COL0002"
}
}
}
```
Caricare nel PHP:
```php
$config = json_decode(file_get_contents(__DIR__ . '/config.json'), true);
$page = new NomePaginaPage($config['component']);
echo $page->render();
```
## Step 4.1: Pagine Interattive e Chiamate Asincrone (AJAX)
Se la pagina richiede autocompletamento o interattività (es. ricerca contratti):
### 1. Proxy Script PHP Locale
Per non esporre credenziali sensibili (API Key, Basic Auth) al client, creare sempre uno script proxy locale (es. `search-contratti.php`) che:
- Riceve i dati dal client.
- Esegue la chiamata protetta tramite `HttpClient`.
- Valida con `json_decode` che la risposta sia JSON valido prima di inviarla.
- Risponde con `500` e un JSON d'errore pulito in caso di eccezioni.
### 2. Risoluzione Percorso URL in JavaScript
Il server integrato PHP non esegue il redirect automatico per aggiungere lo slash alla fine delle cartelle. In JavaScript, calcolare sempre la base directory dinamica per supportare URL sia con che senza slash finale o con `index.php` esplicito:
```javascript
var baseDir = window.location.pathname;
if (!baseDir.endsWith('/') && !baseDir.endsWith('.php')) {
baseDir += '/';
}
var lastSlash = baseDir.lastIndexOf('/');
var dir = baseDir.substring(0, lastSlash + 1);
var url = dir + 'search-contratti.php?term=' + encodeURIComponent(term);
```
### 3. Parsing Sicuro degli Errori HTTP (Fetch)
Non eseguire direttamente `response.json()` su risposte d'errore (500/404) o non-JSON per evitare il crash `Unexpected token '<'`. Utilizzare la tecnica del `response.text()`:
```javascript
fetch(url)
.then(function(response) {
return response.text().then(function(text) {
var data = null;
try {
data = JSON.parse(text);
} catch (e) {
// Risposta non JSON (es. HTML d'errore del server)
}
if (!response.ok) {
var errMsg = (data && data.message) ? data.message :
(data && data.error) ? data.error :
('Errore API: ' + response.status + ' ' + response.statusText);
throw new Error(errMsg);
}
if (data === null) {
throw new Error('Risposta del server non in formato JSON.');
}
return data;
});
})
```
## Step 5: Testing Locale
Per testare le pagine localmente, è necessario avviare il server web dalla cartella radice `php/` (in modo che la cartella `common/` sia accessibile correttamente):
### Opzione 1: PHP CLI Server locale
```powershell
cd php
php -S localhost:8080
```
Aprire nel browser:
```
http://localhost:8080/<nome-pagina>/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3
```
### Opzione 2: Docker
Dalla root del repository, eseguire:
```bash
docker run --rm -p 8080:8080 -v "${PWD}/php:/app" -w /app php:8.2-cli php -S 0.0.0.0:8080
```
Aprire nel browser:
```
http://localhost:8080/<nome-pagina>/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&COL0002=test&COL0015=3
```
## Step 6: Deploy su Apache
1. Caricare la cartella `php/` (o la singola cartella della pagina) sul server Apache
2. Assicurarsi che Apache abbia `mod_php` o `php-fpm` configurato
3. La pagina sarà accessibile via URL diretto (es. `https://server.example.com/php/nome-pagina/`)
4. elixForms reindirizza l'utente alla pagina passando i parametri in query string
## Struttura Finale
```
php/<nome-pagina>/
├── index.php ← Entry point e classe pagina
└── config.json ← Opzionale: configurazione separata
```
## Checklist Nuova Pagina PHP
- [ ] Cartella creata in `php/<nome-pagina>/`
- [ ] `index.php` con `declare(strict_types=1)` e namespace
- [ ] Classe che estende `ElixFormsComponent`
- [ ] `createCustomFormFields()` sovrascritta con i campi corretti
- [ ] Proprietà di configurazione impostate correttamente
- [ ] `require_once __DIR__ . '/../common/ElixFormsComponent.php'`
- [ ] Test locale con `php -S` o Docker
- [ ] Verifica che i campi form producano l'HTML corretto
- [ ] Verifica che i parametri obbligatori vengano letti da `$_GET`
@@ -0,0 +1,124 @@
---
name: elixforms-php-project-architecture
description: >
Architettura e struttura della parte PHP del progetto elixForms Custom Pages.
Descrive la cartella php/, la libreria common condivisa, il pattern delle pagine,
le convenzioni di naming, e il deploy su Apache.
Attiva questa skill quando lavori sulla parte PHP del progetto,
crei nuove pagine PHP, modifichi l'architettura, o devi capire come è organizzato il codice PHP.
---
# Architettura PHP del Progetto elixForms Custom Pages
## Panoramica
La cartella `php/` contiene le custom page elixForms realizzate in **PHP 8.0+ puro** (nessun framework). Ogni pagina è un file PHP autonomo che include la libreria `common/` e produce HTML server-side con **Bootstrap Italia**.
Questa struttura è l'equivalente PHP del monorepo React in `react/`.
## Struttura del Workspace
```
elixforms-custom-pages/
├── composer.json ← Configurazione Composer e autoload PSR-4
├── vendor/ ← Autoloader generato (non versionato)
├── react/ ← Custom pages React (esistente)
└── php/ ← Custom pages PHP
├── AGENTS.md ← Regole dell'agente PHP
├── common/ ← Libreria condivisa PHP
│ ├── ElixFormsComponent.php ← Classe base astratta
│ ├── ElixFormsElement.php ← Wrapper layout label+input
│ └── QueryParamHelper.php ← Utility query string
└── scelta-carriera/ ← Esempio di pagina custom
└── index.php ← Entry point (classe + rendering)
```
## Pattern Architetturale: Singolo File PHP per Pagina
Ogni pagina (es. `scelta-carriera/`) è un file PHP autonomo che:
1. **Carica l'autoloader Composer** tramite `vendor/autoload.php`
2. **Definisce una classe** che estende `ElixFormsComponent`
3. **Sovrascrive `createCustomFormFields()`** per i campi specifici
4. **Istanzia e renderizza** la pagina con `echo $page->render()`
5. **Genera HTML completo** (dalla `<!DOCTYPE html>` al `</html>`)
6. **Non richiede build** — servito direttamente da Apache
## Namespace e Autoloading
Il progetto usa **namespace PSR-4**:
```php
namespace ElixForms\Common; // Per le classi in common/
```
L'autoloading PSR-4 è gestito da Composer:
```php
require_once __DIR__ . '/../../vendor/autoload.php';
```
## Requisiti PHP
- **Versione minima**: PHP 8.0+
- **Funzionalità usate**:
- `declare(strict_types=1)` — strict typing
- Typed properties (`protected array $properties`)
- `match` expression (nel renderField)
- Named arguments
- Union types (`int|null`, `string|null`)
- Arrow functions (`fn() =>`)
## CSS Strategy
Le pagine PHP usano `<link>` tags per caricare i CSS (non bundle JS):
```html
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-italia@2.18.2/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css">
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css">
```
> **Nota**: Stessi CSS del progetto React, ma caricati via `<link>` anziché `@import url()` nel JS bundle.
## Integrazione con elixForms
Identica alla versione React:
1. **Query parameters**: Parametri obbligatori passati nell'URL da elixForms
2. **Form POST**: Il form fa submit a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
3. **Hidden inputs**: I parametri query vengono inseriti come campi hidden nel form
4. **Encoding**: `accept-charset="ISO-8859-1"` per compatibilità con il backend
## Convenzioni di Naming
- **Cartelle pagina**: kebab-case (es. `scelta-carriera`)
- **File entry point**: sempre `index.php`
- **Classi PHP**: PascalCase con suffisso `Page` (es. `SceltaCarrieraPage`)
- **Namespace**: `ElixForms\Common` per la libreria condivisa
- **Configurazione**: opzionale `config.json` nella cartella della pagina
## Differenze con il Progetto React
| Aspetto | React (`react/`) | PHP (`php/`) |
|---|---|---|
| **Linguaggio** | TypeScript/JSX | PHP 8.0+ |
| **Rendering** | Client-side | Server-side |
| **Build tool** | Vite (single bundle JS) | Nessuno (file serviti direttamente) |
| **Dipendenze** | npm (React, Vite, plugin) | Composer (autoload PSR-4) |
| **CSS loading** | `@import url()` + CSS injection plugin | `<link>` tags |
| **State management** | React state (`this.setState`) | Stateless (`$_GET`) |
| **Deploy** | Upload del bundle `.js` | Upload dei file `.php` |
| **Dev server** | `npm run dev` (Vite HMR) | `php -S` / Docker |
## Deploy su Apache
1. Eseguire `composer install --no-dev --optimize-autoloader` dalla root del repository
2. Caricare sul server le cartelle `php/` e `vendor/`, mantenendole allo stesso livello
3. Configurazione Apache minima:
- `mod_php` o `php-fpm` abilitato
- `DirectoryIndex index.php` (default Apache)
4. La pagina è accessibile via URL: `https://server/php/nome-pagina/`
5. elixForms reindirizza l'utente alla pagina passando i parametri via query string
@@ -0,0 +1,149 @@
---
name: elixforms-project-architecture
description: >
Architettura e struttura del progetto elixForms Custom Pages.
Descrive il monorepo, la libreria common condivisa, il pattern delle pagine autonome
e tutte le convenzioni di naming, configurazione e build.
Attiva questa skill quando lavori su qualsiasi parte del progetto,
crei nuove pagine, modifichi l'architettura o devi capire come è organizzato il codice.
---
# Architettura del Progetto elixForms Custom Pages
## Panoramica
Questo progetto è un **monorepo** per la creazione di pagine web React custom destinate alla piattaforma **elixForms** dell'Università di Parma (UniPR). Ogni pagina è un micro-progetto indipendente che produce un **singolo file JS bundle** contenente React, Bootstrap Italia e il codice custom, da pubblicare su un server remoto.
## Struttura del Workspace
```
elixforms-custom-pages/ ← Root del workspace (VS Code)
├── .vscode/ ← Configurazioni VS Code (launch, tasks)
├── docs/ ← Documentazione (PDF manuali)
└── react/ ← Root del monorepo React
├── package.json ← Dipendenze condivise (React, ReactDOM)
├── tsconfig.base.json ← Configurazione TypeScript base condivisa
├── .gitignore
├── common/ ← Libreria condivisa di componenti ElixForms
│ ├── package.json
│ ├── tsconfig.json ← Estende tsconfig.base, composite: true
│ └── src/
│ ├── ElixFormsComponent.tsx
│ ├── ElixFormsComponentAbstract.tsx
│ ├── ElixFormsElement.tsx
│ ├── ElixFormsTypes.tsx
│ ├── ElixFormsComponent.module.scss
│ ├── IElixFormsComponentProperties.ts
│ ├── IElixFormsComponentFormState.tsx
│ ├── IElixFormsComponentCustomFormFieldFactory.ts
│ └── QueryParamHelper.tsx
└── scelta-carriera/ ← Esempio di pagina custom (micro-progetto)
├── package.json
├── tsconfig.json ← Estende tsconfig.base
├── vite.config.js
├── eslint.config.js
├── index.html
└── src/
├── main.jsx ← Entry point React
├── App.jsx ← Componente principale della pagina
├── App.css ← CSS (importa CSS remoti di elixForms)
├── index.css ← CSS globale (attualmente commentato)
├── global.d.ts ← Dichiarazioni di tipo per moduli
└── assets/ ← Immagini e risorse statiche
```
## Pattern Architetturale: Micro-Progetto per Pagina
Ogni pagina (es. `scelta-carriera/`) è un progetto Vite completamente autonomo che:
1. **Importa dalla libreria `common/`** tramite alias `@common` configurato in `vite.config.js`
2. **Ha le proprie dipendenze** nel suo `package.json` (solo devDependencies e plugin Vite)
3. **Condivide le dipendenze runtime** (React, ReactDOM) dal `package.json` root di `react/`
4. **Produce un singolo file JS** tramite la configurazione Rollup in `vite.config.js`
5. **Estende il tsconfig base** per avere configurazione TypeScript coerente
## Dipendenze Condivise (react/package.json)
Le dipendenze runtime sono hoisted al livello `react/`:
- `react` ^19.2.7
- `react-dom` ^19.2.5
## Configurazione TypeScript
### Base (tsconfig.base.json)
- `module`: ESNext
- `target`: ES2022
- `moduleResolution`: bundler
- `jsx`: react-jsx
- `strict`: true
- Path alias: `@common/*``./common/*`
### Common (common/tsconfig.json)
- `composite`: true (per project references)
- `declaration`: true, `declarationMap`: true
### Pagine (scelta-carriera/tsconfig.json)
- `extends`: `../tsconfig.base.json`
- `references`: `../common`
- `allowJs`: true, `checkJs`: true (supporto misto JSX/TSX)
## Configurazione Build (Vite)
Ogni pagina usa questa configurazione per generare un **singolo bundle JS**:
```js
// Plugin chiave
react() // Supporto React JSX
cssInjectedByJsPlugin() // CSS iniettato nel JS (no file .css separati)
// Build options
cssCodeSplit: false
rollupOptions.input: 'src/main.jsx'
rollupOptions.output:
codeSplitting: false
manualChunks: undefined
entryFileNames: '<nome-pagina>.js' // Nome descrittivo (es. 'scelta-carriera.js')
assetFileNames: '[name].[ext]'
sourcemap: true
```
## Resolve Alias
```js
resolve.alias: {
"@common": path.resolve(__dirname, "../common")
}
server.fs.allow: [".."] // Permette import da common/
```
## CSS Strategy
Le pagine caricano CSS dal server remoto elixForms tramite `@import url(...)` in `App.css`:
- CSS da `console-unipr.elixforms.it` per il design system dell'università
- I componenti Bootstrap Italia usano classi HTML e CSS del design system UniPR
- SCSS modules sono usati in `common/` per stili dei componenti condivisi
- `sass-embedded` è installato come devDependency per supporto SCSS
## Integrazione con elixForms
La pagina custom interagisce con la piattaforma elixForms tramite:
1. **Query parameters**: Parametri obbligatori passati nell'URL (es. `RWE2_MODULE_ID`, `RWE2_REQUEST_ID`, `crc`, etc.)
2. **Form POST**: Il form fa submit a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave`
3. **Hidden inputs**: I parametri query vengono inseriti come campi hidden nel form
4. **Encoding**: `acceptCharset="ISO-8859-1"` per compatibilità con il backend
## Convenzioni di Naming
- **Cartelle pagina**: kebab-case (es. `scelta-carriera`)
- **Package name**: `@elixforms/<nome-pagina>` (es. `@elixforms/scelta-carriera`)
- **Bundle output**: `<nome-pagina>.js` (es. `scelta-carriera.js`)
- **Componenti React**: PascalCase
- **File sorgente**: I file correnti sono `.jsx` ma il progetto ha pieno supporto TypeScript (`.tsx`)
- **Tipo moduli**: `"type": "module"` nelle pagine
## Ambiente di Sviluppo
- **Dev server**: `npm run dev` (Vite HMR)
- **Build**: `npm run build` → output in `dist/`
- **Debug VS Code**: Configurazioni in `.vscode/launch.json` per avviare Vite + Chrome
- **Lint**: ESLint con plugin react-hooks e react-refresh
@@ -0,0 +1,171 @@
---
name: elixforms-typescript-conventions
description: >
Convenzioni TypeScript e React per il progetto elixForms.
Copre la configurazione TypeScript, i pattern di codice, la gestione dei tipi,
l'uso di JSDoc per type annotations nei file .jsx, e le best practices.
Attiva questa skill quando scrivi codice TypeScript o React,
crei nuovi componenti, definisci interfacce, o gestisci i tipi nel progetto.
---
# Convenzioni TypeScript e React
## Configurazione TypeScript del Progetto
### Opzioni Compiler Principali
- **target**: ES2022
- **module**: ESNext
- **moduleResolution**: bundler
- **jsx**: react-jsx (trasformazione automatica JSX senza import React)
- **strict**: true
- **isolatedModules**: true
- **verbatimModuleSyntax**: true (usa `import type` per import solo di tipi)
### File Misti JSX/TSX
Il progetto supporta **sia file `.jsx` che `.tsx`** grazie a:
- `allowJs: true` e `checkJs: true` nel tsconfig delle pagine
- Type annotations via JSDoc nei file `.jsx`
#### Pattern JSDoc per Tipi nei File .jsx
```jsx
/** @type {import('@common/src/ElixFormsComponent').ElixFormsReact} */
var myElixFormsReact = new ElixForms.ElixFormsReact({...});
// Per i parametri di callback
customFormFieldsConfiguration: (
/** @type {import('../../common/src/IElixFormsComponentCustomFormFieldFactory').IElixFormsComponentCustomFormFieldFactory} */
formFieldFactory) => (...)
```
## Pattern React Correnti
### Class Components (common/)
La libreria `common/` usa **React Class Components** (non hooks):
```tsx
abstract class ElixFormsComponentAbstract
extends Component<IElixFormsComponentProperties, IElixFormsComponentFormState> {
constructor(props: IElixFormsComponentProperties) {
super(props);
this.state = { formData: {} };
}
// State gestito con this.setState()
this.setState({ formData: { ...this.state.formData, [name]: value } });
}
```
### Functional Components (pagine)
Le pagine usano **Functional Components** con hooks:
```jsx
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
// ...
}
```
### Istanziazione del Componente ElixForms
Il componente ElixForms viene istanziato come classe (non come JSX):
```jsx
// ✅ Corretto - istanziazione diretta
var myElixFormsReact = new ElixForms.ElixFormsReact({...props...});
var elixFormsReactRender = myElixFormsReact.render();
// Poi nel JSX
return <>{elixFormsReactRender}</>;
// ❌ NON usare come componente JSX
return <ElixForms.ElixFormsReact {...props} />;
```
## Import Conventions
### Import Type-Only (verbatimModuleSyntax)
Con `verbatimModuleSyntax: true`, usare sempre `import type` per i tipi:
```tsx
// ✅ Corretto
import type { IElixFormsComponentProperties } from './IElixFormsComponentProperties';
import type { JSX } from 'react';
// ❌ Errato - causerà errore con verbatimModuleSyntax
import { IElixFormsComponentProperties } from './IElixFormsComponentProperties';
```
### Import di Moduli
```tsx
// React - import specifici
import { Component, type FormEvent, type JSX, useState } from 'react';
import React from 'react';
// Componenti common - import namespace
import * as ElixForms from '@common/src/ElixFormsComponent';
// CSS
import './App.css'
```
## Tipi Custom del Progetto
```typescript
// Opzioni per campi form
type ElixFormsCheckboxOption = { value: number; label: string; required?: boolean };
type ElixFormsDropdownOption = { value: number; label: string };
type ElixFormsRadioOption = { value: number; label: string };
// State del form
interface IElixFormsComponentFormState {
formData: { [key: string]: any };
}
```
## Dichiarazioni di Modulo
`global.d.ts` nella cartella `src/` di ogni pagina:
```typescript
declare module "*.css";
declare module "*.svg";
declare module "*.png";
```
## Best Practices per il Progetto
1. **Naming**:
- Interfacce: prefisso `I` (es. `IElixFormsComponentProperties`)
- Tipi: PascalCase (es. `ElixFormsCheckboxOption`)
- File interfacce: nome dell'interfaccia (es. `IElixFormsComponentProperties.ts`)
- File componenti: nome del componente (es. `ElixFormsComponent.tsx`)
2. **Estensioni file**:
- `.tsx` per componenti React con TypeScript (common/)
- `.ts` per moduli TypeScript puri (interfacce, tipi)
- `.jsx` per componenti React con JavaScript (pagine attuali)
- `.scss` per stili con moduli SCSS
3. **Export**:
- Named export per tipi e interfacce
- Default export per classi componente
- Re-export con alias: `export { ElixFormsComponent as ElixFormsReact }`
4. **State management**:
- Usare `this.setState` con spread operator per update immutabili
- I checkbox mantengono un `Map<string, number[]>` separato
5. **Bootstrap Italia**:
- Usare markup HTML nativo con classi Bootstrap Italia e del design system UniPR
- Per i campi testuali usare `input`, `textarea`, `select` con classi `form-control`/`form-select`
- Per i checkbox/radio usare `input type="checkbox"`/`input type="radio"` con classi `form-check-input`
- Il layout dei campi deve rimanere compatibile con `ElixFormsElement`
+136
View File
@@ -0,0 +1,136 @@
---
name: elixforms-vite-build-config
description: >
Configurazione Vite e Rollup per il progetto elixForms.
Copre la build single-bundle, i plugin utilizzati, gli alias,
il sourcemap, e la strategia CSS (injection nel JS).
Attiva questa skill quando devi modificare la build configuration,
risolvere problemi di bundling, aggiungere plugin Vite,
o ottimizzare l'output di produzione.
---
# Configurazione Vite e Build
## Stack di Build
- **Vite** v8.x — Dev server e bundler
- **@vitejs/plugin-react** v6.x — Supporto React/JSX
- **vite-plugin-css-injected-by-js** v5.x — Inietta CSS nel bundle JS
- **sass-embedded** v1.99.x — Compilazione SCSS (per common/)
- **Rollup** — Bundler interno di Vite (configurato via `rollupOptions`)
## Configurazione Vite Completa di Riferimento
```js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import path from "path";
export default defineConfig({
plugins: [
react(),
cssInjectedByJsPlugin()
],
build: {
outDir: 'dist',
cssCodeSplit: false,
rollupOptions: {
input: 'src/main.jsx',
output: {
codeSplitting: false,
manualChunks: undefined,
entryFileNames: '<nome-pagina>.js',
assetFileNames: '[name].[ext]'
},
},
sourcemap: true,
},
resolve: {
alias: {
"@common": path.resolve(__dirname, "../common"),
}
},
server: {
fs: {
allow: [".."]
}
}
})
```
## Dettaglio Opzioni Critiche
### Single Bundle (Rollup)
| Opzione | Valore | Scopo |
|---|---|---|
| `codeSplitting` | `false` | Disabilita il code splitting |
| `manualChunks` | `undefined` | Impedisce separazione vendor/app |
| `entryFileNames` | `'<nome>.js'` | Nome deterministico senza hash |
| `assetFileNames` | `'[name].[ext]'` | Asset senza hash nei nomi |
### CSS Strategy
| Opzione | Valore | Scopo |
|---|---|---|
| `cssCodeSplit` | `false` | No file CSS separati |
| `cssInjectedByJsPlugin()` | plugin | CSS iniettato runtime nel DOM via JS |
### Alias di Import
| Alias | Path | Scopo |
|---|---|---|
| `@common` | `../common` | Accesso alla libreria condivisa |
### Dev Server
| Opzione | Valore | Scopo |
|---|---|---|
| `server.fs.allow` | `[".."]` | Permette import fuori dalla root del progetto |
## Comandi
```powershell
# Dev con HMR
npm run dev
# Build produzione
npm run build
# Preview build produzione
npm run preview
# Lint
npm run lint
```
## Output della Build
```
dist/
<nome-pagina>.js ← Bundle unico (React + CSS + codice)
<nome-pagina>.js.map ← Source map
*.png, *.svg ← Asset referenziati nel codice
```
## Risoluzione Problemi Comuni
### Errore: "The request url is outside of Vite serving allow list"
- **Causa**: Import di file dalla cartella `common/` o altre cartelle esterne
- **Fix**: Aggiungere `server.fs.allow: [".."]` nella configurazione Vite
### Errore: "Cannot find module '@common/...'"
- **Causa**: Alias non configurato o path errato
- **Fix**: Verificare `resolve.alias` e che la cartella `common/` esista
### Bundle troppo grande
- **Causa**: l'uso di componenti UI pesanti o CSS non ottimizzati
- **Fix possibili**:
- Mantenere i CSS locali limitati e affidarsi ai CSS remoti del design system
- Evitare dipendenze UI pesanti per le pagine custom
- Esternalizzare React se la pagina host lo fornisce già
### CSS non applicato in produzione
- **Causa**: `cssInjectedByJsPlugin` non installato o non configurato
- **Fix**: Verificare plugin in `vite.config.js` e `npm install vite-plugin-css-injected-by-js`
### SCSS non compilato
- **Causa**: `sass-embedded` mancante
- **Fix**: `npm install -D sass-embedded`
+171
View File
@@ -0,0 +1,171 @@
# Port elixForms Custom Pages in PHP + Creazione Agenti React e PHP
## Obiettivo
Creare una versione PHP della libreria `common/` e una pagina di esempio `scelta-carriera`, replicando la stessa architettura e logica attualmente implementata in React/TypeScript. Inoltre, creare due agenti specializzati ("React" e "PHP") e le relative skill per il mondo PHP.
## Panoramica dell'architettura React attuale
```mermaid
graph TD
A["ElixFormsComponentAbstract.tsx<br/>(classe astratta)"] --> B["SceltaCarrieraComponent.tsx<br/>(override createCustomFormFields)"]
A --> C["RecuperoPropostaCctDaContrattiComponent.tsx<br/>(override createCustomFormFields + autocomplete)"]
A --> D["ElixFormsElement.tsx<br/>(layout label+input)"]
A --> E["QueryParamHelper.tsx<br/>(lettura query params)"]
A --> F["ElixFormsTypes.tsx<br/>(tipi opzioni)"]
B --> G["App.jsx + main.jsx<br/>(entry point pagina)"]
C --> H["App.jsx + main.jsx<br/>(entry point pagina)"]
```
## Differenze chiave React → PHP
| Aspetto | React | PHP |
|---|---|---|
| **Rendering** | Client-side (JSX → DOM) | Server-side (PHP → HTML) |
| **State management** | `this.state` + `this.setState()` | Valori da `$_GET` / `$_POST` (stateless) |
| **Form field factory** | Callback con oggetto factory | Metodi della classe base chiamati direttamente |
| **Build** | Vite → singolo bundle JS | Nessuna build — file PHP serviti direttamente |
| **CSS** | Iniettati nel JS via plugin | `<link>` tag nell'HTML |
| **Bootstrap Italia** | Classi HTML nel JSX | Stesse classi HTML nel template PHP |
> [!IMPORTANT]
> In PHP non serve un build tool. Ogni pagina è un singolo file PHP (o un set di file PHP) che include la libreria common e produce HTML direttamente.
## Proposed Changes
### 1. Libreria Common PHP
#### [NEW] [ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php)
Classe astratta PHP che replica `ElixFormsComponentAbstract.tsx`:
- **Proprietà**: `$properties` (array associativo equivalente a `IElixFormsComponentProperties`)
- **Metodi factory**: `createTextInput()`, `createTextAreaInput()`, `createNumberInput()`, `createBooleanInput()`, `createRadioInput()`, `createCheckboxInput()`, `createDropdownInput()`, `createHiddenInput()`
- **Metodi di layout**: `render()` che genera l'intera pagina HTML (header Bootstrap Italia, breadcrumb, card, form, footer)
- **Metodi estensibili**: `createCustomFormFields()`, `renderExtraContentPre()`, `renderExtraContentPost()` — da sovrascrivere nelle pagine figlie
- **Validazione**: Controllo parametri obbligatori (`mandatoryFormFieldNames`) dalla query string
- I valori dei campi vengono pre-popolati da `$_GET` (equivalente di `QueryParamHelper`)
#### [NEW] [ElixFormsElement.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsElement.php)
Classe wrapper per layout label+input (replica `ElixFormsElement.tsx`):
- Genera il layout a due colonne Bootstrap (`row`, `col-12 col-md-4`, `col-12 col-md-8`)
#### [NEW] [QueryParamHelper.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/QueryParamHelper.php)
Classe statica utility (replica `QueryParamHelper.tsx`):
- `getCheckedFromQuery($paramName)``int[]`
- `getOptionFromQuery($paramName)``?int`
- `getBooleanFromQuery($paramName)``?bool`
- `getDecodedTextFromQuery($paramName)``?string`
---
### 2. Pagina di Esempio
#### [NEW] [index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/scelta-carriera/index.php)
Pagina di esempio che replica `scelta-carriera` React:
- Estende `ElixFormsComponent` sovrascrivendo `createCustomFormFields()`
- Definisce dropdown, text input, textarea, boolean, radio, checkbox
- Include il CSS di Bootstrap Italia e del design system UniPR via `<link>`
---
### 3. Skill PHP (nuove)
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md)
Skill `elixforms-php-common-library`:
- Documenta la libreria PHP common, i metodi della classe base, le proprietà, e il pattern di estensione
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md)
Skill `elixforms-php-create-new-page`:
- Guida step-by-step per creare una nuova pagina PHP custom
#### [NEW] [SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md)
Skill `elixforms-php-project-architecture`:
- Descrive la struttura della cartella `php/` e le convenzioni del monorepo PHP
---
### 4. Agenti (AGENTS.md personalizzati)
> [!IMPORTANT]
> Gli "agenti" nella struttura Gemini vengono realizzati creando file `AGENTS.md` nelle rispettive cartelle. In questo modo ogni agente "scopre" automaticamente le regole specifiche per il proprio contesto.
#### [NEW] [AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/AGENTS.md)
Agente **"React"** — regole specifiche per il framework React:
- Usa React + TypeScript class components per la libreria common
- Usa Vite per il bundling single-file
- Bootstrap Italia con classi HTML nel JSX
- Regola sugli ID dei form fields (ripresa dal root)
- Riferimenti alle skill React esistenti
#### [NEW] [AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/AGENTS.md)
Agente **"PHP"** — regole specifiche per PHP:
- PHP puro, nessun framework
- Bootstrap Italia con classi HTML nei template PHP
- Pattern ereditarietà con classe base
- Stessa regola sugli ID dei form fields
- Riferimenti alle skill PHP
---
## Struttura finale del workspace
```
elixforms-custom-pages/
├── .agents/
│ ├── AGENTS.md ← Regole globali (invariate)
│ └── skills/
│ ├── common-library/ ← Skill React (esistente)
│ ├── create-new-page/ ← Skill React (esistente)
│ ├── css-design-system/ ← Skill condivisa (esistente)
│ ├── custom-workflow-logic/ ← Skill condivisa (esistente)
│ ├── dev-workflow/ ← Skill React (esistente)
│ ├── project-architecture/ ← Skill React (esistente)
│ ├── typescript-react-conventions/ ← Skill React (esistente)
│ ├── vite-build-config/ ← Skill React (esistente)
│ ├── php-common-library/ ← [NEW] Skill PHP
│ ├── php-create-new-page/ ← [NEW] Skill PHP
│ └── php-project-architecture/ ← [NEW] Skill PHP
├── react/
│ ├── AGENTS.md ← [NEW] Agente React
│ ├── common/
│ ├── scelta-carriera/
│ └── recupero-proposta-cct-da-contratti/
└── php/
├── AGENTS.md ← [NEW] Agente PHP
├── common/
│ ├── ElixFormsComponent.php ← [NEW]
│ ├── ElixFormsElement.php ← [NEW]
│ └── QueryParamHelper.php ← [NEW]
└── scelta-carriera/
└── index.php ← [NEW]
```
## Open Questions
> [!IMPORTANT]
> **Versione PHP**: quale versione minima di PHP devo usare? (es. PHP 7.4+ o PHP 8.0+ con typed properties, union types, match expression, ecc.)
> [!IMPORTANT]
> **Namespace PHP**: vuoi usare i namespace PSR-4 (es. `namespace ElixForms\Common;`) o preferisci un approccio più semplice con `require_once`?
> [!IMPORTANT]
> **Deploy**: le pagine PHP verranno servite da un web server Apache/Nginx? O dovranno essere incluse/embedded in un sistema esistente?
## Verification Plan
### Manual Verification
- Verificare che la pagina PHP `scelta-carriera/index.php` generi HTML valido con la stessa struttura del corrispettivo React
- Verificare che i campi del form (dropdown, text, textarea, boolean, radio, checkbox) siano tutti presenti e funzionanti
- Verificare che i parametri obbligatori vengano correttamente letti dalla query string
- Verificare che il form faccia POST a `https://procedure.unipr.it/rwe2/ComeBackToElixAndSave` con encoding `ISO-8859-1`
- Verificare che le skill e gli agenti siano correttamente scoperti da Gemini
@@ -0,0 +1,57 @@
# Porting "recupero-proposta-cct-da-contratti" in PHP
## Obiettivo
Convertire la pagina custom `recupero-proposta-cct-da-contratti` in PHP, mantenendo la stessa logica di funzionamento della versione React/TypeScript.
## Architettura e Sicurezza
La pagina originale React esegue una chiamata client-side fetch ad un web service esterno (`http://localhost:8000/contratti/cerca`) utilizzando credenziali Basic Auth ed una API Key custom. Poiché in PHP eseguiamo il rendering server-side, esporre le credenziali nel codice JavaScript inviato al browser è una vulnerabilità di sicurezza.
### Soluzione Proposta: PHP Proxy Script
Proponiamo la creazione di un file `search-contratti.php` locale alla pagina che:
1. Riceve in GET il parametro di ricerca `term` ed il codice fiscale dell'utente `cod_fis`.
2. Esegue una chiamata cURL server-to-server verso il WS reale usando le credenziali memorizzate in sicurezza in `config.json`.
3. Restituisce il JSON dei contratti trovati al client.
Il componente client-side eseguirà la fetch locale verso questo file `search-contratti.php` ed implementerà un dropdown di autocompletamento in JavaScript nativo (Vanilla JS) per mantenere le stesse animazioni ed interazioni della versione React (debounce, navigazione con frecce tastiera, selezione ed autocompilazione dei campi nascosti del form).
```
[Browser Client]
│ (fetch /term=...)
[search-contratti.php] (Proxy locale in php/)
│ (cURL server-to-server + Basic Auth & API Key)
[Web Service Contratti]
```
## Proposed Changes
### Componente: `php/recupero-proposta-cct-da-contratti/`
#### [NEW] [config.json](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/config.json)
Copia del file di configurazione con le credenziali API, l'endpoint del Web Service ed i mapping dei campi elixForms.
#### [NEW] [search-contratti.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/search-contratti.php)
Script PHP proxy che:
- Valida l'input.
- Effettua la richiesta HTTP GET al WS esterno tramite `curl`.
- Passa l'header `Authorization: Basic ...` e `X-Api-Key: ...`.
- Ritorna `application/json` con i risultati.
#### [NEW] [index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/recupero-proposta-cct-da-contratti/index.php)
Pagina custom principale:
- Eredita da `ElixFormsComponent`.
- Legge il Codice Fiscale dell'utente tramite `QueryParamHelper::getDecodedTextFromQuery`.
- Genera i campi nascosti per i dati di output (`idDomanda`, `idRicevuta`, `codiceContratto`, `titoloContratto`).
- Rendering dell'input di ricerca ed implementazione del JavaScript nativo per l'autocompletamento (sincronizzato con il file proxy `search-contratti.php`).
## Verification Plan
### Manual Verification
- Avviare il server PHP locale: `php -S localhost:8080` nella cartella `php/recupero-proposta-cct-da-contratti/`.
- Verificare che il proxy `search-contratti.php?term=test&cod_fis=...` risponda correttamente con i dati mock o reali del WS.
- Verificare che digitando nel campo di ricerca "Cerca Contratto" vengano visualizzati i risultati nel menu a discesa.
- Testare la navigazione del menu a discesa con la tastiera (Frecce Su/Giù, Enter, Esc).
- Verificare che alla selezione di un contratto, i campi hidden vengano correttamente compilati con i valori associati (`idDomanda`, `idRicevuta`, ecc.) e venga mostrata la card riepilogativa del contratto selezionato.
+44
View File
@@ -0,0 +1,44 @@
# Walkthrough — Port PHP + Agenti Custom
Ho completato tutte le attività pianificate per la realizzazione della struttura in PHP delle custom pages elixForms e per la configurazione dei due agenti dedicati ("React" e "PHP").
## Modifiche e Creazioni Effettuate
### 1. Libreria Comune PHP (`php/common/`)
Abbiamo replicato la stessa logica e architettura della libreria comune React in PHP 8.0+:
- **[QueryParamHelper.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/QueryParamHelper.php)**: Fornisce metodi statici tipizzati per estrarre e decodificare i dati provenienti dalla query string (`$_GET`).
- **[ElixFormsElement.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsElement.php)**: Gestisce il layout standard a due colonne (label a sinistra, input a destra) con le classi Bootstrap.
- **[ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php)**: Classe astratta base che definisce il ciclo di rendering dell'intera pagina e tutti i metodi factory dei campi (testo, textarea, checkbox, radio, dropdown, boolean). Gestisce automaticamente la presenza dei parametri obbligatori elixForms ed inserisce il JavaScript client-side per la sincronizzazione dei checkbox.
### 2. Pagina di Esempio PHP
- **[index.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/scelta-carriera/index.php)**: Una pagina "Scelta Carriera" d'esempio che estende `ElixFormsComponent` e definisce tutti i campi form (dropdown, text, textarea, boolean, radio, checkbox), replicando fedelmente la controparte React.
### 3. Nuove Skill PHP
Abbiamo creato tre nuove skill all'interno di `.agents/skills/` per guidare lo sviluppo PHP futuro:
- **[php-common-library/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md)**: Dettaglia i metodi factory, il layout ed i parametri del componente base.
- **[php-create-new-page/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md)**: Guida passo-passo per lo scaffolding e testing locale di nuove pagine.
- **[php-project-architecture/SKILL.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md)**: Definisce le differenze architetturali rispetto al mondo React e le modalità di deploy su Apache.
### 4. Configurazione degli Agenti Dedicati
Abbiamo strutturato due file di istruzioni separati che istruiscono l'IDE sul comportamento da adottare a seconda che si stia lavorando in React o in PHP:
- **[react/AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/AGENTS.md)** (Agente React): Focalizzato su Vite, bundling single-file, TypeScript (`import type`), e React Class/Functional Components.
- **[php/AGENTS.md](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/AGENTS.md)** (Agente PHP): Focalizzato su PHP 8.0+ puro, PSR-4 namespaces, server locale integrato, e deploy diretto su Apache.
---
## Istruzioni per il Test Locale PHP
1. Navigare nella cartella della pagina di esempio:
```bash
cd php/scelta-carriera
```
2. Avviare il server built-in di PHP:
```bash
php -S localhost:8080
```
3. Visitare l'URL inserendo dei parametri fittizi di prova per bypassare il controllo di autenticazione:
```
http://localhost:8080/?RWE2_MODULE_ID=123&RWE2_REQUEST_ID=456&custom-workflow-back-url=http://example.com&custom-workflow-generic-id=1&custom-workflow-current-tabrel-genid=2&custom-workflow-source-field=src&crc=abc&MODULE_TESTMODE_KEY=key&ELANG=it&COL0002=ValoreTest
```
4. Il form caricherà la pagina con il tema e layout corretti di Bootstrap Italia e i campi pre-popolati.
+8
View File
@@ -0,0 +1,8 @@
FROM php:8.5.8-cli
RUN pecl install xdebug && docker-php-ext-enable xdebug
WORKDIR /app
EXPOSE 8000
CMD ["php", "-S", "0.0.0.0:8000"]
+10
View File
@@ -0,0 +1,10 @@
#zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20250925/xdebug.so
[xdebug]
xdebug.mode=develop,debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9023
xdebug.log=/tmp/xdebug.log
xdebug.log_level=7
xdebug.idekey=VSCODE
+5
View File
@@ -9,6 +9,7 @@ lerna-debug.log*
# Dependency directories
node_modules
vendor
# Build generated files
dist
@@ -27,6 +28,7 @@ temp
# Coverage directory used by tools like istanbul
coverage
.phpunit.result.cache
# OSX
.DS_Store
@@ -52,3 +54,6 @@ obj
*.njsproj
*.sln
*.sw?
# Configuration files
**/config.json
+61 -22
View File
@@ -5,11 +5,10 @@
"version": "0.2.0",
"configurations": [
{
"name": "Scelta Carriere (BROWSER)",
"cwd": "${workspaceFolder}/react/scelta-carriera",
"name": "Node - Recupero Proposta CCT da contratti",
"cwd": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti",
"env": {
"NO_COLOR": "1"
"NO_COLOR": "true"
},
"outputCapture": "console",
"type": "node",
@@ -17,33 +16,73 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
],
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "Local:\\s+http://localhost:([0-9]+)/",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/react/recupero-proposta-cct-da-contratti/src"
}
},
{
"name": "Node - Scelta Carriere",
"cwd": "${workspaceFolder}/react/scelta-carriera",
"env": {
"NO_COLOR": "true"
},
"outputCapture": "console",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
],
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "Local:\\s+http://localhost:([0-9]+)/",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/react/scelta-carriera/src"
},
}
},
{
"name": "Scelta Carriere (DEBUG ONLY)",
"cwd": "${workspaceFolder}/react/scelta-carriera",
"env": {
"NO_COLOR": "1"
},
"outputCapture": "console",
"type": "node",
"name": "WWW - Listen for Xdebug",
"type": "php",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**",
"**/@vite/client/**"
]
"hostname": "0.0.0.0",
"port": 9023,
"pathMappings": {
"/app": "${workspaceFolder}/php"
},
"log": true
},
{
"name": "WWW - Launch Built-in web server",
"type": "php",
"request": "launch",
"runtimeArgs": [
"-dxdebug.mode=debug",
"-dxdebug.start_with_request=yes",
"-S",
"localhost:0"
],
"program": "",
"cwd": "${workspaceRoot}",
"port": 9023,
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally"
},
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
}
]
}
+10
View File
@@ -0,0 +1,10 @@
@echo off
SETLOCAL EnableDelayedExpansion
echo %cmdcmdline% | findstr /i /c:"%~nx0" >NUL && set iscommandline=1
echo %PSModulePath% | findstr /i /c:"%USERPROFILE%" >NUL && set ispowershell=1
cd /D "%~dp0"
@pwsh -NoProfile -ExecutionPolicy Bypass -Command .\scripts\composer-dump-autoload.ps1
IF DEFINED iscommandline IF NOT DEFINED ispowershell pause
ENDLOCAL
+10
View File
@@ -0,0 +1,10 @@
@echo off
SETLOCAL EnableDelayedExpansion
echo %cmdcmdline% | findstr /i /c:"%~nx0" >NUL && set iscommandline=1
echo %PSModulePath% | findstr /i /c:"%USERPROFILE%" >NUL && set ispowershell=1
cd /D "%~dp0"
@pwsh -NoProfile -ExecutionPolicy Bypass -Command .\scripts\composer-install.ps1
IF DEFINED iscommandline IF NOT DEFINED ispowershell pause
ENDLOCAL
+25
View File
@@ -0,0 +1,25 @@
{
"name": "elixforms-web-pages/pages",
"description": "Custom web pages PHP per elixForms",
"license": "proprietary",
"require": {
"php": ">=8.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"ElixForms\\Common\\": "php/common/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit tests"
}
}
Generated
+2491
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
name: elixforms-www
services:
elixforms-www:
build:
context: .
dockerfile: .docker/php/Dockerfile
container_name: elixforms-www
working_dir: /app/php
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "8080:8000"
volumes:
- .:/app
- ./.docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
#environment:
# XDEBUG_MODE: debug
# XDEBUG_CONFIG: client_host=host.docker.internal client_port=9003 start_with_request=yes
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
# Mock server
## JSON-Server
See: <https://github.com/typicode/json-server>
To start server:
```pwsh
npx json-server db.json
```
+52
View File
@@ -0,0 +1,52 @@
{
"$schema": [
"./node_modules/json-server/schema.json"
],
"posts": [
{
"id": "1",
"title": "a title",
"views": 100
},
{
"id": "2",
"title": "another title",
"views": 200
}
],
"comments": [
{
"id": "1",
"text": "a comment about post 1",
"postId": "1"
},
{
"id": "2",
"text": "another comment about post 1",
"postId": "1"
}
],
"contratti": [
{
"idContratto": "ID_CONTRATTO_1",
"titoloContratto": "Titolo del contratto #1",
"idDomanda": "12345",
"idRicevuta": "ABCDE"
},
{
"idContratto": "ID_CONTRATTO_2",
"titoloContratto": "Titolo del secondo contratto",
"idDomanda": "67890",
"idRicevuta": "FGHIJ"
},
{
"idContratto": "ID_CONTRATTO_3",
"titoloContratto": "Terzo titolo",
"idDomanda": "13579",
"idRicevuta": "KLMNO"
}
],
"profile": {
"name": "typicode"
}
}
+532
View File
@@ -0,0 +1,532 @@
{
"name": "mock-server",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"json-server": "^1.0.0-beta.15"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"license": "MIT"
},
"node_modules/@tinyhttp/accepts": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.3.0.tgz",
"integrity": "sha512-hdKkMGAUqnagpWO1R8rVBYqbu4sWQ2Fo682gkJmO0nl54DPvnzxx81b2WZtV3VwB7EdLfUoasj2BAkyTcyZ5aw==",
"license": "MIT",
"dependencies": {
"mime": "4.1.0"
},
"engines": {
"node": ">=14.13.1"
},
"funding": {
"type": "individual",
"url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
}
},
"node_modules/@tinyhttp/app": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@tinyhttp/app/-/app-3.0.7.tgz",
"integrity": "sha512-btit/gSWisksJ19crNLct1mwZvX+/AYwh/H1x8SB/VGDmNFkSsdRDZaa54wW1Eq3bM7mCIr7+l7Oh3tAi/C+Bw==",
"license": "MIT",
"dependencies": {
"@tinyhttp/accepts": "^2.3.0",
"@tinyhttp/cookie": "2.1.1",
"@tinyhttp/proxy-addr": "3.0.1",
"@tinyhttp/req": "2.2.8",
"@tinyhttp/res": "2.2.11",
"@tinyhttp/router": "2.2.5",
"regexparam": "^2.0.2"
},
"engines": {
"node": ">=16.10.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
}
},
"node_modules/@tinyhttp/content-disposition": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz",
"integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==",
"license": "MIT",
"engines": {
"node": ">=12.17.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
}
},
"node_modules/@tinyhttp/content-type": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@tinyhttp/content-type/-/content-type-0.1.4.tgz",
"integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ==",
"license": "MIT",
"engines": {
"node": ">=12.4"
}
},
"node_modules/@tinyhttp/cookie": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/cookie/-/cookie-2.1.1.tgz",
"integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
}
},
"node_modules/@tinyhttp/cookie-signature": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz",
"integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/@tinyhttp/cors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/cors/-/cors-2.0.1.tgz",
"integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==",
"license": "MIT",
"dependencies": {
"@tinyhttp/vary": "^0.1.3"
},
"engines": {
"node": ">=12.20 || 14.x || >=16"
}
},
"node_modules/@tinyhttp/encode-url": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz",
"integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/@tinyhttp/etag": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@tinyhttp/etag/-/etag-2.1.2.tgz",
"integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/@tinyhttp/forwarded": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@tinyhttp/forwarded/-/forwarded-2.1.2.tgz",
"integrity": "sha512-9H/eulJ68ElY/+zYpTpNhZ7vxGV+cnwaR6+oQSm7bVgZMyuQfgROW/qvZuhmgDTIxnGMXst+Ba4ij6w6Krcs3w==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/@tinyhttp/logger": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tinyhttp/logger/-/logger-2.1.0.tgz",
"integrity": "sha512-Ma1fJ9CwUbn9r61/4HW6+nflsVoslpOnCrfQ6UeZq7GGIgwLzofms3HoSVG7M+AyRMJpxlfcDdbH5oFVroDMKA==",
"license": "MIT",
"dependencies": {
"colorette": "^2.0.20",
"dayjs": "^1.11.13",
"http-status-emojis": "^2.2.0"
},
"engines": {
"node": ">=14.18 || >=16.20"
}
},
"node_modules/@tinyhttp/proxy-addr": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/proxy-addr/-/proxy-addr-3.0.1.tgz",
"integrity": "sha512-vP0JVsy9ZMIldsaP/QHdMF+sb3B6wn7e2QXRdqpX/Cqz1ie35Am29DK88DeVmiwdTQle3FtYaVNtU3RgTGYZ+w==",
"license": "MIT",
"dependencies": {
"@tinyhttp/forwarded": "2.1.2"
},
"engines": {
"node": ">=16.10.0"
}
},
"node_modules/@tinyhttp/req": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@tinyhttp/req/-/req-2.2.8.tgz",
"integrity": "sha512-HCsceFNgMpssUsnRao16iJyyfdWRwKlhL7OMTPUEjZsZGREnBzpjlrPHA31G5xNNzR7XOWVXDXGyrGgSpcwGSA==",
"license": "MIT",
"dependencies": {
"@tinyhttp/accepts": "2.3.0",
"@tinyhttp/type-is": "2.2.5",
"@tinyhttp/url": "2.1.1",
"header-range-parser": "^1.1.3"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@tinyhttp/res": {
"version": "2.2.11",
"resolved": "https://registry.npmjs.org/@tinyhttp/res/-/res-2.2.11.tgz",
"integrity": "sha512-t7GJzjqpG2svJ11RYvqaYU+xTV9MsEr0usbTIAWa5B8d7IIzpMC6AlT2K6wCIu1XGUYRpZY7qMM5cMgluvsfeg==",
"license": "MIT",
"dependencies": {
"@tinyhttp/content-disposition": "2.2.4",
"@tinyhttp/cookie": "2.1.1",
"@tinyhttp/cookie-signature": "2.1.1",
"@tinyhttp/encode-url": "2.1.1",
"@tinyhttp/req": "2.2.8",
"@tinyhttp/send": "2.2.5",
"@tinyhttp/vary": "^0.1.3",
"mime": "4.1.0"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@tinyhttp/router": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tinyhttp/router/-/router-2.2.5.tgz",
"integrity": "sha512-HI9Mpo9+IVpCzx/36okjJvtvifBSh3Ufhl9n1vylAbNLEykceJiMBkr06+W0qqRlo8TiZeUtg2XinEJu+GFcRA==",
"license": "MIT",
"dependencies": {
"regexparam": "^2.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tinyhttp/send": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tinyhttp/send/-/send-2.2.5.tgz",
"integrity": "sha512-XhBwziPOCydOJzb9rVw0xuKX6HmMA0gXKHHqec7V97jU6JtSCCIvwW2FBMh/XrG9S5W8DRETroPohASrIwf7Uw==",
"license": "MIT",
"dependencies": {
"@tinyhttp/content-type": "^0.1.4",
"@tinyhttp/etag": "2.1.2",
"mime": "4.1.0"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@tinyhttp/type-is": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tinyhttp/type-is/-/type-is-2.2.5.tgz",
"integrity": "sha512-BCPEB+NV8v/9lzEE9GbfRPAKVsyayp84m6SSWn70j8yFkPBXeuVeq004pwVrjW1CRdmAZz9ZSH147pqqzAdr5g==",
"license": "MIT",
"dependencies": {
"@tinyhttp/content-type": "^0.1.4",
"mime": "4.1.0"
},
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/@tinyhttp/url": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@tinyhttp/url/-/url-2.1.1.tgz",
"integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/@tinyhttp/vary": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@tinyhttp/vary/-/vary-0.1.3.tgz",
"integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.21",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
"license": "MIT"
},
"node_modules/dot-prop": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz",
"integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==",
"license": "MIT",
"dependencies": {
"type-fest": "^5.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eta": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz",
"integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/bgub/eta?sponsor=1"
}
},
"node_modules/header-range-parser": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.5.tgz",
"integrity": "sha512-n5JOx67HBL0MGqtu6NFoEYWb+xDYAOgBI5dBkyMDff1xHbhGnjCMglj1aiMNPHps6HwXO+2i5jbPU/zJSk7etQ==",
"license": "MIT",
"engines": {
"node": ">=12.22.0"
}
},
"node_modules/http-status-emojis": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz",
"integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg==",
"license": "MIT"
},
"node_modules/inflection": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz",
"integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/json-server": {
"version": "1.0.0-beta.15",
"resolved": "https://registry.npmjs.org/json-server/-/json-server-1.0.0-beta.15.tgz",
"integrity": "sha512-I5UB/OWHLGoQW9IVld2yzZFYYiGYNBn2OYRlGbkfj5xDidT26yKYkV7Sr093zJtPh9zbofaLgyT89Ov5U0RRBQ==",
"license": "MIT",
"dependencies": {
"@tinyhttp/app": "^3.0.1",
"@tinyhttp/cors": "^2.0.1",
"@tinyhttp/logger": "^2.1.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
"dot-prop": "^10.1.0",
"eta": "^4.5.0",
"inflection": "^3.0.2",
"json5": "^2.2.3",
"lowdb": "^7.0.1",
"milliparsec": "^5.1.0",
"sirv": "^3.0.2",
"sort-on": "^7.0.0"
},
"bin": {
"json-server": "lib/bin.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/lowdb": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
"integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
"license": "MIT",
"dependencies": {
"steno": "^4.0.2"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/milliparsec": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-5.1.1.tgz",
"integrity": "sha512-jkEDaSWZp4/Q3vprqdqukBqUEyNNqC1pwTjZ5cp9YkaR1wv5fvTCd8VFsecbw7i8DNBGjzhJ83MDoPZlcTaPQg==",
"license": "MIT",
"engines": {
"node": ">=18.13 || >=19.20 || >=20"
}
},
"node_modules/mime": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
"funding": [
"https://github.com/sponsors/broofa"
],
"license": "MIT",
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexparam": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
"integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/sort-on": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/sort-on/-/sort-on-7.0.0.tgz",
"integrity": "sha512-e+4RRxt7jsWdGPp4H5PKOER/ELYlemNB1plvW686Qi3j4WVaCjCpro2zaTD7Cn0VtBImq/hg3x1JfovMNXXfJQ==",
"license": "MIT",
"dependencies": {
"dot-prop": "^10.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/steno": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
"integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/type-fest": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz",
"integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"json-server": "^1.0.0-beta.15"
}
}
+43
View File
@@ -0,0 +1,43 @@
# Agente PHP — Regole e Convenzioni
Questo file definisce le istruzioni specifiche, le regole e le convenzioni per lo sviluppo e la manutenzione delle pagine custom e dei componenti scritti in **PHP 8.0+** nella cartella `php/`.
## Gestione ID dei Form Element
Gli ID dei campi del form (es. nel Dropdown, Checkbox, TextField) creati tramite i metodi della classe [ElixFormsComponent.php](file:///c:/__Git/_UniPR/elixforms-custom-pages/php/common/ElixFormsComponent.php) **non devono mai essere cambiati o manipolati** (ad es. aggiungendo suffissi come `_dropdown`), in quanto la piattaforma elixForms si basa sul loro nome ed ID esatto.
In caso di conflitti di ID (ad esempio con input `hidden`), applicare il suffisso `_hidden` all'input hidden e mantenere l'ID originale senza suffissi sull'elemento principale visibile.
## Convenzioni di Codice e Framework
1. **PHP Puro (No Framework)**:
- Utilizzare PHP 8.0+ con tipizzazione forte (`declare(strict_types=1);`).
- Sfruttare le feature moderne come `match` expression, property promotion e typed properties.
- Le pagine custom ereditano da `ElixFormsComponent` e sovrascrivono `createCustomFormFields()`.
2. **Namespace PSR-4**:
- Tutte le classi nella libreria comune usano il namespace `ElixForms\Common;`.
- L'autoload PSR-4 è gestito da Composer tramite `vendor/autoload.php`.
- Gli entry point includono `require_once __DIR__ . '/../../vendor/autoload.php';`; le classi comuni non devono includersi manualmente tra loro.
3. **Rendering Server-Side**:
- I file PHP generano direttamente l'HTML server-side usando il design system Bootstrap Italia di UniPR.
- Non c'è alcun passaggio di build o bundling JS.
4. **Checkbox e Stato**:
- I checkbox inviano il proprio stato serializzato come stringa separata da virgole tramite un input hidden (gestito client-side da uno script JS automatico).
## Deploy e Server Locale
- **Dipendenze**: dalla root del repository eseguire `composer install --optimize-autoloader` oppure `composer-install.bat`.
- **Deploy**: Eseguire `composer install --no-dev --optimize-autoloader` e caricare sul server sia `php/` sia `vendor/`, mantenendole allo stesso livello.
- **Server locale**:
- **PHP CLI**: Avviare il server dalla cartella `php/` tramite `php -S localhost:8080` (le pagine saranno raggiungibili su `http://localhost:8080/<nome-pagina>/`).
- **Docker**: Dalla root del repository, eseguire:
```bash
docker run --rm -p 8080:8080 -v "${PWD}:/app" -w /app/php php:8.2-cli php -S 0.0.0.0:8080
```
## Skill PHP Correlate
Fai riferimento alle seguenti skill per maggiori dettagli operativi:
- [elixforms-php-common-library](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-common-library/SKILL.md): Classi, metodi factory e helper.
- [elixforms-php-create-new-page](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-create-new-page/SKILL.md): Guida passo passo per la creazione di nuove pagine.
- [elixforms-php-project-architecture](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/php-project-architecture/SKILL.md): Struttura delle cartelle, convenzioni e deploy.
- [elixforms-css-design-system](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/css-design-system/SKILL.md): Linee guida del design system UniPR.
- [elixforms-custom-workflow-logic](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/custom-workflow-logic/SKILL.md): Query parameters ed integrazione elixForms.
+607
View File
@@ -0,0 +1,607 @@
<?php
declare(strict_types=1);
namespace ElixForms\Common;
/**
* Classe astratta base per le custom page PHP di elixForms.
* Equivalente PHP di ElixFormsComponentAbstract.tsx nel progetto React.
*
* Ogni pagina custom deve:
* 1. Estendere questa classe
* 2. Sovrascrivere createCustomFormFields() per definire i campi del form
* 3. Opzionalmente sovrascrivere renderExtraContentPre() / renderExtraContentPost()
* 4. Chiamare render() per generare l'HTML completo della pagina
*
* Uso tipico:
* ```php
* $page = new MiaPaginaCustom([
* 'moduleName' => 'Nome Modulo',
* 'cardTitle' => 'Titolo Card',
* // ...altre proprietà...
* ]);
* echo $page->render();
* ```
*/
abstract class ElixFormsComponent
{
/**
* Proprietà della pagina (equivalente a IElixFormsComponentProperties).
*
* Chiavi supportate:
* - isDarkTheme: bool (opzionale)
* - userDisplayName: string (opzionale)
* - additionalFieldsJson: string (opzionale, JSON schema per campi dinamici)
* - moduleName: string (obbligatorio)
* - headerTitle: string (opzionale)
* - headerHeroImageSrc: string (opzionale)
* - cardTitle: string (obbligatorio)
* - cardDescription: string (opzionale)
* - alertInfoTitle: string (opzionale)
* - alertInfoMessage: string (opzionale)
* - instructionsTitle: string (opzionale)
* - instructionsMessage: string (opzionale)
* - submitText: string (opzionale, default 'INVIA')
*
* @var array<string, mixed>
*/
protected array $properties;
/**
* Nomi dei parametri obbligatori che elixForms passa in query string.
* Se mancano, la pagina mostra un errore di autenticazione.
*
* @var string[]
*/
private array $mandatoryFormFieldNames = [
'RWE2_MODULE_ID',
'RWE2_REQUEST_ID',
'custom-workflow-back-url',
'custom-workflow-generic-id',
'custom-workflow-current-tabrel-genid',
'custom-workflow-source-field',
'crc',
'MODULE_TESTMODE_KEY',
'ELANG',
];
/**
* @param array<string, mixed> $properties Proprietà della pagina
*/
public function __construct(array $properties)
{
$this->properties = $properties;
}
// =========================================================================
// Metodi estensibili (da sovrascrivere nelle pagine figlie)
// =========================================================================
/**
* Genera i campi custom del form.
* Da sovrascrivere nelle pagine figlie per definire i campi specifici.
*
* @return string HTML dei campi custom
*/
protected function createCustomFormFields(): string
{
return '';
}
/**
* Contenuto extra da renderizzare PRIMA del form.
* Sovrascrivere per aggiungere contenuto personalizzato.
*
* @return string HTML del contenuto extra
*/
protected function renderExtraContentPre(): string
{
return '';
}
/**
* Contenuto extra da renderizzare DOPO il form.
* Sovrascrivere per aggiungere contenuto personalizzato.
*
* @return string HTML del contenuto extra
*/
protected function renderExtraContentPost(): string
{
return '';
}
// =========================================================================
// Metodi factory per campi form
// =========================================================================
/**
* Crea un campo di testo (input type="text").
*
* @param string $paramName Nome/ID del campo (corrisponde alla colonna elixForms)
* @param string $label Etichetta del campo
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createTextInput(string $paramName, string $label, bool $required = false): string
{
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
$requiredAttr = $required ? ' required' : '';
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
$inputHtml = '<input id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" type="text" value="' . $currentValue . '"' . $requiredAttr . ' />';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo textarea.
*
* @param string $paramName Nome/ID del campo
* @param string $label Etichetta del campo
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createTextAreaInput(string $paramName, string $label, bool $required = false): string
{
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
$requiredAttr = $required ? ' required' : '';
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
$inputHtml = '<textarea id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" rows="4"' . $requiredAttr . '>' . $currentValue . '</textarea>';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo numerico (input type="number").
*
* @param string $paramName Nome/ID del campo
* @param string $label Etichetta del campo
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createNumberInput(string $paramName, string $label, bool $required = false): string
{
$currentValue = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
$requiredAttr = $required ? ' required' : '';
$labelHtml = '<label class="form-label fw-semibold" for="' . htmlspecialchars($paramName) . '">' . htmlspecialchars($label) . '</label>';
$inputHtml = '<input id="' . htmlspecialchars($paramName) . '" name="' . htmlspecialchars($paramName) . '" class="form-control" type="number" value="' . $currentValue . '"' . $requiredAttr . ' />';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo booleano (radio Sì/No).
*
* @param string $paramName Nome/ID del campo
* @param string $label Etichetta del campo
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createBooleanInput(string $paramName, string $label, bool $required = false): string
{
$currentValue = QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '';
$requiredAttr = $required ? ' required' : '';
$escapedName = htmlspecialchars($paramName);
$escapedLabel = htmlspecialchars($label);
$labelHtml = '<span class="form-label fw-semibold">' . $escapedLabel . '</span>';
$options = [
['value' => 'true', 'label' => 'Sì'],
['value' => 'false', 'label' => 'No'],
];
$inputHtml = '<div class="d-flex flex-column gap-2" role="radiogroup" aria-label="' . $escapedLabel . '">';
foreach ($options as $option) {
$id = $escapedName . '_' . $option['value'];
$checked = $currentValue === $option['value'] ? ' checked' : '';
$inputHtml .= <<<HTML
<div class="form-check">
<input id="{$id}" class="form-check-input" type="radio" name="{$escapedName}" value="{$option['value']}"{$checked}{$requiredAttr} />
<label class="form-check-label" for="{$id}">{$option['label']}</label>
</div>
HTML;
}
$inputHtml .= '</div>';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo radio con opzioni personalizzate.
*
* @param string $paramName Nome/ID del campo
* @param string $label Etichetta del campo
* @param array<array{value: int, label: string}> $options Opzioni radio
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createRadioInput(string $paramName, string $label, array $options, bool $required = false): string
{
$currentValue = (string)(QueryParamHelper::getOptionFromQuery($paramName) ?? '');
$requiredAttr = $required ? ' required' : '';
$escapedName = htmlspecialchars($paramName);
$escapedLabel = htmlspecialchars($label);
$labelHtml = '<span class="form-label fw-semibold">' . $escapedLabel . '</span>';
$inputHtml = '<div class="d-flex flex-column gap-2" role="radiogroup" aria-label="' . $escapedLabel . '">';
foreach ($options as $option) {
$id = $escapedName . '_' . $option['value'];
$checked = $currentValue === (string)$option['value'] ? ' checked' : '';
$optionLabel = htmlspecialchars($option['label']);
$inputHtml .= <<<HTML
<div class="form-check">
<input id="{$id}" class="form-check-input" type="radio" name="{$escapedName}" value="{$option['value']}"{$checked}{$requiredAttr} />
<label class="form-check-label" for="{$id}">{$optionLabel}</label>
</div>
HTML;
}
$inputHtml .= '</div>';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo checkbox con opzioni multiple.
* I valori selezionati vengono inviati come stringa separata da virgola in un input hidden.
*
* @param string $paramName Nome/ID del campo (usato per l'hidden input)
* @param string $label Etichetta del campo
* @param array<array{value: int, label: string, required?: bool}> $options Opzioni checkbox
* @return string HTML del campo
*/
protected function createCheckboxInput(string $paramName, string $label, array $options): string
{
$checkedValues = QueryParamHelper::getCheckedFromQuery($paramName);
$currentValue = implode(',', $checkedValues);
$escapedName = htmlspecialchars($paramName);
$escapedLabel = htmlspecialchars($label);
$labelHtml = '<label class="form-label fw-semibold" for="' . $escapedName . '">' . $escapedLabel . '</label>';
$inputHtml = '<div class="d-flex flex-column gap-2" role="group" aria-label="' . $escapedLabel . '">';
foreach ($options as $entryIndex => $entry) {
$id = $escapedName . '_' . $entryIndex;
$isChecked = in_array($entryIndex, $checkedValues) ? ' checked' : '';
$optionLabel = htmlspecialchars($entry['label']);
$inputHtml .= <<<HTML
<div class="form-check">
<input id="{$id}" class="form-check-input" type="checkbox" value="{$entryIndex}" data-param="{$escapedName}"{$isChecked} />
<label class="form-check-label" for="{$id}">{$optionLabel}</label>
</div>
HTML;
}
$inputHtml .= '</div>';
// Hidden input che contiene il valore aggregato (aggiornato via JS client-side)
$inputHtml .= '<input type="hidden" id="' . $escapedName . '" name="' . $escapedName . '" value="' . htmlspecialchars($currentValue) . '" />';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo dropdown (select).
*
* @param string $paramName Nome/ID del campo
* @param string $label Etichetta del campo
* @param array<array{value: int, label: string}> $options Opzioni dropdown
* @param bool $required Se il campo è obbligatorio
* @return string HTML del campo
*/
protected function createDropdownInput(string $paramName, string $label, array $options, bool $required = false): string
{
$queryValue = QueryParamHelper::getOptionFromQuery($paramName);
$currentValue = $queryValue !== null ? (string)$queryValue : '';
$requiredAttr = $required ? ' required' : '';
$escapedName = htmlspecialchars($paramName);
$escapedLabel = htmlspecialchars($label);
$labelHtml = '<label class="form-label fw-semibold" for="' . $escapedName . '">' . $escapedLabel . '</label>';
$inputHtml = '<select id="' . $escapedName . '" name="' . $escapedName . '" class="form-select"' . $requiredAttr . '>';
$inputHtml .= '<option value="">Seleziona...</option>';
foreach ($options as $option) {
$selected = $currentValue === (string)$option['value'] ? ' selected' : '';
$optionLabel = htmlspecialchars($option['label']);
$inputHtml .= '<option value="' . $option['value'] . '"' . $selected . '>' . $optionLabel . '</option>';
}
$inputHtml .= '</select>';
return (new ElixFormsElement($labelHtml, $inputHtml))->render();
}
/**
* Crea un campo nascosto (hidden input).
* L'ID dell'hidden ha il suffisso _hidden per evitare conflitti.
*
* @param string $paramName Nome del campo
* @return string HTML dell'input hidden
*/
protected function createHiddenInput(string $paramName): string
{
$value = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($paramName) ?? '', ENT_QUOTES, 'UTF-8');
$escapedName = htmlspecialchars($paramName);
return '<input type="hidden" id="' . $escapedName . '_hidden" name="' . $escapedName . '" value="' . $value . '" />';
}
// =========================================================================
// Metodi interni di rendering
// =========================================================================
/**
* Genera gli hidden input per tutti i parametri obbligatori elixForms.
*
* @return string HTML degli hidden input
*/
private function createMandatoryFormFields(): string
{
$html = '';
foreach ($this->mandatoryFormFieldNames as $paramName) {
$html .= $this->createHiddenInput($paramName);
}
return $html;
}
/**
* Genera i campi form da uno schema JSON (additionalFieldsJson).
*
* @return string HTML dei campi aggiuntivi
*/
private function createAdditionalFormFields(): string
{
$json = $this->properties['additionalFieldsJson'] ?? '[]';
$schema = json_decode($json, true);
if (!is_array($schema)) {
return '';
}
$html = '';
foreach ($schema as $field) {
$html .= $this->renderField($field);
}
return $html;
}
/**
* Renderizza un singolo campo form in base al tipo specificato nello schema.
*
* @param array<string, mixed> $field Definizione del campo
* @return string HTML del campo
*/
private function renderField(array $field): string
{
$key = $field['key'] ?? '';
$fieldLabel = $field['label'] ?? '';
$required = $field['required'] ?? false;
$fieldOptions = $field['options'] ?? [];
return match ($field['type'] ?? '') {
'text' => $this->createTextInput($key, $fieldLabel, $required),
'textarea' => $this->createTextAreaInput($key, $fieldLabel, $required),
'number' => $this->createNumberInput($key, $fieldLabel, $required),
'boolean' => $this->createBooleanInput($key, $fieldLabel, $required),
'radio' => $this->createRadioInput($key, $fieldLabel, $fieldOptions, $required),
'checkbox' => $this->createCheckboxInput($key, $fieldLabel, $fieldOptions),
'dropdown' => $this->createDropdownInput($key, $fieldLabel, $fieldOptions, $required),
default => '',
};
}
// =========================================================================
// Render principale
// =========================================================================
/**
* Genera l'HTML completo della pagina.
* Include: head con CSS, header Bootstrap Italia, breadcrumb, card con form, footer.
*
* @return string HTML completo della pagina
*/
public function render(): string
{
$userDisplayName = $this->properties['userDisplayName'] ?? '';
$moduleName = htmlspecialchars($this->properties['moduleName'] ?? '');
$headerTitle = $this->properties['headerTitle'] ?? '';
$headerHeroImageSrc = $this->properties['headerHeroImageSrc'] ?? '';
$cardTitle = $this->properties['cardTitle'] ?? '';
$cardDescription = $this->properties['cardDescription'] ?? '';
$alertInfoTitle = $this->properties['alertInfoTitle'] ?? '';
$alertInfoMessage = $this->properties['alertInfoMessage'] ?? '';
$instructionsTitle = $this->properties['instructionsTitle'] ?? '';
$instructionsMessage = $this->properties['instructionsMessage'] ?? '';
$submitText = $this->properties['submitText'] ?? 'INVIA';
// Verifica parametri obbligatori
$missingParams = array_filter($this->mandatoryFormFieldNames, fn($p) => !isset($_GET[$p]));
$hasAuthenticationError = count($missingParams) > 0;
// Contenuti del form
$mandatoryFields = $this->createMandatoryFormFields();
$additionalFields = $this->createAdditionalFormFields();
$customFields = $this->createCustomFormFields();
$extraContentPre = $this->renderExtraContentPre();
$extraContentPost = $this->renderExtraContentPost();
// Costruisci l'HTML della pagina
ob_start();
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($cardTitle) ?> — elixForms</title>
<meta name="description" content="<?= htmlspecialchars($cardDescription) ?>">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-italia@2.18.2/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css">
<link rel="stylesheet" href="https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css">
</head>
<body>
<div>
<header class="it-header-wrapper text-white">
<?php if ($userDisplayName): ?>
<div class="it-header-slim-wrapper">
<div class="container">
<div class="row">
<div class="col-12">
<div class="it-header-slim-wrapper-content">
<div class="nav-mobile"></div>
<div class="it-header-slim-right-zone">
<div class="it-access-top-wrapper">
<?= htmlspecialchars($userDisplayName) ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="it-header-center-wrapper">
<div class="container">
<div class="it-header-center-content-wrapper">
<div class="it-brand-wrapper">
<a href="https://www.unipr.it/" title="Torna alla homepage dell'Università di Parma">
<img class="icon" src="https://procedure.unipr.it/UploadImgs/12_marchio_UNIPR_neg_b1_squareLogo.png" alt="Università di Parma" />
<div class="it-brand-text">
<p class="h2 no_toc">Università di Parma</p>
</div>
</a>
</div>
<div class="it-right-zone"></div>
</div>
</div>
</div>
</header>
<div class="container my-2 mx-4">
<div class="cmp-breadcrumb" role="navigation">
<nav class="breadcrumb-container" aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">Procedure Online<span class="separator">&gt;</span></li>
<li class="breadcrumb-item active" aria-current="page"><?= $moduleName ?></li>
</ol>
</nav>
</div>
<?php if ($headerHeroImageSrc): ?>
<img src="<?= htmlspecialchars($headerHeroImageSrc) ?>" alt="Hero Image" class="hero-image img-fluid mb-3" />
<?php endif; ?>
<?php if ($headerTitle): ?>
<p class="h2"><?= htmlspecialchars($headerTitle) ?></p>
<?php endif; ?>
<div class="container">
<div class="card shadow-sm mb-4">
<div class="card-body">
<?php if ($cardTitle): ?>
<p class="h3 mb-3"><?= htmlspecialchars($cardTitle) ?></p>
<?php endif; ?>
<?php if ($cardDescription): ?>
<p class="lead text-secondary"><?= htmlspecialchars($cardDescription) ?></p>
<?php endif; ?>
<?php if ($alertInfoTitle || $alertInfoMessage): ?>
<div class="custom-alert custom-alert-warning" role="status">
<?php if ($alertInfoTitle): ?><div><b><?= htmlspecialchars($alertInfoTitle) ?></b></div><?php endif; ?>
<?php if ($alertInfoMessage): ?><div class="small"><?= htmlspecialchars($alertInfoMessage) ?></div><?php endif; ?>
</div>
<?php endif; ?>
<?php if ($hasAuthenticationError): ?>
<div class="alert alert-danger" role="alert">Accesso non autorizzato!</div>
<?php else: ?>
<?= $extraContentPre ?>
<?php if ($instructionsTitle || $instructionsMessage): ?>
<div class="custom-alert custom-alert-info" role="status">
<?php if ($instructionsTitle): ?><div><b><?= htmlspecialchars($instructionsTitle) ?></b></div><?php endif; ?>
<?php if ($instructionsMessage): ?><div class="small"><?= htmlspecialchars($instructionsMessage) ?></div><?php endif; ?>
</div>
<?php endif; ?>
<form action="https://procedure.unipr.it/rwe2/ComeBackToElixAndSave" method="post" accept-charset="ISO-8859-1" class="mt-3">
<?= $mandatoryFields ?>
<?= $additionalFields ?>
<?= $customFields ?>
<div class="mt-4 d-flex justify-content-end">
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($submitText) ?> <span class="it-arrow-right text-white ms-2"></span></button>
</div>
</form>
<?= $extraContentPost ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<footer class="it-footer text-white">
<div class="it-footer-small-prints clearfix">
<div class="container">
<section>
<div class="row clearfix">
<div class="col-sm-12">
<div class="footer-content text-center p-2">
<div class="subfooter-top">powered by <span class="highlight">elixForms</span></div>
</div>
</div>
</div>
</section>
</div>
</div>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<?= $this->renderCheckboxScript() ?>
</body>
</html>
<?php
return ob_get_clean();
}
/**
* Genera il JavaScript per la gestione client-side dei checkbox.
* Aggiorna l'hidden input con i valori selezionati separati da virgola.
*
* @return string Tag <script> con il codice JS
*/
private function renderCheckboxScript(): string
{
return <<<'SCRIPT'
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('input[type="checkbox"][data-param]').forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
var paramName = this.getAttribute('data-param');
var hiddenInput = document.getElementById(paramName);
if (!hiddenInput) return;
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-param="' + paramName + '"]');
var values = [];
checkboxes.forEach(function(cb) {
if (cb.checked) {
values.push(cb.value);
}
});
hiddenInput.value = values.join(',');
});
});
});
</script>
SCRIPT;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace ElixForms\Common;
/**
* Wrapper per layout label + input in un layout a due colonne Bootstrap.
* Equivalente PHP di ElixFormsElement.tsx nel progetto React.
*
* Genera markup compatibile con il design system Bootstrap Italia:
* - Colonna sinistra (col-12 col-md-4): label
* - Colonna destra (col-12 col-md-8): input
*/
class ElixFormsElement
{
private string $labelHtml;
private string $inputHtml;
private string $separator;
/**
* @param string $labelHtml HTML della label
* @param string $inputHtml HTML dell'input
* @param string $separator Separatore opzionale tra label e input
*/
public function __construct(string $labelHtml, string $inputHtml, string $separator = '')
{
if (empty($labelHtml) || empty($inputHtml)) {
throw new \InvalidArgumentException('labelHtml and inputHtml are required.');
}
$this->labelHtml = $labelHtml;
$this->inputHtml = $inputHtml;
$this->separator = $separator;
}
/**
* Genera l'HTML del campo form con layout a due colonne.
*
* @return string HTML renderizzato
*/
public function render(): string
{
$sep = $this->separator !== '' ? htmlspecialchars($this->separator) : '';
return <<<HTML
<div class="row mb-3 align-items-start">
<div class="col-12 col-md-4">
<div class="text-md-end pe-md-3 mt-2">
{$this->labelHtml}{$sep}
</div>
</div>
<div class="col-12 col-md-8">
{$this->inputHtml}
</div>
</div>
HTML;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace ElixForms\Common;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\RequestOptions;
/**
* Client HTTP robusto per effettuare chiamate REST esterne.
* Adapter applicativo basato su Guzzle per la gestione uniforme di
* header, autenticazione, timeout ed errori delle chiamate esterne.
*/
class HttpClient
{
private ClientInterface $client;
private array $defaultHeaders;
private ?string $username = null;
private ?string $password = null;
private int $timeout = 10;
/**
* @param array<string, string> $defaultHeaders Header predefiniti per ogni richiesta
*/
public function __construct(array $defaultHeaders = [], ?ClientInterface $client = null)
{
$this->defaultHeaders = $defaultHeaders;
$this->client = $client ?? new Client();
}
/**
* Imposta le credenziali per l'autenticazione Basic.
*/
public function setBasicAuth(string $username, string $password): self
{
$this->username = $username;
$this->password = $password;
return $this;
}
/**
* Imposta il timeout massimo per la connessione e l'esecuzione.
*/
public function setTimeout(int $seconds): self
{
if ($seconds <= 0) {
throw new \InvalidArgumentException('Il timeout deve essere maggiore di zero.');
}
$this->timeout = $seconds;
return $this;
}
/**
* Esegue una richiesta HTTP GET.
*
* @param string $url URL della richiesta
* @param array<string, mixed> $queryParams Parametri query aggiuntivi
* @param array<string, string> $headers Header specifici per questa richiesta
* @return string Risposta in formato testuale
* @throws \RuntimeException In caso di errore HTTP o di trasporto
*/
public function get(string $url, array $queryParams = [], array $headers = []): string
{
$options = $queryParams === [] ? [] : [RequestOptions::QUERY => $queryParams];
return $this->request($url, 'GET', $headers, $options);
}
/**
* Esegue una richiesta HTTP POST.
*
* @param string $url URL della richiesta
* @param mixed $data Dati da inviare nel body (array, stringa o JSON)
* @param array<string, string> $headers Header specifici per questa richiesta
* @return string Risposta in formato testuale
* @throws \RuntimeException In caso di errore HTTP o di trasporto
*/
public function post(string $url, $data, array $headers = []): string
{
$options = is_array($data)
? [RequestOptions::FORM_PARAMS => $data]
: [RequestOptions::BODY => $data];
return $this->request($url, 'POST', $headers, $options);
}
/**
* @param array<string, string> $headers
* @param array<string, mixed> $requestOptions
*/
private function request(string $url, string $method, array $headers, array $requestOptions = []): string
{
$options = [
RequestOptions::HEADERS => array_merge($this->defaultHeaders, $headers),
RequestOptions::TIMEOUT => $this->timeout,
RequestOptions::CONNECT_TIMEOUT => $this->timeout,
RequestOptions::ALLOW_REDIRECTS => ['max' => 3],
RequestOptions::VERIFY => true,
RequestOptions::HTTP_ERRORS => true,
];
if ($this->username !== null && $this->password !== null) {
$options[RequestOptions::AUTH] = [$this->username, $this->password];
}
try {
$response = $this->client->request(
strtoupper($method),
$url,
array_replace($options, $requestOptions)
);
} catch (RequestException $exception) {
$response = $exception->getResponse();
if ($response !== null) {
throw new \RuntimeException(
"Richiesta fallita con codice di stato HTTP {$response->getStatusCode()}.",
0,
$exception
);
}
throw new \RuntimeException('Errore di connessione al servizio esterno.', 0, $exception);
} catch (GuzzleException $exception) {
throw new \RuntimeException('Errore durante la chiamata al servizio esterno.', 0, $exception);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode >= 300) {
throw new \RuntimeException("Richiesta fallita con codice di stato HTTP {$statusCode}.");
}
return (string)$response->getBody();
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace ElixForms\Common;
/**
* Utility statica per leggere parametri dalla URL corrente ($_GET).
* Equivalente PHP di QueryParamHelper.tsx nel progetto React.
*/
class QueryParamHelper
{
/**
* Ottiene un array di valori numerici dalla query string (per checkbox).
* Il parametro è una stringa di valori separati da virgola (es. "1,3,5").
*
* @param string $paramName Nome del parametro nella query string
* @return int[] Array di interi
*/
public static function getCheckedFromQuery(string $paramName): array
{
$raw = $_GET[$paramName] ?? '';
if ($raw === '') {
return [];
}
$parts = explode(',', (string)$raw);
$result = [];
foreach ($parts as $part) {
$trimmed = trim($part);
if ($trimmed !== '' && is_numeric($trimmed)) {
$result[] = (int)$trimmed;
}
}
return $result;
}
/**
* Ottiene un singolo valore numerico dalla query string (per radio/dropdown).
*
* @param string $paramName Nome del parametro nella query string
* @return int|null Valore numerico o null se non presente
*/
public static function getOptionFromQuery(string $paramName): ?int
{
if (!isset($_GET[$paramName])) {
return null;
}
$value = $_GET[$paramName];
if (!is_numeric($value)) {
return null;
}
return (int)$value;
}
/**
* Ottiene un valore booleano dalla query string.
*
* @param string $paramName Nome del parametro nella query string
* @return bool|null true/false o null se non presente
*/
public static function getBooleanFromQuery(string $paramName): ?bool
{
if (!isset($_GET[$paramName])) {
return null;
}
return strtolower((string)$_GET[$paramName]) === 'true';
}
/**
* Ottiene un valore testuale decodificato dalla query string.
*
* @param string $paramName Nome del parametro nella query string
* @return string|null Valore decodificato o null se non presente
*/
public static function getDecodedTextFromQuery(string $paramName): ?string
{
if (!isset($_GET[$paramName])) {
return null;
}
return urldecode((string)$_GET[$paramName]);
}
}
@@ -0,0 +1,32 @@
{
"component": {
"minCharsForSearch": 3,
"moduleName": "Ripartizione Utili / Compensi - Presentazione Proposta",
"headerTitle": "",
"headerHeroImageSrc": "",
"cardTitle": "Recupero Proposta CCT da Contratti",
"cardDescription": "",
"alertInfoTitle": "",
"alertInfoMessage": "",
"instructionsTitle": "Istruzioni di compilazione",
"instructionsMessage": "Completa i campi richiesti per procedere con la richiesta.",
"submitText": "CONFERMA E PROSEGUI"
},
"elixForms": {
"inputs": {
"codiceFiscale": "COL0009"
},
"outputs": {
"idDomanda": "COL0001",
"idRicevuta": "COL0002",
"codiceContratto": "COL0003",
"titoloContratto": "COL0004"
}
},
"contrattiWS": {
"apiUrl": "http://localhost:8000/contratti/cerca",
"apiUsername": "apiUsername",
"apiPassword": "[PASSWORD]",
"apiKey": "apiKey"
}
}
@@ -0,0 +1,438 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use ElixForms\Common\ElixFormsComponent;
use ElixForms\Common\QueryParamHelper;
/**
* Pagina custom "Recupero Proposta CCT da Contratti" per elixForms.
* Equivalente PHP del componente React RecuperoPropostaCctDaContrattiComponent.
*/
class RecuperoPropostaCctDaContrattiPage extends ElixFormsComponent
{
private string $codiceFiscale;
private array $fieldKeys;
private int $minCharsForSearch;
public function __construct(array $properties, array $fieldKeys, int $minCharsForSearch = 3)
{
parent::__construct($properties);
$this->fieldKeys = $fieldKeys;
$this->minCharsForSearch = $minCharsForSearch;
// Recupera il codice fiscale dalla query string
$this->codiceFiscale = QueryParamHelper::getDecodedTextFromQuery($fieldKeys['codiceFiscale']) ?? '';
}
/**
* Genera l'HTML e CSS del componente Autocomplete e i relativi campi nascosti.
*/
protected function createCustomFormFields(): string
{
$escapedCodFis = htmlspecialchars($this->codiceFiscale, ENT_QUOTES, 'UTF-8');
$minChars = $this->minCharsForSearch;
// Campi di output definiti nella configurazione
$outCodiceContratto = htmlspecialchars($this->fieldKeys['codiceContratto']);
$outTitoloContratto = htmlspecialchars($this->fieldKeys['titoloContratto']);
$outIdDomanda = htmlspecialchars($this->fieldKeys['idDomanda']);
$outIdRicevuta = htmlspecialchars($this->fieldKeys['idRicevuta']);
// Recupero di eventuali valori correnti dal GET per precompilazione iniziale
$currCodice = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['codiceContratto']) ?? '', ENT_QUOTES, 'UTF-8');
$currTitolo = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['titoloContratto']) ?? '', ENT_QUOTES, 'UTF-8');
$currDomanda = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['idDomanda']) ?? '', ENT_QUOTES, 'UTF-8');
$currRicevuta = htmlspecialchars(QueryParamHelper::getDecodedTextFromQuery($this->fieldKeys['idRicevuta']) ?? '', ENT_QUOTES, 'UTF-8');
$initialSearchText = '';
if ($currCodice !== '' && $currTitolo !== '') {
$initialSearchText = "[{$currCodice}] {$currTitolo}";
}
// Layout del campo di ricerca
$autocompleteInputHtml = <<<HTML
<div class="autocomplete-wrapper" id="autocomplete-container">
<input
id="autocomplete-search"
name="autocomplete-search"
className="form-control"
class="form-control"
type="text"
placeholder="Digita almeno {$minChars} caratteri per cercare..."
value="{$initialSearchText}"
autoComplete="off"
/>
<div id="autocomplete-dropdown" class="autocomplete-dropdown d-none"></div>
<div id="selected-contract-card" class="card mt-3 d-none">
<div class="card-body">
<div class="row g-2">
<div class="col-12 col-md-6"><strong>ID Contratto:</strong> <span id="summary-id"></span></div>
<div class="col-12 col-md-6"><strong>Titolo:</strong> <span id="summary-title"></span></div>
<div class="col-12 col-md-6"><strong>ID Domanda:</strong> <span id="summary-domanda"></span></div>
<div class="col-12 col-md-6"><strong>ID Ricevuta:</strong> <span id="summary-ricevuta"></span></div>
</div>
</div>
</div>
</div>
<!-- Input Nascosti per elixForms -->
<input type="hidden" id="codiceContratto_hidden" name="{$outCodiceContratto}" value="{$currCodice}" />
<input type="hidden" id="titoloContratto_hidden" name="{$outTitoloContratto}" value="{$currTitolo}" />
<input type="hidden" id="idDomanda_hidden" name="{$outIdDomanda}" value="{$currDomanda}" />
<input type="hidden" id="idRicevuta_hidden" name="{$outIdRicevuta}" value="{$currRicevuta}" />
HTML;
$labelHtml = '<label class="form-label fw-semibold" htmlFor="autocomplete-search">Cerca Contratto</label>';
return (new \ElixForms\Common\ElixFormsElement($labelHtml, $autocompleteInputHtml))->render();
}
/**
* Inietta i CSS specifici per l'autocomplete e la card.
*/
protected function renderExtraContentPre(): string
{
ob_start();
?>
<style>
.autocomplete-wrapper {
position: relative;
width: 100%;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: #ffffff;
border: 1px solid #a19f9d;
border-top: none;
border-radius: 0 0 2px 2px;
max-height: 250px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.autocomplete-dropdown-item {
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
border-bottom: 1px solid #f3f2f1;
transition: background-color 0.1s ease;
}
.autocomplete-dropdown-item:last-child {
border-bottom: none;
}
.autocomplete-dropdown-item:hover,
.autocomplete-dropdown-item.highlighted {
background-color: #edebe9;
}
.autocomplete-dropdown-item .contract-id {
font-weight: 600;
color: #323130;
}
.autocomplete-dropdown-item .contract-title {
color: #605e5c;
}
.autocomplete-loading {
padding: 12px;
text-align: center;
color: #605e5c;
font-size: 14px;
font-style: italic;
}
.autocomplete-no-results {
padding: 12px;
text-align: center;
color: #a19f9d;
font-size: 14px;
}
.autocomplete-error {
padding: 12px;
text-align: center;
color: #a4262c;
font-size: 14px;
}
</style>
<?php
return ob_get_clean();
}
/**
* Inietta il codice JavaScript per gestire le chiamate AJAX al proxy ed il comportamento dell'interfaccia.
*/
protected function renderExtraContentPost(): string
{
$escapedCodFis = htmlspecialchars($this->codiceFiscale, ENT_QUOTES, 'UTF-8');
$minChars = $this->minCharsForSearch;
ob_start();
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var searchInput = document.getElementById('autocomplete-search');
var dropdown = document.getElementById('autocomplete-dropdown');
var container = document.getElementById('autocomplete-container');
var card = document.getElementById('selected-contract-card');
var summaryId = document.getElementById('summary-id');
var summaryTitle = document.getElementById('summary-title');
var summaryDomanda = document.getElementById('summary-domanda');
var summaryRicevuta = document.getElementById('summary-ricevuta');
var hiddenCodice = document.getElementById('codiceContratto_hidden');
var hiddenTitolo = document.getElementById('titoloContratto_hidden');
var hiddenDomanda = document.getElementById('idDomanda_hidden');
var hiddenRicevuta = document.getElementById('idRicevuta_hidden');
var codFis = <?= json_encode($escapedCodFis) ?>;
var minChars = <?= $minChars ?>;
var searchResults = [];
var highlightedIndex = -1;
var debounceTimer = null;
// Se i campi hidden sono precompilati all'avvio, mostra la card di riepilogo
if (hiddenCodice.value && hiddenTitolo.value) {
showSummaryCard({
idContratto: hiddenCodice.value,
titoloContratto: hiddenTitolo.value,
idDomanda: hiddenDomanda.value,
idRicevuta: hiddenRicevuta.value
});
}
// Input event listener (con debounce)
searchInput.addEventListener('input', function() {
var text = this.value;
if (text.length === 0) {
clearSelection();
hideDropdown();
return;
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
if (text.length >= minChars) {
// Controlla se l'utente ha solo modificato il testo del contratto selezionato
if (hiddenCodice.value && text !== "[" + hiddenCodice.value + "] " + hiddenTitolo.value) {
clearSelection();
}
debounceTimer = setTimeout(function() {
fetchResults(text);
}, 300);
} else {
hideDropdown();
}
});
// Gestione eventi da tastiera
searchInput.addEventListener('keydown', function(event) {
if (dropdown.classList.contains('d-none')) return;
var items = dropdown.querySelectorAll('.autocomplete-dropdown-item');
if (items.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
updateHighlight(items);
break;
case 'ArrowUp':
event.preventDefault();
highlightedIndex = Math.max(highlightedIndex - 1, 0);
updateHighlight(items);
break;
case 'Enter':
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < searchResults.length) {
selectContract(searchResults[highlightedIndex]);
}
break;
case 'Escape':
event.preventDefault();
hideDropdown();
break;
}
});
// Chiudi il menu a discesa se si clicca all'esterno
document.addEventListener('mousedown', function(event) {
if (!container.contains(event.target)) {
hideDropdown();
}
});
function fetchResults(term) {
showDropdown();
dropdown.innerHTML = '<div class="autocomplete-loading">Ricerca in corso...</div>';
highlightedIndex = -1;
// Calcolo dinamico della base directory per supportare URL con o senza slash finale
var baseDir = window.location.pathname;
if (!baseDir.endsWith('/') && !baseDir.endsWith('.php')) {
baseDir += '/';
}
var lastSlash = baseDir.lastIndexOf('/');
var dir = baseDir.substring(0, lastSlash + 1);
var url = dir + 'search-contratti.php?term=' + encodeURIComponent(term) + '&cod_fis=' + encodeURIComponent(codFis);
fetch(url)
.then(function(response) {
return response.text().then(function(text) {
var data = null;
try {
data = JSON.parse(text);
} catch (e) {
// Risposta non JSON
}
if (!response.ok) {
var errMsg = (data && data.message) ? data.message :
(data && data.error) ? data.error :
('Errore API: ' + response.status + ' ' + response.statusText);
throw new Error(errMsg);
}
if (data === null) {
throw new Error('Risposta del server non in formato JSON.');
}
return data;
});
})
.then(function(data) {
searchResults = data;
renderResults(data);
})
.catch(function(error) {
console.error('Errore durante la ricerca contratti:', error);
dropdown.innerHTML = '<div class="autocomplete-error">' + error.message + '</div>';
});
}
function renderResults(results) {
if (results.length === 0) {
dropdown.innerHTML = '<div class="autocomplete-no-results">Nessun risultato trovato</div>';
return;
}
dropdown.innerHTML = '';
results.forEach(function(contract, index) {
var item = document.createElement('div');
item.className = 'autocomplete-dropdown-item';
item.innerHTML = '<span class="contract-id">[' + contract.idContratto + ']</span> ' +
'<span class="contract-title">' + contract.titoloContratto + '</span>';
// Mouse interactions
item.addEventListener('mousedown', function(event) {
event.preventDefault();
selectContract(contract);
});
item.addEventListener('mouseenter', function() {
highlightedIndex = index;
var items = dropdown.querySelectorAll('.autocomplete-dropdown-item');
updateHighlight(items);
});
dropdown.appendChild(item);
});
}
function updateHighlight(items) {
items.forEach(function(item, index) {
if (index === highlightedIndex) {
item.classList.add('highlighted');
// Scorri il menu se l'elemento evidenziato è fuori vista
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('highlighted');
}
});
}
function selectContract(contract) {
searchInput.value = '[' + contract.idContratto + '] ' + contract.titoloContratto;
hiddenCodice.value = contract.idContratto;
hiddenTitolo.value = contract.titoloContratto;
hiddenDomanda.value = contract.idDomanda;
hiddenRicevuta.value = contract.idRicevuta;
showSummaryCard(contract);
hideDropdown();
}
function showSummaryCard(contract) {
summaryId.textContent = contract.idContratto;
summaryTitle.textContent = contract.titoloContratto;
summaryDomanda.textContent = contract.idDomanda;
summaryRicevuta.textContent = contract.idRicevuta;
card.classList.remove('d-none');
}
function clearSelection() {
hiddenCodice.value = '';
hiddenTitolo.value = '';
hiddenDomanda.value = '';
hiddenRicevuta.value = '';
card.classList.add('d-none');
}
function showDropdown() {
dropdown.classList.remove('d-none');
}
function hideDropdown() {
dropdown.classList.add('d-none');
highlightedIndex = -1;
}
});
</script>
<?php
return ob_get_clean();
}
}
// =============================================================================
// Entry Point della Pagina
// =============================================================================
// Lettura e validazione della configurazione
$configFile = __DIR__ . '/config.json';
if (!file_exists($configFile)) {
die("File di configurazione config.json non trovato.");
}
$config = json_decode(file_get_contents($configFile), true);
if (json_last_error() !== JSON_ERROR_NONE) {
die("Errore di sintassi nel file config.json.");
}
// Istanziazione ed esecuzione del rendering
$page = new RecuperoPropostaCctDaContrattiPage(
$config['component'],
[
'codiceFiscale' => $config['elixForms']['inputs']['codiceFiscale'],
'idDomanda' => $config['elixForms']['outputs']['idDomanda'],
'idRicevuta' => $config['elixForms']['outputs']['idRicevuta'],
'codiceContratto'=> $config['elixForms']['outputs']['codiceContratto'],
'titoloContratto'=> $config['elixForms']['outputs']['titoloContratto'],
],
$config['component']['minCharsForSearch'] ?? 3
);
echo $page->render();
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../vendor/autoload.php';
use ElixForms\Common\HttpClient;
try {
// 1. Lettura dei parametri di input
$term = $_GET['term'] ?? '';
$codFis = $_GET['cod_fis'] ?? '';
if (empty($term) || strlen($term) < 3) {
http_response_code(400);
echo json_encode(['error' => 'Il parametro di ricerca "term" deve contenere almeno 3 caratteri.'], JSON_UNESCAPED_UNICODE);
exit;
}
if (empty($codFis)) {
http_response_code(400);
echo json_encode(['error' => 'Il parametro "cod_fis" (Codice Fiscale) è obbligatorio.'], JSON_UNESCAPED_UNICODE);
exit;
}
// 2. Caricamento configurazione
$configFile = __DIR__ . '/config.json';
if (!file_exists($configFile)) {
throw new \RuntimeException('File di configurazione config.json non trovato.');
}
$configJson = file_get_contents($configFile);
$config = json_decode($configJson, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($config['contrattiWS'])) {
throw new \RuntimeException('Configurazione del Web Service "contrattiWS" mancante o non valida.');
}
$wsConfig = $config['contrattiWS'];
$apiUrl = $wsConfig['apiUrl'] ?? '';
$apiUsername = $wsConfig['apiUsername'] ?? '';
$apiPassword = $wsConfig['apiPassword'] ?? '';
$apiKey = $wsConfig['apiKey'] ?? '';
// 3. Inizializzazione ed esecuzione della chiamata HTTP con HttpClient
$client = new HttpClient([
'X-Api-Key' => $apiKey,
'Accept' => 'application/json'
]);
// Configura autenticazione basic
$client->setBasicAuth($apiUsername, $apiPassword);
// Esegui la richiesta GET
$responseBody = $client->get($apiUrl, [
'cod_fis' => $codFis,
'term' => $term
]);
// Valida che la risposta sia in formato JSON corretto
json_decode($responseBody);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Il servizio contratti ha restituito una risposta in un formato non valido (non JSON).');
}
// Ritorna la risposta del WS direttamente
echo $responseBody;
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode([
'error' => 'Errore durante la ricerca dei contratti.',
'message' => 'Servizio temporaneamente non disponibile o risposta non valida.'
], JSON_UNESCAPED_UNICODE);
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use ElixForms\Common\ElixFormsComponent;
/**
* Pagina custom "Scelta Carriera" per elixForms.
* Equivalente PHP della pagina React scelta-carriera.
*
* Estende ElixFormsComponent sovrascrivendo:
* - createCustomFormFields(): definisce dropdown, text, textarea, boolean, radio, checkbox
* - renderExtraContentPost(): aggiunge un alert informativo in fondo
*/
class SceltaCarrieraPage extends ElixFormsComponent
{
protected function createCustomFormFields(): string
{
$html = '';
// Dropdown: COL0015 Goals
$html .= $this->createDropdownInput('COL0015', 'COL0015 Goals', [
['value' => 0, 'label' => 'Not applicable'],
['value' => 1, 'label' => 'Goal 1: No poverty'],
['value' => 2, 'label' => 'Goal 2: Zero hunger'],
['value' => 3, 'label' => 'Goal 3: Good health and well-being'],
['value' => 4, 'label' => 'Goal 4: Quality education'],
['value' => 5, 'label' => 'Goal 5: Gender equality'],
['value' => 6, 'label' => 'Goal 6: Clean water and sanitation'],
['value' => 7, 'label' => 'Goal 7: Affordable and clean energy'],
['value' => 8, 'label' => 'Goal 8: Decent work and economic growth'],
['value' => 9, 'label' => 'Goal 9: Industry, Innovation, and Infrastructure'],
['value' => 10, 'label' => 'Goal 10: Reduced inequalities'],
['value' => 11, 'label' => 'Goal 11: Sustainable cities and communities'],
['value' => 12, 'label' => 'Goal 12: Responsible consumption and production'],
['value' => 13, 'label' => 'Goal 13: Climate action'],
['value' => 14, 'label' => 'Goal 14: Life below water'],
['value' => 15, 'label' => 'Goal 15: Life on land'],
['value' => 16, 'label' => 'Goal 16: Peace, justice and strong institutions'],
['value' => 17, 'label' => 'Goal 17: Partnerships for the goals'],
], true);
// Text input: COL0002
$html .= $this->createTextInput('COL0002', 'Campo STRING', true);
// Textarea: COL0003
$html .= $this->createTextAreaInput('COL0003', 'Campo TEXTAREA', true);
// Boolean: COL0004
$html .= $this->createBooleanInput('COL0004', 'Campo BOOLEAN', true);
// Radio: COL0005
$html .= $this->createRadioInput('COL0005', 'Campo RADIO', [
['value' => 1, 'label' => 'Opzione 1'],
['value' => 2, 'label' => 'Opzione 2'],
['value' => 3, 'label' => 'Altra opzione'],
], true);
// Checkbox: COL0006
$html .= $this->createCheckboxInput('COL0006', 'Campo CHECKBOX', [
['value' => 4, 'label' => 'Check 1'],
['value' => 5, 'label' => 'Check 2'],
['value' => 6, 'label' => 'Altro check'],
]);
return $html;
}
protected function renderExtraContentPost(): string
{
return '<div class="alert alert-info mt-4">Questa pagina è la versione PHP della custom page Scelta Carriera.</div>';
}
}
// =============================================================================
// Entry point — Istanziazione e rendering della pagina
// =============================================================================
$page = new SceltaCarrieraPage([
'moduleName' => '',
'alertInfoMessage' => '',
'userDisplayName' => 'John Doe',
'headerTitle' => 'Modulo A/13 - Richiesta di Certificato',
'cardTitle' => 'Scelta Carriera',
'cardDescription' => 'Benvenuto nella piattaforma di scelta carriera! Esplora le tue opzioni e trova la strada giusta per te.',
'headerHeroImageSrc' => '',
]);
echo $page->render();
+36
View File
@@ -0,0 +1,36 @@
# Agente React — Regole e Convenzioni
Questo file definisce le istruzioni specifiche, le regole e le convenzioni per lo sviluppo e la manutenzione delle pagine custom e dei componenti scritti in **React e TypeScript** nel monorepo `react/`.
## Gestione ID dei Form Element
Gli ID dei campi del form (es. nel Dropdown, Checkbox, TextField) creati tramite i metodi `create...` in [ElixFormsComponentAbstract.tsx](file:///c:/__Git/_UniPR/elixforms-custom-pages/react/common/src/ElixFormsComponentAbstract.tsx) **non devono mai essere cambiati o manipolati** (ad es. aggiungendo suffissi come `_dropdown`), in quanto la piattaforma elixForms si basa sul loro nome ed ID esatto.
In caso di conflitti di ID (ad esempio con input `hidden`), applicare il suffisso `_hidden` all'input hidden e mantenere l'ID originale senza suffissi sull'elemento principale visibile.
## Convenzioni di Codice e Framework
1. **Libreria Common (`react/common/`)**:
- Scritta in **TypeScript** (`.tsx` o `.ts`).
- Usa **React Class Components** (non hooks/functional components) ereditando da `React.Component`.
- Lo stato è gestito in modo immutabile tramite `this.setState`.
- I checkbox mantengono uno stato locale tramite un `Map<string, number[]>` interno.
2. **Pagine Custom (`react/<nome-pagina>/`)**:
- Possono essere in `.jsx` o `.tsx` (TypeScript con `checkJs: true`).
- Usano **Functional Components** ed eventualmente Hooks se necessario.
- Istanziano la pagina tramite la classe: `new ElixForms.ElixFormsReact(props).render()`.
3. **Import di Tipi**:
- Avendo `verbatimModuleSyntax: true` abilitato in TypeScript, è obbligatorio usare `import type` per i tipi (es. `import type { JSX } from 'react';`).
## Configurazione di Build e Sviluppo
- Ogni pagina è un'applicazione Vite autonoma configurata in `vite.config.js`.
- La build deve produrre un **singolo bundle JS** autolimitato (`codeSplitting: false`, `manualChunks: undefined`).
- I CSS locali e SCSS moduli devono essere iniettati nel bundle finale tramite il plugin `vite-plugin-css-injected-by-js`.
- I CSS globali del design system di UniPR non vanno inclusi nella build ma caricati a runtime via `@import url(...)` in `App.css`.
## Skill React Correlate
Fai riferimento alle seguenti skill per maggiori dettagli operativi:
- [elixforms-common-library](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/common-library/SKILL.md): Documentazione componenti e factory.
- [elixforms-create-new-page](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/create-new-page/SKILL.md): Scaffolding di una nuova pagina React.
- [elixforms-typescript-conventions](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/typescript-react-conventions/SKILL.md): Linee guida di tipizzazione.
- [elixforms-vite-build-config](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/vite-build-config/SKILL.md): Bundling e build singola.
- [elixforms-dev-workflow](file:///c:/__Git/_UniPR/elixforms-custom-pages/.agents/skills/dev-workflow/SKILL.md): Comandi dev, build e debug in VS Code.
+23 -3
View File
@@ -1,13 +1,33 @@
{
"name": "common",
"name": "@elixforms/common",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "common",
"name": "@elixforms/common",
"version": "0.0.1",
"license": "ISC"
"license": "ISC",
"devDependencies": {
"@types/react": "^19.2.17"
}
},
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
}
}
}
+5 -2
View File
@@ -1,9 +1,12 @@
{
"name": "common",
"name": "@elixforms/common",
"version": "0.0.1",
"description": "Common components to build elixForms custom pages",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "index.js"
"main": "index.js",
"devDependencies": {
"@types/react": "^19.2.17"
}
}
+24
View File
@@ -0,0 +1,24 @@
@import url("https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap");
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css");
@import url("https://cdn.jsdelivr.net/npm/bootstrap-italia@2.18.2/dist/css/bootstrap-italia.min.css");
/* elixForms styles customizations */
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/util.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css");
/* These are here for future imports...
@import url("https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_overrides.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/console_user.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/designitalia_adaptation.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/fonts.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/isipcss_overrides.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/iulib.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/print.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/responsive.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/style-bs.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/design-bs/style.css");
@import url("https://console-unipr.elixforms.it/rwe2/css/plugins/modaldisplay.css");
@import url("https://console-unipr.elixforms.it/rwe2/isipcss/css/grid_fluid.css");
*/
+17 -23
View File
@@ -1,34 +1,28 @@
@import '~@fluentui/react/dist/sass/References.scss';
.elixFormsComponent {
overflow: hidden;
padding: 1em;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
&.teams {
font-family: $ms-font-family-fallbacks;
}
}
.welcome {
text-align: center;
.autocompleteDropdown {
margin-top: 0.5rem;
border: 1px solid #c9d1d9;
border-radius: 0.5rem;
background: #fff;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
}
.welcomeImage {
width: 100%;
max-width: 420px;
.autocompleteItem {
padding: 0.75rem 1rem;
cursor: pointer;
}
.links {
a {
text-decoration: none;
color: "[theme:link, default:#03787c]";
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
.autocompleteItem:hover,
.autocompleteItem.isHighlighted {
background-color: #f5f7fa;
}
&:hover {
text-decoration: underline;
color: "[theme:linkHovered, default: #014446]";
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
}
}
.autocompleteStatus {
margin-top: 0.5rem;
font-size: 0.95rem;
color: #5c6f82;
}
-16
View File
@@ -1,16 +0,0 @@
//import * as React from 'react';
import ElixFormsComponentAbstract from './ElixFormsComponentAbstract';
export default class ElixFormsComponent extends ElixFormsComponentAbstract {
// public override createCustomFormFields(): React.ReactElement {
// // const textTest = this.CreateTextInput("COL0018", "TEXT INPUT");
// // const checkTest = this.CreateCheckboxInput("COL0090", "CHECKBOX INPUT", new Map<number, string>([ [1, "Option 1"], [2, "Option 2"], [3, "Option 3"] ]));
// // const radioTest = this.CreateRadioInput("COL0091", "RADIO INPUT", new Map<number, string>([ [1, "Option 1"], [2, "Option 2"], [3, "Option 3"] ]));
// // const boolTest = this.CreateBooleanInput("COL0092", "BOOLEAN INPUT");
// // const textareaTest = this.CreateTextAreaInput("COL0093", "TEXTAREA INPUT");
// return (<></>);
// }
}
export { ElixFormsComponent as ElixFormsReact };
+384 -231
View File
@@ -1,12 +1,13 @@
import * as FluentUI from '@fluentui/react';
import './App.css';
import type { IElixFormsComponentFormState } from './IElixFormsComponentFormState';
import type { IElixFormsComponentProperties } from './IElixFormsComponentProperties';
import { ElixFormsElement } from './ElixFormsElement';
import { QueryParamHelper } from './QueryParamHelper';
import { Component, type FormEvent, type JSX, useState } from 'react';
import React from 'react';
import type { IElixFormsComponentCustomFormFieldFactory } from './IElixFormsComponentCustomFormFieldFactory';
import type { ElixFormsCheckboxOption, ElixFormsDropdownOption, ElixFormsRadioOption } from './ElixFormsTypes';
import { ElixFormsElement } from './ElixFormsElement';
import { QueryParamHelper } from './QueryParamHelper';
import { Component, type JSX } from 'react';
import React from 'react';
export default abstract class ElixFormsComponentAbstract extends Component<IElixFormsComponentProperties, IElixFormsComponentFormState> {
constructor(props: IElixFormsComponentProperties) {
@@ -15,78 +16,51 @@ export default abstract class ElixFormsComponentAbstract extends Component<IElix
this.state = {
formData: {}
};
// const [formData, setFormData] = useState({});
// const handleAnyInputChange = (name: string) => (event: any, valueOrOption: any) => { //ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SyntheticEvent<HTMLElement, Event>
// let value: any = null;
// // Fluent UI Dropdown passes (event, option)
// if (valueOrOption && valueOrOption.key !== undefined) {
// value = valueOrOption.key;
// }
// // Fluent UI TextField passes (event, newValue)
// else if (typeof valueOrOption === "string") {
// value = valueOrOption;
// }
// // Fluent UI Checkbox passes (event, checked)
// else if (typeof valueOrOption === "boolean") {
// value = valueOrOption;
// }
// // Native HTML inputs
// else if (event?.target) {
// value = event.target.type === "checkbox"
// ? event.target.checked
// : event.target.value;
// }
// setFormData(prev => ({
// ...prev,
// [name]: value
// }));
// console.log(`Input ${name} changed to ${value}. Current form data:`, JSON.stringify(formData));
// };
}
private checkboxValues = new Map<string, number[]>();
private mandatoryFormFieldNames: string[] = [
"RWE2_MODULE_ID",
"RWE2_REQUEST_ID",
"custom-workflow-back-url",
"custom-workflow-generic-id",
"custom-workflow-current-tabrel-genid",
"custom-workflow-source-field",
"crc",
"MODULE_TESTMODE_KEY",
"ELANG"
'RWE2_MODULE_ID',
'RWE2_REQUEST_ID',
'custom-workflow-back-url',
'custom-workflow-generic-id',
'custom-workflow-current-tabrel-genid',
'custom-workflow-source-field',
'crc',
'MODULE_TESTMODE_KEY',
'ELANG'
];
private createMandatoryFormFields(queryParams: URLSearchParams) : JSX.Element {
private createMandatoryFormFields(queryParams: URLSearchParams): JSX.Element {
const hiddenInputs: JSX.Element[] = [];
// Create a hidden input for each mandatory Elix parameter
this.mandatoryFormFieldNames.forEach(paramName => {
const value = queryParams.get(paramName) ?? "";
// Create the input element
hiddenInputs.push(this.createHiddenInput(paramName));
hiddenInputs.push(this.createHiddenInput(paramName));
});
return <>{hiddenInputs}</>;
}
private createAdditionalFormFields() : React.ReactElement {
const schema = JSON.parse(this.props.additionalFieldsJson || "[]");
private createAdditionalFormFields(): React.ReactElement {
const schema = JSON.parse(this.props.additionalFieldsJson || '[]');
return (
<>
{schema.map((field: { key: React.Key | null | undefined; }) => (
<div key={field.key}>
{this.renderField(field)}
</div>
))}
</>
<>
{schema.map((field: { key: React.Key | null | undefined; }) => (
<div key={field.key}>
{this.renderField(field)}
</div>
))}
</>
);
}
public createCustomFormFields(callback: (formFieldFactory: IElixFormsComponentCustomFormFieldFactory) => JSX.Element = () => <></>): JSX.Element {
protected createCustomFormFields(formFieldFactory: IElixFormsComponentCustomFormFieldFactory): JSX.Element {
return <></>;
}
private renderCustomFormFields(): JSX.Element {
const formFieldFactory = {
createBooleanInput: (name: string, label: string, required: boolean = false) =>
this.createBooleanInput(name, label, required),
@@ -106,257 +80,436 @@ export default abstract class ElixFormsComponentAbstract extends Component<IElix
this.createTextAreaInput(name, label, required),
};
return callback(formFieldFactory);
return this.createCustomFormFields(formFieldFactory);
}
protected renderExtraContentPre(): JSX.Element | null {
return null;
}
protected renderExtraContentPost(): JSX.Element | null {
return null;
}
public override render(): React.ReactElement<IElixFormsComponentProperties> {
const {
description,
isDarkTheme,
environmentMessage,
hasTeamsContext,
userDisplayName,
additionalFieldsJson,
customFormFieldsConfiguration
moduleName,
headerTitle,
headerHeroImageSrc: headerTitleImageSrc,
cardTitle,
cardDescription,
alertInfoTitle,
alertInfoMessage,
instructionsTitle,
instructionsMessage,
submitText,
} = this.props;
const queryParams = new URLSearchParams(window.location.search);
const missingMandatoryParams = this.mandatoryFormFieldNames.filter(paramName => !queryParams.has(paramName));
const hasAuthenticationError = missingMandatoryParams.length > 0;
return (
<section>
{/* className={`${styles.elixFormsReact} ${hasTeamsContext ? styles.teams : ''}`} */}
<div>{environmentMessage}</div>
<div>{description}</div>
<form action="https://procedure.unipr.it/rwe2/ComeBackToElixAndSave" method="post" acceptCharset="ISO-8859-1">
{this.createMandatoryFormFields(queryParams)}
{this.createAdditionalFormFields()}
{this.createCustomFormFields(customFormFieldsConfiguration)}
<input type="submit" value="Submit"/>
</form>
</section>
<div>
<header className="it-header-wrapper text-white">
{userDisplayName &&
<div className="it-header-slim-wrapper">
<div className="container">
<div className="row">
<div className="col-12">
<div className="it-header-slim-wrapper-content">
{/* <a className="d-none d-lg-block navbar-brand" href="#">Ente appartenenza</a> */}
<div className="nav-mobile">
{/* <nav aria-label="Navigazione accessoria">
<a className="it-opener d-lg-none" data-bs-toggle="collapse" href="#menu1a" role="button"
aria-expanded="false" aria-controls="menu4">
<span>Ente appartenenza</span>
<svg className="icon" aria-hidden="true">
<use href="/bootstrap-italia/dist/svg/sprites.svg#it-expand"></use>
</svg>
</a>
<div className="link-list-wrapper collapse" id="menu1a">
<ul className="link-list">
<li><a className="dropdown-item list-item" href="#">Link 1</a></li>
<li><a className="list-item active" href="#" aria-current="page">Link 2 (Attivo)</a></li>
</ul>
</div>
</nav> */}
</div>
<div className="it-header-slim-right-zone">
{/* <div className="nav-item dropdown">
<a className="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<span className="visually-hidden">Selezione lingua: lingua selezionata</span>
<span>ITA</span>
<svg className="icon d-none d-lg-block">
<use href="/bootstrap-italia/dist/svg/sprites.svg#it-expand"></use>
</svg>
</a>
<div className="dropdown-menu">
<div className="row">
<div className="col-12">
<div className="link-list-wrapper">
<ul className="link-list">
<li><a className="dropdown-item list-item" href="#"><span>ITA <span
className="visually-hidden">selezionata</span></span></a></li>
<li><a className="dropdown-item list-item" href="#"><span>ENG</span></a></li>
</ul>
</div>
</div>
</div>
</div>
</div> */}
<div className="it-access-top-wrapper">
{userDisplayName}
</div>
</div>
</div>
</div>
</div>
</div>
</div>}
<div className="it-header-center-wrapper">
<div className="container">
<div className="it-header-center-content-wrapper">
<div className="it-brand-wrapper">
<a href="https://www.unipr.it/" title="Torna alla homepage dell'Università di Parma">
<img className='icon' src="https://procedure.unipr.it/UploadImgs/12_marchio_UNIPR_neg_b1_squareLogo.png" alt="Università di Parma" />
<div className="it-brand-text">
<p className="h2 no_toc">Università di Parma</p>
{/* <p className="h3 no_toc d-none d-md-block">Procedure Online</p> */}
</div>
</a>
</div>
<div className="it-right-zone">
{/* <div className="it-search-wrapper">
<span className="d-none d-md-block">Cerca</span>
<button className="search-link" aria-label="Cerca">
<span className="it-search"></span>
</button>
</div> */}
</div>
</div>
</div>
</div>
</header>
<div className="container my-2 mx-4">
<div className='cmp-breadcrumb' role='navigation'>
<nav className="breadcrumb-container" aria-label="breadcrumb">
<ol className="breadcrumb">
<li className='breadcrumb-item'>Procedure Online<span className="separator">&gt;</span></li>
<li className="breadcrumb-item active" aria-current="page">{moduleName}</li>
</ol>
</nav>
</div>
{headerTitleImageSrc && <img src={headerTitleImageSrc} alt="Hero Image" className="hero-image img-fluid mb-3" />}
{headerTitle && (<p className='h2'>{headerTitle}</p>)}
<div className="container">
<div className="card shadow-sm mb-4">
<div className="card-body">
{cardTitle && <p className="h3 mb-3">{cardTitle}</p>}
{cardDescription && <p className="lead text-secondary">{cardDescription}</p>}
{(alertInfoTitle || alertInfoMessage) &&
<div className="custom-alert custom-alert-warning" role="status">
{alertInfoTitle && <div><b>{alertInfoTitle}</b></div>}
{alertInfoMessage && <div className='small'>{alertInfoMessage}</div>}
</div>}
{hasAuthenticationError ? (
<div className="alert alert-danger" role="alert">Accesso non autorizzato!</div>
) : (
<>
{this.renderExtraContentPre()}
{(instructionsTitle || instructionsMessage) &&
<div className="custom-alert custom-alert-info" role="status">
{instructionsTitle && <div><b>{instructionsTitle}</b></div>}
{instructionsMessage && <div className='small'>{instructionsMessage}</div>}
</div>
}
<form action="https://procedure.unipr.it/rwe2/ComeBackToElixAndSave" method="post" acceptCharset="ISO-8859-1" className="mt-3">
{this.createMandatoryFormFields(queryParams)}
{this.createAdditionalFormFields()}
{this.renderCustomFormFields()}
<div className="mt-4 d-flex justify-content-end">
<button type="submit" className="btn btn-primary">{submitText ?? 'INVIA'} <span className="it-arrow-right text-white ms-2"></span></button>
</div>
</form>
{this.renderExtraContentPost()}
</>
)}
</div>
</div>
</div>
</div>
<footer className="it-footer text-white">
<div className="it-footer-small-prints clearfix">
<div className="container">
<section>
<div className="row clearfix">
<div className="col-sm-12">
<div className="footer-content text-center p-2">
<div className="subfooter-top">powered by <span className="highlight">elixForms</span></div>
{/* <div className="subfooter-bottom"><div>versione 1.24.0</div></div> */}
</div>
</div>
</div>
</section>
</div>
</div>
</footer>
</div>
);
}
private createCheckboxInput(paramName: string, label: string, values: Array<ElixFormsCheckboxOption>) : JSX.Element {
const checkedValues = QueryParamHelper.getCheckedFromQuery(paramName);
private createCheckboxInput(paramName: string, label: string, values: Array<ElixFormsCheckboxOption>): JSX.Element {
const checkedValues = QueryParamHelper.getCheckedFromQuery(paramName);
if (!this.checkboxValues.get(paramName)) {
this.checkboxValues.set(paramName, checkedValues);
}
const checkboxes: React.ReactElement[] = [];
const stackTokens = { childrenGap: 10 };
const currentValue = this.state.formData[paramName] !== undefined
? this.state.formData[paramName]
: (this.checkboxValues.get(paramName)?.join(',').trim() ?? '');
values.forEach((entry, entryIndex) => {
const id = `${paramName}_${entryIndex}`;
checkboxes.push(
<FluentUI.Checkbox title={label} key={id} id={id} label={entry.label} required={entry.required ?? false} defaultChecked={checkedValues.indexOf(entryIndex) !== -1} className='isiportalPartialAdminCheckboxFieldItem'
onChange={(event, checked) => {
const currentCheckboxValues = this.checkboxValues.get(paramName)!;
const checkboxIndex = currentCheckboxValues.indexOf(entryIndex);
if (checked && checkboxIndex === -1) {
currentCheckboxValues.push(entryIndex);
} else if (!checked && checkboxIndex !== -1) {
currentCheckboxValues.splice(checkboxIndex, 1);
}
this.state.formData[paramName] = currentCheckboxValues.join(',').trim() ?? '';
//this.setState({ formData: { ...this.state.formData, [paramName]: currentCheckboxValues.join(',').trim() ?? '' } }, () =>
console.log(`Checkbox ${id} changed to ${checked}. Current values for ${paramName}: ${this.state.formData[paramName]}`);
//);
}}
/>
);
});
const selectedValues = (currentValue || '').split(',').filter(Boolean).map((value: string) => Number(value));
return new ElixFormsElement(
<><FluentUI.Label htmlFor={paramName}>{label}</FluentUI.Label></>,
<><FluentUI.Stack tokens={stackTokens}>
{checkboxes}
</FluentUI.Stack>
<input type='hidden' id={paramName} name={paramName} value={this.state.formData[paramName] ?? ''} />
<label className="form-label fw-semibold" htmlFor={paramName}>{label}</label>,
<>
<div className="d-flex flex-column gap-2" role="group" aria-label={label}>
{values.map((entry, entryIndex) => {
const id = `${paramName}_${entryIndex}`;
const isChecked = selectedValues.includes(entryIndex);
return (
<div className="form-check" key={id}>
<input
id={id}
className="form-check-input"
type="checkbox"
checked={isChecked}
onChange={(event) => {
const currentCheckboxValues = this.checkboxValues.get(paramName) ?? [];
const checkboxIndex = currentCheckboxValues.indexOf(entryIndex);
const nextValues = [...currentCheckboxValues];
if (event.target.checked && checkboxIndex === -1) {
nextValues.push(entryIndex);
} else if (!event.target.checked && checkboxIndex !== -1) {
nextValues.splice(checkboxIndex, 1);
}
this.checkboxValues.set(paramName, nextValues);
const newValue = nextValues.join(',').trim();
this.setState({ formData: { ...this.state.formData, [paramName]: newValue } });
}}
/>
<label className="form-check-label" htmlFor={id}>{entry.label}</label>
</div>
);
})}
</div>
<input type="hidden" id={paramName} name={paramName} value={currentValue} />
</>
).render();
}
private createRadioInput(paramName: string, label: string, values: Array<ElixFormsRadioOption>, required: boolean = false) : JSX.Element {
const radioValue = QueryParamHelper.getOptionFromQuery(paramName);
const radios: FluentUI.IChoiceGroupOption[] = [];
values.forEach((entry, _) => {
const id = `${paramName}_${entry.value}`;
radios.push(
{
key: id,
text: entry.label,
id: id,
name: paramName,
value: entry.value,
className: 'isiportalPartialAdminFormFieldRadio'
}
);
});
private createRadioInput(paramName: string, label: string, values: Array<ElixFormsRadioOption>, required: boolean = false): JSX.Element {
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: String(QueryParamHelper.getOptionFromQuery(paramName) ?? '');
return new ElixFormsElement(
<></>,
<FluentUI.ChoiceGroup
<span className="form-label fw-semibold">{label}</span>,
<div className="d-flex flex-column gap-2" role="radiogroup" aria-label={label}>
{values.map((entry) => {
const id = `${paramName}_${entry.value}`;
return (
<div className="form-check" key={id}>
<input
id={id}
className="form-check-input"
type="radio"
name={paramName}
value={entry.value}
checked={currentValue === String(entry.value)}
onChange={() => {
this.setState({ formData: { ...this.state.formData, [paramName]: String(entry.value) } });
}}
required={required}
/>
<label className="form-check-label" htmlFor={id}>{entry.label}</label>
</div>
);
})}
</div>
).render();
}
private createBooleanInput(paramName: string, label: string, required: boolean = false): JSX.Element {
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: String(QueryParamHelper.getOptionFromQuery(paramName) ?? '');
return new ElixFormsElement(
<span className="form-label fw-semibold">{label}</span>,
<div className="d-flex flex-column gap-2" role="radiogroup" aria-label={label}>
{[{ value: 'true', label: 'Sì' }, { value: 'false', label: 'No' }].map((val) => {
const id = `${paramName}_${val.value}`;
return (
<div className="form-check" key={id}>
<input
id={id}
className="form-check-input"
type="radio"
name={paramName}
value={val.value}
checked={currentValue === val.value}
onChange={() => {
this.setState({ formData: { ...this.state.formData, [paramName]: val.value } });
}}
required={required}
/>
<label className="form-check-label" htmlFor={id}>{val.label}</label>
</div>
);
})}
</div>
).render();
}
private createTextAreaInput(paramName: string, label: string, required: boolean = false): JSX.Element {
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: (QueryParamHelper.getDecodedTextFromQuery(paramName) ?? '');
return new ElixFormsElement(
<label className="form-label fw-semibold" htmlFor={paramName}>{label}</label>,
<textarea
id={paramName}
name={paramName}
label={label}
options={radios}
className="form-control"
rows={4}
required={required}
defaultSelectedKey={`${paramName}_${radioValue}`}
onChange={(event, option) => {
const value = option?.value ?? '';
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`Radio ${option?.id} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
value={currentValue}
onChange={(event) => {
this.setState({ formData: { ...this.state.formData, [paramName]: event.target.value } });
}}
/>
).render();
}
private createBooleanInput(paramName: string, label: string, required: boolean = false) : JSX.Element {
const boolValue = QueryParamHelper.getOptionFromQuery(paramName);
const radios: FluentUI.IChoiceGroupOption[] = [];
[{ "value": "true", "label": "Sì" }, { "value": "false", "label": "No" }].forEach((val) => {
const id = `${paramName}_${val.value}`;
radios.push(
{
key: id,
text: val.label,
id: id,
name: paramName,
value: val.value,
className: 'isiportalPartialAdminFormFieldRadio'
}
);
});
private createTextInput(paramName: string, label: string, required: boolean = false): JSX.Element {
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: (QueryParamHelper.getDecodedTextFromQuery(paramName) ?? '');
return new ElixFormsElement(
<></>,
<FluentUI.ChoiceGroup
<label className="form-label fw-semibold" htmlFor={paramName}>{label}</label>,
<input
id={paramName}
name={paramName}
label={label}
options={radios}
className="form-control"
type="text"
required={required}
defaultSelectedKey={`${paramName}_${boolValue}`}
onChange={(event, option) => {
const value = option?.value ?? '';
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`Boolean ${option?.id} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
value={currentValue}
onChange={(event) => {
this.setState({ formData: { ...this.state.formData, [paramName]: event.target.value } });
}}
/>
).render();
}
private createTextAreaInput(paramName: string, label: string, required: boolean = false) : JSX.Element {
const textareaValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? "";
return new ElixFormsElement(
<FluentUI.Label htmlFor={paramName}>{label}</FluentUI.Label>,
<FluentUI.TextField id={paramName} name={paramName} defaultValue={textareaValue} multiline rows={4} required={required} className='isiportalPartialAdminFormFieldMultiLineText'
onChange={(event, value) => {
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`TextArea ${paramName} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
}}
/>
).render();
private createHiddenInput(paramName: string): JSX.Element {
const textValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? '';
return <input type="hidden" id={`${paramName}_hidden`} name={paramName} value={textValue} />;
}
private createTextInput(paramName: string, label: string, required: boolean = false) : JSX.Element {
const textValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? "";
private createNumberInput(paramName: string, label: string, required: boolean = false): JSX.Element {
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: (QueryParamHelper.getDecodedTextFromQuery(paramName) ?? '');
return new ElixFormsElement(
<FluentUI.Label htmlFor={paramName}>{label}</FluentUI.Label>,
<FluentUI.TextField id={paramName} name={paramName} defaultValue={textValue} required={required} className='isiportalPartialAdminFormFieldSingleLineText'
onChange={(event, value) => {
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`TextInput ${paramName} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
}}
/>
).render();
}
private createHiddenInput(paramName: string) : JSX.Element {
const textValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? "";
return <><input type='text' id={paramName} name={paramName} defaultValue={textValue} /><br/></>;
}
private createNumberInput(paramName: string, label: string, required: boolean = false) : JSX.Element {
const textValue = QueryParamHelper.getDecodedTextFromQuery(paramName) ?? "";
return new ElixFormsElement(
<FluentUI.Label htmlFor={paramName}>{label}</FluentUI.Label>,
<FluentUI.TextField id={paramName} name={paramName} defaultValue={textValue} type="number" required={required} className='isiportalPartialAdminFormFieldSingleLineText'
onChange={(event, value) => {
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`NumberInput ${paramName} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
<label className="form-label fw-semibold" htmlFor={paramName}>{label}</label>,
<input
id={paramName}
name={paramName}
className="form-control"
type="number"
required={required}
value={currentValue}
onChange={(event) => {
this.setState({ formData: { ...this.state.formData, [paramName]: event.target.value } });
}}
/>
).render();
}
private createDropdownInput(paramName: string, label: string, values: Array<ElixFormsDropdownOption>, required: boolean = false): JSX.Element {
const selectedValue = QueryParamHelper.getOptionFromQuery(paramName);
const options: FluentUI.IDropdownOption[] = [];
values.forEach(entry => {
const id = `${paramName}_${entry.value}`;
options.push(
{
key: id,
text: entry.label
}
);
});
const queryValue = QueryParamHelper.getOptionFromQuery(paramName);
const currentValue = this.state.formData[paramName] !== undefined
? String(this.state.formData[paramName])
: (queryValue?.toString() ?? '');
return new ElixFormsElement(
<FluentUI.Label htmlFor={paramName}>{label}</FluentUI.Label>,
<FluentUI.Dropdown
<label className="form-label fw-semibold" htmlFor={paramName}>{label}</label>,
<select
id={paramName}
options={options}
selectedKey={selectedValue?.toString()}
placeholder='---'
name={paramName}
className="form-select"
required={required}
className='isiportalPartialAdminFormFieldSelect'
onChange={(event, value) => {
this.setState({ formData: { ...this.state.formData, [paramName]: value } }, () =>
console.log(`Dropdown ${paramName} changed to ${value}. Current value for ${paramName}: ${this.state.formData[paramName]}`)
);
value={currentValue}
onChange={(event) => {
this.setState({ formData: { ...this.state.formData, [paramName]: event.target.value } });
}}
/>
>
<option value="">Seleziona...</option>
{values.map(entry => (
<option key={entry.value} value={entry.value}>{entry.label}</option>
))}
</select>
).render();
}
private renderField(field: any): JSX.Element {
//const value = this.state.formData[field.key] || "";
switch (field.type) {
case "text":
case 'text':
return this.createTextInput(field.key, field.label, field.required ?? false);
case "textarea":
case 'textarea':
return this.createTextAreaInput(field.key, field.label, field.required ?? false);
case "radio":
case 'radio':
return this.createRadioInput(field.key, field.label,
new Array<ElixFormsRadioOption>().concat(...field.options.map((o: any) => [[o.value, o.label]])),
field.options.map((o: any) => ({ value: o.value, label: o.label })),
field.required ?? false);
case "boolean":
case 'boolean':
return this.createBooleanInput(field.key, field.label, field.required ?? false);
case "checkbox":
case 'checkbox':
return this.createCheckboxInput(field.key, field.label,
new Array<ElixFormsCheckboxOption>().concat(...field.options.map((o: any) => [[o.value, o.label, o.required ?? false]])));
field.options.map((o: any) => ({ value: o.value, label: o.label, required: o.required ?? false })));
case "number":
case 'number':
return this.createNumberInput(field.key, field.label, field.required ?? false);
case "dropdown":
case 'dropdown':
return this.createDropdownInput(field.key, field.label,
new Array<ElixFormsDropdownOption>().concat(...field.options.map((o: any) => [[o.value, o.label]])),
field.options.map((o: any) => ({ value: o.value, label: o.label })),
field.required ?? false);
default:
+16 -14
View File
@@ -1,14 +1,13 @@
import React, { type JSX } from "react";
import React, { type JSX } from 'react';
// Class to encapsulate the label and input elements
export class ElixFormsElement {
label: JSX.Element;
input: JSX.Element;
separator: string;
constructor(labelElement: React.ReactElement, inputElement: React.ReactElement, separator: string = "")
{
constructor(labelElement: React.ReactElement, inputElement: React.ReactElement, separator: string = '') {
if (!labelElement || !inputElement) {
throw new Error("labelElement and inputElement are required.");
throw new Error('labelElement and inputElement are required.');
}
this.label = labelElement;
@@ -16,15 +15,18 @@ export class ElixFormsElement {
this.separator = separator;
}
// Optional: render both elements together
render() : JSX.Element {
return (<><div className="iuFieldContainer">
<div className="attrDisplay_left attrDisplay_label"><div className="iuLabelContainer"><div className="attrDisplay_center"><div className="attrDisplay_middle">
{this.label}{this.separator}
</div></div><div className="iuClearContainer">&nbsp;</div></div></div>
<div className="attrDisplay_right attrDisplay_input"><div className="iuInputContainer iuTypeString"><div className="attrDisplay_center"><div className="attrDisplay_middle">
render(): JSX.Element {
return (
<div className="row mb-3 align-items-start">
<div className="col-12 col-md-4">
<div className="text-md-end pe-md-3 mt-2">
{this.label}{this.separator}
</div>
</div>
<div className="col-12 col-md-8">
{this.input}
</div></div><div className="iuClearContainer">&nbsp;</div></div></div>
</div></>);
</div>
</div>
);
}
}
@@ -1,14 +1,19 @@
import type { JSX } from "react";
import type { IElixFormsComponentCustomFormFieldFactory } from "./IElixFormsComponentCustomFormFieldFactory";
export interface IElixFormsComponentProperties {
description: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
isDarkTheme?: boolean;
userDisplayName?: string;
additionalFieldsJson?: string;
customFormFieldsConfiguration?: (formFieldFactory: IElixFormsComponentCustomFormFieldFactory) => JSX.Element;
// Layout Properties
moduleName: string;
headerTitle?: string;
headerHeroImageSrc?: string;
cardTitle: string;
cardDescription?: string;
alertInfoTitle?: string;
alertInfoMessage?: string;
instructionsTitle?: string;
instructionsMessage?: string;
submitText?: string;
}
// Additional Fields JSON Schema
+3
View File
@@ -0,0 +1,3 @@
declare module "*.css";
declare module "*.svg";
declare module "*.png";
+1 -1
View File
@@ -9,7 +9,7 @@
"jsx": "react-jsx",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"skipLibCheck": true
},
"include": [
"**/*"
+380 -299
View File
@@ -1,302 +1,383 @@
{
"name": "react",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@fluentui/react": "^8.125.6",
"react": "^19.2.7",
"react-dom": "^19.2.5"
}
},
"node_modules/@fluentui/date-time-utilities": {
"version": "8.6.11",
"resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.6.11.tgz",
"integrity": "sha512-zq49tveFzmzwgaJ73rVvxu9+rqhPBIAJSbevciIQnmvv6dlh2GzZcL14Zevk9QV+q6CWaF6yzvhT11E2TpAv8Q==",
"license": "MIT",
"dependencies": {
"@fluentui/set-version": "^8.2.24",
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/dom-utilities": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.3.10.tgz",
"integrity": "sha512-6WDImiLqTOpkEtfUKSStcTDpzmJfL6ZammomcjawN9xH/8u8G3Hx72CIt2MNck9giw/oUlNLJFdWRAjeP3rmPQ==",
"license": "MIT",
"dependencies": {
"@fluentui/set-version": "^8.2.24",
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/font-icons-mdl2": {
"version": "8.5.73",
"resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.73.tgz",
"integrity": "sha512-pyR9OE8LJMEVDAUTaTu/rGXaMCsyRqpp5124DhBoFNf6q5kI660IgbzuQadCpyMBEQadYI5/GLkLN7b1Lgou9Q==",
"license": "MIT",
"dependencies": {
"@fluentui/set-version": "^8.2.24",
"@fluentui/style-utilities": "^8.15.1",
"@fluentui/utilities": "^8.17.2",
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/foundation-legacy": {
"version": "8.6.6",
"resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.6.tgz",
"integrity": "sha512-PIcMLOymLvIunp9DrBHUT+dZqwyslKsIOPi+5g/fLU/ySzCg3fs6wQyWHxN3esI5FsSfWi+yCpp+6u5+N00kJQ==",
"license": "MIT",
"dependencies": {
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/set-version": "^8.2.24",
"@fluentui/style-utilities": "^8.15.1",
"@fluentui/utilities": "^8.17.2",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/keyboard-key": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.23.tgz",
"integrity": "sha512-9GXeyUqNJUdg5JiQUZeGPiKnRzMRi9YEUn1l9zq6X/imYdMhxHrxpVZS12129cBfgvPyxt9ceJpywSfmLWqlKA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/merge-styles": {
"version": "8.6.14",
"resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.14.tgz",
"integrity": "sha512-vghuHFAfQgS9WLIIs4kgDOCh/DHd5vGIddP4/bzposhlAVLZR6wUBqldm9AuCdY88r5LyCRMavVJLV+Up3xdvA==",
"license": "MIT",
"dependencies": {
"@fluentui/set-version": "^8.2.24",
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/react": {
"version": "8.125.6",
"resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.6.tgz",
"integrity": "sha512-uvq0PdAL+Tznikek54zbS31JefTqcPaCkwjHjJz0t8NAC2ZFHozKnkApaQo2+sf0390K3k1ErGWr/+3Tvc6+JQ==",
"license": "MIT",
"dependencies": {
"@fluentui/date-time-utilities": "^8.6.11",
"@fluentui/font-icons-mdl2": "^8.5.73",
"@fluentui/foundation-legacy": "^8.6.6",
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/react-focus": "^8.10.6",
"@fluentui/react-hooks": "^8.10.2",
"@fluentui/react-portal-compat-context": "^9.0.15",
"@fluentui/react-window-provider": "^2.3.2",
"@fluentui/set-version": "^8.2.24",
"@fluentui/style-utilities": "^8.15.1",
"@fluentui/theme": "^2.7.2",
"@fluentui/utilities": "^8.17.2",
"@microsoft/load-themed-styles": "^1.10.26",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"@types/react-dom": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0",
"react-dom": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/react-focus": {
"version": "8.10.6",
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.6.tgz",
"integrity": "sha512-hMQw7AETttfex3XEAB/XQalJSL8/RgzOdJrL1RPKcrxdkBal0C1MRQ43Bbmw/4WtI0vK0wI4+jHvCIlM0lkodA==",
"license": "MIT",
"dependencies": {
"@fluentui/keyboard-key": "^0.4.23",
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/set-version": "^8.2.24",
"@fluentui/style-utilities": "^8.15.1",
"@fluentui/utilities": "^8.17.2",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/react-hooks": {
"version": "8.10.2",
"resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.2.tgz",
"integrity": "sha512-HAd5cX50yKW/LljWlwt+FpSpdS/pNJutk9kMb7FyzxfoGBulL7sj6vX2HvxhSKyJMRKuTstXTdfJmsh22+3W3w==",
"license": "MIT",
"dependencies": {
"@fluentui/react-window-provider": "^2.3.2",
"@fluentui/set-version": "^8.2.24",
"@fluentui/utilities": "^8.17.2",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/react-portal-compat-context": {
"version": "9.0.15",
"resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.15.tgz",
"integrity": "sha512-DpV+qtFvM3dmH1j8ZD+YcM5vaTvmQPHUAx6tQnnmIoYJWs2R0wU/L5p2EajXy7zSg74jrDbDRxzaziamoOaJdg==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.1"
},
"peerDependencies": {
"@types/react": ">=16.14.0 <20.0.0",
"react": ">=16.14.0 <20.0.0"
}
},
"node_modules/@fluentui/react-window-provider": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.3.2.tgz",
"integrity": "sha512-T15zFPIWr9De8hNkapne7YyvcxclyTK2bMXXHZwbWLkVeH/lGHRG0CIy/calNGKa86wuzMJhq8iqFW2W6+EwVQ==",
"license": "MIT",
"dependencies": {
"@fluentui/set-version": "^8.2.24",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/set-version": {
"version": "8.2.24",
"resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.24.tgz",
"integrity": "sha512-8uNi2ThvNgF+6d3q2luFVVdk/wZV0AbRfJ85kkvf2+oSRY+f6QVK0w13vMorNhA5puumKcZniZoAfUF02w7NSg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/style-utilities": {
"version": "8.15.1",
"resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.1.tgz",
"integrity": "sha512-EwjJE7P7XWViNIN+Klm4rFCaADujsfwHB4nzkYeD0zjMokrznsiAvqERxaGZMCRH7tO+DLKITt7kaoQstNLCVQ==",
"license": "MIT",
"dependencies": {
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/set-version": "^8.2.24",
"@fluentui/theme": "^2.7.2",
"@fluentui/utilities": "^8.17.2",
"@microsoft/load-themed-styles": "^1.10.26",
"tslib": "^2.1.0"
}
},
"node_modules/@fluentui/theme": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.2.tgz",
"integrity": "sha512-UXGNfGa/1bLmYrOpmHXdvyc7CzlNSKUQAADweTncbNoMF1DvscWEjPj5kxFgCmOU8wVtvvn4GraNNUSWtNxeeA==",
"license": "MIT",
"dependencies": {
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/set-version": "^8.2.24",
"@fluentui/utilities": "^8.17.2",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@fluentui/utilities": {
"version": "8.17.2",
"resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.2.tgz",
"integrity": "sha512-TmeWVtGN+Lk0mch7tuRcbkeMdrBwltI68fvQbPwcNLo4igFtTInMmjEnVJGa7pBQN5lQAmHYqB9IJI6RZU/t6w==",
"license": "MIT",
"dependencies": {
"@fluentui/dom-utilities": "^2.3.10",
"@fluentui/merge-styles": "^8.6.14",
"@fluentui/react-window-provider": "^2.3.2",
"@fluentui/set-version": "^8.2.24",
"tslib": "^2.1.0"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <20.0.0",
"react": ">=16.8.0 <20.0.0"
}
},
"node_modules/@microsoft/load-themed-styles": {
"version": "1.10.295",
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz",
"integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT",
"peer": true
},
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.7"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"name": "react",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"bootstrap-italia": "^2.18.2",
"react": "^19.2.7",
"react-dom": "^19.2.5"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@splidejs/splide": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@splidejs/splide/-/splide-4.1.4.tgz",
"integrity": "sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==",
"license": "MIT"
},
"node_modules/@types/bootstrap": {
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.11.tgz",
"integrity": "sha512-69ykbLiNzQxi6Q/y/aYJLrlzI5QkLQjhIYW2U+cxujBxAmaBtmXKtxnFes59Ttfpt936as6xJU0dYnmVpcwIwg==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@videojs/http-streaming": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.5.tgz",
"integrity": "sha512-PRRQVu0hYW5h0XvRWpIyIpEatpMZzZtf7L1avV89Hnu+69f+0c7oLsSL9jwpg2nbvzGkdX5rQxQkz5Vc7tFR9w==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^4.1.2",
"aes-decrypter": "^4.0.2",
"global": "^4.4.0",
"m3u8-parser": "^7.2.0",
"mpd-parser": "^1.4.0",
"mux.js": "7.1.0",
"video.js": "^7 || ^8"
},
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"video.js": "^8.19.0"
}
},
"node_modules/@videojs/vhs-utils": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.2.tgz",
"integrity": "sha512-1Dsv5dtWOq0LP1G37O46FQFA2tKAZON5UMe6dFPxT7JvPhEx8QIXcFQxan8+hHI3fX6aomFRFrUBJNgRYQDSXA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/@videojs/xhr": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/aes-decrypter": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^4.1.1",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
}
},
"node_modules/animejs": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz",
"integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==",
"license": "MIT"
},
"node_modules/bootstrap-italia": {
"version": "2.18.2",
"resolved": "https://registry.npmjs.org/bootstrap-italia/-/bootstrap-italia-2.18.2.tgz",
"integrity": "sha512-qoOrq8lDfmELVS46PJekZVacWCkVqG+PF4g41QBevbIWgw2LII6mESrE+ntADMrEKhDAVYPHt0Z9viHhabzXFw==",
"license": "BSD-3-Clause",
"dependencies": {
"@popperjs/core": "^2.11.6",
"@splidejs/splide": "^4.1.4",
"@types/bootstrap": "^5.2.6",
"animejs": "^3.2.1",
"design-tokens-italia": "^1.3.3",
"just-validate": "^4.3.0",
"minimasonry": "^1.3.2",
"progressbar.js": "^1.1.0",
"uuid": "^14.0.0",
"video.js": "^8.23.7"
}
},
"node_modules/design-tokens-italia": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/design-tokens-italia/-/design-tokens-italia-1.3.3.tgz",
"integrity": "sha512-9vdoT91ln2H/mppLO1LkTD9q18YPbT+BX9seJxzAC8r7gPlDRmbPJwsmt8dqXnULaH8Lqkp1KEdL8HW3Ah6K6A==",
"license": "BSD-3-Clause"
},
"node_modules/dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"license": "MIT",
"dependencies": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"node_modules/is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
"license": "MIT"
},
"node_modules/just-validate": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/just-validate/-/just-validate-4.3.0.tgz",
"integrity": "sha512-9yS7ZnFk95vq+O7CAa7XZEJvZN5i6jTQWNhLwOjtgkSqiLxo7GZJhK8ViylP76fXPfHLs8tYWqi81XgxPF3OGA==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/m3u8-parser": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^4.1.1",
"global": "^4.4.0"
}
},
"node_modules/min-document": {
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
"integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
"license": "MIT",
"dependencies": {
"dom-walk": "^0.1.0"
}
},
"node_modules/minimasonry": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/minimasonry/-/minimasonry-1.3.2.tgz",
"integrity": "sha512-ygvf5JIR4NT1LbM6o8z1oz1icTkta72gXKwidvSqIVxuzVd/P21UI0Wbq1J+MStAu50Vvr8qWYNSz1W4uycqMQ==",
"license": "MIT"
},
"node_modules/mpd-parser": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.4.0.tgz",
"integrity": "sha512-blbA7XpAaOdC/PR0Tu8GiUxlx6CpBuRowQPYV7lhi9Yr9G9+dkPXIUjqjzc/NCn/uZZPopluaJlIYntIEI/VLw==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^4.1.2",
"@xmldom/xmldom": "^0.8.3",
"global": "^4.4.0"
},
"bin": {
"mpd-to-m3u8-json": "bin/parse.js"
}
},
"node_modules/mux.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
},
"bin": {
"muxjs-transmux": "bin/transmux.js"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.5.5"
},
"bin": {
"pkcs7": "bin/cli.js"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/progressbar.js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/progressbar.js/-/progressbar.js-1.1.1.tgz",
"integrity": "sha512-FBsw3BKsUbb+hNeYfiP3xzvAAQrPi4DnGDw66bCmfuRCDLcslxyxv2GyYUdBSKFGSIBa73CUP5WMcl6F8AAXlw==",
"license": "MIT",
"dependencies": {
"lodash.merge": "^4.6.2",
"shifty": "^2.8.3"
}
},
"node_modules/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.7"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/shifty": {
"version": "2.20.4",
"resolved": "https://registry.npmjs.org/shifty/-/shifty-2.20.4.tgz",
"integrity": "sha512-4Y0qRkg8ME5XN8yGNAwmFOmsIURGFKT9UQfNL6DDJQErYtN5HsjyoBuJn41ZQfTkuu2rIbRMn9qazjKsDpO2TA==",
"license": "MIT",
"optionalDependencies": {
"fsevents": "^2.3.2"
}
},
"node_modules/uuid": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/video.js": {
"version": "8.23.9",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.9.tgz",
"integrity": "sha512-3UCqyFW4wIusQu4TuhgRUxqtbzbj0WPT9hps0XIEXST43QBD11zb+bG7tBPrinrZztwVYdXCiOBo2pcGj9L05w==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.28.4",
"@videojs/http-streaming": "^3.17.5",
"@videojs/vhs-utils": "^4.1.2",
"@videojs/xhr": "2.7.0",
"aes-decrypter": "^4.0.2",
"global": "4.4.0",
"m3u8-parser": "^7.2.0",
"mpd-parser": "^1.4.0",
"mux.js": "^7.0.1",
"videojs-contrib-quality-levels": "4.1.0",
"videojs-font": "4.2.0",
"videojs-vtt.js": "0.15.5"
}
},
"node_modules/videojs-contrib-quality-levels": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
"license": "Apache-2.0",
"dependencies": {
"global": "^4.4.0"
},
"engines": {
"node": ">=16",
"npm": ">=8"
},
"peerDependencies": {
"video.js": "^8"
}
},
"node_modules/videojs-font": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
"license": "Apache-2.0"
},
"node_modules/videojs-vtt.js": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
"license": "Apache-2.0",
"dependencies": {
"global": "^4.3.1"
}
}
}
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"dependencies": {
"@fluentui/react": "^8.125.6",
"react": "^19.2.7",
"react-dom": "^19.2.5"
}
"dependencies": {
"bootstrap-italia": "^2.18.2",
"react": "^19.2.7",
"react-dom": "^19.2.5"
}
}
@@ -0,0 +1,21 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
])
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>recupero-proposta-cct-da-contratti</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
{
"name": "@elixforms/recupero-proposta-cct-da-contratti",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"vite-plugin-css-injected-by-js": "^5.0.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"sass-embedded": "^1.99.0",
"vite": "^8.0.10"
}
}
@@ -0,0 +1,104 @@
@import url('https://cdn.jsdelivr.net/npm/bootstrap-italia@2.0.0/dist/css/bootstrap-italia.min.css');
/* ===== Autocomplete Component Styles ===== */
.autocomplete-wrapper {
position: relative;
width: 100%;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: #ffffff;
border: 1px solid #a19f9d;
border-top: none;
border-radius: 0 0 2px 2px;
max-height: 250px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.autocomplete-dropdown-item {
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
line-height: 1.4;
border-bottom: 1px solid #f3f2f1;
transition: background-color 0.1s ease;
}
.autocomplete-dropdown-item:last-child {
border-bottom: none;
}
.autocomplete-dropdown-item:hover,
.autocomplete-dropdown-item.highlighted {
background-color: #edebe9;
}
.autocomplete-dropdown-item .contract-id {
font-weight: 600;
color: #323130;
}
.autocomplete-dropdown-item .contract-title {
color: #605e5c;
}
.autocomplete-loading {
padding: 12px;
text-align: center;
color: #605e5c;
font-size: 14px;
font-style: italic;
}
.autocomplete-no-results {
padding: 12px;
text-align: center;
color: #a19f9d;
font-size: 14px;
}
.autocomplete-error {
padding: 12px;
text-align: center;
color: #a4262c;
font-size: 14px;
}
/* ===== Selected Contract Summary ===== */
.selected-contract-summary {
margin-top: 8px;
padding: 10px 12px;
background-color: #f3f2f1;
border: 1px solid #e1dfdd;
border-radius: 2px;
font-size: 13px;
color: #323130;
}
.selected-contract-summary .summary-row {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.selected-contract-summary .summary-row:last-child {
margin-bottom: 0;
}
.selected-contract-summary .summary-label {
font-weight: 600;
min-width: 120px;
color: #605e5c;
}
.selected-contract-summary .summary-value {
color: #323130;
}
@@ -0,0 +1,22 @@
import './App.css'
import RecuperoPropostaCctDaContrattiComponent from './RecuperoPropostaCctDaContrattiComponent';
import config from './config.json';
function App() {
return (
<RecuperoPropostaCctDaContrattiComponent
moduleName={config.component.moduleName}
headerTitle={config.component.headerTitle}
headerHeroImageSrc={config.component.headerHeroImageSrc}
cardTitle={config.component.cardTitle}
cardDescription={config.component.cardDescription}
alertInfoTitle={config.component.alertInfoTitle}
alertInfoMessage={config.component.alertInfoMessage}
instructionsTitle={config.component.instructionsTitle}
instructionsMessage={config.component.instructionsMessage}
submitText={config.component.submitText}
/>
);
}
export default App
@@ -0,0 +1,305 @@
import React, { type JSX } from 'react';
import ElixFormsComponentAbstract from '@common/src/ElixFormsComponentAbstract';
import type { IElixFormsComponentCustomFormFieldFactory } from '@common/src/IElixFormsComponentCustomFormFieldFactory';
import { ElixFormsElement } from '@common/src/ElixFormsElement';
import { QueryParamHelper } from '@common/src/QueryParamHelper';
import config from './config.json';
interface ContrattoResult {
idContratto: string;
titoloContratto: string;
idDomanda: string;
idRicevuta: string;
}
interface RecuperoPropostaState {
searchText: string;
searchResults: ContrattoResult[];
isLoading: boolean;
isDropdownOpen: boolean;
selectedContract: ContrattoResult | null;
highlightedIndex: number;
errorMessage: string;
}
export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsComponentAbstract {
private autocompleteState: RecuperoPropostaState = {
searchText: '',
searchResults: [],
isLoading: false,
isDropdownOpen: false,
selectedContract: null,
highlightedIndex: -1,
errorMessage: '',
};
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
private autocompleteRef: React.RefObject<HTMLDivElement | null> = React.createRef();
private static readonly formFieldKeys = {
codiceContratto: config.elixForms.outputs.codiceContratto,
titoloContratto: config.elixForms.outputs.titoloContratto,
idDomanda: config.elixForms.outputs.idDomanda,
idRicevuta: config.elixForms.outputs.idRicevuta,
} as const;
private codiceFiscale: string;
constructor(props: any) {
super(props);
this.codiceFiscale = QueryParamHelper.getDecodedTextFromQuery(config.elixForms.inputs.codiceFiscale) ?? '';
}
override componentDidMount(): void {
document.addEventListener('mousedown', this.handleClickOutside);
}
override componentWillUnmount(): void {
document.removeEventListener('mousedown', this.handleClickOutside);
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
}
private handleClickOutside = (event: MouseEvent): void => {
if (this.autocompleteRef.current && !this.autocompleteRef.current.contains(event.target as Node)) {
this.autocompleteState.isDropdownOpen = false;
this.forceUpdate();
}
};
private async fetchContratti(searchTerm: string): Promise<void> {
this.autocompleteState.isLoading = true;
this.autocompleteState.errorMessage = '';
this.forceUpdate();
try {
const url = new URL(config.contrattiWS.apiUrl);
url.searchParams.set('cod_fis', this.codiceFiscale);
url.searchParams.set('term', searchTerm);
const credentials = btoa(`${config.contrattiWS.apiUsername}:${config.contrattiWS.apiPassword}`);
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: `Basic ${credentials}`,
'X-Api-Key': config.contrattiWS.apiKey,
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`Errore API: ${response.status} ${response.statusText}`);
}
const data: ContrattoResult[] = await response.json();
this.autocompleteState.searchResults = data;
this.autocompleteState.isDropdownOpen = data.length > 0;
this.autocompleteState.highlightedIndex = -1;
this.autocompleteState.isLoading = false;
this.forceUpdate();
} catch (error) {
console.error('Errore durante la ricerca contratti:', error);
this.autocompleteState.searchResults = [];
this.autocompleteState.isDropdownOpen = false;
this.autocompleteState.isLoading = false;
this.autocompleteState.errorMessage = error instanceof Error ? error.message : 'Errore durante la ricerca';
this.forceUpdate();
}
}
private handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const text = event.target.value;
this.autocompleteState.searchText = text;
if (text.length === 0) {
this.autocompleteState.searchResults = [];
this.autocompleteState.isDropdownOpen = false;
this.autocompleteState.highlightedIndex = -1;
this.autocompleteState.errorMessage = '';
this.clearSelection();
this.forceUpdate();
return;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
if (text.length >= config.component.minCharsForSearch) {
this.debounceTimer = setTimeout(() => {
this.fetchContratti(text);
}, 300);
} else {
this.autocompleteState.searchResults = [];
this.autocompleteState.isDropdownOpen = false;
this.autocompleteState.highlightedIndex = -1;
this.forceUpdate();
}
};
private handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
const { searchResults, isDropdownOpen, highlightedIndex } = this.autocompleteState;
if (!isDropdownOpen || searchResults.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.autocompleteState.highlightedIndex = Math.min(highlightedIndex + 1, searchResults.length - 1);
this.forceUpdate();
break;
case 'ArrowUp':
event.preventDefault();
this.autocompleteState.highlightedIndex = Math.max(highlightedIndex - 1, 0);
this.forceUpdate();
break;
case 'Enter':
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < searchResults.length) {
this.selectContract(searchResults[highlightedIndex]);
}
break;
case 'Escape':
event.preventDefault();
this.autocompleteState.isDropdownOpen = false;
this.forceUpdate();
break;
}
};
private selectContract(contract: ContrattoResult): void {
this.autocompleteState.selectedContract = contract;
this.autocompleteState.searchText = `[${contract.idContratto}] ${contract.titoloContratto}`;
this.autocompleteState.isDropdownOpen = false;
this.autocompleteState.searchResults = [];
this.autocompleteState.highlightedIndex = -1;
this.setState({
formData: {
...this.state.formData,
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.codiceContratto]: contract.idContratto,
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.titoloContratto]: contract.titoloContratto,
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idDomanda]: contract.idDomanda,
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idRicevuta]: contract.idRicevuta,
},
});
}
private clearSelection(): void {
this.autocompleteState.selectedContract = null;
this.setState({
formData: {
...this.state.formData,
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.codiceContratto]: '',
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.titoloContratto]: '',
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idDomanda]: '',
[RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idRicevuta]: '',
},
});
}
private renderAutocomplete(): JSX.Element {
const { searchResults, isLoading, isDropdownOpen, highlightedIndex, errorMessage, searchText } = this.autocompleteState;
const autocompleteInput = (
<>
<div className="position-relative" ref={this.autocompleteRef}>
<input
id="autocomplete-search"
name="autocomplete-search"
className="form-control"
type="text"
placeholder="Digita almeno 3 caratteri per cercare..."
value={searchText}
onChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
autoComplete="off"
/>
{isLoading && (
<div className="autocompleteDropdown mt-2 p-3 text-muted">
<span className="me-2">
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</span>
Ricerca in corso...
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length > 0 && (
<div className="autocompleteDropdown mt-2">
{searchResults.map((contract, index) => (
<div
key={`${contract.idContratto}_${index}`}
className={`autocompleteItem${index === highlightedIndex ? ' isHighlighted' : ''}`}
onMouseDown={(event) => {
event.preventDefault();
this.selectContract(contract);
}}
onMouseEnter={() => {
this.autocompleteState.highlightedIndex = index;
this.forceUpdate();
}}
>
<span className="fw-semibold">[{contract.idContratto}]</span>{' '}
<span>{contract.titoloContratto}</span>
</div>
))}
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length === 0 && (
<div className="autocompleteDropdown mt-2 p-3 text-muted">Nessun risultato trovato</div>
)}
{errorMessage && (
<div className="custom-alert custom-alert-danger mt-2">{errorMessage}</div>
)}
</div>
{this.autocompleteState.selectedContract && (
<div className="card mt-3">
<div className="card-body">
<div className="row g-2">
<div className="col-12 col-md-6"><strong>ID Contratto:</strong> {this.autocompleteState.selectedContract.idContratto}</div>
<div className="col-12 col-md-6"><strong>Titolo:</strong> {this.autocompleteState.selectedContract.titoloContratto}</div>
<div className="col-12 col-md-6"><strong>ID Domanda:</strong> {this.autocompleteState.selectedContract.idDomanda}</div>
<div className="col-12 col-md-6"><strong>ID Ricevuta:</strong> {this.autocompleteState.selectedContract.idRicevuta}</div>
</div>
</div>
</div>
)}
</>
);
return new ElixFormsElement(
<label className="form-label fw-semibold" htmlFor="autocomplete-search">Cerca Contratto</label>,
autocompleteInput
).render();
}
protected override createCustomFormFields(formFieldFactory: IElixFormsComponentCustomFormFieldFactory): JSX.Element {
const fieldKeys = RecuperoPropostaCctDaContrattiComponent.formFieldKeys;
const idContratto = this.state.formData[fieldKeys.codiceContratto] ?? '';
const titoloContratto = this.state.formData[fieldKeys.titoloContratto] ?? '';
const idDomanda = this.state.formData[fieldKeys.idDomanda] ?? '';
const idRicevuta = this.state.formData[fieldKeys.idRicevuta] ?? '';
return (
<>
{this.renderAutocomplete()}
<input type="hidden" id="idContratto_hidden" name={fieldKeys.codiceContratto} value={idContratto} />
<input type="hidden" id="titoloContratto_hidden" name={fieldKeys.titoloContratto} value={titoloContratto} />
<input type="hidden" id="idDomanda_hidden" name={fieldKeys.idDomanda} value={idDomanda} />
<input type="hidden" id="idRicevuta_hidden" name={fieldKeys.idRicevuta} value={idRicevuta} />
</>
);
}
}
@@ -0,0 +1,32 @@
{
"component": {
"minCharsForSearch": 3,
"moduleName": "Ripartizione Utili / Compensi - Presentazione Proposta",
"headerTitle": "",
"headerHeroImageSrc": "",
"cardTitle": "Recupero Proposta CCT da Contratti",
"cardDescription": "",
"alertInfoTitle": "",
"alertInfoMessage": "",
"instructionsTitle": "Istruzioni di compilazione",
"instructionsMessage": "Completa i campi richiesti per procedere con la richiesta.",
"submitText": "CONFERMA E PROSEGUI"
},
"elixForms": {
"inputs": {
"codiceFiscale": "COL000A"
},
"outputs": {
"idDomanda": "COL00B1",
"idRicevuta": "COL00B2",
"codiceContratto": "COL00B3",
"titoloContratto": "COL00B4"
}
},
"contrattiWS": {
"apiUrl": "http://localhost:8000/contratti/cerca",
"apiUsername": "username",
"apiPassword": "password",
"apiKey": "apitoken"
}
}
@@ -0,0 +1,17 @@
{
"folders": [
{
"name": "elixforms-custom-pages",
"path": "../../.."
},
{
"name": "elixforms-webservices",
"path": "../../../../elixforms-webservices"
},
{
"name": "SSH FS - Dev Server",
"uri": "ssh://dev.unipr.it/home/dev68/dev"
}
],
"settings": {}
}
@@ -0,0 +1,3 @@
declare module "*.css";
declare module "*.svg";
declare module "*.png";
@@ -0,0 +1 @@
/* Global styles - currently empty */
@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Root element not found')
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
)
@@ -0,0 +1,18 @@
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../common"
}
],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"allowJs": true,
"checkJs": true,
//"resolveJsonModule": true
},
"include": [
"src/**/*"
]
}
@@ -0,0 +1,40 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
import path from "path";
const ASSETS_DIR = 'assets';
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
cssInjectedByJsPlugin() // forza l'iniezione del CSS nel JS
],
base: './',
build: {
outDir: 'dist',
cssCodeSplit: false, // evita file CSS separati
rollupOptions: {
output: {
codeSplitting: false, // forza tutto in un singolo bundle
manualChunks: undefined, // disabilita la creazione di chunk multipli
entryFileNames: `${ASSETS_DIR}/recupero-proposta-cct-da-contratti.js`,
assetFileNames: `${ASSETS_DIR}/[name].[ext]`,
},
},
sourcemap: true,
},
resolve: {
alias: {
"@common": path.resolve(__dirname, "../common"),
}
},
server: {
fs: {
allow: [
".." // permette import fuori dalla cartella del progetto
]
}
}
})
+352 -353
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -1,3 +1,14 @@
@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap');
@import url('https://cdn.jsdelivr.net/npm/bootstrap-italia@2.0.0/dist/css/bootstrap-italia.min.css');
:root {
--font-family-sans-serif: "Titillium Web", "Segoe UI", Arial, sans-serif;
}
body {
font-family: "Titillium Web", "Segoe UI", Arial, sans-serif;
}
/*
@import url('https://console-unipr.elixforms.it/elixFormsCustom/css/elixForms.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_adaptation.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/design-bs/bootstrap_overrides.css');
@@ -14,6 +25,7 @@
@import url('https://console-unipr.elixforms.it/rwe2/css/plugins/modaldisplay.css');
@import url('https://console-unipr.elixforms.it/rwe2/css/themes/blu-italia.css');
@import url('https://console-unipr.elixforms.it/rwe2/isipcss/css/grid_fluid.css');
*/
/*
.counter {
+12 -197
View File
@@ -1,204 +1,19 @@
import { useState } from 'react'
// import reactLogo from './assets/react.svg'
// import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
import * as ElixForms from '@common/src/ElixFormsComponent';
import SceltaCarrieraComponent from './SceltaCarrieraComponent';
function App() {
const [count, setCount] = useState(0)
/** @type {import('@common/src/ElixFormsComponent').ElixFormsReact} */
var myElixFormsReact = new ElixForms.ElixFormsReact({
description: "This is a custom form created with ElixFormsReact component.",
isDarkTheme: false,
environmentMessage: "TESTING!",
hasTeamsContext: false,
userDisplayName: "John Doe",
/*
additionalFieldsJson: JSON.stringify([
{
"key": "COL0002",
"type": "text",
"label": "Campo STRING",
"required": false
},
{
"key": "COL0003",
"type": "textarea",
"label": "Campo TEXTAREA",
"required": false
},
{
"key": "COL0004",
"type": "boolean",
"label": "Campo Boolean",
"required": false
},
{
"key": "COL0005",
"type": "radio",
"label": "Campo _RADIO_",
"required": false,
"options": [
{ "value": 1, "label": "Opzione 1" },
{ "value": 2, "label": "Opzione 2" },
{ "value": 3, "label": "Altra opzione" }
]
},
{
"key": "COL0006",
"type": "checkbox",
"label": "Campo CHECKBOX",
"options": [
{ "value": 4, "label": "Check 1" },
{ "value": 5, "label": "Check due" },
{ "value": 6, "label": "Altro check" }
]
},
{
"key": "COL0015",
"type": "dropdown",
"label": "Validazione lista",
"required": true,
"options": [
{ "value": 0, "label": "Not applicable" },
{ "value": 1, "label": "Goal 1: No poverty" },
{ "value": 2, "label": "Goal 2: Zero hunger" },
{ "value": 3, "label": "Goal 3: Good health and well-being" },
{ "value": 4, "label": "Goal 4: Quality education" },
{ "value": 5, "label": "Goal 5: Gender equality" },
{ "value": 6, "label": "Goal 6: Clean water and sanitation" },
{ "value": 7, "label": "Goal 7: Affordable and clean energy" },
{ "value": 8, "label": "Goal 8: Decent work and economic growth" },
{ "value": 9, "label": "Goal 9: Industry, Innovation, and Infrastructure" },
{ "value": 10, "label": "Goal 10: Reduced inequalities" },
{ "value": 11, "label": "Goal 11: Sustainable cities and communities" },
{ "value": 12, "label": "Goal 12: Responsible consumption and production" },
{ "value": 13, "label": "Goal 13: Climate action" },
{ "value": 14, "label": "Goal 14: Life below water" },
{ "value": 15, "label": "Goal 15: Life on land" },
{ "value": 16, "label": "Goal 16: Peace, justice and strong institutions" },
{ "value": 17, "label": "Goal 17: Partnerships for the goals" }
]
}
]),
*/
customFormFieldsConfiguration: (
/** @type {import('../../common/src/IElixFormsComponentCustomFormFieldFactory').IElixFormsComponentCustomFormFieldFactory} */
formFieldFactory) => (
<>
{/* {formFieldFactory.createTextInput("required", "Campo Required", true)}
{formFieldFactory.createNumberInput("number", "Number")}
{formFieldFactory.createBooleanInput("boolean", "Boolean")}
{formFieldFactory.createRadioInput("radio", "Radio", [{ "value": 1, "label": "Opzione 1" }, { "value": 2, "label": "Opzione 2" }, { "value": 3, "label": "Altra opzione" }])}
{formFieldFactory.createDropdownInput("dropdown", "Dropdown", [{ "value": 0, "label": "Not applicable" }, { "value": 1, "label": "Goal 1: No poverty" }, { "value": 2, "label": "Goal 2: Zero hunger" }, { "value": 3, "label": "Goal 3: Good health and well-being" }])}
{formFieldFactory.createCheckboxInput("checkbox", "Checkbox", [{ "value": 0, "label": "Check 1" }, { "value": 1, "label": "Check 2" }, { "value": 2, "label": "Check 3" }])} */}
{formFieldFactory.createDropdownInput("COL0015", "COL0015 Goals", [
{ "value": 0, "label": "Not applicable" },
{ "value": 1, "label": "Goal 1: No poverty" },
{ "value": 2, "label": "Goal 2: Zero hunger" },
{ "value": 3, "label": "Goal 3: Good health and well-being" },
{ "value": 4, "label": "Goal 4: Quality education" },
{ "value": 5, "label": "Goal 5: Gender equality" },
{ "value": 6, "label": "Goal 6: Clean water and sanitation" },
{ "value": 7, "label": "Goal 7: Affordable and clean energy" },
{ "value": 8, "label": "Goal 8: Decent work and economic growth" },
{ "value": 9, "label": "Goal 9: Industry, Innovation, and Infrastructure" },
{ "value": 10, "label": "Goal 10: Reduced inequalities" },
{ "value": 11, "label": "Goal 11: Sustainable cities and communities" },
{ "value": 12, "label": "Goal 12: Responsible consumption and production" },
{ "value": 13, "label": "Goal 13: Climate action" },
{ "value": 14, "label": "Goal 14: Life below water" },
{ "value": 15, "label": "Goal 15: Life on land" },
{ "value": 16, "label": "Goal 16: Peace, justice and strong institutions" },
{ "value": 17, "label": "Goal 17: Partnerships for the goals" }
], true)}
{formFieldFactory.createTextInput("COL0002", "Campo STRING", true)}
{formFieldFactory.createTextAreaInput("COL0003", "Campo TEXTAREA", true)}
{formFieldFactory.createBooleanInput("COL0004", "Campo BOOLEAN", true)}
{formFieldFactory.createRadioInput("COL0005", "Campo RADIO", [
{ "value": 1, "label": "Opzione 1" },
{ "value": 2, "label": "Opzione 2" },
{ "value": 3, "label": "Altra opzione" }
], true)}
{formFieldFactory.createCheckboxInput("COL0006", "Campo CHECKBOX", [
{ "value": 4, "label": "Check 1" },
{ "value": 5, "label": "Check 2" },
{ "value": 6, "label": "Altro check" }
])}
</>
)
});
var elixFormsReactRender = myElixFormsReact.render();
return <>
<div className="container_12" id="pageBody">
<div className="grid_12">
<div id="userConsole" className="fe-behaviour">
<header>
<div className="neutral">
<div className="grid_12">
<div className="console_header">
<div className="leftDiv">
<h1>elixForms</h1>
<div className="logo"><a href="https://www.unipr.it/" title="Torna alla homepage dell'Università: di Parma"><img src="https://console-unipr.elixforms.it/elixFormsCustom/images/logo.png" alt="Logo Università: di Parma" /></a>
</div>
</div>
<div className="rightDiv">
<div className="rightDivInside info padding_r">
<div className="clear"></div>
</div>
</div>
<div className="clear"></div>
</div>
</div>
<div className="clear"></div>
</div>
</header>
<div className="grid_12 console_main">
<div className="inside">
<div>
<div className="fe-module-header-container">
<h2>Modulo A/13 - Richiesta di Certificato</h2>
</div>
</div>
<div className="receipt">
<div className="container">
<h1>Scelta Carriera</h1>
<img src={heroImg} alt="Hero Image" className="hero-image" />
<p>Benvenuto nella piattaforma di scelta carriera! Esplora le tue opzioni e trova la strada giusta per te.</p>
{elixFormsReactRender}
<p>Altre cose</p>
</div>
</div>
</div>
</div>
<div className="clear"></div>
<footer className="it-footer">
<div className="it-footer-main">
<div className="container">
<div className="row clearfix">
<div className="col-sm-12">
<div className="footer-content">
<div className="subfooter-top">powered by <span className="highlight">elixForms</span></div>
<div className="subfooter-bottom"><div>versione 1.24.0</div></div>
</div>
</div>
</div>
</div>
</div>
</footer>
</div>
<div className="clear"></div>
</div>
</div>
</>;
return (
<SceltaCarrieraComponent
moduleName=""
alertInfoMessage=""
userDisplayName="John Doe"
headerTitle="Modulo A/13 - Richiesta di Certificato"
cardTitle="Scelta Carriera"
cardDescription="Benvenuto nella piattaforma di scelta carriera! Esplora le tue opzioni e trova la strada giusta per te."
headerHeroImageSrc={heroImg}
/>
)
}
export default App
@@ -0,0 +1,49 @@
import type { JSX } from 'react';
import ElixFormsComponentAbstract from '@common/src/ElixFormsComponentAbstract';
import type { IElixFormsComponentCustomFormFieldFactory } from '@common/src/IElixFormsComponentCustomFormFieldFactory';
export default class SceltaCarrieraComponent extends ElixFormsComponentAbstract {
protected override createCustomFormFields(formFieldFactory: IElixFormsComponentCustomFormFieldFactory): JSX.Element {
return (
<>
{formFieldFactory.createDropdownInput('COL0015', 'COL0015 Goals', [
{ value: 0, label: 'Not applicable' },
{ value: 1, label: 'Goal 1: No poverty' },
{ value: 2, label: 'Goal 2: Zero hunger' },
{ value: 3, label: 'Goal 3: Good health and well-being' },
{ value: 4, label: 'Goal 4: Quality education' },
{ value: 5, label: 'Goal 5: Gender equality' },
{ value: 6, label: 'Goal 6: Clean water and sanitation' },
{ value: 7, label: 'Goal 7: Affordable and clean energy' },
{ value: 8, label: 'Goal 8: Decent work and economic growth' },
{ value: 9, label: 'Goal 9: Industry, Innovation, and Infrastructure' },
{ value: 10, label: 'Goal 10: Reduced inequalities' },
{ value: 11, label: 'Goal 11: Sustainable cities and communities' },
{ value: 12, label: 'Goal 12: Responsible consumption and production' },
{ value: 13, label: 'Goal 13: Climate action' },
{ value: 14, label: 'Goal 14: Life below water' },
{ value: 15, label: 'Goal 15: Life on land' },
{ value: 16, label: 'Goal 16: Peace, justice and strong institutions' },
{ value: 17, label: 'Goal 17: Partnerships for the goals' }
], true)}
{formFieldFactory.createTextInput('COL0002', 'Campo STRING', true)}
{formFieldFactory.createTextAreaInput('COL0003', 'Campo TEXTAREA', true)}
{formFieldFactory.createBooleanInput('COL0004', 'Campo BOOLEAN', true)}
{formFieldFactory.createRadioInput('COL0005', 'Campo RADIO', [
{ value: 1, label: 'Opzione 1' },
{ value: 2, label: 'Opzione 2' },
{ value: 3, label: 'Altra opzione' }
], true)}
{formFieldFactory.createCheckboxInput('COL0006', 'Campo CHECKBOX', [
{ value: 4, label: 'Check 1' },
{ value: 5, label: 'Check 2' },
{ value: 6, label: 'Altro check' }
])}
</>
);
}
protected override renderExtraContentPost(): JSX.Element {
return <div className="alert alert-info mt-4">Questa pagina è stata ristrutturata con i componenti Bootstrap Italia.</div>;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"files": [],
//"files": [],
"references": [
{
"path": "./common"
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command dump-autoload -Arguments @('--optimize')
+2
View File
@@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'composer-run-command.ps1') -Command install -Arguments @('--optimize-autoloader')
+47
View File
@@ -0,0 +1,47 @@
param(
[Parameter(Mandatory)][string]$Command,
[string[]]$Arguments = @()
)
$ErrorActionPreference = 'Stop'
$root = (Resolve-Path (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..')).Path
$useDockerComposer = $false
$dockerRoot = $null
if (-not (Get-Command composer -ErrorAction SilentlyContinue)) {
Write-Host "Composer non trovato localmente. Verifico se Docker è disponibile..."
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "Composer non trovato e Docker mancante! Installa Composer o Docker prima di continuare."
}
$dockerRoot = $root -replace '\\', '/'
if ($dockerRoot -match '^([A-Za-z]):') {
$dockerRoot = "/$($Matches[1].ToLower())$($dockerRoot.Substring(2))"
}
Write-Host "Docker trovato. Verifico Composer tramite container..."
docker run --rm -v "$($dockerRoot):/app" -w /app composer:2 composer --version 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Docker presente, ma Composer non riesce ad avviarsi tramite container. Installa Composer localmente o verifica Docker."
}
$useDockerComposer = $true
}
Push-Location $root
try {
if ($useDockerComposer) {
docker run --rm -v "$($dockerRoot):/app" -w /app composer:2 composer $Command @Arguments
}
else {
composer $Command @Arguments
}
if ($LASTEXITCODE -ne 0) {
throw "Composer ha restituito il codice di uscita $LASTEXITCODE."
}
}
finally {
Pop-Location
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use ElixForms\Common\HttpClient;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
final class HttpClientTest extends TestCase
{
public function testGetUsesQueryHeadersAuthenticationAndTimeout(): void
{
$history = [];
$stack = HandlerStack::create(new MockHandler([new Response(200, [], '{"ok":true}')]));
$stack->push(Middleware::history($history));
$client = new HttpClient(
['Accept' => 'application/json', 'X-Default' => 'default'],
new Client(['handler' => $stack])
);
$client->setBasicAuth('user', 'secret')->setTimeout(5);
$result = $client->get(
'https://example.test/resource',
['term' => 'Mario Rossi'],
['X-Default' => 'override']
);
self::assertSame('{"ok":true}', $result);
self::assertCount(1, $history);
self::assertSame('GET', $history[0]['request']->getMethod());
self::assertSame('term=Mario%20Rossi', $history[0]['request']->getUri()->getQuery());
self::assertSame('application/json', $history[0]['request']->getHeaderLine('Accept'));
self::assertSame('override', $history[0]['request']->getHeaderLine('X-Default'));
self::assertSame(['user', 'secret'], $history[0]['options']['auth']);
self::assertSame(5, $history[0]['options']['timeout']);
self::assertSame(5, $history[0]['options']['connect_timeout']);
}
public function testPostEncodesArrayAsFormData(): void
{
$history = [];
$stack = HandlerStack::create(new MockHandler([new Response(200, [], 'saved')]));
$stack->push(Middleware::history($history));
$client = new HttpClient([], new Client(['handler' => $stack]));
self::assertSame('saved', $client->post('https://example.test/resource', ['name' => 'Mario Rossi']));
self::assertSame('POST', $history[0]['request']->getMethod());
self::assertSame('name=Mario+Rossi', (string)$history[0]['request']->getBody());
}
public function testHttpErrorsAreExposedWithoutResponseBody(): void
{
$client = new HttpClient([], new Client([
'handler' => HandlerStack::create(new MockHandler([
new Response(503, [], 'sensitive upstream response'),
])),
]));
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Richiesta fallita con codice di stato HTTP 503.');
$client->get('https://example.test/resource');
}
public function testTimeoutMustBePositive(): void
{
$this->expectException(\InvalidArgumentException::class);
(new HttpClient())->setTimeout(0);
}
}