diff --git a/TODO.md b/TODO.md index d8f8465..3265c4c 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,19 @@ - [x] Fix `$enabledAccessTypes` undefined / `array_map()` TypeError on edit page — controller was fetching `getAccessTypes()` instead of `getEnabledFormAccessTypes()` and returning it under the wrong key +## Form help blocks — sortable admin UI + +- [x] Migration 005: add `sort_order` column to `form_help_blocks` +- [x] `Database::getAllFormHelpBlocks()` — ORDER BY sort_order, expose sort_order in returned data +- [x] `Database::reorderFormHelpBlocks(array $keys)` — persist new order +- [x] `actions/form-help-reorder.php` — HTMX POST handler (CSRF-protected, 204 response) +- [x] `templates/admin/contenus.php` — replace table with two-panel layout: + - Left: SortableJS + htmx drag-and-drop card list + - Right: static form structure reference (fieldsets + inputs) +- [x] CSS in admin.css: `.fhb-*` classes for layout, cards, ghost/chosen/drag states +- [x] `schema.sql` — updated `form_help_blocks` DDL with `sort_order` +- [x] Vendor SortableJS 1.15.2 into `assets/js/sortable.min.js` (remove CDN dependency) + ## CSS refactor - [x] Move semantic HTML element baseline styles into common.css diff --git a/app/migrations/applied/005_form_help_blocks_sort_order.sql b/app/migrations/applied/005_form_help_blocks_sort_order.sql new file mode 100644 index 0000000..4a26b87 --- /dev/null +++ b/app/migrations/applied/005_form_help_blocks_sort_order.sql @@ -0,0 +1,12 @@ +-- Add sort_order to form_help_blocks for drag-and-drop reordering in admin. +ALTER TABLE form_help_blocks ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; + +-- Assign initial order matching the canonical FORM_HELP_KEYS array index. +UPDATE form_help_blocks SET sort_order = 0 WHERE key = 'partage_intro'; +UPDATE form_help_blocks SET sort_order = 1 WHERE key = 'fieldset_tfe_info'; +UPDATE form_help_blocks SET sort_order = 2 WHERE key = 'fieldset_synopsis'; +UPDATE form_help_blocks SET sort_order = 3 WHERE key = 'fieldset_jury'; +UPDATE form_help_blocks SET sort_order = 4 WHERE key = 'fieldset_academic'; +UPDATE form_help_blocks SET sort_order = 5 WHERE key = 'fieldset_files'; +UPDATE form_help_blocks SET sort_order = 6 WHERE key = 'fieldset_access'; +UPDATE form_help_blocks SET sort_order = 7 WHERE key = 'fieldset_email'; diff --git a/app/public/admin/actions/form-help-reorder.php b/app/public/admin/actions/form-help-reorder.php new file mode 100644 index 0000000..9c2418a --- /dev/null +++ b/app/public/admin/actions/form-help-reorder.php @@ -0,0 +1,47 @@ +reorderFormHelpBlocks($keys); +} catch (Exception $e) { + error_log('form-help-reorder error: ' . $e->getMessage()); + http_response_code(500); + echo 'Erreur lors de la sauvegarde.'; + exit; +} + +http_response_code(204); +exit; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index d5380db..d586ae5 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -1589,3 +1589,236 @@ color: var(--text-secondary); font-style: italic; } + +/* ═══════════════════════════════════════════════════════════════════════════ + Form Help Blocks — drag-and-drop builder (contenus.php) + ═══════════════════════════════════════════════════════════════════════════ */ + +.fhb-hint { + color: var(--text-secondary); + font-size: var(--step--1); + margin-bottom: var(--space-m); +} + +.fhb-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-m); + align-items: start; + margin-top: var(--space-m); +} + +@media (max-width: 800px) { + .fhb-layout { + grid-template-columns: 1fr; + } +} + +/* ── Panels ─────────────────────────────────────────────────────────────── */ + +.fhb-sortable-panel, +.fhb-form-preview-panel { + border: 1px solid var(--border-primary); + border-radius: 6px; + padding: var(--space-s); + background: var(--bg-secondary); +} + +.fhb-panel-title { + font-size: var(--step-0); + font-weight: 600; + margin: 0 0 var(--space-3xs) 0; + letter-spacing: 0.03em; +} + +.fhb-panel-desc { + font-size: var(--step--2); + color: var(--text-secondary); + margin: 0 0 var(--space-xs) 0; +} + +/* ── Saving indicator ─────────────────────────────────────────────────────── */ + +.fhb-saving { + display: none; + align-items: center; + gap: var(--space-2xs); + font-size: var(--step--1); + color: var(--accent-primary); + padding: var(--space-2xs) 0; +} + +.fhb-saving.htmx-request { + display: flex; +} + +/* ── Draggable block cards ─────────────────────────────────────────────────── */ + +.fhb-sortable { + display: flex; + flex-direction: column; + gap: var(--space-2xs); + padding: 0; + margin: 0; +} + +.fhb-block-card { + display: flex; + align-items: center; + gap: var(--space-xs); + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-left: 4px solid var(--accent-primary); + border-radius: 4px; + padding: var(--space-2xs) var(--space-xs); + cursor: default; + transition: box-shadow 0.15s, border-color 0.15s; +} + +.fhb-block-card:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.08); + border-color: var(--accent-primary); +} + +.fhb-drag-handle { + font-size: 1.2em; + color: var(--text-tertiary); + cursor: grab; + flex-shrink: 0; + line-height: 1; + user-select: none; + padding: 2px 4px; +} + +.fhb-drag-handle:active { + cursor: grabbing; +} + +.fhb-block-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.fhb-block-label { + font-size: var(--step--1); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fhb-block-preview { + font-size: var(--step--2); + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fhb-block-empty { + font-size: var(--step--2); + color: var(--text-tertiary); + font-style: italic; +} + +.fhb-edit-btn { + flex-shrink: 0; + font-size: var(--step--2) !important; + padding: 2px var(--space-xs) !important; +} + +/* ── SortableJS state classes ─────────────────────────────────────────────── */ + +.fhb-ghost { + opacity: 0.35; + background: var(--accent-muted); + border-color: var(--accent-primary); +} + +.fhb-chosen { + box-shadow: 0 4px 16px rgba(149, 87, 181, 0.25); + border-color: var(--accent-primary); +} + +.fhb-dragging { + opacity: 0.9; + box-shadow: 0 8px 24px rgba(0,0,0,0.15); +} + +/* ── Form structure preview (right panel) ─────────────────────────────────── */ + +.fhb-form-preview { + display: flex; + flex-direction: column; + gap: var(--space-2xs); +} + +.fhb-fieldset-preview { + border: 1px solid var(--border-secondary); + border-radius: 4px; + padding: var(--space-xs); + background: var(--bg-primary); +} + +.fhb-fieldset-legend { + font-size: var(--step--1); + font-weight: 600; + color: var(--text-primary); + margin-bottom: var(--space-3xs); + padding-bottom: var(--space-3xs); + border-bottom: 1px solid var(--border-primary); +} + +.fhb-fieldset-inputs { + margin: 0; + padding: 0 0 0 var(--space-s); + list-style: disc; +} + +.fhb-fieldset-inputs li { + font-size: var(--step--2); + color: var(--text-secondary); + line-height: 1.6; +} + +.fhb-anchor { + display: flex; + align-items: center; + gap: var(--space-2xs); + border-radius: 4px; + padding: var(--space-3xs) var(--space-xs); + font-size: var(--step--2); + border: 1px dashed var(--border-primary); + background: transparent; +} + +.fhb-anchor--filled { + border-color: var(--accent-primary); + background: var(--accent-muted); + color: var(--accent-secondary); +} + +.fhb-anchor--empty { + color: var(--text-tertiary); +} + +.fhb-anchor-icon { + flex-shrink: 0; + font-style: normal; +} + +.fhb-anchor-label { + flex: 1; +} + +.fhb-anchor-pos { + font-size: var(--step--2); + font-weight: 600; + color: var(--accent-primary); + background: var(--accent-muted); + border-radius: 2px; + padding: 0 4px; +} diff --git a/app/public/assets/js/sortable.min.js b/app/public/assets/js/sortable.min.js new file mode 100644 index 0000000..bb99533 --- /dev/null +++ b/app/public/assets/js/sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.2 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(m," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(m," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Bt(t){V&&V.parentNode[K]._isOutsideThisEl(t.target)}function Ft(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Pt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ft.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(n in W.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in kt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Nt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),Dt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function jt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Ht(t){t.draggable=!1}function Lt(){Tt=!1}function Kt(t){return setTimeout(t,0)}function Wt(t){return clearTimeout(t)}Ft.prototype={constructor:Ft,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(mt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,V):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){xt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&xt.push(o)}}(o),!V&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||tt===l)){if(ot=j(l),rt=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return q({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),G("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return q({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),G("filter",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!V&&n.parentNode===r&&(o=X(n),Q=r,Z=(V=n).parentNode,J=V.nextSibling,tt=n,lt=a.group,ct={target:Ft.dragged=V,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ct.clientX-o.left,pt=ct.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,V.style["will-change"]="all",o=function(){G("delayEnded",i,{evt:t}),Ft.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(V.draggable=!0),i._triggerDragStart(t,e),q({sortable:i,name:"choose",originalEvent:t}),k(V,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){b(V,t.trim(),Ht)}),h(l,"dragover",Yt),h(l,"mousemove",Yt),h(l,"touchmove",Yt),h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,V.draggable=!0),G("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ft.eventCanceled?this._onDrop():(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){V&&Ht(V),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,"mouseup",this._disableDelayedDrag),f(t,"touchend",this._disableDelayedDrag),f(t,"touchcancel",this._disableDelayedDrag),f(t,"mousemove",this._delayedDragTouchMoveHandler),f(t,"touchmove",this._delayedDragTouchMoveHandler),f(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(V,"dragend",this),h(Q,"dragstart",this._onDragStart));try{document.selection?Kt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;wt=!1,Q&&V?(G("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Bt),n=this.options,t||k(V,n.dragClass,!1),k(V,n.ghostClass,!0),Ft.active=this,t&&this._appendGhost(),q({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ut){this._lastX=ut.clientX,this._lastY=ut.clientY,Rt();for(var t=document.elementFromPoint(ut.clientX,ut.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ut.clientX,ut.clientY))!==e;)e=t;if(V.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ut.clientX,clientY:ut.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Xt()}},_onTouchMove:function(t){if(ct){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=$&&v($,!0),a=$&&r&&r.a,l=$&&r&&r.d,e=Mt&&yt&&E(yt),a=(i.clientX-ct.clientX+o.x)/(a||1)+(e?e[0]-Ct[0]:0)/(a||1),l=(i.clientY-ct.clientY+o.y)/(l||1)+(e?e[1]-Ct[1]:0)/(l||1);if(!Ft.active&&!wt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))D.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>D.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,$),e?t.clientX<_.left-10||t.clientY ['content' => ..., 'updated_at' => ...] ]. + * Return all form help blocks ordered by sort_order, as [ key => ['content' => ..., 'updated_at' => ..., 'sort_order' => ...] ]. */ public function getAllFormHelpBlocks(): array { $stmt = $this->pdo->query( - "SELECT key, content, updated_at FROM form_help_blocks ORDER BY key" + "SELECT key, content, updated_at, sort_order FROM form_help_blocks ORDER BY sort_order, key" ); $rows = $stmt->fetchAll(); $out = []; foreach ($rows as $r) { - $out[$r['key']] = ['content' => $r['content'], 'updated_at' => $r['updated_at']]; + $out[$r['key']] = [ + 'content' => $r['content'], + 'updated_at' => $r['updated_at'], + 'sort_order' => (int)$r['sort_order'], + ]; } return $out; } + /** + * Persist a new sort order for all form help blocks. + * $keys must be an ordered array of known block keys. + */ + public function reorderFormHelpBlocks(array $keys): void { + $stmt = $this->pdo->prepare( + "UPDATE form_help_blocks SET sort_order = ? WHERE key = ?" + ); + foreach ($keys as $i => $key) { + if (in_array($key, self::FORM_HELP_KEYS, true)) { + $stmt->execute([$i, $key]); + } + } + } + // ======================================================================== // SINGLETON PATTERN ENFORCEMENT // ======================================================================== diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index 99ecd42..41a3a9e 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -69,37 +69,158 @@ +

