Compare commits
2 Commits
709b5d4594
...
650b684ae8
| Author | SHA1 | Date | |
|---|---|---|---|
| 650b684ae8 | |||
|
|
5c8559c8e9 |
@ -124,6 +124,25 @@ test('pipeline: L2 drafts, L1 approves, admin publishes; returns, rejects, permi
|
||||
const hist = (await call('GET', `/api/mod/work/${poem.id}`, undefined, l1.token)).json();
|
||||
assert.deepEqual(hist.history.filter((h: any) => h.version).map((h: any) => h.version), [2, 1]);
|
||||
assert.ok((await call('GET', '/api/mod/log', undefined, l2.token)).json().entries.length >= 10);
|
||||
|
||||
// version history (#51): compare any two versions, bring an old one back through the pipeline, filter the log
|
||||
const cmp = (await call('GET', `/api/mod/compare/work/${poem.id}`, undefined, l2.token)).json();
|
||||
assert.deepEqual([cmp.a, cmp.b, cmp.changes], [1, 2, 2], 'by default the latest version against the one before');
|
||||
assert.ok(cmp.diff.some((d: any) => d.op === '+' && d.text.includes('(ترمیم دوم)')));
|
||||
const v0 = (await call('GET', `/api/mod/compare/work/${poem.id}?a=0&b=2`, undefined, l2.token)).json();
|
||||
assert.ok(v0.diff.some((d: any) => d.op === '-' && d.text === misra), 'version 0 is the Wikisource text');
|
||||
assert.equal((await call('GET', `/api/mod/compare/work/${poem.id}?a=7`, undefined, l2.token)).statusCode, 404);
|
||||
assert.equal((await call('POST', `/api/mod/revert/work/${poem.id}`, { version: 0 }, other.token)).statusCode, 403, 'outside the grant');
|
||||
assert.equal((await call('POST', `/api/mod/revert/work/${poem.id}`, { version: 2 }, l2.token)).statusCode, 400, 'already the current version');
|
||||
const back = (await call('POST', `/api/mod/revert/work/${poem.id}`, { version: 0 }, l2.token)).json().id;
|
||||
const br = (await call('GET', `/api/mod/revisions/${back}`, undefined, l2.token)).json();
|
||||
assert.deepEqual([br.revision.status, br.revision.base_version, br.revision.summary], ['draft', 2, 'ویکی ماخذ کا متن واپس لایا']);
|
||||
assert.ok(br.diff.some((d: any) => d.op === '+' && d.text === misra), 'the draft brings the old line back');
|
||||
assert.deepEqual(br.events.map((e: any) => e.action), ['created', 'reverted']);
|
||||
assert.deepEqual((await call('POST', `/api/mod/revisions/${back}/submit`, {}, l2.token)).json().status, 'submitted', 'then reviewed like any edit');
|
||||
const mine = (await call('GET', `/api/mod/log?who=l2-${run}&action=reverted`, undefined, l1.token)).json().entries;
|
||||
assert.deepEqual(mine.map((e: any) => e.revision), [back], 'log filtered by person and step');
|
||||
assert.equal((await call('GET', `/api/mod/log?who=l2-${run}&kind=order`, undefined, l1.token)).json().entries.length, 0);
|
||||
} finally {
|
||||
// restore the work and remove the test people and their revisions
|
||||
await pool.query('DELETE FROM verses WHERE poem_id = $1', [poem.id]);
|
||||
|
||||
@ -23,7 +23,10 @@
|
||||
// POST /api/mod/tags/:kind/:id/draft start (or reopen) my tagging draft
|
||||
// POST /api/mod/revisions/:id/save {content, summary}
|
||||
// POST /api/mod/revisions/:id/:action submit | approve | return | reject | publish {comment}
|
||||
// GET /api/mod/log?page= who did what, newest first
|
||||
// GET /api/mod/log?page=&who=&kind=&action= who did what, newest first; filtered by person, kind of change, step
|
||||
// GET /api/mod/compare/:entity/:id?a=&b= two published versions of a work, order or tags and their diff (0 = the
|
||||
// Wikisource text before Divan's first version)
|
||||
// POST /api/mod/revert/:entity/:id {version}: a draft that brings back that version, through the pipeline
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { pool } from './db.ts';
|
||||
import { sessionUser } from './auth.ts';
|
||||
@ -107,8 +110,7 @@ async function actions(u: any, r: any) {
|
||||
|
||||
async function publish(r: any, u: any, comment?: string) {
|
||||
const order = r.entity === 'order', tags = r.entity.startsWith('tags-'), ebook = r.entity === 'ebook';
|
||||
const cur = order ? await currentOrder(r.entity_id) : tags ? await currentTags(tagKind(r), r.entity_id)
|
||||
: ebook ? await currentEbook(r.entity_id) : await current(r.entity_id);
|
||||
const cur = await currentOf(r.entity, r.entity_id);
|
||||
if (!cur) throw Object.assign(new Error('کلام نہیں ملا'), { code: 404 });
|
||||
if (cur.version !== r.base_version)
|
||||
throw Object.assign(new Error('اس دوران اس کا نیا ورژن شائع ہو چکا ہے۔ مسودہ واپس بھیج کر تازہ متن پر دوبارہ بنوائیں۔'), { code: 409 });
|
||||
@ -162,6 +164,20 @@ async function publish(r: any, u: any, comment?: string) {
|
||||
return version;
|
||||
}
|
||||
|
||||
// what an entity is now (its latest published version), for any kind of revision
|
||||
const currentOf = (entity: string, id: number): Promise<any> => entity === 'order' ? currentOrder(id)
|
||||
: entity.startsWith('tags-') ? currentTags(tagKind({ entity }), id) : entity === 'ebook' ? currentEbook(id) : current(id);
|
||||
// the kinds of change with versions to compare and bring back (an e-book's details are a one-off)
|
||||
const VERSIONED = ['work', 'order', 'tags-work', 'tags-category'];
|
||||
// a published version's content; version 0 is what the first Divan draft started from (the Wikisource text)
|
||||
async function versionText(entity: string, id: number, v: number, cur: { version: number; content: string }) {
|
||||
if (v === cur.version) return cur.content;
|
||||
const { rows } = await pool.query(v
|
||||
? `SELECT content FROM revisions WHERE entity = $1 AND entity_id = $2 AND status = 'published' AND version = $3`
|
||||
: `SELECT base_content AS content FROM revisions WHERE entity = $1 AND entity_id = $2 AND base_version = 0 AND $3 = 0 ORDER BY id LIMIT 1`, [entity, id, v]);
|
||||
return rows[0]?.content as string | undefined;
|
||||
}
|
||||
|
||||
const identityOf = (p: any) => identity({ id: Number(p.id), name: publicName(p) });
|
||||
|
||||
export function moderationRoutes(app: FastifyInstance) {
|
||||
@ -358,14 +374,53 @@ export function moderationRoutes(app: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get<{ Querystring: { page?: string } }>('/api/mod/log', async (req, reply) => {
|
||||
app.get<{ Querystring: { page?: string; who?: string; kind?: string; action?: string } }>('/api/mod/log', async (req, reply) => {
|
||||
const u = await moderator(req, reply); if (!u) return;
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const page = Math.max(1, Number(req.query.page) || 1), { who, kind, action } = req.query;
|
||||
// filters: a person (part of their address), a kind of change (tags covers both), a step; empty = all
|
||||
const { rows } = await pool.query(
|
||||
`SELECT e.at, e.actor_email, e.action, e.comment, r.id AS revision, r.entity, r.version, ${TARGET_TITLE} AS title, ${TARGET_URL} AS url
|
||||
FROM revision_events e JOIN revisions r ON r.id = e.revision_id ${TARGET}
|
||||
WHERE r.entity IN ${ENTITIES} AND ${CHANGED}
|
||||
ORDER BY e.at DESC, e.id DESC LIMIT 50 OFFSET ${(page - 1) * 50}`);
|
||||
AND ($1 = '' OR e.actor_email ILIKE '%' || $1 || '%') AND ($2 = '' OR r.entity = $2 OR r.entity LIKE $2 || '-%') AND ($3 = '' OR e.action = $3)
|
||||
ORDER BY e.at DESC, e.id DESC LIMIT 50 OFFSET ${(page - 1) * 50}`, [who?.trim() ?? '', kind ?? '', action ?? '']);
|
||||
return { page, entries: rows.map((r) => ({ ...r, revision: Number(r.revision) })) };
|
||||
});
|
||||
|
||||
app.get<{ Params: { entity: string; id: string }; Querystring: { a?: string; b?: string } }>('/api/mod/compare/:entity/:id', async (req, reply) => {
|
||||
const u = await moderator(req, reply); if (!u) return;
|
||||
const { entity } = req.params, id = Number(req.params.id) || 0;
|
||||
const cur = VERSIONED.includes(entity) ? await currentOf(entity, id) : null;
|
||||
if (!cur) return reply.code(404).send({ error: 'نہیں ملا' });
|
||||
const pick = (q: string | undefined, d: number) => (q === undefined || q === '' ? d : Number(q));
|
||||
const a = pick(req.query.a, Math.max(0, cur.version - 1)), b = pick(req.query.b, cur.version);
|
||||
const [ta, tb] = await Promise.all([a, b].map((v) => Number.isInteger(v) && v >= 0 && v <= cur.version ? versionText(entity, id, v, cur) : undefined));
|
||||
if (ta === undefined || tb === undefined) return reply.code(404).send({ error: 'یہ ورژن نہیں ملا' });
|
||||
const target = cur.poem ?? cur.cat ?? cur.target, diff = diffLines(ta, tb);
|
||||
return { entity, id, title: target.title, url: target.url, version: cur.version, a, b, diff, changes: changed(diff),
|
||||
may: { revert: await mayChange(u, { entity, entity_id: id }) } };
|
||||
});
|
||||
|
||||
app.post<{ Params: { entity: string; id: string }; Body: { version?: number } }>('/api/mod/revert/:entity/:id', async (req, reply) => {
|
||||
const u = await moderator(req, reply); if (!u) return;
|
||||
const { entity } = req.params, id = Number(req.params.id) || 0, v = Number(req.body?.version);
|
||||
const cur = VERSIONED.includes(entity) ? await currentOf(entity, id) : null;
|
||||
if (!cur) return reply.code(404).send({ error: 'نہیں ملا' });
|
||||
if (!(await mayChange(u, { entity, entity_id: id }))) return reply.code(403).send({ error: 'اس میں تبدیلی کی اجازت نہیں' });
|
||||
const content = Number.isInteger(v) && v >= 0 && v < cur.version ? await versionText(entity, id, v, cur) : undefined;
|
||||
if (content === undefined) return reply.code(400).send({ error: 'یہ ورژن واپس نہیں لایا جا سکتا' });
|
||||
if (content === cur.content) return reply.code(400).send({ error: 'یہ ورژن موجودہ ورژن جیسا ہی ہے' });
|
||||
// the reverted text goes into my open draft (or a new one) and then through review like any edit
|
||||
const summary = v ? `ورژن ${v} واپس لایا` : 'ویکی ماخذ کا متن واپس لایا';
|
||||
const open = (await pool.query(
|
||||
`SELECT id FROM revisions WHERE entity = $1 AND entity_id = $2 AND author_id = $3 AND status IN ('draft', 'returned') LIMIT 1`, [entity, id, u.id])).rows[0];
|
||||
const revId = open ? Number(open.id) : Number((await pool.query(
|
||||
`INSERT INTO revisions (entity, entity_id, base_version, base_content, content, status, author_id, author_email)
|
||||
VALUES ($1, $2, $3, $4, $4, 'draft', $5, $6) RETURNING id`, [entity, id, cur.version, cur.content, u.id, u.email])).rows[0].id);
|
||||
if (!open) await event(revId, u, 'created');
|
||||
await pool.query(`UPDATE revisions SET content = $2, summary = $3, base_version = $4, base_content = $5, updated_at = now() WHERE id = $1`,
|
||||
[revId, content, summary, cur.version, cur.content]);
|
||||
await event(revId, u, 'reverted', summary);
|
||||
return { id: revId };
|
||||
});
|
||||
}
|
||||
|
||||
36
web/src/components/History.astro
Normal file
36
web/src/components/History.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
// A work's, arrangement's or tags' versions (#51): compare any two published versions, see what each one changed
|
||||
// (فرق), and from there bring an old one back. Version 0 is the Wikisource text before Divan's first version.
|
||||
import { STATUS, when } from '../lib/mod';
|
||||
import { ud } from '../lib/urdu';
|
||||
interface Props { entity: string; id: number; history: any[]; version: number; empty: string }
|
||||
const { entity, id, history, version, empty } = Astro.props;
|
||||
const base = `/mod/compare/${entity}/${id}`;
|
||||
const versions = Array.from({ length: version + 1 }, (_, v) => v);
|
||||
const label = (v: number) => (v ? `ورژن ${ud(v)}` : 'ویکی ماخذ');
|
||||
---
|
||||
{version > 0 && (
|
||||
<form method="get" action={base} class="version-tools">
|
||||
<label>ورژن <select name="a">{versions.map((v) => <option value={v} selected={v === version - 1}>{label(v)}</option>)}</select></label>
|
||||
<label>اور <select name="b">{versions.map((v) => <option value={v} selected={v === version}>{label(v)}</option>)}</select></label>
|
||||
<button>موازنہ کریں</button>
|
||||
</form>
|
||||
)}
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>ورژن</th><th>حالت</th><th>خلاصہ</th><th>لکھنے والے</th><th>جائزہ</th><th>اشاعت</th><th>تاریخ</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{history.map((h: any) => (
|
||||
<tr>
|
||||
<td><a href={`/mod/rev/${h.id}`}>{h.version ? ud(h.version) : '—'}</a></td>
|
||||
<td><span class={`status s-${h.status}`}>{STATUS[h.status]}</span></td>
|
||||
<td>{h.summary}</td>
|
||||
<td><bdi dir="ltr">{h.author_email}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.reviewer_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.publisher_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{when(h.published_at ?? h.created_at)}</bdi></td>
|
||||
<td>{h.version && <a href={`${base}?a=${h.version - 1}&b=${h.version}`}>فرق</a>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{history.length === 0 && <p class="muted center">{empty}</p>}
|
||||
@ -4,6 +4,6 @@ export const STATUS: Record<string, string> = {
|
||||
};
|
||||
export const EVENT: Record<string, string> = {
|
||||
created: 'مسودہ بنایا', saved: 'محفوظ کیا', submitted: 'جائزے کے لیے بھیجا', approved: 'منظور کیا', returned: 'واپس بھیجا',
|
||||
rejected: 'مسترد کیا', published: 'شائع کیا',
|
||||
rejected: 'مسترد کیا', published: 'شائع کیا', reverted: 'پرانا ورژن واپس لایا',
|
||||
};
|
||||
export const when = (d: string) => new Date(d).toISOString().slice(0, 16).replace('T', ' ');
|
||||
|
||||
43
web/src/pages/mod/compare/[entity]/[id].astro
Normal file
43
web/src/pages/mod/compare/[entity]/[id].astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
// Two published versions side by side as a diff (#51); "یہ ورژن واپس لائیں" makes a draft of the older one, which then
|
||||
// goes through review like any edit
|
||||
import Base from '../../../../layouts/Base.astro';
|
||||
import ModNav from '../../../../components/ModNav.astro';
|
||||
import { asUser, COOKIE } from '../../../../lib/auth';
|
||||
import { ud } from '../../../../lib/urdu';
|
||||
|
||||
const me = Astro.locals.user;
|
||||
if (!me) return Astro.redirect(`/signin?next=${encodeURIComponent(Astro.url.pathname + Astro.url.search)}`);
|
||||
const token = Astro.cookies.get(COOKIE)!.value, { entity, id } = Astro.params;
|
||||
let error = '';
|
||||
if (Astro.request.method === 'POST') {
|
||||
const r = await asUser(token, `/api/mod/revert/${entity}/${id}`, { version: Number((await Astro.request.formData()).get('version')) });
|
||||
if (r.ok) return Astro.redirect(`/mod/rev/${r.data.id}`);
|
||||
error = r.data.error ?? 'کچھ غلط ہو گیا';
|
||||
}
|
||||
const res = await asUser(token, `/api/mod/compare/${entity}/${id}${Astro.url.search}`);
|
||||
if (!res.ok) return new Response(res.data.error ?? 'نہیں ملا', { status: res.status });
|
||||
const { title, url, version, a, b, diff, changes, may } = res.data;
|
||||
const label = (v: number) => (v ? `ورژن ${ud(v)}` : 'ویکی ماخذ کا متن');
|
||||
const HISTORY: Record<string, string> = { work: `/mod/work/${id}`, order: `/mod/order/${id}`, 'tags-work': `/mod/tags/work/${id}`, 'tags-category': `/mod/tags/category/${id}` };
|
||||
const older = Math.min(a, b);
|
||||
---
|
||||
<Base title={`موازنہ: ${title}`}>
|
||||
<h1>{title}</h1>
|
||||
<ModNav />
|
||||
<p class="center"><a href={url}>صفحہ</a> · <a href={HISTORY[entity]}>تاریخچہ</a> · موجودہ: {label(version)}</p>
|
||||
<p class="center"><span class="del-key">−</span> {label(a)} <span class="add-key">+</span> {label(b)} · {ud(changes)} سطریں بدلیں</p>
|
||||
{error && <p class="form-error" role="alert">{error}</p>}
|
||||
{changes === 0 ? <p class="muted center">دونوں ورژن ایک جیسے ہیں۔</p> : (
|
||||
<div class="diff" dir="rtl">
|
||||
{diff.map((d: any) => d.op === '=' ? null : <div class={d.op === '+' ? 'add' : 'del'}><span>{d.op === '+' ? '+' : '−'}</span>{d.text || ' '}</div>)}
|
||||
</div>
|
||||
)}
|
||||
{may.revert && older < version && (
|
||||
<form method="post" class="center">
|
||||
<input type="hidden" name="version" value={older} />
|
||||
<button class="save-like">{label(older)} واپس لائیں</button>
|
||||
<p class="muted">ایک مسودہ بنے گا جو عام ترمیم کی طرح جائزے اور اشاعت سے گزرے گا۔</p>
|
||||
</form>
|
||||
)}
|
||||
</Base>
|
||||
@ -13,7 +13,9 @@ if (!me) return Astro.redirect('/signin?next=/mod');
|
||||
if (!['mod-l2', 'mod-l1', 'admin'].includes(me.role)) return new Response('صرف موڈریٹرز کے لیے', { status: 403 });
|
||||
const token = Astro.cookies.get(COOKIE)!.value, showLog = Astro.url.searchParams.has('log');
|
||||
const queue = showLog ? null : (await asUser(token, '/api/mod/queue')).data;
|
||||
const log = showLog ? (await asUser(token, '/api/mod/log')).data : null;
|
||||
// the log filtered by person, kind of change and step (#51)
|
||||
const f = Object.fromEntries(['who', 'kind', 'action'].map((k) => [k, Astro.url.searchParams.get(k) ?? '']));
|
||||
const log = showLog ? (await asUser(token, `/api/mod/log?${new URLSearchParams(f)}`)).data : null;
|
||||
// what kind of change a row is (a work's text needs no label)
|
||||
const KIND: Record<string, string> = { order: 'ترتیب: ', 'tags-category': 'ٹیگ: ', 'tags-work': 'ٹیگ: ', ebook: 'ای بک: ' };
|
||||
// each kind of change has its icon; each event its tone on the timeline
|
||||
@ -52,6 +54,21 @@ const groups = queue ? [
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
{log && (
|
||||
<form method="get" class="version-tools" aria-label="ریکارڈ چھانیں">
|
||||
<input type="hidden" name="log" />
|
||||
<input type="search" name="who" value={f.who} placeholder="کس نے (ای میل)" aria-label="کس نے" dir="auto" />
|
||||
<select name="kind" aria-label="کس چیز میں">
|
||||
{[['', 'سب تبدیلیاں'], ['work', 'متن'], ['order', 'ترتیب'], ['tags', 'ٹیگ'], ['ebook', 'ای بکس']].map(([v, t]) => <option value={v} selected={f.kind === v}>{t}</option>)}
|
||||
</select>
|
||||
<select name="action" aria-label="کیا کیا">
|
||||
<option value="">ہر قدم</option>
|
||||
{Object.entries(EVENT).map(([v, t]) => <option value={v} selected={f.action === v}>{t}</option>)}
|
||||
</select>
|
||||
<button>دکھائیں</button>
|
||||
</form>
|
||||
)}
|
||||
{log && log.entries.length === 0 && <p class="muted center">اس چھانٹی میں کچھ نہیں۔</p>}
|
||||
{log && (
|
||||
<ol class="timeline">
|
||||
{log.entries.map((e: any) => (
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
---
|
||||
// A book/section's arrangements (#41): its current order, published versions and drafts; "ترتیب دیں" starts a draft
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
import History from '../../../components/History.astro';
|
||||
import ModNav from '../../../components/ModNav.astro';
|
||||
import { asUser, COOKIE } from '../../../lib/auth';
|
||||
import { STATUS, when } from '../../../lib/mod';
|
||||
import { ud } from '../../../lib/urdu';
|
||||
|
||||
const me = Astro.locals.user;
|
||||
@ -26,21 +26,5 @@ const { section, version, history, count, may } = res.data;
|
||||
{version ? `موجودہ ترتیب: ورژن ${ud(version)}` : 'ویکی ماخذ کی ترتیب (ابھی دیوان کی کوئی ترتیب نہیں)'}</p>
|
||||
{error && <p class="form-error" role="alert">{error}</p>}
|
||||
{may.arrange && count > 1 && <form method="post" class="center"><button class="save-like">ترتیب دیں</button></form>}
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>ورژن</th><th>حالت</th><th>خلاصہ</th><th>لکھنے والے</th><th>جائزہ</th><th>اشاعت</th><th>تاریخ</th></tr></thead>
|
||||
<tbody>
|
||||
{history.map((h: any) => (
|
||||
<tr>
|
||||
<td><a href={`/mod/rev/${h.id}`}>{h.version ? ud(h.version) : '—'}</a></td>
|
||||
<td><span class={`status s-${h.status}`}>{STATUS[h.status]}</span></td>
|
||||
<td>{h.summary}</td>
|
||||
<td><bdi dir="ltr">{h.author_email}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.reviewer_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.publisher_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{when(h.published_at ?? h.created_at)}</bdi></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{history.length === 0 && <p class="muted center">ابھی ترتیب میں کوئی تبدیلی نہیں۔</p>}
|
||||
<History entity="order" id={section.id} history={history} version={version} empty="ابھی ترتیب میں کوئی تبدیلی نہیں۔" />
|
||||
</Base>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
---
|
||||
// A page's tags (#52): its current tags, published versions and drafts; "ٹیگ لگائیں" starts a draft
|
||||
import Base from '../../../../layouts/Base.astro';
|
||||
import History from '../../../../components/History.astro';
|
||||
import ModNav from '../../../../components/ModNav.astro';
|
||||
import Tags from '../../../../components/Tags.astro';
|
||||
import { asUser, COOKIE } from '../../../../lib/auth';
|
||||
import { STATUS, when } from '../../../../lib/mod';
|
||||
import { ud } from '../../../../lib/urdu';
|
||||
|
||||
const me = Astro.locals.user;
|
||||
@ -28,20 +28,5 @@ const current = content.split('\n').filter(Boolean).map((l: string) => { const [
|
||||
<Tags tags={current} />
|
||||
{error && <p class="form-error" role="alert">{error}</p>}
|
||||
{may.tags && <form method="post" class="center"><button class="save-like">ٹیگ لگائیں</button></form>}
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>ورژن</th><th>حالت</th><th>خلاصہ</th><th>لکھنے والے</th><th>جائزہ</th><th>اشاعت</th><th>تاریخ</th></tr></thead>
|
||||
<tbody>
|
||||
{history.map((h: any) => (
|
||||
<tr>
|
||||
<td><a href={`/mod/rev/${h.id}`}>{h.version ? ud(h.version) : '—'}</a></td>
|
||||
<td><span class={`status s-${h.status}`}>{STATUS[h.status]}</span></td>
|
||||
<td>{h.summary}</td>
|
||||
<td><bdi dir="ltr">{h.author_email}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.reviewer_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.publisher_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{when(h.published_at ?? h.created_at)}</bdi></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<History entity={`tags-${kind}`} id={target.id} history={history} version={version} empty="ابھی ٹیگ میں کوئی تبدیلی نہیں۔" />
|
||||
</Base>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
---
|
||||
// A work's versions: published versions and drafts, with who, when, summary and status
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
import History from '../../../components/History.astro';
|
||||
import ModNav from '../../../components/ModNav.astro';
|
||||
import { asUser, COOKIE } from '../../../lib/auth';
|
||||
import { STATUS, when } from '../../../lib/mod';
|
||||
import { ud } from '../../../lib/urdu';
|
||||
|
||||
const me = Astro.locals.user;
|
||||
@ -22,21 +22,5 @@ const { work, version, history, may } = res.data;
|
||||
<ModNav />
|
||||
<p class="center"><a href={work.url}>کلام</a> · {version ? `موجودہ ورژن ${ud(version)}` : 'ویکی ماخذ کا متن (ابھی دیوان کا کوئی ورژن نہیں)'}</p>
|
||||
{may.edit && <form method="post" class="center"><button class="save-like">ترمیم کریں</button></form>}
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>ورژن</th><th>حالت</th><th>خلاصہ</th><th>لکھنے والے</th><th>جائزہ</th><th>اشاعت</th><th>تاریخ</th></tr></thead>
|
||||
<tbody>
|
||||
{history.map((h: any) => (
|
||||
<tr>
|
||||
<td><a href={`/mod/rev/${h.id}`}>{h.version ? ud(h.version) : '—'}</a></td>
|
||||
<td><span class={`status s-${h.status}`}>{STATUS[h.status]}</span></td>
|
||||
<td>{h.summary}</td>
|
||||
<td><bdi dir="ltr">{h.author_email}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.reviewer_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{h.publisher_email ?? ''}</bdi></td>
|
||||
<td><bdi dir="ltr">{when(h.published_at ?? h.created_at)}</bdi></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{history.length === 0 && <p class="muted center">ابھی کوئی ترمیم نہیں۔</p>}
|
||||
<History entity="work" id={work.id} history={history} version={version} empty="ابھی کوئی ترمیم نہیں۔" />
|
||||
</Base>
|
||||
|
||||
@ -412,6 +412,10 @@ h1 + .muted { text-align: center; margin-top: 0; }
|
||||
.timeline li { margin: 4px 0; }
|
||||
.mod-h { text-align: center; color: var(--brand); }
|
||||
.inline-form { display: inline; }
|
||||
.version-tools { display: flex; flex-wrap: wrap; gap: 8px 12px; justify-content: center; align-items: center; margin: 10px 0; }
|
||||
.add-key, .del-key { font-weight: 700; padding: 0 6px; border-radius: 4px; }
|
||||
.add-key { background: color-mix(in srgb, var(--ok) 14%, transparent); }
|
||||
.del-key { background: color-mix(in srgb, var(--danger) 12%, transparent); }
|
||||
/* a page's action row (save, edit, history, tags): spaced as one row of buttons, wrapping on phones */
|
||||
.center:has(> .save) { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; }
|
||||
.mod-history { font-size: .85rem; margin-inline-start: 6px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user