Architecture review fixes: partials carry the locale, mountable locales/, shared core words
CI / full-gate (push) Successful in 2m36s
CI / full-gate (push) Successful in 2m36s
This commit is contained in:
@@ -143,9 +143,9 @@ function listTable(rows: ClientView[], t: Translate) {
|
||||
|
||||
function listFilterBar(state: ListState, t: Translate) {
|
||||
const pills: { label: string; remove: string; value: string }[] = [];
|
||||
if (state.q) pills.push({ label: t("admin.common.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
if (state.q) pills.push({ label: t("filter.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
return {
|
||||
applyLabel: t("admin.common.apply"),
|
||||
applyLabel: t("filter.apply"),
|
||||
clearHref: ADMIN_CLIENTS_BASE,
|
||||
label: t("admin.clients.filter"),
|
||||
pills,
|
||||
@@ -167,7 +167,7 @@ function listPagination(state: ListState, page: ReturnType<typeof paginate>, t:
|
||||
: p.current ? { current: true, label: String(p.page) }
|
||||
: { href: listHref(state, { page: p.page as number }), label: String(p.page) }),
|
||||
prev: { href: page.prev ? listHref(state, { page: page.prev }) : undefined },
|
||||
rows: { hidden, label: t("admin.common.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("admin.common.go"), value: state.pageSize },
|
||||
rows: { hidden, label: t("pagination.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("pagination.go"), value: state.pageSize },
|
||||
summary: { from: page.from, to: page.to, total: page.total },
|
||||
};
|
||||
}
|
||||
@@ -302,7 +302,7 @@ export const clientsDeleteConfirm = withClient((deps, client, id) => {
|
||||
const name = toClientView(client).name;
|
||||
const tt = deps.ctx.t;
|
||||
return Promise.resolve({ data: { chrome: deps.ctx.chrome, model: buildConfirmModel({
|
||||
breadcrumbs: [{ href: ADMIN_CLIENTS_BASE, label: tt("admin.clients.title") }, { href: base, label: name }, { label: tt("admin.common.delete") }],
|
||||
breadcrumbs: [{ href: ADMIN_CLIENTS_BASE, label: tt("admin.clients.title") }, { href: base, label: name }, { label: tt("common.delete") }],
|
||||
cancelHref: base, confirmAction: `${base}/delete`, confirmLabel: tt("admin.clients.delete"),
|
||||
message: tt("admin.clients.deleteMessage", { name }), title: tt("admin.clients.delete"),
|
||||
}) }, view: "confirm" });
|
||||
|
||||
@@ -163,9 +163,9 @@ function listTable(rows: GroupView[], state: ListState, sort: { dir: "asc" | "de
|
||||
|
||||
function listFilterBar(state: ListState, t: Translate) {
|
||||
const pills: { label: string; remove: string; value: string }[] = [];
|
||||
if (state.q) pills.push({ label: t("admin.common.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
if (state.q) pills.push({ label: t("filter.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
return {
|
||||
applyLabel: t("admin.common.apply"),
|
||||
applyLabel: t("filter.apply"),
|
||||
clearHref: ADMIN_GROUPS_BASE,
|
||||
label: t("admin.groups.filter"),
|
||||
pills,
|
||||
@@ -188,7 +188,7 @@ function listPagination(state: ListState, page: ReturnType<typeof paginate>, t:
|
||||
: p.current ? { current: true, label: String(p.page) }
|
||||
: { href: listHref(state, { page: p.page as number }), label: String(p.page) }),
|
||||
prev: { href: page.prev ? listHref(state, { page: page.prev }) : undefined },
|
||||
rows: { hidden, label: t("admin.common.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("admin.common.go"), value: state.pageSize },
|
||||
rows: { hidden, label: t("pagination.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("pagination.go"), value: state.pageSize },
|
||||
summary: { from: page.from, to: page.to, total: page.total },
|
||||
};
|
||||
}
|
||||
@@ -208,7 +208,7 @@ export function buildGroupFormModel(opts: {
|
||||
id: "name", label: t("admin.groups.field.name"), name: "name", required: true, value: opts.values?.name ?? "",
|
||||
};
|
||||
return {
|
||||
breadcrumbs: [{ href: ADMIN_GROUPS_BASE, label: t("admin.groups.title") }, { label: t("admin.common.new") }],
|
||||
breadcrumbs: [{ href: ADMIN_GROUPS_BASE, label: t("admin.groups.title") }, { label: t("common.new") }],
|
||||
error: opts.error,
|
||||
form: {
|
||||
action: ADMIN_GROUPS_BASE,
|
||||
@@ -358,7 +358,7 @@ export const groupsDeleteConfirm = withGroupName((deps, name) => {
|
||||
const base = detailHref(name);
|
||||
const tt = deps.ctx.t;
|
||||
return Promise.resolve({ data: { chrome: deps.ctx.chrome, model: buildConfirmModel({
|
||||
breadcrumbs: [{ href: ADMIN_GROUPS_BASE, label: tt("admin.groups.title") }, { href: base, label: name }, { label: tt("admin.common.delete") }],
|
||||
breadcrumbs: [{ href: ADMIN_GROUPS_BASE, label: tt("admin.groups.title") }, { href: base, label: name }, { label: tt("common.delete") }],
|
||||
cancelHref: base, confirmAction: `${base}/delete`, confirmLabel: tt("admin.groups.delete"),
|
||||
message: tt("admin.groups.deleteMessage", { name }), title: tt("admin.groups.delete"),
|
||||
}) }, view: "confirm" });
|
||||
|
||||
@@ -148,9 +148,9 @@ function listTable(rows: PermissionView[], state: ListState, sort: { dir: "asc"
|
||||
|
||||
function listFilterBar(state: ListState, t: Translate) {
|
||||
const pills: { label: string; remove: string; value: string }[] = [];
|
||||
if (state.q) pills.push({ label: t("admin.common.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
if (state.q) pills.push({ label: t("filter.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
return {
|
||||
applyLabel: t("admin.common.apply"),
|
||||
applyLabel: t("filter.apply"),
|
||||
clearHref: ADMIN_PERMISSIONS_BASE,
|
||||
label: t("admin.permissions.filter"),
|
||||
pills,
|
||||
@@ -173,7 +173,7 @@ function listPagination(state: ListState, page: ReturnType<typeof paginate>, t:
|
||||
: p.current ? { current: true, label: String(p.page) }
|
||||
: { href: listHref(state, { page: p.page as number }), label: String(p.page) }),
|
||||
prev: { href: page.prev ? listHref(state, { page: page.prev }) : undefined },
|
||||
rows: { hidden, label: t("admin.common.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("admin.common.go"), value: state.pageSize },
|
||||
rows: { hidden, label: t("pagination.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("pagination.go"), value: state.pageSize },
|
||||
summary: { from: page.from, to: page.to, total: page.total },
|
||||
};
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function buildPermissionFormModel(opts: {
|
||||
id: "name", label: t("admin.permissions.field.name"), name: "name", required: true, value: opts.values?.name ?? "",
|
||||
};
|
||||
return {
|
||||
breadcrumbs: [{ href: ADMIN_PERMISSIONS_BASE, label: t("admin.permissions.title") }, { label: t("admin.common.new") }],
|
||||
breadcrumbs: [{ href: ADMIN_PERMISSIONS_BASE, label: t("admin.permissions.title") }, { label: t("common.new") }],
|
||||
error: opts.error,
|
||||
form: {
|
||||
action: ADMIN_PERMISSIONS_BASE,
|
||||
@@ -343,7 +343,7 @@ export const rolesDeleteConfirm = withRoleName((deps, name) => {
|
||||
const base = detailHref(name);
|
||||
const tt = deps.ctx.t;
|
||||
return Promise.resolve({ data: { chrome: deps.ctx.chrome, model: buildConfirmModel({
|
||||
breadcrumbs: [{ href: ADMIN_PERMISSIONS_BASE, label: tt("admin.permissions.title") }, { href: base, label: name }, { label: tt("admin.common.delete") }],
|
||||
breadcrumbs: [{ href: ADMIN_PERMISSIONS_BASE, label: tt("admin.permissions.title") }, { href: base, label: name }, { label: tt("common.delete") }],
|
||||
cancelHref: base, confirmAction: `${base}/delete`, confirmLabel: tt("admin.permissions.delete"),
|
||||
message: tt("admin.permissions.deleteMessage", { name }), title: tt("admin.permissions.delete"),
|
||||
}) }, view: "confirm" });
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
// (themed not-found / capability-unavailable). Ported from the former built-in admin screens;
|
||||
// everything imports the host only through the #plugin-api barrel.
|
||||
|
||||
import { can, createTranslator, CSRF_FIELD, GuardError, type NavNode, readFormBody, type RequestContext, requireSession, type RouteResult, type Translate, type User } from "#plugin-api";
|
||||
import { can, CSRF_FIELD, englishTranslator, GuardError, type NavNode, readFormBody, type RequestContext, requireSession, type RouteResult, type Translate, type User } from "#plugin-api";
|
||||
import enUS from "./i18n/en-US.ts";
|
||||
|
||||
// This plugin's English, for a view model built outside a request (its unit tests). At runtime the
|
||||
// handlers pass ctx.t, which reads this catalog in the visitor's locale first, then the host's.
|
||||
export const ADMIN_EN: Translate = createTranslator({ catalogs: [enUS], locale: "en-US" });
|
||||
// This plugin's English (its catalog, then the host's — the screens reuse core words like Cancel and
|
||||
// Search), for a view model built outside a request: its unit tests. At runtime the handlers pass
|
||||
// ctx.t, which reads this catalog in the visitor's locale first, then the host's.
|
||||
export const ADMIN_EN: Translate = englishTranslator(enUS);
|
||||
|
||||
export const ADMIN_PERMISSION = "admin"; // the permission gating the whole admin section
|
||||
export const ADMIN_USERS_BASE = "/admin/users";
|
||||
|
||||
@@ -151,7 +151,7 @@ function listTable(rows: UserView[], state: ListState, sort: { dir: "asc" | "des
|
||||
return { href: listHref(state, { page: 1, sort: next }), label: t(c.label), sort: dir, sortable: true };
|
||||
}),
|
||||
rows: rows.map((u) => ({
|
||||
actions: [{ href: `${ADMIN_USERS_BASE}/${encodeURIComponent(u.id)}`, icon: "i-edit", label: t("admin.common.edit") }],
|
||||
actions: [{ href: `${ADMIN_USERS_BASE}/${encodeURIComponent(u.id)}`, icon: "i-edit", label: t("common.edit") }],
|
||||
cells: [
|
||||
{ user: { initials: u.initials, name: u.name } },
|
||||
u.email,
|
||||
@@ -164,7 +164,7 @@ function listTable(rows: UserView[], state: ListState, sort: { dir: "asc" | "des
|
||||
|
||||
function listFilterBar(state: ListState, total: number, t: Translate) {
|
||||
const pills: { label: string; remove: string; value: string }[] = [];
|
||||
if (state.q) pills.push({ label: t("admin.common.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
if (state.q) pills.push({ label: t("filter.search"), remove: listHref(state, { page: 1, q: "" }), value: state.q });
|
||||
if (state.status !== "all") pills.push({ label: t("admin.users.status.label"), remove: listHref(state, { page: 1, status: "all" }), value: t(`admin.users.status.${state.status}`) });
|
||||
return {
|
||||
applyLabel: t("filter.apply"), // an untranslated core key still resolves: the host catalog is the fallback
|
||||
@@ -196,7 +196,7 @@ function listPagination(state: ListState, page: ReturnType<typeof paginate>, t:
|
||||
: p.current ? { current: true, label: String(p.page) }
|
||||
: { href: listHref(state, { page: p.page as number }), label: String(p.page) }),
|
||||
prev: { href: page.prev ? listHref(state, { page: page.prev }) : undefined },
|
||||
rows: { hidden, label: t("admin.common.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("admin.common.go"), value: state.pageSize },
|
||||
rows: { hidden, label: t("pagination.rows"), name: "pageSize", options: PAGE_SIZES, submitLabel: t("pagination.go"), value: state.pageSize },
|
||||
summary: { from: page.from, to: page.to, total: page.total },
|
||||
};
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export function buildUserFormModel(opts: {
|
||||
if (!editing) fields.push({ autocomplete: "new-password", hint: t("admin.users.field.passwordHint"), icon: "i-lock", id: "password", label: t("admin.users.field.password"), name: "password", optional: true, type: "password" });
|
||||
|
||||
return {
|
||||
breadcrumbs: [{ href: ADMIN_USERS_BASE, label: t("admin.users.title") }, { label: editing ? t("admin.common.edit") : t("admin.common.new") }],
|
||||
breadcrumbs: [{ href: ADMIN_USERS_BASE, label: t("admin.users.title") }, { label: editing ? t("common.edit") : t("common.new") }],
|
||||
edit: editing ? {
|
||||
deleteAction: `${idPath}/delete`,
|
||||
id: view!.id,
|
||||
@@ -351,7 +351,7 @@ export const usersDeleteConfirm = withTarget((deps, identity, id) => {
|
||||
const view = toUserView(identity);
|
||||
const tt = deps.ctx.t;
|
||||
return Promise.resolve({ data: { chrome: deps.ctx.chrome, model: buildConfirmModel({
|
||||
breadcrumbs: [{ href: ADMIN_USERS_BASE, label: tt("admin.users.title") }, { href: back, label: view.name }, { label: tt("admin.common.delete") }],
|
||||
breadcrumbs: [{ href: ADMIN_USERS_BASE, label: tt("admin.users.title") }, { href: back, label: view.name }, { label: tt("common.delete") }],
|
||||
cancelHref: back, confirmAction: `${back}/delete`, confirmLabel: tt("admin.users.delete"),
|
||||
message: tt("admin.users.deleteMessage", { email: view.email }), title: tt("admin.users.delete"),
|
||||
}) }, view: "confirm" });
|
||||
|
||||
@@ -42,20 +42,9 @@ const messages = {
|
||||
"admin.clients.validation.redirectUri": "\"{{uri}}\" is not a valid redirect URI — use an absolute URL like https://app.example.com/callback.",
|
||||
"admin.clients.validation.redirectUris": "Add at least one redirect URI.",
|
||||
|
||||
"admin.common.actions": "Actions",
|
||||
"admin.common.add": "Add",
|
||||
"admin.common.apply": "Apply",
|
||||
"admin.common.cancel": "Cancel",
|
||||
"admin.common.chooseMember": "Choose a user or group…",
|
||||
"admin.common.delete": "Delete",
|
||||
"admin.common.edit": "Edit",
|
||||
"admin.common.go": "Go",
|
||||
"admin.common.group": "Group",
|
||||
"admin.common.member": "Member",
|
||||
"admin.common.new": "New",
|
||||
"admin.common.remove": "Remove",
|
||||
"admin.common.rows": "Rows",
|
||||
"admin.common.search": "Search",
|
||||
"admin.common.type": "Type",
|
||||
"admin.common.user": "User",
|
||||
|
||||
|
||||
@@ -42,20 +42,9 @@ const messages: AdminMessages = {
|
||||
"admin.clients.validation.redirectUri": "\"{{uri}}\" är inte en giltig omdirigerings-URI — använd en absolut URL som https://app.example.com/callback.",
|
||||
"admin.clients.validation.redirectUris": "Lägg till minst en omdirigerings-URI.",
|
||||
|
||||
"admin.common.actions": "Åtgärder",
|
||||
"admin.common.add": "Lägg till",
|
||||
"admin.common.apply": "Använd",
|
||||
"admin.common.cancel": "Avbryt",
|
||||
"admin.common.chooseMember": "Välj en användare eller grupp…",
|
||||
"admin.common.delete": "Ta bort",
|
||||
"admin.common.edit": "Redigera",
|
||||
"admin.common.go": "Visa",
|
||||
"admin.common.group": "Grupp",
|
||||
"admin.common.member": "Medlem",
|
||||
"admin.common.new": "Ny",
|
||||
"admin.common.remove": "Ta bort",
|
||||
"admin.common.rows": "Rader",
|
||||
"admin.common.search": "Sök",
|
||||
"admin.common.type": "Typ",
|
||||
"admin.common.user": "Användare",
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<span class="field-hint"><%= t("admin.clients.field.typeHint") %></span>
|
||||
<label class="check"><input type="checkbox" name="firstParty"<% if (form.firstParty) { %> checked<% } %>> First-party (auto-grant consent — skip the consent screen)</label>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("admin.common.cancel") %></a>
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("common.cancel") %></a>
|
||||
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<section class="form-card admin-actions" aria-label="<%= t("admin.users.confirm") %>">
|
||||
<p><%= locals.message %></p>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= localeHref(locals.cancelHref) %>"><%= t("admin.common.cancel") %></a>
|
||||
<a class="btn" href="<%= localeHref(locals.cancelHref) %>"><%= t("common.cancel") %></a>
|
||||
<form method="post" action="<%= locals.confirm.action %>"><input type="hidden" name="_csrf" value="<%= locals.csrfToken %>"><button class="btn btn-danger" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-trash"/></svg><%= locals.confirm.label %></button></form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<section class="form-card" aria-labelledby="members-h">
|
||||
<h2 class="card-title" id="members-h"><%= t("admin.groups.members") %></h2>
|
||||
<% if (members.rows.length) { -%>
|
||||
<div class="table-wrap"><table class="table"><caption class="sr-only"><%= t("admin.groups.membersOf", { name: group.name }) %></caption><thead><tr><th scope="col"><%= t("admin.common.member") %></th><th scope="col"><%= t("admin.common.type") %></th><th class="col-actions" scope="col"><span class="sr-only"><%= t("admin.common.actions") %></span></th></tr></thead><tbody>
|
||||
<div class="table-wrap"><table class="table"><caption class="sr-only"><%= t("admin.groups.membersOf", { name: group.name }) %></caption><thead><tr><th scope="col"><%= t("admin.common.member") %></th><th scope="col"><%= t("admin.common.type") %></th><th class="col-actions" scope="col"><span class="sr-only"><%= t("table.actions") %></span></th></tr></thead><tbody>
|
||||
<% members.rows.forEach((m) => { -%>
|
||||
<tr><th scope="row"><span class="cell-strong"><%= m.label %></span></th><td><span class="badge info"><span class="dot"></span><%= m.kind === "group" ? t("admin.common.group") : t("admin.common.user") %></span></td><td class="col-actions"><form method="post" action="<%= members.action %>"><input type="hidden" name="_csrf" value="<%= csrf %>"><input type="hidden" name="member" value="<%= m.subject %>"><button class="btn" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-x"/></svg><%= t("admin.common.remove") %></button></form></td></tr>
|
||||
<tr><th scope="row"><span class="cell-strong"><%= m.label %></span></th><td><span class="badge info"><span class="dot"></span><%= m.kind === "group" ? t("admin.common.group") : t("admin.common.user") %></span></td><td class="col-actions"><form method="post" action="<%= members.action %>"><input type="hidden" name="_csrf" value="<%= csrf %>"><input type="hidden" name="member" value="<%= m.subject %>"><button class="btn" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-x"/></svg><%= t("common.remove") %></button></form></td></tr>
|
||||
<% }) -%>
|
||||
</tbody></table></div>
|
||||
<% } else { -%>
|
||||
@@ -31,7 +31,7 @@
|
||||
<section class="form-card" aria-labelledby="add-h">
|
||||
<h2 class="card-title" id="add-h"><%= t("admin.groups.addMember") %></h2>
|
||||
<% if (add.options.length) { -%>
|
||||
<form class="inline-form" method="post" action="<%= add.action %>"><input type="hidden" name="_csrf" value="<%= csrf %>"><label class="sr-only" for="add-member"><%= t("admin.common.member") %></label><span class="select"><select id="add-member" name="member" required><option value="" disabled selected><%= t("admin.common.chooseMember") %></option><% add.options.forEach((o) => { %><option value="<%= o.value %>"><%= o.label %></option><% }) %></select></span><button class="btn btn-primary" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-plus"/></svg><%= t("admin.common.add") %></button></form>
|
||||
<form class="inline-form" method="post" action="<%= add.action %>"><input type="hidden" name="_csrf" value="<%= csrf %>"><label class="sr-only" for="add-member"><%= t("admin.common.member") %></label><span class="select"><select id="add-member" name="member" required><option value="" disabled selected><%= t("admin.common.chooseMember") %></option><% add.options.forEach((o) => { %><option value="<%= o.value %>"><%= o.label %></option><% }) %></select></span><button class="btn btn-primary" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-plus"/></svg><%= t("common.add") %></button></form>
|
||||
<% } else { -%>
|
||||
<p class="cell-muted"><%= t("admin.groups.allMembers") %></p>
|
||||
<% } -%>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="field-hint"><%= t("admin.groups.firstMemberHint") %></span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("admin.common.cancel") %></a>
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("common.cancel") %></a>
|
||||
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<section class="form-card" aria-labelledby="members-h">
|
||||
<h2 class="card-title" id="members-h"><%= t("admin.permissions.assignedTo") %></h2>
|
||||
<% if (members.rows.length) { -%>
|
||||
<div class="table-wrap"><table class="table"><caption class="sr-only"><%= t("admin.groups.membersOf", { name: permission.name }) %></caption><thead><tr><th scope="col"><%= t("admin.common.member") %></th><th scope="col"><%= t("admin.common.type") %></th><th class="col-actions" scope="col"><span class="sr-only"><%= t("admin.common.actions") %></span></th></tr></thead><tbody>
|
||||
<div class="table-wrap"><table class="table"><caption class="sr-only"><%= t("admin.groups.membersOf", { name: permission.name }) %></caption><thead><tr><th scope="col"><%= t("admin.common.member") %></th><th scope="col"><%= t("admin.common.type") %></th><th class="col-actions" scope="col"><span class="sr-only"><%= t("table.actions") %></span></th></tr></thead><tbody>
|
||||
<% members.rows.forEach((m) => { -%>
|
||||
<tr><th scope="row"><span class="cell-strong"><%= m.label %></span></th><td><span class="badge info"><span class="dot"></span><%= m.kind === "group" ? t("admin.common.group") : t("admin.common.user") %></span></td><td class="col-actions"><form method="post" action="<%= members.action %>"><input type="hidden" name="_csrf" value="<%= csrf %>"><input type="hidden" name="member" value="<%= m.subject %>"><button class="btn" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-x"/></svg><%= t("admin.permissions.revoke") %></button></form></td></tr>
|
||||
<% }) -%>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="field-hint">A permission exists once assigned; add more users or groups after creating it.</span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("admin.common.cancel") %></a>
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("common.cancel") %></a>
|
||||
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<%- include("partials/field", field) %>
|
||||
<% }) -%>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("admin.common.cancel") %></a>
|
||||
<a class="btn" href="<%= localeHref(form.cancelHref) %>"><%= t("common.cancel") %></a>
|
||||
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
import type { PluralMessage } from "#plugin-api";
|
||||
|
||||
const messages = {
|
||||
"scheduling.cancel": "Cancel",
|
||||
"scheduling.field.assignee": "Assignee",
|
||||
"scheduling.field.end": "End",
|
||||
"scheduling.field.start": "Start",
|
||||
"scheduling.field.title": "Shift title",
|
||||
"scheduling.filter.label": "Filter shifts",
|
||||
"scheduling.filter.search": "Search",
|
||||
"scheduling.filter.searchLabel": "Search shifts",
|
||||
"scheduling.filter.searchPlaceholder": "Search title or assignee…",
|
||||
"scheduling.form.submit": "Create shift",
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { SchedulingMessages } from "./en-US.ts";
|
||||
|
||||
const messages: SchedulingMessages = {
|
||||
"scheduling.cancel": "Avbryt",
|
||||
"scheduling.field.assignee": "Tilldelad",
|
||||
"scheduling.field.end": "Slut",
|
||||
"scheduling.field.start": "Start",
|
||||
"scheduling.field.title": "Passets namn",
|
||||
"scheduling.filter.label": "Filtrera pass",
|
||||
"scheduling.filter.search": "Sök",
|
||||
"scheduling.filter.searchLabel": "Sök pass",
|
||||
"scheduling.filter.searchPlaceholder": "Sök på namn eller person…",
|
||||
"scheduling.form.submit": "Skapa pass",
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Readable } from "node:stream";
|
||||
import test from "node:test";
|
||||
// Import only from the #plugin-api barrel — the same contract boundary shifts.ts uses (the host may
|
||||
// refactor any deeper src/* freely behind it); the test models the dev/test story the contract preaches.
|
||||
import { createTranslator, GuardError, Log, type PageChrome, type RequestContext, type RouteResult } from "#plugin-api";
|
||||
import { englishTranslator, GuardError, Log, type PageChrome, type RequestContext, type RouteResult } from "#plugin-api";
|
||||
import enUS from "./i18n/en-US.ts";
|
||||
import {
|
||||
assertHttpUrl, buildFormModel, createShift, createUpstream, listShifts, newShiftForm, overview, readInput,
|
||||
SHIFTS_PATH, type Shift, type ShiftInput, type ShiftsUpstream, UpstreamError, validate,
|
||||
} from "./shifts.ts";
|
||||
|
||||
const t = createTranslator({ catalogs: [enUS], locale: "en-US" }); // this plugin's own catalog, as the host would pass it
|
||||
const t = englishTranslator(enUS); // this plugin's catalog then the host's, as the host would chain them
|
||||
const CHROME: PageChrome = { brand: { name: "Test" }, csrfToken: "tok", nav: [], signInHref: "/login", user: { email: "", initials: "T", name: "Tester" } };
|
||||
|
||||
function fakeCtx(opts: { body?: string; permissions?: string[]; url?: string; verifyCsrf?: (s: string | null | undefined) => boolean } = {}): RequestContext {
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
// pure functions against a mock upstream with no network (README.md → Local dev & test story).
|
||||
|
||||
// One import from the host's #plugin-api barrel — the stable author surface (see README.md → Building plugins).
|
||||
import { can, createTranslator, CSRF_FIELD, GuardError, type PageChrome, parseListQuery, readFormBody, type RouteHandler, type Translate, tracedFetch } from "#plugin-api";
|
||||
import { can, CSRF_FIELD, englishTranslator, GuardError, type PageChrome, parseListQuery, readFormBody, type RouteHandler, type Translate, tracedFetch } from "#plugin-api";
|
||||
import enUS from "./i18n/en-US.ts";
|
||||
|
||||
// The plugin's own English, for a view model built outside a request (its unit tests). At runtime a
|
||||
// handler passes ctx.t, which reads this plugin's catalog for the visitor's locale first.
|
||||
const EN: Translate = createTranslator({ catalogs: [enUS], locale: "en-US" });
|
||||
// The plugin's own English (its catalog, then the host's), for a view model built outside a request:
|
||||
// its unit tests. At runtime a handler passes ctx.t, which reads this catalog in the visitor's
|
||||
// locale first, then the host's.
|
||||
const EN: Translate = englishTranslator(enUS);
|
||||
|
||||
export const SCHEDULING_PATH = "/scheduling"; // the plugin's public overview page
|
||||
export const SHIFTS_PATH = "/scheduling/shifts";
|
||||
@@ -102,10 +103,10 @@ export function buildListModel(opts: { canWrite: boolean; chrome: PageChrome; er
|
||||
count: t("scheduling.shifts.count", { count: opts.shifts.length }),
|
||||
...(opts.error ? { error: opts.error } : {}),
|
||||
filterBar: {
|
||||
applyLabel: t("scheduling.filter.search"),
|
||||
applyLabel: t("filter.search"),
|
||||
clearHref: SHIFTS_PATH,
|
||||
label: t("scheduling.filter.label"),
|
||||
pills: opts.q ? [{ label: t("scheduling.filter.search"), remove: SHIFTS_PATH, value: opts.q }] : [],
|
||||
pills: opts.q ? [{ label: t("filter.search"), remove: SHIFTS_PATH, value: opts.q }] : [],
|
||||
rows: [[
|
||||
{ label: t("scheduling.filter.searchLabel"), name: "q", placeholder: t("scheduling.filter.searchPlaceholder"), type: "search", value: opts.q },
|
||||
{ type: "spacer" },
|
||||
@@ -139,7 +140,7 @@ export function buildFormModel(opts: { chrome: PageChrome; errors?: Record<strin
|
||||
action: SHIFTS_PATH,
|
||||
cancelHref: SHIFTS_PATH,
|
||||
csrfToken: opts.chrome.csrfToken,
|
||||
cancelLabel: t("scheduling.cancel"),
|
||||
cancelLabel: t("common.cancel"),
|
||||
fields: [
|
||||
field({ icon: "i-cal", id: "title", label: t("scheduling.field.title"), value: v.title ?? "" }),
|
||||
field({ icon: "i-user", id: "assignee", label: t("scheduling.field.assignee"), value: v.assignee ?? "" }),
|
||||
|
||||
Reference in New Issue
Block a user