From 8487296d410e677b819875ede609eacfc4516a40 Mon Sep 17 00:00:00 2001 From: Jason-2020 Date: Mon, 13 Jan 2020 15:04:33 +0800 Subject: [PATCH 001/110] update readme --- README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c75bd707541..f78d144e9ce 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ erxes is an open source growth marketing platform. Marketing, sales, and customer service platform designed to help your business attract more engaged customers. Replace Hubspot with the mission and community-driven ecosystem. -Online demo | Join us on RocketChat +Live demo | Join us on RocketChat -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/ed8c207f4351446b8ace7a323630889f)](https://www.codacy.com/app/erxes/erxes) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/ed8c207f4351446b8ace7a323630889f)](https://www.codacy.com/app/erxes/erxes) [![Codeclimate Badge](https://api.codeclimate.com/v1/badges/693e2ffc40bc2601630d/maintainability)](https://codeclimate.com/github/erxes/erxes/maintainability) [![Bettercode](https://bettercodehub.com/edge/badge/erxes/erxes?branch=master)](https://bettercodehub.com/results/erxes/erxes) [![codebeat badge](https://codebeat.co/badges/33270439-27de-42e9-b48a-da76192b3b22)](https://codebeat.co/projects/github-com-erxes-erxes-master) @@ -21,10 +21,10 @@ erxes is an open source growth marketing platform. Marketing, sales, and custome erxes helps you attract and engage more customers while giving you high lead conversion. With erxes, all your marketing, sales and customer service tools are merged into one platform for greater output. Replace Hubspot with the mission and community-driven ecosystem. -* **Growth Hacking:** Managing your entire growth operation made easy. From ideas to actual performance, making sure everything recorded, prioritized and centralized in the single platform to get tested with pool of analysis and learnings, which made the growing as pleasure. -* **Email & SMS Marketing:** Reach your customer with personalized messaging. Keeping your customers hooked is definitely a challenge. Start converting your prospects into potential customers through email, SMS, Live chat, and In-app-messaging or more interactions to drive them to a successful close. You can connect to your customers in a whole new way with Erxes! +* **Growth Hacking:** Managing your entire growth operation made easy. From ideas to actual performance, making sure everything recorded, prioritized and centralized in the single platform to get tested with pool of analysis and learnings, which made the growing as pleasure. +* **Email & SMS Marketing:** Reach your customer with personalized messaging. Keeping your customers hooked is definitely a challenge. Start converting your prospects into potential customers through email, SMS, Live chat, and In-app-messaging or more interactions to drive them to a successful close. You can connect to your customers in a whole new way with Erxes! * **Pop-ups & Forms:** Create Stylish Pop-ups and Forms that Bring Leads. Turn regular visitors into qualified leads by capturing them with a customizable pop-ups, forms, and embedded placements. Erxes helps you to create stylish and contextual pop-ups, banners and bars fit all your marketing needs. -* **Sales Pipeline:** Track your entire sales pipeline from one dashboard. All your customer information and sales process in one board to follow up flawlessly. Have your sales managers to know everything needed to deliver increased levels of personalization before they contact customers. +* **Sales Pipeline:** Track your entire sales pipeline from one dashboard. All your customer information and sales process in one board to follow up flawlessly. Have your sales managers to know everything needed to deliver increased levels of personalization before they contact customers. * **Contact Management:** Manage Visitors, Customers, and Companies. Access our all-in-one CRM system in one go so that it’s easier to coordinate and manage your contacts and interactions with your customers. Erxes Contacts provides whole segmentation tools for you to work more effiecently. * **Lead Scoring:** Identify and Target Sales-Ready Leads. * **Shared Team Inbox:** Communicate faster and easier with your customers via one truly omnichannel platform. Combine real-time client and team communication with in-app messaging, live chat, email and form, so your customers can reach you however and wherever they want @@ -32,9 +32,9 @@ erxes helps you attract and engage more customers while giving you high lead con * **Knowledge base:** Create Help Articles for Customer Self-service. Educate both your customers and staff by creating a help center related to your brands, products and services to reach higher level of satisfactions. * **Task Management:** Work More Collaboratively and Get More Done. Save time, manage your projects, monitor your team and increase your productivity in just a few clicks. Erxes helps to turn chaos into clarity. ## Documentation - * Installation instructions
- * Use erxes with Docker
- * Translate erxes at Transifex
+ * Install erxes
+ * erxes documentation
+ * Contributing to erxes
## Contributors @@ -48,7 +48,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com - ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/erxes#sponsor)] @@ -58,11 +57,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - - - ## In-kind sponsors @@ -72,5 +68,5 @@ Support this project by becoming a sponsor. Your logo will show up here with a l       -## Copyright & License -Copyright (c) 2020 erxes Inc - Released under the GNU General Public License v3.0 +## License +GNU General Public License v3.0 From c47a50629d5c2b872a285c390a5e6eb5440865d5 Mon Sep 17 00:00:00 2001 From: Jason-2020 Date: Mon, 13 Jan 2020 15:06:52 +0800 Subject: [PATCH 002/110] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f78d144e9ce..6bc773b469c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ erxes helps you attract and engage more customers while giving you high lead con * **Task Management:** Work More Collaboratively and Get More Done. Save time, manage your projects, monitor your team and increase your productivity in just a few clicks. Erxes helps to turn chaos into clarity. ## Documentation * Install erxes
- * erxes documentation
+ * erxes documentation
* Contributing to erxes
## Contributors From 30753656e14a7f8951a988eeb018910f5b4a353b Mon Sep 17 00:00:00 2001 From: Bilguun Ariunbold Date: Thu, 5 Mar 2020 22:21:30 -0500 Subject: [PATCH 003/110] Create PULL_REQUEST_TEMPLATE.md (#1722) --- .github/PULL_REQUEST_TEMPLATE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..af0e6b8e932 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +[ISSUE](https://github.com/erxes/erxes/issues/ISSUE) + +### Context + +Your context here. Additionally, any screenshots. Delete this line. + + +// Delete the below section once completed +### PR Checklist +- [ ] Description is clearly stated under Context section +- [ ] Screenshots and the additional verifications are attached From 0e8cacd1e1470dc72211caad7aba527bf95f53aa Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Mon, 9 Mar 2020 19:58:58 +0800 Subject: [PATCH 004/110] fix(integrations): invalid check in conversations gmail kind --- .../conversationDetail/workarea/conversation/Conversation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/inbox/components/conversationDetail/workarea/conversation/Conversation.tsx b/src/modules/inbox/components/conversationDetail/workarea/conversation/Conversation.tsx index ce5065a458d..b6f2dd8d32c 100755 --- a/src/modules/inbox/components/conversationDetail/workarea/conversation/Conversation.tsx +++ b/src/modules/inbox/components/conversationDetail/workarea/conversation/Conversation.tsx @@ -60,7 +60,7 @@ class Conversation extends React.Component { const { kind } = conversation.integration; - if (kind.includes('nylas' || kind === 'gmail')) { + if (kind.includes('nylas') || kind === 'gmail') { return ( Date: Mon, 9 Mar 2020 20:01:27 +0800 Subject: [PATCH 005/110] Release 0.12.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a41d1c7116..c851cec153b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.12.1](https://github.com/erxes/erxes/compare/0.12.0...0.12.1) (2020-03-09) + + +### Bug Fixes + +* **integrations:** invalid check in conversations gmail kind ([0e8cacd](https://github.com/erxes/erxes/commit/0e8cacd)) + # [0.12.0](https://github.com/erxes/erxes/compare/0.11.2...0.12.0) (2020-01-08) diff --git a/package.json b/package.json index 7b87181b379..87cee2c9b56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "erxes", - "version": "0.12.0", + "version": "0.12.1", "description": "erxes is an AI meets open source messaging platform for sales and marketing teams.", "homepage": "https://erxes.io", "repository": "https://github.com/erxes/erxes", From f228ad70a93349544df86886eb40689505a93cc0 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Sun, 15 Mar 2020 20:36:14 +0800 Subject: [PATCH 006/110] added file field on form --- .../modules/forms/components/FieldChoices.tsx | 6 +++ ui/src/modules/forms/components/FieldForm.tsx | 5 +++ widgets/client/form/components/Field.tsx | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/ui/src/modules/forms/components/FieldChoices.tsx b/ui/src/modules/forms/components/FieldChoices.tsx index a0f7532fd9e..4858887e998 100644 --- a/ui/src/modules/forms/components/FieldChoices.tsx +++ b/ui/src/modules/forms/components/FieldChoices.tsx @@ -56,6 +56,12 @@ function FieldChoices(props: Props) { text={__('Radio button')} icon="checked" /> + { renderValidation() { const { field } = this.state; + if (field.type === 'file') { + return null; + } + const validation = e => this.onFieldChange( 'validation', @@ -166,6 +170,7 @@ class FieldForm extends React.Component { Field Label + { @@ -100,6 +102,33 @@ export default class Field extends React.Component { this.onChange(e.currentTarget.value); }; + handleFileInput = (e: React.FormEvent) => { + const files = e.currentTarget.files; + const self = this; + + if (files && files.length > 0) { + uploadHandler({ + file: files[0], + + beforeUpload() { + self.setState({ isAttachingFile: true }); + }, + + // upload to server + afterUpload({ response }: any) { + self.setState({ isAttachingFile: false }); + + self.onChange(response); + }, + + onError: message => { + alert(message); + self.setState({ isAttachingFile: false }); + } + }); + } + }; + onDateChange = (momentObj: moment.Moment) => { this.setState({ dateValue: momentObj }); this.onChange(momentObj.toDate()); @@ -169,6 +198,12 @@ export default class Field extends React.Component { this.onRadioButtonsChange ); + case "file": + return Field.renderInput({ + onChange: this.handleFileInput, + type: "file" + }); + case "textarea": return Field.renderTextarea({ onChange: this.onTextAreaChange }); @@ -182,6 +217,7 @@ export default class Field extends React.Component { render() { const { field, error } = this.props; + const { isAttachingFile } = this.state; return (
@@ -189,11 +225,16 @@ export default class Field extends React.Component { {field.text} {field.isRequired ? * : null}: + {error && error.text} + {field.description ? ( {field.description} ) : null} + {this.renderControl()} + + {isAttachingFile ?
: null}
); } From 22bb2bf272e5726379ee1203e53986b0ba50c4f1 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Mon, 16 Mar 2020 00:31:13 +0800 Subject: [PATCH 007/110] remove true mail config --- .../components/items/email/EngageEmail.tsx | 5 +++- .../engage/components/MessageListRow.tsx | 4 ++-- ui/src/modules/engage/graphql/queries.ts | 6 +++++ ui/src/modules/engage/types.ts | 2 ++ .../components/EngageSettingsContent.tsx | 24 ------------------- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/ui/src/modules/activityLogs/components/items/email/EngageEmail.tsx b/ui/src/modules/activityLogs/components/items/email/EngageEmail.tsx index e07d5fe2848..d408cbf952b 100644 --- a/ui/src/modules/activityLogs/components/items/email/EngageEmail.tsx +++ b/ui/src/modules/activityLogs/components/items/email/EngageEmail.tsx @@ -62,17 +62,20 @@ class EngageEmail extends React.Component { render() { const { createdAt } = this.props.activity; + const { email = {} as IEngageEmail, + validCustomersCount, title, fromUser, stats = { send: 0, total: 0 } } = this.props.email; + const { subject } = email; let status = ; - if (stats.total === stats.send) { + if (validCustomersCount === stats.total) { status = ; } diff --git a/ui/src/modules/engage/components/MessageListRow.tsx b/ui/src/modules/engage/components/MessageListRow.tsx index 64a6a1c9b4d..56ccfa63dfd 100755 --- a/ui/src/modules/engage/components/MessageListRow.tsx +++ b/ui/src/modules/engage/components/MessageListRow.tsx @@ -122,11 +122,11 @@ class Row extends React.Component { let status = ; const { isChecked, message, remove } = this.props; - const { stats = { send: '' }, brand = { name: '' } } = message; + const { stats = { send: '' }, brand = { name: '' }, validCustomersCount } = message; const totalCount = stats.total || 0; - if (totalCount === stats.send) { + if (validCustomersCount === totalCount) { status = ; } diff --git a/ui/src/modules/engage/graphql/queries.ts b/ui/src/modules/engage/graphql/queries.ts index 4d5ab72baac..5deba38eecb 100755 --- a/ui/src/modules/engage/graphql/queries.ts +++ b/ui/src/modules/engage/graphql/queries.ts @@ -50,6 +50,8 @@ const engageMessages = ` tagIds brandIds segmentIds + totalCustomersCount + validCustomersCount stats getTags { _id @@ -77,6 +79,10 @@ export const engageDetailFields = ` isLive stopDate createdAt + totalCustomersCount + validCustomersCount + stats + messenger fromUser { _id diff --git a/ui/src/modules/engage/types.ts b/ui/src/modules/engage/types.ts index 9580cb83311..ba4f9388d28 100644 --- a/ui/src/modules/engage/types.ts +++ b/ui/src/modules/engage/types.ts @@ -84,6 +84,8 @@ export interface IEngageMessage extends IEngageMessageDoc { fromUser: IUser; tagIds: string[]; getTags: ITag[]; + totalCustomersCount?: number; + validCustomersCount?: number; stats?: IEngageStats; logs?: Array<{ message: string }>; diff --git a/ui/src/modules/settings/general/components/EngageSettingsContent.tsx b/ui/src/modules/settings/general/components/EngageSettingsContent.tsx index a0a48b63bf8..025f387693d 100644 --- a/ui/src/modules/settings/general/components/EngageSettingsContent.tsx +++ b/ui/src/modules/settings/general/components/EngageSettingsContent.tsx @@ -170,30 +170,6 @@ class EngageSettingsContent extends React.Component { /> - - Email verification type - - - - - Truemail api key - - - {renderButton({ name: 'configsMap', From ada97e0c936f29241708a4767f8c64f0606221a9 Mon Sep 17 00:00:00 2001 From: Narmandakh Enkhtuvshin <37796969+Enkhtuvshin0513@users.noreply.github.com> Date: Mon, 16 Mar 2020 12:28:25 +0800 Subject: [PATCH 008/110] Add renderUserFullName function in utils Closes #1752 --- .../components/items/ConvertLog.tsx | 9 +++--- .../components/items/InternalNote.tsx | 3 +- .../components/items/MergedLog.tsx | 8 ++--- .../items/checklist/ChecklistItem.tsx | 5 ++- .../items/checklist/ChecklistLog.tsx | 5 ++- .../items/create/BoardItemCreate.tsx | 7 ++-- .../items/create/CustomerCreate.tsx | 8 ++--- ui/src/modules/common/components/Chooser.tsx | 32 +++++++++++-------- ui/src/modules/common/utils/index.tsx | 14 ++++++++ .../companies/containers/CompanyChooser.tsx | 10 +++--- .../companies/containers/CompanyForm.tsx | 4 +-- ui/src/modules/companies/graphql/mutations.ts | 2 ++ .../customers/containers/CustomerChooser.tsx | 17 ++++++---- .../customers/containers/CustomerForm.tsx | 6 ++-- ui/src/modules/customers/graphql/mutations.ts | 2 ++ 15 files changed, 76 insertions(+), 56 deletions(-) diff --git a/ui/src/modules/activityLogs/components/items/ConvertLog.tsx b/ui/src/modules/activityLogs/components/items/ConvertLog.tsx index 3fd4ee632b6..93129d854e9 100644 --- a/ui/src/modules/activityLogs/components/items/ConvertLog.tsx +++ b/ui/src/modules/activityLogs/components/items/ConvertLog.tsx @@ -6,6 +6,7 @@ import { } from 'modules/activityLogs/styles'; import { IActivityLog } from 'modules/activityLogs/types'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -26,13 +27,11 @@ class ConvertLog extends React.Component { let userName = 'Unknown'; if (createdByDetail && createdByDetail.type === 'user') { - if (createdByDetail.content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; - } + userName = renderUserFullName(createdByDetail.content); } const conversation = ( - + conversation ); @@ -44,7 +43,7 @@ class ConvertLog extends React.Component { }/${contentType}/board?_id=${activity._id}&itemId=${ contentTypeDetail._id }`} - target="_blank" + target='_blank' > {contentTypeDetail.name} diff --git a/ui/src/modules/activityLogs/components/items/InternalNote.tsx b/ui/src/modules/activityLogs/components/items/InternalNote.tsx index 88fb5af873e..6cdc4a20b41 100644 --- a/ui/src/modules/activityLogs/components/items/InternalNote.tsx +++ b/ui/src/modules/activityLogs/components/items/InternalNote.tsx @@ -9,6 +9,7 @@ import { } from 'modules/activityLogs/styles'; import { IUser } from 'modules/auth/types'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import Form from 'modules/internalNotes/components/Form'; import { IInternalNote } from 'modules/internalNotes/types'; import React from 'react'; @@ -43,7 +44,7 @@ class InternalNote extends React.Component { let userName = 'Unknown'; if (createdUser.details) { - userName = createdUser.details.fullName || 'Unknown'; + userName = renderUserFullName(createdUser); } return ( diff --git a/ui/src/modules/activityLogs/components/items/MergedLog.tsx b/ui/src/modules/activityLogs/components/items/MergedLog.tsx index f59927a795c..6f2a6c24a53 100644 --- a/ui/src/modules/activityLogs/components/items/MergedLog.tsx +++ b/ui/src/modules/activityLogs/components/items/MergedLog.tsx @@ -6,7 +6,7 @@ import { } from 'modules/activityLogs/styles'; import { IActivityLog } from 'modules/activityLogs/types'; import Tip from 'modules/common/components/Tip'; -import { __, renderFullName } from 'modules/common/utils'; +import { __, renderFullName, renderUserFullName } from 'modules/common/utils'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -19,9 +19,9 @@ class MergedLog extends React.Component { const { createdByDetail } = this.props.activity; if (createdByDetail) { - const { details } = createdByDetail.content; + const userName = renderUserFullName(createdByDetail.content); - return {details ? details.fullName : 'Unknown'}; + return {userName}; } return System; @@ -41,7 +41,7 @@ class MergedLog extends React.Component {   {renderFullName(contact)} diff --git a/ui/src/modules/activityLogs/components/items/checklist/ChecklistItem.tsx b/ui/src/modules/activityLogs/components/items/checklist/ChecklistItem.tsx index 3ca1824f915..e27af288563 100644 --- a/ui/src/modules/activityLogs/components/items/checklist/ChecklistItem.tsx +++ b/ui/src/modules/activityLogs/components/items/checklist/ChecklistItem.tsx @@ -7,6 +7,7 @@ import { } from 'modules/activityLogs/styles'; import { IActivityLog } from 'modules/activityLogs/types'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import React from 'react'; type Props = { @@ -27,9 +28,7 @@ class CheckListItem extends React.Component { let userName = 'Unknown'; if (createdByDetail && createdByDetail.type === 'user') { - if (createdByDetail.content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; - } + userName = renderUserFullName(createdByDetail.content); } const name = contentTypeDetail.title || content.name; diff --git a/ui/src/modules/activityLogs/components/items/checklist/ChecklistLog.tsx b/ui/src/modules/activityLogs/components/items/checklist/ChecklistLog.tsx index dade165e2f6..aa67bc1fd1e 100644 --- a/ui/src/modules/activityLogs/components/items/checklist/ChecklistLog.tsx +++ b/ui/src/modules/activityLogs/components/items/checklist/ChecklistLog.tsx @@ -8,6 +8,7 @@ import { } from 'modules/activityLogs/styles'; import { IActivityLog } from 'modules/activityLogs/types'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import React from 'react'; import CheckListItem from './ChecklistItem'; @@ -55,9 +56,7 @@ class ChecklistLog extends React.Component { let userName = 'Unknown'; if (createdByDetail && createdByDetail.type === 'user') { - if (createdByDetail.content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; - } + userName = renderUserFullName(createdByDetail.content); } const checklistName = contentTypeDetail.title || content.name; diff --git a/ui/src/modules/activityLogs/components/items/create/BoardItemCreate.tsx b/ui/src/modules/activityLogs/components/items/create/BoardItemCreate.tsx index cdd988dd853..2c47736a44a 100644 --- a/ui/src/modules/activityLogs/components/items/create/BoardItemCreate.tsx +++ b/ui/src/modules/activityLogs/components/items/create/BoardItemCreate.tsx @@ -7,6 +7,7 @@ import { import { IActivityLog } from 'modules/activityLogs/types'; import Icon from 'modules/common/components/Icon'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -22,11 +23,7 @@ class BoardItemCreate extends React.Component { let userName = 'Unknown'; if (createdByDetail && createdByDetail.type === 'user') { - const { content } = createdByDetail; - - if (content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; - } + userName = renderUserFullName(createdByDetail.content); } const body = ( diff --git a/ui/src/modules/activityLogs/components/items/create/CustomerCreate.tsx b/ui/src/modules/activityLogs/components/items/create/CustomerCreate.tsx index 6966a17b65f..a7d54067e8a 100644 --- a/ui/src/modules/activityLogs/components/items/create/CustomerCreate.tsx +++ b/ui/src/modules/activityLogs/components/items/create/CustomerCreate.tsx @@ -6,6 +6,7 @@ import { } from 'modules/activityLogs/styles'; import { IActivityLog } from 'modules/activityLogs/types'; import Tip from 'modules/common/components/Tip'; +import { renderUserFullName } from 'modules/common/utils'; import React from 'react'; type Props = { @@ -18,12 +19,7 @@ class CustomerCreate extends React.Component { const { createdByDetail } = activity; if (createdByDetail && createdByDetail.type === 'user') { - const { content } = createdByDetail; - let userName = 'Unknown'; - - if (content.details) { - userName = content.details.fullName || 'Unknown'; - } + const userName = renderUserFullName(createdByDetail.content); return ( diff --git a/ui/src/modules/common/components/Chooser.tsx b/ui/src/modules/common/components/Chooser.tsx index 8d1161ba1ea..7a1485e886a 100644 --- a/ui/src/modules/common/components/Chooser.tsx +++ b/ui/src/modules/common/components/Chooser.tsx @@ -4,6 +4,8 @@ import FormControl from 'modules/common/components/form/Control'; import Icon from 'modules/common/components/Icon'; import ModalTrigger from 'modules/common/components/ModalTrigger'; import { __ } from 'modules/common/utils'; +import { ICompany } from 'modules/companies/types'; +import { ICustomer } from 'modules/customers/types'; import React from 'react'; import { ActionTop, Column, Columns, Footer, Title } from '../styles/chooser'; import { CenterContent, ModalFooter } from '../styles/main'; @@ -19,7 +21,7 @@ export type CommonProps = { clearState: () => void; limit?: number; add?: any; - newItemId?: string; + newItem?: ICustomer | ICompany; closeModal: () => void; }; @@ -59,14 +61,18 @@ class CommonChooser extends React.Component { } componentWillReceiveProps(newProps) { - const { datas, perPage, newItemId } = newProps; + const { datas, perPage, newItem } = newProps; this.setState({ loadmore: datas.length === perPage && datas.length > 0 }); - if (newItemId) { - const items = datas.filter(item => item._id === newItemId); + if (newItem) { + const newItems = this.state.datas.filter( + data => data._id === newItem._id + ); - items.map(data => this.setState({ datas: [...this.state.datas, data] })); + if (newItems && newItems.length < 1) { + this.setState({ datas: [...this.state.datas, newItem] }); + } } } @@ -125,7 +131,7 @@ class CommonChooser extends React.Component { ); } - return ; + return ; } render() { @@ -163,10 +169,10 @@ class CommonChooser extends React.Component { {this.state.loadmore && ( @@ -188,17 +194,17 @@ class CommonChooser extends React.Component {
- diff --git a/ui/src/modules/common/utils/index.tsx b/ui/src/modules/common/utils/index.tsx index e0914e6b56f..89b3e8f0e34 100644 --- a/ui/src/modules/common/utils/index.tsx +++ b/ui/src/modules/common/utils/index.tsx @@ -34,6 +34,20 @@ export const renderFullName = data => { return 'Unknown'; }; +export const renderUserFullName = data => { + const { details } = data; + + if (details && details.fullName) { + return details.fullName; + } + + if (data.email || data.username) { + return data.email || data.username; + } + + return 'Unknown'; +}; + export const setTitle = (title: string, force: boolean) => { if (!document.title.includes(title) || force) { document.title = title; diff --git a/ui/src/modules/companies/containers/CompanyChooser.tsx b/ui/src/modules/companies/containers/CompanyChooser.tsx index ecd07485f75..2ec8f654df9 100644 --- a/ui/src/modules/companies/containers/CompanyChooser.tsx +++ b/ui/src/modules/companies/containers/CompanyChooser.tsx @@ -26,13 +26,13 @@ type FinalProps = { class CompanyChooser extends React.Component< WrapperProps & FinalProps, - { newCompanyId?: string } + { newCompany?: ICompany } > { constructor(props) { super(props); this.state = { - newCompanyId: undefined + newCompany: undefined }; } @@ -60,8 +60,8 @@ class CompanyChooser extends React.Component< return company.primaryName || company.website || 'Unknown'; }; - const getAssociatedCompany = (newCompanyId: string) => { - this.setState({ newCompanyId }); + const getAssociatedCompany = (newCompany: ICompany) => { + this.setState({ newCompany }); }; const updatedProps = { @@ -86,7 +86,7 @@ class CompanyChooser extends React.Component< ), renderName, add: addCompany, - newItemId: this.state.newCompanyId, + newItem: this.state.newCompany, datas: companiesQuery.companies || [], refetchQuery: queries.companies }; diff --git a/ui/src/modules/companies/containers/CompanyForm.tsx b/ui/src/modules/companies/containers/CompanyForm.tsx index 2d05771e893..1b7ad37b148 100644 --- a/ui/src/modules/companies/containers/CompanyForm.tsx +++ b/ui/src/modules/companies/containers/CompanyForm.tsx @@ -31,7 +31,7 @@ const CompanyFromContainer = (props: FinalProps) => { closeModal(); if (getAssociatedCompany) { - getAssociatedCompany(data.companiesAdd._id); + getAssociatedCompany(data.companiesAdd); } }; @@ -42,7 +42,7 @@ const CompanyFromContainer = (props: FinalProps) => { callback={afterSave} refetchQueries={getRefetchQueries()} isSubmitted={isSubmitted} - type="submit" + type='submit' successMessage={`You successfully ${ object ? 'updated' : 'added' } a ${name}`} diff --git a/ui/src/modules/companies/graphql/mutations.ts b/ui/src/modules/companies/graphql/mutations.ts index b148cf699fb..a4ae038379d 100755 --- a/ui/src/modules/companies/graphql/mutations.ts +++ b/ui/src/modules/companies/graphql/mutations.ts @@ -46,6 +46,8 @@ const companiesAdd = ` mutation companiesAdd(${commonFields}) { companiesAdd(${commonVariables}) { _id + primaryName + primaryEmail } } `; diff --git a/ui/src/modules/customers/containers/CustomerChooser.tsx b/ui/src/modules/customers/containers/CustomerChooser.tsx index d33ae30b105..82fdf59a543 100644 --- a/ui/src/modules/customers/containers/CustomerChooser.tsx +++ b/ui/src/modules/customers/containers/CustomerChooser.tsx @@ -25,13 +25,13 @@ type FinalProps = { AddMutationResponse; class CustomerChooser extends React.Component< WrapperProps & FinalProps, - { newCustomerId?: string } + { newCustomer?: ICustomer } > { constructor(props) { super(props); this.state = { - newCustomerId: undefined + newCustomer: undefined }; } @@ -55,8 +55,8 @@ class CustomerChooser extends React.Component< }); }; - const getAssociatedCustomer = (newCustomerId: string) => { - this.setState({ newCustomerId }); + const getAssociatedCustomer = (newCustomer: ICustomer) => { + this.setState({ newCustomer }); }; const updatedProps = { @@ -80,7 +80,7 @@ class CustomerChooser extends React.Component< /> ), add: addCustomer, - newItemId: this.state.newCustomerId, + newItem: this.state.newCustomer, datas: customersQuery.customers || [], refetchQuery: queries.customers }; @@ -116,7 +116,12 @@ const WithQuery = withProps( graphql( gql(mutations.customersAdd), { - name: 'customersAdd' + name: 'customersAdd', + options: () => { + return { + refetchQueries: ['customersMain', 'customers', 'customerCounts'] + }; + } } ) )(CustomerChooser) diff --git a/ui/src/modules/customers/containers/CustomerForm.tsx b/ui/src/modules/customers/containers/CustomerForm.tsx index f827459ab1d..082459cf808 100644 --- a/ui/src/modules/customers/containers/CustomerForm.tsx +++ b/ui/src/modules/customers/containers/CustomerForm.tsx @@ -60,7 +60,7 @@ class CustomerFormContainer extends React.Component { }`; if (getAssociatedCustomer) { - getAssociatedCustomer(data.customersAdd._id); + getAssociatedCustomer(data.customersAdd); } if (redirectType === 'new') { @@ -78,8 +78,8 @@ class CustomerFormContainer extends React.Component { isSubmitted={isSubmitted} disableLoading={redirectType ? true : false} disabled={isSubmitted} - type="submit" - icon="user-check" + type='submit' + icon='user-check' resetSubmit={resetSubmit} successMessage={`You successfully ${ object ? 'updated' : 'added' diff --git a/ui/src/modules/customers/graphql/mutations.ts b/ui/src/modules/customers/graphql/mutations.ts index 4c2dcd6537d..3861ab6f362 100755 --- a/ui/src/modules/customers/graphql/mutations.ts +++ b/ui/src/modules/customers/graphql/mutations.ts @@ -48,6 +48,8 @@ const customersAdd = ` mutation customersAdd(${commonFields}) { customersAdd(${commonVariables}) { _id + firstName + primaryEmail } } `; From d61b15243d9711bdc559bcb9ef08d8880ab41917 Mon Sep 17 00:00:00 2001 From: Narmandakh Enkhtuvshin <37796969+Enkhtuvshin0513@users.noreply.github.com> Date: Mon, 16 Mar 2020 12:29:53 +0800 Subject: [PATCH 009/110] Auto assigne Closes #1750 From 13eeb4c4f06a1aeac5ef058a817082f2de533ca5 Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Mon, 16 Mar 2020 13:55:11 +0800 Subject: [PATCH 010/110] Fix doc sidebar (#1749) --- docs/docs/overview/getting-started.md | 17 +- docs/docs/user/email-templates.md | 78 ------- docs/docs/user/general-settings.md | 213 ++++++++++++++++++ docs/docs/user/popups.md | 42 ++-- docs/docs/user/sales-pipeline-settings.md | 93 -------- docs/docs/user/signing-in.md | 42 ---- .../docs/user/subscription-getting-started.md | 39 +++- docs/docs/user/third-party-integrations.md | 51 ----- docs/website/sidebars.json | 10 +- 9 files changed, 282 insertions(+), 303 deletions(-) delete mode 100644 docs/docs/user/email-templates.md delete mode 100644 docs/docs/user/sales-pipeline-settings.md delete mode 100644 docs/docs/user/signing-in.md delete mode 100644 docs/docs/user/third-party-integrations.md diff --git a/docs/docs/overview/getting-started.md b/docs/docs/overview/getting-started.md index 93b1844dd6d..a7b4b4a6693 100644 --- a/docs/docs/overview/getting-started.md +++ b/docs/docs/overview/getting-started.md @@ -20,20 +20,19 @@ title: Getting Started - Subscription getting started - Initial setup - General settings -- Signing in +- Team Inbox +- Knowledge Base +- Popups +- Script installation - Contacts -- Deal +- Sales pipeline - Engage - Insights -- Knowledge base -- Leads -- Marketing -- Mobile Apps +- Profile settings - Notifications -- Sales -- Support +- Mobile Apps
-
+
### Installation Guide diff --git a/docs/docs/user/email-templates.md b/docs/docs/user/email-templates.md deleted file mode 100644 index a3aa6a8b610..00000000000 --- a/docs/docs/user/email-templates.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: email-templates -title: Email templates ---- - ---- - -## How to setup Marketing Team Settings - -Marketing Team settings include following features: - - -
- -
- -1. __Email Template__ -2. __Email Appearance__ -3. __Script Manager__ - - -### Setup Email Template - - -Team members will be able to choose from email templates and send out one message to multiple recipients. You can use the email templates to send out a Mass email for leads/customers or you can send to other team members. - -+ Please follow the steps for setup: __General Settings__ -> __Email Template__ -> __New Email Template__ - -
- -
- -1. Click __New Email Template__ -2. __Edit & Delete__ Created Email Template - - -### Add Email Template - -+ Please follow the steps for setup: __General Settings__ -> __Email Template__ -> __New Email Template__ - -
- -
. - -1. Insert __name__ for New Email Template -2. Insert __HTML__ of the email template -3. __Click Save__ - - -*Created New Email Template will be used in further Engage feature to send mass email to customers* - - ### Appearance of Created Email Template - -+ Please follow the steps for setup: __General Settings__ -> __Email Template__ - -
- - -
- - -*Created New Email Template will be used in further Engage feature to send mass email to customers* - ---- - -## Email Appearance - -
- - -
- -1. Choose your __email template type__ -2. __Insert__ email template HTML -3. __Click Save__ - - -*The email template must be in HTML format* \ No newline at end of file diff --git a/docs/docs/user/general-settings.md b/docs/docs/user/general-settings.md index 059c310ffda..5b1a651b6c2 100644 --- a/docs/docs/user/general-settings.md +++ b/docs/docs/user/general-settings.md @@ -239,3 +239,216 @@ You can specify the user's actions through this permission feature 1. Fill the permission group name 2. Description of the group + +# How to setup third party integrations ? + +## Setup facebook integration + +Integration is a way of communicating with customers who are emerging into the organizations' website, Facebook, Twitter through erxes.io platform. Integration can be created on every brands' social media page. Thence, we would be able to track the percentage of customers who are emerging into the organizations' Facebook page and website form. + ++ Please follow the steps for setup: General Settings->App Store > Add Facebook +
+ + +
+ +1. Click Add to connect Facebook +2. Click Add Account + +
+ +
+ +
+ +3. Link your Facebook Account +4. Select all the Pages you manage +5. Click Next + + + + +--- + +## Link facebook account + +
+ +
+ +6. Insert name for the Facebook Integration +7. Choose the Brand for the Integration +8. Select your Linked Account +9. Select the Social Pages to link +10. Click Save to link the account + + + + +# Email templates + +--- + +## How to setup Marketing Team Settings + +Marketing Team settings include following features: + + +
+ +
+ +1. __Email Template__ +2. __Email Appearance__ + + +### Setup Email Template + + +Team members will be able to choose from email templates and send out one message to multiple recipients. You can use the email templates to send out a Mass email for leads/customers or you can send to other team members. + ++ Please follow the steps for setup: __General Settings__ -> __Email Template__ -> __New Email Template__ + +
+ +
+ +1. Click __New Email Template__ +2. __Edit & Delete__ Created Email Template + + +### Add Email Template + ++ Please follow the steps for setup: __General Settings__ -> __Email Template__ -> __New Email Template__ + +
+ +
. + +1. Insert __name__ for New Email Template +2. Insert __HTML__ of the email template +3. __Click Save__ + + +*Created New Email Template will be used in further Engage feature to send mass email to customers* + + ### Appearance of Created Email Template + ++ Please follow the steps for setup: __General Settings__ -> __Email Template__ + +
+ + +
+ + +*Created New Email Template will be used in further Engage feature to send mass email to customers* + +--- + +## Email Appearance + +
+ + +
+ +1. Choose your __email template type__ +2. __Insert__ email template HTML +3. __Click Save__ + + +*The email template must be in HTML format* + +# Sales Team settings + +Sales Team settings include following features: + +
+ +
+ +1. **Board & Pipelines** +2. **Product & Service** + +--- + +## Setup Board & Pipeline + +Manage your boards and pipelines so that it's easy to manage incoming leads or requests that is adaptable to your team's needs. Add in or delete boards and pipelines to keep business development on track and in check. + +- Please follow the steps for setup: **General Settings -> Board & Pipeline> Add Board -> Add Pipeline** + +
+ +
+ +1. Click **Add** button to create **Board** +2. Click **Add** button to create **Pipeline** + + + +--- + +### Add Board + +- Please follow the steps for setup: **General Settings -> Board & Pipeline> Add Board** + +
+ +
+ +1. Click **Add** to create Board +2. **Insert name** for the **Board** +3. Click **Save** + + + +--- + +### Add Pipeline + +- Please follow the steps for setup: **General Settings -> Board & Pipeline> Add Board** + +
+ +
+ +1. Click **Add Pipeline** +2. Insert **Name** for Pipeline +3. Click **Add Stage** for the Pipeline +4. Insert **Name** for the Stage +5. Choose **Percentage** for Stage +6. Click to **Delete** the Stage +7. Click **Save** + +--- + +## Setup Product & Service + +All information and know-how related to your business's products and services are found here. Create and add in unlimited products and services so that you and your team members can edit and share. + +- Please follow the steps for setup: **General Settings -> Product & Service -> Add Product & Service** + +
+ +
+ +1. Click **Add Product & Service** +2. Insert **Name** for the Product/Service +3. Select **Product/Service type** +4. Insert **Description** for the Product/Service +5. Insert **Stock Keeping Unit** /SKU/ +6. Click **Save** + + diff --git a/docs/docs/user/popups.md b/docs/docs/user/popups.md index a279e5bbd29..9809c16b498 100644 --- a/docs/docs/user/popups.md +++ b/docs/docs/user/popups.md @@ -1,42 +1,42 @@ --- id: popups -title: Popups +title: Pop ups sidebar_label: Popups --- -## Convert visitors into qualified leads +## Convert visitors into qualified pop ups -Turn regular visitors into qualified leads by capturing them with a customizable landing page, forms, pop-up or embed placements. +Turn regular visitors into qualified pop ups by capturing them with a customizable landing page, forms, pop-up or embed placements. --- -Turn regular visitors into qualified leads by capturing them with a customizable landing page, forms, pop-up or embed placements. Filter your leads by tags +Turn regular visitors into qualified pop ups by capturing them with a customizable landing page, forms, pop-up or embed placements. Filter your pop ups by tags -- Please follow the next steps for setup: **Leads** +- Please follow the next steps for setup: **Pop ups** -1. **Filter** your leads by **tags** -2. List of **Created leads** -3. Click **Create lead** +1. **Filter** your pop ups by **tags** +2. List of **Created pop ups** +3. Click **Create pop up** --- -## Create Leads +## Create Pop ups -- Please follow the steps for setup: **Leads->Create Leads->Type** +- Please follow the steps for setup: **Pop ups->Create Pop ups->Type** -1. Insert **Title** for the Leads +1. Insert **Title** for the Pop ups 2. Choose the **Type** 3. Click **Next** @@ -44,11 +44,11 @@ Turn regular visitors into qualified leads by capturing them with a customizable -- Please follow the next steps for setup: **Leads->Create Leads->Type->Callout** +- Please follow the next steps for setup: **Pop ups->Create Pop ups->Type->Callout** 1. **CallOut section** @@ -64,7 +64,7 @@ Turn regular visitors into qualified leads by capturing them with a customizable -- Please follow the steps for setup: **Leads->Create Leads->Type->Callout->Form** +- Please follow the steps for setup: **Pop ups->Create Pop ups->Type->Callout->Form** @@ -83,7 +83,7 @@ Turn regular visitors into qualified leads by capturing them with a customizable 13. Click on the form section to change the order @@ -92,7 +92,7 @@ Turn regular visitors into qualified leads by capturing them with a customizable -- Please follow the next steps for setup: **Leads->Create Leads->Type->Callout->Form->Options** +- Please follow the next steps for setup: **Pop ups->Create Pop ups->Type->Callout->Form->Options** @@ -106,7 +106,7 @@ Turn regular visitors into qualified leads by capturing them with a customizable -- Please follow the next steps for setup: **Leads->Create Leads->Type->Callout->Form->Options->Thank** +- Please follow the next steps for setup: **Pop ups->Create Pop ups->Type->Callout->Form->Options->Thank** @@ -119,9 +119,9 @@ Turn regular visitors into qualified leads by capturing them with a customizable -## Lead Full Preview +## Pop up Full Preview -- Please follow the next steps for setup: **Leads->Create Leads->Type->Callout->Form->Options->Thank Content-> Full Preview** +- Please follow the next steps for setup: **Pop ups->Create Pop ups->Type->Callout->Form->Options->Thank Content-> Full Preview**
@@ -131,7 +131,7 @@ Turn regular visitors into qualified leads by capturing them with a customizable 6. Click **Purchase** 7. Insert **Cardholder’s information** 8. Click **Pay** + +--- + + +## Sign out + + +
+ + + +1. You can sign out from the Main Menu, which is accessed by clicking the __profile image__ in the top header on the right side of the screen. +2. Clicking __Sign out__ logs you out of all teams on the server. diff --git a/docs/docs/user/third-party-integrations.md b/docs/docs/user/third-party-integrations.md deleted file mode 100644 index 14bc3b9dc20..00000000000 --- a/docs/docs/user/third-party-integrations.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: third-party-integrations -title: How to setup third party integrations ? -sidebar_label: Third party integrations ---- - -## Setup facebook integration - -Integration is a way of communicating with customers who are emerging into the organizations' website, Facebook, Twitter through erxes.io platform. Integration can be created on every brands' social media page. Thence, we would be able to track the percentage of customers who are emerging into the organizations' Facebook page and website form. - -+ Please follow the steps for setup: General Settings->App Store > Add Facebook -
- - -
- -1. Click Add to connect Facebook -2. Click Add Account - -
- -
- -
- -3. Link your Facebook Account -4. Select all the Pages you manage -5. Click Next - - - - ---- - -## Link facebook account - -
- -
- -6. Insert name for the Facebook Integration -7. Choose the Brand for the Integration -8. Select your Linked Account -9. Select the Social Pages to link -10. Click Save to link the account - - diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index 07bf77ceb15..bac1cd79636 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -17,6 +17,7 @@ "installation/upgrade" ], "User's Guide": [ + "user/subscription-getting-started", "user/initial-setup", "user/general-settings", "user/team-inbox", @@ -25,16 +26,11 @@ "user/script-install", "user/contacts", "user/sales-pipeline", - "user/sales-pipeline-settings", "user/engage", "user/insights", - "user/email-templates", - "user/third-party-integrations", - "user/signing-in", "user/profile-settings", "user/notification", - "user/mobile-apps", - "user/subscription-getting-started" + "user/mobile-apps" ], "Administrator's guide": [ "administrator/creating-first-user", @@ -52,4 +48,4 @@ "developer/ios-sdk" ] } -} +} \ No newline at end of file From 1e4c4b829972a7469c1b605e223569562d97f3c2 Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Mon, 16 Mar 2020 14:15:24 +0800 Subject: [PATCH 011/110] perf(inbox): add inbox assign loader (close #1754) --- .../components/filterableList/FilterableList.tsx | 8 +++++++- .../modules/inbox/components/assignBox/AssignBox.tsx | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/src/modules/common/components/filterableList/FilterableList.tsx b/ui/src/modules/common/components/filterableList/FilterableList.tsx index a79b48fb8c3..eb77c808cf1 100755 --- a/ui/src/modules/common/components/filterableList/FilterableList.tsx +++ b/ui/src/modules/common/components/filterableList/FilterableList.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import EmptyState from '../EmptyState'; import Icon from '../Icon'; +import Spinner from '../Spinner'; import Filter from './Filter'; import { AvatarImg, @@ -18,6 +19,7 @@ type Props = { links?: any[]; showCheckmark?: boolean; selectable?: boolean; + loading?: boolean; className?: string; // hooks @@ -80,9 +82,13 @@ class FilterableList extends React.Component { }; renderItems() { - const { showCheckmark = true } = this.props; + const { showCheckmark = true, loading } = this.props; const { items, key } = this.state; + if (loading) { + return + } + if (items.length === 0) { return ( { @@ -38,7 +39,8 @@ class AssignBox extends React.Component { super(props); this.state = { - assigneesForList: [] + assigneesForList: [], + loading: true }; } @@ -59,11 +61,12 @@ class AssignBox extends React.Component { requireUsername: true } }) - .then(({ data }: { data: { users?: IUser[] } }) => { + .then((response: { loading: boolean, data: { users?: IUser[] } }) => { const verifiedUsers = - (data.users || []).filter(user => user.username) || []; + (response.data.users || []).filter(user => user.username) || []; this.setState({ + loading: response.loading, assigneesForList: this.generateAssignParams( verifiedUsers, this.props.targets @@ -157,6 +160,7 @@ class AssignBox extends React.Component { className, links, selectable: true, + loading: this.state.loading, items: this.state.assigneesForList, onSearch: this.fetchUsers }; From 26e1a9b47389c7761711d1b1860cf3b83a27cf62 Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Mon, 16 Mar 2020 15:25:15 +0800 Subject: [PATCH 012/110] perf(onboard): fix onboard youtube url --- ui/src/modules/robot/constants.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/modules/robot/constants.ts b/ui/src/modules/robot/constants.ts index 41d7701e29e..b7e5b851fa9 100644 --- a/ui/src/modules/robot/constants.ts +++ b/ui/src/modules/robot/constants.ts @@ -58,7 +58,7 @@ export const FEATURE_DETAILS = { icon: 'piggy-bank', description: 'Control your sales pipeline from one responsive field by precisely analyzing your progress and determining your next best move for success.', - videoUrl: 'https://www.youtube.com/watch?v=jEkxpLdOMvU&feature=emb_title', + videoUrl: 'https://www.youtube.com/embed/jEkxpLdOMvU?autoplay=1', videoThumb: 'https://img.youtube.com/vi/jEkxpLdOMvU/mqdefault.jpg', settingsDetails: { dealBoardsCreate: { @@ -131,7 +131,7 @@ export const FEATURE_DETAILS = { icon: 'clipboard', description: "Organize your own tasks or your team's sprints effectively with erxes Task. It involves planning, testing, tracking, and reporting.", - videoUrl: 'https://www.youtube.com/watch?v=WgMSf_aETdI&feature=emb_title', + videoUrl: 'https://www.youtube.com/embed/WgMSf_aETdI?autoplay=1', videoThumb: 'https://img.youtube.com/vi/WgMSf_aETdI/mqdefault.jpg', settingsDetails: { taskBoardsCreate: { @@ -196,7 +196,7 @@ export const FEATURE_DETAILS = { color: colors.colorCoreBlue, description: 'Access our all-in-one CRM & product, and service system in one go so that it’s easier to coordinate and manage your interactions with your customers.', - videoUrl: 'https://www.youtube.com/watch?v=Axazk8K30Qk&feature=emb_title', + videoUrl: 'https://www.youtube.com/embed/Axazk8K30Qk?autoplay=1', videoThumb: 'https://img.youtube.com/vi/Axazk8K30Qk/mqdefault.jpg', settingsDetails: { customerCreate: { From e16a1eaae9c24c877b2a08d870849dbc54206e67 Mon Sep 17 00:00:00 2001 From: munkhjin Date: Mon, 16 Mar 2020 17:41:32 +0800 Subject: [PATCH 013/110] perf(deal/task/ticket/growthHack) add some reactive actions Closes #1758 --- .../activityLogs/containers/ActivityLogs.tsx | 10 +- .../boards/components/ArchivedItems.tsx | 2 +- .../boards/components/editForm/Left.tsx | 205 +++++++------- .../boards/components/editForm/Top.tsx | 98 ++++--- .../boards/components/stage/ItemList.tsx | 4 +- ui/src/modules/boards/containers/Pipeline.tsx | 2 +- .../boards/containers/PipelineContext.tsx | 11 +- .../boards/containers/editForm/EditForm.tsx | 18 ++ .../boards/containers/withPipeline.tsx | 39 ++- ui/src/modules/boards/graphql/index.ts | 3 +- .../modules/boards/graphql/subscriptions.ts | 11 + ui/src/modules/boards/types.ts | 15 + ui/src/modules/boards/utils.tsx | 4 + ui/src/modules/checklists/components/Item.tsx | 169 ++++++------ ui/src/modules/checklists/components/List.tsx | 261 ++++++++---------- .../checklists/containers/Checklists.tsx | 28 +- ui/src/modules/checklists/containers/List.tsx | 108 ++++---- ui/src/modules/checklists/graphql/index.ts | 3 +- .../checklists/graphql/subscriptions.ts | 20 ++ ui/src/modules/checklists/types.ts | 1 + ui/src/modules/common/components/Button.tsx | 24 +- ui/src/modules/common/components/Uploader.tsx | 107 +++---- ui/src/modules/common/utils/animations.ts | 2 +- ui/src/modules/conformity/types.ts | 1 + ui/src/modules/deals/components/DealBoard.tsx | 2 +- .../modules/deals/components/DealEditForm.tsx | 170 +++++------- .../deals/components/ProductSection.tsx | 11 +- .../deals/components/calendar/DealColumn.tsx | 4 +- .../deals/components/product/ProductForm.tsx | 21 +- .../deals/components/product/ProductItem.tsx | 41 +-- .../deals/components/product/ProductRow.tsx | 141 ++++++---- .../deals/containers/product/ProductForm.tsx | 7 +- ui/src/modules/deals/graphql/index.ts | 3 +- ui/src/modules/deals/graphql/subscriptions.ts | 11 + ui/src/modules/deals/options.ts | 10 +- ui/src/modules/deals/styles.ts | 8 +- ui/src/modules/deals/types.ts | 2 +- .../engage/components/MessageListRow.tsx | 6 +- .../modules/forms/components/FieldChoices.tsx | 7 +- .../growthHacks/components/editForm/Top.tsx | 104 ++++--- ui/src/modules/growthHacks/graphql/index.ts | 3 +- .../growthHacks/graphql/subscriptions.ts | 11 + ui/src/modules/growthHacks/options.ts | 10 +- .../conversationDetail/workarea/Editor.tsx | 11 +- .../workarea/TemplateList.tsx | 5 +- .../modules/internalNotes/components/Form.tsx | 2 +- ui/src/modules/internalNotes/types.ts | 1 + ui/src/modules/layout/styles.ts | 5 +- ui/src/modules/tasks/graphql/index.ts | 3 +- ui/src/modules/tasks/graphql/subscriptions.ts | 11 + ui/src/modules/tasks/options.ts | 10 +- .../tickets/components/TicketEditForm.tsx | 86 +++--- ui/src/modules/tickets/graphql/index.ts | 3 +- .../modules/tickets/graphql/subscriptions.ts | 11 + ui/src/modules/tickets/options.ts | 10 +- ui/src/modules/tickets/types.ts | 2 +- 56 files changed, 1048 insertions(+), 830 deletions(-) create mode 100644 ui/src/modules/boards/graphql/subscriptions.ts create mode 100644 ui/src/modules/checklists/graphql/subscriptions.ts create mode 100644 ui/src/modules/deals/graphql/subscriptions.ts create mode 100644 ui/src/modules/growthHacks/graphql/subscriptions.ts create mode 100644 ui/src/modules/tasks/graphql/subscriptions.ts create mode 100644 ui/src/modules/tickets/graphql/subscriptions.ts diff --git a/ui/src/modules/activityLogs/containers/ActivityLogs.tsx b/ui/src/modules/activityLogs/containers/ActivityLogs.tsx index 0b6d8244a8e..e10fe61eba2 100644 --- a/ui/src/modules/activityLogs/containers/ActivityLogs.tsx +++ b/ui/src/modules/activityLogs/containers/ActivityLogs.tsx @@ -21,10 +21,12 @@ type FinalProps = { } & WithDataProps; class Container extends React.Component { - componentWillMount() { + private unsubscribe; + + componentDidMount() { const { activityLogQuery } = this.props; - activityLogQuery.subscribeToMore({ + this.unsubscribe = activityLogQuery.subscribeToMore({ document: gql(subscriptions.activityLogsChanged), updateQuery: () => { this.props.activityLogQuery.refetch(); @@ -32,6 +34,10 @@ class Container extends React.Component { }); } + componentWillUnmount() { + this.unsubscribe(); + } + render() { const { target, diff --git a/ui/src/modules/boards/components/ArchivedItems.tsx b/ui/src/modules/boards/components/ArchivedItems.tsx index 7bcee953e8f..0f8111dbf4f 100644 --- a/ui/src/modules/boards/components/ArchivedItems.tsx +++ b/ui/src/modules/boards/components/ArchivedItems.tsx @@ -101,7 +101,7 @@ class ArchivedItems extends React.Component { return ( ); } diff --git a/ui/src/modules/boards/components/editForm/Left.tsx b/ui/src/modules/boards/components/editForm/Left.tsx index cebb7a179b9..7658af03304 100644 --- a/ui/src/modules/boards/components/editForm/Left.tsx +++ b/ui/src/modules/boards/components/editForm/Left.tsx @@ -1,6 +1,6 @@ import ActivityInputs from 'modules/activityLogs/components/ActivityInputs'; import ActivityLogs from 'modules/activityLogs/containers/ActivityLogs'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { IItem, IItemParams, IOptions } from 'modules/boards/types'; import Checklists from 'modules/checklists/containers/Checklists'; @@ -26,111 +26,118 @@ type Props = { sendToBoard?: (item: any) => void; }; -class Left extends React.Component { - render() { - const { - item, - saveItem, - options, - copyItem, - removeItem, - onUpdate, - addItem, - sendToBoard - } = this.props; - - const descriptionOnBlur = e => { - const description = e.target.value; - - if (item.description !== description) { - saveItem({ description: e.target.value }); - } - }; - - const onChangeAttachment = (files: IAttachment[]) => - saveItem({ attachments: files }); - - const attachments = - (item.attachments && extractAttachment(item.attachments)) || []; - - return ( - - - - {item.labels.length > 0 && ( - - - - - {__('Labels')} - - - - - - )} - - - - - - {__('Attachments')} - - - - - - +const Left = (props: Props) => { + const { + item, + saveItem, + options, + copyItem, + removeItem, + onUpdate, + addItem, + sendToBoard + } = props; + + const [description, setDescription] = useState(item.description); + + useEffect( + () => { + setDescription(item.description); + }, + [item.description] + ); + + const onBlurDescription = () => { + if (description !== item.description) { + saveItem({ description }); + } + }; + + const onChangeDescription = e => { + setDescription(e.target.value); + }; + + const onChangeAttachment = (files: IAttachment[]) => + saveItem({ attachments: files }); + + const attachments = + (item.attachments && extractAttachment(item.attachments)) || []; + + return ( + + + + {item.labels.length > 0 && ( - - {__('Description')} + + {__('Labels')} - + - - + + + + {__('Attachments')} + + + + + + + + + + + {__('Description')} + + + + - - - - - - ); - } -} + + + + + + + + + ); +}; export default Left; diff --git a/ui/src/modules/boards/components/editForm/Top.tsx b/ui/src/modules/boards/components/editForm/Top.tsx index 65cfb1dabb4..9352ef8143e 100644 --- a/ui/src/modules/boards/components/editForm/Top.tsx +++ b/ui/src/modules/boards/components/editForm/Top.tsx @@ -1,7 +1,7 @@ import { HeaderContent, HeaderRow, TitleRow } from 'modules/boards/styles/item'; import FormControl from 'modules/common/components/form/Control'; import Icon from 'modules/common/components/Icon'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Move from '../../containers/editForm/Move'; import { IItem, IOptions } from '../../types'; import CloseDate from './CloseDate'; @@ -15,9 +15,20 @@ type Props = { amount?: () => React.ReactNode; }; -class Top extends React.Component { - renderMove() { - const { item, stageId, options, onChangeStage } = this.props; +function Top(props: Props) { + const { item } = props; + + const [name, setName] = useState(item.name); + + useEffect( + () => { + setName(item.name); + }, + [item.name] + ); + + function renderMove() { + const { stageId, options, onChangeStage } = props; return ( { ); } - render() { - const { saveItem, amount, item } = this.props; + const { saveItem, amount } = props; - const onNameBlur = e => { - const name = e.target.value; + const onNameBlur = () => { + if (item.name !== name) { + saveItem({ name }); + } + }; - if (item.name !== name) { - saveItem({ name: e.target.value }); - } - }; + const onCloseDateFieldsChange = (key: string, value: any) => { + saveItem({ [key]: value }); + }; - const onCloseDateFieldsChange = (name: string, value: any) => { - saveItem({ [name]: value }); - }; + const onChangeName = e => { + setName(e.target.value); + }; - return ( - - - - - - - - + return ( + + + + + + + + - {amount && amount()} - + {amount && amount()} + - - {this.renderMove()} + + {renderMove()} - - - - ); - } + + + + ); } export default Top; diff --git a/ui/src/modules/boards/components/stage/ItemList.tsx b/ui/src/modules/boards/components/stage/ItemList.tsx index 2ccacaf6ae0..937fe85fdf8 100644 --- a/ui/src/modules/boards/components/stage/ItemList.tsx +++ b/ui/src/modules/boards/components/stage/ItemList.tsx @@ -57,7 +57,7 @@ class DraggableContainer extends React.Component< const { item } = this.props; this.setState({ isDragDisabled: true }, () => { - routerUtils.setParams(history, { itemId: item._id }); + routerUtils.setParams(history, { itemId: item._id, key: '' }); }); if (!this.state.hasNotified) { @@ -99,7 +99,7 @@ class DraggableContainer extends React.Component< return ( diff --git a/ui/src/modules/boards/containers/Pipeline.tsx b/ui/src/modules/boards/containers/Pipeline.tsx index 8d4cf2cd8a2..ea6a614fc01 100644 --- a/ui/src/modules/boards/containers/Pipeline.tsx +++ b/ui/src/modules/boards/containers/Pipeline.tsx @@ -44,7 +44,7 @@ class WithStages extends React.Component { queryParamsChanged = (queryParams, nextProps: Props) => { const nextQueryParams = nextProps.queryParams; - if (nextQueryParams.itemId || queryParams.itemId) { + if (nextQueryParams.itemId || (!queryParams.key && queryParams.itemId)) { return false; } diff --git a/ui/src/modules/boards/containers/PipelineContext.tsx b/ui/src/modules/boards/containers/PipelineContext.tsx index 7ae40c72af8..55a1d90b607 100644 --- a/ui/src/modules/boards/containers/PipelineContext.tsx +++ b/ui/src/modules/boards/containers/PipelineContext.tsx @@ -9,6 +9,7 @@ import { IFilterParams, IItem, IItemMap, + INonFilterParams, IOptions, IPipeline } from '../types'; @@ -19,7 +20,7 @@ type Props = { pipeline: IPipeline; initialItemMap?: IItemMap; options: IOptions; - queryParams: IFilterParams; + queryParams: IFilterParams & INonFilterParams; queryParamsChanged: (queryParams: IFilterParams, args: any) => boolean; }; @@ -163,8 +164,12 @@ export class PipelineProvider extends React.Component { destination }); + // to avoid to refetch current tab + sessionStorage.setItem('currentTab', 'true'); + // update item to database - this.itemChange(result.draggableId, destination.droppableId); + const itemId = result.draggableId.split('-')[0]; + this.itemChange(itemId, destination.droppableId); this.setState({ itemMap @@ -172,6 +177,8 @@ export class PipelineProvider extends React.Component { invalidateCache(); + // Save data to sessionStorage + // save orders to database return this.saveItemOrders(itemMap, [ source.droppableId, diff --git a/ui/src/modules/boards/containers/editForm/EditForm.tsx b/ui/src/modules/boards/containers/editForm/EditForm.tsx index 6266249b3a3..839fd459a7b 100644 --- a/ui/src/modules/boards/containers/editForm/EditForm.tsx +++ b/ui/src/modules/boards/containers/editForm/EditForm.tsx @@ -50,6 +50,8 @@ type FinalProps = { } & ContainerProps; class EditFormContainer extends React.Component { + private unsubcribe; + constructor(props) { super(props); @@ -59,6 +61,22 @@ class EditFormContainer extends React.Component { this.copyItem = this.copyItem.bind(this); } + componentDidUpdate() { + const { detailQuery, itemId, options } = this.props; + + this.unsubcribe = detailQuery.subscribeToMore({ + document: gql(options.subscriptions.changeSubscription), + variables: { _id: itemId }, + updateQuery: () => { + this.props.detailQuery.refetch(); + } + }); + } + + componentWillUnmount() { + this.unsubcribe(); + } + addItem(doc: IItemParams, callback: () => void) { const { onAdd, addMutation, stageId, options } = this.props; diff --git a/ui/src/modules/boards/containers/withPipeline.tsx b/ui/src/modules/boards/containers/withPipeline.tsx index 763a136f2ed..2c0ca5522b0 100644 --- a/ui/src/modules/boards/containers/withPipeline.tsx +++ b/ui/src/modules/boards/containers/withPipeline.tsx @@ -1,24 +1,49 @@ import gql from 'graphql-tag'; import * as compose from 'lodash.flowright'; import EmptyState from 'modules/common/components/EmptyState'; -import { withProps } from 'modules/common/utils'; -import React from 'react'; +import { IRouterProps } from 'modules/common/types'; +import { router as routerUtils, withProps } from 'modules/common/utils'; +import React, { useEffect } from 'react'; import { graphql } from 'react-apollo'; -import { queries } from '../graphql'; +import { withRouter } from 'react-router'; +import { queries, subscriptions } from '../graphql'; import { IOptions, PipelineDetailQueryResponse } from '../types'; type Props = { queryParams: any; - options?: IOptions; + options: IOptions; }; type ContainerProps = { pipelineDetailQuery: PipelineDetailQueryResponse; -} & Props; +} & IRouterProps & + Props; const withPipeline = Component => { const Container = (props: ContainerProps) => { - const { pipelineDetailQuery } = props; + const { pipelineDetailQuery, history, queryParams } = props; + + useEffect(() => { + const pipelineId = queryParams.pipelineId; + + return ( + pipelineDetailQuery && + pipelineDetailQuery.subscribeToMore({ + document: gql(subscriptions.pipelinesChanged), + variables: { _id: pipelineId }, + updateQuery: () => { + const currentTab = sessionStorage.getItem('currentTab'); + + // don't reload current tab + if (!currentTab) { + routerUtils.setParams(history, { key: Math.random() }); + } + + sessionStorage.removeItem('currentTab'); + } + }) + ); + }); const pipeline = pipelineDetailQuery && pipelineDetailQuery.pipelineDetail; @@ -53,7 +78,7 @@ const withPipeline = Component => { }) } ) - )(Container) + )(withRouter(Container)) ); }; diff --git a/ui/src/modules/boards/graphql/index.ts b/ui/src/modules/boards/graphql/index.ts index 8cff0604ab6..ee7aa2506c2 100644 --- a/ui/src/modules/boards/graphql/index.ts +++ b/ui/src/modules/boards/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { queries, mutations }; +export { queries, mutations, subscriptions }; diff --git a/ui/src/modules/boards/graphql/subscriptions.ts b/ui/src/modules/boards/graphql/subscriptions.ts new file mode 100644 index 00000000000..acbac0df7bc --- /dev/null +++ b/ui/src/modules/boards/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +const pipelinesChanged = ` + subscription pipelinesChanged($_id: String!) { + pipelinesChanged(_id: $_id) { + _id + } + } +`; + +export default { + pipelinesChanged +}; diff --git a/ui/src/modules/boards/types.ts b/ui/src/modules/boards/types.ts index 2c8cde3e052..d9668e28a03 100644 --- a/ui/src/modules/boards/types.ts +++ b/ui/src/modules/boards/types.ts @@ -25,6 +25,9 @@ export interface IOptions { copyMutation: string; archiveMutation: string; }; + subscriptionName: { + changeSubscription: string; + }; queries: { itemsQuery: string; detailQuery: string; @@ -41,6 +44,9 @@ export interface IOptions { archiveMutation: string; copyMutation: string; }; + subscriptions: { + changeSubscription: string; + }; texts: { addText: string; addSuccessText?: string; @@ -236,6 +242,7 @@ export type BoardDetailQueryResponse = { export type PipelineDetailQueryResponse = { pipelineDetail: IPipeline; loading: boolean; + subscribeToMore: any; }; export type WatchVariables = { @@ -271,6 +278,8 @@ export type RelatedItemsQueryResponse = { export type DetailQueryResponse = { loading: boolean; error?: Error; + subscribeToMore: any; + refetch: () => void; }; // query response @@ -333,6 +342,12 @@ export interface IFilterParams extends ISavedConformity { userIds?: string; } +export interface INonFilterParams { + key?: string; + pipelineId: string; + id: string; +} + export interface IEditFormContent { state: any; saveItem: (doc: { [key: string]: any }, callback?: (item) => void) => void; diff --git a/ui/src/modules/boards/utils.tsx b/ui/src/modules/boards/utils.tsx index 1e07221b833..f0184ea31c6 100644 --- a/ui/src/modules/boards/utils.tsx +++ b/ui/src/modules/boards/utils.tsx @@ -157,3 +157,7 @@ export const generateButtonClass = (closeDate: Date, isComplete?: boolean) => { return colorName; }; + +export const generateUniqueId = (itemId?: string) => { + return `${itemId}-${Math.random().toString()}`; +}; diff --git a/ui/src/modules/checklists/components/Item.tsx b/ui/src/modules/checklists/components/Item.tsx index 4f063b7e37a..6beb7382d4c 100644 --- a/ui/src/modules/checklists/components/Item.tsx +++ b/ui/src/modules/checklists/components/Item.tsx @@ -4,7 +4,7 @@ import DropdownToggle from 'modules/common/components/DropdownToggle'; import { FormControl } from 'modules/common/components/form'; import Icon from 'modules/common/components/Icon'; import { isEmptyContent } from 'modules/common/utils'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Dropdown from 'react-bootstrap/Dropdown'; import xss from 'xss'; import { @@ -25,119 +25,114 @@ type Props = { removeItem: (checklistItemId: string) => void; }; -type State = { - isEditing: boolean; - content: string; - isChecked: boolean; - disabled: boolean; - beforeContent: string; -}; - -class ListRow extends React.Component { - constructor(props) { - super(props); - - const item = props.item; - - this.state = { - isEditing: false, - content: item.content, - disabled: false, - isChecked: item.isChecked, - beforeContent: item.content - }; +function Item(props: Props) { + const item = props.item; + + const [isEditing, setIsEditing] = useState(false); + const [content, setContent] = useState(item.content); + const [disabled, setDisabled] = useState(false); + const [isChecked, setIsChecked] = useState(item.isChecked || false); + const [beforeContent, setBeforeContent] = useState(item.content); + + useEffect( + () => { + setIsChecked(item.isChecked || false); + setBeforeContent(item.content); + setContent(item.content); + }, + [item] + ); + + function onFocus(event) { + event.target.select(); } - onFocus = event => event.target.select(); - - onClick = () => { - this.setState({ isEditing: true, beforeContent: this.props.item.content }); - }; + function onClick() { + setIsEditing(true); + setBeforeContent(content); + } - onKeyPress = e => { + function onKeyPress(e) { if (e.key === 'Enter') { e.preventDefault(); - this.handleSave(); + handleSave(); } - }; + } - onSubmit = e => { + function onSubmit(e) { e.preventDefault(); - this.handleSave(); - }; + handleSave(); + } - onBlur = () => { - if (isEmptyContent(this.state.content)) { + function onBlur() { + if (isEmptyContent(content)) { return; } - debounce(() => this.setState({ isEditing: false }), 100)(); - }; + debounce(() => setIsEditing(false), 100)(); + } - onCheckChange = e => { - const { editItem } = this.props; + function onCheckChange(e) { + const { editItem } = props; - const isChecked = (e.currentTarget as HTMLInputElement).checked; + const checked = (e.currentTarget as HTMLInputElement).checked; - this.setState({ isChecked, isEditing: false }, () => { - const { content } = this.state; + setIsChecked(checked); + setIsEditing(false); - editItem({ content, isChecked }); - }); - }; + editItem({ content, isChecked: checked }); + } - handleSave = () => { - if (isEmptyContent(this.state.content)) { + function handleSave() { + if (isEmptyContent(content)) { return; } - const { content, isChecked } = this.state; - - this.setState({ disabled: true }); + setDisabled(true); - this.props.editItem({ content, isChecked }, () => { - this.setState({ disabled: false, isEditing: false }); + props.editItem({ content, isChecked }, () => { + setDisabled(false); + setIsEditing(false); }); - }; + } - onRemove = () => { - const { removeItem, item } = this.props; + function onRemove() { + const { removeItem } = props; removeItem(item._id); - }; + } - onConvert = () => { - this.props.convertToCard(this.state.content, this.onRemove); - }; + function onConvert() { + props.convertToCard(content, onRemove); + } - renderContent() { + function renderContent() { const onChangeContent = e => { - this.setState({ - content: (e.currentTarget as HTMLTextAreaElement).value - }); + setContent((e.currentTarget as HTMLTextAreaElement).value); }; const onCancel = () => { - this.setState({ isEditing: false, content: this.state.beforeContent }); + setIsEditing(false); + setContent(beforeContent); }; - if (this.state.isEditing) { + if (isEditing) { return ( - +
); - }; + } - generateDoc = (values: { title: string }) => { + function generateDoc(values: { title: string }) { return { - _id: this.props.item._id, - title: values.title || this.state.title + _id: item._id, + title: values.title || title }; - }; + } - renderTitleInput = (formProps: IFormProps) => { - const { isEditingTitle, title, beforeTitle } = this.state; + function renderTitleInput(formProps: IFormProps) { const { isSubmitted, values } = formProps; if (!isEditingTitle) { return null; } - const cancelEditing = () => - this.setState({ isEditingTitle: false, title: beforeTitle }); + const cancelEditing = () => { + setIsEditingTitle(false); + setTitle(beforeTitle); + }; const onChangeTitle = e => - this.setState({ title: (e.currentTarget as HTMLTextAreaElement).value }); + setTitle((e.currentTarget as HTMLTextAreaElement).value); const onSubmit = () => { - this.setState({ isEditingTitle: false, beforeTitle: title }); + setIsEditingTitle(false); + setBeforeTitle(title); }; return ( @@ -200,8 +189,8 @@ class List extends React.Component { required={true} /> - {this.props.renderButton({ - values: this.generateDoc(values), + {props.renderButton({ + values: generateDoc(values), isSubmitted, callback: onSubmit })} @@ -214,11 +203,9 @@ class List extends React.Component { /> ); - }; - - renderProgressBar = () => { - const { item } = this.props; + } + function renderProgressBar() { return ( {item.percent.toFixed(0)}% @@ -229,59 +216,53 @@ class List extends React.Component { /> ); - }; - - renderItems() { - const { item } = this.props; + } - if (this.state.isHidden) { + function renderItems() { + if (isHidden) { return item.items .filter(data => !data.isChecked) .map(data => ( )); } return item.items.map(data => ( - + )); } - renderAddInput() { - const { isAddingItem } = this.state; - + function renderAddInput() { if (isAddingItem) { + const onClick = () => onCancel(false); + return ( - + ); } - render() { - return ( - <> - - - - - {this.renderTitle()} -
- - - - {this.renderProgressBar()} - - - {this.renderItems()} - {this.renderAddInput()} - - - ); - } + return ( + <> + + + + + {renderTitle()} + + + + + {renderProgressBar()} + + + {renderItems()} + {renderAddInput()} + + + ); } export default List; diff --git a/ui/src/modules/checklists/containers/Checklists.tsx b/ui/src/modules/checklists/containers/Checklists.tsx index 6f36af8446d..70ec44d2f18 100644 --- a/ui/src/modules/checklists/containers/Checklists.tsx +++ b/ui/src/modules/checklists/containers/Checklists.tsx @@ -2,9 +2,9 @@ import gql from 'graphql-tag'; import * as compose from 'lodash.flowright'; import { IItemParams } from 'modules/boards/types'; import { withProps } from 'modules/common/utils'; -import React from 'react'; +import React, { useEffect } from 'react'; import { graphql } from 'react-apollo'; -import { queries } from '../graphql'; +import { queries, subscriptions } from '../graphql'; import { ChecklistsQueryResponse, IChecklistsParam } from '../types'; import List from './List'; @@ -19,12 +19,24 @@ type FinalProps = { checklistsQuery: ChecklistsQueryResponse; } & IProps; -const ChecklistsContainer = (props: FinalProps) => { - const { checklistsQuery, stageId, addItem } = props; +function ChecklistsContainer(props: FinalProps) { + const { + checklistsQuery, + stageId, + addItem, + contentType, + contentTypeId + } = props; - if (checklistsQuery.loading) { - return null; - } + useEffect(() => { + return checklistsQuery.subscribeToMore({ + document: gql(subscriptions.checklistsChanged), + variables: { contentType, contentTypeId }, + updateQuery: () => { + checklistsQuery.refetch(); + } + }); + }); const checklists = checklistsQuery.checklists || []; @@ -36,7 +48,7 @@ const ChecklistsContainer = (props: FinalProps) => { addItem={addItem} /> )); -}; +} export default withProps( compose( diff --git a/ui/src/modules/checklists/containers/List.tsx b/ui/src/modules/checklists/containers/List.tsx index 674cc2a027b..8ee1fd56de5 100644 --- a/ui/src/modules/checklists/containers/List.tsx +++ b/ui/src/modules/checklists/containers/List.tsx @@ -4,10 +4,10 @@ import { IItemParams } from 'modules/boards/types'; import ButtonMutate from 'modules/common/components/ButtonMutate'; import { IButtonMutateProps } from 'modules/common/types'; import { Alert, confirm, withProps } from 'modules/common/utils'; -import React from 'react'; +import React, { useEffect } from 'react'; import { graphql } from 'react-apollo'; import List from '../components/List'; -import { mutations, queries } from '../graphql'; +import { mutations, queries, subscriptions } from '../graphql'; import { AddItemMutationResponse, EditMutationResponse, @@ -29,9 +29,21 @@ type FinalProps = { removeMutation: RemoveMutationResponse; } & Props; -class ListContainer extends React.Component { - remove = (checklistId: string) => { - const { removeMutation } = this.props; +function ListContainer(props: FinalProps) { + const { checklistDetailQuery, listId } = props; + + useEffect(() => { + return checklistDetailQuery.subscribeToMore({ + document: gql(subscriptions.checklistDetailChanged), + variables: { _id: listId }, + updateQuery: () => { + checklistDetailQuery.refetch(); + } + }); + }); + + function remove(checklistId: string) { + const { removeMutation } = props; confirm().then(() => { removeMutation({ variables: { _id: checklistId } }) @@ -43,74 +55,66 @@ class ListContainer extends React.Component { Alert.error(e.message); }); }); - }; + } - addItem = (item: string) => { - const { addItemMutation, listId } = this.props; + function addItem(content: string) { + const { addItemMutation } = props; addItemMutation({ variables: { checklistId: listId, - content: item + content } }); - }; + } - convertToCard = (name: string, callback: () => void) => { - const { stageId } = this.props; + function convertToCard(name: string, callback: () => void) { + const { stageId } = props; const afterConvert = () => { callback(); Alert.success('You successfully converted a card'); }; - this.props.addItem({ stageId, name }, afterConvert); - }; + props.addItem({ stageId, name }, afterConvert); + } - render() { - const renderButton = ({ - values, - isSubmitted, - callback - }: IButtonMutateProps) => { - const callBackResponse = () => { - if (callback) { - callback(); - } - }; - - return ( - - ); + function renderButton({ values, isSubmitted, callback }: IButtonMutateProps) { + const callBackResponse = () => { + if (callback) { + callback(); + } }; - const { checklistDetailQuery } = this.props; + return ( + + ); + } - if (checklistDetailQuery.loading) { - return null; - } + if (checklistDetailQuery.loading) { + return null; + } - const item = checklistDetailQuery.checklistDetail; + const item = checklistDetailQuery.checklistDetail; - const props = { - item, - addItem: this.addItem, - renderButton, - remove: this.remove, - convertToCard: this.convertToCard - }; + const listProps = { + item, + addItem, + renderButton, + remove, + convertToCard + }; - return ; - } + return ; } const options = (props: Props) => { diff --git a/ui/src/modules/checklists/graphql/index.ts b/ui/src/modules/checklists/graphql/index.ts index 710d7cb02c9..05c59decca8 100644 --- a/ui/src/modules/checklists/graphql/index.ts +++ b/ui/src/modules/checklists/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { mutations, queries }; +export { mutations, queries, subscriptions }; diff --git a/ui/src/modules/checklists/graphql/subscriptions.ts b/ui/src/modules/checklists/graphql/subscriptions.ts new file mode 100644 index 00000000000..f730991d5b4 --- /dev/null +++ b/ui/src/modules/checklists/graphql/subscriptions.ts @@ -0,0 +1,20 @@ +const checklistsChanged = ` + subscription checklistsChanged($contentType: String!, $contentTypeId: String!) { + checklistsChanged(contentType: $contentType, contentTypeId: $contentTypeId) { + _id + } + } +`; + +const checklistDetailChanged = ` + subscription checklistDetailChanged($_id: String!) { + checklistDetailChanged(_id: $_id) { + _id + } + } +`; + +export default { + checklistsChanged, + checklistDetailChanged +}; diff --git a/ui/src/modules/checklists/types.ts b/ui/src/modules/checklists/types.ts index 22faa9730a1..1bbfe700536 100644 --- a/ui/src/modules/checklists/types.ts +++ b/ui/src/modules/checklists/types.ts @@ -21,6 +21,7 @@ export type ChecklistsQueryResponse = { checklists: IChecklist[]; loading: boolean; refetch: () => void; + subscribeToMore: any; }; export type AddMutationResponse = ( diff --git a/ui/src/modules/common/components/Button.tsx b/ui/src/modules/common/components/Button.tsx index 35d30df0e95..345b2fc27fc 100644 --- a/ui/src/modules/common/components/Button.tsx +++ b/ui/src/modules/common/components/Button.tsx @@ -64,8 +64,10 @@ const ButtonStyled = styledTS<{ ${props => css` padding: ${sizes[props.hugeness].padding}; background: ${types[props.btnStyle].background}; - font-size: ${props.uppercase ? sizes[props.hugeness].fontSize : `calc(${sizes[props.hugeness].fontSize} + 1px)`}; - text-transform: ${props.uppercase ? 'uppercase' : 'none' }; + font-size: ${props.uppercase + ? sizes[props.hugeness].fontSize + : `calc(${sizes[props.hugeness].fontSize} + 1px)`}; + text-transform: ${props.uppercase ? 'uppercase' : 'none'}; color: ${types[props.btnStyle].color ? types[props.btnStyle].color : colors.colorWhite} !important; @@ -140,9 +142,11 @@ const ButtonGroup = styledTS<{ hasGap: boolean }>(styled.div)` margin-left: ${props => props.hasGap && '10px'}; } - ${props => !props.hasGap && + ${props => + !props.hasGap && css` - button, a { + button, + a { margin: 0; } @@ -150,7 +154,7 @@ const ButtonGroup = styledTS<{ hasGap: boolean }>(styled.div)` > a:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; - border-right: 1px solid rgba(0,0,0,0.13); + border-right: 1px solid rgba(0, 0, 0, 0.13); } > button:not(:first-child), @@ -176,7 +180,7 @@ type ButtonProps = { icon?: string; style?: any; id?: string; - uppercase?: boolean + uppercase?: boolean; }; export default class Button extends React.Component { @@ -216,6 +220,12 @@ export default class Button extends React.Component { } } -function Group({ children, hasGap = true }: { children: React.ReactNode, hasGap?: boolean }) { +function Group({ + children, + hasGap = true +}: { + children: React.ReactNode; + hasGap?: boolean; +}) { return {children}; } diff --git a/ui/src/modules/common/components/Uploader.tsx b/ui/src/modules/common/components/Uploader.tsx index 3a9586a2988..302c5831220 100644 --- a/ui/src/modules/common/components/Uploader.tsx +++ b/ui/src/modules/common/components/Uploader.tsx @@ -1,5 +1,5 @@ import { __, Alert, uploadHandler } from 'modules/common/utils'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { rgba } from '../styles/color'; import colors from '../styles/colors'; @@ -54,76 +54,61 @@ type Props = { multiple?: boolean; }; -type State = { - attachments: IAttachment[]; - loading: boolean; -}; - -class Uploader extends React.Component { - static defaultProps = { - multiple: true - }; - - constructor(props: Props) { - super(props); +function Uploader(props: Props) { + const [attachments, setAttachments] = useState(props.defaultFileList); + const [loading, setLoading] = useState(false); - const { defaultFileList } = this.props; - - this.state = { - attachments: defaultFileList || [], - loading: false - }; - } + useEffect( + () => { + setAttachments(props.defaultFileList); + }, + [props.defaultFileList] + ); - handleFileInput = ({ target }) => { + function handleFileInput({ target }) { const files = target.files; uploadHandler({ files, beforeUpload: () => { - this.setState({ - loading: true - }); + setLoading(true); }, afterUpload: ({ status, response, fileInfo }) => { if (status !== 'ok') { Alert.error(response); - return this.setState({ loading: false }); + + return setLoading(false); } Alert.info('Success'); + setLoading(false); + // set attachments const attachment = { url: response, ...fileInfo }; + const updated = [...attachments, attachment]; - const attachments = [...this.state.attachments, attachment]; - - this.props.onChange(attachments); - - this.setState({ - loading: false, - attachments - }); + setAttachments(updated); + props.onChange(updated); } }); target.value = ''; - }; - - removeAttachment = (index: number) => { - const attachments = [...this.state.attachments]; + } - attachments.splice(index, 1); + function removeAttachmentByIndex(index: number) { + const updated = [...attachments]; - this.setState({ attachments }); + updated.splice(index, 1); - this.props.onChange(attachments); - }; + setAttachments(updated); + props.onChange(updated); + } - renderItem = (item: IAttachment, index: number) => { - const removeAttachment = () => this.removeAttachment(index); + function renderItem(item: IAttachment, index: number) { + const removeAttachment = () => removeAttachmentByIndex(index); const remove = {__('Delete')}; return ( @@ -131,11 +116,10 @@ class Uploader extends React.Component { ); - }; + } - renderBtn() { - const { multiple, limit } = this.props; - const { attachments, loading } = this.state; + function renderBtn() { + const { multiple, limit } = props; if (limit && limit === attachments.length) { return null; @@ -145,11 +129,7 @@ class Uploader extends React.Component { {loading && ( @@ -158,18 +138,17 @@ class Uploader extends React.Component { ); } - render() { - return ( - <> - - {this.state.attachments.map((item, index) => - this.renderItem(item, index) - )} - - {this.renderBtn()} - - ); - } + return ( + <> + {attachments.map((item, index) => renderItem(item, index))} + {renderBtn()} + + ); } +Uploader.defaultProps = { + multiple: true, + defaultFileList: [] +}; + export default Uploader; diff --git a/ui/src/modules/common/utils/animations.ts b/ui/src/modules/common/utils/animations.ts index ccf8dd1a0e6..1014804f004 100644 --- a/ui/src/modules/common/utils/animations.ts +++ b/ui/src/modules/common/utils/animations.ts @@ -94,7 +94,7 @@ const stripe = keyframes` } `; -const highlight = keyframes` +const highlight = keyframes` 0% { box-shadow: 0 0 -5px 0 #63D2D6; } 40% { box-shadow: 0 0 10px 0 #63D2D6; } 60% { box-shadow: 0 0 10px 0 #63D2D6; } diff --git a/ui/src/modules/conformity/types.ts b/ui/src/modules/conformity/types.ts index b20c46785df..57792c9cf43 100644 --- a/ui/src/modules/conformity/types.ts +++ b/ui/src/modules/conformity/types.ts @@ -16,6 +16,7 @@ export type ConformityQueryResponse = { loading: boolean; refetch: () => void; fetchMore: any; + subscribeToMore: any; }; export interface IConformityEdit { diff --git a/ui/src/modules/deals/components/DealBoard.tsx b/ui/src/modules/deals/components/DealBoard.tsx index a543dbf0bf9..dcbd9ba9fca 100644 --- a/ui/src/modules/deals/components/DealBoard.tsx +++ b/ui/src/modules/deals/components/DealBoard.tsx @@ -20,7 +20,7 @@ class DealBoard extends React.Component { } renderActionBar() { - return ; + return ; } render() { diff --git a/ui/src/modules/deals/components/DealEditForm.tsx b/ui/src/modules/deals/components/DealEditForm.tsx index f3b970a2f1e..8cad8dd4051 100644 --- a/ui/src/modules/deals/components/DealEditForm.tsx +++ b/ui/src/modules/deals/components/DealEditForm.tsx @@ -9,8 +9,8 @@ import ProductSection from 'modules/deals/components/ProductSection'; import { IProduct } from 'modules/settings/productService/types'; import PortableTasks from 'modules/tasks/components/PortableTasks'; import PortableTickets from 'modules/tickets/components/PortableTickets'; -import React from 'react'; -import { IDeal, IDealParams, IPaymentsData } from '../types'; +import React, { useEffect, useState } from 'react'; +import { IDeal, IDealParams } from '../types'; type Props = { options: IOptions; @@ -24,33 +24,29 @@ type Props = { sendToBoard?: (item: any) => void; }; -type State = { - amount: any; - products: IProduct[]; - productsData: any; - paymentsData: IPaymentsData; - changePayData: IPaymentsData; -}; - -export default class DealEditForm extends React.Component { - constructor(props) { - super(props); - - const item = props.item; - - this.state = { - amount: item.amount || {}, - productsData: item.products ? item.products.map(p => ({ ...p })) : [], - // collecting data for ItemCounter component - products: item.products ? item.products.map(p => p.product) : [], - paymentsData: item.paymentsData, - changePayData: {} - }; - } - - renderAmount = () => { - const { amount } = this.state; - +export default function DealEditForm(props: Props) { + const item = props.item; + + const [amount, setAmount] = useState(item.amount || {}); + const [productsData, setProductsData] = useState( + item.products ? item.products.map(p => ({ ...p })) : [] + ); + const [products, setProducts] = useState( + item.products ? item.products.map(p => p.product) : [] + ); + const [paymentsData, setPaymentsData] = useState(item.paymentsData || {}); + + useEffect( + () => { + setAmount(item.amount || {}); + setProductsData(item.products ? item.products.map(p => ({ ...p })) : []); + setProducts(item.products ? item.products.map(p => p.product) : []); + setPaymentsData(item.paymentsData || {}); + }, + [item] + ); + + function renderAmount() { if (Object.keys(amount).length === 0) { return null; } @@ -65,17 +61,12 @@ export default class DealEditForm extends React.Component { ))} ); - }; - - onChangeField = (name: T, value: State[T]) => { - this.setState({ [name]: value } as Pick); - }; + } - saveProductsData = () => { - const { productsData } = this.state; - const { saveItem } = this.props; - const products: IProduct[] = []; - const amount: any = {}; + function saveProductsData() { + const { saveItem } = props; + const newProducts: IProduct[] = []; + const newAmount: any = {}; const filteredProductsData: any = []; @@ -84,15 +75,15 @@ export default class DealEditForm extends React.Component { if (data.product) { if (data.currency) { // calculating item amount - if (!amount[data.currency]) { - amount[data.currency] = data.amount || 0; + if (!newAmount[data.currency]) { + newAmount[data.currency] = data.amount || 0; } else { - amount[data.currency] += data.amount || 0; + newAmount[data.currency] += data.amount || 0; } } // collecting data for ItemCounter component - products.push(data.product); + newProducts.push(data.product); data.productId = data.product._id; @@ -100,19 +91,18 @@ export default class DealEditForm extends React.Component { } }); - this.setState( - { productsData: filteredProductsData, products, amount }, - () => { - saveItem({ productsData: this.state.productsData }, updatedItem => { - this.props.onUpdate(updatedItem); - }); - } - ); - }; + setAmount(newAmount); + setProducts(newProducts); + setProductsData(filteredProductsData); + + // need to be implemented a callback + saveItem({ productsData }, updatedItem => { + props.onUpdate(updatedItem); + }); + } - savePaymentsData = () => { - const { paymentsData } = this.state; - const { saveItem } = this.props; + function savePaymentsData() { + const { saveItem } = props; Object.keys(paymentsData || {}).forEach(key => { const perData = paymentsData[key]; @@ -122,57 +112,51 @@ export default class DealEditForm extends React.Component { } }); - this.setState({ paymentsData }, () => { - saveItem({ paymentsData: this.state.paymentsData }, updatedItem => { - this.props.onUpdate(updatedItem); - }); - }); - }; - - renderProductSection = () => { - const { products, productsData, paymentsData } = this.state; + setPaymentsData(paymentsData); - const pDataChange = pData => this.onChangeField('productsData', pData); - const prsChange = prs => this.onChangeField('products', prs); - const payDataChange = payData => - this.onChangeField('paymentsData', payData); + // need to be implemented a callback + saveItem({ paymentsData }, updatedItem => { + props.onUpdate(updatedItem); + }); + } + function renderProductSection() { return ( ); - }; + } - renderItems = () => { + function renderItems() { return ( <> - - + + ); - }; + } - renderFormContent = ({ + function renderFormContent({ saveItem, onChangeStage, copy, remove - }: IEditFormContent) => { - const { item, options, onUpdate, addItem, sendToBoard } = this.props; + }: IEditFormContent) { + const { options, onUpdate, addItem, sendToBoard } = props; return ( <> { ); - }; + } - render() { - const extendedProps = { - ...this.props, - amount: this.renderAmount, - sidebar: this.renderProductSection, - formContent: this.renderFormContent - }; + const extendedProps = { + ...props, + amount: renderAmount, + sidebar: renderProductSection, + formContent: renderFormContent + }; - return ; - } + return ; } diff --git a/ui/src/modules/deals/components/ProductSection.tsx b/ui/src/modules/deals/components/ProductSection.tsx index faafa717d3c..3fe5b823d9e 100644 --- a/ui/src/modules/deals/components/ProductSection.tsx +++ b/ui/src/modules/deals/components/ProductSection.tsx @@ -47,7 +47,7 @@ function ProductSection({ ); return content; - } + }; const tipItems = (product: IProduct) => { const result: React.ReactNode[] = []; @@ -65,7 +65,10 @@ function ProductSection({ return result; }; - const renderProductFormModal = (trigger: React.ReactNode, productId?: string) => { + const renderProductFormModal = ( + trigger: React.ReactNode, + productId?: string + ) => { return ( ); - } + }; const renderProductName = (productName: string, productId: string) => { return renderProductFormModal( @@ -96,7 +99,7 @@ function ProductSection({ ); } - return renderProductName(product.name, product._id) + return renderProductName(product.name, product._id); }; return ( diff --git a/ui/src/modules/deals/components/calendar/DealColumn.tsx b/ui/src/modules/deals/components/calendar/DealColumn.tsx index 61d87027203..78d5ee6af02 100644 --- a/ui/src/modules/deals/components/calendar/DealColumn.tsx +++ b/ui/src/modules/deals/components/calendar/DealColumn.tsx @@ -78,7 +78,7 @@ class DealColumn extends React.Component { const { deals, onUpdate, onRemove } = this.props; if (deals.length === 0) { - return ; + return ; } const contents = deals.map((deal: IDeal, index: number) => ( @@ -134,7 +134,7 @@ class DealColumn extends React.Component { return (
- {__('Load more')} + {__('Load more')}
); diff --git a/ui/src/modules/deals/components/product/ProductForm.tsx b/ui/src/modules/deals/components/product/ProductForm.tsx index ddad218b416..7a3d7faa2bd 100644 --- a/ui/src/modules/deals/components/product/ProductForm.tsx +++ b/ui/src/modules/deals/components/product/ProductForm.tsx @@ -7,7 +7,12 @@ import { ModalFooter } from 'modules/common/styles/main'; import { __, Alert } from 'modules/common/utils'; import { IProduct } from 'modules/settings/productService/types'; import React from 'react'; -import { Add, FooterInfo, FormContainer, ProductTableWrapper } from '../../styles'; +import { + Add, + FooterInfo, + FormContainer, + ProductTableWrapper +} from '../../styles'; import { IPaymentsData, IProductData } from '../../types'; import PaymentForm from './PaymentForm'; import ProductItem from './ProductItem'; @@ -74,7 +79,7 @@ class ProductForm extends React.Component { currency: currencies ? currencies[0] : '', tickUsed: true }); - + onChangeProductsData(productsData); }); }; @@ -129,7 +134,7 @@ class ProductForm extends React.Component { ); } - + return ( @@ -160,7 +165,7 @@ class ProductForm extends React.Component { ))}
-
+ ); } @@ -247,6 +252,7 @@ class ProductForm extends React.Component { renderTabContent() { const { total, tax, discount, currentTab } = this.state; + if (currentTab === 'payments') { const { onChangePaymentsData } = this.props; @@ -335,7 +341,12 @@ class ProductForm extends React.Component { Cancel - diff --git a/ui/src/modules/deals/components/product/ProductItem.tsx b/ui/src/modules/deals/components/product/ProductItem.tsx index 3a570794904..cf2b7e686a6 100644 --- a/ui/src/modules/deals/components/product/ProductItem.tsx +++ b/ui/src/modules/deals/components/product/ProductItem.tsx @@ -18,7 +18,7 @@ import { Measure, ProductButton, ProductItemContainer, - ProductSettings, + ProductSettings } from '../../styles'; import { IProductData } from '../../types'; import { selectConfigOptions } from '../../utils'; @@ -32,12 +32,12 @@ type Props = { removeProductItem?: (productId: string) => void; onChangeProductsData?: (productsData: IProductData[]) => void; updateTotal?: () => void; - currentProduct?: string; + currentProduct?: string; }; type State = { - categoryId: string, - currentProduct: string + categoryId: string; + currentProduct: string; }; class ProductItem extends React.Component { @@ -64,7 +64,7 @@ class ProductItem extends React.Component { // select previous assignee when add new product const tempAssignee = localStorage.getItem('temporaryAssignee'); - if(!productData.assignUserId && tempAssignee && !productData.product) { + if (!productData.assignUserId && tempAssignee && !productData.product) { this.onChangeField('assignUserId', tempAssignee, productData._id); } }; @@ -234,8 +234,10 @@ class ProductItem extends React.Component { }; changeCurrentProduct = (productId: string) => { - this.setState({ currentProduct: this.state.currentProduct === productId ? '' : productId }) - } + this.setState({ + currentProduct: this.state.currentProduct === productId ? '' : productId + }); + }; renderForm = () => { const { productData, uom, currencies } = this.props; @@ -245,7 +247,10 @@ class ProductItem extends React.Component {
); - if (!productData.product || this.state.currentProduct === productData.product._id) { + if ( + !productData.product || + this.state.currentProduct === productData.product._id + ) { return ( @@ -270,7 +275,7 @@ class ProductItem extends React.Component { onChange={this.uomOnChange} optionRenderer={selectOption} options={selectConfigOptions(uom, MEASUREMENTS)} - /> + /> @@ -307,7 +312,7 @@ class ProductItem extends React.Component { {__('Choose Product')}: - {this.renderProductModal(productData)} + {this.renderProductModal(productData)} @@ -345,7 +350,7 @@ class ProductItem extends React.Component { {productData.currency} - + {__('Discount')}: @@ -379,7 +384,7 @@ class ProductItem extends React.Component { - + {__('Tax')}: @@ -413,17 +418,17 @@ class ProductItem extends React.Component { ); } - return null; - } + return null; + }; render() { const { productData } = this.props; return ( - diff --git a/ui/src/modules/deals/components/product/ProductRow.tsx b/ui/src/modules/deals/components/product/ProductRow.tsx index 45a2f837790..6aa7f63f562 100644 --- a/ui/src/modules/deals/components/product/ProductRow.tsx +++ b/ui/src/modules/deals/components/product/ProductRow.tsx @@ -6,73 +6,102 @@ import { IProductData } from 'modules/deals/types'; import React from 'react'; type Props = { - productData: IProductData; - children: React.ReactNode; - activeProduct?: string; - onRemove: () => void; - changeCurrentProduct: (productId: string) => void; + productData: IProductData; + children: React.ReactNode; + activeProduct?: string; + onRemove: () => void; + changeCurrentProduct: (productId: string) => void; }; function ProductRow(props: Props) { - const renderAmmount = (value: number) => { - if (!value || value === 0) { - return '-'; - } + const renderAmmount = (value: number) => { + if (!value || value === 0) { + return '-'; + } - return ( - <> - {value.toLocaleString()} {props.productData.currency} - - ) - } + return ( + <> + {value.toLocaleString()} {props.productData.currency} + + ); + }; - const renderType = (type: string) => { - if(!type) { - return ( - - - - ) - } + const renderType = (type: string) => { + if (!type) { + return ( + + + + + + ); + } - if(type === 'product') { - return ( - - - - ) - } + if (type === 'product') { + return ( + + + + + + ); + } - return ( - - - - ) - } + return ( + + + + + + ); + }; - const { product, quantity, unitPrice, discount, tax, amount, uom, _id } = props.productData; - const id = product ? product._id : _id; + const { + product, + quantity, + unitPrice, + discount, + tax, + amount, + uom, + _id + } = props.productData; + const id = product ? product._id : _id; - const changeCurrent = () => props.changeCurrentProduct(id); + const changeCurrent = () => props.changeCurrentProduct(id); return ( - <> - - - - {renderType(product ? product.type : '')} - {product ? product.name : __('Not selected')} - - - {quantity} {uom} - {renderAmmount(unitPrice)} - {renderAmmount(discount)} - {renderAmmount(tax)} - {renderAmmount(amount)} - - - {props.children && {props.children}} - + <> + + + + {renderType(product ? product.type : '')} + {product ? product.name : __('Not selected')} + + + + {quantity} {uom} + + {renderAmmount(unitPrice)} + {renderAmmount(discount)} + {renderAmmount(tax)} + {renderAmmount(amount)} + + + + + + + {props.children && ( + + {props.children} + + )} + ); } diff --git a/ui/src/modules/deals/containers/product/ProductForm.tsx b/ui/src/modules/deals/containers/product/ProductForm.tsx index d28792e62e5..683b9de80d1 100644 --- a/ui/src/modules/deals/containers/product/ProductForm.tsx +++ b/ui/src/modules/deals/containers/product/ProductForm.tsx @@ -21,7 +21,6 @@ export default class ProductFormContainer extends React.Component { return ( {({ currentUser }) => { - if (!currentUser) { return; } @@ -34,9 +33,9 @@ export default class ProductFormContainer extends React.Component { currencies: configs.dealCurrency || [] }; - return + return ; }} - ) + ); } -} \ No newline at end of file +} diff --git a/ui/src/modules/deals/graphql/index.ts b/ui/src/modules/deals/graphql/index.ts index 8cff0604ab6..ee7aa2506c2 100644 --- a/ui/src/modules/deals/graphql/index.ts +++ b/ui/src/modules/deals/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { queries, mutations }; +export { queries, mutations, subscriptions }; diff --git a/ui/src/modules/deals/graphql/subscriptions.ts b/ui/src/modules/deals/graphql/subscriptions.ts new file mode 100644 index 00000000000..54e4fdda242 --- /dev/null +++ b/ui/src/modules/deals/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +const dealsChanged = ` + subscription dealsChanged($_id: String!) { + dealsChanged(_id: $_id) { + _id + } + } +`; + +export default { + dealsChanged +}; diff --git a/ui/src/modules/deals/options.ts b/ui/src/modules/deals/options.ts index b1b5639f0ac..0e251e1b782 100644 --- a/ui/src/modules/deals/options.ts +++ b/ui/src/modules/deals/options.ts @@ -1,7 +1,7 @@ import { toArray } from 'modules/boards/utils'; import DealEditForm from './components/DealEditForm'; import DealItem from './components/DealItem'; -import { mutations, queries } from './graphql'; +import { mutations, queries, subscriptions } from './graphql'; const options = { EditForm: DealEditForm, @@ -24,6 +24,10 @@ const options = { archiveMutation: 'dealsArchive', copyMutation: 'dealsCopy' }, + subscriptionName: { + changeSubscription: 'dealsChanged', + moveSubscription: 'dealsMoved' + }, queries: { itemsQuery: queries.deals, detailQuery: queries.dealDetail, @@ -40,6 +44,10 @@ const options = { archiveMutation: mutations.dealsArchive, copyMutation: mutations.dealsCopy }, + subscriptions: { + changeSubscription: subscriptions.dealsChanged, + moveSubscription: subscriptions.dealsMoved + }, texts: { addText: 'Add a deal', updateSuccessText: 'You successfully updated a deal', diff --git a/ui/src/modules/deals/styles.ts b/ui/src/modules/deals/styles.ts index 7c677b0bfdd..2cdc294a9d3 100644 --- a/ui/src/modules/deals/styles.ts +++ b/ui/src/modules/deals/styles.ts @@ -7,7 +7,7 @@ import styledTS from 'styled-components-ts'; const FormContainer = styled.div` margin-top: 20px; - + .Select-multi-value-wrapper { display: flex; min-width: 100px; @@ -46,7 +46,7 @@ const ContentRowTitle = styled(ContentRow)` `; const ContentColumn = styledTS<{ flex?: string }>(styled.div)` - flex: ${props => props.flex ? props.flex : '1'}; + flex: ${props => (props.flex ? props.flex : '1')}; margin-right: 10px; &:last-of-type { @@ -124,7 +124,9 @@ const ProductName = styled.a` color: ${colors.textSecondary}; display: block; - > i { visibility: hidden; } + > i { + visibility: hidden; + } &:hover i { visibility: visible; diff --git a/ui/src/modules/deals/types.ts b/ui/src/modules/deals/types.ts index d04195ffc27..d0dd1acd5ad 100644 --- a/ui/src/modules/deals/types.ts +++ b/ui/src/modules/deals/types.ts @@ -65,7 +65,7 @@ export type ProductAddMutationResponse = { export interface IDeal extends IItem { products?: any; - payments?: IPaymentsData; + paymentsData?: IPaymentsData; } export interface IDealParams extends IItemParams { diff --git a/ui/src/modules/engage/components/MessageListRow.tsx b/ui/src/modules/engage/components/MessageListRow.tsx index 56ccfa63dfd..cb0c3ff3027 100755 --- a/ui/src/modules/engage/components/MessageListRow.tsx +++ b/ui/src/modules/engage/components/MessageListRow.tsx @@ -122,7 +122,11 @@ class Row extends React.Component { let status = ; const { isChecked, message, remove } = this.props; - const { stats = { send: '' }, brand = { name: '' }, validCustomersCount } = message; + const { + stats = { send: '' }, + brand = { name: '' }, + validCustomersCount + } = message; const totalCount = stats.total || 0; diff --git a/ui/src/modules/forms/components/FieldChoices.tsx b/ui/src/modules/forms/components/FieldChoices.tsx index 4858887e998..0609382a25f 100644 --- a/ui/src/modules/forms/components/FieldChoices.tsx +++ b/ui/src/modules/forms/components/FieldChoices.tsx @@ -56,12 +56,7 @@ function FieldChoices(props: Props) { text={__('Radio button')} icon="checked" /> - + { - renderMove() { - const { item, options, onChangeStage } = this.props; +function Top(props: Props) { + const { item } = props; + + const [name, setName] = useState(item.name); + + useEffect( + () => { + setName(item.name); + }, + [item.name] + ); + + function renderMove() { + const { options, onChangeStage } = props; return ( { ); } - renderHackStage() { - const hackStages = this.props.item.hackStages || []; + function renderHackStage() { + const hackStages = props.item.hackStages || []; if (hackStages.length === 0) { return null; @@ -55,49 +66,50 @@ class Top extends React.Component { ); } - render() { - const { saveItem, score, dueDate, item } = this.props; - const { assignedUsers = [], name, priority } = item; + const { saveItem, score, dueDate } = props; + const { assignedUsers = [], priority } = item; - const onNameBlur = e => { - const value = e.target.value; + const onNameBlur = e => { + if (item.name !== name) { + saveItem({ name }); + } + }; - if (name !== value) { - saveItem({ name: e.target.value }); - } - }; + const onChangeName = e => { + setName(e.target.value); + }; - return ( - <> - - - - {priority && } - - - - {assignedUsers.length > 0 && ( - - )} - {dueDate} - {this.renderHackStage()} - - - - {score && score()} - - - - {this.renderMove()} - - - ); - } + return ( + <> + + + + {priority && } + + + + {assignedUsers.length > 0 && ( + + )} + {dueDate} + {renderHackStage()} + + + + {score && score()} + + + + {renderMove()} + + + ); } export default Top; diff --git a/ui/src/modules/growthHacks/graphql/index.ts b/ui/src/modules/growthHacks/graphql/index.ts index 8cff0604ab6..ee7aa2506c2 100644 --- a/ui/src/modules/growthHacks/graphql/index.ts +++ b/ui/src/modules/growthHacks/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { queries, mutations }; +export { queries, mutations, subscriptions }; diff --git a/ui/src/modules/growthHacks/graphql/subscriptions.ts b/ui/src/modules/growthHacks/graphql/subscriptions.ts new file mode 100644 index 00000000000..e75aaabefd9 --- /dev/null +++ b/ui/src/modules/growthHacks/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +const growthHacksChanged = ` + subscription growthHacksChanged($_id: String!) { + growthHacksChanged(_id: $_id) { + _id + } + } +`; + +export default { + growthHacksChanged +}; diff --git a/ui/src/modules/growthHacks/options.ts b/ui/src/modules/growthHacks/options.ts index 0c7b46c7909..766642ad8d9 100644 --- a/ui/src/modules/growthHacks/options.ts +++ b/ui/src/modules/growthHacks/options.ts @@ -1,6 +1,6 @@ import GrowthHackEditForm from 'modules/growthHacks/containers/GrowthHackEditForm'; import GrowthHackItem from './components/GrowthHackItem'; -import { mutations, queries } from './graphql'; +import { mutations, queries, subscriptions } from './graphql'; const options = { EditForm: GrowthHackEditForm, @@ -23,6 +23,10 @@ const options = { archiveMutation: 'growthHacksArchive', copyMutation: 'growthHacksCopy' }, + subscriptionName: { + changeSubscription: 'growthHacksChanged', + moveSubscription: 'growthHacksMoved' + }, queries: { itemsQuery: queries.growthHacks, detailQuery: queries.growthHackDetail, @@ -39,6 +43,10 @@ const options = { archiveMutation: mutations.growthHacksArchive, copyMutation: mutations.growthHacksCopy }, + subscriptions: { + changeSubscription: subscriptions.growthHacksChanged, + moveSubscription: subscriptions.growthHacksMoved + }, texts: { addText: 'Add an experiment', updateSuccessText: 'You successfully updated an experiment', diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx index add08f020ec..67b69e687cb 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx @@ -100,7 +100,7 @@ export default class Editor extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.responseTemplate !== this.props.responseTemplate) { + if (nextProps.responseTemplate !== this.props.responseTemplate) { const editorState = createStateFromHTML( this.state.editorState, nextProps.responseTemplate @@ -175,10 +175,7 @@ export default class Editor extends React.Component { }; changeEditorContent = (content: string) => { - let editorState = createStateFromHTML( - this.state.editorState, - content - ); + let editorState = createStateFromHTML(this.state.editorState, content); const selection = EditorState.moveSelectionToEnd( editorState @@ -193,13 +190,13 @@ export default class Editor extends React.Component { selection, ' ' ); - + const es = EditorState.push(editorState, contentState, 'insert-characters'); editorState = EditorState.moveFocusToEnd(es); return this.setState({ editorState, templatesState: null }); - } + }; onSelectTemplate = (index?: number) => { const { templatesState } = this.state; diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/TemplateList.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/TemplateList.tsx index c1e38c33603..2f4000788f6 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/TemplateList.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/TemplateList.tsx @@ -20,7 +20,10 @@ type TemplateListProps = { }; // response templates -export default class TemplateList extends React.Component { +export default class TemplateList extends React.Component< + TemplateListProps, + {} +> { normalizeIndex(selectedIndex: number, max: number) { let index = selectedIndex % max; diff --git a/ui/src/modules/internalNotes/components/Form.tsx b/ui/src/modules/internalNotes/components/Form.tsx index c5226d3d5be..9b713d0b8bb 100755 --- a/ui/src/modules/internalNotes/components/Form.tsx +++ b/ui/src/modules/internalNotes/components/Form.tsx @@ -48,7 +48,7 @@ class Form extends React.PureComponent { super(props); this.state = { - content: props.content ? props.content : '' + content: props.content || '' }; } diff --git a/ui/src/modules/internalNotes/types.ts b/ui/src/modules/internalNotes/types.ts index d24c16ab677..f9a1033bf14 100644 --- a/ui/src/modules/internalNotes/types.ts +++ b/ui/src/modules/internalNotes/types.ts @@ -44,4 +44,5 @@ export type InternalNoteDetailQueryResponse = { internalNoteDetail: IInternalNote; loading: boolean; refetch: () => void; + subscribeToMore: any; }; diff --git a/ui/src/modules/layout/styles.ts b/ui/src/modules/layout/styles.ts index 91599944150..acb9302a8f6 100644 --- a/ui/src/modules/layout/styles.ts +++ b/ui/src/modules/layout/styles.ts @@ -33,12 +33,13 @@ const Layout = styledTS<{ isSqueezed?: boolean }>(styled.main)` position: relative; overflow: hidden; - ${props => props.isSqueezed && + ${props => + props.isSqueezed && css` ${PageHeader} { top: 36px; } - `}; + `}; `; const MainWrapper = styled.div` diff --git a/ui/src/modules/tasks/graphql/index.ts b/ui/src/modules/tasks/graphql/index.ts index 8cff0604ab6..ee7aa2506c2 100644 --- a/ui/src/modules/tasks/graphql/index.ts +++ b/ui/src/modules/tasks/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { queries, mutations }; +export { queries, mutations, subscriptions }; diff --git a/ui/src/modules/tasks/graphql/subscriptions.ts b/ui/src/modules/tasks/graphql/subscriptions.ts new file mode 100644 index 00000000000..ffa4d8e3a3b --- /dev/null +++ b/ui/src/modules/tasks/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +const tasksChanged = ` + subscription tasksChanged($_id: String!) { + tasksChanged(_id: $_id) { + _id + } + } +`; + +export default { + tasksChanged +}; diff --git a/ui/src/modules/tasks/options.ts b/ui/src/modules/tasks/options.ts index b202553ff4e..5a262e5d183 100644 --- a/ui/src/modules/tasks/options.ts +++ b/ui/src/modules/tasks/options.ts @@ -1,7 +1,7 @@ import { toArray } from 'modules/boards/utils'; import TaskEditForm from 'modules/tasks/components/TaskEditForm'; import TaskItem from './components/TaskItem'; -import { mutations, queries } from './graphql'; +import { mutations, queries, subscriptions } from './graphql'; const options = { EditForm: TaskEditForm, @@ -24,6 +24,10 @@ const options = { archiveMutation: 'tasksArchive', copyMutation: 'tasksCopy' }, + subscriptionName: { + changeSubscription: 'tasksChanged', + moveSubscription: 'tasksMoved' + }, queries: { itemsQuery: queries.tasks, detailQuery: queries.taskDetail, @@ -40,6 +44,10 @@ const options = { archiveMutation: mutations.tasksArchive, copyMutation: mutations.tasksCopy }, + subscriptions: { + changeSubscription: subscriptions.tasksChanged, + moveSubscription: subscriptions.tasksMoved + }, texts: { addText: 'Add a task', updateSuccessText: 'You successfully updated a task', diff --git a/ui/src/modules/tickets/components/TicketEditForm.tsx b/ui/src/modules/tickets/components/TicketEditForm.tsx index 2a5b8d5c02e..d76bf96a5b9 100644 --- a/ui/src/modules/tickets/components/TicketEditForm.tsx +++ b/ui/src/modules/tickets/components/TicketEditForm.tsx @@ -12,7 +12,7 @@ import PortableDeals from 'modules/deals/components/PortableDeals'; import { KIND_CHOICES } from 'modules/settings/integrations/constants'; import { Capitalize } from 'modules/settings/permissions/styles'; import PortableTasks from 'modules/tasks/components/PortableTasks'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import Select from 'react-select-plus'; import { ITicket, ITicketParams } from '../types'; @@ -28,24 +28,19 @@ type Props = { sendToBoard?: (item: any) => void; }; -type State = { - source: string; -}; - -export default class TicketEditForm extends React.Component { - constructor(props) { - super(props); - - const item = props.item; +export default function TicketEditForm(props: Props) { + const item = props.item; - this.state = { - source: item.source || '' - }; - } + const [source, setSource] = useState(item.source); - renderSidebarFields = saveItem => { - const { source } = this.state; + useEffect( + () => { + setSource(item.source); + }, + [item.source] + ); + function renderSidebarFields(saveItem) { const sourceValues = KIND_CHOICES.ALL_LIST.map(key => ({ label: __(key), value: key @@ -56,24 +51,19 @@ export default class TicketEditForm extends React.Component { value: 'other' }); - const onSelectChange = ( - option: ISelectedOption, - name: T - ) => { - const value = option ? option.value : ''; - - this.setState({ [name]: value } as Pick, () => { - if (saveItem) { - saveItem({ [name]: value }); - } - }); - }; - const sourceValueRenderer = (option: ISelectedOption): React.ReactNode => ( {option.label} ); - const onSourceChange = option => onSelectChange(option, 'source'); + const onSourceChange = option => { + const value = option ? option.value : ''; + + setSource(value); + + if (saveItem) { + saveItem({ source: value }); + } + }; return ( @@ -88,27 +78,27 @@ export default class TicketEditForm extends React.Component { /> ); - }; + } - renderItems = () => { + function renderItems() { return ( <> - - + + ); - }; + } - renderFormContent = ({ + function renderFormContent({ state, copy, remove, saveItem, onChangeStage - }: IEditFormContent) => { - const { item, options, onUpdate, addItem, sendToBoard } = this.props; + }: IEditFormContent) { + const { options, onUpdate, addItem, sendToBoard } = props; - const renderSidebar = () => this.renderSidebarFields(saveItem); + const renderSidebar = () => renderSidebarFields(saveItem); return ( <> @@ -137,20 +127,18 @@ export default class TicketEditForm extends React.Component { item={item} sidebar={renderSidebar} saveItem={saveItem} - renderItems={this.renderItems} + renderItems={renderItems} /> ); - }; + } - render() { - const extendedProps = { - ...this.props, - formContent: this.renderFormContent, - extraFields: this.state - }; + const extendedProps = { + ...props, + formContent: renderFormContent, + extraFields: { source } + }; - return ; - } + return ; } diff --git a/ui/src/modules/tickets/graphql/index.ts b/ui/src/modules/tickets/graphql/index.ts index 8cff0604ab6..ee7aa2506c2 100644 --- a/ui/src/modules/tickets/graphql/index.ts +++ b/ui/src/modules/tickets/graphql/index.ts @@ -1,4 +1,5 @@ import mutations from './mutations'; import queries from './queries'; +import subscriptions from './subscriptions'; -export { queries, mutations }; +export { queries, mutations, subscriptions }; diff --git a/ui/src/modules/tickets/graphql/subscriptions.ts b/ui/src/modules/tickets/graphql/subscriptions.ts new file mode 100644 index 00000000000..bfac72bdbdc --- /dev/null +++ b/ui/src/modules/tickets/graphql/subscriptions.ts @@ -0,0 +1,11 @@ +const ticketsChanged = ` + subscription ticketsChanged($_id: String!) { + ticketsChanged(_id: $_id) { + _id + } + } +`; + +export default { + ticketsChanged +}; diff --git a/ui/src/modules/tickets/options.ts b/ui/src/modules/tickets/options.ts index 9af6048c5c6..fc484ff2082 100644 --- a/ui/src/modules/tickets/options.ts +++ b/ui/src/modules/tickets/options.ts @@ -1,7 +1,7 @@ import { toArray } from 'modules/boards/utils'; import TicketEditForm from 'modules/tickets/components/TicketEditForm'; import TicketItem from './components/TicketItem'; -import { mutations, queries } from './graphql'; +import { mutations, queries, subscriptions } from './graphql'; const options = { EditForm: TicketEditForm, @@ -24,6 +24,10 @@ const options = { archiveMutation: 'ticketsArchive', copyMutation: 'ticketsCopy' }, + subscriptionName: { + changeSubscription: 'ticketsChanged', + moveSubscription: 'ticketsMoved' + }, queries: { itemsQuery: queries.tickets, detailQuery: queries.ticketDetail, @@ -40,6 +44,10 @@ const options = { archiveMutation: mutations.ticketsArchive, copyMutation: mutations.ticketsCopy }, + subscriptions: { + changeSubscription: subscriptions.ticketsChanged, + moveSubscription: subscriptions.ticketsMoved + }, texts: { addText: 'Add a ticket', updateSuccessText: 'You successfully updated a ticket', diff --git a/ui/src/modules/tickets/types.ts b/ui/src/modules/tickets/types.ts index 60f166f921a..c7125baf9a8 100644 --- a/ui/src/modules/tickets/types.ts +++ b/ui/src/modules/tickets/types.ts @@ -7,7 +7,7 @@ export type ActivityLogQueryResponse = { }; export interface ITicket extends IItem { - channel?: string; + source?: string; } export interface ITicketParams extends IItemParams { From 8a8cc39875a4b335363675428f0eded7c5ae0216 Mon Sep 17 00:00:00 2001 From: munkhjin Date: Mon, 16 Mar 2020 17:48:37 +0800 Subject: [PATCH 014/110] delete unnecessary subscriptions --- ui/src/modules/deals/options.ts | 6 ++---- ui/src/modules/growthHacks/options.ts | 6 ++---- ui/src/modules/tasks/options.ts | 6 ++---- ui/src/modules/tickets/options.ts | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/ui/src/modules/deals/options.ts b/ui/src/modules/deals/options.ts index 0e251e1b782..d1353fc8699 100644 --- a/ui/src/modules/deals/options.ts +++ b/ui/src/modules/deals/options.ts @@ -25,8 +25,7 @@ const options = { copyMutation: 'dealsCopy' }, subscriptionName: { - changeSubscription: 'dealsChanged', - moveSubscription: 'dealsMoved' + changeSubscription: 'dealsChanged' }, queries: { itemsQuery: queries.deals, @@ -45,8 +44,7 @@ const options = { copyMutation: mutations.dealsCopy }, subscriptions: { - changeSubscription: subscriptions.dealsChanged, - moveSubscription: subscriptions.dealsMoved + changeSubscription: subscriptions.dealsChanged }, texts: { addText: 'Add a deal', diff --git a/ui/src/modules/growthHacks/options.ts b/ui/src/modules/growthHacks/options.ts index 766642ad8d9..b4eec3d1ef4 100644 --- a/ui/src/modules/growthHacks/options.ts +++ b/ui/src/modules/growthHacks/options.ts @@ -24,8 +24,7 @@ const options = { copyMutation: 'growthHacksCopy' }, subscriptionName: { - changeSubscription: 'growthHacksChanged', - moveSubscription: 'growthHacksMoved' + changeSubscription: 'growthHacksChanged' }, queries: { itemsQuery: queries.growthHacks, @@ -44,8 +43,7 @@ const options = { copyMutation: mutations.growthHacksCopy }, subscriptions: { - changeSubscription: subscriptions.growthHacksChanged, - moveSubscription: subscriptions.growthHacksMoved + changeSubscription: subscriptions.growthHacksChanged }, texts: { addText: 'Add an experiment', diff --git a/ui/src/modules/tasks/options.ts b/ui/src/modules/tasks/options.ts index 5a262e5d183..e414a1e8074 100644 --- a/ui/src/modules/tasks/options.ts +++ b/ui/src/modules/tasks/options.ts @@ -25,8 +25,7 @@ const options = { copyMutation: 'tasksCopy' }, subscriptionName: { - changeSubscription: 'tasksChanged', - moveSubscription: 'tasksMoved' + changeSubscription: 'tasksChanged' }, queries: { itemsQuery: queries.tasks, @@ -45,8 +44,7 @@ const options = { copyMutation: mutations.tasksCopy }, subscriptions: { - changeSubscription: subscriptions.tasksChanged, - moveSubscription: subscriptions.tasksMoved + changeSubscription: subscriptions.tasksChanged }, texts: { addText: 'Add a task', diff --git a/ui/src/modules/tickets/options.ts b/ui/src/modules/tickets/options.ts index fc484ff2082..e37b323d345 100644 --- a/ui/src/modules/tickets/options.ts +++ b/ui/src/modules/tickets/options.ts @@ -25,8 +25,7 @@ const options = { copyMutation: 'ticketsCopy' }, subscriptionName: { - changeSubscription: 'ticketsChanged', - moveSubscription: 'ticketsMoved' + changeSubscription: 'ticketsChanged' }, queries: { itemsQuery: queries.tickets, @@ -45,8 +44,7 @@ const options = { copyMutation: mutations.ticketsCopy }, subscriptions: { - changeSubscription: subscriptions.ticketsChanged, - moveSubscription: subscriptions.ticketsMoved + changeSubscription: subscriptions.ticketsChanged }, texts: { addText: 'Add a ticket', From 2eaf02828c6c96fcdaa6bb95dfba6928c97107f8 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Mon, 16 Mar 2020 18:33:33 +0800 Subject: [PATCH 015/110] remove REACT_APP_CDN_HOST_API --- ui/.env.sample | 1 - ui/src/apolloClient.ts | 1 - ui/src/modules/settings/scripts/components/InstallCode.tsx | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/.env.sample b/ui/.env.sample index a4c64a4a7e9..7d1ff83db4e 100644 --- a/ui/.env.sample +++ b/ui/.env.sample @@ -1,6 +1,5 @@ PORT=3000 NODE_ENV=development REACT_APP_CDN_HOST=http://localhost:3200 -REACT_APP_CDN_HOST_API=http://localhost:3100 REACT_APP_API_URL=http://localhost:3300 REACT_APP_API_SUBSCRIPTION_URL=ws://localhost:3300/subscriptions \ No newline at end of file diff --git a/ui/src/apolloClient.ts b/ui/src/apolloClient.ts index 44e71b36df8..0b325c43d53 100644 --- a/ui/src/apolloClient.ts +++ b/ui/src/apolloClient.ts @@ -18,7 +18,6 @@ export const getEnv = () => { REACT_APP_API_URL: getItem('REACT_APP_API_URL'), REACT_APP_API_SUBSCRIPTION_URL: getItem('REACT_APP_API_SUBSCRIPTION_URL'), REACT_APP_CDN_HOST: getItem('REACT_APP_CDN_HOST'), - REACT_APP_CDN_HOST_API: getItem('REACT_APP_CDN_HOST_API') }; }; diff --git a/ui/src/modules/settings/scripts/components/InstallCode.tsx b/ui/src/modules/settings/scripts/components/InstallCode.tsx index 09c9aac751d..60c8ea29955 100644 --- a/ui/src/modules/settings/scripts/components/InstallCode.tsx +++ b/ui/src/modules/settings/scripts/components/InstallCode.tsx @@ -19,13 +19,13 @@ type State = { }; const getInstallCode = (id: string) => { - const { REACT_APP_CDN_HOST, REACT_APP_CDN_HOST_API } = getEnv(); + const { REACT_APP_CDN_HOST, REACT_APP_API_URL } = getEnv(); return ` + +``` ## Advanced combination installation In the advanced combination installation is described combination of the following features. diff --git a/docs/docs/user/subscription-getting-started.md b/docs/docs/user/subscription-getting-started.md index 365e6785a44..f6e099f3e32 100644 --- a/docs/docs/user/subscription-getting-started.md +++ b/docs/docs/user/subscription-getting-started.md @@ -5,6 +5,8 @@ title: Subscription getting started +Describe how to set the subscription + ## User access system
diff --git a/docs/docs/user/team-inbox.md b/docs/docs/user/team-inbox.md index c66c7ae89ac..6fb475d7abd 100644 --- a/docs/docs/user/team-inbox.md +++ b/docs/docs/user/team-inbox.md @@ -4,6 +4,8 @@ title: Team Inbox sidebar_label: Team inbox --- +This Document describes how to work with Team Inbox. + Shared inbox for teams diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index bac1cd79636..4046407fd84 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -35,8 +35,7 @@ "Administrator's guide": [ "administrator/creating-first-user", "administrator/environment-variables", - "administrator/file-upload", - "administrator/integrations", + "administrator/system-config", "administrator/migration" ], "Developer's guide": [ From a5c435bae3a50485c83293354be226543dda81db Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Tue, 17 Mar 2020 17:11:07 +0800 Subject: [PATCH 030/110] updated push notifications doc --- docs/docs/developer/push-notifications.md | 41 ++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/docs/developer/push-notifications.md b/docs/docs/developer/push-notifications.md index 21708c4f0a6..adcaa905493 100644 --- a/docs/docs/developer/push-notifications.md +++ b/docs/docs/developer/push-notifications.md @@ -3,8 +3,41 @@ id: push-notifications title: Push notifications sidebar_label: Push notifications --- -Push Notifications are an important feature to retain and re-engage users and monetize on their attention. We use Firebase's cloud messaging feature as a push notification service. In order to use this feature, you will need a firebase project. +Push Notifications are an important feature to retain and re-engage users and monetize on their attention. We use Firebase's cloud messaging feature as a push notification service. Let's get started with following steps. -1. Create a Firebase project. -2. Go to Google Cloud Platform and generate service account config file and download. -3. Replace the values of **google_credentials.json.sample** with your downloaded service account file and rename it to **google_credentials.json** \ No newline at end of file +First things first, The Firebase is part of the Google product so we will need a Google project. +1. [Go to the Google Cloud Console website](https://console.cloud.google.com/) +
+

+ 2. Click on Select a project/New Project and enter the name of the project and create +

+
+
+ +

+ 3. We need to enable Firebase API for our Google project
+ 4. Select a newly created project
+ 5. Click on Library from the left side menu
+ 6. Search Firebase Cloud Messaging API and click on the enable button
+

+
+

+ 7. Now we need a Firebase Project, let's create one
+ 8. Go to the Firebase Console website
+ 9. Click on create a project, enter your firebase project name and continue, it might take a while.
+

+
+

+ 10. Go to project settings. +

+
+

+ 11. We are going to use Firebase Admin SDK to send a push notification. Thus we have to authenticate, in order to use Firebase feature, here comes the Firebase service account. Create the JSON key and download it. +

+

+

+

+

+ 12. Now we have the Firebase service account, copy all values of the file you downloaded and replace all values to erxes-api/google_cred.json file +

+

\ No newline at end of file From 42d798060d0879d069627f3631b76844d56fc641 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Tue, 17 Mar 2020 19:18:16 +0800 Subject: [PATCH 031/110] fix portable board item not found error (#1771) Close #1770 --- ui/src/modules/boards/containers/editForm/EditForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/boards/containers/editForm/EditForm.tsx b/ui/src/modules/boards/containers/editForm/EditForm.tsx index 4551e87797a..54191549eab 100644 --- a/ui/src/modules/boards/containers/editForm/EditForm.tsx +++ b/ui/src/modules/boards/containers/editForm/EditForm.tsx @@ -275,7 +275,7 @@ export default (props: WrapperProps) => { onAdd={onAddItem || props.onAdd} onRemove={onRemoveItem || props.onRemove} onUpdate={onUpdateItem || props.onUpdate} - options={options || props.options} + options={props.options || options} /> ); }} From 2f99eb770c117e5ed3ecad96474b06f8eab5bc86 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Tue, 17 Mar 2020 20:57:39 +0800 Subject: [PATCH 032/110] fix default unit price --- ui/src/modules/deals/components/product/ProductItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/deals/components/product/ProductItem.tsx b/ui/src/modules/deals/components/product/ProductItem.tsx index cf2b7e686a6..3dd9594be7d 100644 --- a/ui/src/modules/deals/components/product/ProductItem.tsx +++ b/ui/src/modules/deals/components/product/ProductItem.tsx @@ -332,7 +332,7 @@ class ProductItem extends React.Component { {__('Unit price')}: Date: Tue, 17 Mar 2020 21:02:25 +0800 Subject: [PATCH 033/110] Remove rtl input --- ui/src/modules/deals/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/deals/styles.ts b/ui/src/modules/deals/styles.ts index 2cdc294a9d3..db3e65747aa 100644 --- a/ui/src/modules/deals/styles.ts +++ b/ui/src/modules/deals/styles.ts @@ -27,7 +27,7 @@ const ProductItemContainer = styled.div` padding: 8px; ${Input} { - direction: rtl; + text-align: right; } `; From c8134e7a9a77e9039b3f73c965d304f79575178a Mon Sep 17 00:00:00 2001 From: munkhsaikhan Date: Tue, 17 Mar 2020 23:22:18 +0800 Subject: [PATCH 034/110] deal product/service list, edit total discount percent, total discount and total tax percent (#1747) --- .../deals/components/product/ProductForm.tsx | 68 ++++--- .../deals/components/product/ProductItem.tsx | 19 +- .../deals/components/product/ProductTotal.tsx | 177 ++++++++++++++++++ ui/src/modules/deals/styles.ts | 8 +- 4 files changed, 230 insertions(+), 42 deletions(-) create mode 100644 ui/src/modules/deals/components/product/ProductTotal.tsx diff --git a/ui/src/modules/deals/components/product/ProductForm.tsx b/ui/src/modules/deals/components/product/ProductForm.tsx index ab52ccd9dda..e7d833b7d28 100644 --- a/ui/src/modules/deals/components/product/ProductForm.tsx +++ b/ui/src/modules/deals/components/product/ProductForm.tsx @@ -16,6 +16,7 @@ import { import { IPaymentsData, IProductData } from '../../types'; import PaymentForm from './PaymentForm'; import ProductItem from './ProductItem'; +import ProductTotal from './ProductTotal'; type Props = { onChangeProductsData: (productsData: IProductData[]) => void; @@ -33,8 +34,8 @@ type Props = { type State = { total: { currency?: string; amount?: number }; - tax: { currency?: string; tax?: number }; - discount: { currency?: string; discount?: number }; + tax: { currency?: string; tax?: number; percent?: number }; + discount: { currency?: string; discount?: number; percent?: number }; currentTab: string; changePayData: { currency?: string; amount?: number }; tempId: string; @@ -65,6 +66,9 @@ class ProductForm extends React.Component { addProductItem = () => { const { productsData, onChangeProductsData, currencies } = this.props; + const { tax, discount } = this.state; + + const currency = currencies ? currencies[0] : ''; this.setState({ tempId: Math.random().toString() }, () => { productsData.push({ @@ -72,11 +76,11 @@ class ProductForm extends React.Component { quantity: 1, unitPrice: 0, tax: 0, - taxPercent: 0, + taxPercent: tax[currency] ? tax[currency].percent : 0, discount: 0, - discountPercent: 0, + discountPercent: discount[currency] ? discount[currency].percent : 0, amount: 0, - currency: currencies ? currencies[0] : '', + currency, tickUsed: true }); @@ -91,12 +95,10 @@ class ProductForm extends React.Component { onChangeProductsData(removedProductsData); - this.updateTotal(); + this.updateTotal(removedProductsData); }; - updateTotal = () => { - const { productsData } = this.props; - + updateTotal = (productsData = this.props.productsData) => { const total = {}; const tax = {}; const discount = {}; @@ -104,25 +106,41 @@ class ProductForm extends React.Component { productsData.forEach(p => { if (p.currency && p.tickUsed) { if (!total[p.currency]) { - total[p.currency] = 0; - tax[p.currency] = 0; - discount[p.currency] = 0; + discount[p.currency] = { percent: 0, value: 0 }; + tax[p.currency] = { percent: 0, value: 0 }; + total[p.currency] = { value: 0 }; } - total[p.currency] += p.amount || 0; - tax[p.currency] += p.tax || 0; - discount[p.currency] += p.discount || 0; + discount[p.currency].value += p.discount || 0; + tax[p.currency].value += p.tax || 0; + total[p.currency].value += p.amount || 0; } }); + for (const currency of Object.keys(discount)) { + let clearTotal = total[currency].value - tax[currency].value; + tax[currency].percent = (tax[currency].value * 100) / clearTotal; + + clearTotal = clearTotal + discount[currency].value; + discount[currency].percent = + (discount[currency].value * 100) / clearTotal; + } + this.setState({ total, tax, discount }); }; - renderTotal(value) { - return Object.keys(value).map(key => ( -
- {value[key].toLocaleString()} {key} -
+ renderTotal(totalKind, kindTxt) { + const { productsData, onChangeProductsData } = this.props; + return Object.keys(totalKind).map(currency => ( + )); } @@ -286,16 +304,16 @@ class ProductForm extends React.Component { - - + + - - + + - +
{__('Tax')}:{this.renderTotal(tax)}{__('Discount')}:{this.renderTotal(discount, 'discount')}
{__('Discount')}:{this.renderTotal(discount)}{__('Tax')}:{this.renderTotal(tax, 'tax')}
{__('Total')}:{this.renderTotal(total)}{this.renderTotal(total, 'total')}
diff --git a/ui/src/modules/deals/components/product/ProductItem.tsx b/ui/src/modules/deals/components/product/ProductItem.tsx index 3dd9594be7d..09ffc55aeeb 100644 --- a/ui/src/modules/deals/components/product/ProductItem.tsx +++ b/ui/src/modules/deals/components/product/ProductItem.tsx @@ -73,19 +73,10 @@ class ProductItem extends React.Component { const amount = productData.unitPrice * productData.quantity; if (amount > 0) { - switch (type) { - case 'discount': { - productData.discountPercent = (productData.discount * 100) / amount; - break; - } - case 'discountPercent': { - productData.discount = (amount * productData.discountPercent) / 100; - break; - } - default: { - productData.discountPercent = (productData.discount * 100) / amount; - productData.discount = (amount * productData.discountPercent) / 100; - } + if (type === 'discount') { + productData.discountPercent = (productData.discount * 100) / amount; + } else { + productData.discount = (amount * productData.discountPercent) / 100; } productData.tax = @@ -94,9 +85,7 @@ class ProductItem extends React.Component { amount - (productData.discount || 0) + (productData.tax || 0); } else { productData.tax = 0; - productData.taxPercent = 0; productData.discount = 0; - productData.discountPercent = 0; productData.amount = 0; } diff --git a/ui/src/modules/deals/components/product/ProductTotal.tsx b/ui/src/modules/deals/components/product/ProductTotal.tsx new file mode 100644 index 00000000000..c60387721bd --- /dev/null +++ b/ui/src/modules/deals/components/product/ProductTotal.tsx @@ -0,0 +1,177 @@ +import { FormControl } from 'modules/common/components/form'; +import { + Amount, + ContentColumn, + ContentRow, + Measure +} from 'modules/deals/styles'; +import { IProductData } from 'modules/deals/types'; +import React from 'react'; + +type Props = { + kindTxt: string; + totalKind: { value: number; percent?: number }; + currency: string; + productsData: IProductData[]; + updateTotal: () => void; + onChangeProductsData: (productsData: IProductData[]) => void; +}; + +type State = {}; + +class ProductTotal extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + + componentDidMount = () => { + this.props.updateTotal(); + }; + + taxAmountLogic = (amount, pData) => { + if (amount > 0) { + pData.tax = ((amount - pData.discount) * pData.taxPercent) / 100; + pData.amount = amount - (pData.discount || 0) + (pData.tax || 0); + } else { + pData.amount = 0; + pData.discount = 0; + pData.tax = 0; + } + }; + + onChangePercent = e => { + const value = Number((e.target as HTMLInputElement).value); + const { + productsData, + kindTxt, + onChangeProductsData, + updateTotal, + currency + } = this.props; + + for (const pData of productsData.filter( + item => item.currency === currency + )) { + const amount = pData.unitPrice * pData.quantity; + switch (kindTxt) { + case 'discount': { + pData.discountPercent = value; + pData.discount = (amount * value) / 100; + break; + } + case 'tax': { + pData.taxPercent = value; + break; + } + } + this.taxAmountLogic(amount, pData); + } + + onChangeProductsData(productsData); + updateTotal(); + }; + + onChange = e => { + // only total discount has editable + const value = Number((e.target as HTMLInputElement).value); + const { + productsData, + onChangeProductsData, + updateTotal, + currency + } = this.props; + + const currencyProData = productsData.filter( + item => item.currency === currency + ); + const sumAmount = currencyProData.reduce( + (sum, cur) => sum + cur.unitPrice * cur.quantity, + 0 + ); + const tmpPercent = (value * 100) / sumAmount; + + for (const pData of currencyProData) { + const amount = pData.unitPrice * pData.quantity; + pData.discount = (amount / sumAmount) * value; + pData.discountPercent = tmpPercent; + this.taxAmountLogic(amount, pData); + } + + onChangeProductsData(productsData); + updateTotal(); + }; + + renderTotalPercent() { + const { totalKind, kindTxt } = this.props; + + if (kindTxt === 'total') { + return; + } + + return ( + + + % + + ); + } + + renderTotalDiscount() { + const { currency, kindTxt, totalKind } = this.props; + + if (kindTxt !== 'discount') { + return; + } + + return ( + + + {currency} + + ); + } + + renderTotal() { + const { currency, kindTxt, totalKind } = this.props; + + if (kindTxt === 'discount') { + return; + } + + return ( + + {totalKind.value.toLocaleString()} {currency} + + ); + } + + render() { + return ( + + {this.renderTotalPercent()} + + {this.renderTotalDiscount()} + {this.renderTotal()} + + + ); + } +} + +export default ProductTotal; diff --git a/ui/src/modules/deals/styles.ts b/ui/src/modules/deals/styles.ts index db3e65747aa..28bee210168 100644 --- a/ui/src/modules/deals/styles.ts +++ b/ui/src/modules/deals/styles.ts @@ -60,8 +60,12 @@ const FooterInfo = styled.div` table { text-align: right; float: right; - width: 40%; - font-size: 16px; + width: 50%; + font-size: 14px; + } + + ${Input} { + direction: rtl; } `; From df5af9c1a60772585b0911da2bbfe3d334d610c5 Mon Sep 17 00:00:00 2001 From: munkhsaikhan Date: Tue, 17 Mar 2020 23:22:54 +0800 Subject: [PATCH 035/110] payments data save with saveProductsData (#1772) --- .../deals/components/ProductSection.test.tsx | 3 +- .../modules/deals/components/DealEditForm.tsx | 30 ++++++------------- .../deals/components/ProductSection.tsx | 5 +--- .../deals/components/product/ProductForm.tsx | 9 +----- .../deals/containers/product/ProductForm.tsx | 1 - 5 files changed, 12 insertions(+), 36 deletions(-) diff --git a/ui/src/__tests__/deals/components/ProductSection.test.tsx b/ui/src/__tests__/deals/components/ProductSection.test.tsx index ff138740ba2..98ecdce5f9a 100644 --- a/ui/src/__tests__/deals/components/ProductSection.test.tsx +++ b/ui/src/__tests__/deals/components/ProductSection.test.tsx @@ -78,8 +78,7 @@ describe('ProductSection component', () => { onChangeProductsData: (productsData: IProductData[]) => null, onChangePaymentsData: (paymentsData: IPaymentsData) => null, onChangeProducts: (prs: IProduct[]) => null, - saveProductsData: () => null, - savePaymentsData: () => null + saveProductsData: () => null }; test('renders shallow successfully', () => { diff --git a/ui/src/modules/deals/components/DealEditForm.tsx b/ui/src/modules/deals/components/DealEditForm.tsx index dac68ad1969..bdd0258fa88 100644 --- a/ui/src/modules/deals/components/DealEditForm.tsx +++ b/ui/src/modules/deals/components/DealEditForm.tsx @@ -73,7 +73,7 @@ export default class DealEditForm extends React.Component { }; saveProductsData = () => { - const { productsData } = this.state; + const { productsData, paymentsData } = this.state; const { saveItem } = this.props; const products: IProduct[] = []; const amount: any = {}; @@ -97,20 +97,6 @@ export default class DealEditForm extends React.Component { } }); - this.setState( - { productsData: filteredProductsData, products, amount }, - () => { - saveItem({ productsData }, updatedItem => { - this.setState({ updatedItem }); - }); - } - ); - }; - - savePaymentsData = () => { - const { paymentsData } = this.state; - const { saveItem } = this.props; - Object.keys(paymentsData || {}).forEach(key => { const perData = paymentsData[key]; @@ -119,11 +105,14 @@ export default class DealEditForm extends React.Component { } }); - this.setState({ paymentsData }, () => { - saveItem({ paymentsData }, updatedItem => { - this.setState({ updatedItem }); - }); - }); + this.setState( + { productsData: filteredProductsData, products, amount, paymentsData }, + () => { + saveItem({ productsData, paymentsData }, updatedItem => { + this.setState({ updatedItem }); + }); + } + ); }; beforePopupClose = (afterPopupClose?: () => void) => { @@ -160,7 +149,6 @@ export default class DealEditForm extends React.Component { paymentsData={paymentsData} products={products} saveProductsData={this.saveProductsData} - savePaymentsData={this.savePaymentsData} /> ); }; diff --git a/ui/src/modules/deals/components/ProductSection.tsx b/ui/src/modules/deals/components/ProductSection.tsx index 3fe5b823d9e..4c4b132abf9 100644 --- a/ui/src/modules/deals/components/ProductSection.tsx +++ b/ui/src/modules/deals/components/ProductSection.tsx @@ -19,7 +19,6 @@ type Props = { onChangePaymentsData: (paymentsData: IPaymentsData) => void; onChangeProducts: (prs: IProduct[]) => void; saveProductsData: () => void; - savePaymentsData: () => void; }; function ProductSection({ @@ -28,8 +27,7 @@ function ProductSection({ paymentsData, onChangeProductsData, onChangePaymentsData, - saveProductsData, - savePaymentsData + saveProductsData }: Props) { const contentWithId = (productId?: string) => { const content = props => ( @@ -42,7 +40,6 @@ function ProductSection({ products={products} paymentsData={paymentsData} saveProductsData={saveProductsData} - savePaymentsData={savePaymentsData} /> ); diff --git a/ui/src/modules/deals/components/product/ProductForm.tsx b/ui/src/modules/deals/components/product/ProductForm.tsx index e7d833b7d28..886aefba266 100644 --- a/ui/src/modules/deals/components/product/ProductForm.tsx +++ b/ui/src/modules/deals/components/product/ProductForm.tsx @@ -21,7 +21,6 @@ import ProductTotal from './ProductTotal'; type Props = { onChangeProductsData: (productsData: IProductData[]) => void; saveProductsData: () => void; - savePaymentsData: () => void; onChangePaymentsData: (paymentsData: IPaymentsData) => void; productsData: IProductData[]; products: IProduct[]; @@ -212,12 +211,7 @@ class ProductForm extends React.Component { }; onClick = () => { - const { - saveProductsData, - productsData, - closeModal - // savePaymentsData - } = this.props; + const { saveProductsData, productsData, closeModal } = this.props; const { total, changePayData } = this.state; @@ -264,7 +258,6 @@ class ProductForm extends React.Component { } saveProductsData(); - // savePaymentsData(); closeModal(); }; diff --git a/ui/src/modules/deals/containers/product/ProductForm.tsx b/ui/src/modules/deals/containers/product/ProductForm.tsx index 683b9de80d1..def19cabab9 100644 --- a/ui/src/modules/deals/containers/product/ProductForm.tsx +++ b/ui/src/modules/deals/containers/product/ProductForm.tsx @@ -7,7 +7,6 @@ import { IPaymentsData, IProductData } from '../../types'; type Props = { onChangeProductsData: (productsData: IProductData[]) => void; saveProductsData: () => void; - savePaymentsData: () => void; onChangePaymentsData: (paymentsData: IPaymentsData) => void; productsData: IProductData[]; products: IProduct[]; From 6eb371866b18a74cd360c663c7089d065f8eeaa1 Mon Sep 17 00:00:00 2001 From: munkhsaikhan Date: Wed, 18 Mar 2020 02:33:27 +0800 Subject: [PATCH 036/110] deal product/services total amount modify then payments broke fix --- .../deals/components/product/PaymentForm.tsx | 3 ++- .../deals/components/product/ProductForm.tsx | 20 ++++++++++--------- .../deals/components/product/ProductTotal.tsx | 19 ++++++++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/ui/src/modules/deals/components/product/PaymentForm.tsx b/ui/src/modules/deals/components/product/PaymentForm.tsx index 45b5c241edd..64103f0a5a9 100644 --- a/ui/src/modules/deals/components/product/PaymentForm.tsx +++ b/ui/src/modules/deals/components/product/PaymentForm.tsx @@ -93,7 +93,7 @@ class PaymentForm extends React.Component { this.paymentStateChange( 'amount', type.name, - parseFloat((e.target as HTMLInputElement).value) + parseFloat((e.target as HTMLInputElement).value || '0') ); }; @@ -140,6 +140,7 @@ class PaymentForm extends React.Component { } type="number" placeholder={__('Type amount')} + min={0} name={type.name} onChange={onChange} onClick={onClick} diff --git a/ui/src/modules/deals/components/product/ProductForm.tsx b/ui/src/modules/deals/components/product/ProductForm.tsx index 886aefba266..32f882d6c16 100644 --- a/ui/src/modules/deals/components/product/ProductForm.tsx +++ b/ui/src/modules/deals/components/product/ProductForm.tsx @@ -32,11 +32,11 @@ type Props = { }; type State = { - total: { currency?: string; amount?: number }; - tax: { currency?: string; tax?: number; percent?: number }; - discount: { currency?: string; discount?: number; percent?: number }; + total: { [currency: string]: number }; + tax: { [currency: string]: { value?: number; percent?: number } }; + discount: { [currency: string]: { value?: number; percent?: number } }; currentTab: string; - changePayData: { currency?: string; amount?: number }; + changePayData: { [currency: string]: number }; tempId: string; }; @@ -75,9 +75,11 @@ class ProductForm extends React.Component { quantity: 1, unitPrice: 0, tax: 0, - taxPercent: tax[currency] ? tax[currency].percent : 0, + taxPercent: tax[currency] ? tax[currency].percent || 0 : 0, discount: 0, - discountPercent: discount[currency] ? discount[currency].percent : 0, + discountPercent: discount[currency] + ? discount[currency].percent || 0 + : 0, amount: 0, currency, tickUsed: true @@ -107,17 +109,17 @@ class ProductForm extends React.Component { if (!total[p.currency]) { discount[p.currency] = { percent: 0, value: 0 }; tax[p.currency] = { percent: 0, value: 0 }; - total[p.currency] = { value: 0 }; + total[p.currency] = 0; } discount[p.currency].value += p.discount || 0; tax[p.currency].value += p.tax || 0; - total[p.currency].value += p.amount || 0; + total[p.currency] += p.amount || 0; } }); for (const currency of Object.keys(discount)) { - let clearTotal = total[currency].value - tax[currency].value; + let clearTotal = total[currency] - tax[currency].value; tax[currency].percent = (tax[currency].value * 100) / clearTotal; clearTotal = clearTotal + discount[currency].value; diff --git a/ui/src/modules/deals/components/product/ProductTotal.tsx b/ui/src/modules/deals/components/product/ProductTotal.tsx index c60387721bd..a0f4305983c 100644 --- a/ui/src/modules/deals/components/product/ProductTotal.tsx +++ b/ui/src/modules/deals/components/product/ProductTotal.tsx @@ -147,10 +147,10 @@ class ProductTotal extends React.Component { ); } - renderTotal() { + renderTax() { const { currency, kindTxt, totalKind } = this.props; - if (kindTxt === 'discount') { + if (kindTxt !== 'tax') { return; } @@ -161,12 +161,27 @@ class ProductTotal extends React.Component { ); } + renderTotal() { + const { currency, kindTxt, totalKind } = this.props; + + if (kindTxt !== 'total') { + return; + } + + return ( + + {totalKind.toLocaleString()} {currency} + + ); + } + render() { return ( {this.renderTotalPercent()} {this.renderTotalDiscount()} + {this.renderTax()} {this.renderTotal()} From 3baab7b6037e6b18144a7f536e75f27f71c47e8b Mon Sep 17 00:00:00 2001 From: AsherThomasBabu <44612160+AsherThomasBabu@users.noreply.github.com> Date: Wed, 18 Mar 2020 08:58:27 +0530 Subject: [PATCH 037/110] Update README.md (#1781) --- docs/website/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/website/README.md b/docs/website/README.md index f3da77ff342..37959ea0485 100644 --- a/docs/website/README.md +++ b/docs/website/README.md @@ -10,13 +10,13 @@ This website was created with [Docusaurus](https://docusaurus.io/). # Get Started in 5 Minutes -1. Make sure all the dependencies for the website are installed: +1. Install the dependancies needed for the website: ```sh # Install dependencies $ yarn ``` -2. Run your dev server: +2. Start the dev server: ```sh # Start the site From 706de30db1e4d51911d7e0f78812fe4813a231e4 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Wed, 18 Mar 2020 12:14:46 +0800 Subject: [PATCH 038/110] fix sendEvent --- widgets/client/events/index.ts | 10 ++++++---- widgets/client/events/widget/index.ts | 2 +- widgets/server/views/widget-test.ejs | 27 ++++++++------------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/widgets/client/events/index.ts b/widgets/client/events/index.ts index 4cab93a14ca..ae6db948285 100644 --- a/widgets/client/events/index.ts +++ b/widgets/client/events/index.ts @@ -3,11 +3,8 @@ import { getEnv } from "../utils"; const Events: any = { init(args: any) { - const customerId = getLocalStorageItem("customerId"); - this.sendEvent({ name: "pageView", - customerId, attributes: { url: args.url } }); }, @@ -49,7 +46,12 @@ const Events: any = { }, sendEvent(data: any) { - this.sendRequest("events-receive", data); + const customerId = getLocalStorageItem("customerId"); + + this.sendRequest("events-receive", { + customerId, + ...data, + }); } }; diff --git a/widgets/client/events/widget/index.ts b/widgets/client/events/widget/index.ts index 71da9a4285c..cf511bcde5b 100644 --- a/widgets/client/events/widget/index.ts +++ b/widgets/client/events/widget/index.ts @@ -46,4 +46,4 @@ iframe.onload = async () => { }); sendMessage("init", { url: window.location.href }); -}; +}; \ No newline at end of file diff --git a/widgets/server/views/widget-test.ejs b/widgets/server/views/widget-test.ejs index 81c09577bf6..db34f229c59 100644 --- a/widgets/server/views/widget-test.ejs +++ b/widgets/server/views/widget-test.ejs @@ -25,34 +25,23 @@ -
-
-

Knowledge Base

-
-
- -
-

Form embed

-
-
-
+ From add2e644d248aa432197403496864a2171eb9560 Mon Sep 17 00:00:00 2001 From: aryangulati <42711978+aryangulati@users.noreply.github.com> Date: Wed, 18 Mar 2020 10:48:24 +0530 Subject: [PATCH 039/110] Add some new words (#1787) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6bc773b469c..b896c1c1dd1 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ erxes helps you attract and engage more customers while giving you high lead con * **Sales Pipeline:** Track your entire sales pipeline from one dashboard. All your customer information and sales process in one board to follow up flawlessly. Have your sales managers to know everything needed to deliver increased levels of personalization before they contact customers. * **Contact Management:** Manage Visitors, Customers, and Companies. Access our all-in-one CRM system in one go so that it’s easier to coordinate and manage your contacts and interactions with your customers. Erxes Contacts provides whole segmentation tools for you to work more effiecently. * **Lead Scoring:** Identify and Target Sales-Ready Leads. -* **Shared Team Inbox:** Communicate faster and easier with your customers via one truly omnichannel platform. Combine real-time client and team communication with in-app messaging, live chat, email and form, so your customers can reach you however and wherever they want +* **Shared Team Inbox:** Communicate faster and easier with your customers via one truly omnichannel platform. Combine real-time client and team communication with in-app messaging, live chat, email and form, so your customers can reach you however and wherever they want. * **Messenger:** Talk to Your Customers in Continuous Omnichannel Conversations. Enable businesses to capture every single customer feedback and communicate in real time. You can educate your customers through knowledge-base from the erxes Messenger. * **Knowledge base:** Create Help Articles for Customer Self-service. Educate both your customers and staff by creating a help center related to your brands, products and services to reach higher level of satisfactions. -* **Task Management:** Work More Collaboratively and Get More Done. Save time, manage your projects, monitor your team and increase your productivity in just a few clicks. Erxes helps to turn chaos into clarity. +* **Task Management:** Work More Collaboratively and Get More Done. Save time, manage your projects, monitor your team and increase your productivity in just a few clicks. Erxes helps to turn chaos into clarity and make everything perfect. ## Documentation * Install erxes
* erxes documentation
From 88c4b3384e8fe329e91d427cb917e2955c095599 Mon Sep 17 00:00:00 2001 From: SinithH <45849343+SinithH@users.noreply.github.com> Date: Wed, 18 Mar 2020 10:49:05 +0530 Subject: [PATCH 040/110] Update subscription-getting-started.md (#1786) --- docs/docs/user/subscription-getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/user/subscription-getting-started.md b/docs/docs/user/subscription-getting-started.md index 365e6785a44..e49389d9973 100644 --- a/docs/docs/user/subscription-getting-started.md +++ b/docs/docs/user/subscription-getting-started.md @@ -138,7 +138,7 @@ Create your Password 3. Insert your **Username** 4. Insert your **Short Name** 5. Insert your **Position** -6. Insert you **Email Address** +6. Insert your **Email Address** 7. Choose your **Location** 8. Insert your short bio **Description** 9. Link your **Social Accounts** @@ -158,7 +158,7 @@ Create your Password
-1. Choose you **Plan** +1. Choose your **Plan** 2. Choose your **Team size** 3. **Total Payment & Expiry Date** 4. Click **Pay Now** From fd24e68fa2b47fdefc18898e8fe9e1a63e960928 Mon Sep 17 00:00:00 2001 From: Munkhjargal <57760698+MujiGMJ@users.noreply.github.com> Date: Wed, 18 Mar 2020 13:19:33 +0800 Subject: [PATCH 041/110] Update doc (#1773) --- docs/docs/administrator/system-config.md | 194 +++++++++++++++------- docs/docs/developer/push-notifications.md | 36 ++-- docs/docs/overview/getting-started.md | 3 +- docs/docs/user/script-install.md | 18 +- 4 files changed, 161 insertions(+), 90 deletions(-) diff --git a/docs/docs/administrator/system-config.md b/docs/docs/administrator/system-config.md index 1aee1472004..74520f81eaf 100644 --- a/docs/docs/administrator/system-config.md +++ b/docs/docs/administrator/system-config.md @@ -149,60 +149,150 @@ You can also determine whether your account is in the sandbox by sending email t ### Google +Erxes app can be integrated with Google APIs to enable other services such as Gmail API, Cloud Pub/Sub API, .. etc. With the help of gmail API we have many more possibilities, like realtime email synchronization, send & reply email etc. Cloud Pub/Sub API provides reliable, many-to-many, asynchronous messaging between applications. -**Configuration:** +Following steps explain the integration with Gmail API. Which allows us to receive our gmail inbox messages directly to our Erxes app's inbox. + +#### Configuration: - Go to Erxes Settings => System config => General System Config => Google. ``` GOOGLE_PROJECT_ID="your google project's id" GOOGLE_APPLICATION_CREDENTIALS="your downloaded google's credentials which is json file" -GOOGLE_CLIENT_ID="your google project's client id" -GOOGLE_CLIENT_SECRET="your google project's secret key" +GOOGLE_CLIENT_ID="your google project's OAuth client id" +GOOGLE_CLIENT_SECRET="your google project's OAuth secret key" ``` +In order to enable Gmail integration, you also need to configure additionally `GOOGLE_GMAIL_TOPIC `and `GOOGLE_GMAIL_SUBSCRIPTION_NAME`. Also click on `USE_DEFAULT_GMAIL_SERVICE`. **For other Google API service, you do not need to configure following parameters.** -Requirements: +- Go to Erxes Settings => Integrations config => Gmail. +``` +USE_DEFAULT_GMAIL_SERVICE = 'true' +GOOGLE_GMAIL_TOPIC = 'gmail_topic' +GOOGLE_GMAIL_SUBSCRIPTION_NAME = 'gmail_topic_subscription' +``` -- To create Google project. -- Enable Gmail API. -- Configure Google cloud pub/sub. -Creating Google project: +#### Create a Google Cloud Project + - Go to the [ Google Cloud Project ](https://console.cloud.google.com/) + - Navigate to Select a project => NEW PROJECT + + -- Go to https://console.cloud.google.com/cloud-resource-manager and create new project. + -Enable Gmail API: +#### Enable Gmail API + - Now we need to enable Gmail API in order to add scopes + - Side menu => APIs & Services => Library => Search => Gmail API and enable -- Go to the APIs & Services/library & enable Gmail API. -- Go to the APIs & Services/credentials & create new `OAuth client ID` credentials. If you see warning about `product name` follow the instruction, make it disappear. Afterwards select `Web application` & add `http://localhost:3000/service/oauth/gmail_callback` in `Authorized redirect URIs` & create. -- Copy `Client ID` & `Client secret` paste in your erxes-api/.env file. It looks like following example: + + -Configure google cloud pub/sub: +#### Create consent screen + - Side menu => APIs Services => OAuth Consent screen => Create -- Go to https://console.cloud.google.com/cloudpubsub/enableApi & select your project. -- Enable api & create topic. -- On the topic we must create subscription which is shown on the right of the topic as 3 dots. Select `New subscrition` & create. -- Now grant publish rights on your topic. To do this, select `permissions` from the menu shown as a 3 dots & add member `serviceAccount:gmail-api-push@system.gserviceaccount.com` role as a pub/sub publisher. -- Then copy the topic & subscrition put it in erxes-api/.env file. It should looks like following example: + + -```shell -#.env -GOOGLE_TOPIC = "projects/myproject/topics/erxes-topic" -GOOGLE_SUPSCRIPTION_NAME = "projects/myproject/subscriptions/erxes-subscription" -``` + Fill out the form below and click on add scope the button -- Go to https://console.cloud.google.com/apis/credentials/serviceaccountkey -- Select your project & create new service account with role as project owner. -- Download json file, put file path in erxes/.env file. It should looks like following example: + -```shell -#.env -GOOGLE_APPLICATION_CREDENTIALS = "/Users/user/Downloads/9bb5b70c121c.json" -``` + Since we already enabled the Gmail API, we are able to add the Gmail scopes. Search Gmail API and select the following scopes and add. Afterward, do not forget to click on save on bottom + + ```Shell + https://mail.google.com/ + https://www.googleapis.com/auth/gmail.modify + https://www.googleapis.com/auth/gmail.compose + https://www.googleapis.com/auth/gmail.send + https://www.googleapis.com/auth/gmail.readonly + ``` + + + +#### Create an OAuth client + - In order to enable Google Cloud Project, we need to have a OAuth client for authorization + + + + - In application, type select web application and fill out the rest of the form + + -Add integration: + - Keep the client id and client secret we're going to use later on + + + + + +#### Add authorization callback + - Now we need to add authorization callback for our OAuth2 client + - Go to => Side menu => APIs and Services => Credentials and select an OAuth client you just created. + + + + + + - erxes-integrations repo works on PORT 3400, so that for the test purpose you can add as follows + + ```shell + http://localhost:3400/gmaillogin + ``` + + + +#### Enable PubSub API + -In order to send and receive an email you need to enable the Cloud PubSub API. + + + + + +#### Create a service account + - Go to => IAM & Admin => Service Accounts => CREATE SERVICE ACCOUNTS + - Enter service account name and Create + + + + + + + - You will automatically download the JSON file and replace it as `GOOGLE_APPLICATION_CREDENTIALS=./google_cred.json` + + + - Let's grant publish topic right to Gmail's service account. + + - Go to => Side menu => IAM & Admin => IAM => Add + + + + + + - In new members add the following value + + ```shell + gmail-api-push@system.gserviceaccount.com + ``` + + - In role select PubSub/Publisher + + + + + + Basic Gmail integration setting has done. Your parameter configuration is as follows + + + + Now you need to connect your account to Erxes. + +**Erxes Gmail integration settings:** + +1. Go to Erxes settings => App store +2. Click on Add **Gmail**. Connect your account. +3. Select your brand and click save. +4. Go to Setting=> Channel=> Add new channel=> Connect gmail integration. -- Go to erxes settings - App store - add gmail. (Make sure you create new brand beforehand) ### Common mail config @@ -374,7 +464,7 @@ TWITTER_WEBHOOK_ENV='' 3. Select your brand and click save. 4. Go to Setting=> Channel=> Add new channel=> Connect Twitter integration. -### Daily +### Video calls Erxes app can be integrated with the Daily.co API for video calls. It allows us to easy to create and configure on-demand video call URLs. Learn how to integrate Daily integration. #### Requirements: @@ -387,6 +477,7 @@ Erxes app can be integrated with the Daily.co API for video calls. It allows us - Go to Erxes Settings => System config => Integrations config => Daily. ``` +VIDEO_CALL_TYPE = 'select the video calls integration server' DAILY_API_KEY="your daily application's api key" DAILY_END_POINT="your daily application's end point" ``` @@ -460,32 +551,19 @@ ENCRYPTION_KEY='' ### Gmail -Erxes app can be integrated with Gmail API by Nylas and that means we can receive our gmail inbox messages directly to our erxes app's inbox. With the help of gmail API we have many more possibilities, like realtime email synchronization, send & reply email etc. +Erxes app can be integrated with Gmail API by Nylas and that means we can receive our gmail inbox messages directly to our erxes app's inbox. With the help of Gmail API we have many more possibilities, like realtime email synchronization, send & reply email etc. -**Requirements:** +**Configuration** -- Create Google project and OAuth Client ID. -- Enable Gmail API. +According to the following steps, click the link and set up configurations. +- [Create Google project.](/administrator/system-config#create-a-google-cloud-project) +- [Enable Gmail API.](/administrator/system-config#enable-gmail-api) +- [Configure OAuth Consent Screen.](/administrator/system-config#create-consent-screen) +- [Create an OAuth client.](/administrator/system-config#create-an-oauth-client) +- [Configure the Google parameters.](/administrator/system-config#configuration) -**Configuration:** -- Go to Erxes Settings => System config => General system config => Google. - - -``` -GOOGLE_PROJECT_ID="your google project's id" -GOOGLE_APPLICATION_CREDENTIALS="./google_cred.json" -GOOGLE_CLIENT_ID="your google project's client id" -GOOGLE_CLIENT_SECRET="your google project's secret key" -``` - - -#### Creating Google project -According to [Nylas guide](https://docs.nylas.com/docs/creating-a-google-project-for-dev), follow the configurations to set variables. -1. Create the Google project and config gmail. -2. Enable APIs. -3. Configure OAuth Consent Screen. -4. Create an OAuth Credential. In order to have Google OAuth token, add authorized redirect URIs to your google credentials. +In order to have Google OAuth token, add authorized callback (redirect URIs) to your google credentials. - Select Google project - Go to credentials from left side menu - Select OAuth 2.0 client ID @@ -499,9 +577,9 @@ According to [Nylas guide](https://docs.nylas.com/docs/creating-a-google-project `Add URI = https://api.nylas.com/oauth/callback` -After you create the Google service account download json and replace with google_cred.json. - +After you create the [Google service account (refer to the link)](/administrator/system-config#create-a-service-account) download json and replace with google_cred.json. +Basic integration setting has done. Now you need to connect your account to Erxes. **Erxes Gmail integration settings:** diff --git a/docs/docs/developer/push-notifications.md b/docs/docs/developer/push-notifications.md index adcaa905493..24a6ba7ada8 100644 --- a/docs/docs/developer/push-notifications.md +++ b/docs/docs/developer/push-notifications.md @@ -6,38 +6,40 @@ sidebar_label: Push notifications Push Notifications are an important feature to retain and re-engage users and monetize on their attention. We use Firebase's cloud messaging feature as a push notification service. Let's get started with following steps. First things first, The Firebase is part of the Google product so we will need a Google project. -1. [Go to the Google Cloud Console website](https://console.cloud.google.com/) -
-

- 2. Click on Select a project/New Project and enter the name of the project and create -

-
+ +1. [Go to the Google Cloud Console website.](https://console.cloud.google.com/) +2. [Create Google project.](/administrator/system-config#create-a-google-cloud-project) +3. Select the project. +
-

- 3. We need to enable Firebase API for our Google project
- 4. Select a newly created project
- 5. Click on Library from the left side menu
- 6. Search Firebase Cloud Messaging API and click on the enable button
+4. We need to enable Firebase API for our Google project
+5. Select a newly created project
+6. Click on Library from the left side menu
+7. Search Firebase Cloud Messaging API and click on the enable button

- 7. Now we need a Firebase Project, let's create one
- 8. Go to the Firebase Console website
- 9. Click on create a project, enter your firebase project name and continue, it might take a while.
+ + 8. Now we need a Firebase Project, let's create one
+ 9. Go to the Firebase Console website
+ 10. Click on create a project, enter your firebase project name and continue, it might take a while.

- 10. Go to project settings. + + 11. Go to project settings.

- 11. We are going to use Firebase Admin SDK to send a push notification. Thus we have to authenticate, in order to use Firebase feature, here comes the Firebase service account. Create the JSON key and download it. + + 12. We are going to use Firebase Admin SDK to send a push notification. Thus we have to authenticate, in order to use Firebase feature, here comes the Firebase service account. Create the JSON key and download it.




- 12. Now we have the Firebase service account, copy all values of the file you downloaded and replace all values to erxes-api/google_cred.json file + + 13. Now we have the Firebase service account, copy all values of the file you downloaded and replace all values to erxes-api/google_cred.json file


\ No newline at end of file diff --git a/docs/docs/overview/getting-started.md b/docs/docs/overview/getting-started.md index a7b4b4a6693..61c99801ac5 100644 --- a/docs/docs/overview/getting-started.md +++ b/docs/docs/overview/getting-started.md @@ -48,7 +48,6 @@ title: Getting Started - Creating first user - Environment Variables -- File Upload - Integrations - Migration @@ -60,6 +59,6 @@ title: Getting Started - Push Notification - Android SDK - iOS SDK -- Extend +
diff --git a/docs/docs/user/script-install.md b/docs/docs/user/script-install.md index d4e71eed038..38577bee4f6 100644 --- a/docs/docs/user/script-install.md +++ b/docs/docs/user/script-install.md @@ -589,22 +589,14 @@ bio:””, -### Change your messenger styles -You can change look of a messenger style, position with following code. +### Manipulate your messenger function +You able to manipulate your messenger function such change look of a messenger style, position and you can configure that click button to call action. #### Button submit -Ability to call submit from outside, which means that listen for callSubmit action from outside (parent website) to force submit action. +Ability to call submit from outside, which means that listen for callSubmit action from outside (parent website) to force submit action. For example, you can add any button to call action to open your messenger. ``` -document.getElementById('button').onclick = () => { - const iframe = document.querySelector('#container iframe'); - - iframe.contentWindow.postMessage( - { - fromPublisher: true, - action: "callSubmit" - }, - "*" - ); + document.getElementById('button').onclick = () => { + window.Erxes.showMessenger() } ``` #### Messenger position From 4856704860e0c376bfeecb16a4cff9f4ca97552d Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Wed, 18 Mar 2020 19:18:41 +0800 Subject: [PATCH 042/110] show spinner, emptyState in ItemChooser --- .../modules/boards/containers/portable/ItemChooser.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ui/src/modules/boards/containers/portable/ItemChooser.tsx b/ui/src/modules/boards/containers/portable/ItemChooser.tsx index 12992a8e707..92a5939da5c 100644 --- a/ui/src/modules/boards/containers/portable/ItemChooser.tsx +++ b/ui/src/modules/boards/containers/portable/ItemChooser.tsx @@ -1,5 +1,7 @@ import gql from 'graphql-tag'; import * as compose from 'lodash.flowright'; +import EmptyState from 'modules/common/components/EmptyState'; +import Spinner from 'modules/common/components/Spinner'; import { withProps } from 'modules/common/utils'; import ConformityChooser from 'modules/conformity/containers/ConformityChooser'; import React from 'react'; @@ -94,6 +96,14 @@ class ItemChooserContainer extends React.Component< refetchQuery: data.options.queries.itemsQuery }; + if (itemsQuery.loading) { + return ; + } + + if (updatedProps.datas.length === 0) { + return ; + } + return ; } } From 365183a680d5040407145ca4e1f3dab7f821182e Mon Sep 17 00:00:00 2001 From: munkhjin Date: Wed, 18 Mar 2020 22:04:22 +0800 Subject: [PATCH 043/110] show time for popup message --- .../workarea/conversation/messages/FormMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx index 2e750313ab3..12e2806244d 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx @@ -11,7 +11,7 @@ type Props = { export default class FormMessage extends React.Component { displayValue(data) { if (data.validation === 'date') { - return dayjs(data.value).format('YYYY/MM/DD'); + return dayjs(data.value).format('YYYY/MM/DD HH:mm'); } return data.value; From 832d00bc7e529f4ee499b07aedc748b0595cd054 Mon Sep 17 00:00:00 2001 From: munkhjin Date: Wed, 18 Mar 2020 22:04:39 +0800 Subject: [PATCH 044/110] set time for popup --- widgets/client/form/components/Field.tsx | 42 +++++++++++++----------- widgets/server/views/widget-test.ejs | 14 +++++--- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/widgets/client/form/components/Field.tsx b/widgets/client/form/components/Field.tsx index 1f0f1fef14b..add0d5605ed 100644 --- a/widgets/client/form/components/Field.tsx +++ b/widgets/client/form/components/Field.tsx @@ -1,8 +1,8 @@ -import * as moment from "moment"; -import * as React from "react"; -import DatePicker from "react-datepicker"; -import uploadHandler from "../../uploadHandler"; -import { FieldValue, IField, IFieldError } from "../types"; +import * as moment from 'moment'; +import * as React from 'react'; +import DatePicker from 'react-datepicker'; +import uploadHandler from '../../uploadHandler'; +import { FieldValue, IField, IFieldError } from '../types'; type Props = { field: IField; @@ -49,8 +49,8 @@ export default class Field extends React.Component {
+ +``` + +#### Register events +``` + + + + + +``` + +#### Create segments using registered events +
+ +
+ + +#### Update customer properties +``` + + + + + +``` + +#### Check registered attribute +
+ +
\ No newline at end of file diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index 8e75e01a24a..a35829c6898 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -26,6 +26,7 @@ "user/popups", "user/script-install", "user/contacts", + "user/segments", "user/sales-pipeline", "user/engage", "user/insights", diff --git a/widgets/server/views/widget-test.ejs b/widgets/server/views/widget-test.ejs index 9ed47870e94..59b5c8767b8 100644 --- a/widgets/server/views/widget-test.ejs +++ b/widgets/server/views/widget-test.ejs @@ -25,38 +25,38 @@ -
-
-

Knowledge Base

-
-
- -
-

Form embed

-
-
-
+ - (function() { - var script = document.createElement('script'); - script.src = 'http://localhost:3200/build/formWidget.bundle.js'; - script.async = true; + - var entry = document.getElementsByTagName('script')[0]; - entry.parentNode.insertBefore(script, entry); - })(); - From 41d2e21f4dfb97b9d1c4a46e948a67388003f6b7 Mon Sep 17 00:00:00 2001 From: Batnasan Byambasuren Date: Thu, 19 Mar 2020 19:32:00 +1100 Subject: [PATCH 048/110] Update some docs (#1805) * add jwt token env to the heroku dos * add centos doc link in sidebar & getting-started * add https and pm2 doc * remove widget api url * update syntax * remove widget api from env vars * add https and pm2 doc for centos 8 * install erlang silently * install pm2 using npm * upgrade to version 0.13 * enable elasticsearch daemon * fix ecosystem.conf * create a new user erxes if does not exist * create a new user erxes if does not exist * fix useradd * fix nginx erxes home dir * update debian 10 installation doc * add AWS Markeplace doc * update doc: creating admin user * little update * fix permission error --- docs/docs/administrator/creating-first-user.md | 14 ++++++++------ scripts/install/centos8.sh | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/docs/administrator/creating-first-user.md b/docs/docs/administrator/creating-first-user.md index 204fc3ab6f1..6342dc207c1 100644 --- a/docs/docs/administrator/creating-first-user.md +++ b/docs/docs/administrator/creating-first-user.md @@ -3,11 +3,12 @@ id: creating-first-user title: Creating first user --- -Through these steps you can create a user to start using the system. +The following steps are required prior using the system. ## Create admin user -Below command will create first admin user with following credentials. +The below command will create first admin user with a random password. +The password will be printed in the terminal. ``` yarn initProject @@ -15,19 +16,20 @@ yarn initProject ``` username: admin@erxes.io -password: erxes +password: ******** ``` ## Load initial data -Below command will create initial permission groups, permissions, growth hack templates, email templates and some sample data. +The below command will create initial permission groups, permissions, growth hack templates, email templates and some sample data, and reset the admin password. +The password will be printed in the terminal. ``` yarn loadInitialData ``` -If do not want to load sample data then you can run following command just to load permissions. +If you do not want to load sample data then you can run the following command just to load permissions. ``` yarn loadPermission -``` \ No newline at end of file +``` diff --git a/scripts/install/centos8.sh b/scripts/install/centos8.sh index c3508f17188..2b24d5f241f 100644 --- a/scripts/install/centos8.sh +++ b/scripts/install/centos8.sh @@ -393,7 +393,7 @@ EOF # add user nginx to erxes group gpasswd -a nginx $username -chmod 750 /home/$username +chmod 750 /home/$username /home/$username/erxes chmod -R 750 $erxes_dir # allow nginx to server build dir From a5bc52e9cfec076469e67c26f87602877ac3c715 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Thu, 19 Mar 2020 17:12:29 +0800 Subject: [PATCH 049/110] add module filter to log list (#1808) close #1807 --- .../settings/logs/components/LogList.tsx | 66 +++++++++++++++++-- .../settings/logs/containers/LogList.tsx | 2 + ui/src/modules/settings/logs/queries.ts | 6 +- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/ui/src/modules/settings/logs/components/LogList.tsx b/ui/src/modules/settings/logs/components/LogList.tsx index b80585fd494..f866f8c5ddc 100644 --- a/ui/src/modules/settings/logs/components/LogList.tsx +++ b/ui/src/modules/settings/logs/components/LogList.tsx @@ -1,5 +1,6 @@ import Datetime from '@nateradebaugh/react-datetime'; import dayjs from 'dayjs'; +import _ from 'lodash'; import Button from 'modules/common/components/Button'; import DataWithLoader from 'modules/common/components/DataWithLoader'; import EmptyState from 'modules/common/components/EmptyState'; @@ -28,6 +29,7 @@ type State = { page?: string; perPage?: string; userId?: string; + type?: string; }; type commonProps = { @@ -42,6 +44,49 @@ const actionOptions = [ { value: 'delete', label: __('Delete') } ]; +// module names are saved exactly as these values in backend +// consider both ends when changing +const moduleOptions = [ + { value: 'board', label: 'Boards' }, + { value: 'dealBoards', label: 'Deal boards' }, + { value: 'taskBoards', label: 'Task boards' }, + { value: 'ticketBoards', label: 'Ticket boards' }, + { value: 'growthHackBoards', label: 'Growth hack boards' }, + { value: 'dealPipelines', label: 'Deal pipelines' }, + { value: 'taskPipelines', label: 'Task pipelines' }, + { value: 'ticketPipelines', label: 'Ticket pipelines' }, + { value: 'growthHackPipelines', label: 'Growth hack pipelines' }, + { value: 'checklist', label: 'Checklists' }, + { value: 'checkListItem', label: 'Checklist items' }, + { value: 'brand', label: 'Brands' }, + { value: 'channel', label: 'Channels' }, + { value: 'company', label: 'Companies' }, + { value: 'customer', label: 'Customers' }, + { value: 'deal', label: 'Deals' }, + { value: 'emailTemplate', label: 'Email templates' }, + { value: 'importHistory', label: 'Import histories' }, + { value: 'product', label: 'Products' }, + { value: 'product-category', label: 'Product categories' }, + { value: 'responseTemplate', label: 'Response templates' }, + { value: 'tag', label: 'Tags' }, + { value: 'task', label: 'Tasks' }, + { value: 'ticket', label: 'Tickets' }, + { value: 'permission', label: 'Permissions' }, + { value: 'user', label: 'Users' }, + { value: 'knowledgeBaseTopic', label: 'Knowledgebase topics' }, + { value: 'knowledgeBaseCategory', label: 'Knowledgebase categories' }, + { value: 'knowledgeBaseArticle', label: 'Knowledgebase articles' }, + { value: 'userGroup', label: 'User groups' }, + { value: 'internalNote', label: 'Internal notes' }, + { value: 'pipelineLabel', label: 'Pipeline labels' }, + { value: 'pipelineTemplate', label: 'Pipeline templates' }, + { value: 'growthHack', label: 'Growth hacks' }, + { value: 'integration', label: 'Integrations' }, + { value: 'segment', label: 'Segments' }, + { value: 'engage', label: 'Engages' }, + { value: 'script', label: 'Scripts' }, +]; + const breadcrumb = [ { title: 'Settings', link: '/settings' }, { title: __('Logs') } @@ -55,14 +100,16 @@ class LogList extends React.Component { start: '', end: '', action: '', - userId: '' + userId: '', + type: '' }; this.state = { start: qp.start, end: qp.end, action: qp.action, - userId: qp.userId + userId: qp.userId, + type: qp.type }; } @@ -99,13 +146,14 @@ class LogList extends React.Component { onClick = () => { const { history } = this.props; - const { start, end, action, userId } = this.state; + const { start, end, action, userId, type } = this.state; router.setParams(history, { start, end, action, - userId + userId, + type }); }; @@ -164,7 +212,7 @@ class LogList extends React.Component { }; renderActionBar() { - const { action, userId } = this.state; + const { action, userId, type } = this.state; const onUserChange = user => { this.setFilter('userId', user); @@ -175,6 +223,14 @@ class LogList extends React.Component { {__('Filters')}: {this.renderDateFilter('start')} {this.renderDateFilter('end')} + + { end: queryParams.end, userId: queryParams.userId, action: queryParams.action, + type: queryParams.type, ...generatePaginationParams(queryParams) }; @@ -56,6 +57,7 @@ export default compose( end: queryParams.end, userId: queryParams.userId, action: queryParams.action, + type: queryParams.type, ...generatePaginationParams(queryParams) } }) diff --git a/ui/src/modules/settings/logs/queries.ts b/ui/src/modules/settings/logs/queries.ts index 2ef492c4648..3b5c473af0c 100644 --- a/ui/src/modules/settings/logs/queries.ts +++ b/ui/src/modules/settings/logs/queries.ts @@ -5,7 +5,8 @@ const logs = ` $userId: String, $action: String, $page: Int, - $perPage: Int + $perPage: Int, + $type: String ) { logs( start: $start, @@ -13,7 +14,8 @@ const logs = ` userId: $userId, action: $action, page: $page, - perPage: $perPage + perPage: $perPage, + type: $type ) { totalCount logs { From 0a4c23a146271306a5c986de7fa6f8d425dd2eca Mon Sep 17 00:00:00 2001 From: Shinebayar G <3091558+shinebayar-g@users.noreply.github.com> Date: Thu, 19 Mar 2020 14:39:53 -0500 Subject: [PATCH 050/110] Update issue templates I think it's better to assign labels manually after carefully reviewed by team rather than auto assignment. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/enhancement-request.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 327cd59f134..c4a9694df0c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: '' -labels: 'type: bug' +labels: '' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/enhancement-request.md b/.github/ISSUE_TEMPLATE/enhancement-request.md index 2a0cb210d3c..d4bcdce4b08 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-request.md +++ b/.github/ISSUE_TEMPLATE/enhancement-request.md @@ -2,7 +2,7 @@ name: Enhancement request about: Suggest an enhancement to the erxes project title: '' -labels: 'type: enhancement' +labels: '' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index dfc65414055..df81be2bee2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: '' -labels: 'type: feature' +labels: '' assignees: '' --- From 3ad7ae1b81a87f390dd29b16977530e4ed387d29 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Fri, 20 Mar 2020 12:36:44 +0800 Subject: [PATCH 051/110] fix popup handler --- widgets/client/form/widget/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/widgets/client/form/widget/index.ts b/widgets/client/form/widget/index.ts index 0d075eeeff1..8ef0854127f 100644 --- a/widgets/client/form/widget/index.ts +++ b/widgets/client/form/widget/index.ts @@ -166,7 +166,8 @@ window.addEventListener("message", async (event: MessageEvent) => { iframe.contentWindow.postMessage( { fromPublisher: true, - action: "showPopup" + action: "showPopup", + formId: setting.form_id }, "*" ); From c7cbf109573c6bfd05a4bfc53130f471dac04762 Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Fri, 20 Mar 2020 12:45:10 +0800 Subject: [PATCH 052/110] add: google cloud storage bucket name in system config --- .../settings/general/components/GeneralSettings.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ui/src/modules/settings/general/components/GeneralSettings.tsx b/ui/src/modules/settings/general/components/GeneralSettings.tsx index 64513ebca1e..90ac07f50f9 100755 --- a/ui/src/modules/settings/general/components/GeneralSettings.tsx +++ b/ui/src/modules/settings/general/components/GeneralSettings.tsx @@ -171,6 +171,18 @@ class GeneralSettings extends React.Component { + + + + {__("More: Create or find your Google Cloud Storage bucket")} + + + + Google Bucket Name + {this.renderItem('GOOGLE_CLOUD_STORAGE_BUCKET')} + + + From 56616f16062e28dc876c4eb48b7aa8e8471d5964 Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Fri, 20 Mar 2020 18:14:37 +0800 Subject: [PATCH 053/110] update: google related docs --- docs/docs/administrator/system-config.md | 339 ++++++++++---------- docs/docs/developer/push-notifications.md | 59 ++-- docs/docs/overview/getting-started.md | 4 +- docs/docs/overview/integrations-overview.md | 8 +- 4 files changed, 197 insertions(+), 213 deletions(-) diff --git a/docs/docs/administrator/system-config.md b/docs/docs/administrator/system-config.md index 74520f81eaf..d7073663ba4 100644 --- a/docs/docs/administrator/system-config.md +++ b/docs/docs/administrator/system-config.md @@ -23,6 +23,57 @@ CURRENCY='United Stated Dollar' UNIT_OF_MEASUREMENT='Pieces PCS' ``` + +### Google +Google Cloud Platform (GCP), offered by Google, is a suite of cloud computing services that runs on the same infrastructure that Google uses internally for its end-user products, such as Firebase, Gmail and Pubsub. Alongside a set of management tools, it provides a series of modular cloud services including computing, data storage, data analytics and machine learning. + +Following steps explain the Google Cloud Project. Which allows us to use Google Cloud Platform's services to our Erxes app. + +- Create a Google Cloud Project [click here](https://console.cloud.google.com/) +- Click on the Select Project + + +- Click on the New Project + + +- Enter project name and click on the Create button + + + +#### Service account +- Navigate to sidebar IAM & Admin => Service Accounts + + +- Now let's create service account for our app + + +- Enter service account name and description then click on the Create button + + +- Select Owner role and click on the Continue button + + +- Create key for service account, you will download json file automatically and keep it + + + + +- Successfully created service account + + + +- Replace the file you downloaded from google (service account) with **erxes-api/google_cred.json.sample erxes-integrations/google_cred.json.sample** and rename them to **google_cred.json** + + + +- One last touch, we need to configure erxes, Go to Settings => System Config => General System config + And configure **GOOGLE PROJECT ID**, **GOOGLE APPLICATION CREDENTIALS** fields as in the sceenshot + + - **GOOGLE APPLICATION CREDENTIALS** is google_cred file's path by default it's ./google_cred.json no need to change + + + +That's it, now you are good use Google Cloud Platform Services which you can find them [here](https://console.cloud.google.com/apis/library) ### File upload **A media type** (Multipurpose Internet Mail Extensions or MIME type) is a standard that indicates the nature and format of a document, file, or assortment of bytes. The simplest MIME type consists of a type and a subtype **(type/subtype)**. @@ -55,6 +106,34 @@ You have to ensure that public access to all your S3 buckets and objects is bloc +### Google Cloud Storage +Cloud Storage provides worldwide, highly durable object storage that scales to exabytes of data. You can access data instantly from any storage class, integrate storage into your applications with a single unified API, and easily optimize price and performance. + +#### Requirement: + - Google Cloud Platform project, follow [this](#google) guide to create one + + - Enable Google Cloud Storage API [here](https://console.cloud.google.com/apis/library) + + + + + + - Navigate to [here](https://console.cloud.google.com/storage/browser) and Create bucket for file upload + + + + - Enter bucket name and fill out rest of the form + + + + + + - Copy your bucket name and configure it in the Erxes app as follows + + + + Now final step, set upload service type to Google in [here](#file-upload) + ### AWS S3 Amazon Simple Storage Service (Amazon S3) is storage for the internet. You can use Amazon S3 to store and retrieve any amount of data at any time, from anywhere on the web. @@ -146,190 +225,115 @@ You can also determine whether your account is in the sandbox by sending email t 5. **If you move out of the Sandbox,** follow the instructions described [here](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html) to move out of the Amazon SES Sandbox. +### Common mail config +Common mail config enables you to your transaction emails will be sent by the specified email address(`FROM_EMAIL`). You can define whether transaction emails sent by AWS SES or other email services which is configured in custom mail service (`DEFAULT_EMAIL_SERVICE`). +**Configuration:** +- Go to Erxes Settings => System config => General System Config => Common mail config. -### Google -Erxes app can be integrated with Google APIs to enable other services such as Gmail API, Cloud Pub/Sub API, .. etc. With the help of gmail API we have many more possibilities, like realtime email synchronization, send & reply email etc. Cloud Pub/Sub API provides reliable, many-to-many, asynchronous messaging between applications. - -Following steps explain the integration with Gmail API. Which allows us to receive our gmail inbox messages directly to our Erxes app's inbox. - -#### Configuration: -- Go to Erxes Settings => System config => General System Config => Google. - -``` -GOOGLE_PROJECT_ID="your google project's id" -GOOGLE_APPLICATION_CREDENTIALS="your downloaded google's credentials which is json file" -GOOGLE_CLIENT_ID="your google project's OAuth client id" -GOOGLE_CLIENT_SECRET="your google project's OAuth secret key" ``` -In order to enable Gmail integration, you also need to configure additionally `GOOGLE_GMAIL_TOPIC `and `GOOGLE_GMAIL_SUBSCRIPTION_NAME`. Also click on `USE_DEFAULT_GMAIL_SERVICE`. **For other Google API service, you do not need to configure following parameters.** +FROM_EMAIL='your email address' +DEFAULT_EMAIL_SERVICE='your configured email service name' -- Go to Erxes Settings => Integrations config => Gmail. ``` -USE_DEFAULT_GMAIL_SERVICE = 'true' -GOOGLE_GMAIL_TOPIC = 'gmail_topic' -GOOGLE_GMAIL_SUBSCRIPTION_NAME = 'gmail_topic_subscription' -``` - - -#### Create a Google Cloud Project - - Go to the [ Google Cloud Project ](https://console.cloud.google.com/) - - Navigate to Select a project => NEW PROJECT - - - - - -#### Enable Gmail API - - Now we need to enable Gmail API in order to add scopes - - Side menu => APIs & Services => Library => Search => Gmail API and enable - - - - - -#### Create consent screen - - Side menu => APIs Services => OAuth Consent screen => Create - - - - - Fill out the form below and click on add scope the button - - - Since we already enabled the Gmail API, we are able to add the Gmail scopes. Search Gmail API and select the following scopes and add. Afterward, do not forget to click on save on bottom - ```Shell - https://mail.google.com/ - https://www.googleapis.com/auth/gmail.modify - https://www.googleapis.com/auth/gmail.compose - https://www.googleapis.com/auth/gmail.send - https://www.googleapis.com/auth/gmail.readonly - ``` - - -#### Create an OAuth client - - In order to enable Google Cloud Project, we need to have a OAuth client for authorization - - +### Custom mail service +Mail service enables you to delivering your transactional and marketing emails through the cloud-based email delivery platform. You can set any custom mail service in this fields. For example, Sendgrid custom mail service. Create your account in Sendgrid and fill it into the fields. - - In application, type select web application and fill out the rest of the form +**Configuration:** +- Go to Erxes Settings => System config => General System Config => Custom mail service. - +``` +MAIL_SERVICE_NAME='Sendgrid' +PORT='Sendgrid port id' +USERNAME='your account user name' +PASSWORD='your account password' +HOST='smtp.sendgrid.net' +``` - - Keep the client id and client secret we're going to use later on +## Integrations configuration + +Erxes app enables you to integrate with developer API and that means we can receive our integrated applications inbox messages directly to our erxes app's inbox. With the developer API, we have many more possibilities, like receiving notifications about page comment, page post feed etc. Learn how to integrate the Erxes platform into your applications as stated follows. - +### Gmail +Read and send messages, manage drafts and attachments, search threads and messages, work with labels, setup push notifications, and manage Gmail settings. + - Create **Google Cloud Platform project**, follow [this](#google) guide to create one + - Enable Gmail API [here](https://console.cloud.google.com/apis/library) -#### Add authorization callback - - Now we need to add authorization callback for our OAuth2 client - - Go to => Side menu => APIs and Services => Credentials and select an OAuth client you just created. + - + - + - SideMenu => APIs Services => OAuth Consent screen => Create - - erxes-integrations repo works on PORT 3400, so that for the test purpose you can add as follows + - ```shell - http://localhost:3400/gmaillogin - ``` + - Fill out rest of the form and Click on the Add scope button - + -#### Enable PubSub API - -In order to send and receive an email you need to enable the Cloud PubSub API. + - Search for Gmail API and select scopes as below - + - + -#### Create a service account - - Go to => IAM & Admin => Service Accounts => CREATE SERVICE ACCOUNTS - - Enter service account name and Create + - SideMenu => APIs & Services => Credentials => Create credentials => OAuth Client - + - - + - Select Web application and fill out rest of the form and click on the Create button - - You will automatically download the JSON file and replace it as `GOOGLE_APPLICATION_CREDENTIALS=./google_cred.json` + - - - Let's grant publish topic right to Gmail's service account. + - You will get your CLIENT_ID, CLIENT_SECRET, We are going to use these in the Erxes App system config later on - - Go to => Side menu => IAM & Admin => IAM => Add + - + - Now select your newly created OAuth client and add redirect URI for OAuth2 authorization - + - - In new members add the following value + - ```shell - gmail-api-push@system.gserviceaccount.com - ``` + - We also need to enable **Cloud Pub/Sub API** in order to receive our email as **real-time** - - In role select PubSub/Publisher - - + + + - Navigate to SideMenu => IAM & Admin => IAM - Basic Gmail integration setting has done. Your parameter configuration is as follows + - Click on the Add button and add grant publish right to **gmail-api-push@system.gserviceaccount.com** account - + - Select role Pub/Sub Publisher and click on the Save button - Now you need to connect your account to Erxes. + -**Erxes Gmail integration settings:** + - Now Let's config our Erxes app -1. Go to Erxes settings => App store -2. Click on Add **Gmail**. Connect your account. -3. Select your brand and click save. -4. Go to Setting=> Channel=> Add new channel=> Connect gmail integration. + - Navigate to Settings => System configs => General system config + - Add your **CLIENT_ID** to **GOOGLE_CLIENT_ID** and **CLIENT_SECRET** to **GOOGLE_CLIENT_SECRET** then click on the Save button + -### Common mail config -Common mail config enables you to your transaction emails will be sent by the specified email address(`FROM_EMAIL`). You can define whether transaction emails sent by AWS SES or other email services which is configured in custom mail service (`DEFAULT_EMAIL_SERVICE`). + - Final touch, navigate to Settings => System configs => Integrations config -**Configuration:** -- Go to Erxes Settings => System config => General System Config => Common mail config. + - Enable **USE DEFAULT GMAIL SERVICE** -``` -FROM_EMAIL='your email address' -DEFAULT_EMAIL_SERVICE='your configured email service name' + - Enter your **GOOGLE GMAIL TOPIC**, **GOOGLE GMAIL SUBSCRIPTION NAME** names as single string -``` + - - -### Custom mail service -Mail service enables you to delivering your transactional and marketing emails through the cloud-based email delivery platform. You can set any custom mail service in this fields. For example, Sendgrid custom mail service. Create your account in Sendgrid and fill it into the fields. - -**Configuration:** -- Go to Erxes Settings => System config => General System Config => Custom mail service. - -``` -MAIL_SERVICE_NAME='Sendgrid' -PORT='Sendgrid port id' -USERNAME='your account user name' -PASSWORD='your account password' -HOST='smtp.sendgrid.net' -``` - -## Integrations configuration - -Erxes app enables you to integrate with developer API and that means we can receive our integrated applications inbox messages directly to our erxes app's inbox. With the developer API, we have many more possibilities, like receiving notifications about page comment, page post feed etc. Learn how to integrate the Erxes platform into your applications as stated follows. + Now you are good to create your a Gmail integration ### Facebook - Erxes app can be integrated with facebook developer API and that means we can receive our Facebook pages' inbox messages directly to our erxes app's inbox. With the help of Facebook developer API we have many more possibilities, like receiving notifications about page comment, page post feed etc. There is an active development process going on this subject. #### Requirements: @@ -486,35 +490,26 @@ DAILY_END_POINT="your daily application's end point" Integrated video chat is used on the Erxes messenger widget. It is assumed that the one conversation can be activated one video call. -### Nylas -Learn how to integrate Nylas Accounts With Erxes. +## Nylas Integrations +Connect your application to every email, calendar, and contacts provider in the world. Learn how to integrate Nylas Accounts With Erxes. + +1. Create the Nylas account [here](https://dashboard.nylas.com/register) +2. Copy **NYLAS_CLIENT_ID**, **NYLAS_CLIENT_SECRET** from your app dashboard [here](https://dashboard.nylas.com/applications/) **Configuration:** - Go to Erxes Settings => System config => Integrations config => Nylas. -``` -NYLAS_CLIENT_ID='nylas account client id' -NYLAS_CLIENT_SECRET='nylas account client secret' -NYLAS_WEBHOOK_CALLBACK_URL='https://ID.ngrok.io/nylas/webhook' -``` - - -1. Create the Nylas account go to [website](https://dashboard.nylas.com/register) -2. After you created the Nylas account [here](https://dashboard.nylas.com/applications/), copy the variables as following. + -`NYLAS_CLIENT_ID`, nylas account client id -`NYLAS_CLIENT_SECRET`, nylas account client secret -`NYLAS_WEBHOOK_CALLBACK_URL`, insert nylas webhook call back URL +**For then test purpose you can use [ngrok](http://ngrok.io/) for your webhook** -3. In order to receive email and updates, we need to have endpoint for our webhook. - - Use ngrok service for erxes-integration repo as follows: - ```Shell - cd /path/to/erxes-integrations - ngrok http 3400 - ``` +```Shell +cd /path/to/erxes-integrations +ngrok http 3400 +``` - When you start erxes-integration repo webhook will automatically created according to your configuration. - #### Now we are ready to config our Nylas provider +When you start erxes-integration repo webhook will automatically created according to your configuration and you can find in your Nylas app [dashboard](https://dashboard.nylas.com/) +#### Now we are ready to config our Nylas provider @@ -555,29 +550,25 @@ Erxes app can be integrated with Gmail API by Nylas and that means we can receiv **Configuration** -According to the following steps, click the link and set up configurations. - -- [Create Google project.](/administrator/system-config#create-a-google-cloud-project) -- [Enable Gmail API.](/administrator/system-config#enable-gmail-api) -- [Configure OAuth Consent Screen.](/administrator/system-config#create-consent-screen) -- [Create an OAuth client.](/administrator/system-config#create-an-oauth-client) -- [Configure the Google parameters.](/administrator/system-config#configuration) + - Create a Google Cloud Project and config gmail for Nylas [click here](https://docs.nylas.com/docs/creating-a-google-project-for-dev) -In order to have Google OAuth token, add authorized callback (redirect URIs) to your google credentials. -- Select Google project -- Go to credentials from left side menu -- Select OAuth 2.0 client ID -- Add **Authorized JavaScript origins** + - In order to have Google OAuth token, add authorized callback (redirect URIs) to your google credentials. - `Add URI = https://nylas.com ` - -- Add **Authorized redirect URI** + - `Add URI = http://localhost:3400/nylas/oauth2/callback` + - Add following scopes in your OAuth consent screen - `Add URI = https://api.nylas.com/oauth/callback` + ```Shell + 'https://www.googleapis.com/auth/gmail.compose', + 'https://www.googleapis.com/auth/gmail.send' + 'https://www.googleapis.com/auth/gmail.readonly' + 'https://www.googleapis.com/auth/gmail.modify' + 'https://www.googleapis.com/auth/userinfo.email' + 'https://www.googleapis.com/auth/userinfo.profile' + ``` + - After you create the [Google service account (refer to the link)](/administrator/system-config#service-account) download JSON and replace with **erxes-integrations/google_cred.json** -After you create the [Google service account (refer to the link)](/administrator/system-config#create-a-service-account) download json and replace with google_cred.json. + Basic integration setting has done. Now you need to connect your account to Erxes. @@ -586,7 +577,7 @@ Basic integration setting has done. Now you need to connect your account to Erxe 1. Go to Erxes settings => App store 2. Click on Add Gmail by Nylas. Connect your account. 3. Select your brand and click save. -4. Go to Setting=> Channel=> Add new channel=> Connect gmail integration. +4. Go to Setting => Channel=> Add new channel=> Connect gmail integration. ### Yahoo In order to integrate the Yahoo you will need to generate app password for the Erxes, please follow below steps. diff --git a/docs/docs/developer/push-notifications.md b/docs/docs/developer/push-notifications.md index 24a6ba7ada8..74c7e18df4f 100644 --- a/docs/docs/developer/push-notifications.md +++ b/docs/docs/developer/push-notifications.md @@ -7,39 +7,28 @@ Push Notifications are an important feature to retain and re-engage users and mo First things first, The Firebase is part of the Google product so we will need a Google project. -1. [Go to the Google Cloud Console website.](https://console.cloud.google.com/) -2. [Create Google project.](/administrator/system-config#create-a-google-cloud-project) -3. Select the project. - -
- -4. We need to enable Firebase API for our Google project
-5. Select a newly created project
-6. Click on Library from the left side menu
-7. Search Firebase Cloud Messaging API and click on the enable button
-

-
-

+#### Configuration + - Create the Google Cloud Project [click here](/administrator/system-config#google) + - Enable Firebase Cloud Messaging API [click here](https://console.cloud.google.com/apis/library) + + - 8. Now we need a Firebase Project, let's create one
- 9.
Go to the Firebase Console website
- 10. Click on create a project, enter your firebase project name and continue, it might take a while.
-

-
-

- - 11. Go to project settings. -

-
-

- - 12. We are going to use Firebase Admin SDK to send a push notification. Thus we have to authenticate, in order to use Firebase feature, here comes the Firebase service account. Create the JSON key and download it. -

-

-

-

-

- - 13. Now we have the Firebase service account, copy all values of the file you downloaded and replace all values to erxes-api/google_cred.json file -

-

\ No newline at end of file + - Create the Firebase project [click here](https://console.firebase.google.com/) + + + + - Go to project settings. + + + + - We are going to use Firebase Admin SDK to send a push notification. Thus we have to authenticate, in order to use Firebase feature, here comes the Firebase service account. Create the JSON key and download it. + + + + + + + + - Now we have the Firebase service account, copy all values of the file you downloaded and replace all values to erxes-api/google_cred.json file + + \ No newline at end of file diff --git a/docs/docs/overview/getting-started.md b/docs/docs/overview/getting-started.md index b3b7485bf96..ecb1e72709c 100644 --- a/docs/docs/overview/getting-started.md +++ b/docs/docs/overview/getting-started.md @@ -49,7 +49,7 @@ title: Getting Started - Creating first user - Environment Variables -- Integrations +- System configs - Migration ### Developer's guide @@ -57,7 +57,7 @@ title: Getting Started - GraphQL API - Coding standards - Contributing -- Push Notification +- Push Notification - Android SDK - iOS SDK diff --git a/docs/docs/overview/integrations-overview.md b/docs/docs/overview/integrations-overview.md index db136560e28..067ad7a4a98 100644 --- a/docs/docs/overview/integrations-overview.md +++ b/docs/docs/overview/integrations-overview.md @@ -16,10 +16,14 @@ Erxes app can be integrated with Facebook developer API and that means we can re Erxes app can be integrated with Twitter developer API and that means we can receive our Twitter accounts DMs, Tweets directly into our Erxes app's inbox. -* [Gmail integration guide](../administrator/system-config#google) +* [Gmail integration guide](../administrator/system-config#gmail) Erxes app can be integrated with Gmail API and that means we can receive our gmail inbox messages directly to our Erxes app's inbox. +* [Google Cloud Storage](../administrator/system-config#google-cloud-storage) + +Cloud Storage provides worldwide, highly durable object storage that scales to exabytes of data. You can access data instantly from any storage class, integrate storage into your applications with a single unified API, and easily optimize price and performance. + * [AWS S3 integration](../administrator/system-config#aws-s3) Erxes app can be integrated with AWS S3 service and that means we can upload files and photos to our Erxes app. @@ -28,6 +32,6 @@ Erxes app can be integrated with AWS S3 service and that means we can upload fil Erxes app can be integrated with AWS SES service and that means we can send emails to customers we want. Moreover, we can monitor about how many emails have sent successfully, how many emails are opened by customers and so on. -* [Nylas integration](../administrator/system-config#nylas) +* [Nylas integration](../administrator/system-config#nylas-integrations) Erxes app can be integrated with Nylas service and that means we can send emails to customers we want. Moreover, we can monitor about how many emails have sent successfully, how many emails are opened by customers and so on. From fcdc0968931183b800a0d647d0ebe8a68901f1b9 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 20 Mar 2020 19:25:24 +0800 Subject: [PATCH 054/110] perf(knowledgebase): fix can not write rgb color or hex --- .../modules/knowledgeBase/components/knowledge/KnowledgeList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeList.tsx b/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeList.tsx index d845b126f89..99a77016970 100644 --- a/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeList.tsx +++ b/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeList.tsx @@ -69,6 +69,7 @@ class KnowledgeList extends React.Component { autoOpenKey="showKBAddModal" trigger={trigger} content={content} + enforceFocus={false} /> ); From b278c83a491cba65c804d4acb4dec0fd35bbd9c1 Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Sat, 21 Mar 2020 20:18:12 +0800 Subject: [PATCH 055/110] fix: on open reload search with values --- ui/src/modules/common/components/SelectWithSearch.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/modules/common/components/SelectWithSearch.tsx b/ui/src/modules/common/components/SelectWithSearch.tsx index 6cc55992244..187c00cdd8a 100644 --- a/ui/src/modules/common/components/SelectWithSearch.tsx +++ b/ui/src/modules/common/components/SelectWithSearch.tsx @@ -205,7 +205,10 @@ const withQuery = ({ customQuery }) => if (searchValue === 'reload') { return { context, - variables: { searchValue: '', ...filterParams }, + variables: { + ids: typeof values === 'string' ? [values] : values, + ...filterParams + }, fetchPolicy: 'network-only', notifyOnNetworkStatusChange: true }; From 8b60c9eb9b9854d2a513b9c9c25af94e8b5d18fd Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Sat, 21 Mar 2020 22:05:11 +0800 Subject: [PATCH 056/110] fix: refetch list in lead --- .../modules/leads/containers/CreateLead.tsx | 4 +-- ui/src/modules/leads/containers/List.tsx | 31 ++++++++++++++----- ui/src/modules/leads/routes.tsx | 7 +++-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/ui/src/modules/leads/containers/CreateLead.tsx b/ui/src/modules/leads/containers/CreateLead.tsx index e4a8582162b..3a8c16c3142 100644 --- a/ui/src/modules/leads/containers/CreateLead.tsx +++ b/ui/src/modules/leads/containers/CreateLead.tsx @@ -39,7 +39,6 @@ class CreateLeadContainer extends React.Component { render() { const { addIntegrationMutation, history } = this.props; - const afterFormDbSave = id => { this.setState({ isReadyToSaveForm: false }); @@ -57,9 +56,8 @@ class CreateLeadContainer extends React.Component { }) .then(() => { Alert.success('You successfully added a lead'); - history.push('/leads'); - this.setState({ isLoading: false }); + history.push({ pathname: '/leads', search: "?refetchList=true" }); }) .catch(error => { diff --git a/ui/src/modules/leads/containers/List.tsx b/ui/src/modules/leads/containers/List.tsx index 76310c57fdd..784675bd0c5 100755 --- a/ui/src/modules/leads/containers/List.tsx +++ b/ui/src/modules/leads/containers/List.tsx @@ -1,12 +1,14 @@ import gql from 'graphql-tag'; import * as compose from 'lodash.flowright'; import Bulk from 'modules/common/components/Bulk'; +import { IRouterProps } from 'modules/common/types'; import { Alert, confirm, withProps } from 'modules/common/utils'; import { generatePaginationParams } from 'modules/common/utils/router'; import { mutations as integrationMutations } from 'modules/settings/integrations/graphql/index'; import { ArchiveIntegrationResponse } from 'modules/settings/integrations/types'; import React from 'react'; import { graphql } from 'react-apollo'; +import routerUtils from '../../common/utils/router'; import { TagsQueryResponse } from '../../tags/types'; import List from '../components/List'; import { mutations, queries } from '../graphql'; @@ -27,9 +29,27 @@ type FinalProps = { tagsQuery: TagsQueryResponse; } & RemoveMutationResponse & ArchiveIntegrationResponse & + IRouterProps & Props; class ListContainer extends React.Component { + componentDidMount() { + const { history } = this.props; + + const shouldRefetchList = routerUtils.getParam(history, 'refetchList'); + + if (shouldRefetchList) { + this.refetch(); + } + } + + refetch = () => { + const { integrationsQuery, integrationsTotalCountQuery } = this.props; + + integrationsQuery.refetch(); + integrationsTotalCountQuery.refetch(); + } + render() { const { integrationsQuery, @@ -47,11 +67,6 @@ class ListContainer extends React.Component { const integrations = integrationsQuery.integrations || []; - const refetch = () => { - integrationsQuery.refetch(); - integrationsTotalCountQuery.refetch(); - }; - const remove = (integrationId: string) => { const message = ` If you remove a pop ups, then all related conversations, customers will also be removed. @@ -64,7 +79,7 @@ class ListContainer extends React.Component { }) .then(() => { // refresh queries - refetch(); + this.refetch(); Alert.success('You successfully deleted a pop ups.'); }) @@ -90,7 +105,7 @@ class ListContainer extends React.Component { Alert.success('Pop ups has been archived.'); } - refetch(); + this.refetch(); }) .catch((e: Error) => { Alert.error(e.message); @@ -113,7 +128,7 @@ class ListContainer extends React.Component { return ; }; - return ; + return ; } } diff --git a/ui/src/modules/leads/routes.tsx b/ui/src/modules/leads/routes.tsx index 8614ef60a68..69f00cb7ccd 100644 --- a/ui/src/modules/leads/routes.tsx +++ b/ui/src/modules/leads/routes.tsx @@ -15,9 +15,12 @@ const List = asyncComponent(() => import(/* webpackChunkName: "List - Form" */ './containers/List') ); -const forms = ({ location }) => { +const forms = (history) => { + const { location } = history; + const queryParams = queryString.parse(location.search); - return ; + + return ; }; const createLead = () => { From 44161bf7784dfca9698ac5863e744e2446cb332a Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Sat, 21 Mar 2020 22:05:44 +0800 Subject: [PATCH 057/110] remove unused query --- ui/src/modules/leads/containers/CreateLead.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/src/modules/leads/containers/CreateLead.tsx b/ui/src/modules/leads/containers/CreateLead.tsx index 3a8c16c3142..f7445a7c6d2 100644 --- a/ui/src/modules/leads/containers/CreateLead.tsx +++ b/ui/src/modules/leads/containers/CreateLead.tsx @@ -92,10 +92,7 @@ export default withProps<{}>( AddIntegrationMutationResponse, AddIntegrationMutationVariables >(gql(mutations.integrationsCreateLeadIntegration), { - name: 'addIntegrationMutation', - options: { - refetchQueries: ['leadIntegrations', 'leadIntegrationCounts'] - } + name: 'addIntegrationMutation' }) )(withRouter(CreateLeadContainer)) ); From 7a45f5be8abcbdfd68eda323531b7c2b90552d78 Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Sat, 21 Mar 2020 22:37:46 +0800 Subject: [PATCH 058/110] fix: update engage list after create and edit --- .../modules/engage/containers/MessageList.tsx | 20 +++++++++++++++++- .../engage/containers/withFormMutations.tsx | 21 +++++++++---------- ui/src/modules/engage/routes.tsx | 6 +++++- .../modules/leads/containers/CreateLead.tsx | 2 +- ui/src/modules/leads/containers/EditLead.tsx | 4 +--- ui/src/modules/leads/containers/List.tsx | 2 +- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/ui/src/modules/engage/containers/MessageList.tsx b/ui/src/modules/engage/containers/MessageList.tsx index eff108151e0..fe54bd836ee 100755 --- a/ui/src/modules/engage/containers/MessageList.tsx +++ b/ui/src/modules/engage/containers/MessageList.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { graphql } from 'react-apollo'; import { withRouter } from 'react-router'; import { withProps } from '../../common/utils'; +import routerUtils from '../../common/utils/router'; import MessageList from '../components/MessageList'; import { queries } from '../graphql'; import { @@ -25,7 +26,7 @@ type Props = { type FinalProps = { engageMessagesQuery: EngageMessagesQueryResponse; engageMessagesTotalCountQuery: EngageMessagesTotalCountQueryResponse; -} & Props; +} & Props & IRouterProps; type State = { bulk: any[]; @@ -42,6 +43,23 @@ class MessageListContainer extends React.Component { }; } + componentDidMount() { + const { history } = this.props; + + const shouldRefetchList = routerUtils.getParam(history, 'engageRefetchList'); + + if (shouldRefetchList) { + this.refetch(); + } + } + + refetch = () => { + const { engageMessagesQuery, engageMessagesTotalCountQuery } = this.props; + + engageMessagesQuery.refetch() + engageMessagesTotalCountQuery.refetch() + }; + render() { const { queryParams, diff --git a/ui/src/modules/engage/containers/withFormMutations.tsx b/ui/src/modules/engage/containers/withFormMutations.tsx index fc6745e23c5..df681e8e5f6 100644 --- a/ui/src/modules/engage/containers/withFormMutations.tsx +++ b/ui/src/modules/engage/containers/withFormMutations.tsx @@ -61,9 +61,8 @@ function withSaveAndEdit(Component) { }) .then(() => { Alert.success(msg); - history.push('/engage'); - this.setState({ isLoading: false }); + history.push({ pathname: '/engage', search: '?engageRefetchList=true' }); }) .catch(error => { Alert.error(error.message); @@ -169,11 +168,7 @@ function withSaveAndEdit(Component) { { name: 'addMutation', options: { - refetchQueries: [ - ...crudMutationsOptions().refetchQueries, - 'engageMessageDetail', - 'activityLogs' - ] + refetchQueries: engageRefetchQueries({}) } } ), @@ -182,10 +177,7 @@ function withSaveAndEdit(Component) { { name: 'editMutation', options: { - refetchQueries: [ - ...crudMutationsOptions().refetchQueries, - 'engageMessageDetail' - ] + refetchQueries: engageRefetchQueries({ isEdit: true }) } } ) @@ -193,4 +185,11 @@ function withSaveAndEdit(Component) { ); } + +export const engageRefetchQueries = ({ isEdit }: { isEdit?: boolean }): string[] => [ + ...crudMutationsOptions().refetchQueries, + ...isEdit ? ['activityLogs'] : [], + 'engageMessageDetail', +] + export default withSaveAndEdit; diff --git a/ui/src/modules/engage/routes.tsx b/ui/src/modules/engage/routes.tsx index 4f2407b534d..76161285124 100755 --- a/ui/src/modules/engage/routes.tsx +++ b/ui/src/modules/engage/routes.tsx @@ -15,6 +15,10 @@ const EmailStatistics = asyncComponent(() => import(/* webpackChunkName: "EmailStatistics - Engage" */ './containers/EmailStatistics') ); +const engageList = history => { + return ; +}; + const createForm = ({ location }) => { const queryParams = queryString.parse(location.search); return ; @@ -35,7 +39,7 @@ const routes = () => { key="/engage/home" exact={true} path="/engage" - component={MessageList} + component={engageList} /> { .then(() => { Alert.success('You successfully added a lead'); - history.push({ pathname: '/leads', search: "?refetchList=true" }); + history.push({ pathname: '/leads', search: "?popUpRefetchList=true" }); }) .catch(error => { diff --git a/ui/src/modules/leads/containers/EditLead.tsx b/ui/src/modules/leads/containers/EditLead.tsx index 71f7ed6a789..80f75cfb3b0 100644 --- a/ui/src/modules/leads/containers/EditLead.tsx +++ b/ui/src/modules/leads/containers/EditLead.tsx @@ -76,9 +76,7 @@ class EditLeadContainer extends React.Component { .then(() => { Alert.success('You successfully updated a lead'); - history.push('/leads'); - - this.setState({ isReadyToSaveForm: false, isLoading: false }); + history.push({ pathname: '/leads', search: "?popUpRefetchList=true" }); }) .catch(error => { diff --git a/ui/src/modules/leads/containers/List.tsx b/ui/src/modules/leads/containers/List.tsx index 784675bd0c5..93e3ef1616b 100755 --- a/ui/src/modules/leads/containers/List.tsx +++ b/ui/src/modules/leads/containers/List.tsx @@ -36,7 +36,7 @@ class ListContainer extends React.Component { componentDidMount() { const { history } = this.props; - const shouldRefetchList = routerUtils.getParam(history, 'refetchList'); + const shouldRefetchList = routerUtils.getParam(history, 'popUpRefetchList'); if (shouldRefetchList) { this.refetch(); From 21818f46d0d21f1505812c3fee7115e34976751c Mon Sep 17 00:00:00 2001 From: Jason-2020 <58852063+Jason-2020@users.noreply.github.com> Date: Sat, 21 Mar 2020 14:11:24 -0500 Subject: [PATCH 059/110] Our rocketchat server is renamed to community.erxes.io Our rocketchat server is renamed to community.erxes.io. Old domain rocketchat.erxes.io will be permanently redirecting traffic to our new domain for a while. Rocketchat desktop or mobile app users have to reconfigure their server settings. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b896c1c1dd1..ec02d25f254 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ erxes is an open source growth marketing platform. Marketing, sales, and customer service platform designed to help your business attract more engaged customers. Replace Hubspot with the mission and community-driven ecosystem. -Live demo | Join us on RocketChat +Live demo | Join us on RocketChat [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ed8c207f4351446b8ace7a323630889f)](https://www.codacy.com/app/erxes/erxes) [![Codeclimate Badge](https://api.codeclimate.com/v1/badges/693e2ffc40bc2601630d/maintainability)](https://codeclimate.com/github/erxes/erxes/maintainability) From 83ae766d5aad4f3e8bba69c0a44ff28a53de44dd Mon Sep 17 00:00:00 2001 From: Jason-2020 <58852063+Jason-2020@users.noreply.github.com> Date: Sat, 21 Mar 2020 14:41:00 -0500 Subject: [PATCH 060/110] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec02d25f254..81dc3527b9f 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,4 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## License -GNU General Public License v3.0 +GNU General Public License v3.0 From 06b418042221f19ffe859af8debad87111e3197d Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Mon, 23 Mar 2020 11:26:32 +0800 Subject: [PATCH 061/110] perf(teaminbox): convert text to link (close #1820) --- README.md | 2 +- .../conversation/messages/SimpleMessage.tsx | 5 ++++- ui/src/modules/inbox/utils.ts | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6bc773b469c..59135dfbb58 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ erxes is an open source growth marketing platform. Marketing, sales, and customer service platform designed to help your business attract more engaged customers. Replace Hubspot with the mission and community-driven ecosystem. -Live demo | Join us on RocketChat +Live demo | Join us on RocketChat [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ed8c207f4351446b8ace7a323630889f)](https://www.codacy.com/app/erxes/erxes) [![Codeclimate Badge](https://api.codeclimate.com/v1/badges/693e2ffc40bc2601630d/maintainability)](https://codeclimate.com/github/erxes/erxes/maintainability) diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/SimpleMessage.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/SimpleMessage.tsx index e7d95601b9a..fb40ff20252 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/SimpleMessage.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/SimpleMessage.tsx @@ -6,6 +6,7 @@ import NameCard from 'modules/common/components/nameCard/NameCard'; import TextDivider from 'modules/common/components/TextDivider'; import Tip from 'modules/common/components/Tip'; import { __ } from 'modules/common/utils'; +import { urlify } from 'modules/inbox/utils'; import React from 'react'; import xss from 'xss'; import { IMessage } from '../../../../../types'; @@ -125,7 +126,9 @@ export default class SimpleMessage extends React.Component { return ( <> - + {this.renderAttachment(hasAttachment)} diff --git a/ui/src/modules/inbox/utils.ts b/ui/src/modules/inbox/utils.ts index 748969611ca..d67c84ab948 100644 --- a/ui/src/modules/inbox/utils.ts +++ b/ui/src/modules/inbox/utils.ts @@ -69,3 +69,24 @@ export const extractEmail = (str?: string) => { return emails.join(' '); }; + +export const urlify = (text: string) => { + let content = text; + const urlRegex = /(\b((https?|ftp|file):\/\/)?(www\.)[-A-Z0-9+&@#%?=~_|!:,.;]*[-A-Z0-9+&@#%=~_|])/gi; + + if (urlRegex) { + content = text.replace(urlRegex, url => { + if (url.includes('http://') || url.includes('https://')) { + return '' + url + ''; + } + + return '' + url + ''; + }); + } + + if (text.includes(' You may add article in each category. -You can use Knowledgebase script to install on your webpage or inside the messenger or both. There are different installation case of Knowledgebase form. Go to [instruction](https://docs.erxes.io/docs/user/script-install). +You can use Knowledgebase script to install on your webpage or inside the messenger or both. There are different installation case of Knowledgebase form. Go to [instruction](https://docs.erxes.io/user/script-install). diff --git a/docs/docs/user/popups.md b/docs/docs/user/popups.md index 9809c16b498..3cc70cb377c 100644 --- a/docs/docs/user/popups.md +++ b/docs/docs/user/popups.md @@ -134,6 +134,6 @@ Turn regular visitors into qualified pop ups by capturing them with a customizab Full Preview of Created Pop up in Desktop, Tablet and Mobile. You can use Pop-Ups script to install on your webpage or inside the messenger or both. There are different installation case of Pop-Up form. -Go to [instruction](https://docs.erxes.io/docs/user/script-install). +Go to [instruction](https://docs.erxes.io/user/script-install). diff --git a/docs/docs/user/script-install.md b/docs/docs/user/script-install.md index 38577bee4f6..f9b7d46c357 100644 --- a/docs/docs/user/script-install.md +++ b/docs/docs/user/script-install.md @@ -820,7 +820,7 @@ This is the script install instruction of Pop-Ups form with Messenger which cont 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) 2. Go to Pop-Ups menu from left sidebar (see the below figure). 3. Click on the install code button from the right side (see the below figure). @@ -862,7 +862,7 @@ This is the script install instruction of Knowledgebase form with Messenger whic 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) 2. Go to Knowledge Base menu from left sidebar (see the below figure). @@ -900,7 +900,7 @@ This is the script install instruction of Pop-Ups, Knowledgebase form with Messe 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) 2. Go to Pop-Ups menu from left sidebar (see the below figure). 3. Click on the install code button from the right side (see the below figure). @@ -1246,7 +1246,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) 2. Go to Pop-Ups menu from left sidebar (see the below figure). 3. Click on the install code button from the right side (see the below figure). @@ -1317,7 +1317,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) 2. Go to Knowledge Base menu from left sidebar (see the below figure). @@ -1371,7 +1371,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. In this combination, first you need to follow the instruction of (M+P+K). Click to the link and check reference. - [**(M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) + [**(M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-1) 2. Go to Pop-Ups menu from left sidebar (see the below figure). 3. Click on the install code button from the right side (see the below figure). @@ -1489,14 +1489,14 @@ This is the script install instruction of Messenger contains Pop-Ups form or Mes 1. Go to Advanced combination installation => Web messenger + Pop-Ups (or Knowledgebase). -[**Advanced combination installation**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups-or-knowledgebase) +[**Advanced combination installation**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups-or-knowledgebase) 2. Then follow steps number from 1 to 6 of the instruction for Web messenger + Pop-Ups (or Knowledgebase). #### Step 2: Copy script and paste the code 3. After that, follow the instruction of Erxes script manager => Web messenger. The messenger, you have to select which you created messenger. -[**Erxes script installation**](https://docs.erxes.io/docs/user/script-install#web-messenger-2) +[**Erxes script installation**](https://docs.erxes.io/user/script-install#web-messenger-2) #### Step 3: Result @@ -1518,7 +1518,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. Go to Advanced combination installation => Web messenger + Pop-Ups + Knowledgebase. -[**Advanced combination installation**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) +[**Advanced combination installation**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk) 2. Then follow steps number from 1 to 9 of the instruction for Web messenger + Pop-Ups + Knowledgebase. @@ -1526,7 +1526,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 3. After that, follow the instruction of Erxes script manager => Web messenger. The messenger, you have to select which you created messenger. -[**Erxes script installation**](https://docs.erxes.io/docs/user/script-install#web-messenger-2) +[**Erxes script installation**](https://docs.erxes.io/user/script-install#web-messenger-2) #### Step 3: Result @@ -1540,7 +1540,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. In this combination, first you need to follow the Erxes script instruction of (M+P+K). Click to the link and check reference. - [**Erxes Script (M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-2) + [**Erxes Script (M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-2) 2. Go to Settings menu => Script manager (see the below figure). @@ -1596,7 +1596,7 @@ This is the install instruction of messenger based popup and knowledgebase combi 1. In this combination, first you need to follow the Erxes script instruction of (M+P+K). Click to the link and check reference. - [**Erxes Script (M + P + K)**](https://docs.erxes.io/docs/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-2) + [**Erxes Script (M + P + K)**](https://docs.erxes.io/user/script-install#web-messenger--pop-ups--knowledgebase-mpk-2) 2. Go to Settings menu => Script manager (see the below figure). From d725c2bbc33495d3d69bf5eb4c76fafcfe8fcbdd Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Mon, 23 Mar 2020 23:14:26 +0800 Subject: [PATCH 063/110] fix: show clear filter button only for search filters --- .../boards/containers/MainActionBar.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ui/src/modules/boards/containers/MainActionBar.tsx b/ui/src/modules/boards/containers/MainActionBar.tsx index cbd7a3b275b..6a121621a65 100644 --- a/ui/src/modules/boards/containers/MainActionBar.tsx +++ b/ui/src/modules/boards/containers/MainActionBar.tsx @@ -25,6 +25,18 @@ type FinalProps = { boardDetailQuery?: BoardDetailQueryResponse; } & Props; +const FILTER_PARAMS = [ + 'search', + 'userIds', + 'priority', + 'assignedUserIds', + 'labelIds', + 'productIds', + 'companyIds', + 'customerIds', + 'closeDateType', +]; + const generateQueryParams = ({ location }) => { return queryString.parse(location.search); }; @@ -61,8 +73,10 @@ class Main extends React.Component { isFiltered = (): boolean => { const params = generateQueryParams(this.props.history); - if (Object.keys(params).length > 2) { - return true; + for (const param in params) { + if (FILTER_PARAMS.includes(param)) { + return true; + } } return false; From c8b6dfd7f67c454b36acff41f5b8518ccef2e89a Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Mon, 23 Mar 2020 23:49:45 +0800 Subject: [PATCH 064/110] improve upload file mime type chooser (#1827) close #1826 --- .../general/components/GeneralSettings.tsx | 63 ++++-- ui/src/modules/settings/general/constants.ts | 183 ++++++++++++++++++ 2 files changed, 230 insertions(+), 16 deletions(-) diff --git a/ui/src/modules/settings/general/components/GeneralSettings.tsx b/ui/src/modules/settings/general/components/GeneralSettings.tsx index 90ac07f50f9..7e9d7debf27 100755 --- a/ui/src/modules/settings/general/components/GeneralSettings.tsx +++ b/ui/src/modules/settings/general/components/GeneralSettings.tsx @@ -11,7 +11,7 @@ import Wrapper from 'modules/layout/components/Wrapper'; import React from 'react'; import Select from 'react-select-plus'; import { ContentBox } from '../../styles'; -import { FILE_SYSTEM_TYPES, KEY_LABELS, LANGUAGES, MEASUREMENTS, SERVICE_TYPES } from '../constants'; +import { FILE_MIME_TYPES, FILE_SYSTEM_TYPES, KEY_LABELS, LANGUAGES, MEASUREMENTS, SERVICE_TYPES } from '../constants'; import { IConfigsMap } from '../types'; import Header from './Header'; import Sidebar from './Sidebar'; @@ -50,14 +50,20 @@ class GeneralSettings extends React.Component { onChangeConfig = (code: string, value) => { const { configsMap } = this.state; - + configsMap[code] = value; this.setState({ configsMap }); }; onChangeMultiCombo = (code: string, values) => { - this.onChangeConfig(code, values.map(el => el.value)); + let value = values; + + if (Array.isArray(values)) { + value = values.map(el => el.value); + } + + this.onChangeConfig(code, value); }; onChangeSingleCombo = (code: string, obj) => { @@ -88,7 +94,7 @@ class GeneralSettings extends React.Component { } render() { - const { configsMap } = this.state; + const { configsMap, language } = this.state; const breadcrumb = [ { title: __('Settings'), link: '/settings' }, @@ -106,6 +112,9 @@ class GeneralSettings extends React.Component { ); + const mimeTypeOptions = FILE_MIME_TYPES.map(item => ({ value: item.value, label: `${item.label} (${item.extension})` })); + const mimeTypeDesc = 'Comma-separated list of media types. Leave it blank for accepting all media types'; + const content = ( @@ -113,7 +122,7 @@ class GeneralSettings extends React.Component { Language + + + {KEY_LABELS.WIDGETS_UPLOAD_FILE_TYPES} + {mimeTypeDesc &&

{__(mimeTypeDesc)}

} + { {__("More: Create or find your Google Cloud Storage bucket")}
- + Google Bucket Name {this.renderItem('GOOGLE_CLOUD_STORAGE_BUCKET')} @@ -211,19 +242,19 @@ class GeneralSettings extends React.Component {
- {this.renderItem('GOOGLE_PROJECT_ID')} - {this.renderItem('GOOGLE_APPLICATION_CREDENTIALS')} - {this.renderItem('GOOGLE_CLIENT_ID')} - {this.renderItem('GOOGLE_CLIENT_SECRET')} + {this.renderItem('GOOGLE_PROJECT_ID')} + {this.renderItem('GOOGLE_APPLICATION_CREDENTIALS')} + {this.renderItem('GOOGLE_CLIENT_ID')} + {this.renderItem('GOOGLE_CLIENT_SECRET')} - + {__("More: Understanding Email Settings")} - + {this.renderItem('COMPANY_EMAIL_FROM')} {this.renderItem('DEFAULT_EMAIL_SERVICE', 'Write your default email service name. Default email service is SES')} @@ -237,7 +268,7 @@ class GeneralSettings extends React.Component { {this.renderItem('MAIL_USER')} {this.renderItem('MAIL_PASS')} {this.renderItem('MAIL_HOST')} - + ); diff --git a/ui/src/modules/settings/general/constants.ts b/ui/src/modules/settings/general/constants.ts index decd265242d..fe5c2640ed6 100755 --- a/ui/src/modules/settings/general/constants.ts +++ b/ui/src/modules/settings/general/constants.ts @@ -114,3 +114,186 @@ export const KEY_LABELS = { DAILY_API_KEY: 'Daily api key', DAILY_END_POINT: 'Daily end point' }; + +export const FILE_MIME_TYPES = [ + // images + { + value: 'image/gif', + label: 'Graphics Interchange Format', + extension: '.gif' + }, + { + value: 'image/vnd.microsoft.icon', + label: 'Icon format', + extension: '.ico' + }, + { + value: 'image/tiff', + label: 'Tagged Image File Format', + extension: '.tif' + }, + { + value: 'image/jpeg', + label: 'JPEG image', + extension: '.jpeg' + }, + { + value: 'image/bmp', + label: 'Windows OS/2 Bitmap Graphics', + extension: '.bmp' + }, + { + value: 'image/png', + label: 'Portable Network Graphics', + extension: '.png' + }, + { + value: 'image/svg+xml', + label: 'Scalable Vector Graphics', + extension: '.svg' + }, + { + value: 'image/webp', + label: 'WEBP image', + extension: '.webp' + }, + { + value: 'image/heic', + label: 'High Efficiency Image Coding', + extension: '.heic' + }, + { + value: 'image/heif', + label: 'High Efficiency Image Format', + extension: '.heif' + }, + // documents + { + value: 'text/csv', + label: 'Comma-separated values', + extension: '.csv' + }, + { + value: 'application/msword', + label: 'Microsoft Word', + extension: '.doc' + }, + { + value: + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + label: 'Microsoft Word (OpenXML) ', + extension: '.docx' + }, + { + value: 'application/vnd.ms-excel', + label: 'Microsoft Excel', + extension: 'xls' + }, + { + value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + label: 'Microsoft Excel OpenXML', + extension: 'xlsx' + }, + { + value: 'application/vnd.ms-powerpoint', + label: 'Microsoft PowerPoint', + extension: '.ppt' + }, + { + value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + label: 'Microsoft PowerPoint (OpenXML)', + extension: '.pptx' + }, + { + value: 'application/vnd.oasis.opendocument.presentation', + label: 'OpenDocument presentation document', + extension: '.odp' + }, + { + value: 'application/vnd.oasis.opendocument.spreadsheet', + label: 'OpenDocument spreadsheet document', + extension: '.ods' + }, + { + value: 'application/vnd.oasis.opendocument.text', + label: 'OpenDocument text document', + extension: '.odt' + }, + { + value: 'application/pdf', + label: 'Adobe Portable Document Format', + extension: '.pdf' + }, + { + value: 'application/rtf', + label: 'Rich Text Format', + extension: '.rtf' + }, + { + value: 'text/plain', + label: 'Plain text', + extension: '.txt' + }, + // media + { + value: 'audio/aac', + label: 'AAC audio', + extension: '.aac' + }, + { + value: 'audio/mpeg', + label: 'MP3 audio', + extension: '.mp3' + }, + { + value: 'audio/ogg', + label: 'OGG audio', + extension: '.oga' + }, + { + value: 'audio/3gpp', + label: '3GPP audio/video container', + extension: '.3gpp' + }, + { + value: 'audio/3gpp2', + label: '3GPP audio/video container', + extension: '.3gpp2' + }, + { + value: 'video/mpeg', + label: 'MPEG video', + extension: '.mpeg' + }, + { + value: 'video/ogg', + label: 'OGG video', + extension: '.ogv' + }, + { + value: 'video/mp4', + label: 'MP4 video', + extension: '.mp4' + }, + // archives + { + value: 'application/vnd.rar', + label: 'RAR archive', + extension: '.rar' + }, + { + value: 'application/x-tar', + label: 'Tape archive', + extension: '.tar' + }, + { + value: 'application/x-7z-compressed', + label: '7-zip archive', + extension: '.7z' + }, + { + value: 'application/gzip', + label: 'GZip Compressed Archive', + extension: '.gz' + } +]; From 6ddd60ac3f7738058ec2cb02c61d4664f2842ecd Mon Sep 17 00:00:00 2001 From: Munkh-Orgil Date: Tue, 24 Mar 2020 00:12:41 +0800 Subject: [PATCH 065/110] ref: component to function component in entry --- .../integrations/components/store/Entry.tsx | 409 +++++++++--------- 1 file changed, 202 insertions(+), 207 deletions(-) diff --git a/ui/src/modules/settings/integrations/components/store/Entry.tsx b/ui/src/modules/settings/integrations/components/store/Entry.tsx index 5cb039c5a48..e0f7ebaa180 100644 --- a/ui/src/modules/settings/integrations/components/store/Entry.tsx +++ b/ui/src/modules/settings/integrations/components/store/Entry.tsx @@ -15,267 +15,262 @@ import Twitter from '../../containers/twitter/Twitter'; import Website from '../../containers/website/Form'; import { Box, IntegrationItem, Ribbon, Type } from './styles'; +type TotalCount = { + messenger: number; + form: number; + facebook: number; + callpro: number; + chatfuel: number; + gmail: number; + imap: number; + office365: number; + outlook: number; + yahoo: number; +}; + type Props = { integration: any; getClassName: (selectedKind: string) => string; toggleBox: (kind: string) => void; messengerAppsCount?: number; queryParams: any; - totalCount: { - messenger: number; - form: number; - facebook: number; - callpro: number; - chatfuel: number; - gmail: number; - imap: number; - office365: number; - outlook: number; - yahoo: number; - }; + totalCount: TotalCount }; -class Entry extends React.Component { - getCount = kind => { - const { totalCount, messengerAppsCount } = this.props; - const countByKind = totalCount[kind]; +function getCount(kind: string, totalCount: TotalCount, messengerAppsCount?: number) { + const countByKind = totalCount[kind]; + + if (typeof messengerAppsCount === 'number') { + return ({messengerAppsCount}); + } - if (typeof messengerAppsCount === 'number') { - return ({messengerAppsCount}); - } + if (typeof countByKind === 'undefined') { + return null; + } - if (typeof countByKind === 'undefined') { - return null; - } + return ({countByKind}); +} - return ({countByKind}); - }; +function renderType(type: string) { + if (!type) { + return null; + } - renderCreate(createUrl, createModal) { - if (!createUrl && !createModal) { - return null; - } - - if (createModal === KIND_CHOICES.FACEBOOK_MESSENGER) { - const trigger =
+ {__('Add')}
; - - const content = props => ( - - ); - - return ( - - ); - } - - if (createModal === KIND_CHOICES.FACEBOOK_POST) { - const trigger =
+ {__('Add')}
; - - const content = props => ( - - ); - - return ( - - ); - } - - if (!createUrl && !createModal) { - return null; - } - - if (createModal === 'lead') { - const trigger =
+ {__('Add')}
; - - const content = props => ; - - return ( - - ); - } - - if (createModal === 'knowledgeBase') { - const trigger =
+ {__('Add')}
; - - const content = props => ; - - return ( - - ); - } + return ( + + {__('Works with messenger')} + + ); +} - if (createModal === 'website') { - const trigger =
+ {__('Add')}
; - const content = props => ; +function renderCreate(createUrl, createModal) { + if (!createUrl && !createModal) { + return null; + } - return ( - - ); - } + if (createModal === KIND_CHOICES.FACEBOOK_MESSENGER) { + const trigger =
+ {__('Add')}
; - if (createModal === 'callpro') { - const trigger =
+ {'Add'}
; + const content = props => ( + + ); - const content = props => ; + return ( + + ); + } - return ( - - ); - } + if (createModal === KIND_CHOICES.FACEBOOK_POST) { + const trigger =
+ {__('Add')}
; - if (createModal === 'chatfuel') { - const trigger =
+ {'Add'}
; + const content = props => ( + + ); + + return ( + + ); + } + + if (createModal === 'lead') { + const trigger =
+ {__('Add')}
; + + const content = props => ; + + return ( + + ); + } + + if (createModal === 'knowledgeBase') { + const trigger =
+ {__('Add')}
; + + const content = props => ; + + return ( + + ); + } - const content = props => ; + if (createModal === 'website') { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.NYLAS_OFFICE365) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === 'callpro') { + const trigger =
+ {'Add'}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.NYLAS_IMAP) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === 'chatfuel') { + const trigger =
+ {'Add'}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.NYLAS_GMAIL) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === KIND_CHOICES.NYLAS_OFFICE365) { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.NYLAS_OUTLOOK) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === KIND_CHOICES.NYLAS_IMAP) { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.NYLAS_YAHOO) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === KIND_CHOICES.NYLAS_GMAIL) { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - if (createModal === KIND_CHOICES.GMAIL) { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === KIND_CHOICES.NYLAS_OUTLOOK) { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - if (createModal === 'twitter') { - const trigger =
+ {__('Add')}
; + return ( + + ); + } - const content = props => ; + if (createModal === KIND_CHOICES.NYLAS_YAHOO) { + const trigger =
+ {__('Add')}
; - return ( - - ); - } + const content = props => ; - return + {__('Add')}; + return ( + + ); } - renderType = type => { - if (!type) { - return null; - } + if (createModal === KIND_CHOICES.GMAIL) { + const trigger =
+ {__('Add')}
; + + const content = props => ; return ( - - {__('Works with messenger')} - + ); - }; + } - boxOnClick = () => { - return this.props.toggleBox(this.props.integration.kind); - }; + if (createModal === 'twitter') { + const trigger =
+ {__('Add')}
; - render() { - const { integration, getClassName } = this.props; - const { createUrl, createModal } = integration; + const content = props => ; return ( - - - logo -
- {integration.name} {this.getCount(integration.kind)} -
-

- {integration.description} - {this.renderType(integration.inMessenger)} -

- {!integration.isAvailable && ( - - Coming soon - - )} -
- {this.renderCreate(createUrl, createModal)} -
+ ); } + + return + {__('Add')}; + }; + +function Entry({ integration, getClassName, toggleBox, messengerAppsCount, totalCount }: Props) { + const { kind } = integration; + + const boxOnClick = () => toggleBox(kind); + + const { createUrl, createModal } = integration; + + return ( + + + logo +
+ {integration.name} {getCount(kind, totalCount, messengerAppsCount)} +
+

+ {integration.description} + {renderType(integration.inMessenger)} +

+ {!integration.isAvailable && ( + + Coming soon + + )} +
+ {renderCreate(createUrl, createModal)} +
+ ); } export default Entry; From e949f5c88bb985eee57e09a4a4ef8cba1445906d Mon Sep 17 00:00:00 2001 From: munkhjin Date: Tue, 24 Mar 2020 11:59:20 +0800 Subject: [PATCH 066/110] show only name when no code for product chooser --- .../modules/deals/containers/product/ProductChooser.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/src/modules/deals/containers/product/ProductChooser.tsx b/ui/src/modules/deals/containers/product/ProductChooser.tsx index 98260c1abbe..fa44b00b7da 100644 --- a/ui/src/modules/deals/containers/product/ProductChooser.tsx +++ b/ui/src/modules/deals/containers/product/ProductChooser.tsx @@ -85,8 +85,13 @@ class ProductChooser extends React.Component { data: { name: data.name, datas: data.products }, search: this.search, title: 'Product', - renderName: (product: IProduct) => - product.code.concat(' - ', product.name), + renderName: (product: IProduct) => { + if (product.code) { + return product.code.concat(' - ', product.name); + } + + return product.name; + }, renderForm: ({ closeModal }: { closeModal: () => void }) => ( ), From 346fbe9b143a3ee7d4b216c42740ff1276fa0bf0 Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Tue, 24 Mar 2020 12:30:43 +0800 Subject: [PATCH 067/110] Fix doc menu (#1830) --- docs/website/siteConfig.js | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/docs/website/siteConfig.js b/docs/website/siteConfig.js index 51ea194e46c..8309214ca9e 100644 --- a/docs/website/siteConfig.js +++ b/docs/website/siteConfig.js @@ -23,7 +23,7 @@ const users = [{ const siteConfig = { title: 'erxes', // Title for your website. tagline: 'Documentation', - url: 'https://docs.erxes.io', // Your website URL + url: 'https://erxes.io', // Your website URL baseUrl: '/', // Base URL for your project */ editUrl: 'https://github.com/erxes/erxes/edit/develop/docs/docs/', cname: 'docs.erxes.io', @@ -49,33 +49,13 @@ const siteConfig = { // For no header links in the top nav bar -> headerLinks: [], headerLinks: [{ - href: 'https://erxes.io/growthHacking', - label: 'Products', + href: 'https://erxes.io/signin', + label: 'Sign in', external: true }, { - href: 'https://erxes.io/install', - label: 'Install', - external: true - }, - { - href: 'https://erxes.io/blog/customer-stories', - label: 'Case Stuies', - external: true - }, - { - href: 'https://erxes.io/blog/', - label: 'Resources', - external: true - }, - { - href: 'https://erxes.io/pricing', - label: 'Pricing', - external: true - }, - { - href: 'https://github.com/erxes/erxes', - label: 'GitHub', + href: 'https://erxes.io/create', + label: 'Get started', external: true }, ], From 7817d3319be7f6a52bcb439604af6f4cf991db01 Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Tue, 24 Mar 2020 13:02:47 +0800 Subject: [PATCH 068/110] Fix doc update (#1831) From cca738eda90e50c0b83d66f56949321bacb2663b Mon Sep 17 00:00:00 2001 From: soyombo <40427263+soyombo-baterdene@users.noreply.github.com> Date: Wed, 25 Mar 2020 11:48:09 +0800 Subject: [PATCH 069/110] feat(integration): integration whatsapp close #1105 --- docs/docs/administrator/system-config.md | 30 ++++++ docs/docs/overview/integrations-overview.md | 6 ++ ui/src/modules/activityLogs/constants.ts | 4 + .../common/components/IntegrationIcon.tsx | 4 + ui/src/modules/common/styles/colors.ts | 4 +- .../general/components/IntegrationConfigs.tsx | 14 +++ ui/src/modules/settings/general/constants.ts | 8 +- .../integrations/components/store/Entry.tsx | 48 ++++++---- .../components/whatsapp/Whatsapp.tsx | 93 +++++++++++++++++++ .../settings/integrations/constants.ts | 25 +++-- .../integrations/containers/StoreEntry.tsx | 1 + .../integrations/containers/whatsapp/Form.tsx | 46 +++++++++ ui/src/modules/settings/integrations/types.ts | 1 + 13 files changed, 252 insertions(+), 32 deletions(-) create mode 100644 ui/src/modules/settings/integrations/components/whatsapp/Whatsapp.tsx create mode 100644 ui/src/modules/settings/integrations/containers/whatsapp/Form.tsx diff --git a/docs/docs/administrator/system-config.md b/docs/docs/administrator/system-config.md index 9185629ebde..4c317d61709 100644 --- a/docs/docs/administrator/system-config.md +++ b/docs/docs/administrator/system-config.md @@ -636,6 +636,36 @@ In order to integrate the Yahoo you will need to generate app password for the E +## WhatsApp Integration + +1. Create the Chat-API account go to [website](https://app.chat-api.com/registration) +2. Copy **API key** from [here](https://app.chat-api.com/user/settings) + + **Configuration:** + +- Go to Erxes Settings => System config => Integrations config => Chat-API. + + + +**For then test purpose you can use [ngrok](http://ngrok.io/) for your webhook** + +```Shell +cd /path/to/erxes-integrations +ngrok http 3400 +``` + +When you start erxes-integration repo webhook will automatically created according to your configuration + +### Erxes WhatsApp integration settings. + +1. Go to your erxes.domain.com - settings - integrations page +2. Copy your instanceId and token from [here](https://app.chat-api.com/dashboard) + +3. Click on **Add Integrations** and select WhatsApp. +4. Paste instanceId and token into corresponding fields +5. Select your brand and click save. + + ## Engage configuration ### AWS SES diff --git a/docs/docs/overview/integrations-overview.md b/docs/docs/overview/integrations-overview.md index 067ad7a4a98..af88957655a 100644 --- a/docs/docs/overview/integrations-overview.md +++ b/docs/docs/overview/integrations-overview.md @@ -35,3 +35,9 @@ Erxes app can be integrated with AWS SES service and that means we can send emai * [Nylas integration](../administrator/system-config#nylas-integrations) Erxes app can be integrated with Nylas service and that means we can send emails to customers we want. Moreover, we can monitor about how many emails have sent successfully, how many emails are opened by customers and so on. + + +* [WhatsApp integration guide](../administrator/system-config#whatsapp-integration) + +Erxes app can be integrated with chat-api and that means we can receive our Whatsapp accounts messages directly into our erxes app's inbox. + diff --git a/ui/src/modules/activityLogs/constants.ts b/ui/src/modules/activityLogs/constants.ts index 8b869fc66c9..99253ebb06a 100644 --- a/ui/src/modules/activityLogs/constants.ts +++ b/ui/src/modules/activityLogs/constants.ts @@ -75,6 +75,10 @@ export const ICON_AND_COLOR_TABLE = { icon: 'twitter', color: '#1da1f2' }, + whatsapp: { + icon: 'whatsapp', + color: '#128c7e' + }, assignee: { icon: 'user-check', color: '#6569df' diff --git a/ui/src/modules/common/components/IntegrationIcon.tsx b/ui/src/modules/common/components/IntegrationIcon.tsx index 610111579aa..92e93442cec 100644 --- a/ui/src/modules/common/components/IntegrationIcon.tsx +++ b/ui/src/modules/common/components/IntegrationIcon.tsx @@ -23,6 +23,7 @@ const RoundedBackground = styledTS<{ type: string; size?: number }>( (props.type === 'facebook' && colors.socialFacebook) || (props.type === 'facebook-messenger' && colors.socialFacebookMessenger) || (props.type === 'gmail' && colors.socialGmail) || + (props.type === 'whatsapp' && colors.socialWhatsApp) || (props.type.includes('nylas') && colors.socialGmail) || colors.colorCoreBlue}; @@ -80,6 +81,9 @@ class IntegrationIcon extends React.PureComponent { case 'chatfuel': icon = 'comment-dots'; break; + case 'whatsapp': + icon = 'whatsapp'; + break; default: icon = 'doc-text-inv-1'; } diff --git a/ui/src/modules/common/styles/colors.ts b/ui/src/modules/common/styles/colors.ts index 6c78d13893d..79192d5e974 100644 --- a/ui/src/modules/common/styles/colors.ts +++ b/ui/src/modules/common/styles/colors.ts @@ -53,6 +53,7 @@ const socialFacebookMessenger = '#1472FB'; const socialTwitter = '#1DA1F2'; const socialGmail = '#D44638'; const socialGoogleMeet = '#038476'; +const socialWhatsApp = '#128c7e'; export default { colorPrimary, @@ -98,5 +99,6 @@ export default { socialFacebookMessenger, socialTwitter, socialGmail, - socialGoogleMeet + socialGoogleMeet, + socialWhatsApp }; diff --git a/ui/src/modules/settings/general/components/IntegrationConfigs.tsx b/ui/src/modules/settings/general/components/IntegrationConfigs.tsx index 995813250b9..6af7e06c84e 100644 --- a/ui/src/modules/settings/general/components/IntegrationConfigs.tsx +++ b/ui/src/modules/settings/general/components/IntegrationConfigs.tsx @@ -179,6 +179,20 @@ class IntegrationConfigs extends React.Component { {this.renderItem('GOOGLE_GMAIL_SUBSCRIPTION_NAME')} + + + + + {__('More: Understanding WhatsApp Integration Variables')} + + + {this.renderItem('CHAT_API_UID')} + {this.renderItem('CHAT_API_WEBHOOK_CALLBACK_URL')} + ); }; diff --git a/ui/src/modules/settings/general/constants.ts b/ui/src/modules/settings/general/constants.ts index fe5c2640ed6..e9ec48f8b8e 100755 --- a/ui/src/modules/settings/general/constants.ts +++ b/ui/src/modules/settings/general/constants.ts @@ -112,7 +112,10 @@ export const KEY_LABELS = { GOOGLE_CLIENT_SECRET: 'Google Client Secret', DAILY_API_KEY: 'Daily api key', - DAILY_END_POINT: 'Daily end point' + DAILY_END_POINT: 'Daily end point', + + CHAT_API_UID: 'Chat-API API key', + CHAT_API_WEBHOOK_CALLBACK_URL: 'Chat-API Webhook Callback Url' }; export const FILE_MIME_TYPES = [ @@ -200,7 +203,8 @@ export const FILE_MIME_TYPES = [ extension: '.ppt' }, { - value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + value: + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', label: 'Microsoft PowerPoint (OpenXML)', extension: '.pptx' }, diff --git a/ui/src/modules/settings/integrations/components/store/Entry.tsx b/ui/src/modules/settings/integrations/components/store/Entry.tsx index e0f7ebaa180..c93ef7f20d0 100644 --- a/ui/src/modules/settings/integrations/components/store/Entry.tsx +++ b/ui/src/modules/settings/integrations/components/store/Entry.tsx @@ -13,6 +13,7 @@ import KnowledgeBase from '../../containers/knowledgebase/Form'; import Lead from '../../containers/lead/Form'; import Twitter from '../../containers/twitter/Twitter'; import Website from '../../containers/website/Form'; +import WhatsappForm from '../../containers/whatsapp/Form'; import { Box, IntegrationItem, Ribbon, Type } from './styles'; type TotalCount = { @@ -26,6 +27,7 @@ type TotalCount = { office365: number; outlook: number; yahoo: number; + whatsapp: number; }; type Props = { @@ -34,10 +36,14 @@ type Props = { toggleBox: (kind: string) => void; messengerAppsCount?: number; queryParams: any; - totalCount: TotalCount + totalCount: TotalCount; }; -function getCount(kind: string, totalCount: TotalCount, messengerAppsCount?: number) { +function getCount( + kind: string, + totalCount: TotalCount, + messengerAppsCount?: number +) { const countByKind = totalCount[kind]; if (typeof messengerAppsCount === 'number') { @@ -63,7 +69,6 @@ function renderType(type: string) { ); } - function renderCreate(createUrl, createModal) { if (!createUrl && !createModal) { return null; @@ -142,11 +147,7 @@ function renderCreate(createUrl, createModal) { const content = props => ; return ( - + ); } @@ -156,11 +157,7 @@ function renderCreate(createUrl, createModal) { const content = props => ; return ( - + ); } @@ -238,10 +235,26 @@ function renderCreate(createUrl, createModal) { ); } + if (createModal === KIND_CHOICES.WHATSAPP) { + const trigger =
+ {__('Add')}
; + + const content = props => ; + + return ( + + ); + } + return + {__('Add')}; - }; +} -function Entry({ integration, getClassName, toggleBox, messengerAppsCount, totalCount }: Props) { +function Entry({ + integration, + getClassName, + toggleBox, + messengerAppsCount, + totalCount +}: Props) { const { kind } = integration; const boxOnClick = () => toggleBox(kind); @@ -249,10 +262,7 @@ function Entry({ integration, getClassName, toggleBox, messengerAppsCount, total const { createUrl, createModal } = integration; return ( - + logo
diff --git a/ui/src/modules/settings/integrations/components/whatsapp/Whatsapp.tsx b/ui/src/modules/settings/integrations/components/whatsapp/Whatsapp.tsx new file mode 100644 index 00000000000..338f7f539d4 --- /dev/null +++ b/ui/src/modules/settings/integrations/components/whatsapp/Whatsapp.tsx @@ -0,0 +1,93 @@ +import FormControl from 'modules/common/components/form/Control'; +import Form from 'modules/common/components/form/Form'; +import FormGroup from 'modules/common/components/form/Group'; +import ControlLabel from 'modules/common/components/form/Label'; +import Spinner from 'modules/common/components/Spinner'; +import { ModalFooter } from 'modules/common/styles/main'; +import { IButtonMutateProps, IFormProps } from 'modules/common/types'; +import React from 'react'; +import SelectBrand from '../../containers/SelectBrand'; + +type Props = { + renderButton: (props: IButtonMutateProps) => JSX.Element; + closeModal: () => void; +}; + +class Whatsapp extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loading: false + }; + } + + generateDoc = (values: { + name: string; + instanceId: string; + token: string; + brandId: string; + }) => { + return { + name: values.name, + brandId: values.brandId, + kind: 'whatsapp', + data: { + instanceId: values.instanceId, + token: values.token + } + }; + }; + + renderContent = (formProps: IFormProps) => { + const { renderButton } = this.props; + const { values, isSubmitted } = formProps; + + return ( + <> + {this.state.loading && } + + Name + + + + + Chat-API Instance id + + + + + Chat-API token + + + + + + + {renderButton({ + name: 'integration', + values: this.generateDoc(values), + isSubmitted, + callback: this.props.closeModal + })} + + + ); + }; + + render() { + return ; + } +} + +export default Whatsapp; diff --git a/ui/src/modules/settings/integrations/constants.ts b/ui/src/modules/settings/integrations/constants.ts index a77b3cc4c46..aca664b8af6 100755 --- a/ui/src/modules/settings/integrations/constants.ts +++ b/ui/src/modules/settings/integrations/constants.ts @@ -77,6 +77,7 @@ export const KIND_CHOICES = { CALLPRO: 'callpro', TWITTER_DM: 'twitter-dm', CHATFUEL: 'chatfuel', + WHATSAPP: 'whatsapp', ALL_LIST: [ 'messenger', 'facebook-post', @@ -89,7 +90,8 @@ export const KIND_CHOICES = { 'nylas-gmail', 'nylas-imap', 'nylas-office365', - 'nylas-outlook' + 'nylas-outlook', + 'whatsapp' ] }; @@ -100,7 +102,8 @@ export const KIND_CHOICES_WITH_TEXT = [ { text: 'Pop Ups', value: 'lead' }, { text: 'Callpro', value: 'callpro' }, { text: 'Chatfuel', value: 'chatfuel' }, - { text: 'Gmail', value: 'nylas-gmail' } + { text: 'Gmail', value: 'nylas-gmail' }, + { text: 'WhatsApp', value: 'whatsapp' } ]; export const FORM_LOAD_TYPES = { @@ -315,6 +318,16 @@ export const INTEGRATIONS = [ createUrl: '/settings/integrations/website', category: 'All integrations, For support teams, Marketing automation' }, + { + name: 'WhatsApp', + description: 'Get a hold of your Whatsapp messages through your Team Inbox', + inMessenger: false, + isAvailable: true, + kind: 'whatsapp', + logo: '/images/integrations/whatsapp.png', + createModal: 'whatsapp', + category: 'All integrations, For support teams, Messaging, Conversation' + }, { name: 'Viber', description: `Soon you'll be able to connect Viber straight to your Team Inbox`, @@ -324,14 +337,6 @@ export const INTEGRATIONS = [ category: 'All integrations, For support teams, Marketing automation, Messaging, Conversation' }, - { - name: 'WhatsApp', - description: 'Get a hold of your Whatsapp messages through your Team Inbox', - inMessenger: false, - isAvailable: false, - logo: '/images/integrations/whatsapp.png', - category: 'All integrations, For support teams, Messaging, Conversation' - }, { name: 'Wechat', description: diff --git a/ui/src/modules/settings/integrations/containers/StoreEntry.tsx b/ui/src/modules/settings/integrations/containers/StoreEntry.tsx index b210a505e30..c125b369636 100644 --- a/ui/src/modules/settings/integrations/containers/StoreEntry.tsx +++ b/ui/src/modules/settings/integrations/containers/StoreEntry.tsx @@ -25,6 +25,7 @@ type Props = { office365: number; outlook: number; yahoo: number; + whatsapp: number; }; }; diff --git a/ui/src/modules/settings/integrations/containers/whatsapp/Form.tsx b/ui/src/modules/settings/integrations/containers/whatsapp/Form.tsx new file mode 100644 index 00000000000..3f04d6c0cd4 --- /dev/null +++ b/ui/src/modules/settings/integrations/containers/whatsapp/Form.tsx @@ -0,0 +1,46 @@ +import ButtonMutate from 'modules/common/components/ButtonMutate'; +import { IButtonMutateProps, IRouterProps } from 'modules/common/types'; +import Whatsapp from 'modules/settings/integrations/components/whatsapp/Whatsapp'; +import { mutations } from 'modules/settings/integrations/graphql'; +import React from 'react'; +import { withRouter } from 'react-router'; + +type Props = { + type?: string; + closeModal: () => void; +}; + +type FinalProps = {} & IRouterProps & Props; + +class WhatsappContainer extends React.Component { + renderButton = ({ + name, + values, + isSubmitted, + callback + }: IButtonMutateProps) => { + return ( + + ); + }; + + render() { + const { closeModal } = this.props; + + const updatedProps = { + closeModal, + renderButton: this.renderButton + }; + + return ; + } +} + +export default withRouter(WhatsappContainer); diff --git a/ui/src/modules/settings/integrations/types.ts b/ui/src/modules/settings/integrations/types.ts index de5c783ed7a..c84e0e10c38 100644 --- a/ui/src/modules/settings/integrations/types.ts +++ b/ui/src/modules/settings/integrations/types.ts @@ -144,6 +144,7 @@ export type ByKindTotalCount = { office365: number; outlook: number; yahoo: number; + whatsapp: number; }; type IntegrationsCount = { From 523bb0d017cacedca8a1ae721f933c6b85333aaf Mon Sep 17 00:00:00 2001 From: Narmandakh Enkhtuvshin <37796969+Enkhtuvshin0513@users.noreply.github.com> Date: Thu, 26 Mar 2020 11:51:31 +0800 Subject: [PATCH 070/110] perf(growthhack): fix arviched growth hack list (#1842) --- ui/src/modules/growthHacks/graphql/queries.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/src/modules/growthHacks/graphql/queries.ts b/ui/src/modules/growthHacks/graphql/queries.ts index 74821e9e561..4c63121fe82 100644 --- a/ui/src/modules/growthHacks/graphql/queries.ts +++ b/ui/src/modules/growthHacks/graphql/queries.ts @@ -220,9 +220,7 @@ const archivedGrowthHacksCount = ` archivedGrowthHacksCount( pipelineId: $pipelineId, search: $search - ) { - ${growthHackFields} - } + ) } `; From d007dc7219f28084137d4404310a7a2b4f913df4 Mon Sep 17 00:00:00 2001 From: soyombo <40427263+soyombo-baterdene@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:09:28 +0800 Subject: [PATCH 071/110] Improvement whatsapp doc (#1838) --- docs/docs/administrator/system-config.md | 39 ++++++++++++++----- .../general/components/IntegrationConfigs.tsx | 2 +- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/docs/administrator/system-config.md b/docs/docs/administrator/system-config.md index 4c317d61709..d958d521e55 100644 --- a/docs/docs/administrator/system-config.md +++ b/docs/docs/administrator/system-config.md @@ -641,30 +641,51 @@ In order to integrate the Yahoo you will need to generate app password for the E 1. Create the Chat-API account go to [website](https://app.chat-api.com/registration) 2. Copy **API key** from [here](https://app.chat-api.com/user/settings) ++ Click on your profile, then select settings. + + + ++ Copy API key value + + + + **Configuration:** -- Go to Erxes Settings => System config => Integrations config => Chat-API. +- Go to Erxes Settings => System config => Integrations config => WhatsApp Chat-API. -**For then test purpose you can use [ngrok](http://ngrok.io/) for your webhook** ++ Paste API key to corresponding field. -```Shell -cd /path/to/erxes-integrations -ngrok http 3400 -``` ++ Put your webhook url into CHAT-API WEBHOOK CALLBACK URL field. ++ For example 'https://erxes-integrations/whatsapp/webhook' When you start erxes-integration repo webhook will automatically created according to your configuration ### Erxes WhatsApp integration settings. 1. Go to your erxes.domain.com - settings - integrations page + 2. Copy your instanceId and token from [here](https://app.chat-api.com/dashboard) + -3. Click on **Add Integrations** and select WhatsApp. -4. Paste instanceId and token into corresponding fields -5. Select your brand and click save. +3. To connect to api, you need to scan the QR code from the device on which WhatsApp is registered. + ++ If your account is registered less than a month ago, you need to pass a secure authorization to reduce the likelihood of blocking or authorization failure. + + + +4. Click on **Add Integrations** and select WhatsApp. + + + +5. Paste instanceId and token into corresponding fields + +6. Select your brand and click save. + +7. Go to Setting=> Channel=> Add new channel=> Connect facebook integration. ## Engage configuration diff --git a/ui/src/modules/settings/general/components/IntegrationConfigs.tsx b/ui/src/modules/settings/general/components/IntegrationConfigs.tsx index 6af7e06c84e..0fe9ba34cb6 100644 --- a/ui/src/modules/settings/general/components/IntegrationConfigs.tsx +++ b/ui/src/modules/settings/general/components/IntegrationConfigs.tsx @@ -184,7 +184,7 @@ class IntegrationConfigs extends React.Component { {__('More: Understanding WhatsApp Integration Variables')} From a0803af49578614dcbd87a4cdb9bd75c28097f4b Mon Sep 17 00:00:00 2001 From: Indra Ganzorig <53030705+indraganzorig@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:09:47 +0800 Subject: [PATCH 072/110] Update contributing.md with Swag for Contributions (#1844) --- docs/docs/developer/contributing.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/docs/developer/contributing.md b/docs/docs/developer/contributing.md index ce6150949a7..5d6094999d5 100644 --- a/docs/docs/developer/contributing.md +++ b/docs/docs/developer/contributing.md @@ -11,7 +11,8 @@ We would love for you to contribute to Erxes and help make it even better than i * [Feature Requests](#missing-a-feature) * [Submission Guidelines](#submission-guidelines) * [Coding Standards](#coding-standards) -* [Commit Message Guidelines]() +* [Commit Message Guidelines](#commit-message-guidelines) +* [Swag for Contributions]() ### Found a Bug? @@ -383,3 +384,9 @@ perf(inbox): remove graphiteWidth option revert: feat(inbox): add 'graphiteWidth' option This reverts commit 667ecc1654a317a13331b17617d973392f415f02. ``` +## Swag for Contributions + +To show our appreciation, we are sending everyone who contributes to erxes a special package, which includes a t-shirt and stickers. [Click here](https://erxes.io/hubspot-alternative-erxes-swag) to claim your swag. (Worldwide free shipping included!) + +

+

From 699f3b1797fcbf5c16c3a5b27dbfe2760c1cdfe5 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Thu, 26 Mar 2020 17:35:08 +0800 Subject: [PATCH 073/110] update widgets snyk --- widgets/.snyk | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/widgets/.snyk b/widgets/.snyk index 9e950822fe4..23914709e49 100644 --- a/widgets/.snyk +++ b/widgets/.snyk @@ -6,4 +6,14 @@ ignore: - node-pre-gyp > tar > chownr: reason: Devepment only expires: '2018-12-13T03:06:41.161Z' + SNYK-JS-MINIMIST-559764: + - node-pre-gyp > mkdirp > minimist: + reason: None given + expires: '2020-04-25T09:34:53.734Z' + - node-pre-gyp > tar > mkdirp > minimist: + reason: None given + expires: '2020-04-25T09:34:53.734Z' + - node-pre-gyp > rc > minimist: + reason: None given + expires: '2020-04-25T09:34:53.734Z' patch: {} From 73d74da452e128c2c3a5fda06d503e056c4c2898 Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Thu, 26 Mar 2020 17:36:07 +0800 Subject: [PATCH 074/110] update ui snyk --- ui/.snyk | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/ui/.snyk b/ui/.snyk index 93f2f9108f8..69eb95c3ea5 100644 --- a/ui/.snyk +++ b/ui/.snyk @@ -1,6 +1,37 @@ # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. version: v1.13.5 -ignore: {} +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-DOTPROP-543489: + - snyk > configstore > dot-prop: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - snyk > update-notifier > configstore > dot-prop: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + SNYK-JS-MINIMIST-559764: + - snyk > update-notifier > latest-version > package-json > registry-auth-token > rc > minimist: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - snyk > update-notifier > latest-version > package-json > registry-url > rc > minimist: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - '@babel/core > json5 > minimist': + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - node-pre-gyp > rc > minimist: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - node-pre-gyp > mkdirp > minimist: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + - node-pre-gyp > tar > mkdirp > minimist: + reason: None given + expires: '2020-04-25T09:35:45.413Z' + SNYK-JS-SERIALIZEJAVASCRIPT-536840: + - '@daily-co/daily-js > rollup-plugin-terser > serialize-javascript': + reason: None given + expires: '2020-04-25T09:35:45.413Z' # patches apply the minimum changes required to fix a vulnerability patch: SNYK-JS-LODASH-450202: From 97e390f6704e6bee640e677f4f48dc2488e6b50f Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 27 Mar 2020 13:45:11 +0800 Subject: [PATCH 075/110] perf(common): update icon package, change some new icons, show attachment file type as icon. (close #1848 #1843) --- ui/package.json | 2 +- .../boards/components/MainActionBar.tsx | 2 +- .../boards/components/PipelineWatch.tsx | 2 +- .../boards/components/editForm/Left.tsx | 2 +- .../boards/components/editForm/Watch.tsx | 4 +- .../boards/components/label/LabelChooser.tsx | 2 +- .../modules/common/components/Attachment.tsx | 39 ++++++++++--------- .../common/components/IntegrationIcon.tsx | 16 ++------ .../common/components/editor/Editor.tsx | 2 +- ui/src/modules/common/styles/colors.ts | 2 +- .../components/common/InfoSection.tsx | 4 +- .../components/common/InfoSection.tsx | 4 +- ui/src/modules/deals/components/DealItem.tsx | 2 +- .../modules/forms/components/FieldChoices.tsx | 33 +++++++++------- .../growthHacks/components/editForm/Left.tsx | 2 +- .../growthHacks/components/home/Home.tsx | 4 +- .../conversationDetail/workarea/mail/Mail.tsx | 4 +- .../workarea/mail/MailHeader.tsx | 2 +- .../modules/knowledgeBase/icons.constant.ts | 6 --- ui/src/modules/leads/components/Row.tsx | 2 +- .../components/NotificationList.tsx | 2 +- .../growthHacks/components/TemplateList.tsx | 2 +- .../settings/status/components/Status.tsx | 2 +- .../team/components/detail/LeftSidebar.tsx | 4 +- ui/yarn.lock | 8 ++-- 25 files changed, 74 insertions(+), 80 deletions(-) diff --git a/ui/package.json b/ui/package.json index 8c75eeb5fe1..a683a794a25 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,7 @@ "draft-js-export-html": "^1.2.0", "draft-js-plugins-editor": "^2.0.3", "draft-js-static-toolbar-plugin": "^3.0.0", - "erxes-icon": "^1.1.0", + "erxes-icon": "^1.2.1", "fuzzysearch-highlight": "^1.0.3", "graphql": "^0.12.3", "graphql-tag": "^2.6.1", diff --git a/ui/src/modules/boards/components/MainActionBar.tsx b/ui/src/modules/boards/components/MainActionBar.tsx index e47eaf516ae..4482b44796e 100644 --- a/ui/src/modules/boards/components/MainActionBar.tsx +++ b/ui/src/modules/boards/components/MainActionBar.tsx @@ -210,7 +210,7 @@ class MainActionBar extends React.Component { currentBoard ? currentBoard._id : '' }`} > - + diff --git a/ui/src/modules/boards/components/PipelineWatch.tsx b/ui/src/modules/boards/components/PipelineWatch.tsx index 8322c637453..de61445bf7b 100644 --- a/ui/src/modules/boards/components/PipelineWatch.tsx +++ b/ui/src/modules/boards/components/PipelineWatch.tsx @@ -20,7 +20,7 @@ class Watch extends React.Component { return ( - + {isWatched ? __('Watching') : __('Watch')} ); diff --git a/ui/src/modules/boards/components/editForm/Left.tsx b/ui/src/modules/boards/components/editForm/Left.tsx index 6b46aa5d304..83c5c71e7fd 100644 --- a/ui/src/modules/boards/components/editForm/Left.tsx +++ b/ui/src/modules/boards/components/editForm/Left.tsx @@ -105,7 +105,7 @@ const Left = (props: Props) => { - + {__('Labels')} diff --git a/ui/src/modules/boards/components/editForm/Watch.tsx b/ui/src/modules/boards/components/editForm/Watch.tsx index 4c59baf183e..a638cc56b04 100644 --- a/ui/src/modules/boards/components/editForm/Watch.tsx +++ b/ui/src/modules/boards/components/editForm/Watch.tsx @@ -24,14 +24,14 @@ class Watch extends React.Component { if (isSmall) { return ( - + {__('Watch')} ); } return ( - + {__('Watch')} {isWatched && ( diff --git a/ui/src/modules/boards/components/label/LabelChooser.tsx b/ui/src/modules/boards/components/label/LabelChooser.tsx index 9a4de2bbcc5..f9f2ef6935c 100644 --- a/ui/src/modules/boards/components/label/LabelChooser.tsx +++ b/ui/src/modules/boards/components/label/LabelChooser.tsx @@ -77,7 +77,7 @@ class ChooseLabel extends React.Component< container={this} > - + {__('Labels')} diff --git a/ui/src/modules/common/components/Attachment.tsx b/ui/src/modules/common/components/Attachment.tsx index bb77b69c0f5..99e5b027b94 100644 --- a/ui/src/modules/common/components/Attachment.tsx +++ b/ui/src/modules/common/components/Attachment.tsx @@ -56,7 +56,8 @@ const PreviewWrapper = styled.div` overflow: hidden; i { - font-size: 26px; + font-size: 36px; + color: ${colors.colorSecondary}; } `; @@ -178,33 +179,35 @@ class Attachment extends React.Component { let filePreview; switch (fileExtension) { - case 'png': - case 'jpeg': - case 'doc': case 'docx': - case 'txt': - case 'pdf': - case 'xls': - case 'xlsx': - case 'ppt': + filePreview = this.renderOtherFile(attachment, 'doc'); + break; case 'pptx': - filePreview = this.renderOtherFile(attachment, 'file'); + filePreview = this.renderOtherFile(attachment, 'ppt'); + break; + case 'xlsx': + filePreview = this.renderOtherFile(attachment, 'xls'); break; case 'mp4': filePreview = this.renderVideoFile(attachment); break; + case 'zip': + case 'csv': + case 'doc': + case 'ppt': + case 'psd': case 'avi': - filePreview = this.renderOtherFile(attachment, 'videocamera'); - break; + case 'txt': + case 'rar': case 'mp3': - case 'wav': - filePreview = this.renderOtherFile(attachment, 'music'); - break; - case 'zip': - filePreview = this.renderOtherFile(attachment, 'cube'); + case 'pdf': + case 'png': + case 'xls': + case 'jpeg': + filePreview = this.renderOtherFile(attachment, fileExtension); break; default: - filePreview = this.renderOtherFile(attachment, 'clipboard-1'); + filePreview = this.renderOtherFile(attachment, 'file-2'); } return filePreview; }; diff --git a/ui/src/modules/common/components/IntegrationIcon.tsx b/ui/src/modules/common/components/IntegrationIcon.tsx index 92e93442cec..8aab8d30398 100644 --- a/ui/src/modules/common/components/IntegrationIcon.tsx +++ b/ui/src/modules/common/components/IntegrationIcon.tsx @@ -58,31 +58,23 @@ class IntegrationIcon extends React.PureComponent { icon = 'comment'; break; case 'nylas-gmail': - icon = 'mail-alt'; + case 'gmail': + icon = 'gmail'; break; case 'nylas-imap': - icon = 'mail-alt'; - break; case 'nylas-office365': - icon = 'mail-alt'; - break; case 'nylas-outlook': - icon = 'mail-alt'; - break; case 'nylas-yahoo': icon = 'mail-alt'; break; - case 'gmail': - icon = 'mail-alt'; - break; case 'callpro': - icon = 'phone-call'; + icon = 'phone-volume'; break; case 'chatfuel': icon = 'comment-dots'; break; case 'whatsapp': - icon = 'whatsapp'; + icon = 'whatsapp-fill'; break; default: icon = 'doc-text-inv-1'; diff --git a/ui/src/modules/common/components/editor/Editor.tsx b/ui/src/modules/common/components/editor/Editor.tsx index a797c284636..4dacb855e28 100755 --- a/ui/src/modules/common/components/editor/Editor.tsx +++ b/ui/src/modules/common/components/editor/Editor.tsx @@ -66,7 +66,7 @@ export class ErxesEditor extends React.Component { this.toolbarPlugin = createToolbarPlugin(); this.emojiPlugin = createEmojiPlugin({ useNativeArt: true, - selectButtonContent: , + selectButtonContent: , positionSuggestions: settings => { return { left: settings.decoratorRect.x + 'px', diff --git a/ui/src/modules/common/styles/colors.ts b/ui/src/modules/common/styles/colors.ts index 79192d5e974..f2cc1be4349 100644 --- a/ui/src/modules/common/styles/colors.ts +++ b/ui/src/modules/common/styles/colors.ts @@ -53,7 +53,7 @@ const socialFacebookMessenger = '#1472FB'; const socialTwitter = '#1DA1F2'; const socialGmail = '#D44638'; const socialGoogleMeet = '#038476'; -const socialWhatsApp = '#128c7e'; +const socialWhatsApp = '#25D366'; export default { colorPrimary, diff --git a/ui/src/modules/companies/components/common/InfoSection.tsx b/ui/src/modules/companies/components/common/InfoSection.tsx index 4c3542438d0..dadb15b77c2 100644 --- a/ui/src/modules/companies/components/common/InfoSection.tsx +++ b/ui/src/modules/companies/components/common/InfoSection.tsx @@ -35,11 +35,11 @@ class InfoSection extends React.Component { return ( {this.renderLink(links.facebook, 'facebook')} + {this.renderLink(links.linkedIn, 'linkedin')} {this.renderLink(links.twitter, 'twitter')} - {this.renderLink(links.linkedIn, 'linkedin-logo')} {this.renderLink(links.youtube, 'youtube-play')} {this.renderLink(links.github, 'github-circled')} - {this.renderLink(links.website, 'link-alt')} + {this.renderLink(links.website, 'external-link-alt')} ); } diff --git a/ui/src/modules/customers/components/common/InfoSection.tsx b/ui/src/modules/customers/components/common/InfoSection.tsx index 188bf128038..85a5f678733 100644 --- a/ui/src/modules/customers/components/common/InfoSection.tsx +++ b/ui/src/modules/customers/components/common/InfoSection.tsx @@ -36,11 +36,11 @@ class InfoSection extends React.Component { return ( {this.renderLink(links.facebook, 'facebook-official')} + {this.renderLink(links.linkedIn, 'linkedin')} {this.renderLink(links.twitter, 'twitter')} - {this.renderLink(links.linkedIn, 'linkedin-logo')} {this.renderLink(links.youtube, 'youtube-play')} {this.renderLink(links.github, 'github-circled')} - {this.renderLink(links.website, 'link-alt')} + {this.renderLink(links.website, 'external-link-alt')} ); } diff --git a/ui/src/modules/deals/components/DealItem.tsx b/ui/src/modules/deals/components/DealItem.tsx index 9efa0acb874..88a81638ff7 100644 --- a/ui/src/modules/deals/components/DealItem.tsx +++ b/ui/src/modules/deals/components/DealItem.tsx @@ -111,7 +111,7 @@ class DealItem extends React.PureComponent {
- {item.isWatched ? : __('Last updated')} + {item.isWatched ? : __('Last updated')} {this.renderDate(item.modifiedAt)}
diff --git a/ui/src/modules/forms/components/FieldChoices.tsx b/ui/src/modules/forms/components/FieldChoices.tsx index 0609382a25f..d83581e37b5 100644 --- a/ui/src/modules/forms/components/FieldChoices.tsx +++ b/ui/src/modules/forms/components/FieldChoices.tsx @@ -35,46 +35,51 @@ function FieldChoices(props: Props) { {...props} type="input" text={__('Text input')} - icon="edit" + icon="edit-alt" /> - + - + - + ); diff --git a/ui/src/modules/growthHacks/components/editForm/Left.tsx b/ui/src/modules/growthHacks/components/editForm/Left.tsx index a6215c6e9b6..4186ac09964 100644 --- a/ui/src/modules/growthHacks/components/editForm/Left.tsx +++ b/ui/src/modules/growthHacks/components/editForm/Left.tsx @@ -72,7 +72,7 @@ class Left extends React.Component { - + {__('Labels')} diff --git a/ui/src/modules/growthHacks/components/home/Home.tsx b/ui/src/modules/growthHacks/components/home/Home.tsx index c18de40e349..bfcb3f125b0 100644 --- a/ui/src/modules/growthHacks/components/home/Home.tsx +++ b/ui/src/modules/growthHacks/components/home/Home.tsx @@ -107,11 +107,11 @@ class Home extends React.Component { }; renderCountItem = (state: string) => { - let iconContent = 'e8df'; + let iconContent = 'eabd'; switch (state) { case 'In progress': - iconContent = 'e8e9'; + iconContent = 'ecc5'; break; case 'Not started': diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/mail/Mail.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/mail/Mail.tsx index e6794ab9907..1877cfa5668 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/mail/Mail.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/mail/Mail.tsx @@ -62,7 +62,7 @@ class Mail extends React.PureComponent { return ( {addressLength > 1 && ( diff --git a/ui/src/modules/settings/growthHacks/components/TemplateList.tsx b/ui/src/modules/settings/growthHacks/components/TemplateList.tsx index f477dcb7cc9..a09e35b214a 100755 --- a/ui/src/modules/settings/growthHacks/components/TemplateList.tsx +++ b/ui/src/modules/settings/growthHacks/components/TemplateList.tsx @@ -76,7 +76,7 @@ class TemplateList extends React.Component { {this.renderDuplicateAction(object)}
- +
diff --git a/ui/src/modules/settings/status/components/Status.tsx b/ui/src/modules/settings/status/components/Status.tsx index 9e2373e9e65..59721716d34 100644 --- a/ui/src/modules/settings/status/components/Status.tsx +++ b/ui/src/modules/settings/status/components/Status.tsx @@ -18,7 +18,7 @@ class Status extends React.PureComponent<{ - {__('Info')} + {__('Info')}
{__('Package version')} - {ver.packageVersion} diff --git a/ui/src/modules/settings/team/components/detail/LeftSidebar.tsx b/ui/src/modules/settings/team/components/detail/LeftSidebar.tsx index 72d8b0b9daa..f5100d3f0b9 100644 --- a/ui/src/modules/settings/team/components/detail/LeftSidebar.tsx +++ b/ui/src/modules/settings/team/components/detail/LeftSidebar.tsx @@ -36,11 +36,11 @@ class LeftSidebar extends React.Component { return ( {this.renderLink(links.facebook, 'facebook-official')} + {this.renderLink(links.linkedIn, 'linkedin')} {this.renderLink(links.twitter, 'twitter')} - {this.renderLink(links.linkedIn, 'linkedin-logo')} {this.renderLink(links.youtube, 'youtube-play')} {this.renderLink(links.github, 'github-circled')} - {this.renderLink(links.website, 'earthgrid')} + {this.renderLink(links.website, 'external-link-alt')} ); } diff --git a/ui/yarn.lock b/ui/yarn.lock index b3a970af976..f7cd9a68e56 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5086,10 +5086,10 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -erxes-icon@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/erxes-icon/-/erxes-icon-1.1.0.tgz#bed98d6fe086f04cedec54d447549819d7257e40" - integrity sha512-O1T91M6F3icF/q2IadAmAYOo3jJln5iM8/wjk+tPGvMI8hh+2jdZ1zKws4liuFrhP5EkllOLJ7hBn/90KBaEiQ== +erxes-icon@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/erxes-icon/-/erxes-icon-1.2.1.tgz#1df585ff385146dfffd0261398de474eed39c2c6" + integrity sha512-qk1yavXsBCub9/XBaiTgg9OCouRfZnDxWIqtfi/tVr51zFYgZuGm5GkswglIhF5FCayJ+XtOFqf/Z/9UUc676g== es-abstract@^1.10.0, es-abstract@^1.13.0: version "1.14.1" From 2f4875195d92708f1ae1ab499f45ae0a2659b471 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 27 Mar 2020 13:53:26 +0800 Subject: [PATCH 076/110] perf(appStore): fix typo (close #1845) --- ui/src/modules/settings/integrations/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/settings/integrations/constants.ts b/ui/src/modules/settings/integrations/constants.ts index aca664b8af6..ff96332832b 100755 --- a/ui/src/modules/settings/integrations/constants.ts +++ b/ui/src/modules/settings/integrations/constants.ts @@ -367,7 +367,7 @@ export const INTEGRATIONS = [ }, { name: 'Instagram', - description: 'Connect to your twitter posts here in your Team Inbox', + description: 'Connect to your instagram posts here in your Team Inbox', inMessenger: false, isAvailable: false, logo: '/images/integrations/instagram.png', From e4046dc57c19137d8dfcbebe83b153fa84d7c0dd Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Fri, 27 Mar 2020 16:11:59 +0800 Subject: [PATCH 077/110] perf(customers): added lead logic close #1850 --- ui/public/images/avatar-colored.svg | 2 +- ui/public/images/avatar.svg | 2 +- ui/public/images/reactions.png | Bin 3362 -> 0 bytes .../images/sandbox-banner-send-statistics.png | Bin 60219 -> 0 bytes .../components/ModifiableSelect.test.tsx | 1 + ui/src/locales/en.json | 4 +- ui/src/locales/es.json | 1 - ui/src/modules/activityLogs/styles.ts | 20 +- .../auth/components/UserCommonInfos.tsx | 308 ++++++------ .../common/components/AvatarUpload.tsx | 31 +- .../common/components/ButtonMutate.tsx | 2 +- .../common/components/CollapseContent.tsx | 11 +- .../common/components/IntegrationIcon.tsx | 2 +- .../common/components/ModifiableSelect.tsx | 133 ++++-- .../common/components/filter/Filter.tsx | 2 - .../modules/common/components/form/styles.tsx | 1 - .../common/components/nameCard/Avatar.tsx | 28 +- .../modules/common/components/tabs/styles.ts | 2 +- ui/src/modules/common/styles/main.ts | 28 +- ui/src/modules/common/utils/menus.ts | 6 +- .../components/common/BasicInfoSection.tsx | 4 - .../components/common/DetailInfo.tsx | 12 - .../components/common/InfoSection.tsx | 32 +- .../components/detail/CompanyDetails.tsx | 10 + .../companies/components/list/CompanyForm.tsx | 412 ++++++++-------- .../companies/components/list/Sidebar.tsx | 4 - ui/src/modules/companies/constants.ts | 6 - .../companies/containers/CompaniesList.tsx | 2 - .../containers/filters/LeadStatusFilter.tsx | 45 -- .../filters/LifecycleStateFilter.tsx | 45 -- .../companies/containers/filters/index.ts | 10 +- ui/src/modules/companies/graphql/mutations.ts | 6 - ui/src/modules/companies/graphql/queries.ts | 6 - ui/src/modules/companies/types.ts | 6 +- .../components/common/ActionSection.tsx | 88 +++- .../components/common/BasicInfoSection.tsx | 5 +- .../components/common/DetailInfo.tsx | 50 +- .../components/common/InfoSection.tsx | 58 ++- .../components/detail/CustomerDetails.tsx | 18 +- .../customers/components/detail/LeadState.tsx | 72 +++ .../components/list/CustomerForm.tsx | 444 +++++++++--------- .../customers/components/list/CustomerRow.tsx | 2 +- .../components/list/CustomersList.tsx | 15 +- .../components/list/LeadStatusFilter.tsx | 2 +- .../components/list/LifecycleStateFilter.tsx | 90 ---- .../customers/components/list/Sidebar.tsx | 22 +- ui/src/modules/customers/constants.ts | 22 +- .../customers/containers/CustomerForm.tsx | 8 +- .../customers/containers/CustomersList.tsx | 1 - .../customers/containers/LeadState.tsx | 57 +++ .../containers/common/ActionSection.tsx | 39 +- .../containers/filters/BrandFilter.tsx | 35 +- .../containers/filters/IntegrationFilter.tsx | 26 +- .../containers/filters/LeadFilter.tsx | 30 +- .../containers/filters/LeadStatusFilter.tsx | 26 +- .../filters/LifecycleStateFilter.tsx | 45 -- .../containers/filters/SegmentFilter.tsx | 26 +- .../containers/filters/TagFilter.tsx | 26 +- .../customers/containers/filters/index.ts | 2 - ui/src/modules/customers/graphql/mutations.ts | 16 +- ui/src/modules/customers/graphql/queries.ts | 5 +- ui/src/modules/customers/routes.tsx | 5 +- ui/src/modules/customers/styles.ts | 213 ++++++++- ui/src/modules/customers/types.ts | 28 +- ui/src/modules/customers/utils.ts | 19 +- .../conversationDetail/sidebar/Sidebar.tsx | 2 +- .../conversationDetail/sidebar/styles.ts | 30 +- .../conversationDetail/workarea/mail/style.ts | 2 +- ui/src/modules/inbox/graphql/messageFields.ts | 2 +- ui/src/modules/inbox/graphql/queries.ts | 2 +- .../modules/layout/components/Navigation.tsx | 2 +- ui/src/modules/leads/components/Row.tsx | 4 +- ui/src/modules/robot/constants.ts | 2 +- .../settings/common/components/Form.tsx | 2 +- .../integrations/containers/mail/constants.ts | 2 +- .../properties/components/ManageColumns.tsx | 2 +- .../settings/team/components/UserForm.tsx | 11 +- .../team/containers/UserDetailForm.tsx | 2 + .../settings/utils/commonListComposer.tsx | 1 + widgets/client/events/index.ts | 2 +- widgets/client/events/widget/index.ts | 2 +- widgets/client/form/components/Field.tsx | 38 +- widgets/server/views/widget-test.ejs | 2 +- 83 files changed, 1519 insertions(+), 1270 deletions(-) delete mode 100644 ui/public/images/reactions.png delete mode 100644 ui/public/images/sandbox-banner-send-statistics.png delete mode 100644 ui/src/modules/companies/containers/filters/LeadStatusFilter.tsx delete mode 100644 ui/src/modules/companies/containers/filters/LifecycleStateFilter.tsx create mode 100644 ui/src/modules/customers/components/detail/LeadState.tsx delete mode 100644 ui/src/modules/customers/components/list/LifecycleStateFilter.tsx create mode 100644 ui/src/modules/customers/containers/LeadState.tsx delete mode 100644 ui/src/modules/customers/containers/filters/LifecycleStateFilter.tsx diff --git a/ui/public/images/avatar-colored.svg b/ui/public/images/avatar-colored.svg index 2e96d3e75d7..f6a5986c782 100644 --- a/ui/public/images/avatar-colored.svg +++ b/ui/public/images/avatar-colored.svg @@ -1 +1 @@ -avatar-colored \ No newline at end of file + \ No newline at end of file diff --git a/ui/public/images/avatar.svg b/ui/public/images/avatar.svg index c408e609dba..e72c15829c2 100644 --- a/ui/public/images/avatar.svg +++ b/ui/public/images/avatar.svg @@ -1 +1 @@ -avatar \ No newline at end of file + \ No newline at end of file diff --git a/ui/public/images/reactions.png b/ui/public/images/reactions.png deleted file mode 100644 index e0de78cf7666460c5c5515e57348f7e9666f6712..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3362 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 o%u$$X+`KaRE6+>+0Y6XLe*gdg diff --git a/ui/public/images/sandbox-banner-send-statistics.png b/ui/public/images/sandbox-banner-send-statistics.png deleted file mode 100644 index 72f8010dc8b32b26e69b5f9c8c75e4a6ccae8a5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60219 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 U12fD3QB3bO5#~2?Z0- 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Ν9^I;l!#4A)50#MeJ5l9Haq0Ddw znWV`Yf23^E`z^`KfVd~}NdbEl#dM1Q4_sjnM+9l{llCgYRV6jun2Of#54}vfp43>} z8~Eg*pL*o58$G*iVxwMbJ`fTju6gx%VZ0~%#Ck;F_E#hyZj?Ji%L*7r%yg8fZ`-nJ z!-x!tkDUJy{TgfR0dq<6GxM28hHN&`GRL;FCX%q_deWkjtye1hx-n3s9^o`R;l*Gb zx&-Ot6&c0o9N^DxN=h^*x+ED%gz>S=nYsV`Yg=}(>MM-1ad$UEa=Avany=N!zIkM@ ze>WqGg&&ZiQA&1cDKL%~mgG2#91|U`e#o(o4v~14X}!iR6_my6nly*t5~SKqldG*Y zQwBYVN8J;m3yUa}vCb*Ra1E>qd|x&XVjwqBebVkfq(34y|o zJRegY*EGM~yr2gD!smpSkiZTn+14T$p4$)8OfWb+s{=wVLHx2|&q|pp>XTCnF(ri< z@jCPut#(Z=1Gxs7Be;%I=ldr8ksQcG?Ugc)6;zVv8i^9=EnY6`8LpXCA{dJj(h4h^+H&g1k{5w@-j<_+n z?Q4Y}SELz9-&98xiA8J`V7hk1ZP)yz7=G2jHEZ8Yo-M)`v&x%hky{zi_gJ z_;m(-HR;}rtG33gAOac(6iYC-x5;EZadgovWeeGv^q&Aa}u;-xw4 zfIp3A%JJhr_71O10fB1==xFPLQsQArr82@#VmTLzw9dUu&@`r<3)nAz7xqfQT^0E|( z=mab-?yEvRQ{Jrnz9a07o)2ZDN7bhM5L>{FSz*f9?aHzO*$k0IUN}qOr-kGfn|ixY zLW`?1rOmOX^IH*yS!Q%B-?+#>{P&VE%QDQ7V@0|TYR{~1MYD`+%|JJFHGDMl$2b!$ z^e5fA98BQz9|*`bInPyr32u4m?Q}7*6$LNZ3lW89x$D~PAzgL({i?Psw6pZA!{IQG zcqmZ6_IP~rObauQjuZHB##L2jZvlY2eDkA6s6=O@H>RmxXDm`O9neW5~ zXL~mICrJVlrtHo*=5O&*&bko&st+N>*&F~#+IW*M?M{7wemo1=F25^Lyfl;p9hjL` zHXPR!zvJ0?qeBgxA2_8^q(ew4Ksu6OwHM1>jQTUGKzjd&Kw8A!NT!|nMwRqP<*!Jv zP67*_T%g-b^ZaitS-LOX=c_MA{b)zj_aa})MvE$t;xA9Muo*Wlch6k_FRavtN(mJe z#GMg2un*^7Q5#~}5Q9;Cqee@QT`b&)7Zx~_X+&_^xlJ|*HVDJAC&|t zOe@aoPTxxYRu4mN#EPNjojib%tt<65gzk3t7~mHxB_cU4{G?v=Cic-Kb825OMeT6` zYx?L|I7CGcfO>nxw>j`eeNUAYWPV1ro@cq7i4#iegBjjR%Jh5nv51xs}{n~+lQs*sh*DVBtf&Pmo6-ayUQWOkPWm@ zS?us!o5t5=8kcdMF41P4tzX4kTqg9USSzMFBC(Okk(XCGQd#W&izs! z)i2M3>WFvl_^^+;ZM!$L@vmAttw3(0{HlYgEk|h`-D?T6o5ALn8+%sDC4JnUBgew{ z!)(>%B?3ir&m0IhqNbL(FQs^=R6Fg2)w^-kx%k}maRhZ2{KV;$%7%y<7` zLbummgL`Vu*Ob!(zAga9bbMUi(0ZnWgOXU@=y>YdqSWu$WvoK-l@%#1k%OH&*xGOt zBN;X{3YB*^mLx*fC3vN}yxQ@+wN`wBjm!mH5C$406OtqKJyMn@OO1TF2|~(+FQVQH zyv1&7Hal?fmic24JT4Z*X-k#``nH$I-|}zF1kz+o2DKlvTQJAH8CERUe281&h}4TO z80;DM0K5*bl{)`|UR~*_2m;IR?XglUlKoz`*us+=7|v5nCNiZBI4JNOqn{(&Q^MN; zN0OTB+EoX-+-wEA?8S@_ndtH;KEzqD?u1{2uYMt$CfL`#XX>;PGeaibs0WYM2bnY^ zm}VQfA?eOEtC_?eoKGs|e_w-CJMWgs6YXYjQIPDcfnPp7sxjemR|W zR)|XQ!k^R0x;S^IakRiLEb6dy^AkA7=m@cCr@@vcC6gGfT3m)803^j$Ayb(-yf(16l3>1{q>n}iY`c9!oV9(s;nrCI&_!_|9&+mwUa1syi<4|*y2f-gdS5du1d z_r{#I%|TdaFm%;MBYL@NS#S2-IC8W<|IPMB$`y-q0}6=%o0OIvJ2^2`@)gHl<@Wc} zSCg9=g-1l&e#71rtQb~{_b56;xkC1op!>;KNEhL~1FsW}zfZ`W`Nz3m?<&-dt}XS+cyrl0@3pXENfIcQok!~fK%HMzTTs>0IldD-6WMNqH4$Jy6i^DLmPR*tRTp!lDk%81ObsfLs({>$y?J7pi8h! z3@e)YIrb~FH!>+(b^|>4aQ6QPBJC0;l2THT8q|?~QF79IUp#wl z$VQW-tk?NPkrZ?B7K={#@Bv2_dpw71O6v2+R#EMr0#$?@PVd_kr+W5!sfWhl6?LRT67}o z&#YM>OJ*X>zFU?_0rG$i(pSK}g7PqEUzik3(aCkPtJB2EzkdprJ(RAoHM&g&N{*JC zk~JpI#^`FUPg5J1axdd4JqFjpE)u1$JGNcbx_hZUARX1I{os$NtmpcHwLZnBv>mh6 z7c7NCJYk`M6@Fc?ce%kAu=(eBUy4TP2WzR-AJFz_1X_dEk#HVX3(~4~e@ybCDuE9^ zTsjMJ9|+D{FB@>WE?RpI<;LO`yP8=P4^ZP|0`HGd?RJ`Jbh%y{jfRr3De7@q}=qirYMF(xoASj6`Y;G)ilD5UGrPDSYvmK3VOaFGP=jO4|vdtjZax% zZ1=OwmW{#k!wfEGR?R;Jj=dGqzIFL!nbLnCu)~!U9z<1XpU>}TyfIc`qWJdWDx1W+ zb;g6uK*I{u(c(%s^`f>b-|5N+}1H7UT_@r=Lz2mfXCX5ILhh za-7%s2TccJzuqLMcZn<*r3%|ZH)lr9(*awa5Ih2vwoe#~j{qFwej5SG+S$Q$3XYXA zUraYiIIkIRggR8caEr|~(wDXt7?iyzSJ-R5FH*%g$^0Wy=0Y|i?HJVjaUk8+mxO#< zN7Pbaq=SOAA(e2_Y@ID}WFbu30P@`+Cc;OFw_GYiK{Z!~fp4yiFI#%t`4GpVmzgIt zoF%`0Qz7k&uYE#72VPQxsju_^khLfccQSajzDb$&qQ^OY8m1#ZR9;sT_eH08m*WIc zQL3WE>RKfao#$TpyRg5_s0!~E=5Et{hx`ZaN^gwWT+KD%449vBh-H^5sN*g;%~)!} z4;!&PyCE~iC?Csg&Dff=_(Wy|P~EqNaQxDCy4SSGl%VS-c-MSHKZczvBsYY-Hv>Nx zH-++U7xp%Tt2?_S6>-~xzoX5O z@>%O2Ss8&>=(;?d;hqWiVo}0vbM&G$%a3I=lwTI$_m<8`gsg_<;mRQPfUTc}ugAiE zg7+}h(~RX*17VC~M`TC2I%!8j4lQxM`k+k`g%U)tY5EM-tPhW&Daoa2<+h4tI1q~! zR|2(HENru77)!xZa$BR8CKiYFZ2$;LXOcf68>b_-A$zz-{w&%NMAs!cx@5x3rP5@o z?$PnX6rTogRlM3lSfaSqn#aKlQ_%gQCP{Ca7uh@CaBw2_0D6XzP@0G$O;R2sDz$9e zm%=}%b2;yJ%tJ;{xQd?jU^DdQ0tb0&&-ia|Ch!@d_t;AA-eO^;KzG8X=c#*>M(4Rd zqXF*znUu}aLpsdn;B<4)7%8AyY7fgHwr>fIK50VslujrUXw00YCJ_%Auz&Cq zU5&J?@Y|g4TKf%TPhuY%j`q>y^`;fm5{l#0Vb90xj)D|vi_l8 zCd`ptIEz-yz2JManh8)}j24?a3dZbmOC>cZp0J`NnRdVe6E3|$>ujlB)h(EAf=~IB5QusZwXv~b+r|*UW_xN3G>67R=VBOZoM*~Wseo~K70fhJ; zYdXa8$QCDKQJJh}QQ0q{p>fJ6r_bkI1`AOBmtCWt#@eodN7cN;YJF*qnkBAbiieme zZmBdhY3sc6s!^Q{_b8`<@i!SuaMs1g9o_LGO*{<`c>`DYP*RmQi>1{EB`onAX>pvl zH+CNAO!7vh#UH#>9&=Fx4$SPJGUnih_)wI>EGVBF;|46&EQ%y(UJ|-`&5+%vNoYQE z@+i1W%-N`~iiP8p48xUw3Vj<}^b(g}P}>vn50ol5toEAT}18LLBl z3ZWy4)YceghXOOV$7_N~qOQ=OXbQo5A{@(#PhtVCY)7Ko){(p+tM6@dl4Rf>5?Rst z5Z6D9ahoDzubqv!`eYHcrXHW1(ds3IcEtjgVLQTlKwf4MO6HjiFP+Y@x?+E7+eLtN z%hvqNddSv&^&f)m$X+qz-WhSLy~u^0%$k1%z5hMF{H-;h@hWJ4s(8GE1J6(@#xDw7l?{3lof&}8+hMrc$3g! zk6Y^wuTfsj8D$0Kn!5gCl0ay`PkkuT6oEcv*#&VC0s7gk1Vz3k$J-W z%3+2Lx7CrYuE5YUhu=C@&a!V*zg`iO$IpciFwLQqPAIm%fw9^LGwN%%jnbDT?Ds=k zY9pl1Vrb^ZvT3SX-Oes6`jJwI%P$4>+Hil0LuzdrfZ7@m@>y#Ji&fnx9qN`L!TkrS)0#mM2PAv6&<-BXkX|5WRO?xvd3GeLvDi4Q&B5kOe+29kMY#Ck^v4Js61}ES z-Kvt!7?$e9t}5z{FV$|#Uw!PF%SrJfrddff5R7@@+8JhQav$U+HKpL?;Tw4Km7&R> z2$~5Vdm<|F#c`S0vrl(TO4*&LRiU@SxDhq0aB&($$R(PYJ&cxg1cu`$e6Q!AIDlWy1+kZP2c~G*KBs$ZP2i$#V4zK>YC$SZCT&4b#Iwd?MRjgN( z-;XU#k@NBN3=tehZWt5qb@J?!nT*$I?#_X)M_;pAU%kgFJd8P}qLvZ7lr8kkX3?2< zBXR82L_J0ss}5lg=dy)R4}O%UExMrT z?v%2yv8S#qk{9WU!7_0oqOI#LU(XT5ncSjh)VM2dz3wGDimQdvNq!MedqfwYnuK>VTTp7v_9Edfxv;(zsXfFgk6vMXEMRP4D~=f zT0IaB@f7NPEb&hA&k7xhAG_Ip=VQVlygp6kpYZA9m`L z^L0%&t8bEw7bQ2wqI`?trPK*g{1qkl=j7C@_Bq&Veq5(p&@sY%J~eY}C;)@vi-u7V z820($TeK!3aixULywiy~RXd9{ra}pDI^x!jR6bFL&T#imRU%pPWkic$^9g|qhPKeQ zS3CkhM%uBd?Mvn_pnwHGe1$FE0`024f95)Sk-Un{wO&K3GMoOe#no}wE zzzuB-7N2AZW;5@F;XA9rM=Qyb^dY+3Ffi_fzw1V>pxZlkYMfcaME~gh&Ja(QbMi)u zY)LN_qgEHe$ax&OZj5t3aZ-Ugs=fKzsHNTJR^^kS4xd1(WE9-IPwBCOk-gthzbOX& zcuL(95Bi2P8$y>sg=vo*W7?X==L~qu8c)Ix5JWs0wJKC>|I8i@G4zBoeeqab%{32~ zT2)7iq4B^Qc59HG%68~08Ybo56|$vmS!d!>xl z0;??D;&&#=%Kv_ILERz}vb1VQ52d4^v6L8QLR7KC>Zo z7CiQs3curdq68$av5x{gtDgN^6CV=bB8ua3V}DcQ4d0$p>_p|Tb#WGc3I4A5G1IVO z5-9-a-aW&EoYM`Oj6nMy34+E?t}f<(YF51QcQ)=Ey2_SlNMnRtGW zsVMIGV~;TUMc$_$!o(0~%T82M)-U0YDgMSZfFdtwnKmx3dazBLmx@EXN;-E|j9OQ2 z`dIXrnIqV|{G*8;?MG(1%}N9vA`W5BYf&_tTtBP1xq*P=a%9nyyEC~5c^s=)8=7!d z%21*8T2HgmnLJOgYXc~hf%Z5|^vUmu?#Y;f7nC9v1%GK&+ra~kyVK9qDY@ssVPRh_ zOAtBUyxkb=#{IfI&h#S|`ziUcNPqF=L3J)!7L+bg!xUOwYwL^K8=_;HGQ zym}iv#x>SHY1FF`rD>`!_DCy@pkQBTU`rdn2kZT9dvZA)?5ev-{^1GKguzVNPgv4V z>aiqiW;NhepgS-2jUBr1Bc=K%Kr0X9>fg6C+do3F_q2tOFAg0|%>FLHD=f{zDXS6j zNZxZH=XQJUCYW)vo4eEMR;7rO>I&n_GZz+qxHj)4m@|9`B4_tcaTT`+UDt2DHJ3s@ zT@u_2eQ%@^D?fAM; zjy9}7S{bI#*n??Gv)ql@`9{fBxlSD&-d!OG2`oCKo-hg5jfH2VT8| zJpb$xHxA@(oT`5&bEHKm3&={sXSzw1e~LfTdbQb0dHiqZ3b%=yBfw;&Jy&aIdM1?nF%=77qwrM?S&ky{~~b) zXGVDywT3S7iFY{on{^{I>sI-}D-w1vEJv`(6HdP??3Y*IfaJY>bJ9{P+?!LZHK$I=MZ?Uoksh?D>!ZQUH%I4fXDV8i2UIY6tnX=m0q%Qy;Jwf zHHU&0`j1Ou#AVr6Qn#tLIHQmKy^9a4RsIXwud!^uWMJ0y)9qnPR(UT5EIQO^7&5Ac zm?L*oCYNP4Dx>Ih?}YdAvtp-M!PpI2a>l0!QMDbx>E(+g z`3ljMgH?4UaG$9P`HLVx<`Qv8M-;ZRmHT=QJXL5~Cjc={p@I{;0$ z+d@Bkvl!v>kdP}7|LRDC&{E=I0nz;(gJ&{g%=l*R_UO!al$F~GrVdtUpTr9yGZh%N zT-!SpMiPj```-IeJ!JB&l|No?RSZAx`1JZmP4+6<@}n3zh0j=S|3Elpq!#av#MQ+1 zk>&StWw?`yV%VqhJq-h|V>UDGe;A%GI1#~wJP3DRqHB&i9bbl*W~$@(EVwB-XPERW zzNl>1qKqa#uxR-BN4go+?(>`;_2!QRW?#7iHAic(D*lVO{udg(@hS9o;Jw`!ef1#= z(l^Kk{qKSQWl`foLEC{JmZw#z1^@4pzuUBU3qTXr-3`;i)p~WM65lPmyiR%w)cJt= zP!kQ$SzC#{A>>5fP!Bv4YyVnjCXO|4-yt?)zuixU0=e*w_uzXe?Y9qbf4W`Vy-nqr zT@424Z<_4MMSKRGvdLN2LrR|zjdOlgD$hLTNYLYTzXZ>*EPgY$yI#^DTfmQhaJ$<& zlP%(Xi?c8$^zPo=at>a0&}NDs)K$IM?+9&!Lq_I3;Oy$UD4vH05%%umIf}gJZ!KAH z&%R+rb$tRqB-m&R9TfE|M>-B)&!f5JJv`NQ)n^9e>%f*M^Xn?-&CpkbienLxq3ux} zNxz{9a#~OKVf&e2z&9c(`WP_rw{5<~;Sqagg2B!?i|r@LfE#Jx1D~-N>F+l5;Pp-Jp}+24 zUX#Yuj!ET#OhKs|b#pF2&LA*d<2vDYVvsUbWn8&`G0uxFN-Zu_>HYJlS6!sf^zX6B zCV=kT6+({80bWUAD6n*;Y~}!4LU}Vk#1T;Wta%u0NS2By}Avt-AZ7hq=}S zOX)h%SCx3-8SMt#t(^|GYsp`}_g5?5j#}R7~eGei?a8 zy&c?oW@Solc_JSNym|e#xPo}AZL)1TI-&j8l^*%BcnpQBwpv1z_x`{5Q%hci$aw%k zd1wCZQt|li&+Ry%fsUeP>852XnG0dcs*Po&jNm6voXO%aOzQJUPbYRu<+{5Fp5oor zuR#M_!MQMvdV^N|o94c66sdm5CdsUi>AlUe;;*30xD7e3SQn(y!=4#>!}QUZhU`%$q!rgW2QU2-)?9=UlIr zQbAQ&{6EZ*V;a%?0LLwoMme;fhuBAO^Q#B8dmpy6&W7W&AfBsML@Sg&_OVAVQ8?Z? z`s+zCz38X)1m%7RNfOpdo~bjVGzC(4!eu9G>dMf>W7>cCD2&CQ`>*b_R1h-^!rK!t zq%6V7&Kd~6ad>0(5XKXH5bfJ0zg_@g#uoMubNc-2E0>bg`oHTgQ#our*duL+HsDZT zWZL!i%oc$T#m5be{IK#xPoNEkn{& zF@DwcPdp=8W6^Z$M9<0`sSBY#zf(FVKd__^5LKYK>Oo*nf{{>EtkA#g2UqNO7~>dZ z5+1Htt5Q1uILT-9QPS^{hk4Fj$9N!gU#dAhc}}Kj;Ej@kCvO8+?kGxecPzY+)X&f^`i%4sU+Xf zreM3u(Q9~VYxau&w3rh{9!FJrdeoer#lkEh*HzfKHr^|M)>zxs+7?q9zFpw5$__>) zR_$-aDBm)=GonJF1#-8YiknLn(wUCj{d1yV`QkAeUaV6Jo76;;4xJIhKFzd-D!Mqg!mjjZLO%r(CC)1L$;T zoAcL(^jj3aI_cYA;`vAQf~{#O3)WiK6g>v)h&y+k6Z*C2zcUQ0!+~|tn1(0`gKi`H z*l$`}X{r2EzN(YHqC;L<0B~t{m-}=^ym{D4mOOvQuP!1_$kW7bGm5utgZ9=!>J1lK z7vN+~&-r)0%J<(DuOLX=-ZQUaA&qMO2rawyVdwLYNpUbHu`|JYUR%KIkK!4SzpqAC z4P=g1M8De6drR?N>keK#EL(0~ooQod2Em1HFN!JyS#Wr_VMRVNV>F*~)eQ%fGio~i z)^AJ0Y5g_E_g1U7{I`6meGXs?O^LCCs=dY`Nw>iji;*>OT>DyPh_h5%V~5-~FU-J@ zHn`G%uB_naLV!marYMQu;Lal7S2DzE5L%B37yG;lJ)WWwZ2_r|+yGkwozI+G$j$^l z^`;*9%6P+iuJ1sYDbPU5Nn)yeQy3yV_W-43;UtYeuCGJuxK`JWk-1Y6@=*x6=`F*) zAttuJo}+5T&hal(l>WJm6`t^W8lcI?_tn^jc>%TC6+dF=o&XW8w+reFSm*-;;hZxH zF%`DAHM2i};D+Z1N;o&o^1vtA&XD&|kDCrtoy02-HRJ2^+!lZY)SPsl!1PYBP`Ptt z?8s@HcDX{sZ67b4sA<$4P>`|F?JZvQ2#Yr==bVx0n|+B1OlwIX`zEBmKL4s+Y$Lp^ z6moj0eDGaf8Fxj-qHXeaPVNcZv4r_lb#SfgClaYmJRC3c&Eyn(~IJ3-F~H7rRo!5g=>@8GcdP!8*@;&JkiCW_4t|W z;#HU$yGOEr;s<*eLT2XLqxCA2{`V9CHE1setuMiz#3sR_Mz!~d`5Cjb#r5=?9$A{E^a+)Y$EeEdM%HfbJ;trE~b95s=Q6(>BOy$E;o%R_`<+fqj#I>A(ULEe$5YseFBJ)eJ7z=GdH?JvF- zXxvGjxOhqQuU+D{zeJ7KfNfC~|^(#MXVzmK;&U?%@5wZ8EU zOM!iH8cP7Mm*34;o9Wfi2@#5Fd%Ab!C9mhy*WCI|HM%lc5@-~xlD^y3zb@yu{E$_cwTx#`?0n-^W;<6K72X6Ou?G~MhZzSr7+f1-9FnWNP47A8}IQO-R;>!l^t^| zn^dDelP0|G;otLA;3wfw{3M?rG3ts%I4UyYnK#y}{*5MBfkM{GadA6k;&d{J9O8~R z!OC!C7jPvT`{xMi`VynIoln-rfYZ|3=#xvJbq3I!I!W`~&$bD5Z5$hPhEaB4%9CHE zkPz0nbp8D>Qk-#EC`7qB@-2Q2nTAW3@D2-w-`Etwyp#~;-&;}SrZ?<SITqox6Tb)R#o6CnV{_2n~msg?6%@O&}%V-H(R&awCb08v8)AA0A=pvrC+oW1l z0|sovu@><0tmFx^WGOK)*9M=RjoVZR3E{%rb85?if7SI{?W-kOZCP!ZM`a!G3FrSV zGgrQ8Nd29{FuArNv=Vr|0&wR5 z>g((&GDB7{0l@T`kD=(spG)YQjtl!*Ba77eTK?P`7v6x580QAi2dB}l4*vl zA-P83|CDvsVNo{StF%s)Caxnzn*vds7*brlLLFl z`hur@;f@APj6mL;wY-aT_gUn5y7I-lFFj8CWvu~dC;i*MSk@={v?h#tzZ>{}#$!!1 zw~c?5ivCOU6f2r1JijEn!W z0`ZVR>f@9m`FpeyGtEILcEAq~fo?0{m-o<@eqzLez2@;0hJqn>4#GCqS=>z*V-fFG zg7#$41Oh}z`@XGtp5%c5vZ;Ks4L71uC-*wy5~2@On?%I{%G!@9NW;wA>xr{1DLhE% zSi5+h>Sg{Yji40!u0oq|ge@_^E@=yU9-ZW@d%twCPu?*k6pq?YCk`wSWEE{rp%*9|#Xv|8Y^^Ogf7dwi&mN*xKqoN`x^z(at?wNW1 zPaz}FWJ&8zTiuU&^SWnXB}9s~$+vocyq+ilJ42!fE~~+J8iYg{{bHWb(dMC~yF^$W z!9cYPpC!NL^G}nd3`30sZ-EDW>VNSr?$iw@xfqcS!6D@~q!*y3YAX zXS{?tT~)94B+*P;^}c`n;XWSRK)lRmWxg@$-q4fldPxgKCki5q; zqKz9gdE_@+_HmPG4B_Q;8SkCyAAb4fc{UYVUsUV8$|plvA{+>yY|QO>Z;?SMq19*8 zHOxMIMJjh=YkHPU{t9)&VzXqDf|@n4Cpr-ZhO*Dzw_KvWf5L8PI+ve0?@Oe^OCL>@ zywtOna)0rYsXa%zLRF0Sdj`?p)9gz>A+RqhiQvNe%h5-R7CGvoURso{mj|zhuDam% zuA;TO%Zq-HcZq>Lxd2 ztxJejv-MM#wD^{y8pMZ>zq?d0naPrS2YoTQe6C)NiOtfxbe-LkNFUT)u=O5@$>(lD z473Q^N9cR&!Q=wTm^ED5fnlDQRzW)c$K6s19wW-7MfGzRpkeRqCw-Trq5+QL+flVoE9qN5izSRpz~w9h&TokeC}gwQee1?{hCqm8|P*&z@QML@IN}=RR*h zG0yYvsH1=vPsFgb%7+}}8H`6cvMJuyx`r3-3in6pEPK(!$zm$bTL`jL_K^5HA9>_VOU17DyTErqy1*M6_yl zhB%3be`O!eL3}mTWu5hILjr-|c?cJL<(2Th815Td;=6eju3rvqJ-|KDLK;C;uQoI# zdyibD!<;IFGET2?o@-J5dSM)0=sjt5-TidZUFRgTe$XRzgAdd?dPMg8M-VtkX^CEK zU1@b-&kh|rh|p@dy}s{J&S^o5O}}BFpeex*$vP?Tvkg-@=pr8$=Vx49Y;1_^-WtcN zw$FTPE9C@09v%#%mh>2yS*^d|a65n>CO!o+7T#K8_YEjL0bwWW9%_k^m^DITs4B<` zgo-hg?ldeLl&baNGU&k@p7n(a8=R@t;+rz~(37|!PqEyi?uqE|po>t)xW5eRuPQGI zvF^!jv)?QG`4s!lyIWU63pK ziIB$TgRz%uOg^|(`$zWHcvId+9VFM;)tUcGy*@U%nqrSnw?riU*Z;|4ak^Ji=pR9< z>r1D!{$1w-1mTwKvUAe&sr{ab5^rlW6`Y)EZawzN+rsUUitSYU-81i=nnJyqg%~nD z+0siT|L)EYiY(keHha$HD}vrVpMK6RB}i_;l6^0y3yTFcO0z~s3Y*5iM@-pgZU^aGDBqU< zL2w0`AUq=27HOVmRM(5?LC-tAO5ys`u0*ZB+LB?qo&VhxsYReR%CThfu617!Ixz*g z?HtQRf<%9i*AJNFq4@?+qi%ZKf-pm!_CMc88yoU6kOVvOD3#oed^IUB&`sHQ*GQ%a z^oi^|q%`gIQrDD@fK>H=>mt|uJR0prK5t%;#-?N>E!x)dri5`V=ov^_^s1d~DJJx2 zPJW9JV^d z{$nnRUn%vEhTUA~RzGN%Os<4op7qCwU4VQ+{7msVnL~HL%!EZ`$QH~g!C_598=-vL z$tlG0rj#he{e)MeZ&yumpQ*^v-R20PDL?{0(L#WG@GelQ{`Y|v4-s*K?QUG0T|dXd z)+_d`n-*&~$y1?o)A)$g+!u{o;%*fJ@WFr{Jr3~+i#_Bk*A6CVakT<~QEGJ8e)(AZ zIm7ND->&7_{O|?vp|!J+E1hcQEBO4ukAj_Fqk#GbwE)t*s|s0A#bFwHdPh<1lEth} z@1Is(N@7thvk9T{U2d!emzTrrcUyGy+G`D;tP<}EJ2vKW+*`gl^)B5BG{gNBe|zn{ za84FsS~H#x=*jI7#S^T%ko2?BW_u1-7J7!q4llgfxR%mlBDJJ!mI{>I<%g~ScFWy} zhuVXdlLs{WjaiRb+_W+c-rY z`7+0MeFGizXtkEHV4S%y+DUHnx&kgEU1-no#7I&BGwN)lNY(b!!Gd800qWu9c@Mpn zli_r?nh|_yh{1n+2U!R1WeX}*f}+K|khMLw(Tx;Owv_cC0kbisC|+Neiy-j?@v{?w z4c_wCu>{Eq-lGw^VY@mVlHzEqa9^fDDvGfT-b8%*Os|KyjtBD2i%$mKdbMOd%9dLw`m;`Qf-Wtld`872*F*36S#sP3MeRxNO$e>rf zsqkmW`aw`s6TeAJb%HpVn`vffB2jNT)9W)PWgRbIRMOlLB0BSjtl$0e!RSY)OAzUk zNaECG7U7!!tX@u;N;?-Bdx44&x5)=Beg>+AV%{tDDZ@Yz7Ks~uOthz|yUef{&=p{Q zxvN&B;DQ?@!q!o$u8p0I-llanwg}#a+7$-p5R>?M&dp$7on2k`IgEC4ns?gKF)(Bv z4O+PLhy$5BxjSwT0i2F1n-@W5m>t$Lc88pszstn$?POw!%~(6;X1tds7j?DzZ; zf-%^;5TOHKM)vp7CA1q@b0-LpqT(KBQow%g=biY}u) zJZ7n9uj`9S-{4?O`=VUoTEecq@jhN>1iZtVrO9JU9l%^f;&6ED=X@qQn0L#NY%J21 zbN551Y-GY&ZNq9=-Dw|-OuIlDSP`#;pbA+dQLs^id;kejJbQ^;#ngKjROwC$3!WtTD=-ejV}>FrnH91=Z)}m zcb-hhUT9MWUF^E}e)Nu&^(3UuP8&duXRMr#UsIvof7KX4NZ%~C45GuPwb1j|bS=UV z_9o~>+y9)go??$wTa{f3?gb)M*n3u%BkTjOEoduxaSQ|)qbqpZYV{JTmSDwsI%jZ>BPr2f!=IF9YYFH%)SWmX|p=ILSCd%I3_ z40}?se&eM5hTSo3uqHoWJ9~V*(yyeA#cIfOHIJL1MmJa1>&6vNWW>!CjzE_tULo|} zv-mhwI~S5$tQl6D;&XUj3s zM%2&)0BiWrCokaH@JrJ}@MlsPUcmy4Q7~4Hh}yEt^`mmfh=ZWio3%~coyRk_@^>mA zHY^2gRPiZN0OiD+Uxl;<3M?{g1S~_YJ@4Nz{gw}+W(YUtG+K2=uq?>ocD<|iQmJv# z#7ATqNpY3wDsK;ASp{;@oi7?G=Ey{F3tKzrUScu*<~3p!;v!a25hTN&0c)c+rUggVv?am9Iy~7K8#77T<;Lek`SI{K7Lhs?o_w z6Z05z;(0h%SJ2NMrFDC?6 zU25!F_r&iya@E+E+1BT^5;*(&+z&c7ji=3Y6$CB zSg=6GBU)%xr)76`>WVk}?!~zPJa-iPBCTxh?FYP^%U%7a>AfY`Z;GxqGlAx$XOMK+ z8R}8?P1rrNi}~x$n(kX@>s5)#HjbEa5IE zh;=xa%K_+mJ;pc*w0MUd7pUoBfclhX$#n3l(=BHva;Bpgbv>RH1m>W;*vqxut=p>t zQQD=@OqZm(6hOWY70I3eWhM@y<6xh{wL(nxEi-8PH` z?Nzg&Z$mB}vZ5I~AO}XAJS6irtbM!lmfLR4wizsxKib}qCB`T^JeFnJ{3Lb%JjUY^ z%Bn8NtSjVs*d?lw$|OqroPu_&e8xP|fdJ6;?xu{y{m5f&amVnf^FkZ}>D>?~G&c&( zTqBVDOwotFb7p|a7IRgi!$@Ay6>xU3Cd)s4P4yNIy**Q|!x6Pq5`bez59=7)e{e%s z8^Y_ge^!No69O6rQsO>9f_hR%EAKeIch`c1u8_>PIv9Ff%#b z1|j{jzJZ?{Vq==3BqgFv@ut&`)cd+j_GOL&rK&rEVw6kz`f0A9Xd zWu1w+5+t31=@S<>xWHUf{u6a8Kfnqge2A?hr@$4deiH9Hxb-pSVfZ5c9=Mx}^_!(g zE{SR5x^QsfxPa~%e?`rQdtLfMEf;pS&~rNhR3rI4=JNI<15Ts)IetF6H1h<@4?dXp zSl0$tbhm#{W+oxT#@!=l43pxl+g|Lx=qJF-~oi0T-TK)@P6Ejjfpe zNau0i|0>-}OaGWXvWe7iS!<8nDvow^^Hj!1psP2;hXm+|1@c4aG218_DnM*_vEB=GhtEb!}{+^^{&c*r&?~x(;*mLLf(z zrX6Aix>^m7-EV5Kf$jzhzuF#<_&vfq99Q(H)D@FuK)sjDxTgM1zRNTt9#3$VOb2Hk z0pXl}@C+f2O$r~401-e`NjN6E)Oj;dm{$|aQ0S>U@TzPTb(-%7Tn0L~h$)!unH$^i z9~%MBNhT^huj^@^3Az+5)B)D_$mkZS2OX;A&_i2^sA+Mes0=(3@2-sKV>8n9^+w-i z>34y$3lPJ(VkTM7gflAhkAG4m%CcA{#H!!3;6n;Xl1z)aYKgY*`_m)n4z7swWaM6R zH~N_<^vo5`w0pk#@k-!>5e!kPUqrS$Vgrkv)91#7RO@nQH zTr}lLnPfM)ik&t^w(p^UfJo{`@lmqfuQ4TMVn$)ST`C=Gh`ZkrW1KT*yR5HPis~|C zE001J4`T;Keex)dk}a}hw@d1+j;raR7)bqPlx8PW+XZ#`$~PWH+B5E&Usww95dGL@ zT|p6Lr;cKrqaGz3&N+^RkGn^`yK;$v{#XU}?xN{8KID8{4P7a;!v=oYy#xafabp8B z*_*wujU-`wEoU0cYMON4D01F|do&j#n&)`|ycVzo-WEA6fcsn3+WCOb}QD zX_UcsC$|@5lBPSiBuj39G^}G=NM2BdY6KSj;6N6gB3ycNf2LNhXRv+U2YWS*M3_i= zRE+=zmPfc|6pBOQ>oa(X4Pg@GIXP7{A<&ZyE46kpG^49vz`mX{8pqqcC!_6G_q_&y zU4RUDmde3uApc4tG>cn(8w-K!uRRrbB9M25%<0^GSB+rGZb=?dJ`_^+9K zxqnLf4buJ32E>9bBk%WnGmFm;3A+S=2a9YA$LdDV_2j=KJ*g;=sHxXbpi9StUL@>9 zZ{gatOeU9}X`g0^Y97>st3W!|Y{O7~?U@3-|Kfeange&$CbuwzIs%7!br^YOnIFM_ z5T%qR8gyhn)Ourqs+<}U7ndXI=@*k-2(Fee0VCsH2^iOq*qz-4GJ|_Q=H+-p(tr&( znWa?PPZ}w-!1OM)+A&&s-TBnq=)i;2;q>bCWZ8XSNXx*Ye)t8TqMIU|CQY1cp|fjei8OY9gUYGJU;Zn zp;`h_;nex+z>@J*$ks|Q~oCTaE6BEe*@mE?54?z*F01vHAPI7UwwF~E~#fu>Qc z8pkana-YQI)-R^)(EztWGQ+Q-cK23O`moGOT?K+?Ofs_X^IQOZfej*?N(mnLq9f^d&JH@Z zI)TVb{BmCNmn!VGi{FE6^;%dreJ0xty2Ko7grl|4k*%@KtFOLde>fRsivL(bYu`C@ zSq+7BOJA3vw&P)ue!UZHCP&>iEGxg{TVkf^-Wipji|84X08NuTB4(t84r-bVyJQn< zY8k>Ie)7}&n_@AlSsupRQ*zx<=}Natx?N|+gSR$Nsvow+D2_{mZ>8#;0^Q6C~p{F8Yn1LKbW3XEN@ftUuy z59ugVsgW#-MaF2TlE)I%AGQ{|#ChdR`|W9&VVUPXGF=jeG`=kWCi-*4(_a^qcZBM?6z}MeqSx8N@ zh}~qDH^j-2A~HcQ;=)}K-vq8gAEcCwG-jGqg1bmf>uTwjaE*yYO)tVT+Q2X0TP&B| zsN;3cxS>MdbRYN_1u*Fb0pUZ&)d!zIpz0IQ&{>KlrHn*Ea$+m&Z!&YcK8R4iO@8a( z^`$>4Pqpl!NoQb|Hb)02zFZuB{7m#|Ox9$^D)ELPDGMhT?j@Cj` za#r~Jpu^rHgSfC^Cm%h|PN)7L!YQeW}Ku;g=0@ld|(%W7`BQ}Jo zM`$CjHvCVn3ma4~bIkvn@#g!BX~3z!T0b{Su6o*#8SR3yb(Hjfm*0U=u+sFnDVKRd zGxHPkfcn_TFBIE=86H6(H&Xr0?mkkNLRvmIG5*(wfS3P^b-8{N_MD$OY+5l-(+a7J z=P1l*V=XOp%(yvW=Yg);i3!Fa-H?g)z@(HFHasBQQg~5`2SSd z*?3kKv;+gPW@9;M()f^FuUeQs{ia>87IW?w_JxlB#Z6Y)m|}8dU~J5~-q=koQuOlX z3RX2tY4Hq%O@$JF=thL?UjRj`>*{#iP_$fxO0Qxr@(tC!!`g8519exqIvt|le%KU9 z!7p55h_cS+(dH2Be;6Fu-&u2`Bw-f}LV@+_S?s^?T|1GE;s*bJeZ(Icw<1k;3)dJQ zIFbJ!5~rz1ScEbH{jK(o;pt<;BX6-C68B~FpFI74wD7@x68u@Y{v+KEW?T7?|9!!Q z30^EMsQPbir%3c$9y_-|#YYZKQ?R;URtJfHJN4rKN-%Q&BjJ|yG(v58rXX(_PSir3 z_irlrqk*umsA0=xY;mNd7vS+p3dlqQf)Ni&-R|f=FoAupd-iV*DXogD>=g~8&Jy%D z@oMN2+6@KxTm-dMGqR*k@3K$1@O=M2;`-QX>WbDaoRi9`Eh@fRk~ajT!r~5o@vb>{ PZXXR5UFGt}mhb)_lV { let select; const defaultProps = { + name: 'name', options: ['11111111', '22222222'], value: '80', placeholder: 'phone', diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index b52ae204190..087e5cd7c4f 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -353,7 +353,7 @@ "Account default": "Account default", "Team members": "Team members", "Full name": "Full name", - "Leads": "Pop Ups", + "Leads": "Leads", "Email Signature": "Email Signature", "Response Template": "Response Template", "Email Template": "Email Template", @@ -617,7 +617,6 @@ "Amount": "Amount", "Owner": "Owner", "Department": "Department", - "Lead Status": "Pop Ups status", "Lifecycle State": "Lifecycle State", "Has Authority": "Has Authority", "Do not disturb": "Do not disturb", @@ -675,7 +674,6 @@ "You can only import max 600 at a time": "You can only import max 600 at a time", "Invalid import type": "Invalid import type", "Last updated": "Last updated", - "Filter by lead status": "Filter by Pop Ups status", "No lead status chosen": "No lead status chosen", "Schedule:": "Schedule:", "Every Day": "Every Day", diff --git a/ui/src/locales/es.json b/ui/src/locales/es.json index 96c8f4006c4..3ce6f04d371 100644 --- a/ui/src/locales/es.json +++ b/ui/src/locales/es.json @@ -353,7 +353,6 @@ "Amount": "Amount", "Owner": "Owner", "Department": "Department", - "Lead Status": "Pop Ups status", "Lifecycle State": "Lifecycle State", "Has Authority": "Has Authority", "Do not disturb": "Do not disturb", diff --git a/ui/src/modules/activityLogs/styles.ts b/ui/src/modules/activityLogs/styles.ts index de90611d7e2..6a8ca7e80ed 100644 --- a/ui/src/modules/activityLogs/styles.ts +++ b/ui/src/modules/activityLogs/styles.ts @@ -119,11 +119,14 @@ const Row = styled.div` margin-right: ${dimensions.coreSpacing}px; `; -const AvatarWrapper = styledTS<{ isOnline?: boolean; hideIndicator?: boolean }>( - styled.div -)` - margin-right: ${dimensions.unitSpacing}px; +const AvatarWrapper = styledTS<{ + isOnline?: boolean; + hideIndicator?: boolean; + size?: number; +}>(styled.div)` + margin-right: ${dimensions.unitSpacing * 1.5}px; position: relative; + max-height: ${props => (props.size ? `${props.size}px` : '50px')}; a { float: none; @@ -135,20 +138,15 @@ const AvatarWrapper = styledTS<{ isOnline?: boolean; hideIndicator?: boolean }>( right: -3px; top: 32px; background: ${props => - props.isOnline ? colors.colorCoreGreen : colors.colorCoreLightGray}; + props.isOnline ? colors.colorCoreGreen : colors.colorShadowGray}; width: 14px; height: 14px; border-radius: ${dimensions.unitSpacing}px; font-size: ${dimensions.unitSpacing}px; border: 1px solid ${colors.colorWhite}; - z-index: 2; + z-index: 1; display: ${props => props.hideIndicator && 'none'}; } - - > div { - text-align: center; - font-size: ${typography.fontSizeUppercase}px; - } `; const ActivityIcon = styledTS<{ color?: string }>(styled.span)` diff --git a/ui/src/modules/auth/components/UserCommonInfos.tsx b/ui/src/modules/auth/components/UserCommonInfos.tsx index 470061284bc..0cd01f91c58 100755 --- a/ui/src/modules/auth/components/UserCommonInfos.tsx +++ b/ui/src/modules/auth/components/UserCommonInfos.tsx @@ -1,13 +1,10 @@ import AvatarUpload from 'modules/common/components/AvatarUpload'; +import CollapseContent from 'modules/common/components/CollapseContent'; import FormControl from 'modules/common/components/form/Control'; import FormGroup from 'modules/common/components/form/Group'; import ControlLabel from 'modules/common/components/form/Label'; import timezones from 'modules/common/constants/timezones'; -import { - ColumnTitle, - FormColumn, - FormWrapper -} from 'modules/common/styles/main'; +import { FormColumn, FormWrapper } from 'modules/common/styles/main'; import { IFormProps } from 'modules/common/types'; import { __ } from 'modules/common/utils'; import React from 'react'; @@ -27,151 +24,162 @@ class UserCommonInfos extends React.PureComponent { return ( - - - - - Full name - - - - Short name - - - - Email - - - - Phone (operator) - - - - - - Username - - - - Position - - - - Location - - - - Description - - - - - {__('Links')} - - - - LinkedIn - - - - Twitter - - - - Facebook - - - - - - Youtube - - - - Github - - - - Website - - - - + + + + + + Full name + + + + Short name + + + + Email + + + + Description + + + + + + Username + + + + Position + + + + Phone (operator) + + + + Location + + + + + + + + + + + LinkedIn + + + + Twitter + + + + Facebook + + + + + + Youtube + + + + Github + + + + Website + + + + + ); } diff --git a/ui/src/modules/common/components/AvatarUpload.tsx b/ui/src/modules/common/components/AvatarUpload.tsx index c7406847234..be2a274283a 100644 --- a/ui/src/modules/common/components/AvatarUpload.tsx +++ b/ui/src/modules/common/components/AvatarUpload.tsx @@ -2,8 +2,6 @@ import { colors } from 'modules/common/styles'; import React from 'react'; import styled from 'styled-components'; import { Alert, readFile, uploadHandler } from '../utils'; -import FormGroup from './form/Group'; -import ControlLabel from './form/Label'; import Icon from './Icon'; import Spinner from './Spinner'; @@ -11,7 +9,7 @@ const Avatar = styled.div` width: 100px; height: 100px; position: relative; - margin-top: 10px; + margin-bottom: 20px; display: flex; align-items: center; overflow: hidden; @@ -135,21 +133,18 @@ class AvatarUpload extends React.Component { const { avatarPreviewStyle, avatarPreviewUrl } = this.state; return ( - - Photo - - avatar - - {this.renderUploadLoader()} - - + + avatar + + {this.renderUploadLoader()} + ); } } diff --git a/ui/src/modules/common/components/ButtonMutate.tsx b/ui/src/modules/common/components/ButtonMutate.tsx index 2681c4477a7..336889313c9 100644 --- a/ui/src/modules/common/components/ButtonMutate.tsx +++ b/ui/src/modules/common/components/ButtonMutate.tsx @@ -44,7 +44,7 @@ type Props = { class ButtonMutate extends React.Component { static defaultProps = { btnSize: 'medium', - icon: 'checked-1' + icon: 'check-circle' }; constructor(props: Props) { diff --git a/ui/src/modules/common/components/CollapseContent.tsx b/ui/src/modules/common/components/CollapseContent.tsx index 571c8bae82c..404915b25c8 100644 --- a/ui/src/modules/common/components/CollapseContent.tsx +++ b/ui/src/modules/common/components/CollapseContent.tsx @@ -5,8 +5,8 @@ import styledTS from 'styled-components-ts'; import colors from '../styles/colors'; import Icon from './Icon'; -const Title = styled.div` - padding: 20px; +const Title = styledTS<{ compact?: boolean }>(styled.div)` + padding: ${props => (props.compact ? '10px 20px' : '20px')}; transition: background 0.3s ease; display: flex; align-items: center; @@ -28,6 +28,10 @@ const Container = styledTS<{ open: boolean }>(styled.div)` border-radius: 4px; background: ${props => (props.open ? colors.bgLight : colors.colorWhite)}; + &:last-child { + margin-bottom: 5px; + } + ${Title} i { font-size: 20px; transition: transform ease 0.3s; @@ -47,6 +51,7 @@ type Props = { title: string; children: React.ReactNode; open?: boolean; + compact?: boolean; }; function CollapseContent(props: Props) { @@ -56,7 +61,7 @@ function CollapseContent(props: Props) { return ( - + <Title onClick={onClick} compact={props.compact}> <h4>{props.title}</h4> <Icon icon="angle-down" /> diff --git a/ui/src/modules/common/components/IntegrationIcon.tsx b/ui/src/modules/common/components/IntegrationIcon.tsx index 8aab8d30398..cd0314ebc14 100644 --- a/ui/src/modules/common/components/IntegrationIcon.tsx +++ b/ui/src/modules/common/components/IntegrationIcon.tsx @@ -25,7 +25,7 @@ const RoundedBackground = styledTS<{ type: string; size?: number }>( (props.type === 'gmail' && colors.socialGmail) || (props.type === 'whatsapp' && colors.socialWhatsApp) || (props.type.includes('nylas') && colors.socialGmail) || - colors.colorCoreBlue}; + colors.colorCoreRed}; i { color: ${colors.colorWhite}; diff --git a/ui/src/modules/common/components/ModifiableSelect.tsx b/ui/src/modules/common/components/ModifiableSelect.tsx index 97ce28d7265..e1258d0c9a7 100644 --- a/ui/src/modules/common/components/ModifiableSelect.tsx +++ b/ui/src/modules/common/components/ModifiableSelect.tsx @@ -1,12 +1,45 @@ import React from 'react'; import Select from 'react-select-plus'; +import styled from 'styled-components'; import { IFormProps } from '../types'; import { __, Alert } from '../utils'; import Button from './Button'; import FormControl from './form/Control'; -import FormGroup from './form/Group'; import Icon from './Icon'; +const Wrapper = styled.div` + display: flex; + align-items: center; +`; + +const OptionWrapper = styled(Wrapper)` + padding: 8px 16px; + font-weight: 500; + border-bottom: 1px solid #eee; + + &:last-child { + border: none; + } + + &:hover { + background: #fafafa; + cursor: default; + } + + i { + color: #ea475d; + + &:hover { + cursor: pointer; + } + } +`; + +const FillContent = styled.div` + flex: 1; + margin-right: 5px; +`; + type OptionProps = { option: any; onSelect: (option: any[], e: any) => void; @@ -16,24 +49,19 @@ class Option extends React.PureComponent { render() { const { option, onSelect } = this.props; const { onRemove } = option; - const style = { - display: 'inline-block', - width: '100%', - padding: '8px 20px' - }; const onClick = e => onSelect(option, e); const onRemoveClick = () => onRemove(option.value); return ( -
- {option.label} + + {option.label} -
+ ); } } @@ -42,8 +70,7 @@ type Props = { options: any[]; onChange: (params: { options: any[]; selectedOption: any }) => void; value?: string; - placeholder?: string; - buttonText?: string; + name: string; checkFormat?: (value) => boolean; adding?: boolean; formProps?: IFormProps; @@ -175,7 +202,7 @@ class ModifiableSelect extends React.PureComponent { }; renderInput = () => { - const { buttonText, placeholder, type, required } = this.props; + const { name, type, required } = this.props; if (this.state.adding) { const onPress = e => { @@ -186,54 +213,48 @@ class ModifiableSelect extends React.PureComponent { }; return ( - - + + - - - - + + +
+ +
+ ); } - return ( - - ); - }; - - render() { - const { placeholder } = this.props; const { selectedOption } = this.state; const onChange = obj => this.setItem(obj); return ( - - + + + + + + Email + + + + + Description + + + + + + Parent Company + + + + Business Type + + {loading && ( @@ -138,17 +156,18 @@ function Uploader(props: Props) { ); } - return ( - <> - {attachments.map((item, index) => renderItem(item, index))} - {renderBtn()} - - ); + render() { + return ( + <> + + {this.state.attachments.map((item, index) => + this.renderItem(item, index) + )} + + {this.renderBtn()} + + ); + } } -Uploader.defaultProps = { - multiple: true, - defaultFileList: [] -}; - export default Uploader; From 84dcb23600c18e6be6ea8b757280bc7c06570bc3 Mon Sep 17 00:00:00 2001 From: munkhjin Date: Thu, 2 Apr 2020 12:10:29 +0800 Subject: [PATCH 096/110] refactor integration kinds constant --- .../components/list/CustomersList.tsx | 1 - .../components/list/IntegrationFilter.tsx | 8 +-- .../customers/containers/CustomersList.tsx | 2 - .../insights/components/ExportReport.tsx | 5 +- .../components/filter/InboxFilter.tsx | 5 +- ui/src/modules/insights/utils.ts | 21 +++---- .../components/common/IntegrationListItem.tsx | 6 +- .../integrations/components/store/Entry.tsx | 32 +++++------ .../settings/integrations/constants.ts | 57 ++++++++----------- .../tickets/components/TicketEditForm.tsx | 10 ++-- .../components/TicketMainActionBar.tsx | 11 ++-- 11 files changed, 68 insertions(+), 90 deletions(-) diff --git a/ui/src/modules/customers/components/list/CustomersList.tsx b/ui/src/modules/customers/components/list/CustomersList.tsx index b15a118ff1e..809d18da878 100755 --- a/ui/src/modules/customers/components/list/CustomersList.tsx +++ b/ui/src/modules/customers/components/list/CustomersList.tsx @@ -34,7 +34,6 @@ interface IProps extends IRouterProps { customers: ICustomer[]; totalCount: number; columnsConfig: IConfigColumn[]; - integrations: string[]; bulk: any[]; isAllSelected: boolean; emptyBulk: () => void; diff --git a/ui/src/modules/customers/components/list/IntegrationFilter.tsx b/ui/src/modules/customers/components/list/IntegrationFilter.tsx index b55e1884192..5ca026afbc3 100644 --- a/ui/src/modules/customers/components/list/IntegrationFilter.tsx +++ b/ui/src/modules/customers/components/list/IntegrationFilter.tsx @@ -3,7 +3,7 @@ import DataWithLoader from 'modules/common/components/DataWithLoader'; import { IRouterProps } from 'modules/common/types'; import { __, router } from 'modules/common/utils'; import { FieldStyle, SidebarCounter, SidebarList } from 'modules/layout/styles'; -import { KIND_CHOICES_WITH_TEXT } from 'modules/settings/integrations/constants'; +import { INTEGRATION_KINDS } from 'modules/settings/integrations/constants'; import React from 'react'; import { withRouter } from 'react-router-dom'; @@ -18,8 +18,8 @@ function IntegrationFilter({ history, counts }: IProps) { const data = ( - {KIND_CHOICES_WITH_TEXT.map((kind, index) => ( -
  • + {INTEGRATION_KINDS.ALL.map(kind => ( +
  • { customers: list, totalCount, exportData, - integrations: KIND_CHOICES.ALL_LIST, searchValue, loading: customersMainQuery.loading || this.state.loading, mergeCustomers, diff --git a/ui/src/modules/insights/components/ExportReport.tsx b/ui/src/modules/insights/components/ExportReport.tsx index a463d3beb08..3cd7117c5dd 100644 --- a/ui/src/modules/insights/components/ExportReport.tsx +++ b/ui/src/modules/insights/components/ExportReport.tsx @@ -1,5 +1,5 @@ import { IUser } from 'modules/auth/types'; -import { ISelectedOption } from 'modules/common/types'; +import { IOption, ISelectedOption } from 'modules/common/types'; import { __, Alert } from 'modules/common/utils'; import { menuInbox } from 'modules/common/utils/menus'; import Wrapper from 'modules/layout/components/Wrapper'; @@ -15,7 +15,6 @@ import { InsightWrapper } from '../styles'; import { IQueryParams } from '../types'; -import { OptionsType } from '../utils'; import InboxFilter from './filter/InboxFilter'; import Sidebar from './Sidebar'; @@ -54,7 +53,7 @@ class ExportReport extends React.Component { selectOptions() { const { users } = this.props; - const options: OptionsType[] = []; + const options: IOption[] = []; users.map(user => options.push({ diff --git a/ui/src/modules/insights/components/filter/InboxFilter.tsx b/ui/src/modules/insights/components/filter/InboxFilter.tsx index ac880672727..86eba79a997 100644 --- a/ui/src/modules/insights/components/filter/InboxFilter.tsx +++ b/ui/src/modules/insights/components/filter/InboxFilter.tsx @@ -2,7 +2,6 @@ import ControlLabel from 'modules/common/components/form/Label'; import { ISelectedOption } from 'modules/common/types'; import { __, router } from 'modules/common/utils'; import { IBrand } from 'modules/settings/brands/types'; -import { KIND_CHOICES as INTEGRATIONS_TYPES } from 'modules/settings/integrations/constants'; import React from 'react'; import Select from 'react-select-plus'; import { FlexItem } from '../../styles'; @@ -57,8 +56,6 @@ class InboxFilter extends React.Component { }; renderIntegrations() { - const integrations = INTEGRATIONS_TYPES.ALL_LIST; - const options = option => (
    {option.label} @@ -73,7 +70,7 @@ class InboxFilter extends React.Component { value={this.state.integrationIds || []} onChange={this.onTypeChange} optionRenderer={options} - options={integrationOptions([...integrations])} + options={integrationOptions()} multi={true} /> diff --git a/ui/src/modules/insights/utils.ts b/ui/src/modules/insights/utils.ts index 01c9943e3ee..ea243e46b0f 100755 --- a/ui/src/modules/insights/utils.ts +++ b/ui/src/modules/insights/utils.ts @@ -1,25 +1,20 @@ import dayjs from 'dayjs'; -import { KIND_CHOICES } from 'modules/settings/integrations/constants'; - -export type OptionsType = { - value: string; - label: string; -}; +import { IOption } from 'modules/common/types'; +import { INTEGRATION_KINDS } from 'modules/settings/integrations/constants'; export function selectOptions(array) { - const options: OptionsType[] = []; + const options: IOption[] = []; array.map(item => options.push({ value: item._id, label: item.name })); return options; } -export function integrationOptions(array) { - const options: OptionsType[] = []; - const types = KIND_CHOICES.ALL_LIST; +export function integrationOptions() { + const options: IOption[] = []; - array.map(item => + INTEGRATION_KINDS.ALL.map(item => options.push({ - value: types.includes(item) ? item : '', - label: item + value: item.value, + label: item.text }) ); diff --git a/ui/src/modules/settings/integrations/components/common/IntegrationListItem.tsx b/ui/src/modules/settings/integrations/components/common/IntegrationListItem.tsx index 4c1b0c6f00e..ca19d4c6d1b 100644 --- a/ui/src/modules/settings/integrations/components/common/IntegrationListItem.tsx +++ b/ui/src/modules/settings/integrations/components/common/IntegrationListItem.tsx @@ -7,7 +7,7 @@ import Tip from 'modules/common/components/Tip'; import WithPermission from 'modules/common/components/WithPermission'; import { __ } from 'modules/common/utils'; import InstallCode from 'modules/settings/integrations/components/InstallCode'; -import { KIND_CHOICES } from 'modules/settings/integrations/constants'; +import { INTEGRATION_KINDS } from 'modules/settings/integrations/constants'; import React from 'react'; import { Link } from 'react-router-dom'; import { cleanIntegrationKind } from '../../containers/utils'; @@ -48,7 +48,7 @@ class IntegrationListItem extends React.Component { renderEditAction() { const { integration, editIntegration } = this.props; - if (integration.kind === KIND_CHOICES.MESSENGER) { + if (integration.kind === INTEGRATION_KINDS.MESSENGER) { return null; } @@ -87,7 +87,7 @@ class IntegrationListItem extends React.Component { renderMessengerActions(integration) { const kind = integration.kind; - if (kind === KIND_CHOICES.MESSENGER) { + if (kind === INTEGRATION_KINDS.MESSENGER) { const editTrigger = (
  • diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index a35829c6898..2d36bcf13a2 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -46,7 +46,8 @@ "developer/graphql-api", "developer/push-notifications", "developer/android-sdk", - "developer/ios-sdk" + "developer/ios-sdk", + "developer/troubleshooting" ] } } From cea4312d43f3f15998a1f366e09ca42cc70a1134 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Thu, 2 Apr 2020 19:11:05 +0800 Subject: [PATCH 099/110] perf(teaminbox): can download or view a file from popups --- .../workarea/conversation/messages/FormMessage.tsx | 14 ++++++++++++++ ui/src/modules/layout/components/Navigation.tsx | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx index 12e2806244d..8bb87152aa5 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; import Table from 'modules/common/components/table'; +import { __ } from 'modules/common/utils'; import React from 'react'; import { IMessage } from '../../../../../types'; import { FormTable } from '../styles'; @@ -14,6 +15,19 @@ export default class FormMessage extends React.Component { return dayjs(data.value).format('YYYY/MM/DD HH:mm'); } + if (data.type === 'file') { + return ( + + {__('Download attachment')} + + ); + } + return data.value; } diff --git a/ui/src/modules/layout/components/Navigation.tsx b/ui/src/modules/layout/components/Navigation.tsx index 3ff3e666513..6c9a1fbb1cb 100644 --- a/ui/src/modules/layout/components/Navigation.tsx +++ b/ui/src/modules/layout/components/Navigation.tsx @@ -194,7 +194,7 @@ class Navigation extends React.Component<{ )} {this.renderNavItem( 'showForms', - __('Leads'), + __('Pop ups'), '/leads', 'icon-laptop' )} From de0d2cf9db534405cd8126e4a8b935ae54e1db85 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Thu, 2 Apr 2020 20:10:22 +0800 Subject: [PATCH 100/110] Fix/segment bugs (#1880) close #1879 --- .../modules/engage/containers/SegmentStep.tsx | 2 +- ui/src/modules/engage/graphql/mutations.ts | 2 - ui/src/modules/engage/types.ts | 4 +- .../segments/components/common/Filter.tsx | 2 +- .../segments/components/common/Form.tsx | 5 +- ui/src/modules/segments/graphql/mutations.ts | 54 +++++++------------ 6 files changed, 27 insertions(+), 42 deletions(-) diff --git a/ui/src/modules/engage/containers/SegmentStep.tsx b/ui/src/modules/engage/containers/SegmentStep.tsx index ed405828608..77772d1a8d0 100644 --- a/ui/src/modules/engage/containers/SegmentStep.tsx +++ b/ui/src/modules/engage/containers/SegmentStep.tsx @@ -67,7 +67,7 @@ const SegmentStepContainer = (props: FinalProps) => { ) : []; - const count = segment => { + const count = () => { customerCountsQuery.refetch(); }; diff --git a/ui/src/modules/engage/graphql/mutations.ts b/ui/src/modules/engage/graphql/mutations.ts index 3bae51e12ec..c7c3ae6217a 100644 --- a/ui/src/modules/engage/graphql/mutations.ts +++ b/ui/src/modules/engage/graphql/mutations.ts @@ -114,7 +114,6 @@ const segmentsAdd = ` $description: String, $subOf: String, $color: String, - $connector: String, $conditions: [SegmentCondition], ) { @@ -124,7 +123,6 @@ const segmentsAdd = ` description: $description, subOf: $subOf, color: $color, - connector: $connector, conditions: $conditions, ) { _id diff --git a/ui/src/modules/engage/types.ts b/ui/src/modules/engage/types.ts index ba4f9388d28..7bc8344f09a 100644 --- a/ui/src/modules/engage/types.ts +++ b/ui/src/modules/engage/types.ts @@ -111,7 +111,7 @@ export type SetLiveMutationResponse = { export type SetLiveManualMutationResponse = { setLiveManualMutation: ( - params: { vairables: MutationVariables } + params: { variables: MutationVariables } ) => Promise; }; @@ -135,7 +135,7 @@ export type WithFormAddMutationResponse = { export type WithFormEditMutationResponse = { editMutation: ( params: { - vairables: WithFormMutationVariables; + variables: WithFormMutationVariables; } ) => Promise; }; diff --git a/ui/src/modules/segments/components/common/Filter.tsx b/ui/src/modules/segments/components/common/Filter.tsx index 7ba6acac2d7..d7a3fa5b07d 100644 --- a/ui/src/modules/segments/components/common/Filter.tsx +++ b/ui/src/modules/segments/components/common/Filter.tsx @@ -79,7 +79,7 @@ class Filter extends React.Component { return fields.reduce((acc, field) => { const value = field.value; - const key = value.includes('.') + const key = value && value.includes('.') ? value.substr(0, value.indexOf('.')) : 'general'; diff --git a/ui/src/modules/segments/components/common/Form.tsx b/ui/src/modules/segments/components/common/Form.tsx index f4a367573a8..fbad91f861f 100644 --- a/ui/src/modules/segments/components/common/Form.tsx +++ b/ui/src/modules/segments/components/common/Form.tsx @@ -130,7 +130,7 @@ class Form extends React.Component { }; handleChange = (name: T, value: State[T]) => { - this.setState({ [name]: value } as Pick); + this.setState(({ [name]: value } as unknown) as Pick); }; generateDoc = (values: { @@ -140,7 +140,7 @@ class Form extends React.Component { color: string; }) => { const { segment, contentType } = this.props; - const { conditions } = this.state; + const { color, conditions } = this.state; const finalValues = values; const updatedConditions: ISegmentCondition[] = []; @@ -156,6 +156,7 @@ class Form extends React.Component { return { ...finalValues, + color, contentType, conditions: updatedConditions }; diff --git a/ui/src/modules/segments/graphql/mutations.ts b/ui/src/modules/segments/graphql/mutations.ts index 6d78072b0dc..13c9301684e 100755 --- a/ui/src/modules/segments/graphql/mutations.ts +++ b/ui/src/modules/segments/graphql/mutations.ts @@ -1,44 +1,30 @@ -const segmentsAdd = ` - mutation segmentsAdd( - $contentType: String!, - $name: String!, - $description: String, - $subOf: String, - $color: String, - $conditions: [SegmentCondition], - ) { +const paramDefs = ` + $name: String!, + $description: String, + $subOf: String, + $color: String, + $conditions: [SegmentCondition], +`; + +const params = ` + name: $name, + description: $description, + subOf: $subOf, + color: $color, + conditions: $conditions, +`; - segmentsAdd( - contentType: $contentType, - name: $name, - description: $description, - subOf: $subOf, - color: $color, - conditions: $conditions, - ) { +const segmentsAdd = ` + mutation segmentsAdd($contentType: String!, ${paramDefs}) { + segmentsAdd(contentType: $contentType, ${params}) { _id } } `; const segmentsEdit = ` - mutation segmentsEdit( - $_id: String!, - $name: String!, - $description: String, - $subOf: String, - $color: String, - $conditions: [SegmentCondition], - ) { - - segmentsEdit( - _id: $_id, - name: $name, - description: $description, - subOf: $subOf, - color: $color, - conditions: $conditions, - ) { + mutation segmentsEdit($_id: String!, ${paramDefs}) { + segmentsEdit(_id: $_id, ${params}) { _id } } From d6b8c3002135203e36357cc49b056077631ffc48 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Thu, 2 Apr 2020 21:17:38 +0800 Subject: [PATCH 101/110] perf(teaminbox): add file preview on file from popup --- .../modules/common/components/FilePreview.tsx | 161 ++++++++++++++++++ .../conversation/messages/FormMessage.tsx | 15 +- .../workarea/conversation/styles.ts | 7 +- 3 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 ui/src/modules/common/components/FilePreview.tsx diff --git a/ui/src/modules/common/components/FilePreview.tsx b/ui/src/modules/common/components/FilePreview.tsx new file mode 100644 index 00000000000..d87b5c30a9c --- /dev/null +++ b/ui/src/modules/common/components/FilePreview.tsx @@ -0,0 +1,161 @@ +import Icon from 'modules/common/components/Icon'; +import ImageWithPreview from 'modules/common/components/ImageWithPreview'; +import React from 'react'; +import styled from 'styled-components'; +import { rgba } from '../styles/color'; +import colors from '../styles/colors'; + +const Wrapper = styled.a` + border-radius: 4px; + transition: all 0.3s ease; + display: flex; + color: ${colors.textPrimary}; + position: relative; + background: ${rgba(colors.colorCoreDarkBlue, 0.04)}; + + &:hover { + background: ${rgba(colors.colorCoreDarkBlue, 0.08)}; + } + + img, + video { + max-width: 100%; + max-height: 320px; + } +`; + +const Content = styled.div` + flex: 1; + padding: 10px 15px; + display: flex; + align-items: center; + font-weight: 500; + max-width: 320px; + justify-content: space-between; + + i { + color: ${colors.colorCoreGray}; + margin-left: 10px; + font-size: 14px; + + &:hover { + color: ${colors.colorCoreBlack}; + } + } +`; + +const IconWrapper = styled.div` + height: 40px; + width: 50px; + background: ${rgba(colors.colorCoreDarkBlue, 0.08)}; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + overflow: hidden; + + i { + font-size: 26px; + color: ${colors.colorSecondary}; + } +`; + +const Name = styled.span` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +type Props = { + fileUrl: string; + fileName?: string; +}; + +export default function FilePreview({ fileUrl, fileName }: Props) { + if (!fileUrl) { + return null; + } + + const renderFile = (icon: string) => { + const attr = { + rel: 'noopener noreferrer', + href: fileUrl, + target: '_blank' + }; + + return ( + + + + + + {fileName || fileUrl} + + + + ); + }; + + const renderVideo = () => { + return ( + + + + + + ); + }; + + const renderImagePreview = () => { + return ( + + + + ); + }; + + const fileExtension = fileUrl.split('.').pop(); + + let filePreview; + + switch (fileExtension) { + case 'docx': + filePreview = renderFile('doc'); + break; + case 'pptx': + filePreview = renderFile('ppt'); + break; + case 'xlsx': + filePreview = renderFile('xls'); + break; + case 'mp4': + filePreview = renderVideo(); + break; + case 'jpeg': + case 'jpg': + case 'gif': + case 'png': + filePreview = renderImagePreview(); + break; + case 'zip': + case 'csv': + case 'doc': + case 'ppt': + case 'psd': + case 'avi': + case 'txt': + case 'rar': + case 'mp3': + case 'pdf': + case 'xls': + filePreview = renderFile(fileExtension); + break; + default: + filePreview = renderFile('file-2'); + } + + return filePreview; +} diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx index 8bb87152aa5..d24ab1b2467 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/messages/FormMessage.tsx @@ -1,9 +1,9 @@ import dayjs from 'dayjs'; +import FilePreview from 'modules/common/components/FilePreview'; import Table from 'modules/common/components/table'; -import { __ } from 'modules/common/utils'; import React from 'react'; import { IMessage } from '../../../../../types'; -import { FormTable } from '../styles'; +import { CellWrapper, FormTable } from '../styles'; type Props = { message: IMessage; @@ -17,14 +17,9 @@ export default class FormMessage extends React.Component { if (data.type === 'file') { return ( - - {__('Download attachment')} - + + + ); } diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/styles.ts b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/styles.ts index f7f647aec8a..e329869903b 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/styles.ts +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/conversation/styles.ts @@ -171,6 +171,10 @@ const FormTable = styled.div` } `; +const CellWrapper = styled.div` + display: inline-block; +`; + const CallBox = styled.div` border: 1px solid ${colors.borderPrimary}; border-radius: 5px; @@ -251,5 +255,6 @@ export { CallButton, UserInfo, FlexItem, - CallBox + CallBox, + CellWrapper }; From 72a66173e68d0e48db0a4ef9f81052da8ba2f109 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Thu, 2 Apr 2020 23:06:42 +0800 Subject: [PATCH 102/110] perf(deal): improve performance when deal item dragging --- .../modules/boards/components/Assignees.tsx | 42 +++++++++++++++++++ ui/src/modules/boards/styles/common.ts | 4 +- ui/src/modules/boards/styles/item.ts | 4 ++ ui/src/modules/boards/styles/stage.ts | 4 -- ui/src/modules/deals/components/DealItem.tsx | 4 +- ui/src/modules/tasks/components/TaskItem.tsx | 4 +- .../modules/tickets/components/TicketItem.tsx | 4 +- 7 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 ui/src/modules/boards/components/Assignees.tsx diff --git a/ui/src/modules/boards/components/Assignees.tsx b/ui/src/modules/boards/components/Assignees.tsx new file mode 100644 index 00000000000..8b743accaab --- /dev/null +++ b/ui/src/modules/boards/components/Assignees.tsx @@ -0,0 +1,42 @@ +import { IUser } from 'modules/auth/types'; +import { getUserAvatar } from 'modules/common/utils'; +import React from 'react'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + > img { + border-radius: 14px; + float: left; + margin-left: 2px; + } +`; + +type Props = { + users: IUser[]; + limit?: number; +}; + +function Assignees(props: Props) { + const getFullName = (user: IUser) => { + return user.details ? user.details.fullName : 'Unknown'; + }; + + const { users = [], limit = 3 } = props; + + return ( + + {users.slice(0, limit).map(user => ( + {getFullName(user)} + ))} + + ); +} + +export default Assignees; diff --git a/ui/src/modules/boards/styles/common.ts b/ui/src/modules/boards/styles/common.ts index 89a99547c6b..014409a2ea0 100644 --- a/ui/src/modules/boards/styles/common.ts +++ b/ui/src/modules/boards/styles/common.ts @@ -30,6 +30,7 @@ export const ScrolledContent = styled.div` padding: 4px 0 8px; margin: 6px 10px 4px 5px; flex: 1; + will-change: contents; overflow: auto; `; @@ -44,6 +45,7 @@ export const RootBack = styled.div` // IItem list export const DropZone = styled.div` min-height: 160px; + will-change: height; `; export const EmptyContainer = styled.div` @@ -92,7 +94,6 @@ export const FormContainer = styled.div` export const ItemDate = styled.span` font-size: 11px; color: rgb(136, 136, 136); - z-index: 10; `; export const NotifiedContainer = styled.div` @@ -120,7 +121,6 @@ export const ItemContainer = styledTS<{ padding: 8px; outline: 0px; font-size: 12px; - border-radius: ${borderRadius}; transition: box-shadow 0.3s ease-in-out 0s; -webkit-box-pack: justify; justify-content: space-between; diff --git a/ui/src/modules/boards/styles/item.ts b/ui/src/modules/boards/styles/item.ts index 7c4913aad59..fcb509f6a15 100644 --- a/ui/src/modules/boards/styles/item.ts +++ b/ui/src/modules/boards/styles/item.ts @@ -12,6 +12,8 @@ export const FlexContent = styled.div` `; export const PriceContainer = styled.div` + overflow: hidden; + ul { float: left; } @@ -208,6 +210,7 @@ export const MoveContainer = styled(FlexContent)` margin-bottom: 20px; align-items: center; position: relative; + will-change: contents; `; export const ActionContainer = styled(MoveContainer)` @@ -221,6 +224,7 @@ export const ActionContainer = styled(MoveContainer)` export const MoveFormContainer = styled.div` margin-right: 20px; position: relative; + will-change: transform; `; export const PipelineName = styled.div` diff --git a/ui/src/modules/boards/styles/stage.ts b/ui/src/modules/boards/styles/stage.ts index 4ff7ca9efba..7e2f73a701d 100644 --- a/ui/src/modules/boards/styles/stage.ts +++ b/ui/src/modules/boards/styles/stage.ts @@ -32,10 +32,6 @@ const StageRoot = styledTS<{ isDragging: boolean }>(styled.div)` `; const Content = styledTS<{ type?: string }>(styled.div)` - flex-grow: 1; - flex-basis: 100%; - display: flex; - flex-direction: column; h5 { ${props => css` diff --git a/ui/src/modules/deals/components/DealItem.tsx b/ui/src/modules/deals/components/DealItem.tsx index 88a81638ff7..171a8362095 100644 --- a/ui/src/modules/deals/components/DealItem.tsx +++ b/ui/src/modules/deals/components/DealItem.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import Assignees from 'modules/boards/components/Assignees'; import Details from 'modules/boards/components/Details'; import DueDateLabel from 'modules/boards/components/DueDateLabel'; import Labels from 'modules/boards/components/label/Labels'; @@ -16,7 +17,6 @@ import { renderAmount, renderPriority } from 'modules/boards/utils'; import Icon from 'modules/common/components/Icon'; import { colors } from 'modules/common/styles'; import { __ } from 'modules/common/utils'; -import Participators from 'modules/inbox/components/conversationDetail/workarea/Participators'; import React from 'react'; import { IDeal } from '../types'; @@ -104,7 +104,7 @@ class DealItem extends React.PureComponent { {renderAmount(item.amount)} - + diff --git a/ui/src/modules/tasks/components/TaskItem.tsx b/ui/src/modules/tasks/components/TaskItem.tsx index 56855d8cc4e..f6b5fe1bb30 100644 --- a/ui/src/modules/tasks/components/TaskItem.tsx +++ b/ui/src/modules/tasks/components/TaskItem.tsx @@ -9,8 +9,8 @@ import { Content } from 'modules/boards/styles/stage'; import { IItem, IOptions } from 'modules/boards/types'; import { renderPriority } from 'modules/boards/utils'; import { __ } from 'modules/common/utils'; -import Participators from 'modules/inbox/components/conversationDetail/workarea/Participators'; import React from 'react'; +import Assignees from 'modules/boards/components/Assignees'; type Props = { stageId: string; @@ -68,7 +68,7 @@ class TaskItem extends React.PureComponent { - + diff --git a/ui/src/modules/tickets/components/TicketItem.tsx b/ui/src/modules/tickets/components/TicketItem.tsx index 2ee858e32cd..501b763b969 100644 --- a/ui/src/modules/tickets/components/TicketItem.tsx +++ b/ui/src/modules/tickets/components/TicketItem.tsx @@ -9,9 +9,9 @@ import { Content } from 'modules/boards/styles/stage'; import { IOptions } from 'modules/boards/types'; import { renderPriority } from 'modules/boards/utils'; import { __ } from 'modules/common/utils'; -import Participators from 'modules/inbox/components/conversationDetail/workarea/Participators'; import React from 'react'; import { ITicket } from '../types'; +import Assignees from 'modules/boards/components/Assignees'; type Props = { stageId: string; @@ -68,7 +68,7 @@ class TicketItem extends React.PureComponent { - + From d9822b8304ff8012e194c0ee4a439ebe88837b0a Mon Sep 17 00:00:00 2001 From: BatAmar Battulga Date: Fri, 3 Apr 2020 06:22:25 +0800 Subject: [PATCH 103/110] lint fix --- ui/src/modules/tasks/components/TaskItem.tsx | 2 +- ui/src/modules/tickets/components/TicketItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/modules/tasks/components/TaskItem.tsx b/ui/src/modules/tasks/components/TaskItem.tsx index f6b5fe1bb30..2a7c77fa17f 100644 --- a/ui/src/modules/tasks/components/TaskItem.tsx +++ b/ui/src/modules/tasks/components/TaskItem.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import Assignees from 'modules/boards/components/Assignees'; import Details from 'modules/boards/components/Details'; import DueDateLabel from 'modules/boards/components/DueDateLabel'; import Labels from 'modules/boards/components/label/Labels'; @@ -10,7 +11,6 @@ import { IItem, IOptions } from 'modules/boards/types'; import { renderPriority } from 'modules/boards/utils'; import { __ } from 'modules/common/utils'; import React from 'react'; -import Assignees from 'modules/boards/components/Assignees'; type Props = { stageId: string; diff --git a/ui/src/modules/tickets/components/TicketItem.tsx b/ui/src/modules/tickets/components/TicketItem.tsx index 501b763b969..e12498df193 100644 --- a/ui/src/modules/tickets/components/TicketItem.tsx +++ b/ui/src/modules/tickets/components/TicketItem.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import Assignees from 'modules/boards/components/Assignees'; import Details from 'modules/boards/components/Details'; import DueDateLabel from 'modules/boards/components/DueDateLabel'; import Labels from 'modules/boards/components/label/Labels'; @@ -11,7 +12,6 @@ import { renderPriority } from 'modules/boards/utils'; import { __ } from 'modules/common/utils'; import React from 'react'; import { ITicket } from '../types'; -import Assignees from 'modules/boards/components/Assignees'; type Props = { stageId: string; From ad4a9a46678e3251292ab134b7b324c2e3bf376c Mon Sep 17 00:00:00 2001 From: munkhjin Date: Fri, 3 Apr 2020 09:24:09 +0800 Subject: [PATCH 104/110] fix drag and drop task in stage --- ui/src/modules/boards/components/stage/Stage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/modules/boards/components/stage/Stage.tsx b/ui/src/modules/boards/components/stage/Stage.tsx index c7ba38b05c8..7e87ba52b1d 100644 --- a/ui/src/modules/boards/components/stage/Stage.tsx +++ b/ui/src/modules/boards/components/stage/Stage.tsx @@ -106,7 +106,7 @@ export default class Stage extends React.Component { loadingItems() !== nextProps.loadingItems() || length !== nextProps.length || JSON.stringify(stage) !== JSON.stringify(nextProps.stage) || - items.length !== nextProps.items.length + JSON.stringify(items) !== JSON.stringify(nextProps.items) ) { return true; } From 504bbe27cb61f68958650c4c9ee8f41d8b2720c2 Mon Sep 17 00:00:00 2001 From: Buyantogtokh Date: Fri, 3 Apr 2020 14:34:59 +0800 Subject: [PATCH 105/110] close #1885 (#1887) --- ui/src/modules/common/components/Uploader.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/modules/common/components/Uploader.tsx b/ui/src/modules/common/components/Uploader.tsx index 57cb9d6c119..e5338a83dd5 100644 --- a/ui/src/modules/common/components/Uploader.tsx +++ b/ui/src/modules/common/components/Uploader.tsx @@ -1,4 +1,4 @@ -import { __, Alert, uploadHandler } from 'modules/common/utils'; +import { __, Alert, confirm, uploadHandler } from 'modules/common/utils'; import React from 'react'; import styled from 'styled-components'; import { rgba } from '../styles/color'; @@ -121,7 +121,10 @@ class Uploader extends React.Component { }; renderItem = (item: IAttachment, index: number) => { - const removeAttachment = () => this.removeAttachment(index); + const removeAttachment = () => { + confirm().then(() => this.removeAttachment(index)); + }; + const remove = {__('Delete')}; return ( From 284dee46ac54da98c3615b3617100657fd0dfa9f Mon Sep 17 00:00:00 2001 From: Batnasan Byambasuren Date: Fri, 3 Apr 2020 19:03:16 +1100 Subject: [PATCH 106/110] DigitalOcean Marketplace doc (#1888) --- docs/docs/installation/aws.md | 38 ++++++++++- docs/docs/installation/digitalocean.md | 77 +++++++++++++++++++++++ docs/docs/overview/deployment-overview.md | 1 + docs/docs/overview/getting-started.md | 1 + docs/website/sidebars.json | 1 + 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 docs/docs/installation/digitalocean.md diff --git a/docs/docs/installation/aws.md b/docs/docs/installation/aws.md index 0dae286cf5d..2ca938a5ed9 100644 --- a/docs/docs/installation/aws.md +++ b/docs/docs/installation/aws.md @@ -17,7 +17,7 @@ Run the following commands. ```sh sudo su erxes cd ~/erxes-api -export MONGO_URL=mongodb://localhost/erxes +export MONGO_URL=mongodb://localhost/erxes?replicaSet=rs0 ``` The following will create an admin user admin@erxes.io with a random password (check your console to grab the password) @@ -42,3 +42,39 @@ yarn loadPermission Now you can access erxes using the EC2 public hostname. Hooray!!! + +## Use your own domain + +To be able to use your own domain with erxes, you will need to do a few steps. + +1. Update your domain DNS records - point your domain to your EC2 public IP address. The DNS changes may take up to 72 hours to propagate worldwide. + +2. Log in to your server as `erxes` via `ssh`. + +3. Edit `/home/erxes/erxes/ui/build/js/env.js` file where env vars for frontend app are stored. + The content of the file should be as follows: + + ```javascript + window.env = { + PORT: 3000, + NODE_ENV: "production", + REACT_APP_API_URL: "http://your_domain/api", + REACT_APP_API_SUBSCRIPTION_URL: "ws://your_domain/api/subscriptions", + REACT_APP_CDN_HOST: "http://your_domain/widgets" + }; + ``` + +4. Update all env vars with HTTP url in the `/home/erxes/ecosystem.json` file. + +5. Now, you need to restart pm2 erxes processes by running the following command: + + ```sh + pm2 restart ecosystem.json + ``` + +6. Switch to `root` user and update your nginx config + `server_name` with your domain. + +7. Lastly reload your nginx service by running `systemctl reload nginx` + +Now you can use erxes with your own domain. diff --git a/docs/docs/installation/digitalocean.md b/docs/docs/installation/digitalocean.md new file mode 100644 index 00000000000..6ae28f51149 --- /dev/null +++ b/docs/docs/installation/digitalocean.md @@ -0,0 +1,77 @@ +--- +id: digitalocean +title: DigitalOcean Marketplace +--- + +Launch a Droplet selecting `erxes` in the DigitalOcean Marketplace. +Once you have created the Droplet, you will then have erxes up and running and it will be accessible by public IP address of the Droplet. + +## Create an admin user + +Connect to your Droplet instance via ssh. + +Run the following commands. + +```sh +sudo su erxes +cd ~/erxes-api +export MONGO_URL=mongodb://localhost/erxes?replicaSet=rs0 +``` + +The following will create an admin user admin@erxes.io with a random password (check your console to grab the password) + +``` +yarn initProject +``` + +## Load initial data + +The below command will create initial permission groups, permissions, growth hack templates, email templates and some sample data and reset the admin password (check your console to grab the password) + +``` +yarn loadInitialData +``` + +If do not want to load sample data then you can run the following command just to load permissions. + +``` +yarn loadPermission +``` + +Now you can access erxes by your Droplet IP address. + +## Use your own domain + +To be able to use your own domain with erxes, you will need to do a few steps. + +1. Update your domain DNS records - point your domain to your Droplet public IP address. The DNS changes may take up to 72 hours to propagate worldwide. + +2. Log in to your server as `erxes` via `ssh`. + +3. Edit `/home/erxes/erxes/ui/build/js/env.js` file where env vars for frontend app are stored. + The content of the file should be as follows: + + ```javascript + window.env = { + PORT: 3000, + NODE_ENV: "production", + REACT_APP_API_URL: "http://your_domain/api", + REACT_APP_API_SUBSCRIPTION_URL: "ws://your_domain/api/subscriptions", + REACT_APP_CDN_HOST: "http://your_domain/widgets" + }; + ``` + +4. Update all env vars with HTTP url in the `/home/erxes/ecosystem.json` file. + +5. Now, you need to restart pm2 erxes processes by running the following command: + + ```sh + pm2 restart ecosystem.json + ``` + +6. Switch to `root` user and update your nginx config + `server_name` with your domain. + +7. Lastly reload your nginx service by running `systemctl reload nginx` + +Now you can use erxes with your own domain. diff --git a/docs/docs/overview/deployment-overview.md b/docs/docs/overview/deployment-overview.md index aea31b6935c..ba3698fb038 100644 --- a/docs/docs/overview/deployment-overview.md +++ b/docs/docs/overview/deployment-overview.md @@ -22,6 +22,7 @@ We recommend to start with the [docker images](installation/docker.md) for the f - [Docker](installation/docker.md) - [Heroku](installation/heroku.md) - [AWS Marketplace](installation/aws.md) +- [DigitalOcean Marketplace](installation/digitalocean.md) ## Prerequisites diff --git a/docs/docs/overview/getting-started.md b/docs/docs/overview/getting-started.md index 4d3dcadf58a..185861f062a 100644 --- a/docs/docs/overview/getting-started.md +++ b/docs/docs/overview/getting-started.md @@ -43,6 +43,7 @@ title: Getting Started - Docker - Heroku - AWS Marketplace +- DigitalOcean Marketplace - Upgrade ### Administrator's guide diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index 2d36bcf13a2..3f19fb0c9c7 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -15,6 +15,7 @@ "installation/docker", "installation/heroku", "installation/aws", + "installation/digitalocean", "installation/upgrade" ], "User's Guide": [ From 3b5ef8eb95aa0cd431a8d8677501c7f6d74987b8 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 3 Apr 2020 18:48:34 +0800 Subject: [PATCH 107/110] perf(common): improve uploader component --- .../modules/common/components/Attachment.tsx | 14 ++- ui/src/modules/common/components/Uploader.tsx | 101 ++++++++++++++---- .../components/knowledge/KnowledgeForm.tsx | 1 + .../properties/components/GenerateField.tsx | 2 +- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/ui/src/modules/common/components/Attachment.tsx b/ui/src/modules/common/components/Attachment.tsx index 99e5b027b94..55708673371 100644 --- a/ui/src/modules/common/components/Attachment.tsx +++ b/ui/src/modules/common/components/Attachment.tsx @@ -30,15 +30,13 @@ const ItemInfo = styled.div` h5 { margin: 0 0 5px; - display: flex; font-weight: bold; } `; const Download = styled.a` color: ${colors.colorCoreGray}; - padding: 0 5px; - margin-left: 5px; + margin-left: 10px; &:hover { color: ${colors.colorCoreBlack}; @@ -54,6 +52,7 @@ const PreviewWrapper = styled.div` align-items: center; border-radius: 4px; overflow: hidden; + align-self: center; i { font-size: 36px; @@ -72,10 +71,9 @@ export const Meta = styled.div` `; const AttachmentName = styled.span` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 200px; + word-wrap: break-word; + word-break: break-word; + line-height: 20px; `; type Props = { @@ -98,7 +96,7 @@ class Attachment extends React.Component { href={readFile(attachment.url)} target="_blank" > - +
    diff --git a/ui/src/modules/common/components/Uploader.tsx b/ui/src/modules/common/components/Uploader.tsx index 57cb9d6c119..d31c145a66d 100644 --- a/ui/src/modules/common/components/Uploader.tsx +++ b/ui/src/modules/common/components/Uploader.tsx @@ -1,4 +1,4 @@ -import { __, Alert, uploadHandler } from 'modules/common/utils'; +import { __, Alert, confirm, uploadHandler } from 'modules/common/utils'; import React from 'react'; import styled from 'styled-components'; import { rgba } from '../styles/color'; @@ -17,6 +17,8 @@ const Item = styled.div` const Delete = styled.span` text-decoration: underline; + transition: all 0.3s ease; + color: ${colors.colorCoreGray}; &:hover { color: ${colors.colorCoreBlack}; @@ -24,14 +26,37 @@ const Delete = styled.span` } `; +const ToggleButton = styled(Delete.withComponent('div'))` + padding: 7px 15px; + border-radius: 4px; + margin-bottom: 15px; + + &:hover { + background: ${rgba(colors.colorCoreDarkBlue, 0.07)}; + } +`; + +const LoadingContainer = styled(List)` + background: ${colors.bgActive}; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + + > div { + height: 80px; + margin-right: 7px; + } +`; + const UploadBtn = styled.div` position: relative; margin-top: 10px; label { - padding: 8px 20px; + padding: 7px 15px; background: ${rgba(colors.colorCoreDarkBlue, 0.05)}; - border-radius: 20px; + border-radius: 4px; font-weight: 500; transition: background 0.3s ease; display: inline-block; @@ -50,6 +75,7 @@ const UploadBtn = styled.div` type Props = { defaultFileList: IAttachment[]; onChange: (attachments: IAttachment[]) => void; + single?: boolean; limit?: number; multiple?: boolean; }; @@ -57,11 +83,13 @@ type Props = { type State = { attachments: IAttachment[]; loading: boolean; + hideOthers: boolean; }; class Uploader extends React.Component { static defaultProps = { - multiple: true + multiple: true, + limit: 4 }; constructor(props: Props) { @@ -69,7 +97,8 @@ class Uploader extends React.Component { this.state = { attachments: props.defaultFileList || [], - loading: false + loading: false, + hideOthers: true }; } @@ -96,7 +125,7 @@ class Uploader extends React.Component { // set attachments const attachment = { url: response, ...fileInfo }; - const attachments = [...this.state.attachments, attachment]; + const attachments = [attachment, ...this.state.attachments]; this.props.onChange(attachments); @@ -111,13 +140,15 @@ class Uploader extends React.Component { }; removeAttachment = (index: number) => { - const attachments = [...this.state.attachments]; + confirm().then(() => { + const attachments = [...this.state.attachments]; - attachments.splice(index, 1); + attachments.splice(index, 1); - this.setState({ attachments }); + this.setState({ attachments }); - this.props.onChange(attachments); + this.props.onChange(attachments); + }); }; renderItem = (item: IAttachment, index: number) => { @@ -131,11 +162,10 @@ class Uploader extends React.Component { ); }; - renderBtn() { - const { multiple, limit } = this.props; - const { attachments, loading } = this.state; + renderUploadButton() { + const { multiple, single } = this.props; - if (limit && limit === attachments.length) { + if (single && this.state.attachments.length > 0) { return null; } @@ -149,22 +179,51 @@ class Uploader extends React.Component { onChange={this.handleFileInput} /> - {loading && ( - - )} ); } + toggleAttachments = () => { + this.setState({ hideOthers: !this.state.hideOthers }); + }; + + renderToggleButton = (hiddenCount: number) => { + if (hiddenCount > 0) { + const buttonText = this.state.hideOthers + ? `${__('View all attachments')} (${hiddenCount} ${__('hidden')})` + : `${__('Show fewer attachments')}`; + + return ( + + {buttonText} + + ); + } + + return null; + }; + render() { + const { limit = 4 } = this.props; + const { attachments, hideOthers, loading } = this.state; + + const length = attachments.length; + return ( <> + {loading && ( + + + {__('Uploading')}... + + )} - {this.state.attachments.map((item, index) => - this.renderItem(item, index) - )} + {this.state.attachments + .slice(0, limit && hideOthers ? limit : length) + .map((item, index) => this.renderItem(item, index))} - {this.renderBtn()} + {this.renderToggleButton(length - limit)} + {this.renderUploadButton()} ); } diff --git a/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx b/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx index 011c09d7f5f..fe1af45f4c7 100644 --- a/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx +++ b/ui/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx @@ -292,6 +292,7 @@ class KnowledgeForm extends React.Component { Background image: { defaultFileList={value || []} onChange={onChangeFile} multiple={false} - limit={1} + single={true} /> ); } From 0f8a5e10029b10ffe6365b0991acd125b15a2361 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 3 Apr 2020 18:51:49 +0800 Subject: [PATCH 108/110] fix duplicated code --- ui/src/modules/common/components/Attachment.tsx | 4 ++++ ui/src/modules/common/components/Uploader.tsx | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/src/modules/common/components/Attachment.tsx b/ui/src/modules/common/components/Attachment.tsx index 55708673371..c5719a354c3 100644 --- a/ui/src/modules/common/components/Attachment.tsx +++ b/ui/src/modules/common/components/Attachment.tsx @@ -32,6 +32,10 @@ const ItemInfo = styled.div` margin: 0 0 5px; font-weight: bold; } + + video { + width: 100%; + } `; const Download = styled.a` diff --git a/ui/src/modules/common/components/Uploader.tsx b/ui/src/modules/common/components/Uploader.tsx index b9d54298433..66a33fdd661 100644 --- a/ui/src/modules/common/components/Uploader.tsx +++ b/ui/src/modules/common/components/Uploader.tsx @@ -140,15 +140,13 @@ class Uploader extends React.Component { }; removeAttachment = (index: number) => { - confirm().then(() => { - const attachments = [...this.state.attachments]; + const attachments = [...this.state.attachments]; - attachments.splice(index, 1); + attachments.splice(index, 1); - this.setState({ attachments }); + this.setState({ attachments }); - this.props.onChange(attachments); - }); + this.props.onChange(attachments); }; renderItem = (item: IAttachment, index: number) => { From dd5db75b8666b4bde9ea1e39970bc6e54b395db6 Mon Sep 17 00:00:00 2001 From: Ganzorig Bayarsaikhan Date: Fri, 3 Apr 2020 19:18:43 +0800 Subject: [PATCH 109/110] fix move position bug --- ui/src/modules/boards/styles/item.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/modules/boards/styles/item.ts b/ui/src/modules/boards/styles/item.ts index fcb509f6a15..32962d805dd 100644 --- a/ui/src/modules/boards/styles/item.ts +++ b/ui/src/modules/boards/styles/item.ts @@ -224,6 +224,7 @@ export const ActionContainer = styled(MoveContainer)` export const MoveFormContainer = styled.div` margin-right: 20px; position: relative; + z-index: 100; will-change: transform; `; From 34b62abab1e8f5b1a9e0cab51c323b072f590493 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 3 Apr 2020 20:10:01 -0300 Subject: [PATCH 110/110] Implemented escape to dismiss response templates --- .../modules/common/components/editor/Editor.tsx | 3 +++ .../conversationDetail/workarea/Editor.tsx | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ui/src/modules/common/components/editor/Editor.tsx b/ui/src/modules/common/components/editor/Editor.tsx index a797c284636..d7850805497 100755 --- a/ui/src/modules/common/components/editor/Editor.tsx +++ b/ui/src/modules/common/components/editor/Editor.tsx @@ -35,6 +35,7 @@ type ErxesEditorProps = { keyBindingFn?: (e: any) => any; onUpArrow?: (e: KeyboardEvent) => void; onDownArrow?: (e: KeyboardEvent) => void; + onEscape?: (e: KeyboardEvent) => void; handleFileInput?: (e: React.FormEvent) => void; placeholder?: string | React.ReactNode; }; @@ -128,6 +129,7 @@ export class ErxesEditor extends React.Component { controls, onUpArrow, onDownArrow, + onEscape, bordered, isTopPopup = false, plugins @@ -172,6 +174,7 @@ export class ErxesEditor extends React.Component { keyBindingFn={this.props.keyBindingFn} onUpArrow={onUpArrow} onDownArrow={onDownArrow} + onEscape={onEscape} ref={element => { this.editor = element; }} diff --git a/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx b/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx index add08f020ec..80e34f3cacd 100644 --- a/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx +++ b/ui/src/modules/inbox/components/conversationDetail/workarea/Editor.tsx @@ -38,6 +38,7 @@ type State = { collectedMentions: any; suggestions: any; templatesState: any; + hideTemplates: boolean; }; const MentionEntry = props => { @@ -91,7 +92,8 @@ export default class Editor extends React.Component { ), collectedMentions: [], suggestions: this.props.mentions.toArray(), - templatesState: null + templatesState: null, + hideTemplates: false }; this.mentionPlugin = createMentionPlugin({ @@ -126,7 +128,7 @@ export default class Editor extends React.Component { } onChange = editorState => { - this.setState({ editorState }); + this.setState({ editorState, hideTemplates: false }); this.props.onChange(this.getContent(editorState)); @@ -235,11 +237,15 @@ export default class Editor extends React.Component { this.onArrow(e, 1); }; + onEscape = () => { + this.setState({ hideTemplates: true }); + } + // Render response templates suggestions renderTemplates() { - const { templatesState } = this.state; + const { templatesState, hideTemplates } = this.state; - if (!templatesState) { + if (!templatesState || hideTemplates) { return null; } @@ -312,7 +318,7 @@ export default class Editor extends React.Component { // handle enter in editor if (e.key === 'Enter') { // select response template - if (this.state.templatesState) { + if (this.state.templatesState && !this.state.hideTemplates) { this.onSelectTemplate(); return null; @@ -359,6 +365,7 @@ export default class Editor extends React.Component { keyBindingFn: this.keyBindingFn, onUpArrow: this.onUpArrow, onDownArrow: this.onDownArrow, + onEscape: this.onEscape, handleFileInput: this.props.handleFileInput, isTopPopup: true, plugins,