Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Filtering Capabilities to SummaryHtml Formatter #2062

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions app/formatters/SummaryHtml/Formatter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,100 @@
import { beforeEach, describe, test, expect } from "@jest/globals";
import * as moment from "moment-timezone";
import { AppConfigurations, Dates, Tags, WorklogSets } from "../../../tests/entities";
import { Tag } from "../../models";
import { IAppConfiguration } from "../../models/AppConfiguration";
import { SummaryHtmlFormatter, SummaryHtmlFormatterConfiguration } from ".";

describe('format with filters', () => {
let formatter: SummaryHtmlFormatter;
let appConfiguration: IAppConfiguration;

beforeEach(() => {
appConfiguration = AppConfigurations.normal();
});

test('filters worklog entries by tag value', async () => {
const configuration = new SummaryHtmlFormatterConfiguration(
[['client', 'project']],
[{
tagNames: ['client', 'project'],
filter: (worklog) => worklog.getTagValue('project') === 'Project1'
}]
);
formatter = new SummaryHtmlFormatter(configuration, appConfiguration);

const worklogSet = WorklogSets.double();
worklogSet.worklogs[0].addTag(Tags.client.ProCorp());
worklogSet.worklogs[0].addTag(new Tag('project', 'Project1'));
worklogSet.worklogs[1].addTag(Tags.client.ProCorp());
worklogSet.worklogs[1].addTag(new Tag('project', 'Project2'));

const formatted = await formatter.format(worklogSet);

expect(formatted).toMatch('Project1');
expect(formatted).not.toMatch('Project2');
});

test('applies multiple filters to grouped entries', async () => {
const configuration = new SummaryHtmlFormatterConfiguration(
[['client', 'project']],
[{
tagNames: ['client', 'project'],
filter: (worklog) => worklog.getTagValue('project') === 'Project1'
}, {
tagNames: ['client', 'project'],
filter: (worklog) => worklog.getDurationInMinutes() > 60
}]
);
formatter = new SummaryHtmlFormatter(configuration, appConfiguration);

const worklogSet = WorklogSets.double();
worklogSet.worklogs[0].startDateTime = Dates.pastTwoHours();
worklogSet.worklogs[0].endDateTime = Dates.now();
worklogSet.worklogs[0].addTag(Tags.client.ProCorp());
worklogSet.worklogs[0].addTag(new Tag('project', 'Project1'));

worklogSet.worklogs[1].startDateTime = Dates.pastHalfHour();
worklogSet.worklogs[1].endDateTime = Dates.now();
worklogSet.worklogs[1].addTag(Tags.client.ProCorp());
worklogSet.worklogs[1].addTag(new Tag('project', 'Project1'));

const formatted = await formatter.format(worklogSet);

expect(formatted).toMatch('2hs 0m');
expect(formatted).not.toMatch('0hs 30m');
});

test('handles empty filters gracefully', async () => {
const configuration = new SummaryHtmlFormatterConfiguration(
[['client', 'project']],
[]
);
formatter = new SummaryHtmlFormatter(configuration, appConfiguration);

const worklogSet = WorklogSets.double();
const formatted = await formatter.format(worklogSet);

expect(formatted).toMatch('Total time:');
});

test('maintains HTML structure with filtered groups', async () => {
const configuration = new SummaryHtmlFormatterConfiguration(
[['client', 'project']],
[{
tagNames: ['client', 'project'],
filter: () => false // Filter out everything
}]
);
formatter = new SummaryHtmlFormatter(configuration, appConfiguration);

const worklogSet = WorklogSets.double();
const formatted = await formatter.format(worklogSet);

expect(formatted).toMatch(/<[^>]+>/); // Contains HTML tags
expect(formatted).toMatch(/<p>.*<\/p>/); // Has proper HTML structure
});
});foreEach, describe, test, expect } from "@jest/globals";

import * as moment from "moment-timezone";
import { AppConfigurations, Dates, Tags, WorklogSets } from "../../../tests/entities";
Expand Down
51 changes: 51 additions & 0 deletions app/formatters/SummaryHtml/Formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,57 @@ import { WorklogSet } from '../../models/WorklogSet';
import { getLogger, LoggerCategory } from '../../services/Logger';
import { SummaryTextFormatter } from '../SummaryText';
import { SummaryHtmlFormatterConfiguration } from '.';
import { Worklog } from '../../models/Worklog';

const logger = getLogger(LoggerCategory.Formatters);

export class SummaryHtmlFormatter extends SummaryTextFormatter {
protected _configuration: SummaryHtmlFormatterConfiguration;

_generateAggregationLines(worklogs: Worklog[], tags: string[], indentationLevel = 1): string[] {
if (!worklogs || !worklogs.length || !tags || !tags.length) return [];

// Apply filters for this tag group
const filteredWorklogs = this._applyFilters(worklogs, tags);

return super._generateAggregationLines(filteredWorklogs, tags, indentationLevel);
}

private _applyFilters(worklogs: Worklog[], tags: string[]): Worklog[] {
const matchingFilters = this._configuration.filters.filter(f =>
this._arraysHaveSameElements(f.tagNames, tags));

if (!matchingFilters.length) {
return worklogs;
}

return worklogs.filter(worklog =>
matchingFilters.every(filter => filter.filter(worklog))
);
}

private _arraysHaveSameElements(arr1: string[], arr2: string[]): boolean {
if (arr1.length !== arr2.length) return false;
const sorted1 = [...arr1].sort();
const sorted2 = [...arr2].sort();
return sorted1.every((element, index) => element === sorted2[index]);
}

async format(worklogSet: WorklogSet): Promise<string> {
const summaryText = await super.format(worklogSet);

logger.debug('Initializing markdown converter');
const markdownConverter = new showdown.Converter(this._configuration);

logger.debug('Converting summary to html');
const output = markdownConverter.makeHtml(summaryText);

logger.debug('SummaryHtmlFormatter output:', output);
return output;
}
}

export default SummaryHtmlFormatter;

const logger = getLogger(LoggerCategory.Formatters);

Expand Down
19 changes: 16 additions & 3 deletions app/formatters/SummaryHtml/SummaryHtmlFormatterConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { SummaryTextFormatterConfiguration } from "../SummaryText";
import { Worklog } from "../../models/Worklog";

export class SummaryHtmlFormatterConfiguration extends SummaryTextFormatterConfiguration
{
public type = 'SummaryHtml';
export type FilterFunction = (worklog: Worklog) => boolean;
export type TagGroupFilter = {
tagNames: string[];
filter: FilterFunction;
};

export class SummaryHtmlFormatterConfiguration extends SummaryTextFormatterConfiguration {
public type = "SummaryHtml";

constructor(
aggregateByTags: string[][],
public filters: TagGroupFilter[] = [],
) {
super(aggregateByTags);
}
}
Loading