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

CS-7889 task page lhs filter #2068

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"end": "2025-02-04"
},
"priority": {
"index": 2,
"label": "Medium"
"index": 3,
"label": "High"
},
"name": "Meet Prospect Customer",
"details": null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"data": {
"type": "card",
"attributes": {
"status": {
"index": 0,
"label": "Not Started",
"color": null,
"completed": false
},
"dateRange": {
"start": "2025-01-21",
"end": "2025-01-22"
},
"priority": {
"index": null,
"label": null
},
"name": "Today",
"details": null,
"description": null,
"thumbnailURL": null
},
"relationships": {
"crmApp": {
"links": {
"self": "../CrmApp/4e73712d-2a31-4ffe-9c22-d3de277257a6"
}
},
"subtasks": {
"links": {
"self": null
}
},
"assignee": {
"links": {
"self": null
}
},
"contact": {
"links": {
"self": null
}
},
"account": {
"links": {
"self": null
}
},
"deal": {
"links": {
"self": null
}
},
"tags": {
"links": {
"self": null
}
}
},
"meta": {
"adoptsFrom": {
"module": "../crm/task",
"name": "CRMTask"
}
}
}
}
101 changes: 85 additions & 16 deletions packages/experiments-realm/crm-app.gts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import type Owner from '@ember/owner';
import { tracked } from '@glimmer/tracking';
import { TrackedMap } from 'tracked-built-ins';
import { restartableTask } from 'ember-concurrency';
import { format, startOfWeek } from 'date-fns';
import { debounce } from 'lodash';

const dateFormat = `yyyy-MM-dd`;

import { Component, realmURL } from 'https://cardstack.com/base/card-api';

Expand All @@ -35,8 +39,10 @@ import HeartHandshakeIcon from '@cardstack/boxel-icons/heart-handshake';
import TargetArrowIcon from '@cardstack/boxel-icons/target-arrow';
import CalendarExclamation from '@cardstack/boxel-icons/calendar-exclamation';
import PresentationAnalytics from '@cardstack/boxel-icons/presentation-analytics';
import ListDetails from '@cardstack/boxel-icons/list-details';
import { urgencyTagValues } from './crm/account';
import { dealStatusValues } from './crm/deal';
import { taskStatusValues } from './crm/shared';
import type { Deal } from './crm/deal';
import DealSummary from './crm/deal-summary';
import { CRMTaskPlannerIsolated } from './crm/task-planner';
Expand Down Expand Up @@ -108,10 +114,16 @@ const ACCOUNT_FILTERS: LayoutFilter[] = [
const TASK_FILTERS: LayoutFilter[] = [
{
displayName: 'All Tasks',
icon: CalendarExclamation,
icon: ListDetails,
cardTypeName: 'CRM Task',
createNewButtonText: 'Create Task',
},
...taskStatusValues.map((status) => ({
displayName: status.label,
icon: status.icon,
cardTypeName: 'CRM Task',
createNewButtonText: 'Create Task',
})),
];

// need to use as typeof AppCard rather than CrmApp otherwise tons of lint errors
Expand All @@ -123,10 +135,14 @@ class CrmAppTemplate extends Component<typeof AppCard> {
['Account', ACCOUNT_FILTERS],
['Task', TASK_FILTERS],
]);
private taskPlannerAPI: CRMTaskPlannerIsolated;
@tracked private activeFilter: LayoutFilter = CONTACT_FILTERS[0];
@action private onFilterChange(filter: LayoutFilter) {
this.activeFilter = filter;
this.loadDealCards.perform();
if (this.activeTabId === 'Task') {
this.taskPlannerAPI.loadCards.perform();
}
}
//tabs
@tracked activeTabId: string | undefined = this.args.model.tabs?.[0]?.tabId;
Expand Down Expand Up @@ -193,6 +209,16 @@ class CrmAppTemplate extends Component<typeof AppCard> {
return result;
});

private setupTaskPlanner = (taskPlanner: CRMTaskPlannerIsolated) => {
this.taskPlannerAPI = taskPlanner;
};

private debouncedLoadTaskCards = debounce(() => {
if (this.activeTabId === 'Task') {
this.taskPlannerAPI.loadCards.perform();
}
}, 300);

