4 blocs interpolés extraits avec la recette de la 2a (config JSON inline +
JS statique, substitutions sur le CORPS du bloc) :
- local_workspace.html → local_workspace.js (2 031 L, lw-config :
current_folder_id, workspace_id)
- settings.html → settings.js (1 093 L, st-config : avatar, user
full_name/login/email, is_admin (bool), auth_method — 2 routes rendent ce
template, expressions « or "" » préservées pour les valeurs Undefined)
- _page_editor_realtime.html → page_editor_realtime.js (531 L, rt-config :
SELF id/login/full_name/color)
- board.html → board.js (146 L, bd-config : owner/repo/initial_view)
BONUS sécurité : les valeurs passent par |tojson (échappement JSON explicite)
au lieu d'être interpolées dans des strings JS. Tags : config JSON (nonce
conservé) + <script src> ?v={{ asset_version }} ; loaders JSON.parse en tête
(try/catch → {}). Correctif sur le loader (accolade try en trop, caught par
node --check avant tout commit).
Cumul A27 : 10 560 L extraites (13 904 → 3 344 restantes, -76 %).
Reste structurel : base 1 338 ({% block %}/{% for %}) + database_table 1 323
(if/else) + 279 warnings eslint (12 fichiers, 0 erreur).
suite **1089/1089** · ruff OK · node --check ×4 vert · docs à jour
157 lines
6.8 KiB
HTML
157 lines
6.8 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_icon %}{{ fd_icon("folder",18) }}{% endblock %}
|
|
{% block page_title %}Kanban board new{% endblock %}
|
|
{% block title_prefix %}Kanban board new{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Cover image -->
|
|
<div class="page-cover">
|
|
<div class="page-cover-actions">
|
|
<button class="cover-btn">Change</button>
|
|
<button class="cover-btn">Reposition</button>
|
|
<button class="cover-btn">↓</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page title -->
|
|
<div class="page-title-area">
|
|
<div class="page-title">
|
|
<span class="page-icon-lg">{{ fd_icon("folder",24) }}</span>
|
|
<h1>{{ repo }}</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="page-description" x-data="{ hideDesc: false }">
|
|
<template x-if="!hideDesc">
|
|
<div>
|
|
<p>Use this Kanban board to track tasks across your project. Group by team, filter by status, and sort by priority.</p>
|
|
</div>
|
|
</template>
|
|
<button class="text-xs text-dim" @click="hideDesc = !hideDesc" style="margin-top: 4px;">
|
|
<span x-text="hideDesc ? 'Show description ▾' : 'Hide description ▴'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- View tabs with HTMX -->
|
|
<div class="view-tabs" id="view-tabs"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/kanban"
|
|
hx-trigger="load" hx-target="#view-content" hx-swap="innerHTML">
|
|
<button class="view-tab active" data-view="kanban"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/kanban"
|
|
hx-target="#view-content" hx-swap="innerHTML"
|
|
@click="setActiveTab($el)">
|
|
<span class="tab-icon">{{ fd_icon("bar-chart",14) }}</span> Kanban board
|
|
</button>
|
|
<button class="view-tab" data-view="detailed"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/detailed"
|
|
hx-target="#view-content" hx-swap="innerHTML"
|
|
@click="setActiveTab($el)">
|
|
<span class="tab-icon">{{ fd_icon("list",14) }}</span> Detailed board
|
|
</button>
|
|
<button class="view-tab" data-view="table"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/table"
|
|
hx-target="#view-content" hx-swap="innerHTML"
|
|
@click="setActiveTab($el)">
|
|
<span class="tab-icon">{{ fd_icon("align-left",14) }}</span> Table view
|
|
</button>
|
|
<button class="view-tab" data-view="status"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/status"
|
|
hx-target="#view-content" hx-swap="innerHTML"
|
|
@click="setActiveTab($el)">
|
|
<span class="tab-icon">{{ fd_icon("trending-up",14) }}</span> Status overview
|
|
</button>
|
|
<button class="view-tab" data-view="teamload"
|
|
hx-get="/board/{{ owner }}/{{ repo }}/view/teamload"
|
|
hx-target="#view-content" hx-swap="innerHTML"
|
|
@click="setActiveTab($el)">
|
|
<span class="tab-icon">{{ fd_icon("users",14) }}</span> Team Load
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="view-toolbar" x-data="filterSystem()">
|
|
<template x-for="(f, i) in activeFilters" :key="i">
|
|
<div class="filter-pill">
|
|
<span x-text="f.property + ': ' + f.value"></span>
|
|
<button class="remove-filter" @click="removeFilter(i); refreshView()">✕</button>
|
|
</div>
|
|
</template>
|
|
<div class="filter-pill" style="cursor:pointer; position:relative;" @click="showStatusMenu = !showStatusMenu">
|
|
<span>Status: </span>
|
|
<span class="text-dim" x-text="statusFilterLabel"></span>
|
|
<span style="font-size:10px;">▾</span>
|
|
<div class="dropdown-panel" :class="{ visible: showStatusMenu }" style="top:100%; left:0;"
|
|
@click.stop>
|
|
<template x-for="s in statusOptions" :key="s.value">
|
|
<div class="dropdown-option" @click="toggleStatus(s.value); showStatusMenu = false; refreshView()">
|
|
<span class="option-checkbox" :class="{ selected: statusFilters.includes(s.value) }">
|
|
<span x-show="statusFilters.includes(s.value)">✓</span>
|
|
</span>
|
|
<span class="option-color-dot" :style="'background:' + s.color"></span>
|
|
<span x-text="s.label"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<button class="filter-add-btn" @click="addFilter()">+ Filter</button>
|
|
<button class="filter-reset-btn" @click="resetFilters(); refreshView()" x-show="activeFilters.length > 0">Reset</button>
|
|
|
|
<div class="view-toolbar-spacer"></div>
|
|
|
|
<div style="position:relative;" x-data="sortSystem()">
|
|
<button class="toolbar-btn" @click="showSortPanel = !showSortPanel" title="Sort">⇅</button>
|
|
<div class="dropdown-panel" :class="{ visible: showSortPanel }" style="right:0; left:auto;" @click.stop>
|
|
<div class="sort-list">
|
|
<template x-for="(s, i) in sorts" :key="i">
|
|
<div class="sort-item">
|
|
<span class="sort-field">
|
|
<span x-text="s.field"></span>
|
|
</span>
|
|
<span class="sort-direction" @click="toggleDir(i)">
|
|
<span x-text="s.dir === 'asc' ? 'Ascending ↑' : 'Descending ↓'"></span>
|
|
</span>
|
|
<button class="sort-delete" @click="removeSort(i)" title="Delete">{{ fd_icon("trash",14) }}</button>
|
|
</div>
|
|
</template>
|
|
<button class="dropdown-option" @click="addSort()">+ Add sort</button>
|
|
<button class="dropdown-option" @click="clearSorts()" x-show="sorts.length > 0">Delete sort</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="toolbar-btn" title="Properties">{{ fd_icon("zap",14) }}</button>
|
|
<button class="toolbar-btn" title="Search">{{ fd_icon("search",14) }}</button>
|
|
<button class="toolbar-btn">⋯</button>
|
|
<button class="btn-new" @click="showNewIssue()">New ▾</button>
|
|
</div>
|
|
|
|
<!-- New Issue Form (hidden) -->
|
|
<div id="new-issue-form" style="display:none; padding: 12px 24px; background: var(--bg-secondary); border-bottom: 1px solid var(--border);"
|
|
x-data="newIssueForm()">
|
|
<div style="display:flex; gap:12px; align-items:center;">
|
|
<input type="text" placeholder="Issue title..." x-model="title"
|
|
style="flex:1; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:6px;
|
|
padding:8px 12px; color:var(--text-primary); font-size:14px; outline:none;"
|
|
@keydown.enter="create()">
|
|
<select x-model="status" style="background:var(--bg-tertiary); border:1px solid var(--border); border-radius:6px;
|
|
padding:8px; color:var(--text-primary); font-size:13px;">
|
|
<option value="todo">To-do</option>
|
|
<option value="progress">In progress</option>
|
|
<option value="done">Done</option>
|
|
</select>
|
|
<button class="btn-new" @click="create()">Create</button>
|
|
<button class="toolbar-btn" @click="hide()">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dynamic view content -->
|
|
<div id="view-content">
|
|
<!-- Loaded via HTMX -->
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script type="application/json" id="bd-config" nonce="{{ csp_nonce() }}">{{ {"owner": owner, "repo": repo, "initial_view": initial_view} | tojson }}</script>
|
|
<script data-cfasync="false" src="/static/js/board.js?v={{ asset_version }}"></script>
|
|
{% endblock %}
|