fix(home): notices via labels API

This commit is contained in:
2026-06-22 22:23:56 +09:30
parent d99198ea12
commit a106033b87
3 changed files with 151 additions and 76 deletions
+37 -34
View File
@@ -18,6 +18,10 @@ import {
toISODate,
weekRangeContaining,
} from "@/seqta/utils/Loaders/engageParentTimetable";
import {
noticeMatchesLabelFilter,
resolveNoticeFilterTokens,
} from "@/seqta/utils/notices/noticeLabelFilters";
export function updateEngageHomeMenuActive(isHome: boolean): void {
const home = document.getElementById("homebutton");
@@ -267,30 +271,17 @@ function processEngageNotices(response: any, labelArray: string[]): void {
const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID);
if (!noticeContainer) return;
noticeContainer.classList.remove("loading");
noticeContainer.innerHTML = "";
const notices = response?.payload;
if (!Array.isArray(notices)) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = "No notices for today.";
emptyState.append(img, text);
noticeContainer.append(emptyState);
appendEngageNoticeEmptyState(noticeContainer, "No notices for today.");
return;
}
if (!notices.length) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = "No notices for today.";
emptyState.append(img, text);
noticeContainer.append(emptyState);
appendEngageNoticeEmptyState(noticeContainer, "No notices for today.");
return;
}
@@ -298,9 +289,7 @@ function processEngageNotices(response: any, labelArray: string[]): void {
notices.forEach((notice: any) => {
const shouldInclude =
settingsState.mockNotices ||
labelArray.length === 0 ||
labelArray.includes(JSON.stringify(notice.label));
settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelArray);
if (shouldInclude) {
const colour = processEngageNoticeColor(notice.colour);
@@ -309,9 +298,25 @@ function processEngageNotices(response: any, labelArray: string[]): void {
}
});
if (fragment.childNodes.length === 0) {
appendEngageNoticeEmptyState(noticeContainer, "No notices for today.");
return;
}
noticeContainer.appendChild(fragment);
}
function appendEngageNoticeEmptyState(container: HTMLElement, message: string) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = message;
emptyState.append(img, text);
container.append(emptyState);
}
function createEngageNoticeElement(
notice: any,
colour: string | undefined,
@@ -613,6 +618,12 @@ async function fetchEngageNoticesFromApi(
date: string,
labelTokens: string[],
): Promise<void> {
const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID);
if (noticeContainer) {
noticeContainer.classList.add("loading");
noticeContainer.innerHTML = "";
}
try {
const data = settingsState.mockNotices
? getMockNotices()
@@ -662,7 +673,7 @@ async function initEngageNoticesUi(todayFormatted: string): Promise<void> {
const noticeContainer = document.getElementById(ENGAGE_NOTICE_CONTAINER_ID);
if (!noticeContainer) return;
let labelFilterValues: string[] = [];
let prefsPayload: unknown = [];
try {
const prefsRes = await fetch(`${location.origin}/seqta/parent/load/prefs?`, {
method: "POST",
@@ -671,21 +682,15 @@ async function initEngageNoticesUi(todayFormatted: string): Promise<void> {
body: JSON.stringify({ asArray: true, request: "userPrefs" }),
});
const prefs = await prefsRes.json();
const payload = prefs?.payload;
if (Array.isArray(payload)) {
labelFilterValues = payload
.filter((item: { name?: string }) => item.name === "notices.filters")
.map((item: { value?: string }) => item.value)
.filter((v): v is string => typeof v === "string");
}
prefsPayload = prefs?.payload ?? [];
} catch {
labelFilterValues = [];
prefsPayload = [];
}
const labelTokens =
labelFilterValues.length > 0
? String(labelFilterValues[0]).split(" ").filter(Boolean)
: [];
const labelTokens = await resolveNoticeFilterTokens(
prefsPayload,
`${location.origin}/seqta/parent/load/notices`,
);
const dateControl = document.getElementById(ENGAGE_NOTICES_DATE_ID);
if (dateControl) {
@@ -696,8 +701,6 @@ async function initEngageNoticesUi(todayFormatted: string): Promise<void> {
const cleanup = bindEngageNoticesDateInput(labelTokens, todayFormatted);
engageMergeNoticeCleanup(cleanup);
noticeContainer.classList.remove("loading");
}
function engageMergeNoticeCleanup(noticeCleanup: () => void): void {
+41 -42
View File
@@ -20,6 +20,10 @@ import {
filterAssessmentsForActiveSubjects,
subjectsWithUpcomingAssessments,
} from "@/plugins/built-in/assessmentsOverview/utils";
import {
noticeMatchesLabelFilter,
resolveNoticeFilterTokens,
} from "@/seqta/utils/notices/noticeLabelFilters";
let LessonInterval: any;
let currentSelectedDate = new Date();
@@ -131,33 +135,20 @@ export async function loadHomePage() {
upcomingItems.classList.remove("loading");
}
const labelArray = prefs.payload
.filter((item: any) => item.name === "notices.filters")
.map((item: any) => item.value);
const labelTokens = await resolveNoticeFilterTokens(
prefs.payload,
`${location.origin}/seqta/student/load/notices?`,
);
const noticeContainer = document.getElementById("notice-container");
if (noticeContainer) {
if (labelArray.length > 0) {
const dateControl = document.querySelector(
'input[type="date"]',
) as HTMLInputElement;
if (dateControl) {
dateControl.value = TodayFormatted;
setupNotices(labelArray[0].split(" "), TodayFormatted);
}
noticeContainer.classList.remove("loading");
} else {
noticeContainer.classList.remove("loading");
noticeContainer.innerHTML = "";
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = "No notices available.";
emptyState.append(img, text);
noticeContainer.append(emptyState);
const dateControl = document.querySelector(
'input[type="date"]',
) as HTMLInputElement;
if (dateControl) {
dateControl.value = TodayFormatted;
}
setupNotices(labelTokens, TodayFormatted);
}
return cleanup;
@@ -248,6 +239,12 @@ function setupNotices(labelArray: string[], date: string) {
) as HTMLInputElement;
const fetchNotices = async (date: string) => {
const container = document.getElementById("notice-container");
if (container) {
container.classList.add("loading");
container.innerHTML = "";
}
try {
const data = settingsState.mockNotices
? getMockNotices()
@@ -255,13 +252,13 @@ function setupNotices(labelArray: string[], date: string) {
await fetch(`${location.origin}/seqta/student/load/notices?`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
credentials: "include",
body: JSON.stringify({ date }),
})
).json();
processNotices(data, labelArray);
} catch {
// Notices failed to load; processNotices will show empty state if container exists
processNotices({ payload: [] }, labelArray);
}
};
@@ -294,30 +291,17 @@ function processNotices(response: any, labelArray: string[]) {
const NoticeContainer = document.getElementById("notice-container");
if (!NoticeContainer) return;
NoticeContainer.classList.remove("loading");
NoticeContainer.innerHTML = "";
const notices = response?.payload;
if (!Array.isArray(notices)) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = "No notices for today.";
emptyState.append(img, text);
NoticeContainer.append(emptyState);
appendNoticeEmptyState(NoticeContainer, "No notices for today.");
return;
}
if (!notices.length) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = "No notices for today.";
emptyState.append(img, text);
NoticeContainer.append(emptyState);
appendNoticeEmptyState(NoticeContainer, "No notices for today.");
return;
}
@@ -325,8 +309,7 @@ function processNotices(response: any, labelArray: string[]) {
notices.forEach((notice: any) => {
const shouldInclude =
settingsState.mockNotices ||
labelArray.includes(JSON.stringify(notice.label));
settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelArray);
if (shouldInclude) {
const colour = processNoticeColor(notice.colour);
@@ -335,9 +318,25 @@ function processNotices(response: any, labelArray: string[]) {
}
});
if (fragment.childNodes.length === 0) {
appendNoticeEmptyState(NoticeContainer, "No notices for today.");
return;
}
NoticeContainer.appendChild(fragment);
}
function appendNoticeEmptyState(container: HTMLElement, message: string) {
const emptyState = document.createElement("div");
emptyState.classList.add("day-empty");
const img = document.createElement("img");
img.src = browser.runtime.getURL(LogoLight);
const text = document.createElement("p");
text.innerText = message;
emptyState.append(img, text);
container.append(emptyState);
}
function processNoticeColor(colour: string): string | undefined {
if (typeof colour === "string") {
const rgb = GetThresholdOfColor(colour);
@@ -0,0 +1,73 @@
type PrefEntry = { name?: string; value?: unknown };
/** Parse `notices.filters` pref (space-separated label IDs). */
export function parseNoticesFilterPref(prefsPayload: unknown): string[] {
if (!Array.isArray(prefsPayload)) return [];
const values = (prefsPayload as PrefEntry[])
.filter((item) => item?.name === "notices.filters")
.map((item) => item?.value)
.filter((v): v is string => typeof v === "string");
if (values.length === 0) return [];
return String(values[0]).split(" ").filter(Boolean);
}
/** Label IDs from `load/notices` with `{ mode: "labels" }`. */
export async function fetchNoticeLabelIds(noticesUrl: string): Promise<string[]> {
try {
const res = await fetch(noticesUrl, {
method: "POST",
headers: { "Content-Type": "application/json; charset=utf-8" },
credentials: "include",
body: JSON.stringify({ mode: "labels" }),
});
if (!res.ok) return [];
const json = (await res.json()) as { payload?: Array<{ id?: number }> };
const payload = json?.payload;
if (!Array.isArray(payload)) return [];
return payload
.map((entry) => entry?.id)
.filter((id): id is number => typeof id === "number" && !Number.isNaN(id))
.map(String);
} catch {
return [];
}
}
/** Pref filters when set; otherwise all label IDs from the labels API (native SEQTA home). */
export async function resolveNoticeFilterTokens(
prefsPayload: unknown,
noticesUrl: string,
): Promise<string[]> {
const fromPref = parseNoticesFilterPref(prefsPayload);
if (fromPref.length > 0) return fromPref;
return await fetchNoticeLabelIds(noticesUrl);
}
export function normalizeNoticeLabelId(label: unknown): string | null {
if (typeof label === "number" && !Number.isNaN(label)) {
return String(label);
}
if (typeof label === "string" && label.trim()) {
return label.trim();
}
if (label && typeof label === "object") {
const obj = label as Record<string, unknown>;
if (typeof obj.id === "number" && !Number.isNaN(obj.id)) {
return String(obj.id);
}
if (typeof obj.id === "string" && obj.id.trim()) {
return obj.id.trim();
}
}
return null;
}
export function noticeMatchesLabelFilter(
notice: { label?: unknown },
filterTokens: string[],
): boolean {
if (filterTokens.length === 0) return true;
const id = normalizeNoticeLabelId(notice?.label);
if (id !== null && filterTokens.includes(id)) return true;
return filterTokens.includes(JSON.stringify(notice?.label));
}