richardhjtan marked this conversation as resolved.
Show resolved Hide resolved
get filters() {
return this.filterMap.get(this.activeTabId!)!;
}
Expand Down Expand Up @@ -265,7 +291,7 @@ class CrmAppTemplate extends Component<typeof AppCard> {

//query for tabs and filters
get query() {
const { loadAllFilters, activeFilter, activeTabId, searchKey } = this;
const { loadAllFilters, activeFilter, activeTabId } = this;

if (!loadAllFilters.isIdle || !activeFilter?.query) return;

Expand Down Expand Up @@ -299,40 +325,80 @@ class CrmAppTemplate extends Component<typeof AppCard> {
]
: [];

const searchFilter = searchKey
? [
{
any: [
{
on: activeFilter.cardRef,
contains: { name: searchKey },
},
],
},
]
: [];

return {
filter: {
on: activeFilter.cardRef,
every: [
defaultFilter,
...accountFilter,
...dealFilter,
...searchFilter,
...this.searchFilter,
...this.taskFilter,
],
},
sort: this.selectedSort?.sort ?? sortByCardTitleAsc,
} as Query;
}

get searchFilter() {
return this.searchKey
? [
{
any: [
{
on: this.activeFilter.cardRef,
contains: { name: this.searchKey },
},
],
},
]
: [];
}

get taskFilter() {
let taskFilter: Query['filter'][] = [];
if (
this.activeTabId === 'Task' &&
this.activeFilter.displayName !== 'All Tasks'
) {
const today = new Date();
switch (this.activeFilter.displayName) {
case 'Overdue':
const formattedDate = format(today, dateFormat);
taskFilter = [{ range: { 'dateRange.end': { lt: formattedDate } } }];
break;
case 'Due Today':
const formattedDueToday = format(today, dateFormat);
taskFilter = [{ eq: { 'dateRange.end': formattedDueToday } }];
break;
case 'Due this week':
const dueThisWeek = startOfWeek(today, { weekStartsOn: 1 });
const formattedDueThisWeek = format(dueThisWeek, dateFormat);
taskFilter = [
{ range: { 'dateRange.start': { gt: formattedDueThisWeek } } },
];
break;
case 'High Priority':
taskFilter = [{ eq: { 'priority.label': 'High' } }];
break;
case 'Unassigned':
taskFilter = [{ eq: { 'assignee.id': null } }];
break;
default:
break;
}
}
return taskFilter;
}

get searchPlaceholder() {
return `Search ${this.activeFilter.displayName}`;
}

@action
private setSearchKey(searchKey: string) {
this.searchKey = searchKey;
this.debouncedLoadTaskCards();
}

@action private onChangeView(id: ViewOption) {
Expand Down Expand Up @@ -439,6 +505,9 @@ class CrmAppTemplate extends Component<typeof AppCard> {
@fields={{@fields}}
@set={{@set}}
@fieldName={{@fieldName}}
@searchFilter={{this.searchFilter}}
@taskFilter={{this.taskFilter}}
@setupTaskPlanner={{this.setupTaskPlanner}}
/>
{{else if this.query}}
{{#if (eq this.selectedView 'card')}}
Expand Down
38 changes: 38 additions & 0 deletions packages/experiments-realm/crm/shared.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import AlertHexagon from '@cardstack/boxel-icons/alert-hexagon';
import CalendarStar from '@cardstack/boxel-icons/calendar-star';
import CalendarMonth from '@cardstack/boxel-icons/calendar-month';
import ChevronsUp from '@cardstack/boxel-icons/chevrons-up';
import UserQuestion from '@cardstack/boxel-icons/user-question';

export const taskStatusValues = [
{
index: 0,
icon: AlertHexagon,
label: 'Overdue',
value: 'overdue',
},
{
index: 1,
icon: CalendarStar,
label: 'Due Today',
value: 'due-today',
},
{
index: 2,
icon: CalendarMonth,
label: 'Due this week',
value: 'due-this-week',
},
{
index: 3,
icon: ChevronsUp,
label: 'High Priority',
value: 'high-priority',
},
{
index: 4,
icon: UserQuestion,
label: 'Unassigned',
value: 'unassigned',
},
];
32 changes: 32 additions & 0 deletions packages/experiments-realm/crm/task-planner.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import {
import type { LooseSingleCardDocument } from '@cardstack/runtime-common';
import type { Query } from '@cardstack/runtime-common/query';
import { getCards } from '@cardstack/runtime-common';
import { tracked } from '@glimmer/tracking';
import { restartableTask } from 'ember-concurrency';

export class CRMTaskPlannerIsolated extends BaseTaskPlannerIsolated<
typeof CRMTaskPlanner
> {
@tracked cardsQuery: { instances: CardDef[]; isLoading?: boolean };

constructor(owner: Owner, args: any) {
const config: TaskPlannerConfig = {
status: {
Expand Down Expand Up @@ -119,8 +123,21 @@ export class CRMTaskPlannerIsolated extends BaseTaskPlannerIsolated<
},
};
super(owner, args, config);

this.args.setupTaskPlanner(this);
// Initialize query once
this.cardsQuery = getCards(this.getTaskQuery, this.realmHrefs, {
isLive: true,
});
Comment on lines +138 to +140
Copy link
Contributor

@tintinthong tintinthong Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't getTaskQuery the only one which is changing? I think that is the expectation for the resource pattern. Generally, we shudn't reassign the resource particularly inside of a getter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tintinthong yes it should be only query changing. I have same thought that getCards API should be reactive when query changes, but currently it is not.

One example I did before was in deal summary page, we do a restartable tasks when the filter changes: #2002. This PR currently use this approach as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is surprising to me

}

private loadCards = restartableTask(async () => {
this.cardsQuery = getCards(this.getTaskQuery, this.realmHrefs, {
isLive: true,
});
return this.cardsQuery;
});

get parentId() {
return this.args.model?.id;
}
Expand All @@ -142,6 +159,17 @@ export class CRMTaskPlannerIsolated extends BaseTaskPlannerIsolated<
everyArr.push({ eq: { 'crmApp.id': this.parentId } });
}

const taskFilter = this.args.taskFilter || [];
const searchFilter = this.args.searchFilter || [];

if (taskFilter.length > 0) {
everyArr.push(...taskFilter);
}

if (searchFilter.length > 0) {
everyArr.push(...searchFilter);
}

return everyArr.length > 0
? {
filter: {
Expand All @@ -162,6 +190,10 @@ export class CRMTaskPlannerIsolated extends BaseTaskPlannerIsolated<
};
}

get cardInstances() {
return this.cardsQuery?.instances ?? [];
}

assigneeQuery = getCards(
{
filter: {
Expand Down
Loading