From 774dc484192ccf4a09d52da978223dba66ec0b85 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Date: Wed, 15 Jul 2026 19:37:01 -0300 Subject: [PATCH] feat(sessions): filter study history --- sample-vault/.obsidian/plugins/leif/main.js | 84 +++++++++++++++- .../.obsidian/plugins/leif/styles.css | 11 +++ src/ui/view/components/SessionsTab.ts | 98 ++++++++++++++++++- styles.css | 11 +++ tests/ui/LeifView.test.ts | 67 +++++++++++++ 5 files changed, 263 insertions(+), 8 deletions(-) diff --git a/sample-vault/.obsidian/plugins/leif/main.js b/sample-vault/.obsidian/plugins/leif/main.js index 2346389..75718e0 100644 --- a/sample-vault/.obsidian/plugins/leif/main.js +++ b/sample-vault/.obsidian/plugins/leif/main.js @@ -3969,6 +3969,10 @@ var SessionsTab = class { this.editingSessionId = null; this.isCreatingSession = false; this.pendingDeleteSessionId = null; + this.historySubjectFilter = ""; + this.historyTypeFilter = ""; + this.historyFromFilter = ""; + this.historyToFilter = ""; const repositoryFactory = new EntityRepositoryFactory(dataStore); this.registerStudySessionUseCase = new RegisterStudySessionUseCase(dataStore, repositoryFactory); this.deleteStudySessionUseCase = new DeleteStudySessionUseCase(dataStore, repositoryFactory); @@ -4047,10 +4051,18 @@ var SessionsTab = class { container.appendChild(this.renderCreateSessionForm(data)); } const subjects = data.subjects.filter((subject) => subject.contestId === activeContest.id); - const recentSessions = DomHelpers.createCard("Hist\xF3rico recente"); - const sessions = data.studySessions.filter((session) => session.contestId === activeContest.id).slice().reverse().slice(0, 10); + const recentSessions = DomHelpers.createCard("Hist\xF3rico de sess\xF5es"); + const allSessions = data.studySessions.filter((session) => session.contestId === activeContest.id).slice().reverse(); + const sessions = this.filterSessions(allSessions); + if (allSessions.length > 0) { + recentSessions.appendChild(this.renderHistoryFilters(subjects)); + } if (sessions.length === 0) { - recentSessions.appendChild(DomHelpers.createParagraph("Nenhuma sess\xE3o registrada.")); + recentSessions.appendChild( + DomHelpers.createParagraph( + allSessions.length === 0 ? "Nenhuma sess\xE3o registrada." : "Nenhuma sess\xE3o encontrada com esses filtros." + ) + ); } else { const { container: tableContainer, tbody } = DomHelpers.createCrudTable([ "Data", @@ -4070,6 +4082,72 @@ var SessionsTab = class { } container.appendChild(recentSessions); } + renderHistoryFilters(subjects) { + const subjectSelect = DomHelpers.createSelect( + [ + ["", "Todas"], + ...subjects.map((subject) => [subject.id, subject.name]) + ], + this.historySubjectFilter + ); + const typeSelect = DomHelpers.createSelect( + [ + ["", "Todos"], + [StudySessionType.PDF, "PDF"], + [StudySessionType.VIDEO, "V\xEDdeo"], + [StudySessionType.QUESTIONS, "Quest\xF5es"] + ], + this.historyTypeFilter + ); + const fromInput = DomHelpers.createInput("date", "De", this.historyFromFilter); + const toInput = DomHelpers.createInput("date", "At\xE9", this.historyToFilter); + subjectSelect.dataset.sessionFilter = "subject"; + typeSelect.dataset.sessionFilter = "type"; + fromInput.dataset.sessionFilter = "from"; + toInput.dataset.sessionFilter = "to"; + subjectSelect.addEventListener("change", async () => { + this.historySubjectFilter = subjectSelect.value; + await this.onUpdate(); + }); + typeSelect.addEventListener("change", async () => { + this.historyTypeFilter = typeSelect.value; + await this.onUpdate(); + }); + fromInput.addEventListener("change", async () => { + this.historyFromFilter = fromInput.value; + await this.onUpdate(); + }); + toInput.addEventListener("change", async () => { + this.historyToFilter = toInput.value; + await this.onUpdate(); + }); + const filters = DomHelpers.createElement("div", "leif-session-filters"); + filters.append( + DomHelpers.createStackedLabel("Mat\xE9ria", subjectSelect), + DomHelpers.createStackedLabel("Tipo", typeSelect), + DomHelpers.createStackedLabel("De", fromInput), + DomHelpers.createStackedLabel("At\xE9", toInput) + ); + return filters; + } + filterSessions(sessions) { + return sessions.filter((session) => { + if (this.historySubjectFilter && session.subjectId !== this.historySubjectFilter) { + return false; + } + if (this.historyTypeFilter && session.type !== this.historyTypeFilter) { + return false; + } + const sessionDate = session.studiedAt.slice(0, 10); + if (this.historyFromFilter && sessionDate < this.historyFromFilter) { + return false; + } + if (this.historyToFilter && sessionDate > this.historyToFilter) { + return false; + } + return true; + }); + } renderDisplayRow(session, data) { const tr = DomHelpers.createElement("tr"); tr.dataset.sessionId = session.id; diff --git a/sample-vault/.obsidian/plugins/leif/styles.css b/sample-vault/.obsidian/plugins/leif/styles.css index fa5922f..8e6e817 100644 --- a/sample-vault/.obsidian/plugins/leif/styles.css +++ b/sample-vault/.obsidian/plugins/leif/styles.css @@ -880,6 +880,13 @@ min-width: 0; } +.leif-session-filters { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(120px, 0.6fr) minmax(140px, 0.5fr) minmax(140px, 0.5fr); + gap: var(--size-4-3); + align-items: end; +} + @media (max-width: 720px) { .leif-view { padding: 14px; @@ -898,6 +905,10 @@ grid-template-columns: 1fr; } + .leif-session-filters { + grid-template-columns: 1fr; + } + .leif-list-row, .leif-key-value { flex-direction: column; diff --git a/src/ui/view/components/SessionsTab.ts b/src/ui/view/components/SessionsTab.ts index 22d5ead..71109cf 100644 --- a/src/ui/view/components/SessionsTab.ts +++ b/src/ui/view/components/SessionsTab.ts @@ -29,6 +29,10 @@ export class SessionsTab { private editingSessionId: string | null = null; private isCreatingSession = false; private pendingDeleteSessionId: string | null = null; + private historySubjectFilter = ""; + private historyTypeFilter = ""; + private historyFromFilter = ""; + private historyToFilter = ""; constructor( private readonly dataStore: PluginDataStore, @@ -126,15 +130,25 @@ export class SessionsTab { const subjects = data.subjects.filter((subject) => subject.contestId === activeContest.id); - const recentSessions = DomHelpers.createCard("Histórico recente"); - const sessions = data.studySessions + const recentSessions = DomHelpers.createCard("Histórico de sessões"); + const allSessions = data.studySessions .filter((session) => session.contestId === activeContest.id) .slice() - .reverse() - .slice(0, 10); + .reverse(); + const sessions = this.filterSessions(allSessions); + + if (allSessions.length > 0) { + recentSessions.appendChild(this.renderHistoryFilters(subjects)); + } if (sessions.length === 0) { - recentSessions.appendChild(DomHelpers.createParagraph("Nenhuma sessão registrada.")); + recentSessions.appendChild( + DomHelpers.createParagraph( + allSessions.length === 0 + ? "Nenhuma sessão registrada." + : "Nenhuma sessão encontrada com esses filtros." + ) + ); } else { const { container: tableContainer, tbody } = DomHelpers.createCrudTable([ "Data", @@ -158,6 +172,80 @@ export class SessionsTab { container.appendChild(recentSessions); } + private renderHistoryFilters(subjects: LeifPluginData["subjects"]): HTMLElement { + const subjectSelect = DomHelpers.createSelect( + [ + ["", "Todas"], + ...subjects.map((subject): [string, string] => [subject.id, subject.name]) + ], + this.historySubjectFilter + ); + const typeSelect = DomHelpers.createSelect( + [ + ["", "Todos"], + [StudySessionType.PDF, "PDF"], + [StudySessionType.VIDEO, "Vídeo"], + [StudySessionType.QUESTIONS, "Questões"] + ], + this.historyTypeFilter + ); + const fromInput = DomHelpers.createInput("date", "De", this.historyFromFilter); + const toInput = DomHelpers.createInput("date", "Até", this.historyToFilter); + + subjectSelect.dataset.sessionFilter = "subject"; + typeSelect.dataset.sessionFilter = "type"; + fromInput.dataset.sessionFilter = "from"; + toInput.dataset.sessionFilter = "to"; + + subjectSelect.addEventListener("change", async () => { + this.historySubjectFilter = subjectSelect.value; + await this.onUpdate(); + }); + typeSelect.addEventListener("change", async () => { + this.historyTypeFilter = typeSelect.value; + await this.onUpdate(); + }); + fromInput.addEventListener("change", async () => { + this.historyFromFilter = fromInput.value; + await this.onUpdate(); + }); + toInput.addEventListener("change", async () => { + this.historyToFilter = toInput.value; + await this.onUpdate(); + }); + + const filters = DomHelpers.createElement("div", "leif-session-filters"); + filters.append( + DomHelpers.createStackedLabel("Matéria", subjectSelect), + DomHelpers.createStackedLabel("Tipo", typeSelect), + DomHelpers.createStackedLabel("De", fromInput), + DomHelpers.createStackedLabel("Até", toInput) + ); + + return filters; + } + + private filterSessions(sessions: StudySession[]): StudySession[] { + return sessions.filter((session) => { + if (this.historySubjectFilter && session.subjectId !== this.historySubjectFilter) { + return false; + } + if (this.historyTypeFilter && session.type !== this.historyTypeFilter) { + return false; + } + + const sessionDate = session.studiedAt.slice(0, 10); + if (this.historyFromFilter && sessionDate < this.historyFromFilter) { + return false; + } + if (this.historyToFilter && sessionDate > this.historyToFilter) { + return false; + } + + return true; + }); + } + private renderDisplayRow(session: StudySession, data: LeifPluginData): HTMLElement { const tr = DomHelpers.createElement("tr"); tr.dataset.sessionId = session.id; diff --git a/styles.css b/styles.css index fa5922f..8e6e817 100644 --- a/styles.css +++ b/styles.css @@ -880,6 +880,13 @@ min-width: 0; } +.leif-session-filters { + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(120px, 0.6fr) minmax(140px, 0.5fr) minmax(140px, 0.5fr); + gap: var(--size-4-3); + align-items: end; +} + @media (max-width: 720px) { .leif-view { padding: 14px; @@ -898,6 +905,10 @@ grid-template-columns: 1fr; } + .leif-session-filters { + grid-template-columns: 1fr; + } + .leif-list-row, .leif-key-value { flex-direction: column; diff --git a/tests/ui/LeifView.test.ts b/tests/ui/LeifView.test.ts index e26db0e..f8206b0 100644 --- a/tests/ui/LeifView.test.ts +++ b/tests/ui/LeifView.test.ts @@ -386,6 +386,73 @@ describe("LeifView", () => { expect(questionsRow?.querySelector("td.leif-session-date-cell button[title='Excluir']")).not.toBeNull(); }); + it("filters session history by subject, type and period", async () => { + const dataStore = new InMemoryPluginDataStore(); + const factory = await seedUiData(dataStore); + const registerStudySession = new RegisterStudySessionUseCase(dataStore, factory); + + await registerStudySession.execute({ + id: "session-video-constitutional", + contestId: "contest-1", + subjectId: "subject-2", + type: "video", + studiedAt: "2026-06-13T20:00:00.000Z", + pagesOrCount: 1, + completed: true + }); + await registerStudySession.execute({ + id: "session-questions-portuguese", + contestId: "contest-1", + subjectId: "subject-1", + type: "questions", + studiedAt: "2026-06-20T20:00:00.000Z", + pagesOrCount: 20, + correctAnswers: 15, + completed: true + }); + + const { leaf } = await openLeifView(dataStore); + const sessionsTabButton = leaf.containerEl.querySelector("[data-tab='sessions']"); + + if (!sessionsTabButton) { + throw new Error("Sessions tab button was not rendered."); + } + + sessionsTabButton.click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const setFilter = async (name: string, value: string): Promise => { + const control = leaf.containerEl.querySelector( + `[data-session-filter='${name}']` + ); + + if (!control) { + throw new Error(`${name} filter was not rendered.`); + } + + control.value = value; + control.dispatchEvent(new Event("change")); + await new Promise((resolve) => setTimeout(resolve, 0)); + }; + + expect(leaf.containerEl.querySelector(".leif-session-filters")).not.toBeNull(); + + await setFilter("subject", "subject-2"); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).not.toBeNull(); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-1']")).toBeNull(); + + await setFilter("subject", ""); + await setFilter("type", "questions"); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-questions-portuguese']")).not.toBeNull(); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).toBeNull(); + + await setFilter("type", ""); + await setFilter("from", "2026-06-14"); + await setFilter("to", "2026-06-30"); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-questions-portuguese']")).not.toBeNull(); + expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).toBeNull(); + }); + it("keeps contest actions next to the contest name", async () => { const dataStore = new InMemoryPluginDataStore(); await seedUiData(dataStore);