Blocs d'aide du formulaire étudiant·e

-

Ces textes apparaissent dans le formulaire de soumission accessible via les liens de partage. Ils permettent d'expliquer aux étudiant·es comment remplir chaque section. Supporte le Markdown.

+

Ces textes apparaissent dans le formulaire de soumission accessible via les liens de partage. + Ils permettent d'expliquer aux étudiant·es comment remplir chaque section. Supporte le Markdown.

+

+ Glissez les blocs d'aide (cartes violettes) pour les réorganiser dans le formulaire. + Cliquez sur Éditer pour modifier le contenu d'un bloc. + L'ordre est sauvegardé automatiquement après chaque déplacement. +

+ + $block) { + $orderedBlocks[] = array_merge($block, [ + 'key' => $key, + 'label' => Database::FORM_HELP_LABELS[$key] ?? $key, + ]); + } + + // Static form structure: each item is either a 'fieldset' (visual container) + // or an 'anchor' for a specific block key showing where it sits in the form. + // We also need a mapping from block key → where it currently sits in the sorted list. + // The entire sorted order is what matters; we render the form structure as a visual + // reference alongside the sortable list. + $formStructure = [ + ['type' => 'anchor', 'key' => 'partage_intro', 'position' => 'before-form', 'label' => 'Avant le formulaire (introduction)'], + ['type' => 'fieldset', 'name' => 'Informations du TFE', 'inputs' => ['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact', 'Synopsis']], + ['type' => 'anchor', 'key' => 'fieldset_tfe_info', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Informations du TFE »'], + ['type' => 'anchor', 'key' => 'fieldset_synopsis', 'position' => 'intro-fieldset', 'label' => 'Note sous le champ Synopsis'], + ['type' => 'fieldset', 'name' => 'Composition du jury', 'inputs' => ['Président·e', 'Promoteur·ice', 'Lecteur·ices (×4)']], + ['type' => 'anchor', 'key' => 'fieldset_jury', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Jury »'], + ['type' => 'fieldset', 'name' => 'Cadre académique', 'inputs' => ['Année', 'Orientation', 'AP', 'Finalité', 'Langues', 'Formats', 'Mots-clés']], + ['type' => 'anchor', 'key' => 'fieldset_academic', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Cadre académique »'], + ['type' => 'fieldset', 'name' => 'Fichiers', 'inputs' => ['Fichier principal (PDF)', 'Annexes']], + ['type' => 'anchor', 'key' => 'fieldset_files', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Fichiers »'], + ['type' => 'fieldset', 'name' => 'Visibilité / Accès', 'inputs' => ["Type d'accès", 'Licence']], + ['type' => 'anchor', 'key' => 'fieldset_access', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « Visibilité »'], + ['type' => 'fieldset', 'name' => 'E-mail de confirmation', 'inputs' => ['Adresse e-mail']], + ['type' => 'anchor', 'key' => 'fieldset_email', 'position' => 'intro-fieldset', 'label' => 'Intro du fieldset « E-mail »'], + ]; + ?> + +
+ + +
+

Ordre des blocs

+

Glissez pour réorganiser

+ +
+ +
Sauvegarde…
+ + +
+ + +
+ + + + + — vide — + +
+ Éditer +
+ +
+
+ + +
+

Structure du formulaire

+

Référence visuelle — non modifiable

+ +
+ + + '', 'sort_order' => 99]; + $hasContent = trim($bData['content'] ?? '') !== ''; + ?> +
+ + + + # + +
+ +
+
+
    + +
  • + +
+
+ + +
+
+ +
- - - - - - - - - - - - '', 'updated_at' => null]; - $label = Database::FORM_HELP_LABELS[$key] ?? $key; - $preview = $block['content'] !== '' - ? mb_strimwidth($block['content'], 0, 80, '…') - : '— vide —'; - ?> - - - - - - - - -
BlocAperçuMis à jourAction
- Éditer -
+ +