feat: A20 phase 3 LOT 1 — shell + library migres, harnais csp_preview vert (v7.38.0)
Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
This commit is contained in:
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.37.0",
|
||||
version="7.38.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: accountsData »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('accountsData', accountsData); });
|
||||
function accountsData() {
|
||||
return {
|
||||
profile: { full_name: '', email: '' },
|
||||
|
||||
+51
-14
@@ -188,8 +188,8 @@
|
||||
:class="{ collapsed: sidebarCollapsed && !sidebarPeek, 'mobile-open': mobileSidebarOpen, peeking: sidebarPeek }"
|
||||
id="sidebar"
|
||||
@mouseleave="!sidebarResizing && (sidebarPeek = false)"
|
||||
x-effect="document.documentElement.classList.toggle('fd-sidebar-collapsed', sidebarCollapsed && !sidebarPeek)"
|
||||
x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
|
||||
x-effect="syncSidebarClass()"
|
||||
x-init="initSidebarSort()"
|
||||
<!-- Header + dropdown wrapper -->
|
||||
<div style="position:relative;">
|
||||
<div class="sidebar-workspace-header" @click="workspaceMenuOpen = !workspaceMenuOpen" @mouseenter="wsHeaderHover=true" @mouseleave="wsHeaderHover=false"
|
||||
@@ -272,8 +272,8 @@
|
||||
</div>
|
||||
{% if has_active_workspace %}
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" title="New Page" @click.stop="window.FlowDeck.createPage()">{{ fd_icon("file",16) }}</button>
|
||||
<button class="section-action-btn" title="New Folder" @click.stop="window.FlowDeck.showCreateFolderModal()">{{ fd_icon("folder",16) }}</button>
|
||||
<button class="section-action-btn" title="New Page" @click.stop="fdCreatePage()">{{ fd_icon("file",16) }}</button>
|
||||
<button class="section-action-btn" title="New Folder" @click.stop="fdCreateFolder()">{{ fd_icon("folder",16) }}</button>
|
||||
<a class="section-action-btn" href="/local-workspace" title="Open workspace page" style="text-decoration:none;">{{ fd_icon("external-link",16) }}</a>
|
||||
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'workspace')" @contextmenu.stop="openSectionMenu($event, 'workspace')">⋮</button>
|
||||
</div>
|
||||
@@ -308,7 +308,7 @@
|
||||
<span class="section-label">Repository (Gitea)</span>
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" title="Refresh" @click.stop="if(window._gwData)window._gwData.refreshTree()">{{ fd_icon("refresh",16) }}</button>
|
||||
<button class="section-action-btn" title="Refresh" @click.stop="fdGwRefresh()">{{ fd_icon("refresh",16) }}</button>
|
||||
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'gitea')">⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,7 +330,7 @@
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
{% if has_active_workspace %}
|
||||
<button class="section-action-btn" title="Add" @click.stop="window.FlowDeck.createPage()">+</button>
|
||||
<button class="section-action-btn" title="Add" @click.stop="fdCreatePage()">+</button>
|
||||
{% endif %}
|
||||
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'meetings')">⋮</button>
|
||||
</div>
|
||||
@@ -407,7 +407,7 @@
|
||||
<ul class="sidebar-items" data-section="agents">
|
||||
<template x-for="a in agentList" :key="a.id">
|
||||
<li class="sidebar-item" @click="agentOpen(a.id)">
|
||||
<span class="page-icon page-icon-svg" x-html="a.icon || '🤖'"></span>
|
||||
<span class="page-icon page-icon-svg" x-init="bindAgentIcon($el, a)"></span>
|
||||
<span class="page-name" x-text="a.name"></span>
|
||||
</li>
|
||||
</template>
|
||||
@@ -541,12 +541,7 @@
|
||||
<button class="scp-done" @click="toggleCustomize()">Done</button>
|
||||
</div>
|
||||
<div class="scp-list">
|
||||
<template x-for="(cfg, key) in (Object.keys(sidebarConfig).length ? sidebarConfig : {
|
||||
workspace:{visible:true,order:0},teamspaces:{visible:true,order:1},
|
||||
meetings:{visible:true,order:2},recents:{visible:true,order:3},
|
||||
favorites:{visible:true,order:4},agents:{visible:true,order:5},
|
||||
shared:{visible:true,order:6},published:{visible:true,order:7}
|
||||
})" :key="key">
|
||||
<template x-for="(cfg, key) in sidebarSections()" :key="key">
|
||||
<div class="scp-item" @click="toggleSectionVisibility(key)">
|
||||
<div class="scp-item-left">
|
||||
<span class="scp-item-icon" x-text="getSectionIcon(key)"></span>
|
||||
@@ -595,7 +590,7 @@
|
||||
<!-- Section options menu (⋮ dropdown) -->
|
||||
<div class="section-menu-overlay" x-show="sectionMenu.visible" @click="closeSectionMenu()" @contextmenu.prevent="closeSectionMenu()"></div>
|
||||
<div class="section-menu" x-show="sectionMenu.visible" x-cloak
|
||||
:style="{ top: sectionMenu.y + 'px', left: Math.min(sectionMenu.x, window.innerWidth - 220) + 'px' }"
|
||||
:style="sectionMenuPos()"
|
||||
@click.outside="closeSectionMenu()">
|
||||
<div class="section-menu-item" @click="closeSectionMenu(); setShowCount(sectionMenu.section, 5)">
|
||||
<span class="smi-label">Show 5 items</span>
|
||||
@@ -1127,6 +1122,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
// A20 phase 3 : le build CSP ne résout que le registre Alpine.data
|
||||
// (probe : globale window → « Undefined variable: appState »).
|
||||
document.addEventListener('alpine:init', function () {
|
||||
Alpine.data('appState', appState);
|
||||
});
|
||||
|
||||
function appState() {
|
||||
return {
|
||||
init() {
|
||||
@@ -1568,6 +1569,42 @@
|
||||
else window.location.href = url;
|
||||
},
|
||||
|
||||
// ── A20 phase 3 : expressions → méthode (build CSP : appel seul ;
|
||||
// document/Math/window/FlowsDeck = JS réel, hors évaluateur) ──
|
||||
bindProjectIcon(el, p) {
|
||||
Alpine.effect(() => { el.innerHTML = getSvgIcon(p.icon || 'folder', 20); });
|
||||
},
|
||||
bindAgentIcon(el, a) {
|
||||
Alpine.effect(() => { el.innerHTML = a.icon || '🤖'; });
|
||||
},
|
||||
syncSidebarClass() {
|
||||
document.documentElement.classList.toggle(
|
||||
'fd-sidebar-collapsed', this.sidebarCollapsed && !this.sidebarPeek);
|
||||
},
|
||||
initSidebarSort() {
|
||||
Alpine.nextTick(() => {
|
||||
if (typeof initTreeSortable === 'function') initTreeSortable();
|
||||
});
|
||||
},
|
||||
fdCreatePage() { window.FlowDeck.createPage(); },
|
||||
fdCreateFolder() { window.FlowDeck.showCreateFolderModal(); },
|
||||
fdGwRefresh() { if (window._gwData) window._gwData.refreshTree(); },
|
||||
sidebarSections() {
|
||||
if (Object.keys(this.sidebarConfig).length) return this.sidebarConfig;
|
||||
return {
|
||||
workspace: { visible: true, order: 0 }, teamspaces: { visible: true, order: 1 },
|
||||
meetings: { visible: true, order: 2 }, recents: { visible: true, order: 3 },
|
||||
favorites: { visible: true, order: 4 }, agents: { visible: true, order: 5 },
|
||||
shared: { visible: true, order: 6 }, published: { visible: true, order: 7 },
|
||||
};
|
||||
},
|
||||
sectionMenuPos() {
|
||||
return {
|
||||
top: this.sectionMenu.y + 'px',
|
||||
left: Math.min(this.sectionMenu.x, window.innerWidth - 220) + 'px',
|
||||
};
|
||||
},
|
||||
|
||||
toggleSidebar() {
|
||||
this.sidebarPeek = false;
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: cardDetail »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('cardDetail', cardDetail); });
|
||||
function cardDetail() {
|
||||
return {
|
||||
updateField(field, value) {
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
<div class="dd-item" :class="{active: sortBy==='author'}" @click="setSort('author')"><span class="check" x-text="sortBy==='author' ? '✓' : ''"></span> Created by</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="lib-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="searchOpen=!searchOpen; $nextTick(()=>{ if(searchOpen) document.getElementById('lib-search-input').focus(); })">
|
||||
<button class="lib-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>
|
||||
<div class="lib-toolbar-wrap">
|
||||
@@ -310,7 +310,7 @@
|
||||
<button @click="copyLinks()" title="Copy links">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||||
</button>
|
||||
<button @click="openMovePicker(Object.keys(selected).map(Number))" title="Move to">
|
||||
<button @click="openMoveSelected()" title="Move to">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||||
</button>
|
||||
<div style="position:relative;margin-left:auto;">
|
||||
@@ -326,7 +326,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||||
Copy links to all
|
||||
</div>
|
||||
<div class="menu-item" @click="openMovePicker(Object.keys(selected).map(Number))">
|
||||
<div class="menu-item" @click="openMoveSelected()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||||
Move to
|
||||
</div>
|
||||
@@ -378,7 +378,7 @@
|
||||
|
||||
<!-- ── Empty state ── -->
|
||||
<div class="lib-empty" id="lib-empty">
|
||||
<div class="empty-icon" x-html="getSvgIcon(emptyIcon,48)"></div>
|
||||
<div class="empty-icon" x-init="bindHtmlIcon($el)"></div>
|
||||
<h3 x-text="emptyTitle"></h3>
|
||||
<p x-text="emptyText"></p>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@
|
||||
<div class="move-modal-item" @click="confirmMove(0)">{{ fd_icon("file",14) }} <span>Root (no parent)</span></div>
|
||||
<template x-for="it in moveCandidates" :key="it.id">
|
||||
<div class="move-modal-item" @click="confirmMove(it.id)">
|
||||
<span x-html="_renderIcon(it)"></span> <span x-text="it.title"></span>
|
||||
<span x-init="bindHtmlItem($el, it)"></span> <span x-text="it.title"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -414,7 +414,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
<div class="peek-title">
|
||||
<span x-html="_renderIcon(peekItem)"></span>
|
||||
<span x-init="bindHtmlItem($el, peekItem)"></span>
|
||||
<span x-text="peekItem.title"></span>
|
||||
</div>
|
||||
<button @click="openItem(peekItem)" title="Open full page">
|
||||
|
||||
@@ -100,6 +100,11 @@
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: tableView »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('tableView', tableView); });
|
||||
function tableView() {
|
||||
return {
|
||||
sortField: '',
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: teamLoad »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('teamLoad', teamLoad); });
|
||||
function teamLoad() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: trashData »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('trashData', trashData); });
|
||||
function trashData() {
|
||||
return {
|
||||
search: '',
|
||||
|
||||
@@ -134,6 +134,11 @@ h1{font-size:22px;font-weight:700;margin-bottom:6px;}
|
||||
</div>
|
||||
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: onboarding »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('onboarding', onboarding); });
|
||||
function onboarding() {
|
||||
return {
|
||||
steps: ['Bienvenue', 'Espace de travail', 'Connecter une forge', 'Premier projet'],
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<template x-for="p in builtinProjects" :key="p.id">
|
||||
<div class="project-card" @click="openProject(p)">
|
||||
<div class="project-card-top">
|
||||
<span class="project-card-icon" x-html="getSvgIcon(p.icon || 'folder',20)"></span>
|
||||
<span class="project-card-icon" x-init="bindProjectIcon($el, p)"></span>
|
||||
<span class="forge-badge builtin">Built-in</span>
|
||||
</div>
|
||||
<div class="project-card-name" x-text="p.name"></div>
|
||||
@@ -141,6 +141,11 @@
|
||||
</div>
|
||||
|
||||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
||||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||||
|
||||
// globales window — probe « Undefined variable: workspacePage »).
|
||||
|
||||
document.addEventListener('alpine:init', function () { Alpine.data('workspacePage', workspacePage); });
|
||||
function workspacePage() {
|
||||
return {
|
||||
builtinProjects: [],
|
||||
|
||||
Reference in New Issue
Block a user