Files
flowdeck/app/templates/workspaces.html
T
bruno 6d7af3fb64
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m9s
feat: éditeur visuel d'automations (pipeline steps) + fix CSP multi-instructions (v7.45.0)
Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
  TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
  leurs champs reels), ajout/edition/suppression/haut-bas via
  POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
  JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
  carte « Action · webhook » sous CSP reel.

Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
  menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
  (setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
  library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
  (addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.

Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
2026-10-02 16:56:29 -04:00

177 lines
9.2 KiB
HTML

{% extends "base.html" %}
{% block page_title %}Workspaces{% endblock %}
{% block title_prefix %}Workspaces{% endblock %}
{% block page_icon %}{{ fd_icon("home",18) }}{% endblock %}
{% block topbar %}
{% set breadcrumb_items = [{"label": "Workspaces", "url": None}] %}
{% set page_icon = "home" %}
{% set right_actions %}<a href="/auth/login?provider=local" class="topbar-btn" title="Login">{{ fd_icon("key",16) }}</a><a href="/accounts/settings" class="topbar-btn" title="Settings">{{ fd_icon("settings",16) }}</a>{% endset %}
{% include '_header.html' %}
{% endblock %}
{% block content %}
<style>
.ws-page{max-width:1000px;margin:0 auto;padding:40px 24px;}
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;}
.ws-header h1{font-size:24px;}
.ws-section{margin-bottom:40px;}
.ws-section-title{font-size:14px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;display:flex;align-items:center;gap:8px;}
.ws-section-title .badge{font-size:11px;background:var(--bg-tertiary);color:var(--text-dim);padding:1px 8px;border-radius:10px;}
.ws-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px;}
.ws-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px;cursor:pointer;transition:background 150ms, border-color 150ms;}
.ws-card:hover{background:var(--bg-tertiary);border-color:var(--border-strong);}
.ws-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
.ws-card-name{font-size:15px;font-weight:500;margin-bottom:2px;}
.ws-card-sub{font-size:12px;color:var(--text-dim);margin-bottom:4px;}
.ws-card-count{font-size:12px;color:var(--text-dim);}
.ws-card-actions{display:flex;gap:4px;margin-top:8px;opacity:0;transition:opacity 150ms;}
.ws-card:hover .ws-card-actions{opacity:1;}
/* Gitea org grouping */
.gitea-org{margin-bottom:16px;}
.gitea-org-header{font-size:14px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px;margin-bottom:8px;}
.gitea-org-header img{border-radius:4px;}
.gitea-org-avatar{width:24px;height:24px;border-radius:4px;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;font-size:14px;}
.gitea-empty{padding:24px;text-align:center;color:var(--text-dim);font-size:14px;border:1px dashed var(--border);border-radius:var(--radius);}
.gitea-connect{display:inline-block;margin-top:8px;color:var(--accent);cursor:pointer;font-size:13px;}
.gitea-connect:hover{text-decoration:underline;}
.gitea-loading{color:var(--text-dim);font-size:13px;padding:12px 0;}
/* Org tabs */
.org-tab{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:13px;color:var(--text-dim);background:transparent;border:1px solid var(--border);cursor:pointer;white-space:nowrap;transition:all 100ms;}
.org-tab:hover{background:var(--bg-tertiary);color:var(--text);}
.org-tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
.org-tab-badge{font-size:10px;padding:0 5px;border-radius:8px;background:rgba(255,255,255,.15);}
[data-theme="light"] .org-tab-badge{background:rgba(0,0,0,.08);}
.search-input:focus{border-color:var(--accent);}
</style>
<div class="ws-page" x-data="workspacesPage()">
<div class="ws-header">
<h1>{{ fd_icon("home",20) }} Workspaces</h1>
</div>
<!-- ── Local Workspaces ── -->
<div class="ws-section">
<div class="ws-section-title">{{ fd_icon("folder",16) }} My Workspaces <span class="badge" x-text="workspaces.length"></span></div>
<div class="ws-grid">
<template x-for="ws in workspaces" :key="'local-'+ws.id">
<div class="ws-card" :class="{active:ws.id===activeId}" @click="selectLocal(ws)">
<div class="ws-card-name" x-text="ws.name"></div>
<div class="ws-card-count" x-text="ws.page_count+' page(s)'"></div>
<div class="ws-card-actions" @click.stop>
<button class="btn-sm" @click="renameWs(ws)">{{ fd_icon("edit",14) }}</button>
<button class="btn-sm" style="color:var(--danger);border-color:rgba(255,80,80,.3)" @click="deleteWs(ws)">{{ fd_icon("trash",14) }}</button>
</div>
</div>
</template>
<div class="ws-card btn-new" @click="showCreate=true" style="display:flex;align-items:center;justify-content:center;height:auto;min-height:140px;">
<span style="font-size:48px;line-height:1;">+</span>
</div>
</div>
</div>
<!-- ── Gitea Projects ── -->
<div class="ws-section">
<div class="ws-section-title">{{ fd_icon("link",16) }} Gitea Projects <span class="badge" x-text="giteaTotal"></span></div>
<!-- Not linked -->
<div class="gitea-empty" x-show="giteaStatus==='not_linked'">
<div>Connect your Gitea account to see your projects.</div>
<a class="gitea-connect" href="/auth/login?provider=gitea&mode=link">{{ fd_icon("link",14) }} Connect Gitea</a>
<span style="margin:0 8px;color:var(--text-dim);">or</span>
<a class="gitea-connect" href="/auth/login?provider=gitea">{{ fd_icon("edit",14) }} Register with Gitea</a>
</div>
<!-- Loading skeletons -->
<div x-show="giteaStatus==='loading'" class="ws-grid">
<template x-for="i in 6" :key="i">
<div class="ws-card" style="pointer-events:none;">
<div class="skeleton" style="height:16px;width:65%;margin-bottom:8px;"></div>
<div class="skeleton" style="height:12px;width:45%;margin-bottom:6px;"></div>
<div class="skeleton" style="height:12px;width:80%;"></div>
</div>
</template>
</div>
<!-- Error -->
<div class="gitea-empty" x-show="giteaStatus==='error'">
<div>{{ fd_icon("alert-triangle",14) }} Could not load Gitea projects.</div>
<button class="gitea-connect" @click="loadGitea()">Retry</button>
</div>
<!-- Projects (tabs by org + search) -->
<div x-show="giteaStatus==='ok' && giteaGroups.length > 0">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
<!-- Org tabs -->
<div style="display:flex;gap:4px;overflow-x:auto;flex:1;min-width:0;">
<button class="org-tab" :class="{active:activeOrg==='all'}" @click="activeOrg='all'">
All <span class="org-tab-badge" x-text="giteaTotal"></span>
</button>
<template x-for="g in giteaGroups" :key="g.org">
<button class="org-tab" :class="{active:activeOrg===g.org}" @click="activeOrg=g.org">
<span x-text="g.org"></span>
<span class="org-tab-badge" x-text="g.projects.length"></span>
</button>
</template>
</div>
<!-- Search -->
<input type="text" class="search-input" x-model="giteaSearch" placeholder="Filter projects…" style="width:200px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;">
</div>
<!-- Project cards (filtered) -->
<template x-for="group in filteredGroups" :key="group.org">
<div class="gitea-org">
<div class="gitea-org-header">
<span class="gitea-org-avatar" x-text="group.org.charAt(0).toUpperCase()"></span>
<span x-text="group.org"></span>
</div>
<div class="ws-grid">
<template x-for="proj in group.projects" :key="proj.id">
<div class="ws-card" @click="openGitea(proj)">
<div class="ws-card-name" x-text="proj.name"></div>
<div class="ws-card-sub">
<span x-text="group.org"></span>
<span style="margin:0 4px">/</span>
<span x-text="proj.name"></span>
</div>
<div class="ws-card-count">
<span x-show="proj.private" style="margin-right:6px">{{ fd_icon("lock",14) }}</span>
<span x-text="proj.description || proj.language || ''" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;"></span>
</div>
</div>
</template>
</div>
</div>
</template>
<div class="gitea-empty" x-show="filteredGroups.length===0" style="margin-top:12px;">
No projects match your search.
</div>
</div>
<!-- No projects at all -->
<div class="gitea-empty" x-show="giteaStatus==='ok' && giteaTotal==0 && giteaGroups.length===0">
No Gitea projects found.
</div>
</div>
<!-- Create/Rename dialog -->
<div class="dialog-overlay" x-show="showCreate||showRename" @click.outside="closeCreateRename()">
<div class="dialog-box">
<h3 x-text="showRename?'Rename Workspace':'New Workspace'"></h3>
<input class="dialog-input" x-model="wsName" placeholder="Workspace name" @keydown.enter="showRename?doRename():doCreate()">
<div class="dialog-actions">
<button class="btn btn-secondary" @click="closeCreateRename()">Cancel</button>
<button class="btn btn-primary" @click="showRename?doRename():doCreate()" x-text="showRename?'Rename':'Create'"></button>
</div>
</div>
</div>
</div>
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/workspaces.js?v={{ asset_version }}"></script>
{% endblock %}