Skip to content

Commit fb28ded

Browse files
committed
filter for creation/modification date
1 parent 892a83e commit fb28ded

8 files changed

Lines changed: 84 additions & 6 deletions

File tree

src/api/IBMiContent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ export default class IBMiContent {
747747
* @param filter: the criterias used to list the members
748748
* @returns
749749
*/
750-
async getMemberList(filter: { library: string, sourceFile: string, members?: string | string[], extensions?: string, memberText?: string, sort?: SortOptions, filterType?: FilterType }): Promise<IBMiMember[]> {
750+
async getMemberList(filter: { library: string, sourceFile: string, members?: string | string[], extensions?: string, memberText?: string, memberCreated?: string, memberChanged?: string, sort?: SortOptions, filterType?: FilterType }): Promise<IBMiMember[]> {
751751
const sort = filter.sort || { order: 'name' };
752752
const library = this.ibmi.upperCaseName(filter.library);
753753
const sourceFile = this.ibmi.upperCaseName(filter.sourceFile);
@@ -762,6 +762,9 @@ export default class IBMiContent {
762762
const memberTextFilter = parseFilter(filter.memberText, filter.filterType);
763763
const singleMemberText = memberTextFilter.noFilter && filter.memberText && !filter.memberText.includes(",") ? filter.memberText.toUpperCase().replace(/[*]/g, `%`) : undefined;
764764

765+
const createdFrom = Tools.parseFilterDate(filter.memberCreated);
766+
const changedFrom = Tools.parseFilterDate(filter.memberChanged);
767+
765768
const statement = /* sql */
766769
`SELECT RTRIM(OBJ_STAT.OBJNAME) AS SOURCE_FILE,
767770
RTRIM(PART_STAT.SYSTEM_TABLE_MEMBER) AS NAME,
@@ -777,6 +780,8 @@ export default class IBMiContent {
777780
${singleMember ? `AND RTRIM(PART_STAT.SYSTEM_TABLE_MEMBER) like '${singleMember.replaceAll('_', '+_')}' escape '+'` : ``}
778781
${singleMemberExtension && singleMemberExtension.trim() !== '%' ? `AND RTRIM(CAST(PART_STAT.SOURCE_TYPE AS VARCHAR(10))) like '${singleMemberExtension.replaceAll('_', '+_')}' escape '+'` : ``}
779782
${singleMemberText && singleMemberText.trim() !== '%' ? `AND UPPER(RTRIM(VARCHAR(PART_STAT.TEXT))) like '${singleMemberText.replaceAll('+', '++').replaceAll('_', '+_').replaceAll(`'`, `''`)}' escape '+'` : ``}
783+
${createdFrom ? `AND DATE(PART_STAT.CREATE_TIMESTAMP) >= DATE('${createdFrom}')` : ``}
784+
${changedFrom ? `AND DATE(PART_STAT.LAST_SOURCE_UPDATE_TIMESTAMP) >= DATE('${changedFrom}')` : ``}
780785
ORDER BY ${sort.order === 'name' ? 'NAME' : 'CHANGED'} ${!sort.ascending ? 'DESC' : 'ASC'}`;
781786

782787
const results = await this.ibmi.runSQL(statement);

src/api/Tools.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ export namespace Tools {
159159
return 0;
160160
}
161161

162+
/**
163+
* Validates a date used in a filter and returns it in its `YYYY-MM-DD` canonical form.
164+
* @param date a date string, expected to be in the `YYYY-MM-DD` format
165+
* @returns the validated date, or `undefined` if it's blank or invalid
166+
*/
167+
export function parseFilterDate(date?: string) {
168+
const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec((date || ``).trim());
169+
if (parts) {
170+
const [year, month, day] = parts.slice(1).map(Number);
171+
const parsed = new Date(Date.UTC(year, month - 1, day));
172+
if (parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day) {
173+
return parts[0];
174+
}
175+
}
176+
return undefined;
177+
}
178+
162179
/**
163180
* Transforms a file path into an OS agnostic path.
164181
* - Replaces full home directory path by ~

src/api/configuration/config/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export interface ObjectFilters {
9090
member: string
9191
memberType: string
9292
memberText?: string
93+
memberCreated?: string
94+
memberChanged?: string
9395
protected: boolean
9496
}
9597

src/api/tests/suites/tools.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,35 @@ describe('Tools.parseLsPermissions tests', { concurrent: true }, () => {
256256
expect(Tools.parseLsPermissions('drwxr-zr-x 2 me grp 4096 Jun 8 weird')).toBeUndefined();
257257
});
258258
});
259-
});
259+
});
260+
describe('Tools.parseFilterDate tests', { concurrent: true }, () => {
261+
it('should accept a valid date', () => {
262+
expect(Tools.parseFilterDate('2024-02-29')).toBe('2024-02-29');
263+
});
264+
265+
it('should trim surrounding spaces', () => {
266+
expect(Tools.parseFilterDate(' 2024-01-31 ')).toBe('2024-01-31');
267+
});
268+
269+
it('should return undefined when no date is given', () => {
270+
expect(Tools.parseFilterDate(undefined)).toBeUndefined();
271+
expect(Tools.parseFilterDate('')).toBeUndefined();
272+
expect(Tools.parseFilterDate(' ')).toBeUndefined();
273+
});
274+
275+
it('should reject a wrong format', () => {
276+
expect(Tools.parseFilterDate('31/01/2024')).toBeUndefined();
277+
expect(Tools.parseFilterDate('2024-1-1')).toBeUndefined();
278+
expect(Tools.parseFilterDate('2024-01-01 00:00:00')).toBeUndefined();
279+
});
280+
281+
it('should reject a non existing date', () => {
282+
expect(Tools.parseFilterDate('2023-02-29')).toBeUndefined();
283+
expect(Tools.parseFilterDate('2024-13-01')).toBeUndefined();
284+
expect(Tools.parseFilterDate('2024-04-31')).toBeUndefined();
285+
});
286+
287+
it('should reject an SQL injection attempt', () => {
288+
expect(Tools.parseFilterDate(`2024-01-01') OR 1=1 --`)).toBeUndefined();
289+
});
290+
});

