Skip to content

Commit

Permalink
add creator, created and modified filters
Browse files Browse the repository at this point in the history
  • Loading branch information
tantaman committed Dec 15, 2023
1 parent b94e8f3 commit 8c6374c
Show file tree
Hide file tree
Showing 7 changed files with 465 additions and 142 deletions.
79 changes: 55 additions & 24 deletions client/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {generateKeyBetween} from 'fractional-indexing';
import type {UndoManager} from '@rocicorp/undo';
import {HotKeys} from 'react-hotkeys';
import {
useCreatedFilterState,
useCreatorFilterState,
useIssueDetailState,
useModifiedFilterState,
useOrderByState,
usePriorityFilterState,
useStatusFilterState,
Expand All @@ -27,11 +30,23 @@ import {
IssueUpdateWithID,
ISSUE_KEY_PREFIX,
} from 'shared';
import {getFilters, getIssueOrder} from './filters';
import {Layout} from './layout/layout';
import {db} from './materialite/db';
import {useQuery} from '@vlcn.io/materialite-react';
import {issueFromKeyAndValue} from './issue/issue';
import {
getCreatedFilter,
getCreatorFilter,
getCreators,
getIssueOrder,
getModifiedFilter,
getPriorities,
getPriorityFilter,
getStatuses,
getStatusFilter,
getViewFilter,
getViewStatuses,
} from './filters';

type AppProps = {
rep: Replicache<M>;
Expand Down Expand Up @@ -72,34 +87,58 @@ function onNewDiff(diff: Diff) {

const App = ({rep, undoManager}: AppProps) => {
const [view] = useViewState();
const [priorityFilter] = usePriorityFilterState();
const [statusFilter] = useStatusFilterState();
const [orderBy] = useOrderByState();
const [detailIssueID, setDetailIssueID] = useIssueDetailState();
const [menuVisible, setMenuVisible] = useState(false);
const [priorityFilter] = usePriorityFilterState();
const [statusFilter] = useStatusFilterState();
const [createdFilter] = useCreatedFilterState();
const [creatorFilter] = useCreatorFilterState();
const [modifiedFilter] = useModifiedFilterState();

const [filters, setFilters] = useState(
getFilters(view, priorityFilter, statusFilter),
);
const [issueOrder, setIssueOrder] = useState(getIssueOrder(view, orderBy));

const [, filterdIssues] = useQuery(() => {
const start = performance.now();
const source = db.issues.getSortedSource(issueOrder);
const ret = source.stream
.filter(issue => filters.issuesFilter(issue))
.materialize(source.comparator);

const viewStatuses = getViewStatuses(view);
const statuses = getStatuses(statusFilter);
const filterFns = [
getStatusFilter(viewStatuses, statuses),
getPriorityFilter(getPriorities(priorityFilter)),
getCreatorFilter(getCreators(creatorFilter)),
getCreatedFilter(createdFilter),
getModifiedFilter(modifiedFilter),
];

let {stream} = source;
for (const filter of filterFns) {
if (filter !== null) {
stream = stream.filter(filter);
}
}

const ret = stream.materialize(source.comparator);
console.log(`Filter update duration: ${performance.now() - start}ms`);
return ret;
}, [issueOrder, filters]);
}, [
view,
issueOrder,
priorityFilter,
statusFilter,
createdFilter,
creatorFilter,
modifiedFilter,
]);

const [, viewIssueCount] = useQuery(() => {
const viewStatuses = getViewStatuses(view);
const viewFilterFn = getViewFilter(viewStatuses);

const source = db.issues.getSortedSource(issueOrder);
return source.stream
.filter(issue => filters.viewFilter(issue))
.size()
.materializeValue(0);
}, [filters]);
return source.stream.filter(viewFilterFn).size().materializeValue(0);
}, [view]);

const partialSync = useSubscribe(
rep,
Expand Down Expand Up @@ -132,14 +171,6 @@ const App = ({rep, undoManager}: AppProps) => {
});
}, [rep]);

useEffect(() => {
const f = getFilters(view, priorityFilter, statusFilter);
if (f.equals(filters)) {
return;
}
setFilters(f);
}, [view, priorityFilter?.join(), statusFilter?.join()]);

