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)
196 lines
9.0 KiB
HTML
196 lines
9.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_title %}Workspace{% endblock %}
|
|
{% block title_prefix %}Workspace{% endblock %}
|
|
{% block page_icon %}{{ fd_icon("home",18) }}{% endblock %}
|
|
|
|
{% block topbar %}
|
|
{% set page_icon = "home" %}
|
|
{% set page_title = "Workspace — Projects" %}
|
|
{% 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>' %}
|
|
{% include '_header.html' %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.workspace-layout{max-width:900px;margin:0 auto;padding:32px 24px;}
|
|
.workspace-header{margin-bottom:24px;}
|
|
.workspace-header h1{font-size:28px;font-weight:600;margin-bottom:8px;color:var(--text-primary);}
|
|
.workspace-header p{color:var(--text-secondary);font-size:14px;}
|
|
|
|
.forge-section{margin-bottom:32px;}
|
|
.forge-section h2{font-size:14px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;font-weight:500;display:flex;align-items:center;gap:8px;}
|
|
.forge-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500;}
|
|
.forge-badge.gitea{background:rgba(96,155,80,.15);color:var(--green);}
|
|
.forge-badge.github{background:rgba(110,84,148,.15);color:var(--purple);}
|
|
.forge-badge.builtin{background:rgba(35,131,226,.15);color:var(--accent);}
|
|
|
|
.project-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;}
|
|
.project-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;cursor:pointer;transition:background var(--transition);}
|
|
.project-card:hover{background:var(--bg-hover);border-color:var(--border-strong);}
|
|
.project-card-top{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:8px;}
|
|
.project-card-icon{font-size:20px;}
|
|
.project-card-name{font-size:14px;font-weight:500;color:var(--text-primary);margin-bottom:4px;}
|
|
.project-card-desc{font-size:12px;color:var(--text-secondary);margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
|
|
.project-card-meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-dim);}
|
|
|
|
.empty-state{text-align:center;padding:40px;color:var(--text-dim);font-size:14px;}
|
|
.empty-state .btn{margin-top:12px;display:inline-block;padding:8px 16px;background:var(--accent);color:#fff;border-radius:var(--radius-md);text-decoration:none;font-size:13px;}
|
|
|
|
.new-project-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:rgba(35,131,226,.15);color:var(--accent);border:1px solid rgba(35,131,226,.3);border-radius:var(--radius-md);cursor:pointer;font-size:13px;}
|
|
.new-project-btn:hover{background:rgba(35,131,226,.25);}
|
|
|
|
/* Modal */
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:500;}
|
|
.modal-box{background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);padding:32px;width:400px;max-width:90vw;box-shadow:var(--shadow-modal);}
|
|
.modal-box h3{font-size:16px;margin-bottom:16px;color:var(--text-primary);}
|
|
.modal-input{width:100%;padding:10px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-size:14px;margin-bottom:12px;outline:none;}
|
|
.modal-input:focus{border-color:var(--accent);}
|
|
.modal-actions{display:flex;gap:8px;justify-content:flex-end;}
|
|
</style>
|
|
|
|
<div class="workspace-layout" x-data="workspacePage()">
|
|
|
|
<div class="workspace-header">
|
|
<h1>{{ fd_icon("home",20) }} Workspace</h1>
|
|
<p>All your projects in one place — built-in, Gitea, and GitHub</p>
|
|
</div>
|
|
|
|
<!-- Built-in Projects -->
|
|
<div class="forge-section">
|
|
<h2><span class="forge-badge builtin">Built-in</span> My Projects</h2>
|
|
<div class="project-grid">
|
|
<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-init="bindProjectIcon($el, p)"></span>
|
|
<span class="forge-badge builtin">Built-in</span>
|
|
</div>
|
|
<div class="project-card-name" x-text="p.name"></div>
|
|
<div class="project-card-desc" x-text="p.description || 'No description'"></div>
|
|
<div class="project-card-meta">
|
|
<span x-text="p.pageCount + ' pages'"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div class="project-card" style="border:1px dashed var(--border-strong);display:flex;align-items:center;justify-content:center;min-height:100px" @click="showCreateModal = true">
|
|
<span style="color:var(--text-dim);font-size:24px">+</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gitea Projects -->
|
|
<div class="forge-section" x-show="giteaProjects.length">
|
|
<h2><span class="forge-badge gitea">{{ fd_icon("link",14) }} Gitea</span> Repositories</h2>
|
|
<div class="project-grid">
|
|
<template x-for="p in giteaProjects" :key="p.id">
|
|
<div class="project-card" @click="openProject(p)">
|
|
<div class="project-card-top">
|
|
<span class="project-card-icon">{{ fd_icon("folder",14) }}</span>
|
|
<span class="forge-badge gitea">Gitea</span>
|
|
</div>
|
|
<div class="project-card-name" x-text="p.name"></div>
|
|
<div class="project-card-desc" x-text="p.description || 'No description'"></div>
|
|
<div class="project-card-meta">
|
|
<span x-text="p.full_name"></span>
|
|
<span x-show="p.language" x-text="'· ' + p.language"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GitHub Projects -->
|
|
<div class="forge-section" x-show="githubProjects.length">
|
|
<h2><span class="forge-badge github">{{ fd_icon("link",14) }} GitHub</span> Repositories</h2>
|
|
<div class="project-grid">
|
|
<template x-for="p in githubProjects" :key="p.id">
|
|
<div class="project-card" @click="openProject(p)">
|
|
<div class="project-card-top">
|
|
<span class="project-card-icon">{{ fd_icon("folder",14) }}</span>
|
|
<span class="forge-badge github">GitHub</span>
|
|
</div>
|
|
<div class="project-card-name" x-text="p.name"></div>
|
|
<div class="project-card-desc" x-text="p.description || 'No description'"></div>
|
|
<div class="project-card-meta">
|
|
<span x-text="p.full_name"></span>
|
|
<span x-show="p.language" x-text="'· ' + p.language"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div class="empty-state" x-show="!builtinProjects.length && !giteaProjects.length && !githubProjects.length">
|
|
<p>No projects yet. Create a new project or connect a forge.</p>
|
|
<a href="/auth/login?provider=local" class="btn">Get Started</a>
|
|
</div>
|
|
|
|
<!-- Create Modal -->
|
|
<div class="modal-overlay" x-show="showCreateModal" @click.outside="showCreateModal=false" @keydown.escape="showCreateModal=false">
|
|
<div class="modal-box">
|
|
<h3>Create New Project</h3>
|
|
<input class="modal-input" x-model="newProjectName" placeholder="Project name" @keydown.enter="createProject">
|
|
<div class="modal-actions">
|
|
<button class="settings-btn settings-btn-secondary" @click="showCreateModal=false">Cancel</button>
|
|
<button class="settings-btn settings-btn-primary" @click="createProject">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</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: [],
|
|
giteaProjects: [],
|
|
githubProjects: [],
|
|
showCreateModal: false,
|
|
newProjectName: '',
|
|
|
|
async init() {
|
|
try {
|
|
const r = await fetch('/api/workspace/projects');
|
|
const data = await r.json();
|
|
this.builtinProjects = data.builtin || [];
|
|
this.giteaProjects = data.gitea || [];
|
|
this.githubProjects = data.github || [];
|
|
} catch(e) { console.error('Failed to load projects', e); }
|
|
},
|
|
|
|
openProject(p) {
|
|
if (p.forge === 'gitea') {
|
|
const parts = p.full_name.split('/');
|
|
window.location = `/board/${parts[0]}/${parts[1]}`;
|
|
} else if (p.forge === 'github') {
|
|
window.open(p.html_url, '_blank');
|
|
} else {
|
|
window.location = `/pages/${p.id}`;
|
|
}
|
|
},
|
|
|
|
async createProject() {
|
|
if (!this.newProjectName.trim()) return;
|
|
const r = await fetch('/api/workspace/projects', {
|
|
method: 'POST',
|
|
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
|
body: JSON.stringify({name: this.newProjectName.trim()})
|
|
});
|
|
if (r.ok) {
|
|
const p = await r.json();
|
|
this.builtinProjects.unshift({...p, icon: 'folder', pageCount: 0});
|
|
this.newProjectName = '';
|
|
this.showCreateModal = false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|