src/ui/Tools.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ export namespace VscodeTools {
228228
"Object types": escapeHtml(filter.types.join(`, `)),
229229
"Members": escapeHtml(filter.member),
230230
"Member type": escapeHtml(filter.memberType || `*`),
231-
"Member text": escapeHtml(filter.memberText || `*`),
231+
"Member text": filter.memberText ? escapeHtml(filter.memberText) : undefined,
232+
"Member created from": filter.memberCreated ? escapeHtml(filter.memberCreated) : undefined,
233+
"Member changed from": filter.memberChanged ? escapeHtml(filter.memberChanged) : undefined,
232234
"Protected": filter.protected ? vscode.l10n.t(`Yes`) : undefined
233235
}));
234236
tooltip.supportHtml = true;

src/ui/views/objectBrowser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ class ObjectBrowserFilterItem extends ObjectBrowserItem implements WithLibrary {
181181
const details = filterDetails();
182182
if (details !== `tooltip`) {
183183
const memberTextSuffix = filter.memberText && !/^\*(?:ALL)?$/.test(filter.memberText.trim()) ? ` [${filter.memberText}]` : ``;
184-
this.description = `${filter.library}/${filter.object}/${filter.member}.${filter.memberType || `*`} (${filter.types.join(`, `)})${memberTextSuffix}`;
184+
const dateSuffix = [
185+
filter.memberCreated ? `created ≥ ${filter.memberCreated}` : ``,
186+
filter.memberChanged ? `changed ≥ ${filter.memberChanged}` : ``
187+
].filter(Boolean).join(`, `);
188+
this.description = `${filter.library}/${filter.object}/${filter.member}.${filter.memberType || `*`} (${filter.types.join(`, `)})${memberTextSuffix}${dateSuffix ? ` {${dateSuffix}}` : ``}`;
185189
}
186190
this.tooltip = details !== `description` ? VscodeTools.filterToToolTip(filter) : ``;
187191

@@ -289,6 +293,8 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements O
289293
members: this.filter.member,
290294
extensions: this.filter.memberType,
291295
memberText: this.filter.memberText,
296+
memberCreated: this.filter.memberCreated,
297+
memberChanged: this.filter.memberChanged,
292298
filterType: this.filter.filterType,
293299
sort: this.sort
294300
});

