diff --git a/api/src/routes/applications.ts b/api/src/routes/applications.ts index 92820f5..db21cc8 100644 --- a/api/src/routes/applications.ts +++ b/api/src/routes/applications.ts @@ -54,7 +54,7 @@ router.post('/', [requireLogin], async (req: Request, res: Response) => { try { let appID = await createApplication(memberID, appVersion, JSON.stringify(App)); - await setUserState(memberID, MemberState.Applicant); + await setUserState(memberID, MemberState.Applicant, "Application Submitted", memberID); res.sendStatus(201); @@ -230,7 +230,7 @@ router.post('/approve/:id', [requireLogin, requireRole("Recruiter")], async (req await approveApplication(appID, approved_by); //update user profile - await setUserState(app.member_id, MemberState.Member); + await setUserState(app.member_id, MemberState.Member, "Application Accepted", approved_by); await pool.query('CALL sp_accept_new_recruit_validation(?, ?, ?, ?)', [Number(process.env.CONFIG_ID), app.member_id, approved_by, approved_by]) @@ -262,7 +262,7 @@ router.post('/deny/:id', [requireLogin, requireRole("Recruiter")], async (req: R try { const app = await getApplicationByID(appID); await denyApplication(appID, approver); - await setUserState(app.member_id, MemberState.Denied); + await setUserState(app.member_id, MemberState.Denied, "Application Denied", approver); logger.info('app', "Member application approved", { application: app.id, @@ -403,7 +403,7 @@ VALUES(?, ?, ?, 1);` router.post('/restart', async (req: Request, res: Response) => { const user = req.user.id; try { - await setUserState(user, MemberState.Guest); + await setUserState(user, MemberState.Guest, "Restarted Application", user); logger.info('app', "Member restarted application", { user: user diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index 0d47c0f..fd78f62 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -240,11 +240,12 @@ router.put('/:id/displayname', async (req, res) => { router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { try { var con = await pool.getConnection(); + let author = req.user.id; con.beginTransaction(); var data: Discharge = req.body; - setUserState(data.userID, MemberState.Retired, con); + setUserState(data.userID, MemberState.Discharged, "Member Discharged", author, con); cancelLatestRank(data.userID, con); cancelLatestUnit(data.userID, con); con.commit(); diff --git a/api/src/services/db/memberService.ts b/api/src/services/db/memberService.ts index 347c971..fb71063 100644 --- a/api/src/services/db/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -6,43 +6,43 @@ import { memberCache } from "../../routes/auth"; import * as mariadb from 'mariadb'; export async function getFilteredMembers( - page: number = 1, - pageSize: number = 15, - search?: string, - status?: string, - unitId?: string + page: number = 1, + pageSize: number = 15, + search?: string, + status?: string, + unitId?: string ): Promise { - try { - const offset = (page - 1) * pageSize; - const whereClauses: string[] = []; - const params: any[] = []; + try { + const offset = (page - 1) * pageSize; + const whereClauses: string[] = []; + const params: any[] = []; - if (status && status !== 'all') { - whereClauses.push(`m.state = ?`); - params.push(status); - } + if (status && status !== 'all') { + whereClauses.push(`m.state = ?`); + params.push(status); + } - if (search) { - whereClauses.push(`v.member_name LIKE ?`); - params.push(`%${search}%`); - } + if (search) { + whereClauses.push(`v.member_name LIKE ?`); + params.push(`%${search}%`); + } - if (unitId && unitId !== 'all') { - whereClauses.push(`v.unit = ?`); - params.push(unitId); - } + if (unitId && unitId !== 'all') { + whereClauses.push(`v.unit = ?`); + params.push(unitId); + } - const whereClause = whereClauses.length > 0 - ? ` WHERE ${whereClauses.join(' AND ')}` - : ''; + const whereClause = whereClauses.length > 0 + ? ` WHERE ${whereClauses.join(' AND ')}` + : ''; - // COUNT QUERY - const countQuery = `SELECT COUNT(*) as total FROM view_member_rank_unit_status_latest v INNER JOIN members m ON v.member_id = m.id ${whereClause}`; - const [countResults]: any[] = await pool.query(countQuery, params); - const total = Number(countResults?.total) || 0; + // COUNT QUERY + const countQuery = `SELECT COUNT(*) as total FROM view_member_rank_unit_status_latest v INNER JOIN members m ON v.member_id = m.id ${whereClause}`; + const [countResults]: any[] = await pool.query(countQuery, params); + const total = Number(countResults?.total) || 0; - // DATA QUERY - const dataQuery = ` + // DATA QUERY + const dataQuery = ` SELECT v.*, CASE @@ -60,106 +60,123 @@ export async function getFilteredMembers( LIMIT ? OFFSET ? `; - const rows: any[] = await pool.query(dataQuery, [...params, pageSize, offset]); + const rows: any[] = await pool.query(dataQuery, [...params, pageSize, offset]); - // Map rows to Member type - const members: Member[] = rows.map(row => ({ - member_id: Number(row.member_id), - member_name: row.member_name, - displayName: row.displayName, - rank: row.rank, - rank_date: row.rank_date, - unit: row.unit, - unit_date: row.unit_date, - status: row.status, - status_date: row.status_date, - loa_until: row.loa_until ? new Date(row.loa_until) : undefined, - })); + // Map rows to Member type + const members: Member[] = rows.map(row => ({ + member_id: Number(row.member_id), + member_name: row.member_name, + displayName: row.displayName, + rank: row.rank, + rank_date: row.rank_date, + unit: row.unit, + unit_date: row.unit_date, + status: row.status, + status_date: row.status_date, + loa_until: row.loa_until ? new Date(row.loa_until) : undefined, + })); - return { - data: members, - pagination: { - page, - pageSize, - total, - totalPages: Math.ceil(total / pageSize), - }, - }; - } catch (error) { - logger.error('app', 'Error fetching filtered members', { - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } + return { + data: members, + pagination: { + page, + pageSize, + total, + totalPages: Math.ceil(total / pageSize), + }, + }; + } catch (error) { + logger.error('app', 'Error fetching filtered members', { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } } export async function getUserData(userID: number): Promise { - const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; - const res: Member = await pool.query(sql, [userID]); - return res[0] ?? null; + const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; + const res: Member = await pool.query(sql, [userID]); + return res[0] ?? null; } -export async function setUserState(userID: number, state: MemberState, con: mariadb.Pool | mariadb.Connection = pool) { - try { - const sql = `UPDATE members - SET state = ? - WHERE id = ?;`; - return await con.query(sql, [state, userID]); - } catch (error) { - logger.error('app', 'Error setting user state', error); - } finally { - memberCache.Invalidate(userID); - } +export async function setUserState(userID: number, state: MemberState, reason: string, creatorID: number, externalCon?: mariadb.Connection | mariadb.PoolConnection) { + const isInternalConn = !externalCon; + const con = (externalCon || await pool.getConnection()) as mariadb.PoolConnection; + + try { + if (isInternalConn) await con.beginTransaction(); + + await endLatestMemberState(userID, con); + + const sql = `UPDATE members SET state = ? WHERE id = ?;`; + await con.query(sql, [state, userID]); + + const insertHistorySql = `INSERT INTO member_state_history + (member_id, state_id, reason, created_by_id, start_date, end_date) + VALUES (?, ?, ?, ?, NOW(), NULL);`; + await con.query(insertHistorySql, [userID, state, reason, creatorID]); + + if (isInternalConn) await con.commit(); + } catch (error) { + if (isInternalConn) { + await con.rollback(); + } + logger.error('app', 'Error setting user state', error); + throw error; + } finally { + memberCache.Invalidate(userID); + if (isInternalConn && con) con.release(); + } } export async function getUserState(user: number): Promise { - let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]); - return (out[0].state as MemberState); + let out = await pool.query(`SELECT state FROM members WHERE id = ?`, [user]); + return (out[0].state as MemberState); } export async function getMemberSettings(id: number): Promise { - const sql = `SELECT * FROM view_member_settings WHERE id = ?`; - let out: memberSettings[] = await pool.query(sql, [id]); + const sql = `SELECT * FROM view_member_settings WHERE id = ?`; + let out: memberSettings[] = await pool.query(sql, [id]); - if (out.length != 1) - throw new Error("Could not get user settings"); + if (out.length != 1) + throw new Error("Could not get user settings"); - return out[0]; + return out[0]; } export async function setUserSettings(id: number, settings: memberSettings) { - const sql = `UPDATE view_member_settings SET + const sql = `UPDATE view_member_settings SET displayName = ? WHERE id = ?;`; - let result = await pool.query(sql, [settings.displayName, id]) + let result = await pool.query(sql, [settings.displayName, id]) } export async function getMembersLite(ids: number[]): Promise { - const sql = `SELECT m.member_id AS id, + const sql = `SELECT m.member_id AS id, m.member_name AS username, m.displayName, u.color FROM view_member_rank_unit_status_latest m LEFT JOIN units u ON u.name = m.unit WHERE member_id IN (?);`; - const res: MemberLight[] = await pool.query(sql, [ids]); - return res; + const res: MemberLight[] = await pool.query(sql, [ids]); + return res; } export async function getAllMembersLite(): Promise { - const sql = `SELECT m.member_id AS id, + const sql = `SELECT m.member_id AS id, m.member_name AS username, m.displayName, u.color FROM view_member_rank_unit_status_latest m LEFT JOIN units u ON u.name = m.unit;`; - const res: MemberLight[] = await pool.query(sql); - return res; + const res: MemberLight[] = await pool.query(sql); + return res; } export async function getMembersFull(ids: number[]): Promise { - const sql = ` + const sql = ` SELECT m.*, COALESCE( JSON_ARRAYAGG( @@ -181,30 +198,60 @@ export async function getMembersFull(ids: number[]): Promise { - const member: Member = { - member_id: row.member_id, - member_name: row.member_name, - displayName: row.displayName, - rank: row.rank, - rank_date: row.rank_date, - unit: row.unit, - unit_date: row.unit_date, - status: row.status, - status_date: row.status_date, - loa_until: row.loa_until ? new Date(row.loa_until) : undefined, - }; - // roles comes as array of strings; parse each one - const roles: Role[] = row.roles; + return rows.map(row => { + const member: Member = { + member_id: row.member_id, + member_name: row.member_name, + displayName: row.displayName, + rank: row.rank, + rank_date: row.rank_date, + unit: row.unit, + unit_date: row.unit_date, + status: row.status, + status_date: row.status_date, + loa_until: row.loa_until ? new Date(row.loa_until) : undefined, + }; + // roles comes as array of strings; parse each one + const roles: Role[] = row.roles; - return { member, roles }; - }); + return { member, roles }; + }); } export async function mapDiscordtoID(id: number): Promise { - const sql = `SELECT id FROM members WHERE discord_id = ?;` - let res = await pool.query(sql, [id]); - return res.length > 0 ? res[0].id : null; + const sql = `SELECT id FROM members WHERE discord_id = ?;` + let res = await pool.query(sql, [id]); + return res.length > 0 ? res[0].id : null; +} + +export async function endLatestMemberState(memberID: number, con: mariadb.Pool | mariadb.Connection = pool) { + const sql = `UPDATE member_state_history + SET end_date = NOW(), + updated_at = NOW() + WHERE id = ( + SELECT id + FROM ( + SELECT id + FROM member_state_history + WHERE member_id = ? + AND end_date IS NULL + ORDER BY start_date DESC, + created_at DESC + LIMIT 1 + ) AS x + );`; + + try { + let res = await con.query(sql, [memberID]); + console.log(res); + } catch (error) { + logger.error('app', 'Error ending latest member state', { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + let res = await pool.query(sql, [memberID]); + console.log(res); } \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts index 1cda2dd..699f3b6 100644 --- a/shared/types/member.ts +++ b/shared/types/member.ts @@ -9,12 +9,14 @@ export interface memberSettings { export type PaginatedMembers = PagedData; export enum MemberState { - Guest = "guest", - Applicant = "applicant", - Member = "member", - Retired = "retired", - Banned = "banned", - Denied = "denied" + Guest = 1, + Applicant = 2, + Member = 3, + Retired = 4, + Discharged = 5, + Suspended = 6, + Banned = 7, + Denied = 8 } export type Member = { diff --git a/ui/package-lock.json b/ui/package-lock.json index 25a2e81..32c6433 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -35,7 +35,8 @@ "@types/node": "^24.2.1", "@vitejs/plugin-vue": "^6.0.1", "vite": "^7.0.6", - "vite-plugin-vue-devtools": "^8.0.0" + "vite-plugin-vue-devtools": "^8.0.0", + "vue-tsc": "^3.2.4" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1884,6 +1885,35 @@ "vue": "^3.2.25" } }, + "node_modules/@volar/language-core": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", + "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.27" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz", + "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz", + "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.27", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, "node_modules/@vue/babel-helper-vue-transform-on": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", @@ -2083,6 +2113,22 @@ "rfdc": "^1.4.1" } }, + "node_modules/@vue/language-core": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.4.tgz", + "integrity": "sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.27", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.18", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", @@ -2171,6 +2217,13 @@ "vue": "^3.5.0" } }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/ansis": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", @@ -3123,6 +3176,13 @@ "dev": true, "license": "MIT" }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3216,6 +3276,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3646,6 +3713,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -3932,6 +4014,13 @@ "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vue": { "version": "3.5.18", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", @@ -3974,6 +4063,23 @@ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "license": "MIT" }, + "node_modules/vue-tsc": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.4.tgz", + "integrity": "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.27", + "@vue/language-core": "3.2.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 9110023..bdb5edd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -39,6 +39,7 @@ "@types/node": "^24.2.1", "@vitejs/plugin-vue": "^6.0.1", "vite": "^7.0.6", - "vite-plugin-vue-devtools": "^8.0.0" + "vite-plugin-vue-devtools": "^8.0.0", + "vue-tsc": "^3.2.4" } } diff --git a/ui/src/App.vue b/ui/src/App.vue index 018ded8..19d15fc 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -1,25 +1,27 @@ - - - diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index 9d06a5b..b5107c8 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,5 +1,5 @@ import { Discharge } from "@shared/schemas/dischargeSchema"; -import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member"; +import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -18,7 +18,7 @@ export async function getMembersFiltered(params: { page?: number; pageSize?: number; search?: string; - status?: string; + status?: string | MemberState; unitId?: string; } = {}): Promise { @@ -27,7 +27,7 @@ export async function getMembersFiltered(params: { if (params.page) query.append('page', params.page.toString()); if (params.pageSize) query.append('pageSize', params.pageSize.toString()); if (params.search) query.append('search', params.search); - if (params.status && params.status !== 'all') query.append('status', params.status); + if (params.status && params.status !== 'all') query.append('status', String(params.status)); if (params.unitId && params.unitId !== 'all') query.append('unitId', params.unitId); const response = await fetch(`${addr}/members/filtered?${query.toString()}`, { diff --git a/ui/src/components/Navigation/Navbar.vue b/ui/src/components/Navigation/Navbar.vue index b90d84d..8c83549 100644 --- a/ui/src/components/Navigation/Navbar.vue +++ b/ui/src/components/Navigation/Navbar.vue @@ -21,6 +21,7 @@ import { useAuth } from '@/composables/useAuth'; import { ArrowUpRight, CircleArrowOutUpRight } from 'lucide-vue-next'; import DropdownMenuGroup from '../ui/dropdown-menu/DropdownMenuGroup.vue'; import DropdownMenuSeparator from '../ui/dropdown-menu/DropdownMenuSeparator.vue'; +import { MemberState } from '@shared/types/member'; const userStore = useUserStore(); const auth = useAuth(); @@ -51,7 +52,7 @@ function blurAfter() { -
+
diff --git a/ui/src/components/calendar/ViewCalendarEvent.vue b/ui/src/components/calendar/ViewCalendarEvent.vue index cb40420..2084108 100644 --- a/ui/src/components/calendar/ViewCalendarEvent.vue +++ b/ui/src/components/calendar/ViewCalendarEvent.vue @@ -15,6 +15,7 @@ import { Calendar } from 'lucide-vue-next'; import MemberCard from '../members/MemberCard.vue'; import Spinner from '../ui/spinner/Spinner.vue'; import { CopyLink } from '@/lib/copyLink'; +import { MemberState } from '@shared/types/member'; const route = useRoute(); @@ -86,7 +87,7 @@ async function setAttendance(state: CalendarAttendance) { const canEditEvent = computed(() => { if (!userStore.isLoggedIn) return false; - if (userStore.state !== 'member') return false; + if (userStore.state !== MemberState.Member) return false; if (userStore.user.member.member_id == activeEvent.value.creator_id) return true; }); @@ -231,7 +232,7 @@ defineExpose({ forceReload }) This event has been cancelled
-
+
-