PASSWORD_MIN_LENGTH (default 8) is now the length below which a sign-in is REPORTED, in the new Admin Panel / Problems / Password length, and PASSWORD_MIN_LENGTH_ENFORCE (off by default) is what makes it a rule: a shorter new password is refused, and an account that signs in with an older shorter one sees only "Change your password" until it has a long enough one. Signing in is never refused, enforced or not, so nobody is locked out by a minimum set after their password. LDAP, OAuth2, OIDC, SAML, CAS and Sandstorm accounts are neither reported nor asked: their password is their provider's. An administrator's Set password no longer applies WeKan's policy to such an account. Meteor sends a password hashed, so only the browser knows its length: it notes it on the sign-in form and, after a sign-in that worked, says "it was short" - never the password or its length. The page has both settings and a table, one row per account: username, first and last short-password login, the latest IPv4 and IPv6 address with the country's flag and city when a CDN says where it is, the count, and when it was fixed, with the Too short (default) / Fixed / All filter. Fixed is a changed password not seen short since; a short one seen again reopens the row. Changing one's own password - Member Settings, the forced change, a reset link - now signs EVERY session of the account out, the one that changed it too, and only the new password signs in again. The browser goes to the sign-in page as soon as it is signed out, since the server closes the connection before the answer can arrive. Documented in docs/Features/Admin-Panel/Problems/Password-length.md, the settings on every platform. The new strings are in every locale in English for now. Tests: passwordPolicy, passwordChangeRequired and shortPasswordLogins (node); password-min-length-signup-login, password-length-report and password-change-other-sessions (Playwright, Chromium and WebKit), which change each setting on the page and check what it does, and that sign-up with no mail server registers and signs the user in. Thanks to xet7 !
23 KiB
Design: the Table Page
One design, one implementation, for every paginated table in WeKan: a title, an optional status line, a controls row (search + pagination) and a table of rows.
This page is the single source for everything those pages have in common. A page-specific design doc describes only what is different about that page and links back here — it must not restate the layout, the controls or the paging rules, because they are defined once, here and in one template.
Related files
Everything that makes a table page work. Paths are from the repository root.
| File Path | File Type | Description |
|---|---|---|
client/components/settings/tablePage.jade |
.jade template |
The table page: title, optional status row, controls, table. The row order is defined here, once. |
client/components/settings/tablePage.css |
.css stylesheet |
The layout: full width, equal columns, wrapping cells, and the ≤ 800px stacking of menu and table. No button colours — see the next row. |
client/components/main/paginationControls.css |
.css stylesheet |
The colours of every prev/next pager in WeKan, table pages included: the per-user theme accent, falling back to the WeKan default blue. |
client/components/forms/forms.css |
.css stylesheet |
The global button rules the pager stylesheet has to out-specify. Read the note in paginationControls.css before touching either. |
models/lib/tablePage.js |
.js module, pure helpers |
pageInfo(), buildRows(), buildHeader(), columnWidthPercent(). No DOM and no database, so they are unit-testable. |
client/components/settings/adminProblems.js |
.js Blaze template logic |
The column spec for each Admin Panel table, the paging state, the subscriptions, and the shared control handlers. |
client/components/settings/adminProblems.jade |
.jade template |
Admin Panel / Problems: the side menu, and which page it renders. |
client/components/settings/adminProblems.css |
.css stylesheet |
Only what is not shared: the CPU status card and the side-menu separator. |
client/components/settings/translationBody.jade |
.jade template |
Admin Panel / Settings / Translation: the pane, its interactive row and its "New" header. |
client/components/settings/translationBody.js |
.js Blaze template logic |
The Translation column spec, its paging state and its share of the shared control handlers. |
server/publications/translation.js |
.js publication |
Translation: one page of custom translation strings (limit/skip). |
client/features/settings.js |
.js import list |
Registers the template, its stylesheet and the logic into the client bundle. A file that is never imported is simply not loaded. |
server/publications/attachments.js |
.js publication + method |
Files Report: one page of attachments, and getAttachmentsReportCount. |
server/publications/rules.js |
.js publication + method |
Rules Report: one page of rules, and getRulesReportCount. |
server/publications/boards.js |
.js publication + method |
Boards Report: one page of boards, and getBoardsReportCount. |
server/publications/cards.js |
.js publication + method |
Cards Report and Broken cards: one page of cards each, with getCardsReportCount / getBrokenCardsReportCount. |
server/publications/impersonationReport.js |
.js publication + method |
Impersonation Report: one page of events, and getImpersonationReportCount. |
server/publications/recoveryReport.js |
.js publication + method |
Recovery: one page of recovery events, and getRecoveryReportCount. |
models/eventLog.js |
.js model + methods |
The Security / Speed / Tests / CPU usage streams: eventLogPage, eventLogCount, and the {stream, at} index that keeps them fast. |
tests/tablePage.test.cjs |
.cjs Node test |
The one suite for all of the above: the pure helpers, the template, the layout rules this page promises, the themed pager, that paging stays server-side and index-backed, and the "one implementation" guarantee. It also checks that every path in this table still exists. |
Pages that do not use this design
Pages with a paginated table that are not built from this design, and why. Listed so nobody has to re-derive the answer by reading the code, and so the gap is visible rather than forgotten.
| Table name | Menu path | Why it does not use this design |
|---|---|---|
| People — its two remaining non-table panes | Admin Panel / People | Locked users and Shared templates are not tables: Locked users is a form of numeric lockout settings with a Save button, and Shared templates is a checkbox list of scopes. There is no set of rows to render, so this design does not apply to them and forcing it would only add a table around a form. All four of People's TABLE panes — Domains, Organizations, Teams and People — are converted and listed below, and Roles has since gained a table of its own (Roles Status), also listed below. |
People is fully converted: its four table panes render through this design, and its search, filter dropdown, action buttons and total are the shared controls row. Its separate Search button is gone — every table page searches on Enter.
Pages that use this design
Admin Panel
Menu path is what you click to reach the page.
| Table name | Menu path | Description |
|---|---|---|
| People | Admin Panel / People / People | Every user with e-mail, admin flag, active state, lockout status and created date. Interactive rows (rowTemplate) and two template headers: the new-user row and the select-all checkbox. |
| Teams | Admin Panel / People / Teams | Every team with its details and per-team feature switches. Same shape as Organizations: a rowTemplate and three headerTemplate columns. |
| Organizations | Admin Panel / People / Organizations | Every organization with its details and per-organization feature switches. Interactive rows, so it supplies a rowTemplate; three of its headers carry a select-all pair, supplied as headerTemplate. |
| Domains | Admin Panel / People / Domains | Every e-mail domain in use with the number of users on it. The first People pane converted to this design. |
| Roles Status | Admin Panel / People / Roles | What each board role may do — which cards it sees, and whether it may comment, create and edit, or change the board's settings. Read-only: no rowTemplate, no actions, nothing editable, because a role's capabilities are a property of the code and not a setting. It sits BELOW the Save button of the checkbox list above it and follows that list's working copy, so an admin sees what a change means before saving it. Rendered from models/lib/boardRoleCapabilities.js, the same table the allow rules decide with — see Board roles. |
| Security | Admin Panel / Problems / Security | Security events from the event log: blocked uploads, rejected URL schemes, auth failures. One row per event, newest first. |
| Password length | Admin Panel / Problems / Password length | Accounts that signed in with a password shorter than the minimum: username, first and last such login, the latest IPv4 and IPv6 address each with its flag and city when known, the count, and when it was fixed. Its filter is the status - Too short password (the default), Fixed to long enough password, All - and above the table are the two settings, the minimum length and whether it is enforced. See Password length. |
| Speed | Admin Panel / Problems / Speed | Slow-operation events — what took too long, where, and for how long. |
| Tests | Admin Panel / Problems / Tests | Test-run events recorded on the server. |
| CPU usage | Admin Panel / Problems / CPU usage | Past high-CPU periods. Adds a status row: the live CPU percent, core count and load average, because the table itself is history, not the current state. |
| Broken cards | Admin Panel / Problems / Broken cards | Cards with no board, swimlane or list, or a type that is not a card type. A column spec like the reports beside it — it used to run on the global-search results list, which is why it was the one report here with a different set of controls. |
| Files Report | Admin Panel / Problems / Files Report | Every attachment: file name, size, MIME type, and the attachment / board / card ids. |
| Rules Report | Admin Panel / Problems / Rules Report | Every automation rule with its board, action type and trigger type. |
| Boards Report | Admin Panel / Problems / Boards Report | Every board with its id, permission, archived state, members, organizations and teams. |
| Cards Report | Admin Panel / Problems / Cards Report | Every card with its board, swimlane, list, members and assignees. |
| Impersonation Report | Admin Panel / Problems / Impersonation Report | Who impersonated whom, on which board, when and why. |
| Recovery | Admin Panel / Problems / Recovery | Database recovery events with severity, database and detail. |
| Translation | Admin Panel / Settings / Translation | The custom translation strings: language, source text, translation. Interactive rows (an Edit link and the ⋯ menu) and one template header, the "New" link. |
Elsewhere in WeKan
| Table name | Menu path | Description |
|---|---|---|
| All Boards (Table view) | /board → the view menu → Table |
Every board of the selected section: Edit, board title and board description. EDITABLE, which is what separates it from Public Boards below - the Edit cell opens the same board-title popup the Swimlanes view opens. Its rows carry their board's colour. See All-Boards. |
| Public Boards | /public |
The boards anybody may open: title and description, ten per page. Read-only — a row's only action is to open its board — and the page is nothing but the table. Its rows carry their board's colour and background image, so a board is recognised here the way it is on All Boards. See Public. |
Card, board and member history
Designed, not yet implemented — see History.
| Table name | Menu path | Description |
|---|---|---|
| Card history | Card / hamburger menu / History | Every change to this card, restorable. |
| Member history | Member settings / History | One user's changes, across the boards the caller can see. |
| Board history | Board Settings / History | Every change on the board and everything inside it. |
| Swimlane history | Swimlane menu / History | Changes to the swimlane and its lists and cards. |
| List history | List menu / History | Changes to the list and its cards. |
Layout
Rows, top to bottom. This order is fixed, and is defined once in
client/components/settings/tablePage.jade:
- Title — what the page is. Rendered only when the page supplies one
(
titleKeyortitle). Inside the Admin Panel it does not: every pane there gets one heading from the active left-menu entry, rendered by the section around it — see Left Menu. A table page that printed a title of its own as well would show the same words twice, which is why People, Organizations, Teams, Domains, Translation and the Problems reports pass no title. The heading carries.admin-pane-titleeither way, so a table pane and a form pane have the same title at the same size. - Status (optional) — live state that the table itself cannot show, e.g. the
current CPU percent on the CPU usage page. Rendered from a named template
passed in as
statusTemplate; pages without one render no status row. - Controls — the search field on the start side, pagination
(
‹ page X / N ›) pushed to the end side. - Table — header row plus one row per record. It is always rendered, empty or not: several pages put controls in the header — the "New" link of Organizations, Teams, People and Translation, the select-all pairs — so hiding the table when there are no rows hid the only way to create the first row. An empty pane shows the header and the "no items" message below the table, not instead of it.
Width behaviour:
- Wide windows — the left menu of the section keeps its width and the table sits beside it, filling the rest.
- Narrow windows (≤ 800px) — the left menu goes full width on top and the table sits below it. This deliberately overrides the older "always side by side" rule: side by side on a phone left the table a few dozen pixels wide with its right-hand columns unreachable.
- The table is
width: 100%withtable-layout: fixed, so all columns get the same percentage of the width and the table can never grow wider than the panel. That is what keeps the right-hand side of the table inside the browser window instead of off the right edge. - Cell text wraps:
overflow-wrap: anywherebreaks even a long unbroken id, URL or file name inside its column instead of widening it — so a long value wraps onto a second line rather than being cut off at the column edge. Like the width, the wrapping is!important: app-widetable td { white-space: nowrap }rules had been switching it off. A column can opt out withnowrap(used for the datetime columns), which is!importanttoo so the opt-out still works. - Only the table's own wrapper may scroll sideways, and only as a last resort; the page itself never does.
- Nothing outside this design may force a width. Two admin stylesheets used to
set
min-width: 1200px !important; width: max-content !importanton a baretableselector — one of them (peopleBody.css) with no page in the selector at all, so it reached every table in WeKan and its!importantbeat this page's own layout. The result was the report this design exists to prevent: the right-hand columns off screen until you scrolled. Both are scoped away from.table-page-tablenow, and neither forces a floor on any admin table any more. The same rules also made a short table shrink: withwidth: max-contenta two-column table sat at a third of the panel instead of filling it.width,max-widthandtable-layouton.table-page-tableare therefore!important— a guarantee a stylesheet in another folder can silently override is not a guarantee.
The controls row shares one height (34px) and no margins, set in
tablePage.css: the search field, the filter, every action button and the
prev/next pair line up on the same top and bottom edge. The global button rule in
forms.css is display: block; margin-bottom: 14px, written for stacked form
buttons — a control that keeps that margin is centred higher than one that does
not, which is exactly how People's "Unlock all users" ended up lower than "Teams"
beside it.
Data loading
A table page loads one page of rows, never the whole collection.
pageInfo(total, page, perPage)inmodels/lib/tablePage.jsreturns{ page, totalPages, hasPrev, hasNext, skip, limit }. The same call feeds the subscription and the "page X / N" counter, so what is fetched and what is displayed cannot drift apart.- The publication applies
limit/skipserver-side and sends only that page, so only those rows reach minimongo. The client renders that page; it must not re-slice an already-paginated set. - The total row count comes from a separate count method, called when the page is opened or the search changes — not on every prev/next click. The total cannot change because you moved to the next page, and recounting there added a second round trip to every click.
Rows per page — one number, TABLE_PAGE_ROWS_PER_PAGE, for the whole app
Every paginated page in WeKan loads ten rows at a time. The number lives once,
as TABLE_PAGE_ROWS_PER_PAGE in models/lib/tablePage.js, and every page reads it
from there — including the pages that are not built from this design and draw a
pager of their own. A page that writes its own number is a bug, whichever template
it uses:
| Where | Reads it as |
|---|---|
Every table page (pageInfo's default) |
TABLE_PAGE_ROWS_PER_PAGE |
| Admin Panel / Problems — all reports and the event streams | REPORTS_PER_PAGE, EVENTS_PER_PAGE |
| Admin Panel / People — People, Organizations, Teams, Domains | usersPerPage, orgsPerPage, teamsPerPage, domainsPerPage |
| Admin Panel / Settings / Translation | TABLE_PAGE_ROWS_PER_PAGE |
The search pages (CardSearchPaged: global search, due cards, broken cards) |
resultsPerPage |
| Archived boards | ARCHIVED_BOARDS_PER_PAGE |
Two deliberate exceptions, both of which have no pager and are therefore not
paginated pages: the activity feed, which is infinite scroll and whose page size
is a live-cursor cost decision (see the note in
client/components/activities/activities.js), and PER_PAGE_MAX in
models/lib/domainTablePage.js, which is an upper bound on what a client may ask
for, not a page size. PER_PAGE_DEFAULT in that same module is the same ten,
written out rather than imported because a plain-node test require()s the file;
tests/tablePage.test.cjs pins the two together.
Columns
A page differs from another only in its column list:
{ label | labelKey, value(doc), align: 'end', nowrap: true, cls, userId(doc), data(doc) }
labelis a literal,labelKeyan i18n key.value(doc)returns the cell text. A missing field renders as an empty cell, never the stringundefined.userId(doc), when given, renders the cell as a link that opens the same "Edit user" popup as Admin Panel / People.align: 'end'right-aligns (sizes, counts);nowrapkeeps a datetime on one line;data(doc)setsdata-sev, which colours high/critical severities.
buildHeader(columns) and buildRows(docs, columns) turn that list into what the
template iterates. Both are pure functions in models/lib/tablePage.js with unit
tests, so a row can never end up with fewer cells than the header — which is how a
hand-written table gets its columns shifted by one.
Interactive rows and headers
Some panes cannot be expressed as text cells: their rows carry inline checkboxes and edit links, and their headers carry controls. Two slots cover that without every page reinventing the table:
rowTemplate— a template name plusdocs, an array of data contexts. The page owns its<tr>and is then responsible for matching the column count; the guarantee that a row can never be shorter than the header applies only to thecellsform.headerTemplate/headerDataon a column — the<th>still belongs to the shared template; only its contents come from the page.
Everything else — layout, paging, search, the themed pager, the total — is shared either way. Prefer the column spec; reach for these only when the pane is genuinely interactive.
Controls
The controls row holds, in reading order: the search field, any filters, any actions, the total, and the pagination pushed to the end side.
Filters, actions and the total are on by default — there is no flag to turn them on. A page that supplies a filter or an action gets it rendered, and the total is shown whenever the page knows one. They came from Admin Panel / People, where they were hand-written into that page's own markup; they are general enough for any table, so they live here.
filters: buildFilters([{ id, labelKey, options: [{ value, labelKey|label }] }], current)
actions: buildActions([{ id, labelKey, icon, cls }])
total: <number> // shown when non-zero
totalLabelKey: 'people-number' // optional label before it
- A filter is one
<select>; the page readsdata-filterand the selected value. The option matchingcurrentis selected, compared as strings so a numeric value still matches. - An action is one button; the page reads
data-actionto know which was pressed. It is filled with the theme accent (an action, not navigation), and it brings no geometry of its own — a page that gave its action button a class with a margin and a height of its own put it at a different height from every other control in the row. - The total counts the whole result set, not the page — it sits with the controls for that reason, and inherits its colour so it stays readable on a dark theme.
Three controls are always present, with the same class names on every page:
.js-table-page-search— type and press Enter. Searching resets to page 1 and recounts..js-table-page-prev/.js-table-page-next— disabled at the ends viahasPrev/hasNext..js-table-page-edit-user— a username cell, opening the Edit user popup.
Because the class names are shared, the handlers are shared too: the page being acted on is identified by which report is open, not by a per-page class. Adding a table page therefore adds no new event handler, no new markup and no new CSS.
Theme
A table page never invents a colour. Its buttons and counter follow whatever theme is in force:
- Per-user theme — Member Settings → Change color sets
--theme-accent(and--theme-accent-2) on:root. Everything themed picks that up immediately, for that user only. - WeKan default — with no custom colour chosen,
--theme-accentis unset and every rule falls back to the WeKan blue#01628cthroughvar(--theme-accent, #01628c).
The prev/next buttons, the action buttons and the "3 / 42" counter are styled
in one place for the whole app —
client/components/main/paginationControls.css — which the Admin
Panel tables, People / Organizations / Teams / Domains, All Boards, the board
Table view, Archived boards and the Cron settings tables all share. A table page
gets that look by using the shared class names (.table-page-pagination,
.table-page-page-info); it adds nothing of its own.
Do not restate those colours in a page stylesheet. That file spells out
:hover, :focus, :active and :active:hover deliberately, because the
global button rules in client/components/forms/forms.css set the same states
from --theme-accent with a black / dark-grey fallback, at equal or higher
specificity:
button { background: var(--theme-accent, #000) }
button:focus { background: var(--theme-accent, #222) }
button:active { background: var(--theme-accent, #111) }
button:active:hover { background: #e6e6e6 }
A partial copy — say only the base state and :hover — therefore looks right
until the button is clicked, and then loses to button:focus /
button:active:hover and leaves a black or grey button sitting on the page for as
long as it keeps focus. That is why the colours live in one file that covers every
state, and why tablePage.css carries layout only.
The rest of the page inherits: the title and cell text take the surrounding text
colour, and the counter uses color: inherit so it stays readable in both the
light and the dark theme rather than being pinned to a hard-coded grey.
RTL and dates
- The layout is plain block order plus logical properties (
margin-inline-start,text-align: start), so it mirrors underdir=rtlwithout duplicated markup. - Datetime columns use the app's configured date format helper, so a table page matches the rest of the UI.
Adding a table page
- Add the column list.
- Point it at a publication that honours
limit/skipand a count method. - Add the menu item.
Nothing else: no template, no CSS, no handlers, no paging code.
The files are listed at the top of this page.