useEffect(() => {
setIssueOrder(getIssueOrder(view, orderBy));
}, [view, orderBy]);
Expand Down Expand Up @@ -249,10 +280,10 @@ const App = ({rep, undoManager}: AppProps) => {
view={view}
detailIssueID={detailIssueID}
isLoading={!partialSyncComplete}
filters={filters}
viewIssueCount={viewIssueCount || 0}
filteredIssues={filterdIssues}
rep={rep}
hasNonViewFilters={false}
onCloseMenu={handleCloseMenu}
onToggleMenu={handleToggleMenu}
onUpdateIssues={handleUpdateIssues}
Expand Down
180 changes: 107 additions & 73 deletions client/src/filters.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,125 @@
import {isEqual} from 'lodash';
import {Issue, Order, Priority, Status} from 'shared';

export class Filters {
private readonly _viewStatuses: Set<Status> | undefined;
private readonly _issuesStatuses: Set<Status> | undefined;
private readonly _issuesPriorities: Set<Priority> | undefined;
readonly hasNonViewFilters: boolean;
constructor(
view: string | null,
priorityFilter: Priority[] | null,
statusFilter: Status[] | null,
) {
this._viewStatuses = undefined;
switch (view?.toLowerCase()) {
case 'active':
this._viewStatuses = new Set(['IN_PROGRESS', 'TODO']);
break;
case 'backlog':
this._viewStatuses = new Set(['BACKLOG']);
break;
default:
this._viewStatuses = undefined;
}
export type Op = '<=' | '>=';
export type DateQueryArg = `${number}|${Op}`;

this._issuesStatuses = undefined;
this._issuesPriorities = undefined;
this.hasNonViewFilters = false;
if (statusFilter) {
this._issuesStatuses = new Set<Status>();
for (const s of statusFilter) {
if (!this._viewStatuses || this._viewStatuses.has(s)) {
this.hasNonViewFilters = true;
this._issuesStatuses.add(s);
}
}
}
if (!this.hasNonViewFilters) {
this._issuesStatuses = this._viewStatuses;
export function hasNonViewFilters(
viewStatuses: Set<string>,
statuses: Set<Status>,
) {
for (const s of statuses) {
if (!viewStatuses.has(s)) {
return true;
}
}

if (priorityFilter) {
this._issuesPriorities = new Set<Priority>();
for (const p of priorityFilter) {
this.hasNonViewFilters = true;
this._issuesPriorities.add(p);
}
if (this._issuesPriorities.size === 0) {
this._issuesPriorities = undefined;
}
}
return false;
}

export function getViewStatuses(view: string | null): Set<Status> {
switch (view?.toLowerCase()) {
case 'active':
return new Set(['IN_PROGRESS', 'TODO']);
case 'backlog':
return new Set(['BACKLOG']);
default:
return new Set();
}
}

export function getStatuses(statusFilter: Status[] | null): Set<Status> {
return new Set(statusFilter ? statusFilter : []);
}

export function getPriorities(
priorityFilter: Priority[] | null,
): Set<Priority> {
return new Set(priorityFilter ? priorityFilter : []);
}

export function getPriorityFilter(
priorities: Set<Priority>,
): null | ((issue: Issue) => boolean) {
if (priorities.size === 0) {
return null;
}
return issue => priorities.has(issue.priority);
}

export function getStatusFilter(
viewStatuses: Set<Status>,
statuses: Set<Status>,
): null | ((issue: Issue) => boolean) {
const allStatuses = new Set<Status>([...viewStatuses, ...statuses]);
if (allStatuses.size === 0) {
return null;
}
return issue => allStatuses.has(issue.status);
}

viewFilter(issue: Issue): boolean {
return this._viewStatuses ? this._viewStatuses.has(issue.status) : true;
export function getCreatorFilter(
creators: Set<string>,
): null | ((issue: Issue) => boolean) {
if (creators.size === 0) {
return null;
}
return issue => creators.has(issue.creator.toLowerCase());
}

export function getViewFilter(
viewStatuses: Set<Status>,
): (issue: Issue) => boolean {
return issue =>
viewStatuses.size === 0 ? true : viewStatuses.has(issue.status);
}

export function getModifiedFilter(
args: DateQueryArg[] | null,
): (issue: Issue) => boolean {
return createTimeFilter('modified', args);
}

export function getCreatedFilter(
args: DateQueryArg[] | null,
): (issue: Issue) => boolean {
return createTimeFilter('created', args);
}

issuesFilter(issue: Issue): boolean {
if (this._issuesStatuses) {
if (!this._issuesStatuses.has(issue.status)) {
return false;
}
function createTimeFilter(
property: 'created' | 'modified',
args: DateQueryArg[] | null,
): (issue: Issue) => boolean {
let before: number | null = null;
let after: number | null = null;
for (const arg of args || []) {
const [timePart, op] = arg.split('|') as [string, Op];
const time = parseInt(timePart);
switch (op) {
case '<=':
before = before ? Math.min(before, time) : time;
break;
case '>=':
after = after ? Math.max(after, time) : time;
break;
}
if (this._issuesPriorities) {
if (!this._issuesPriorities.has(issue.priority)) {
return false;
}
}
return issue => {
if (before && issue[property] > before) {
return false;
}
if (after && issue[property] < after) {
return false;
}
return true;
}
};
}

equals(other: Filters): boolean {
return (
this === other ||
(isEqual(this._viewStatuses, other._viewStatuses) &&
isEqual(this._issuesStatuses, other._issuesStatuses) &&
isEqual(this._issuesPriorities, other._issuesPriorities) &&
isEqual(this.hasNonViewFilters, other.hasNonViewFilters))
);
}
export function getCreators(creatorFilter: string[] | null): Set<string> {
return new Set(creatorFilter ? creatorFilter : []);
}

export function getFilters(
view: string | null,
priorityFilter: Priority[] | null,
statusFilter: Status[] | null,
): Filters {
return new Filters(view, priorityFilter, statusFilter);
export function getTitleFilter(title: string): (issue: Issue) => boolean {
return issue => issue.title.toLowerCase().includes(title.toLowerCase());
}

export function getIssueOrder(
Expand Down
38 changes: 25 additions & 13 deletions client/src/hooks/query-state-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,31 @@ import useQueryState, {
QueryStateProcessor,
} from './useQueryState';
import {Order, Priority, Status} from 'shared';
import {DateQueryArg} from '../filters';

const processOrderBy: QueryStateProcessor<Order> = {
toString: (value: Order) => value,
fromString: (value: string | null) => (value ?? 'MODIFIED') as Order,
};

const processStatuFilter: QueryStateProcessor<Status[]> = {
toString: (value: Status[]) => value.join(','),
fromString: (value: string | null) =>
value === null ? null : (value.split(',') as Status[]),
};

const processPriorityFilter: QueryStateProcessor<Priority[]> = {
toString: (value: Priority[]) => value.join(','),
fromString: (value: string | null) =>
value === null ? null : (value.split(',') as Priority[]),
};
function makeStringSetProcessor<T extends string>(): QueryStateProcessor<T[]> {
return {
toString: (value: T[]) => value.join(','),
fromString: (value: string | null) =>
value === null ? null : (value.split(',') as T[]),
};
}

export function useOrderByState() {
return useQueryState('orderBy', processOrderBy);
}

export function useStatusFilterState() {
return useQueryState('statusFilter', processStatuFilter);
return useQueryState('statusFilter', makeStringSetProcessor<Status>());
}

export function usePriorityFilterState() {
return useQueryState('priorityFilter', processPriorityFilter);
return useQueryState('priorityFilter', makeStringSetProcessor<Priority>());
}

export function useViewState() {
Expand All @@ -40,3 +37,18 @@ export function useViewState() {
export function useIssueDetailState() {
return useQueryState('iss', identityProcessor);
}

export function useCreatorFilterState() {
return useQueryState('creatorFilter', makeStringSetProcessor<string>());
}

export function useCreatedFilterState() {
return useQueryState('createdFilter', makeStringSetProcessor<DateQueryArg>());
}

export function useModifiedFilterState() {
return useQueryState(
'modifiedFilter',
makeStringSetProcessor<DateQueryArg>(),
);
}
Loading

0 comments on commit 8c6374c

Please sign in to comment.