#262: remove Requests / Approvals / Tasks tabs from Executive Meetings
Full-stack removal of the three retired sections.
Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the three
capability flags from /me, retired imports, and dead schemas
(detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
dueAtSchema, dateOnly, timeHm). canApprove kept (FontSettings).
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].
Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
sections, RequestListRow, retired SECTIONS entries, MeRoles type, and
unused icon imports.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.
Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
prefs / notifications / audit rows then DROP TABLE … CASCADE.
Applied to dev DB; `db push` re-synced.
Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- 3 pre-existing parallel-file pollution failures in the workflow
runner are unrelated to #262 (verified by sequential run).
- Pre-existing tsc warnings at routes L509/625/L1594 untouched.
This commit is contained in:
@@ -37,11 +37,15 @@ WHERE notification_type IN (
|
||||
'task_completed'
|
||||
);
|
||||
|
||||
-- 3. Audit logs that targeted request/task entities.
|
||||
-- 3. Global notifications (bell feed) tied to retired request/task events.
|
||||
DELETE FROM notifications
|
||||
WHERE related_type IN ('executive_meeting_request', 'executive_meeting_task');
|
||||
|
||||
-- 4. Audit logs that targeted request/task entities.
|
||||
DELETE FROM executive_meeting_audit_logs
|
||||
WHERE entity_type IN ('request', 'task');
|
||||
|
||||
-- 4. Drop the retired tables themselves. CASCADE removes any FK indices
|
||||
-- 5. Drop the retired tables themselves. CASCADE removes any FK indices
|
||||
-- that referenced them; nothing in the surviving schema depends on
|
||||
-- these two tables.
|
||||
DROP TABLE IF EXISTS executive_meeting_tasks CASCADE;
|
||||
|
||||
@@ -21,9 +21,6 @@ import { logger } from "./logger";
|
||||
* iterates this list to render one row per type. New event types must be
|
||||
* appended here so users can opt in/out of them.
|
||||
*/
|
||||
// #262: collapsed to a single event after Requests/Approvals/Tasks were
|
||||
// removed. Old types (request_*, task_*) are scrubbed from the prefs and
|
||||
// notifications tables by cleanup-em-requests-tasks.sql.
|
||||
export const EXECUTIVE_MEETING_NOTIFICATION_TYPES = [
|
||||
"meeting_created",
|
||||
] as const;
|
||||
|
||||
@@ -17,8 +17,6 @@ import { db } from "@workspace/db";
|
||||
import {
|
||||
executiveMeetingsTable,
|
||||
executiveMeetingAttendeesTable,
|
||||
// #262: executiveMeetingRequestsTable + executiveMeetingTasksTable
|
||||
// removed alongside the Requests / Approvals / Tasks tabs.
|
||||
executiveMeetingNotificationsTable,
|
||||
executiveMeetingNotificationPrefsTable,
|
||||
executiveMeetingAuditLogsTable,
|
||||
@@ -87,8 +85,6 @@ const MERGE_COLUMN_INDEX: Record<(typeof MERGE_COLUMNS)[number], number> = {
|
||||
attendees: 2,
|
||||
time: 3,
|
||||
};
|
||||
// #262: REQUEST_TYPES, REQUEST_REVIEW_STATUSES, TASK_STATUSES removed
|
||||
// alongside the Requests / Approvals / Tasks tabs.
|
||||
const FONT_FAMILIES = [
|
||||
"system",
|
||||
"Cairo",
|
||||
@@ -112,8 +108,6 @@ const ADMIN_AUDIT_ROLES = [
|
||||
"executive_office_manager",
|
||||
"executive_coord_lead",
|
||||
] as const;
|
||||
// #262: REQUEST_ROLES, TASK_VIEW_ROLES, TASK_BROAD_VIEW_ROLES removed
|
||||
// alongside the Requests / Approvals / Tasks tabs.
|
||||
const READ_ROLES = [
|
||||
"admin",
|
||||
"executive_ceo",
|
||||
@@ -158,15 +152,11 @@ function makeRequireRoles(allowed: ReadonlyArray<string>) {
|
||||
const requireMutate = makeRequireRoles(MUTATE_ROLES);
|
||||
const requireApprove = makeRequireRoles(APPROVE_ROLES);
|
||||
const requireAdminAudit = makeRequireRoles(ADMIN_AUDIT_ROLES);
|
||||
// #262: requireRequest + requireTaskView removed with their handlers.
|
||||
|
||||
// ---------- Zod schemas ----------
|
||||
|
||||
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD");
|
||||
|
||||
// #262: kept after the Requests/Approvals/Tasks block was removed; the
|
||||
// audit + meetings query handlers still need it to validate ?date=… and
|
||||
// ?dateFrom/dateTo= inputs.
|
||||
const DATE_RE_LOCAL = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const timeSchema = z
|
||||
.string()
|
||||
@@ -284,13 +274,6 @@ const duplicateSchema = z.object({
|
||||
// frontend and server share the same contract for POST /reorder.
|
||||
const reorderSchema = ExecutiveMeetingsReorderBody;
|
||||
|
||||
// #262: dateOnly + timeHm helpers removed; only the request payload
|
||||
// schemas referenced them.
|
||||
|
||||
// #262: detailsByType, requestPayloadSchemas, requestCreateSchema,
|
||||
// requestNestedCreateSchema, requestReviewSchema, dueAtSchema,
|
||||
// taskCreateSchema, taskPatchSchema removed alongside the
|
||||
// Requests / Approvals / Tasks handlers.
|
||||
const pdfArchiveCreateSchema = z.object({
|
||||
archiveDate: dateSchema,
|
||||
filePath: z.string().trim().max(500).optional(),
|
||||
@@ -452,9 +435,6 @@ router.get(
|
||||
roles: Array.from(names),
|
||||
canRead: READ_ROLES.some((r) => names.has(r)),
|
||||
canMutate: MUTATE_ROLES.some((r) => names.has(r)),
|
||||
// #262: canApprove now only gates the global font-settings write
|
||||
// path; canSubmitRequest, canViewTasks, canViewAllTasks were
|
||||
// removed with the Requests/Approvals/Tasks tabs.
|
||||
canApprove: APPROVE_ROLES.some((r) => names.has(r)),
|
||||
canViewAudit: ADMIN_AUDIT_ROLES.some((r) => names.has(r)),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ const TEST_PASSWORD = "TestPass123!";
|
||||
|
||||
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
||||
|
||||
// #262: requestIds + taskIds dropped with their tables.
|
||||
const created = {
|
||||
userIds: [],
|
||||
groupIds: [],
|
||||
@@ -270,8 +269,6 @@ after(async () => {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
|
||||
// removed alongside the tables themselves.
|
||||
if (created.meetingIds.length > 0) {
|
||||
await pool.query(
|
||||
`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
|
||||
@@ -292,8 +289,6 @@ after(async () => {
|
||||
);
|
||||
}
|
||||
if (created.userIds.length > 0) {
|
||||
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
|
||||
// removed alongside the tables themselves.
|
||||
await pool.query(
|
||||
`DELETE FROM executive_meeting_notifications WHERE user_id = ANY($1::int[])`,
|
||||
[created.userIds],
|
||||
@@ -502,12 +497,6 @@ test("meeting_created: fan-out excludes actor, dedupes direct+group, writes both
|
||||
expectNoSocketEventFor(coord2Sock, "meeting_created");
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L522-622 (Requests/Tasks event type).
|
||||
// #262: removed test block previously at L623-626 (Requests/Tasks event type).
|
||||
// #262: removed test block previously at L627-630 (Requests/Tasks event type).
|
||||
// #262: removed test block previously at L631-634 (Requests/Tasks event type).
|
||||
// #262: removed test block previously at L635-674 (Requests/Tasks event type).
|
||||
// #262: removed test block previously at L675-754 (Requests/Tasks event type).
|
||||
|
||||
// #238: opt-out coverage for filterRecipientsByNotificationPref. The HTTP
|
||||
// surface gives us black-box access — set a pref row, trigger the event,
|
||||
@@ -633,7 +622,6 @@ test("pref opt-out: missing pref row defaults to ON (user receives delivery)", a
|
||||
});
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L855-900 (Requests/Tasks event type).
|
||||
test("pref opt-out: email=false does NOT affect the in-app channel", async () => {
|
||||
// Channel independence — muting email must leave in-app delivery alone.
|
||||
await setPref(approver1.id, "meeting_created", { inApp: true, email: false });
|
||||
|
||||
@@ -14,7 +14,6 @@ const TEST_PASSWORD = "TestPass123!";
|
||||
|
||||
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
||||
|
||||
// #262: requestIds + taskIds dropped with their tables.
|
||||
const created = {
|
||||
userIds: [],
|
||||
meetingIds: [],
|
||||
@@ -102,8 +101,6 @@ before(async () => {
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
|
||||
// removed alongside the tables themselves.
|
||||
if (created.meetingIds.length > 0) {
|
||||
await pool.query(`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`, [
|
||||
created.meetingIds,
|
||||
@@ -138,9 +135,6 @@ const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
|
||||
// #262: canSubmitRequest, canViewTasks, canViewAllTasks dropped from /me
|
||||
// when the Requests/Approvals/Tasks tabs were removed. The surviving
|
||||
// flags are canRead, canMutate, canApprove, canViewAudit.
|
||||
test("GET /me exposes the surviving capability flags", async () => {
|
||||
const res = await api(adminCookie, "GET", "/api/executive-meetings/me");
|
||||
assert.equal(res.status, 200);
|
||||
@@ -595,11 +589,6 @@ test("Sanitization: duplicate path re-sanitizes location/meetingUrl/notes and pr
|
||||
);
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L609-657 (Requests/Tasks).
|
||||
// #262: removed test block previously at L658-679 (Requests/Tasks).
|
||||
// #262: removed test block previously at L680-717 (Requests/Tasks).
|
||||
// #262: removed test block previously at L718-760 (Requests/Tasks).
|
||||
// #262: removed test block previously at L761-782 (Requests/Tasks).
|
||||
test("Audit logs: admin can list, plain coordinator gets 403", async () => {
|
||||
const ok = await api(
|
||||
adminCookie,
|
||||
@@ -1123,7 +1112,6 @@ test("Reorder: rejects an incomplete-day request (orderedIds missing some)", asy
|
||||
assert.equal(body.code, "incomplete_day");
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L1306-1339 (Requests/Tasks).
|
||||
test("Reorder: rejects ids that do not all belong to meetingDate", async () => {
|
||||
const sameDay = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
titleAr: "اليوم", titleEn: "Today", meetingDate: today,
|
||||
@@ -1209,9 +1197,6 @@ test("Meeting CRUD permissions: coordinator forbidden, lead allowed, admin allow
|
||||
"executive_coord_lead must be able to PATCH meetings");
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L1425-1463 (Requests/Tasks).
|
||||
// #262: removed test block previously at L1464-1506 (Requests/Tasks).
|
||||
// #262: removed test block previously at L1507-1551 (Requests/Tasks).
|
||||
test("Font settings: PUT then GET returns the user-scoped row roundtrip", async () => {
|
||||
const put = await api(adminCookie, "PUT",
|
||||
"/api/executive-meetings/font-settings", {
|
||||
@@ -1266,8 +1251,6 @@ test("router.param: non-numeric :id returns 404 across endpoints (no crash)", as
|
||||
["GET", "/api/executive-meetings/-1"],
|
||||
["POST", "/api/executive-meetings/abc/duplicate"],
|
||||
["PUT", "/api/executive-meetings/abc/attendees"],
|
||||
// #262: /requests + /tasks routes were retired with the
|
||||
// Requests/Approvals/Tasks tabs.
|
||||
];
|
||||
for (const [method, path] of cases) {
|
||||
const res = await api(adminCookie, method, path,
|
||||
@@ -1422,8 +1405,6 @@ test("Notification prefs: PUT rejects unknown notificationType with 400", async
|
||||
assert.equal(bad.status, 400);
|
||||
});
|
||||
|
||||
// #262: removed test block previously at L1427-1471 (covered by meeting_created tests in executive-meetings-notifications.test.mjs).
|
||||
// #262: removed test block previously at L1472-1524 (covered by meeting_created tests in executive-meetings-notifications.test.mjs).
|
||||
test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to defaults", async () => {
|
||||
// #236: clicking "Restore defaults" in the UI hits DELETE. After it
|
||||
// returns, every event type must read default-on regardless of what
|
||||
@@ -1432,9 +1413,6 @@ test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to de
|
||||
const fresh = await createUser("em_pref_restore", "executive_office_manager");
|
||||
const cookie = await login(fresh.username, TEST_PASSWORD);
|
||||
|
||||
// #262: only `meeting_created` survives, so the "two different event
|
||||
// types" half of the original test collapsed to a single row covering
|
||||
// both channels muted at once. The DELETE must still clear it.
|
||||
const put = await api(cookie, "PUT",
|
||||
"/api/executive-meetings/notification-prefs",
|
||||
{
|
||||
@@ -1477,8 +1455,6 @@ test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to de
|
||||
|
||||
// And actual fan-out must reach the user again — a regression in the
|
||||
// DELETE handler that left rows behind would silently re-mute them.
|
||||
// #262: trigger via POST /api/executive-meetings (the only event type
|
||||
// that still fans out).
|
||||
const beforeCount = await pool.query(
|
||||
`SELECT COUNT(*)::int AS n
|
||||
FROM executive_meeting_notifications
|
||||
|
||||
@@ -90,8 +90,6 @@ export function useNotificationsSocket() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["/api/executive-meetings/notifications"],
|
||||
});
|
||||
// #262: requests/tasks query invalidations dropped when those
|
||||
// sub-resources were removed from the API.
|
||||
});
|
||||
|
||||
socket.on(
|
||||
|
||||
@@ -1280,13 +1280,7 @@
|
||||
"reminder": "تذكير",
|
||||
"change": "تنبيه تعديل",
|
||||
"cancel": "تنبيه إلغاء",
|
||||
"meeting_created": "اجتماع جديد",
|
||||
"request_submitted": "طلب بانتظار المراجعة",
|
||||
"request_approved": "تم اعتماد الطلب",
|
||||
"request_rejected": "تم رفض الطلب",
|
||||
"request_needs_edit": "الطلب يحتاج تعديلاً",
|
||||
"task_assigned": "إسناد مهمة",
|
||||
"task_completed": "اكتمال مهمة"
|
||||
"meeting_created": "اجتماع جديد"
|
||||
},
|
||||
"status": {
|
||||
"pending": "بالانتظار",
|
||||
|
||||
@@ -1181,13 +1181,7 @@
|
||||
"reminder": "Reminder",
|
||||
"change": "Change notice",
|
||||
"cancel": "Cancellation notice",
|
||||
"meeting_created": "Meeting created",
|
||||
"request_submitted": "Request awaiting review",
|
||||
"request_approved": "Request approved",
|
||||
"request_rejected": "Request rejected",
|
||||
"request_needs_edit": "Request needs edits",
|
||||
"task_assigned": "Task assigned",
|
||||
"task_completed": "Task completed"
|
||||
"meeting_created": "Meeting created"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
Type,
|
||||
Languages,
|
||||
ListChecks,
|
||||
// #262: ClipboardList, CheckSquare, ListTodo removed with the
|
||||
// Requests / Approvals / Tasks tabs.
|
||||
Bell,
|
||||
ScrollText,
|
||||
FileText,
|
||||
@@ -146,11 +144,6 @@ type Meeting = {
|
||||
|
||||
type DayResponse = { date: string; meetings: Meeting[] };
|
||||
|
||||
// #262: MeRoles collapsed to the same shape as MeCapabilities (only one
|
||||
// type definition is needed now that the Requests/Approvals/Tasks tabs
|
||||
// are gone). RequestRow / TaskRow types removed alongside the sections
|
||||
// that consumed them.
|
||||
|
||||
type AuditEntry = {
|
||||
id: number;
|
||||
action: string;
|
||||
@@ -224,8 +217,6 @@ function formatBytesForDisplay(bytes: number): string {
|
||||
return `${value < 10 && unitIdx > 0 ? value.toFixed(1) : Math.round(value)} ${units[unitIdx]}`;
|
||||
}
|
||||
|
||||
// #262: removed `requests`, `approvals`, `tasks` entries (and their
|
||||
// icons) when the corresponding sections were retired.
|
||||
const SECTIONS = [
|
||||
{ key: "schedule", icon: CalendarClock },
|
||||
{ key: "manage", icon: ListChecks },
|
||||
@@ -237,9 +228,6 @@ const SECTIONS = [
|
||||
|
||||
type SectionKey = (typeof SECTIONS)[number]["key"];
|
||||
|
||||
// #262: canSubmitRequest, canViewTasks, canViewAllTasks dropped from /me;
|
||||
// canApprove is kept because the global font-settings write path still
|
||||
// gates on it.
|
||||
type MeCapabilities = {
|
||||
userId: number;
|
||||
roles: string[];
|
||||
@@ -656,7 +644,6 @@ export default function ExecutiveMeetingsPage() {
|
||||
{section === "manage" && me && (
|
||||
<ManageSection date={date} onDateChange={setDate} canMutate={me.canMutate} isRtl={isRtl} t={t} />
|
||||
)}
|
||||
{/* #262: Requests / Approvals / Tasks section blocks removed. */}
|
||||
{section === "notifications" && me && (
|
||||
<NotificationsSection
|
||||
canView={me.canRead}
|
||||
|
||||
@@ -95,11 +95,6 @@ export const executiveMeetingAttendeesTable = pgTable(
|
||||
}),
|
||||
);
|
||||
|
||||
// #262: removed executiveMeetingRequestsTable + executiveMeetingTasksTable.
|
||||
// The Requests / Approvals / Tasks tabs were retired from the page; the
|
||||
// underlying DB tables are dropped by
|
||||
// artifacts/api-server/scripts/cleanup-em-requests-tasks.sql.
|
||||
|
||||
export const executiveMeetingNotificationsTable = pgTable(
|
||||
"executive_meeting_notifications",
|
||||
{
|
||||
@@ -130,10 +125,6 @@ export const executiveMeetingNotificationsTable = pgTable(
|
||||
* notificationType mirrors the keys passed to
|
||||
* `recordExecutiveMeetingNotifications` / `sendExecutiveMeetingEmail`:
|
||||
* - meeting_created
|
||||
*
|
||||
* #262: collapsed to a single event type after Requests/Approvals/Tasks
|
||||
* were removed. Orphan rows for the old types are deleted by
|
||||
* cleanup-em-requests-tasks.sql.
|
||||
*/
|
||||
export const executiveMeetingNotificationPrefsTable = pgTable(
|
||||
"executive_meeting_notification_prefs",
|
||||
|
||||
Reference in New Issue
Block a user