Skip to content

[ADD] outlook: add search and refresh button #47

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
14,072 changes: 14,036 additions & 36 deletions outlook/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions outlook/src/taskpane/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const api = {
searchPartner: '/mail_plugin/partner/search',
getTranslations: '/mail_plugin/get_translations',
searchProject: '/mail_plugin/project/search',
Leads: '/mail/plugin/leads',
Tasks: '/mail/plugin/tasks',
Tickets: '/mail/plugin/tickets',

// Authentication
loginPage: '/web/login', // Should be the usual Odoo login page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { faChevronDown, faChevronRight, faPlus } from '@fortawesome/free-solid-s
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import './CollapseSection.css';
import { ReactElement } from 'react';
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
import Lead from '../../../classes/Lead';
import Partner from '../../../classes/Partner';
import SearchRefrech from '../SearchRefrech/SearchRefrech';
import Task from '../../../classes/Task';

type CollapseSectionProps = {
title: string;
Expand All @@ -12,10 +17,15 @@ type CollapseSectionProps = {
hideCollapseButton?: boolean;
children: ReactElement;
className?: string;
partner?: Partner;
searchType?: 'lead' | 'task' | 'ticket';
setIsLoading?: (isLoading: boolean) => void;
updateRecords?: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
};

type CollapseSectionSate = {
isCollapsed: boolean;
isSearching: boolean;
};

/***
Expand All @@ -26,13 +36,18 @@ class CollapseSection extends React.Component<CollapseSectionProps, CollapseSect
super(props, context);
this.state = {
isCollapsed: props.isCollapsed,
isSearching: false,
};
}

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

private setIsSearching = (isSearching: boolean) => {
this.setState({ isSearching });
};

render() {
const addButton = this.props.hasAddButton && (
<FontAwesomeIcon icon={faPlus} className="collapse-section-button" onClick={this.props.onAddButtonClick} />
Expand All @@ -46,13 +61,29 @@ class CollapseSection extends React.Component<CollapseSectionProps, CollapseSect
/>
);

const searchRefrech = this.props.searchType && (
<SearchRefrech
title={this.props.title}
partner={this.props.partner}
searchType={this.props.searchType}
setIsSearching={this.setIsSearching}
setIsLoading={this.props.setIsLoading}
updateRecords={this.props.updateRecords}
/>
);

return (
<div className={`section-card ${this.props.className}`}>
<div className="section-top">
<div className="section-title-text">{this.props.title}</div>
<div>
{addButton}
{collapseButton}
{!this.state.isSearching && <div className="section-title-text">{this.props.title}</div>}
<div style={{ display: 'flex' }}>
{searchRefrech}
{!this.state.isSearching && (
<>
{addButton}
{collapseButton}
</>
)}
</div>
</div>
{!this.state.isCollapsed && this.props.children}
Expand Down
179 changes: 179 additions & 0 deletions outlook/src/taskpane/components/SearchRefrech/SearchRefrech.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import * as React from 'react';
import { _t } from '../../../utils/Translator';
import { ContentType, HttpVerb, sendHttpRequest } from '../../../utils/httpRequest';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { TextField, TooltipHost } from 'office-ui-fabric-react';
import { faArrowLeft, faRedoAlt, faSearch } from '@fortawesome/free-solid-svg-icons';
import AppContext from '../AppContext';
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
import Lead from '../../../classes/Lead';
import Partner from '../../../classes/Partner';
import Task from '../../../classes/Task';
import api from '../../api';

type SearchRefrechProps = {
title: string;
partner: Partner;
searchType: 'lead' | 'task' | 'ticket';
setIsSearching: (isSearching: boolean) => void;
setIsLoading: (isLoading: boolean) => void;
updateRecords: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
};

type SearchRefrechState = {
isSearching: boolean;
query: string;
};

class SearchRefrech extends React.Component<SearchRefrechProps, SearchRefrechState> {
constructor(props, context) {
super(props, context);
this.state = {
isSearching: false,
query: '',
};
}

private onBackClick = () => {
this.setState({ isSearching: !this.state.isSearching, query: '' });
this.props.updateRecords(null);
this.props.setIsSearching(false);
};

private onRefrechClick = () => {
this.searchData();
};

private onSearchClick = () => {
this.setState({ isSearching: true });
this.props.setIsSearching(true);
};

private onKeyDown = (event) => {
if (event.key == 'Enter') {
if (!this.state.query.trim()) {
return;
}
this.searchData(this.state.query);
}
};

private getEndPoint = (params: string) => {
let endPoint = '';
if (this.props.searchType === 'lead') {
endPoint = api.Leads;
} else if (this.props.searchType === 'task') {
endPoint = api.Tasks;
} else {
endPoint = api.Tickets;
}
return endPoint + `/${params}`;
};

private searchData = async (query?: string) => {
this.props.setIsLoading(true);
let endPoint = query ? this.getEndPoint('search') : this.getEndPoint('refresh');

try {
const res = sendHttpRequest(
HttpVerb.POST,
api.baseURL + endPoint,
ContentType.Json,
this.context.getConnectionToken(),
{ query: query, partner: this.props.partner },
true,
);
const data = JSON.parse(await res.promise);

if (this.props.searchType === 'lead') {
data.result = data.result.map((lead: Lead) => Lead.fromJSON(lead));
} else if (this.props.searchType === 'task') {
data.result = data.result.map((task: Task) => Task.fromJSON(task));
} else {
data.result = data.result.map((ticket: HelpdeskTicket) => HelpdeskTicket.fromJSON(ticket));
}
if (data.result) {
this.props.updateRecords(data.result);
}
} catch (error) {
this.context.showHttpErrorMessage(error);
} finally {
this.props.setIsLoading(false);
}
};

render() {
let broadCampStyle = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: 'medium',
color: '#787878',
fontWeight: 600,
};

let searchButton = null;
let refrechButton = null;
if (!this.state.isSearching) {
searchButton = (
<div style={{ display: 'flex' }}>
<TooltipHost content={_t(`Search ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}>
<div className="odoo-muted-button" onClick={this.onSearchClick} style={{ border: 'none' }}>
<FontAwesomeIcon icon={faSearch} />
</div>
</TooltipHost>
</div>
);
refrechButton = (
<TooltipHost content={_t(`Refresh ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}>
<div className="odoo-muted-button" onClick={this.onRefrechClick} style={{ border: 'none' }}>
<FontAwesomeIcon icon={faRedoAlt} />
</div>
</TooltipHost>
);
}

let backButton = null;
let searchBar = null;
if (this.state.isSearching) {
backButton = (
<div className="odoo-muted-button" onClick={this.onBackClick} style={{ border: 'none' }}>
<FontAwesomeIcon icon={faArrowLeft} />
</div>
);
searchBar = (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'stretch' }}>
<TextField
className="input-search"
style={{ marginLeft: '2px', marginRight: '2px' }}
placeholder={_t(`Search ${this.props.title.slice(0, this.props.title.indexOf(' '))}`)}
value={this.state.query}
onKeyDown={this.onKeyDown}
onChange={(_, newValue) => this.setState({ query: newValue || '' })}
onFocus={(e) => e.target.select()}
autoFocus
/>
<div
className="odoo-muted-button search-icon"
style={{ border: 'none' }}
onClick={() => this.state.query.trim() && this.searchData(this.state.query)}>
<FontAwesomeIcon icon={faSearch} />
</div>
</div>
);
}

return (
<div style={broadCampStyle}>
{backButton}
{searchButton}
{refrechButton}
{searchBar}
</div>
);
}
}

SearchRefrech.contextType = AppContext;

export default SearchRefrech;
60 changes: 42 additions & 18 deletions outlook/src/taskpane/components/Section/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import CollapseSection from '../CollapseSection/CollapseSection';
import ListItem from '../ListItem/ListItem';
import api from '../../api';
import AppContext from '../AppContext';
import { OdooTheme } from '../../../utils/Themes';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react';
import HelpdeskTicket from '../../../classes/HelpdeskTicket';
import Lead from '../../../classes/Lead';
import Task from '../../../classes/Task';

type SectionAbstractProps = {
className?: string;
Expand Down Expand Up @@ -34,11 +39,14 @@ type SectionAbstractProps = {
msgNoRecord: string;
msgLogEmail: string;
getRecordDescription: (any) => string;
searchType: 'lead' | 'task' | 'ticket';
updateRecords: (records: Lead[] | Task[] | HelpdeskTicket[]) => void;
};

type SectionAbstractState = {
records: any[];
isCollapsed: boolean;
isLoading: boolean;
};

/**
Expand All @@ -49,7 +57,7 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
constructor(props, context) {
super(props, context);
const isCollapsed = !props.records || !props.records.length;
this.state = { records: this.props.records, isCollapsed: isCollapsed };
this.state = { records: this.props.records, isCollapsed: isCollapsed, isLoading: false };
}

private onClickCreate = () => {
Expand Down Expand Up @@ -102,30 +110,42 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
});
};

private setIsLoading = (isLoading: boolean) => {
this.setState({ isLoading });
};

private getSection = () => {
if (!this.props.partner.isAddedToDatabase()) {
return (
<div className="list-text">
{_t(this.props.canCreatePartner ? this.props.msgNoPartner : this.props.msgNoPartnerNoAccess)}
</div>
);
} else if (this.state.records.length > 0) {
return (
<div className="section-content">
{this.state.records.map((record) => (
<ListItem
model={this.props.model}
res_id={record.id}
key={record.id}
title={record.name}
description={this.props.getRecordDescription(record)}
logTitle={_t(this.props.msgLogEmail)}
/>
))}
</div>
);
} else {
if (this.state.isLoading) {
return (
<div className="section-card search-spinner">
<Spinner theme={OdooTheme} size={SpinnerSize.large} style={{ margin: 'auto' }} />
</div>
);
} else if (this.props.records.length > 0) {
return (
<div className="section-content">
{this.props.records.map((record) => (
<ListItem
model={this.props.model}
res_id={record.id}
key={record.id}
title={record.name}
description={this.props.getRecordDescription(record)}
logTitle={_t(this.props.msgLogEmail)}
/>
))}
</div>
);
}
return <div className="list-text">{_t(this.props.msgNoRecord)}</div>;
}
return <div className="list-text">{_t(this.props.msgNoRecord)}</div>;
};

render() {
Expand All @@ -140,7 +160,11 @@ class Section extends React.Component<SectionAbstractProps, SectionAbstractState
isCollapsed={this.state.isCollapsed}
title={title}
hasAddButton={this.props.partner.isAddedToDatabase()}
onAddButtonClick={this.onClickCreate}>
onAddButtonClick={this.onClickCreate}
partner={this.props.partner}
searchType={this.props.searchType}
setIsLoading={this.setIsLoading}
updateRecords={this.props.updateRecords}>
{this.getSection()}
</CollapseSection>
);
Expand Down
Loading