Skip to content

Commit ac680fa

Browse files
committed
[ADD] outlook: add search, refresh button to fetch the data from extension
After this commit: - Add functionality to search leads/tasks/tickets in the database directly from the extension. - Add a refrech button to refrech the data from the database. - Generalize the search & refresh functionality.
1 parent ef529cd commit ac680fa

File tree

9 files changed

+14335
-62
lines changed

9 files changed

+14335
-62
lines changed

outlook/package-lock.json

Lines changed: 14036 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

outlook/src/taskpane/api.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const api = {
1414
searchPartner: '/mail_plugin/partner/search',
1515
getTranslations: '/mail_plugin/get_translations',
1616
searchProject: '/mail_plugin/project/search',
17+
Leads: '/mail/plugin/leads',
18+
Tasks: '/mail/plugin/tasks',
19+
Tickets: '/mail/plugin/tickets',
1720

1821
// Authentication
1922
loginPage: '/web/login', // Should be the usual Odoo login page.

outlook/src/taskpane/components/CollapseSection/CollapseSection.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { faChevronDown, faChevronRight, faPlus } from '@fortawesome/free-solid-s
33
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
44
import './CollapseSection.css';
55
import { ReactElement } from 'react';
6+
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
7+
import Lead from '../../../classes/Lead';
8+
import Partner from '../../../classes/Partner';
9+
import SearchRefrech from '../SearchRefrech/SearchRefrech';
10+
import Task from '../../../classes/Task';
611

712
type CollapseSectionProps = {
813
title: string;
@@ -12,10 +17,15 @@ type CollapseSectionProps = {
1217
hideCollapseButton?: boolean;
1318
children: ReactElement;
1419
className?: string;
20+
partner?: Partner;
21+
searchType?: 'lead' | 'task' | 'ticket';
22+
setIsLoading?: (isLoading: boolean) => void;
23+
updateRecords?: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
1524
};
1625

1726
type CollapseSectionSate = {
1827
isCollapsed: boolean;
28+
isSearching: boolean;
1929
};
2030

2131
/***
@@ -26,13 +36,18 @@ class CollapseSection extends React.Component<CollapseSectionProps, CollapseSect
2636
super(props, context);
2737
this.state = {
2838
isCollapsed: props.isCollapsed,
39+
isSearching: false,
2940
};
3041
}
3142

3243
private onCollapseButtonClick = () => {
3344
this.setState({ isCollapsed: !this.state.isCollapsed });
3445
};
3546

47+
private setIsSearching = (isSearching: boolean) => {
48+
this.setState({ isSearching });
49+
};
50+
3651
render() {
3752
const addButton = this.props.hasAddButton && (
3853
<FontAwesomeIcon icon={faPlus} className="collapse-section-button" onClick={this.props.onAddButtonClick} />
@@ -46,13 +61,29 @@ class CollapseSection extends React.Component<CollapseSectionProps, CollapseSect
4661
/>
4762
);
4863

64+
const searchRefrech = this.props.searchType && (
65+
<SearchRefrech
66+
title={this.props.title}
67+
partner={this.props.partner}
68+
searchType={this.props.searchType}
69+
setIsSearching={this.setIsSearching}
70+
setIsLoading={this.props.setIsLoading}
71+
updateRecords={this.props.updateRecords}
72+
/>
73+
);
74+
4975
return (
5076
<div className={`section-card ${this.props.className}`}>
5177
<div className="section-top">
52-
<div className="section-title-text">{this.props.title}</div>
53-
<div>
54-
{addButton}
55-
{collapseButton}
78+
{!this.state.isSearching && <div className="section-title-text">{this.props.title}</div>}
79+
<div style={{ display: 'flex' }}>
80+
{searchRefrech}
81+
{!this.state.isSearching && (
82+
<>
83+
{addButton}
84+
{collapseButton}
85+
</>
86+
)}
5687
</div>
5788
</div>
5889
{!this.state.isCollapsed && this.props.children}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import * as React from 'react';
2+
import { _t } from '../../../utils/Translator';
3+
import { ContentType, HttpVerb, sendHttpRequest } from '../../../utils/httpRequest';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
import { TextField, TooltipHost } from 'office-ui-fabric-react';
6+
import { faArrowLeft, faRedoAlt, faSearch } from '@fortawesome/free-solid-svg-icons';
7+
import AppContext from '../AppContext';
8+
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
9+
import Lead from '../../../classes/Lead';
10+
import Partner from '../../../classes/Partner';
11+
import Task from '../../../classes/Task';
12+
import api from '../../api';
13+
14+
type SearchRefrechProps = {
15+
title: string;
16+
partner: Partner;
17+
searchType: 'lead' | 'task' | 'ticket';
18+
setIsSearching: (isSearching: boolean) => void;
19+
setIsLoading: (isLoading: boolean) => void;
20+
updateRecords: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
21+
};
22+
23+
type SearchRefrechState = {
24+
isSearching: boolean;
25+
query: string;
26+
};
27+
28+
class SearchRefrech extends React.Component<SearchRefrechProps, SearchRefrechState> {
29+
constructor(props, context) {
30+
super(props, context);
31+
this.state = {
32+
isSearching: false,
33+
query: '',
34+
};
35+
}
36+
37+
private onBackClick = () => {
38+
this.setState({ isSearching: !this.state.isSearching, query: '' });
39+
this.props.updateRecords(null);
40+
this.props.setIsSearching(false);
41+
};
42+
43+
private onRefrechClick = () => {
44+
this.searchData();
45+
};
46+
47+
private onSearchClick = () => {
48+
this.setState({ isSearching: true });
49+
this.props.setIsSearching(true);
50+
};
51+
52+
private onKeyDown = (event) => {
53+
if (event.key == 'Enter') {
54+
if (!this.state.query.trim()) {
55+
return;
56+
}
57+
this.searchData(this.state.query);
58+
}
59+
};
60+
61+
private getEndPoint = (params: string) => {
62+
let endPoint = '';
63+
if (this.props.searchType === 'lead') {
64+
endPoint = api.Leads;
65+
} else if (this.props.searchType === 'task') {
66+
endPoint = api.Tasks;
67+
} else {
68+
endPoint = api.Tickets;
69+
}
70+
return endPoint + `/${params}`;
71+
};
72+
73+
private searchData = async (query?: string) => {
74+
this.props.setIsLoading(true);
75+
let endPoint = query ? this.getEndPoint('search') : this.getEndPoint('refresh');
76+
77+
try {
78+
const res = sendHttpRequest(
79+
HttpVerb.POST,
80+
api.baseURL + endPoint,
81+
ContentType.Json,
82+
this.context.getConnectionToken(),
83+
{ query: query, partner: this.props.partner },
84+
true,
85+
);
86+
const data = JSON.parse(await res.promise);
87+
88+
if (this.props.searchType === 'lead') {
89+
data.result = data.result.map((lead: Lead) => Lead.fromJSON(lead));
90+
} else if (this.props.searchType === 'task') {
91+
data.result = data.result.map((task: Task) => Task.fromJSON(task));
92+
} else {
93+
data.result = data.result.map((ticket: HelpdeskTicket) => HelpdeskTicket.fromJSON(ticket));
94+
}
95+
if (data.result) {
96+
this.props.updateRecords(data.result);
97+
}
98+
} catch (error) {
99+
this.context.showHttpErrorMessage(error);
100+
} finally {
101+
this.props.setIsLoading(false);
102+
}
103+
};
104+
105+
render() {
106+
let broadCampStyle = {
107+
display: 'flex',
108+
justifyContent: 'space-between',
109+
alignItems: 'center',
110+
fontSize: 'medium',
111+
color: '#787878',
112+
fontWeight: 600,
113+
};
114+
115+
let searchButton = null;
116+
let refrechButton = null;
117+
if (!this.state.isSearching) {
118+
searchButton = (
119+
<div style={{ display: 'flex' }}>
120+
<TooltipHost content={_t(`Search ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}>
121+
<div className="odoo-muted-button" onClick={this.onSearchClick} style={{ border: 'none' }}>
122+
<FontAwesomeIcon icon={faSearch} />
123+
</div>
124+
</TooltipHost>
125+
</div>
126+
);
127+
refrechButton = (
128+
<TooltipHost content={_t(`Refresh ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}>
129+
<div className="odoo-muted-button" onClick={this.onRefrechClick} style={{ border: 'none' }}>
130+
<FontAwesomeIcon icon={faRedoAlt} />
131+
</div>
132+
</TooltipHost>
133+
);
134+
}
135+
136+
let backButton = null;
137+
let searchBar = null;
138+
if (this.state.isSearching) {
139+
backButton = (
140+
<div className="odoo-muted-button" onClick={this.onBackClick} style={{ border: 'none' }}>
141+
<FontAwesomeIcon icon={faArrowLeft} />
142+
</div>
143+
);
144+
searchBar = (
145+
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'stretch' }}>
146+
<TextField
147+
className="input-search"
148+
style={{ marginLeft: '2px', marginRight: '2px' }}
149+
placeholder={_t(`Search ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}
150+
value={this.state.query}
151+
onKeyDown={this.onKeyDown}
152+
onChange={(_, newValue) => this.setState({ query: newValue || '' })}
153+
onFocus={(e) => e.target.select()}
154+
autoFocus
155+
/>
156+
<div
157+
className="odoo-muted-button search-icon"
158+
style={{ border: 'none' }}
159+
onClick={() => this.state.query.trim() && this.searchData(this.state.query)}>
160+
<FontAwesomeIcon icon={faSearch} />
161+
</div>
162+
</div>
163+
);
164+
}
165+
166+
return (
167+
<div style={broadCampStyle}>
168+
{backButton}
169+
{searchButton}
170+
{refrechButton}
171+
{searchBar}
172+
</div>
173+
);
174+
}
175+
}
176+
177+
SearchRefrech.contextType = AppContext;
178+
179+
export default SearchRefrech;

outlook/src/taskpane/components/Section/Section.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import CollapseSection from '../CollapseSection/CollapseSection';
66
import ListItem from '../ListItem/ListItem';
77
import api from '../../api';
88
import AppContext from '../AppContext';
9+
import { OdooTheme } from '../../../utils/Themes';
10+
import { Spinner, SpinnerSize } from 'office-ui-fabric-react';
11+
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
12+
import Lead from '../../../classes/Lead';
13+
import Task from '../../../classes/Task';
914

1015
type SectionAbstractProps = {
1116
className?: string;
@@ -34,11 +39,14 @@ type SectionAbstractProps = {
3439
msgNoRecord: string;
3540
msgLogEmail: string;
3641
getRecordDescription: (any) => string;
42+
searchType: 'lead' | 'task' | 'ticket';
43+
updateRecords: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
3744
};
3845

3946
type SectionAbstractState = {
4047
records: any[];
4148
isCollapsed: boolean;
49+
isLoading: boolean;
4250
};
4351

4452
/**
@@ -49,7 +57,7 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
4957
constructor(props, context) {
5058
super(props, context);
5159
const isCollapsed = !props.records || !props.records.length;
52-
this.state = { records: this.props.records, isCollapsed: isCollapsed };
60+
this.state = { records: this.props.records, isCollapsed: isCollapsed, isLoading: false };
5361
}
5462

5563
private onClickCreate = () => {
@@ -102,35 +110,47 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
102110
});
103111
};
104112

113+
private setIsLoading = (isLoading: boolean) => {
114+
this.setState({ isLoading });
115+
};
116+
105117
private getSection = () => {
106118
if (!this.props.partner.isAddedToDatabase()) {
107119
return (
108120
<div className="list-text">
109121
{_t(this.props.canCreatePartner ? this.props.msgNoPartner : this.props.msgNoPartnerNoAccess)}
110122
</div>
111123
);
112-
} else if (this.state.records.length > 0) {
113-
return (
114-
<div className="section-content">
115-
{this.state.records.map((record) => (
116-
<ListItem
117-
model={this.props.model}
118-
res_id={record.id}
119-
key={record.id}
120-
title={record.name}
121-
description={this.props.getRecordDescription(record)}
122-
logTitle={_t(this.props.msgLogEmail)}
123-
/>
124-
))}
125-
</div>
126-
);
124+
} else {
125+
if (this.state.isLoading) {
126+
return (
127+
<div className="section-card search-spinner">
128+
<Spinner theme={OdooTheme} size={SpinnerSize.large} style={{ margin: 'auto' }} />
129+
</div>
130+
);
131+
} else if (this.props.records.length > 0) {
132+
return (
133+
<div className="section-content">
134+
{this.props.records.map((record) => (
135+
<ListItem
136+
model={this.props.model}
137+
res_id={record.id}
138+
key={record.id}
139+
title={record.name}
140+
description={this.props.getRecordDescription(record)}
141+
logTitle={_t(this.props.msgLogEmail)}
142+
/>
143+
))}
144+
</div>
145+
);
146+
}
147+
return <div className="list-text">{_t(this.props.msgNoRecord)}</div>;
127148
}
128-
return <div className="list-text">{_t(this.props.msgNoRecord)}</div>;
129149
};
130150

131151
render() {
132-
const recordCount = this.state.records && this.state.records.length;
133-
const title = this.state.records
152+
const recordCount = this.props.records && this.props.records.length;
153+
const title = this.props.records
134154
? _t(this.props.titleCount, { count: recordCount.toString() })
135155
: _t(this.props.title);
136156

@@ -140,7 +160,11 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
140160
isCollapsed={this.state.isCollapsed}
141161
title={title}
142162
hasAddButton={this.props.partner.isAddedToDatabase()}
143-
onAddButtonClick={this.onClickCreate}>
163+
onAddButtonClick={this.onClickCreate}
164+
partner={this.props.partner}
165+
searchType={this.props.searchType}
166+
setIsLoading={this.setIsLoading}
167+
updateRecords={this.props.updateRecords}>
144168
{this.getSection()}
145169
</CollapseSection>
146170
);

0 commit comments

Comments
 (0)