src/webviews/CustomUI.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ interface WebviewMessageRequest {
4646
data?: any;
4747
}
4848

49-
type InputType = "text" | "number" | "color";
49+
type InputType = "text" | "number" | "color" | "date";
5050

5151
export class Section {
5252
readonly fields: Field[] = [];
@@ -206,6 +206,10 @@ export class CustomHTML extends Section {
206206
width: 15%;
207207
}
208208
209+
.date-input {
210+
width: 12em;
211+
}
212+
209213
:root{
210214
--dropdown-z-index: 666;
211215
}
@@ -595,7 +599,7 @@ export class Field {
595599
case `input`:
596600
const multiline = (this.rows || 1) > 1;
597601
const tag = multiline ? "vscode-textarea" : "vscode-textfield";
598-
const inputClass = this.inputType === 'color' ? `short-input` : `long-input`;
602+
const inputClass = this.inputType === 'color' ? `short-input` : this.inputType === 'date' ? `date-input` : `long-input`;
599603
return /* html */`
600604
<vscode-form-group variant="settings-group">
601605
${this.renderLabel()}

src/webviews/filters/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export async function editFilter(filter?: ObjectFilters, copy = false) {
2323
member: filter.member,
2424
memberType: filter.memberType,
2525
memberText: filter.memberText || `*`,
26+
memberCreated: filter.memberCreated,
27+
memberChanged: filter.memberChanged,
2628
protected: filter.protected
2729
}
2830

@@ -39,6 +41,8 @@ export async function editFilter(filter?: ObjectFilters, copy = false) {
3941
member: `*`,
4042
memberType: `*`,
4143
memberText: `*`,
44+
memberCreated: ``,
45+
memberChanged: ``,
4246
protected: false
4347
}
4448

@@ -57,6 +61,8 @@ export async function editFilter(filter?: ObjectFilters, copy = false) {
5761
.addInput(`member`, `Members`, `Member names filter.`, { default: filter.member })
5862
.addInput(`memberType`, `Member type`, `Member types filter.`, { default: filter.memberType })
5963
.addInput(`memberText`, `Member text`, `Member text description filter.`, { default: filter.memberText || `*` })
64+
.addInput(`memberCreated`, `Member created from`, `Only list members created on or after this date. Leave blank to list members whatever their creation date is.`, { default: filter.memberCreated || ``, inputType: `date` })
65+
.addInput(`memberChanged`, `Member changed from`, `Only list members last changed on or after this date. Leave blank to list members whatever their last change date is.`, { default: filter.memberChanged || ``, inputType: `date` })
6066
.addCheckbox(`protected`, `Protected`, `Make this filter protected, preventing modifications and source members from being saved.`, filter.protected)
6167
.addButtons({ id: `save`, label: `Save settings` })
6268
.loadPage<any>(`Filter: ${newFilter ? `New` : filter.name}`);
@@ -94,6 +100,11 @@ export async function editFilter(filter?: ObjectFilters, copy = false) {
94100
case `memberText`:
95101
data[key] = String(data[key].trim()) || `*`;
96102
break;
103+
case `memberCreated`:
104+
case `memberChanged`:
105+
// reset anything that isn't a valid date
106+
data[key] = Tools.parseFilterDate(String(data[key])) || ``;
107+
break;
97108
case `protected`:
98109
// Do nothing. It's a boolean
99110
break;

0 commit comments

Comments
 (0)