Files
flowdeck/app/templates/workspaces.html
T
bruno c5398757ca
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
fix: hamburger menu sur toutes les pages + cookies path="/" Chrome fix
- local_workspace.html: supprimé Alpine.data dupliqué + ajout hamburger
- workspaces.html, workspace.html, settings.html, page_editor.html: +hamburger
- auth.py: +path="/" sur tous les set_cookie de session (Chrome compat)
- csrf.py: +path="/" sur cookie CSRF

Root cause des bugs:
1. Chrome: cookie sans path="/" → non envoyé sur certaines routes
2. Firefox: les templates écrasaient le block topbar → pas de hamburger
   → sidebar inaccessible sur mobile (overlay + slide-in ne fonctionnaient pas)
2026-07-11 00:30:59 -04:00

153 lines
6.2 KiB
HTML

{% extends "base.html" %}
{% block page_title %}Workspaces{% endblock %}
{% block page_icon %}🏠{% endblock %}
{% block topbar %}
<header class="topbar">
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
<span class="page-title-topbar">Workspaces</span>
</div>
<div class="topbar-right">
<button class="page-action-btn" onclick="window.location='/accounts/settings'" title="Settings">⚙</button>
</div>
</header>
{% endblock %}
{% block content %}
<style>
.ws-page{max-width:800px;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-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;}
.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:16px;font-weight:500;margin-bottom:4px;}
.ws-card-count{font-size:12px;color:var(--text-tertiary);}
.ws-card-actions{display:flex;gap:4px;margin-top:8px;opacity:0;transition:opacity 150ms;}
.ws-card:hover .ws-card-actions{opacity:1;}
.dialog-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:500;}
.dialog-box{background:#1F1F1F;border:1px solid var(--border);border-radius:18px;padding:24px;width:400px;max-width:90vw;}
.dialog-box h3{margin-bottom:16px;}
.dialog-input{width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;margin-bottom:12px;outline:none;}
.dialog-input:focus{border-color:var(--accent);}
.dialog-actions{display:flex;gap:8px;justify-content:flex-end;}
.btn{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
.btn-primary{background:var(--accent);color:#fff;}
.btn-primary:hover{background:var(--accent-hover);}
.btn-secondary{background:var(--bg-tertiary);color:var(--text);}
.btn-secondary:hover{background:var(--bg-hover);}
.btn-danger{background:rgba(255,80,80,.15);color:var(--danger);}
.btn-danger:hover{background:rgba(255,80,80,.25);}
.btn-new{border:1px dashed var(--border-strong);background:transparent;color:var(--text-secondary);display:flex;align-items:center;justify-content:center;min-height:120px;}
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
</style>
<div class="ws-page" x-data="workspacesPage()">
<div class="ws-header">
<h1>🏠 Workspaces</h1>
<button class="btn btn-primary" @click="showCreate=true">+ New Workspace</button>
</div>
<div class="ws-grid">
<template x-for="ws in workspaces" :key="ws.id">
<div class="ws-card" :class="{active:ws.id===activeId}" @click="select(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 btn-secondary" style="padding:4px 8px;font-size:11px" @click="renameWs(ws)">✏️</button>
<button class="btn btn-danger" style="padding:4px 8px;font-size:11px" @click="deleteWs(ws)">🗑</button>
</div>
</div>
</template>
<div class="ws-card btn-new" @click="showCreate=true">
<span style="font-size:32px">+</span>
</div>
</div>
<!-- Create/Rename dialog -->
<div class="dialog-overlay" x-show="showCreate||showRename" @click.away="showCreate=false;showRename=false">
<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="showCreate=false;showRename=false">Cancel</button>
<button class="btn btn-primary" @click="showRename?doRename():doCreate()" x-text="showRename?'Rename':'Create'"></button>
</div>
</div>
</div>
</div>
<script>
function workspacesPage() {
return {
workspaces: [],
activeId: null,
showCreate: false,
showRename: false,
wsName: '',
renameTarget: null,
async init() {
await this.load();
},
async load() {
const r = await fetch('/api/workspaces');
const d = await r.json();
this.workspaces = d.workspaces || [];
this.activeId = d.active_id;
},
async select(ws) {
await fetch(`/api/workspaces/${ws.id}/select`, {method:'POST'});
if (ws.page_count > 0) {
window.location = '/local-workspace';
} else {
window.location = '/local-workspace';
}
},
async doCreate() {
if (!this.wsName.trim()) return;
await fetch('/api/workspaces', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:this.wsName.trim()})
});
this.wsName = '';
this.showCreate = false;
await this.load();
},
renameWs(ws) {
this.renameTarget = ws;
this.wsName = ws.name;
this.showRename = true;
},
async doRename() {
if (!this.wsName.trim()||!this.renameTarget) return;
await fetch(`/api/workspaces/${this.renameTarget.id}`, {
method:'PUT',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({name:this.wsName.trim()})
});
this.wsName = '';
this.showRename = false;
this.renameTarget = null;
await this.load();
},
async deleteWs(ws) {
if (!confirm(`Delete workspace "${ws.name}" and all its pages?`)) return;
await fetch(`/api/workspaces/${ws.id}`, {method:'DELETE'});
await this.load();
}
};
}
</script>
{% endblock %}