mirror of
https://github.com/ttusk/leif.git
synced 2026-07-22 08:32:51 +00:00
feat(sessions): filter study history
This commit is contained in:
parent
535b1adc74
commit
774dc48419
5 changed files with 263 additions and 8 deletions
84
sample-vault/.obsidian/plugins/leif/main.js
vendored
84
sample-vault/.obsidian/plugins/leif/main.js
vendored
|
|
@ -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;
|
||||
|
|
|
|||
11
sample-vault/.obsidian/plugins/leif/styles.css
vendored
11
sample-vault/.obsidian/plugins/leif/styles.css
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
11
styles.css
11
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;
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement>("[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<void> => {
|
||||
const control = leaf.containerEl.querySelector<HTMLInputElement | HTMLSelectElement>(
|
||||
`[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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue