feat: A20 phase 3 LOT 2 — settings + local workspace verts en CSP preview (v7.39.0)
Ajout : - 2 gates csp_preview de plus : settings (composant lie, overlay visible) et local workspace (recherche focalisee via Alpine.nextTick, chips filtre en SVG via bindSvg, 0 erreur) → 3 surfaces vertes sous build CSP : library, settings, local workspace. Changed : - settings : window.history.back()/new Date(...) → methodes historyBack/fmtLastLogin/fmtAuditDate ; ?. → ternaires. - local workspace : x-data="_wsInitData" → registre wsInitData() ; 14 x-html → x-init + Alpine.effect (bindSvg/bindFileIcon/bindNodeIcon/ bindChildren/bindPreview) ; $nextTick+$refs arrow → toggleSearch() ; window.FlowDeck.* → createPageAt/createFolderAt ; ?. → ternaires ; @contextmenu="_wsInitData.*" → appel de methode. Piesges resolus (CHANGELOG en details) : - snapshot ji du build CSP = valeurs globalThis au boot → l'objet mis sur window avant Alpine est banni (« Accessing global variables ») → objet porte par une CONST LEXICALE (non propriete globalThis) + factory Alpine.data → MEME objet partage, reactivite intacte. - bloc preview hors div racine (structure pre-existante, masquee par le fallback window standard) → composant wsPreview DELEGUANT vers _wsInitData via Alpine.reactive (wrapper unique : les magics $nextTick ne sont redefinissables qu'une fois). - .env local : RATE_LIMIT_REQUESTS=600 (rafales E2E vs 60/min par IP ; defaut produit inchange). suite **1093/1093** · ruff OK · eslint 0/0 · E2E **5/5** (3 csp_preview + 2 smoke) · CSP preview ET standard = 0 erreur sur /local-workspace · docs a jour
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
{% block content %}
|
||||
<span id="fd-local-ws-id" hidden>{{ workspace_id }}</span>
|
||||
<div x-data="_wsInitData"
|
||||
<div x-data="wsInitData()"
|
||||
@dragover.prevent="onDragOver($event)"
|
||||
@dragleave="onDragLeave($event)"
|
||||
@drop.prevent="onDrop($event)"
|
||||
@@ -482,10 +482,10 @@
|
||||
|
||||
<script type="application/json" id="lw-config" nonce="{{ csp_nonce() }}">{{ {"current_folder_id": current_folder_id, "workspace_id": workspace_id} | tojson }}</script>
|
||||
<script data-cfasync="false" src="/static/js/local_workspace.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">console.log('FLOWDECK v2.4.6 — ws-split about to render, _wsInitData exists:', typeof window._wsInitData !== 'undefined');</script>
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">console.log('FLOWDECK v2.4.6 — ws-split about to render, _wsInitData exists:', typeof _wsInitData !== 'undefined');</script>
|
||||
|
||||
<div class="ws-split"
|
||||
@contextmenu.prevent="_wsInitData.onContextMenu($event)"
|
||||
@contextmenu.prevent="onContextMenu($event)"
|
||||
@touchstart="onTouchStart($event)"
|
||||
@touchend="onTouchEnd($event)"
|
||||
@touchmove="onTouchMove($event)"
|
||||
@@ -597,7 +597,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Search toggle -->
|
||||
<button class="ws-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="searchOpen=!searchOpen; if(searchOpen) $nextTick(()=>$refs.searchInput?.focus())">
|
||||
<button class="ws-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="toggleSearch()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<!-- Expand/Collapse (tree only) -->
|
||||
@@ -650,12 +650,12 @@
|
||||
|
||||
<!-- Filter chips (visual indicator when filter is active — shown in all views) -->
|
||||
<div class="filter-row" x-show="filterType" style="padding:4px 0;margin-bottom:6px;">
|
||||
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders" x-html="getSvgIcon('folder',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages" x-html="getSvgIcon('file',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF" x-html="getSvgIcon('file',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images" x-html="getSvgIcon('image',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code" x-html="getSvgIcon('file',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text" x-html="getSvgIcon('file',14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders" x-init="bindSvg($el, 'folder', 14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages" x-init="bindSvg($el, 'file', 14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF" x-init="bindSvg($el, 'file', 14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images" x-init="bindSvg($el, 'image', 14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code" x-init="bindSvg($el, 'file', 14)"></button>
|
||||
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text" x-init="bindSvg($el, 'file', 14)"></button>
|
||||
<span class="filter-clear" @click="filterType='', doFilter()">clear filter</span>
|
||||
</div>
|
||||
|
||||
@@ -723,7 +723,7 @@
|
||||
<span style="width:16px;flex-shrink:0;"></span>
|
||||
</template>
|
||||
|
||||
<span class="icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span class="icon" x-init="bindFileIcon($el, node)"></span>
|
||||
|
||||
<template x-if="node.is_folder">
|
||||
<span class="name folder" @click.stop="navigateToFolder(node.id)"
|
||||
@@ -754,8 +754,8 @@
|
||||
<div class="actions">
|
||||
<template x-if="node.is_folder">
|
||||
<span style="display:flex;gap:2px">
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.createPage(node.id)" title="New file">{{ fd_icon("file",14) }}</button>
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.showCreateFolderModal(node.id)" title="New folder">{{ fd_icon("folder",14) }}</button>
|
||||
<button class="ws-act" @click.stop="createPageAt(node)" title="New file">{{ fd_icon("file",14) }}</button>
|
||||
<button class="ws-act" @click.stop="createFolderAt(node)" title="New folder">{{ fd_icon("folder",14) }}</button>
|
||||
<a class="ws-act" href="/local-workspace" title="Open workspace page" style="text-decoration:none;display:inline-flex;align-items:center;">{{ fd_icon("external-link",14) }}</a>
|
||||
</span>
|
||||
</template>
|
||||
@@ -765,7 +765,7 @@
|
||||
</div>
|
||||
<!-- Children -->
|
||||
<ul class="ws-tree" x-show="expanded[node.id]" x-transition
|
||||
x-html="renderChildren(node.children, (node.depth||0)+1, tagsVersion)">
|
||||
x-init="bindChildren($el, node)">
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@@ -807,7 +807,7 @@
|
||||
<tr :class="{ selected: !!selectedIds[node.id] }">
|
||||
<td><input type="checkbox" :checked="!!selectedIds[node.id]" @click.stop="toggleSelect(node, $event)"></td>
|
||||
<td>
|
||||
<span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span class="table-icon" x-init="bindFileIcon($el, node)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span class="col-name" @click="node.is_folder ? navigateToFolder(node.id) : openPage(node.id)" x-text="node.name"></span>
|
||||
</td>
|
||||
@@ -864,7 +864,7 @@
|
||||
</tr>
|
||||
<template x-for="node in displayTree" :key="'l'+node.id">
|
||||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer">
|
||||
<td><span x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<td><span x-init="bindFileIcon($el, node)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||||
<td class="col-name" x-text="node.name"></td>
|
||||
<td style="font-size:12px;color:var(--text-tertiary)" x-text="node.is_folder ? 'Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
|
||||
@@ -902,7 +902,7 @@
|
||||
<template x-for="node in displayTree" :key="'d'+node.id">
|
||||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer"
|
||||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||||
<td><span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<td><span class="table-icon" x-init="bindFileIcon($el, node)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||||
<td class="col-name" x-text="node.name"></td>
|
||||
<td class="col-path" x-text="node._path || '—'"></td>
|
||||
@@ -933,7 +933,7 @@
|
||||
<template x-for="node in displayTree" :key="'g'+node.id">
|
||||
<div class="title-card" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||||
:class="{ 'selected-card': previewItem && previewItem.id === node.id }">
|
||||
<div class="title-card-icon" x-html="node.is_folder ? getSvgIcon('folder',24) : _fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></div>
|
||||
<div class="title-card-icon" x-init="bindNodeIcon($el, node)"></div>
|
||||
<div class="title-card-name">
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span x-text="node.name"></span>
|
||||
@@ -960,7 +960,7 @@
|
||||
<div class="content-item" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||||
<div class="content-item-header">
|
||||
<span x-html="_fileIcon(node.name, node.is_folder, node.content_format, node.page_icon)"></span>
|
||||
<span x-init="bindFileIcon($el, node)"></span>
|
||||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||||
<span class="col-name" x-text="node.name"></span>
|
||||
<span style="font-size:11px;color:var(--text-tertiary);margin-left:auto" x-text="_formatDate(node.updated_at)"></span>
|
||||
@@ -1023,7 +1023,7 @@
|
||||
<h3><span x-show="createType==='folder'">{{ fd_icon('folder',16) }}</span><span x-show="createType!='folder'">{{ fd_icon('file',16) }}</span> <span x-text="createType==='folder'?'New Folder':'New File'"></span></h3>
|
||||
<p class="modal-sub">
|
||||
In <strong>{{ workspace_name }}</strong> <span x-show="folderStack.length>1">/ <span x-text="(folderStack[folderStack.length-1]||{}).name||''"></span></span>
|
||||
<span x-show="parentFolder"> / <span x-text="parentFolder?.name"></span></span>
|
||||
<span x-show="parentFolder"> / <span x-text="parentFolder ? parentFolder.name : ''"></span></span>
|
||||
</p>
|
||||
<input class="modal-input" x-model="newName" :placeholder="createType==='folder'?'Folder name':'File name'" @keydown.enter="doCreate" x-ref="cinp">
|
||||
<div class="modal-actions">
|
||||
@@ -1037,7 +1037,7 @@
|
||||
<div class="modal-overlay" x-show="showRename" @click.outside="showRename=false" @keydown.escape="showRename=false" style="display:none">
|
||||
<div class="modal-box">
|
||||
<h3>{{ fd_icon("edit",14) }} Rename</h3>
|
||||
<p class="modal-sub">Rename <strong x-text="target?.name"></strong></p>
|
||||
<p class="modal-sub">Rename <strong x-text="target ? target.name : ''"></strong></p>
|
||||
<input class="modal-input" x-model="newName" @keydown.enter="doRename" x-ref="rinp">
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" @click="showRename=false">Cancel</button>
|
||||
@@ -1050,7 +1050,7 @@
|
||||
<div class="modal-overlay" x-show="showDelete" @click.outside="showDelete=false" @keydown.escape="showDelete=false" style="display:none">
|
||||
<div class="modal-box">
|
||||
<h3>{{ fd_icon("trash",16) }} Delete</h3>
|
||||
<p class="modal-sub">Delete <strong x-text="target?.name"></strong>?</p>
|
||||
<p class="modal-sub">Delete <strong x-text="target ? target.name : ''"></strong>?</p>
|
||||
<div class="delete-confirm">This cannot be undone. Children will also be deleted.</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" @click="showDelete=false">Cancel</button>
|
||||
@@ -1065,11 +1065,14 @@
|
||||
{% include "_ctx_menu.html" %}
|
||||
|
||||
<!-- Preview tooltip -->
|
||||
<div class="file-preview" x-show="previewVisible"
|
||||
<!-- ponytail: le parseur referme la div racine avant ce bloc (structure
|
||||
pré-existante, masquée par le fallback window d'Alpine standard) →
|
||||
wsPreview = wrapper déléguant vers _wsInitData (voir local_workspace.js) -->
|
||||
<div class="file-preview" x-data="wsPreview()" x-show="previewVisible"
|
||||
:style="{ left: previewX + 'px', top: previewY + 'px' }"
|
||||
@mouseenter="previewVisible = true" @mouseleave="previewVisible = false">
|
||||
<div class="file-preview-header" x-text="previewName"></div>
|
||||
<div class="file-preview-body" x-html="previewContent"></div>
|
||||
<div class="file-preview-body" x-init="bindPreview($el)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Panel — identical to Library peek-overlay -->
|
||||
|
||||
Reference in New Issue
Block a user