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: '' --- diff --git a/.gitignore b/.gitignore index af0997a389c..ebe9efc67a3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ node_modules *.swp *.swo *.log -/build +build +static .env.local .env.development.local .env.development diff --git a/ui/.release-it.json b/.release-it.json similarity index 100% rename from ui/.release-it.json rename to .release-it.json diff --git a/ui/CHANGELOG.md b/CHANGELOG.md similarity index 56% rename from ui/CHANGELOG.md rename to CHANGELOG.md index c851cec153b..8bfcbb20db3 100644 --- a/ui/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,86 @@ +# [0.13.0](https://github.com/erxes/erxes/compare/0.12.1...0.13.0) (2020-03-17) + + +### Bug Fixes + +* **deal/task/ticket/growthHack:** not copying labels ([7f99cf9](https://github.com/erxes/erxes/commit/7f99cf948df86a38b0754aba456a8b5c9e5b1e4d)), closes [#1598](https://github.com/erxes/erxes/issues/1598) +* **docker:** fix dockerfile permission error ([68ef0b3](https://github.com/erxes/erxes/commit/68ef0b387e61b174f5e408c4d16ce7fdb2b53e52)) +* **drone:** workaround for wrong version information showing on version.json ([ea0aa9f](https://github.com/erxes/erxes/commit/ea0aa9f3ca3794c4081b56916b8a1c9ba0d71679)) +* **form:** checkbox error ([4b64aa5](https://github.com/erxes/erxes/commit/4b64aa573fdb2464fd309582b09631ec4720610f)), closes [#157](https://github.com/erxes/erxes/issues/157) +* **form:** multiple submit bug ([e04e206](https://github.com/erxes/erxes/commit/e04e2063a45abfcfa12206f6d5816c4b15aa30fd)), closes [#160](https://github.com/erxes/erxes/issues/160) +* little fix in nylas doc ([e9dd5bd](https://github.com/erxes/erxes/commit/e9dd5bd46a2c6aa82bec8c438bf5d64c76945d89)) +* **typo:** url to push-notifications in overview ([#1719](https://github.com/erxes/erxes/issues/1719)) ([48f4a45](https://github.com/erxes/erxes/commit/48f4a451c6871749928827b3eb2c26a52a31fb40)) +* **upload:** forbidden error ([2c84ad5](https://github.com/erxes/erxes/commit/2c84ad5d889a47f2a8e801069791a6f260e9dec4)), closes [#156](https://github.com/erxes/erxes/issues/156) +* merge with master and fix conflict ([01eb251](https://github.com/erxes/erxes/commit/01eb25150e0aa5330922e1b0d051a8d3f6a14fba)) +* reset submit state after fail mutation ([aa62e8a](https://github.com/erxes/erxes/commit/aa62e8a69e4511560146861a409a9804731a5f73)) +* send missing argument in onSubmitResolve ([0bc5201](https://github.com/erxes/erxes/commit/0bc52017e6e4e7b1a60ec49f87624005e78648ab)) +* **widget:** fix popup can not cancelling (close [#1672](https://github.com/erxes/erxes/issues/1672)) ([b9fefdf](https://github.com/erxes/erxes/commit/b9fefdf41e2a47bcba0bf160a9d0f10756a433e3)) + + +### Features + +* **customers:** added birthdate & gender ([f5af3e7](https://github.com/erxes/erxes/commit/f5af3e7011c31a1ee614e1c41a83d8562973fdd5)), closes [#1641](https://github.com/erxes/erxes/issues/1641) +* **deal/task/ticket:** add load more on archived list ([75a0d50](https://github.com/erxes/erxes/commit/75a0d50c286562fda824ef90f2ac31afd3e30061)), closes [#1739](https://github.com/erxes/erxes/issues/1739) +* **deal/task/ticket:** assignee, checklist activity log ([b7fad20](https://github.com/erxes/erxes/commit/b7fad2020baf8767c93efb349dbcbd16eff8e8be)), closes [#1594](https://github.com/erxes/erxes/issues/1594) +* **env:** store env variables to database ([a4fa05f](https://github.com/erxes/erxes/commit/a4fa05f96f247b42d1ea37e25693b52d6ab56c52)), closes [#1700](https://github.com/erxes/erxes/issues/1700) +* **form:** ability call submit action from parent website ([dbb252c](https://github.com/erxes/erxes/commit/dbb252cf65f75eb8c0a20f2b45a1f023ee51efbe)), closes [#158](https://github.com/erxes/erxes/issues/158) +* **form:** ability to change css from parent ([d833901](https://github.com/erxes/erxes/commit/d8339011af1b039c5d2bab531b680f6f224e4313)), closes [#159](https://github.com/erxes/erxes/issues/159) +* **growthhack:** change UI of growthhack page entirely (close [#1634](https://github.com/erxes/erxes/issues/1634)) ([78ca651](https://github.com/erxes/erxes/commit/78ca651c3f2eb4b721b9c059e4fe35f4e4e27215)) +* **heroku:** added heroku deployment ([b313681](https://github.com/erxes/erxes/commit/b313681bfdf07660875d3fe432a39afbc682ecb1)), closes [#848](https://github.com/erxes/erxes/issues/848) +* **installation:** quick install on debian 10 ([5568719](https://github.com/erxes/erxes/commit/55687192fbbccf7409a47637f0ad0f19d886879d)), closes [#1649](https://github.com/erxes/erxes/issues/1649) +* **knowledgebase:** ability run without iframe ([1652c28](https://github.com/erxes/erxes/commit/1652c280ba6e1f6729c1d14526c34590d0de6bfc)), closes [#126](https://github.com/erxes/erxes/issues/126) +* **knowledgebase:** add article reactions ([27d3f21](https://github.com/erxes/erxes/commit/27d3f21315728517add2f51362a6207528856d04)), closes [#128](https://github.com/erxes/erxes/issues/128) +* **language:** add italian language ([0b1a38e](https://github.com/erxes/erxes/commit/0b1a38e63dc1d11b580964ddd289aa45c4eb7cfb)) +* **messenger:** ability to hide launcher from admin ([3d72041](https://github.com/erxes/erxes/commit/3d7204149b0393e72e05546ff8d6baf7f6104e4d)), closes [#123](https://github.com/erxes/erxes/issues/123) +* nylas-gmail doc ([78aa297](https://github.com/erxes/erxes/commit/78aa29784d1cb825e5e9c88ebeafe5dd8ffd6983)) +* **messenger:** added showErxesMessenger trigger ([80469cc](https://github.com/erxes/erxes/commit/80469cc80f5ab1ca0840e516979d44751bbe60de)), closes [#148](https://github.com/erxes/erxes/issues/148) +* **segments:** reimplemented using elk ([016aa66](https://github.com/erxes/erxes/commit/016aa667e1f9f476130838e3e61d63ac9f72fd59)), closes [#1686](https://github.com/erxes/erxes/issues/1686) +* **settings:** add engage environment variables ([5f3a595](https://github.com/erxes/erxes/commit/5f3a5956f1a908601632339c25af8d689c53d7ee)), closes [#1724](https://github.com/erxes/erxes/issues/1724) +* **settings:** improve UI of channel, brands page (close [#1597](https://github.com/erxes/erxes/issues/1597)) ([0a76eb7](https://github.com/erxes/erxes/commit/0a76eb7e5f000032b3980bdfd17a3b8f5edc8698)) +* **translation:** add indonesia lang ([2a893c8](https://github.com/erxes/erxes/commit/2a893c868383942655a52a46690270d6a11a3d9b)) +* **videoCall:** add video call ([3397802](https://github.com/erxes/erxes/commit/3397802ea46e7ccfb6625e9ad5ac1f2c10bcfc84)) + + +### Performance Improvements + +* **board:** change UI of board pipeline, campaign & projects (close [#1612](https://github.com/erxes/erxes/issues/1612)) ([316fad8](https://github.com/erxes/erxes/commit/316fad80a2fff4e2ca3b6ff118df5f473d518f85)) +* **common:** choosing the same file doesn't trigger onChange (close [#1571](https://github.com/erxes/erxes/issues/1571)) ([9848226](https://github.com/erxes/erxes/commit/9848226131ba61c716f66cc106e186cb389f443a)) +* **common:** show confirmation when clear team members (close [#1677](https://github.com/erxes/erxes/issues/1677)) ([c5d0fe7](https://github.com/erxes/erxes/commit/c5d0fe7083e5fdb3fef335e5f8e48ecd47bb466d)) +* **contacts:** fix page menu or breadcrumb not positioned properly when import contacts (close [#1741](https://github.com/erxes/erxes/issues/1741)) ([93a34e8](https://github.com/erxes/erxes/commit/93a34e8ea4b16aa0ceb7f8f4d5adeff2ac914e5a)) +* **contacts:** show gender, birthday in customerDetails (close [#1670](https://github.com/erxes/erxes/issues/1670)) ([61c6532](https://github.com/erxes/erxes/commit/61c6532d2875a7b7074b687a3f7550bfd21705a3)) +* **customer:** add new customer and not email visitorContactInfo to open email input field (close [#1573](https://github.com/erxes/erxes/issues/1573)) ([ed89d77](https://github.com/erxes/erxes/commit/ed89d778f9fd5d375d2cd8225f86cf35649d3e14)) +* **customer:** change user indicator (close [#1689](https://github.com/erxes/erxes/issues/1689)) ([9c10b86](https://github.com/erxes/erxes/commit/9c10b86c154e4c8f5a06c52ac63c070b30dd575e)) +* **customer:** export pop-ups data for customer list when filtering by pop ups ([a62308b](https://github.com/erxes/erxes/commit/a62308b27d104ecc6638b507a2276faecad79fc0)), closes [#1674](https://github.com/erxes/erxes/issues/1674) +* **deal:** improve UI of deal products (close [#1629](https://github.com/erxes/erxes/issues/1629)) ([8da4cb3](https://github.com/erxes/erxes/commit/8da4cb32199eb8a9ef48b96fa763706f277830e8)) +* **editor:** show default avatar when user has an invalid avatar (close [#1619](https://github.com/erxes/erxes/issues/1619)) ([a6f2963](https://github.com/erxes/erxes/commit/a6f2963286b944e0b7e6d19d7a6d53c5d27fd848)) +* **inbox:** add inbox assign loader (close [#1754](https://github.com/erxes/erxes/issues/1754)) ([1e4c4b8](https://github.com/erxes/erxes/commit/1e4c4b829972a7469c1b605e223569562d97f3c2)) +* **integration:** improve integration view in App store ([2f7a16e](https://github.com/erxes/erxes/commit/2f7a16e780baf3af8e0664e43795d4b363517bb7)), closes [#1583](https://github.com/erxes/erxes/issues/1583) +* **knowledgebase:** change UI of knowledge base (close [#1611](https://github.com/erxes/erxes/issues/1611)) ([43497e3](https://github.com/erxes/erxes/commit/43497e3ff62bafd7c7559dacba0f5b6fb0b4c8b7)) +* **logs:** enhancement logs ([af6b1fe](https://github.com/erxes/erxes/commit/af6b1fe709025c9c92042b412f7d3fed685c147e)), closes [#1576](https://github.com/erxes/erxes/issues/1576) +* **notification:** add recent, unread tab on notification popup (close [#1560](https://github.com/erxes/erxes/issues/1560)) ([7895e09](https://github.com/erxes/erxes/commit/7895e09e8f4b20f1940c20168af23d4826b5d5c0)) +* **onboard:** add video to onboarding robot (close [#1693](https://github.com/erxes/erxes/issues/1693)) ([e9d7dce](https://github.com/erxes/erxes/commit/e9d7dceba5c071141ec11d4d1bc902d9069d2e3e)) +* **onboard:** fix onboard youtube url ([26e1a9b](https://github.com/erxes/erxes/commit/26e1a9b47389c7761711d1b1860cf3b83a27cf62)) +* **permission:** improve ui of permission, logs page ([c22239a](https://github.com/erxes/erxes/commit/c22239a8d0bc16b4dde0ab817310f7ce0ed58c70)) +* **product:** change UI of product and services (close [#1613](https://github.com/erxes/erxes/issues/1613)) ([929c56d](https://github.com/erxes/erxes/commit/929c56d15d5c2e0080649cb838497f2b6b778b11)) +* **product:** first registered values of UOM and the currency selected automatically in the deal (close [#1627](https://github.com/erxes/erxes/issues/1627)) ([645e5c5](https://github.com/erxes/erxes/commit/645e5c5d31e8cefd778cebbdf396f0809f31c1dc)) +* **product:** fix currency and uom dropdown (close [#1703](https://github.com/erxes/erxes/issues/1703)) ([dbc5c66](https://github.com/erxes/erxes/commit/dbc5c661614497b080c6b8e6d4cad9cc8499b6f7)) +* **product:** fix style of manage product service (close [#1680](https://github.com/erxes/erxes/issues/1680)) ([b1b7027](https://github.com/erxes/erxes/commit/b1b7027efcb58dccb80c485bb88b10174781bae1)) +* **product:** make it easy to navigate from the Deal edit window to the product service selection window (close [#1675](https://github.com/erxes/erxes/issues/1675)) ([79cbbbe](https://github.com/erxes/erxes/commit/79cbbbef28cf4016c740bd51db6e324d0588844f)) +* **properties:** improve ui of properties page ([b27f9e3](https://github.com/erxes/erxes/commit/b27f9e3e527fe3e6c9bcf5edf64b4b6f36572084)) +* **settings:** change google button (close [#1694](https://github.com/erxes/erxes/issues/1694)) ([9b428da](https://github.com/erxes/erxes/commit/9b428da31420a427991717b63de86860150cd554)) +* **settings:** display no channel, brand even though there are few (close [#1621](https://github.com/erxes/erxes/issues/1621)) ([99a4cd8](https://github.com/erxes/erxes/commit/99a4cd875a90cca8649221a03c54f8efe411d140)) +* **settings:** fix integration search in app store (close [#1673](https://github.com/erxes/erxes/issues/1673)) ([9ea09ef](https://github.com/erxes/erxes/commit/9ea09efc896dc35215d9f53cfd56d618ce041d29)) +* **tags:** improve ui of tags ([b70ab3f](https://github.com/erxes/erxes/commit/b70ab3f9627007debb00e2f21d3c5648843e686c)) +* **teaminbox:** fix overlaping image in editor (close [#1667](https://github.com/erxes/erxes/issues/1667)) ([2e95430](https://github.com/erxes/erxes/commit/2e95430f45cbca6466b9db80122b471ee7f08235)) +* **teaminbox:** fix response template not sent when press enter (close [#1642](https://github.com/erxes/erxes/issues/1642)) ([f2f18a4](https://github.com/erxes/erxes/commit/f2f18a410eea4df295ccb1e5a7e0ff2ee63a526e)) +* **teaminbox:** overlapping big image in message item (close [#1668](https://github.com/erxes/erxes/issues/1668)) ([ee10d9e](https://github.com/erxes/erxes/commit/ee10d9ef7f7c3921454720f3a33426421bf7c43f)) +* **translation:** loading all locales ([20bb930](https://github.com/erxes/erxes/commit/20bb930ef82e144cac8025b26d0c2848ee580656)), closes [#130](https://github.com/erxes/erxes/issues/130) + + +### BREAKING CHANGES + +* **translation:** renamed some language codes (np -> hi, jp -> ja, kr -> ko, ptBr -> pt-br, vn -> vi, zh -> zh-cn) + ## [0.12.1](https://github.com/erxes/erxes/compare/0.12.0...0.12.1) (2020-03-09) diff --git a/README.md b/README.md index 6bc773b469c..81dc3527b9f 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) @@ -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
@@ -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 diff --git a/docs/docs/administrator/creating-first-user.md b/docs/docs/administrator/creating-first-user.md index 3074a65749f..6342dc207c1 100644 --- a/docs/docs/administrator/creating-first-user.md +++ b/docs/docs/administrator/creating-first-user.md @@ -3,9 +3,12 @@ id: creating-first-user title: Creating first user --- +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 @@ -13,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/docs/docs/administrator/environment-variables.md b/docs/docs/administrator/environment-variables.md index 5fd18c4e31a..5ddd35e0716 100644 --- a/docs/docs/administrator/environment-variables.md +++ b/docs/docs/administrator/environment-variables.md @@ -1,8 +1,10 @@ --- id: environment-variables -title: Environment Variables +title: Environment variables --- +On this page you can see how to configure the work environment. It is very important to follow the steps as indicated. + ## MongoDB ``` @@ -10,9 +12,9 @@ MONGO_URL=mongodb://localhost/erxes TEST_MONGO_URL=mongodb://localhost/test ``` -- MONGO_URL is your application's database URL +- `MONGO_URL` is your application's database URL. -- TEST_MONGO_URL is when you run +- `TEST_MONGO_URL` is when you run testing. ``` yarn test @@ -26,11 +28,11 @@ REDIS_PORT=6379 REDIS_PASSWORD= ``` -Redis is necessary for reactive subscriptions +Redis is necessary for reactive subscriptions. -- REDIS_HOST is your redis server's URL -- REDIST_PORT defines which port is redis running -- REDIS_PASSWORD fill this if you have password on your redis server +- `REDIS_HOST` is your redis server's URL. +- `REDIST_PORT` defines which port is redis running. +- `REDIS_PASSWORD` fill this if you have password on your redis server. ## General configs @@ -47,118 +49,12 @@ API_PATH='' WIDGET_PATH='' WIDGET_API_PATH='' -UPLOAD_SERVICE_TYPE='' -GOOGLE_CLOUD_STORAGE_BUCKET='' -``` - -- HTTPS this is boolean variables, set true if you are using secure ssl -- MAIN_APP_DOMAIN this is your main application's domain where is erxes/erxes repository is running -- WIDGETS_DOMAIN your widget application's domain where is erxes/widgets repository is running -- DOMAIN this is your erxes-api application's domain where is erxes/erxes-api repository is running -- NODE_ENV set to production on live mode, set to development on development mode -- PORT this option will define which port is your application running, you can change to any port you want -- UPLOAD_SERVICE_TYPE this is upload service type amazon s3 or gcs. GCS is short for Google Cloud Service -- GOOGLE_CLOUD_STORAGE_BUCKET defines the bucket of gcs - -## Email settings - -``` -COMPANY_EMAIL_FROM=noreply@erxes.io -DEFAULT_EMAIL_SERVICE=sendgrid -MAIL_SERVICE=sendgrid -MAIL_PORT='' -MAIL_USER='' -MAIL_PASS='' -MAIL_HOST='' -``` - -- COMPANY_EMAIL_FROM transaction emails will be sent by this email address -- DEFAULT_EMAIL_SERVICE defines whether transaction emails sent by ses or other email services -- MAIL_SERVICE defines your email service's name -- MAIL_PORT your email service's port -- MAIL_USER defines your email service's login username -- MAIL_PASS defines your email service's login password -- MAIL_HOST your email service's host - -## Twitter Settings - -``` -TWITTER_CONSUMER_KEY='' -TWITTER_CONSUMER_SECRET='' -TWITTER_REDIRECT_URL='https://erxes.domain.com/service/oauth/twitter_callback' -``` - -- TWITTER_CONSUMER_KEY Your twitter developer account's Consumer Key (API Key) here -- TWITTER_CONSUMER_SECRET Your twitter developer account's Consumer Secret (API Secret) here -- TWITTER_REDIRECT_URL you should only change the domain of this env variables. This is twitter's callback url - -## Aws S3 - -``` -AWS_ACCESS_KEY_ID='' -AWS_SECRET_ACCESS_KEY='' -AWS_BUCKET='' -AWS_PREFIX='' -``` - -- AWS_ACCESS_KEY_ID your amazon account's access key id -- AWS_SECRET_ACCESS_KEY your amazon account's secret access key -- AWS_BUCKET your s3 service's bucket name to use -- AWS_PREFIX you can use prefix names to specify the names of the files to be uploaded - -## Aws SES - -Engages are sent by ses service - -``` -AWS_SES_ACCESS_KEY_ID='' -AWS_SES_SECRET_ACCESS_KEY='' -AWS_SES_CONFIG_SET='' -AWS_REGION='' -AWS_ENDPOINT='' -``` - -- AWS_SES_ACCESS_KEY_ID your amazon account's access key id -- AWS_SES_SECRET_ACCESS_KEY your amazon account's secret access key -- AWS_SES_CONFIG_SET to detect bounce, complaints, click, open events you will need this option. This name can be anything -- AWS_REGION your amazon account's region -- AWS_ENDPOINT this is the URL where the amazon events sent to. Basically it is your erxes/erxes-api repository's path or domain - -To be able to detect the ses events you should run this command in console - -``` -yarn engageSubscriptions ``` -## Facebook - -``` -FACEBOOK_APP_ID='' -FACEBOOK_APP_SECRET='' -FACEBOOK_PERMISSIONS='manage_pages, pages_show_list, pages_messaging, publish_pages, pages_messaging_phone_number, pages_messaging_subscriptions' - -``` - -- FACEBOOK_APP_ID is your faceboook application's app id -- FACEBOOK_APP_SECRET is your faceboook application's secret key -- FACEBOOK_PERMISSIONS you should not have to modify this option. Those are the necessary permissions for facebook integration - -## Gmail - -``` -GOOGLE_CLIENT_ID='' -GOOGLE_CLIENT_SECRET='' -GOOGLE_APPLICATION_CREDENTIALS='' -GOOGLE_TOPIC='' -GOOGLE_SUBSCRIPTION_NAME='' -GOOGLE_PROJECT_ID='' -GMAIL_REDIRECT_URL = 'http://localhost:3000/service/oauth/gmail_callback' -``` +- `HTTPS `this is boolean variables, set true if you are using secure ssl. +- `MAIN_APP_DOMAIN` this is your main application's domain where is erxes/erxes repository is running. +- `WIDGETS_DOMAIN` your widget application's domain where is erxes/widgets repository is running. +- `DOMAIN` this is your erxes-api application's domain where is erxes/erxes-api repository is running. +- `NODE_ENV` set to production on live mode, set to development on development mode. +- `PORT` this option will define which port is your application running, you can change to any port you want. -- GOOGLE_CLIENT_ID your google project's Clint id -- GOOGLE_CLIENT_SECRET your google project's secret key -- GOOGLE_APPLICATION_CREDENTIALS Your downloaded google's credentials which is json file -- GOOGLE_TOPIC Your google cloud project's subscribed topic's name -- GOOGLE_SUBSCRIPTION_NAME Your google cloud project's subscription name -- GOOGLE_PROJECT_ID your google project's id -- GMAIL_REDIRECT_URL this is gmail's callback URL you should only change the domain of it diff --git a/docs/docs/administrator/file-upload.md b/docs/docs/administrator/file-upload.md deleted file mode 100644 index 250f7bb44f2..00000000000 --- a/docs/docs/administrator/file-upload.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -id: file-upload -title: File Upload ---- - -## Amazon s3 - -You can upload the files through amazon s3 service. -Below configurations must be configured. You can see more info from Environment variable settings - -``` -AWS_ACCESS_KEY_ID='' -AWS_SECRET_ACCESS_KEY='' -AWS_BUCKET='' -AWS_PREFIX='' -``` - -## Google Cloud storage - -You can upload the files through google's cloud service. -Below configurations must be configured. You can see more info from Environment variable settings - -``` -GOOGLE_PROJECT_ID='' -GOOGLE_APPLICATION_CREDENTIALS='' -GOOGLE_CLOUD_STORAGE_BUCKET='' -``` diff --git a/docs/docs/administrator/integrations.md b/docs/docs/administrator/integrations.md deleted file mode 100644 index 07abb9d451b..00000000000 --- a/docs/docs/administrator/integrations.md +++ /dev/null @@ -1,428 +0,0 @@ ---- -id: integrations -title: Integrations ---- - -## Facebook Integration - -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: - -- working sub domain with SSL pointing to your erxes-api server. -- facebook app's owner also must be an admin of facebook page that's going to connect. - -## Creating facebook app. - -1. Go to https://developers.facebook.com and create new app.
- ( Your application status must be "Live" ) -2. Your application must have these permissions:
- `manage_pages, pages_show_list, pages_messaging, publish_pages`
- (You can get these permissions through your App Review) -3. Go to you `erxes-api` directory and edit `.env` file like below:
- `FACEBOOK_APP_ID='Your Facebook application's ID'`
- `FACEBOOK_APP_SECRET='Your Facebook application's Secret code'`
- You can get these informations from `settings` => `basic` from right side menu. -4. You must have `Facebook Login` product set up. -5. Go to `Facebook Login` => `Settings` fill the field names `Valid OAuth Redirect URIs` like below:
- `/fblogin` -6. Now we are done on configurations. Go to your `App Store` => `Linked Accounts` to link your facebook accounts - -## Getting facebook permissions (*manage_pages*, *publish_pages*) - -**manage_pages:** - -Grants an app permission to retrieve Page Access Tokens for the Pages and Apps that the app user administers. Apps that allow users to publish as a Page must also have the ***publish_pages*** permission. - -To take this permission, you must complete the following 2 steps. - -1. **Provide verification details. In this step, you must provide detailed step-by-step instructions on how a reviewer can test your integration and how you are using the requested permissions or features.** To do so, - - You provide the testing account & password. (Note: Do not provide your personal Facebook account credentials.) - - Then you provide each steps required to test your integration. For example: - 1. Navigate to - 2. Login using by username: admin password: password - 3. Once you've accessed the website, click the button to connect their managed Facebook pages - 4. Then click the button navigated in to sort their pages. After completing these steps, our users can receive their managed FB page messenger messages, post comments, and likes. permission will give our users to reply back to their posts and comments. - -2. **Tell facebook how you will use this permission.** In this step, you must provide - - a. a detailed description of how your app uses the permission or feature requested, how it adds value for a person using your app, and why it's necessary for app functionality. - - b. a step-by-step video walkthrough of above. - - c. [Here](https://bit.ly/2J6j5Oi) is a sample video. - -**publish_pages:** - -Grants your app permission to publish posts, comments, and like Pages managed by a person using your app. Also requires the ***manage_pages*** permission. - -To take this permission, you must complete the following 3 steps. - -1. **Provide verification details. In this step, you must provide detailed step-by-step instructions on how a reviewer can test your integration and how you are using the requested permissions or features.** To do so, - - You provide the testing account & password. (Note: Do not provide your personal Facebook account credentials.) - - Then you provide each steps required to test your integration. For example: - 1. Navigate to - 2. Login using by username: admin password: password - 3. Once you've accessed the website, click the button to connect their managed Facebook pages - 4. Then click the button navigated in to sort their pages. After completing these steps, our users can receive their managed FB page messenger messages, post comments, and likes. permission will give our users to reply back to their posts and comments. - -2. **Tell facebook how you will use this permission.** In this step, you must provide - - a. a detailed description of how your app uses the permission or feature requested, how it adds value for a person using your app, and why it's necessary for app functionality. - - b. a step-by-step video walkthrough of above. - - c. your subbmission must include ***manage_pages*** permission - - d. [Here](https://bit.ly/2J6j5Oi) is a sample video. - -3. **Tell facebook how you will use *manage_pages* permission if you haven't taken it yet.** - -## Twitter Integration - -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. - -Requirements: - -- To create twitter app, profile must be phone verified. - -Notes: - -- Twitter now allows you to receive DM from anyone. You don't have to follow each other. To enable this option - go to https://twitter.com/settings/safety and select `Receive Direct Messages from anyone` from `Direct Messages` section. - -### Creating Twitter app. - -1. Go to https://apps.twitter.com/ and create new app. -2. Fill the form with following example and create your application. - -- Name: erxes.domain.com -- Website: erxes.domain.com -- Callback URL: https://erxes.domain.com/service/oauth/twitter_callback - -3. Go to Permissions tab and select Read, Write and Access direct messages. Don't forget to Update settings button. -4. Go to Keys and Access Tokens tab and copy: - Consumer Key (API Key), Consumer Secret (API Secret) values to `erxes-api/.env` file. - -Your .env should look like: - -`TWITTER_CONSUMER_KEY='Consumer Key (API Key) here'` - -`TWITTER_CONSUMER_SECRET='Consumer Secret (API Secret) here'` - -`TWITTER_REDIRECT_URL='https://erxes.domain.com/service/oauth/twitter_callback - Your callback url from app settings.'` - -### Erxes twitter integration settings. - -1. Go to your erxes.domain.com - settings - integrations page -2. Click on **Add Integrations** and select Twitter. Click on Authorize app. -3. Select your brand and click save. - -## Gmail Integration - -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. With the help of gmail API we have many more possibilities, like realtime email synchronization, send & reply email etc. There is an active development process going on this subject. - -### 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 - - - - - In application, type select web application and fill out the rest of the form - - - - - 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 we will need the 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 - - 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 - - - -### Shell env - - You also need to set env to your shell session - ```shell - export GOOGLE_APPLICATION_CREDENTIALS='path/to/your/google_cred.json' # service account json file - ``` - -### Config env - - Now we are good to config our env as follows, - - - - You need to restart the erxes-integration repo after configuring .env - - - Go to erxes settings - App store - add gmail. (Make sure you create new brand beforehand) - -## AWS S3 Integration - -1. Configure AWS account settings in `erxes-api/.env` like below - -```Shell -AWS_ACCESS_KEY_ID='your aws account access key id' -AWS_SECRET_ACCESS_KEY='your aws account secret key' - -AWS_BUCKET='aws bucket name' -AWS_PREFIX='' -``` - -- You can get your aws access key id and region from [here](https://console.aws.amazon.com/console/home) - , You can not get your current aws secret access key, however you can always create new one and claim newly created aws secret access key -- Make sure your IAM user has proper access to S3 services. - -## AWS SES Integration - -Amazon Simple Email Service enables you to send and receive email using a reliable and scalable email platform. Set up your custom amazon simple email service account. - -### Configure Amazon SES and Amazon SNS to track each email responses. - -1. [ Log in to your AWS Management Console. ](https://console.aws.amazon.com) -2. Click on your user name at the top right of the page. -3. Click on the My Security Credentials link from the drop-down menu. -4. Click on the Users menu from left Sidebar. -5. Click on the Add user. -6. Then create your username and check Programmatic access type and click next. -7. Click on the Create group then write group name and check amazonSesFullAccess and amazonSNSFullAccess. -8. Then check your created group and click on the Next button. -9. Finally click on the create user and copy the Access Key Id and Secret Access Key. - - -### To find your Region. - -1. [ Log in to your AWS Management Console.](https://console.aws.amazon.com) -2. Click on services menu at the top left of the page. -3. Find Simple Email Service and Copy region code from url. - -**If you choose not available region** -1. Click on your region at the top right of the menu. -2. Select any active region from list. -3. Copy the selected Region code. -_(example: us-east-1, us-west-2, ap-south-1, ap-southeast-2, eu-central-1, eu-west-1)_ - - -### To determine if your account is in the sandbox. -1. [Open the Amazon SES console at https://console.aws.amazon.com/ses/](https://console.aws.amazon.com/ses/) -2. Use the Region selector to choose an AWS Region. -3. If your account is in the sandbox in the AWS Region that you selected, you see a banner at the top of the page that resembles the example in the following figure. - - - -4. If the banner doesn't appear on this page, then your account is no longer in the sandbox in the current Region. - - - - -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. - - -### Paste Amazon-Ses Access Keys to Erxes AWS-SES engage. - -1. Login Erxes, go to Settings menu => Appstore. -2. Click on the “Appstore” menu -3. Click manage to AWS-SES engage configuration - - - -4. Paste the AWS-SES access key ID, AWS-SES secret access key and AWS-SES region which you have created user in AWS Management console. - - - -### Test configuration. - -Amazon places all new accounts in the Amazon SES sandbox. While your account is in the sandbox, you can use all of the features of Amazon SES. However, when your account is in the sandbox, Amazon have applied the following restrictions to your account: - -+ You can only send mail to verified email addresses and domains, or to the Amazon SES mailbox simulator. - -+ You can only send mail from verified email addresses and domains. - - - -## Nylas Integration - -1. Create the Nylas account go to [website](https://dashboard.nylas.com/register) -2. After you created the Nylas account, copy your clientId and clientSecret from [here](https://dashboard.nylas.com/applications/) and config in `erxes-integrations/.env` like below -```Shel -# Nylas -NYLAS_CLIENT_ID='nylas account CLIENT ID' -NYLAS_CLIENT_SECRET='nylas account CLIENT_SECRET' -NYLAS_WEBHOOK_CALLBACK_URL=http://localhost:3400/nylas/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 - ``` - - Copy the IP address with https and replace `erxes-integrations/.env` as follows: - ```Shell - NYLAS_WEBHOOK_CALLBACK_URL=http://localhost:3400/nylas/webhook - NYLAS_WEBHOOK_CALLBACK_URL=https://NGROK_IP/nylas/webhook - ``` - When you start erxes-integration repo webhook will automatically created according to `.env` - #### Now we are ready to config our provider -### Gmail -1. Create the Google project and config gmail for the [Nylas guide](https://docs.nylas.com/docs/creating-a-google-project-for-dev) - - Get the following config from your Google project and config as follows in `erxes-integrations/.env` - ```Shell - GOOGLE_PROJECT_ID='google project id' - GOOGLE_CLIENT_ID='google client id' - GOOGLE_CLIENT_SECRET='google client secret' - GOOGLE_APPLICATION_CREDENTIALS=./google_cred.json - ``` - - In order to have Google OAuth token, add authorized redirect URIs to your google credentials - - Select Google project - - Go to credentials from left side menu - - Select OAuth 2.0 client ID - - Add following uri in authorized redirect URI `http://localhost:3400/nylas/oauth2/callback` - - After you create the Google service account download json and replace with `google_cred.json` -### Yahoo -2. In order to integrate the Yahoo you will need to generate app password for the Erxes, please follow below steps. - - Go to Settings/App Store and click on Add button of the Yahoo section -
- -
- - - You will see a modal, then click on add account - -
- -
- - - Now you need to generate password for erxes, go ahead and click the link. - -
- -
- - - You will be jump into Yahoo, sign in and click on Account Security in Settings as follows. - -
- -
- - - Scroll to bottom and click on Generate app password link. - -
- -
- - - Click on the Select an app and select Other app. - -
- -
- - - Then name your app as Erxes and click on the Generate button. - -
- -
- - - Great, you got the password, Now copy password and navigate back to the Erxes Settings/App Store - -
- -
- - - Fill your email address and paste your password, that is it click on the save button and create yahoo integration. - -
- -
- -### Outlook -3. Integrating the Outlook is easy peasy lemon squeezy, all we need is email and password no additional steps. - - Go to Settings/App Store and click on Add button of the Outlook section -
- -
- - - Click on the Add account button then you will see form -
- -
- - - Enter your outlook email, password and click on save button that's it now you can create your Outlook integration. -
- -
\ No newline at end of file diff --git a/docs/docs/administrator/migration.md b/docs/docs/administrator/migration.md index 23c8edb36a5..39bf8ab0936 100644 --- a/docs/docs/administrator/migration.md +++ b/docs/docs/administrator/migration.md @@ -3,7 +3,7 @@ id: migration title: Migration --- -If upgrading erxes version requires database migration, you have to run following command: +If upgrading Erxes version requires database migration, you have to run following command: ``` yarn migrate diff --git a/docs/docs/administrator/system-config.md b/docs/docs/administrator/system-config.md new file mode 100644 index 00000000000..786d13f15cb --- /dev/null +++ b/docs/docs/administrator/system-config.md @@ -0,0 +1,769 @@ +--- +id: system-config +title: System config +--- + +In this tutorials enable you to set up administration system configurations, which allows you work on sysadmin without require coding skills. + +--- + + +## General system configuration + +### General settings + +In the general setting, you can configure the system language, currency and the unit of measurement. Select the variables relied upon your requirement. + +**Configuration:** +- Go to Erxes Settings => System config => General System Config => General settings. + +``` +LANGUAGE='English' +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)**. + + +**Configuration:** +- Go to Erxes Settings => System config => General System Config => File upload. + +``` +UPLOAD_FILE_TYPES='image/png, application/pdf' +UPLOAD_FILE_TYPES_OF_WIDGET='image/png, application/pdf' +UPLOAD_SERVICE_TYPE='Amazon Web Services' +BUCKET_FILE_SYSTEM_TYPE='Public' +``` + +See the following figure which approves the **png**, **pdf** files and other type of media do not allowed to upload the server. If there is nothing configured media type, it accepts all media types. + + + + +- `UPLOAD_FILE_TYPES`, `UPLOAD_FILE_TYPES_OF_WIDGET` have to set same file type. [Here is a list of MIME types, associated by type of documents, ordered by their common extensions.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types) +- You can upload the files through **Amazon Web Services** or **Google Cloud Service** on `UPLOAD_SERVICE_TYPE`. + +- You can select the permission as **public** or **private** on `BUCKET_FILE_SYSTEM_TYPE`. + + + + +### 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. + +**Configuration:** +- Go to Erxes Settings => System config => General System Config => AWS S3. + +``` +AWS_ACCESS_KEY_ID='your aws account access key id' +AWS_SECRET_ACCESS_KEY='your aws account secret key' +AWS_BUCKET='aws bucket name' +AWS_PREFIX='you can use prefix names to specify the names of the files to be uploaded' +AWS_COMPATIBLE_SERVICE_ENDPOINT='' +AWS_FORCE_PATH_STYLE='' +``` +- [ Log in to your AWS Management Console. ](https://console.aws.amazon.com) +- You can get your `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from AWS My Security Credentials=> Access keys (access key ID and secret access key). +- [Create new bucket](https://docs.aws.amazon.com/AmazonS3/latest/gsg/CreatingABucket.html) and insert name in `AWS_BUCKET`, make sure bucket permission configuration. + +- Make sure your IAM user has proper access to S3 services. [Learn more about public access.](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/block-public-access.html) + +- `AWS_COMPATIBLE_SERVICE_ENDPOINT`, if you need to override an endpoint for a service, you can set the endpoint on a service by passing the endpoint object with the endpoint option key. [Refer to AWS service endpoint.](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Endpoint.html) + +- `AWS_FORCE_PATH_STYLE`, some services have very specific configuration options that are not shared by other services. [Refer to AWS force path style.](https://docs.aws.amazon.com/sdkforruby/api/Aws/Plugins/GlobalConfiguration.html) + + + +### AWS SES + +Amazon Simple Email Service enables you to send and receive email using a reliable and scalable email platform. Set up your custom Amazon simple email service account. + +**Configuration:** +- Go to Erxes Settings => System config => General System Config => AWS SES. + +``` +AWS_SES_ACCESS_KEY_ID='your aws account access key id' +AWS_SES_SECRET_ACCESS_KEY='your aws account secret key' +AWS_REGION='region of your account' +AWS_SES_CONFIG_SET='' +``` +`AWS_SES_CONFIG_SET` is detect bounce, complaints, click and open events which you can be use this option. This name can be anything. + +#### Configure Amazon SES and Amazon SNS to track each email responses. + +1. [ Log in to your AWS Management Console. ](https://console.aws.amazon.com) +2. Click on your user name at the top right of the page. +3. Click on the My Security Credentials link from the drop-down menu. +4. Click on the Users menu from left Sidebar. +5. Click on the Add user. +6. Then create your username and check Programmatic access type and click next. +7. Click on the Create group then write group name and check amazonSesFullAccess and amazonSNSFullAccess. +8. Then check your created group and click on the Next button. +9. Finally click on the create user and copy the `AWS_SES_ACCESS_KEY_ID` and `AWS_SES_SECRET_ACCESS_KEY`. + + +#### To find your `AWS_REGION`. + +1. [ Log in to your AWS Management Console.](https://console.aws.amazon.com) +2. Click on services menu at the top left of the page. +3. Find Simple Email Service and Copy region code from url. + +**If you choose not available region** +1. Click on your region at the top right of the menu. +2. Select any active region from list. +3. Copy the selected Region code. +_(example: us-east-1, us-west-2, ap-south-1, ap-southeast-2, eu-central-1, eu-west-1)_ + + +#### Determine whether your account is in the sandbox or not. + +Amazon places all new accounts in the Amazon SES sandbox. While your account is in the sandbox, you can use all of the features of Amazon SES. However, when your account is in the sandbox, Amazon have applied the following restrictions to your account: + ++ You can only send mail to verified email addresses and domains, or to the Amazon SES mailbox simulator. + ++ You can only send mail from verified email addresses and domains. + +**Verify it for following steps:** +1. [Open the Amazon SES console at https://console.aws.amazon.com/ses/](https://console.aws.amazon.com/ses/) +2. Use the Region selector to choose an AWS Region. +3. If your account is in the sandbox in the AWS Region that you selected, you see a banner at the top of the page that resembles the example in the following figure. + + + +4. If the banner doesn't appear on this page, then your account is no longer in the sandbox in the current Region. + + + +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. + +``` +FROM_EMAIL='your email address' +DEFAULT_EMAIL_SERVICE='your configured email service name' + +``` + + + +### 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. + +### 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) + + + + + + - SideMenu => APIs Services => OAuth Consent screen => Create + + + + - Fill out rest of the form and Click on the Add scope button + + + + - Search for Gmail API and select scopes as below + + + + + + - 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 get your CLIENT_ID, CLIENT_SECRET, We are going to use these in the Erxes App system config later on + + + + - Now select your newly created OAuth client and add redirect URI for OAuth2 authorization + + + + + + - We also need to enable **Cloud Pub/Sub API** in order to receive our email as **real-time** + + + + + + - Navigate to SideMenu => IAM & Admin => IAM + + - 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 Let's config our Erxes app + + - 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 + + + + - Final touch, navigate to Settings => System configs => Integrations config + + - Enable **USE DEFAULT GMAIL SERVICE** + + - Enter your **GOOGLE GMAIL TOPIC**, **GOOGLE GMAIL SUBSCRIPTION NAME** names as single string + + + + 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: + +- Working sub domain with SSL pointing to your erxes-api server. +- [Create a Facebook App](https://developers.facebook.com/docs/apps/) +- [Create a Facebook Page](https://www.facebook.com/pages/creation/) + + +**Configuration:** +- Go to Erxes Settings => System config => Integrations config => Facebook. + +``` +FACEBOOK_APP_ID="your faceboook application's app id" +FACEBOOK_APP_SECRET="your faceboook application's secret key" +FACEBOOK_VERIFY_TOKEN="insert facebook application verify token" + +``` + + +#### Creating facebook app. + +1. Create new app.
+ ( Your application status must be "Live" ) +2. Your application must have these permissions:
+ `manage_pages, pages_show_list, pages_messaging, publish_pages, pages_messaging_phone_number, pages_messaging_subscriptions`
+ (You can get these permissions through your App Review) +3. Go to developer application setting=> basic=> get variables
+ `FACEBOOK_APP_ID`, your Facebook application's ID
+ `FACEBOOK_APP_SECRET`, your Facebook application's Secret code
+ +4. You must have `Facebook Login` product set up. +5. Go to `Facebook Login` => `Settings` fill the field names `Valid OAuth Redirect URIs` like below:
+ `/fblogin` +6. Now we are done on configurations. + +#### Erxes facebook integration settings. + +1. Go to Erxes settings => App store +2. Click on **Add Facebook messenger**. Click on Authorize app. +3. Select your brand and click save. +4. Go to Setting=> Channel=> Add new channel=> Connect facebook integration. + + +#### Getting facebook permissions (*manage_pages*, *publish_pages*) + +**manage_pages:** + +Grants an app permission to retrieve Page Access Tokens for the Pages and Apps that the app user administers. Apps that allow users to publish as a Page must also have the ***publish_pages*** permission. To take this permission, you must complete the following 2 steps. + +1. Provide verification details. In this step, you must provide detailed step-by-step instructions on how a reviewer can test your integration and how you are using the requested permissions or features. To do so, + - You provide the testing account & password. (Note: Do not provide your personal Facebook account credentials.) + - Then you provide each steps required to test your integration. For example: + 1. Navigate to + 2. Login using by username: admin password: password + 3. Once you've accessed the website, click the button to connect their managed Facebook pages + 4. Then click the button navigated in to sort their pages. After completing these steps, our users can receive their managed FB page messenger messages, post comments, and likes. permission will give our users to reply back to their posts and comments. + +2. You need to send request to facebook and explain that how you will use this permission. In this step, you must provide + - a. a detailed description of how your app uses the permission or feature requested, how it adds value for a person using your app, and why it's necessary for app functionality. + - b. a step-by-step video walkthrough of below. + - c. [Here](https://bit.ly/2J6j5Oi) is a sample video. + +**publish_pages:** + +Grants your app permission to publish posts, comments, and like Pages managed by a person using your app. Also requires the ***manage_pages*** permission. + +To take this permission, you must complete the following 3 steps. + +1. Provide verification details. In this step, you must provide detailed step-by-step instructions on how a reviewer can test your integration and how you are using the requested permissions or features. To do so, + - You provide the testing account & password. (Note: Do not provide your personal Facebook account credentials.) + - Then you provide each steps required to test your integration. For example: + 1. Navigate to + 2. Login using by username: admin password: password + 3. Once you've accessed the website, click the button to connect their managed Facebook pages + 4. Then click the button navigated in to sort their pages. After completing these steps, our users can receive their managed FB page messenger messages, post comments, and likes. permission will give our users to reply back to their posts and comments. + +2. Tell facebook how you will use this permission. In this step, you must provide + - a. a detailed description of how your app uses the permission or feature requested, how it adds value for a person using your app, and why it's necessary for app functionality. + - b. a step-by-step video walkthrough of above. + - c. your subbmission must include ***manage_pages*** permission + - d. [Here](https://bit.ly/2J6j5Oi) is a sample video. + +3. Tell facebook how you will use *manage_pages* permission if you haven't taken it yet. + +### Twitter + +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. + +Requirements: + +- [Create twitter app,](https://developer.twitter.com/en/docs/basics/apps/overview) profile must be phone verified. + +Notes: + +- Twitter now allows you to receive DM from anyone. You don't have to follow each other. To enable this option + go to https://twitter.com/settings/safety and select `Receive Direct Messages from anyone` from `Direct Messages` section. + +**Configuration:** +- Go to Erxes Settings => System config => Integrations config => Twitter. + + +``` +TWITTER_CONSUMER_KEY="your app consumer key" +TWITTER_CONSUMER_SECRET="your app consumer secret key" +TWITTER_ACCESS_TOKEN="your app consumer Secret" +TWITTER_ACCESS_TOKEN_SECRET='' +TWITTER_WEBHOOK_ENV='' +``` + +- `TWITTER_CONSUMER_KEY`, your twitter developer account's Consumer Key (API Key) here +- `TWITTER_CONSUMER_SECRET` your twitter developer account's Consumer Secret (API Secret) here +- `TWITTER_ACCESS_TOKEN` your twitter developer account's secret token ID (API Secret) here +- `TWITTER_ACCESS_TOKEN_SECRET` you should only change the domain of this env variables. This is twitter's callback url +- `TWITTER_WEBHOOK_ENV`='https://erxes.domain.com/service/oauth/twitter_callback' + +1. [Create twitter app,](https://developer.twitter.com/en/docs/basics/apps/overview) +2. Fill the form with following example and create your application. + +- Name: erxes.domain.com +- Website: erxes.domain.com +- Callback URL: https://erxes.domain.com/service/oauth/twitter_callback + +3. Go to Permissions tab and select Read, Write and Access direct messages. Don't forget to Update settings button. + + + +#### Erxes twitter integration settings. + +1. Go to Erxes settings => App store +2. Click on **Add Twitter direct message**. Click on Authorize app. +3. Select your brand and click save. +4. Go to Setting=> Channel=> Add new channel=> Connect Twitter integration. + +### 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: + +- [Create a Daily account](https://www.daily.co) +- Create new custom domain (subdomain) on the account. For instance: example.daily.co + + +**Configuration:** +- 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" +``` +- `DAILY_API_KEY='######'` Get API key from Daily account Developers tab. +- `DAILY_END_POINT ='example.daily.co'` is your subdomain name. + +Integrated video chat is used on the Erxes messenger widget. It is assumed that the one conversation can be activated one video call. + +## 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. + + + +**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 and you can find in your Nylas app [dashboard](https://dashboard.nylas.com/) +#### Now we are ready to config our Nylas provider + + + +### Outlook +Learn how to integrate Outlook Accounts With Erxes. +Integrating the Outlook is easy peasy lemon squeezy, all we need is email and password no additional steps. + +**Configuration:** +- Go to Erxes Settings => System config => Integrations config => Outlook. + +``` +# 32 bit characters +# ex: aes-256-cbc +ALGORITHM = ''; +ENCRYPTION_KEY='' +``` + + - Go to Settings/App Store and click on Add button of the Outlook section +
+ +
+ + - Click on the Add account button then you will see form +
+ +
+ + - Enter your outlook email, password and click on save button that's it now you can create your Outlook integration. +
+ +
+ + + +### 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. + +**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. + + + + - Add following scopes in your OAuth consent screen + + ```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** + + + +Basic integration setting has done. 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 by Nylas. Connect your account. +3. Select your brand and click save. +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. + - Go to Settings/App Store and click on Add button of the Yahoo section +
+ +
+ + - You will see a modal, then click on add account + +
+ +
+ + - Now you need to generate password for erxes, go ahead and click the link. + +
+ +
+ + - You will be jump into Yahoo, sign in and click on Account Security in Settings as follows. + +
+ +
+ + - Scroll to bottom and click on Generate app password link. + +
+ +
+ + - Click on the Select an app and select Other app. + +
+ +
+ + - Then name your app as Erxes and click on the Generate button. + +
+ +
+ + - Great, you got the password, Now copy password and navigate back to the Erxes Settings/App Store + +
+ +
+ + - Fill your email address and paste your password, that is it click on the save button and create yahoo integration. + +
+ +
+ + + +## 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) + ++ Click on your profile, then select settings. + + + ++ Copy API key value + + + + + **Configuration:** + +- Go to Erxes Settings => System config => Integrations config => WhatsApp Chat-API. + + + ++ Paste API key to corresponding field. + ++ 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. 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. + + +## Sunshine Conversations API Integration + +1. Create your Sunshine Conversations API account [here](https://smooch.io/signup/). + +2. After you create a account. Sign in to [smooch.io](https://app.smooch.io/login). + +3. Create new Sunshine Conversations app + + + +4. Go to your created app. Then select Settings tab. + + + +5. In order to create api key click on "Generate API key". + +6. Copy and paste your App id, API Key ID and secret to Erxes Settings => System config => Integrations config => Sunshine Conversations API + + + + + +7. Put your webhook url into SMOOCH WEBHOOK CALLBACK URL field. ++ For example 'https://erxes-integrations/smooch/webhook' + +### Viber + +1. Create a Public Account ++ You can create an account for testing and development purpose by registering on the [Viber admin panel](https://partners.viber.com/). Note that accounts created this way can’t be discovered by the public and Viber limits the messaging volume on them. +To create an account for production usage, contact Viber directly using [this form](https://support.viber.com/customer/portal/emails/new). + +2. Once you have your Public Account token, copy and paste it into Viber token field on the Add Viber page from erxes App Store. Then click on “Save”. + + + + + + +### Telegram + +1. Sign in to telegram [here](https://web.telegram.org/#/login). + +2. In order to create telegram bot go to [BotFather](https://telegram.me/botfather). + + + +3. Type /newbot and send it to BotFather. Then follow the instructions from BotFather to create a bot. + + + +4. Then copy your bot token and paste it into Telegram Bot Token field on the add Telegram page from erxes App Store. Then click on “Save” + + + + + + + +## Engage configurationsmo + +### AWS SES + +Amazon SES service enables on Erxes Engage system. Another custom mail service is not allowed on Engage system. +AWS SES configuration is similar with Integration AWS SES. [Go to settings here](https://docs.erxes.io/administrator/system-config#aws-ses) + +### Verify email + +Amazon places all new accounts in the Amazon SES sandbox. While your account is in the sandbox, you can use all of the features of Amazon SES. However, when your account is in the sandbox, Amazon have applied the following restrictions to your account: + ++ You can only send mail to verified email addresses and domains, or to the Amazon SES mailbox simulator. + ++ You can only send mail from verified email addresses and domains. + +Insert emails and verify it. + + +### Send test email + + diff --git a/docs/docs/developer/android-sdk.md b/docs/docs/developer/android-sdk.md index a878e13cb57..d349dff8cc1 100644 --- a/docs/docs/developer/android-sdk.md +++ b/docs/docs/developer/android-sdk.md @@ -6,6 +6,7 @@ sidebar_label: Android SDK +These steps indicate how to install the Android SDK, for that you must follow the instructions explained below. ## Installation diff --git a/docs/docs/developer/contributing.md b/docs/docs/developer/contributing.md index 6d9d95f11ea..5d6094999d5 100644 --- a/docs/docs/developer/contributing.md +++ b/docs/docs/developer/contributing.md @@ -3,15 +3,16 @@ id: contributing title: Contributing --- -## Contributing to erxes +## Contributing to Erxes -We would love for you to contribute to erxes and help make it even better than it is today! As a contributor, here are the guidelines we would like you to follow: +We would love for you to contribute to Erxes and help make it even better than it is today! As a contributor, here are the guidelines we would like you to follow: * [Issues and Bugs](#found-a-bug) * [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!) + +

+

diff --git a/docs/docs/developer/developer.md b/docs/docs/developer/developer.md index 641bb6872ab..b9335715477 100644 --- a/docs/docs/developer/developer.md +++ b/docs/docs/developer/developer.md @@ -4,7 +4,7 @@ title: Developing erxes sidebar_label: Developer --- -This document describes how to set up your development environment to develop and test erxes. It also explains the basic mechanics of using `git`, `node`, and `yarn`. +This document describes how to set up your development environment to develop and test Erxes. It also explains the basic mechanics of using `git`, `node`, and `yarn`. * [Prerequisite Software](#prerequisite-software) * [Getting the Sources](#getting-the-sources) @@ -27,7 +27,7 @@ Before you can develop and test erxes, you must install and configure the follow 2. Configure your ssh key [here](https://github.com/settings/keys). -3. Run erxes backend. +3. Run Erxes backend. ```sh # Clone your GitHub repository: @@ -49,7 +49,7 @@ yarn loadInitialData yarn dev ``` -4. Run erxes frontend. +4. Run Erxes frontend. ```sh # Clone your GitHub repository: diff --git a/docs/docs/developer/graphql-api.md b/docs/docs/developer/graphql-api.md index 8e742980361..5d1def735c7 100644 --- a/docs/docs/developer/graphql-api.md +++ b/docs/docs/developer/graphql-api.md @@ -4,9 +4,12 @@ title: GraphQL API Schema sidebar_label: GraphQL API --- +This document explains how to work with GraphQL API Schema. + + ## GraphQL API Schema -To see the GraphQL schema of the erxes GraphQL API, you should use some GraphQL graphical tool. +To see the GraphQL schema of the Erxes GraphQL API, you should use some GraphQL graphical tool. These tools are able to show the schema, using the Instropection tool of GraphQL. You can use the instrospection queries to show the schema, but by a graphical tool is easier. Below we have some options of tools, and where you can see the schema in each of them. diff --git a/docs/docs/developer/ios-sdk.md b/docs/docs/developer/ios-sdk.md index 929402fd650..9a825996e67 100644 --- a/docs/docs/developer/ios-sdk.md +++ b/docs/docs/developer/ios-sdk.md @@ -4,6 +4,8 @@ title: iOS SDK sidebar_label: iOS SDK --- +Learn how to install the iOS SDK. + ## Requirement @@ -46,12 +48,12 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau #### NEXT -### To start erxes SDK in your app: +### To start Erxes SDK in your app: ```swift import ErxesSDK ``` -##### This function will start erxes SDK with authentication options: +##### This function will start Erxes SDK with authentication options: ```swift @ibaction func buttonAction(sender:Uibutton){ Erxes.start() diff --git a/docs/docs/developer/push-notifications.md b/docs/docs/developer/push-notifications.md index adcaa905493..74c7e18df4f 100644 --- a/docs/docs/developer/push-notifications.md +++ b/docs/docs/developer/push-notifications.md @@ -6,38 +6,29 @@ 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 -

-
-
- -

- 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 + +#### 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) + + + + - 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/developer/troubleshooting.md b/docs/docs/developer/troubleshooting.md new file mode 100644 index 00000000000..74f38f71e0d --- /dev/null +++ b/docs/docs/developer/troubleshooting.md @@ -0,0 +1,40 @@ +--- +id: troubleshooting +title: Troubleshooting +sidebar_label: Troubleshooting +--- + +## Nylas +Having troubles with your own Nylas App? We recommend you to read this [developer guide](https://docs.nylas.com/docs) + +## Integration +* After you create an integration do not forget to add it to a channel, Otherwise, you will not see an email or message integration in Inbox + +## Password Encryption +In Outlook, Yahoo, IMAP providers you will need to enter your password so following configs required for encryption +- ALGORITHM +- ENCRYPTION KEY + + + +## Nylas IMAP +> Before you add IMAP account please make sure that you already config ENCRYPTION KEY, ALGORITHM [here](#password-encryption) + +When you create the IMAP account check you entered correct values for example: + + + +* Common [a List of SMTP and IMAP Server](https://www.arclab.com/en/kb/email/list-of-smtp-and-imap-servers-mailserver-list.html) +* If you have Nylas IMAP specific problem read [here](https://docs.nylas.com/docs/imap) + +## Nylas Gmail +* After removing your Nylas Gmail account from Erxes you will also need to revoke your Erxes app [Google App permissions]( https://myaccount.google.com/permissions) + +## Gmail +* Before you use Gmail integration please make sure that you enter correct GOOGLE TOPIC, GOOGLE GMAIL SUBSCRIPTION NAME it should be single string otherwise you will get invalid_format error. + + + +* Permission Denied, when creating or checking Google Topic and Subscription make sure your service account has owner role in IAM & Admin -> IAM + +* If you are not receiving any emails, please check you add Grant Publish Topic right to **gmail-api-push@system.gserviceaccount.com** in IAM & Admin -> IAM -> Add \ No newline at end of file diff --git a/docs/docs/installation/aws.md b/docs/docs/installation/aws.md new file mode 100644 index 00000000000..2ca938a5ed9 --- /dev/null +++ b/docs/docs/installation/aws.md @@ -0,0 +1,80 @@ +--- +id: aws +title: AWS Marketplace +--- + +Launch an EC2 instance selecting `erxes` in the AWS Marketplace. +Once you have created the EC2 instance using erxes AMI product in the AWS Marketplace, you will then have erxes up and running and it will be accessible by public hostname of the EC2 instance. + +## Create an admin user + +Connect to your EC2 instance via ssh. + +`ssh -i your.pem ubuntu@your-instance-dns` + +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 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/centos8.md b/docs/docs/installation/centos8.md index a690799b2b4..25006b02a60 100644 --- a/docs/docs/installation/centos8.md +++ b/docs/docs/installation/centos8.md @@ -3,6 +3,8 @@ id: centos8 title: CentOS 8 --- +This steps explain how to install Erxes on CentOS 8. + ## Installing erxes on CentOS 8 To have erxes up and running quickly, you can follow the following steps. @@ -15,11 +17,37 @@ To have erxes up and running quickly, you can follow the following steps. **Note**: you will be asked to provide a domain for nginx server to set up config for erxes -3. This installation will create a new user `erxes`. Run the following command to change your password. +3. Log in to your domain DNS and create A record based on your new server IP. + +## Create an admin user + +Switch to user `erxes` and run the following commands based on your needs. + +```sh +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 +``` - `passwd erxes` +If do not want to load sample data then you can run following command just to load permissions. -4. Log in to your domain DNS and create A record based on your new server IP. +``` +yarn loadPermission +``` Now you have erxes up and running! @@ -31,34 +59,34 @@ The following is the demonstration of how to secure your nginx by installing let 1. Log in to your server as `root` via `ssh` and run the following commands. - ```sh - curl -O https://dl.eff.org/certbot-auto - mv certbot-auto /usr/local/bin/certbot-auto - chmod 0755 /usr/local/bin/certbot-auto - /usr/local/bin/certbot-auto --nginx - ``` +```sh +curl -O https://dl.eff.org/certbot-auto +mv certbot-auto /usr/local/bin/certbot-auto +chmod 0755 /usr/local/bin/certbot-auto +/usr/local/bin/certbot-auto --nginx +``` - `/usr/local/bin/certbot-auto --nginx` command will install multiple Python packages that are required by certbot and prompt you to answer some questions. Please follow the interactive prompt. +`/usr/local/bin/certbot-auto --nginx` command will install multiple Python packages that are required by certbot and prompt you to answer some questions. Please follow the interactive prompt. 2. Log in to your server as `erxes` via `ssh` or switch to `erxes` from `root` user. Edit `erxes/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: "https://your_domain/api", - REACT_APP_API_SUBSCRIPTION_URL: "wss://your_domain/api/subscriptions", - REACT_APP_CDN_HOST: "https://your_domain/widgets" - }; - ``` +```javascript +window.env = { + PORT: 3000, + NODE_ENV: "production", + REACT_APP_API_URL: "https://your_domain/api", + REACT_APP_API_SUBSCRIPTION_URL: "wss://your_domain/api/subscriptions", + REACT_APP_CDN_HOST: "https://your_domain/widgets" +}; +``` 3. Update all env vars with HTTPS url in the `ecosystem.json` file. 4. Finally, you need to restart pm2 erxes processes by running the following command: - ```sh - pm2 restart ecosystem.json - ``` +```sh +pm2 restart ecosystem.json +``` - If you need more information about pm2, please go to official documentation [here](https://pm2.keymetrics.io/docs/usage/application-declaration/). +If you need more information about pm2, please go to official documentation [here](https://pm2.keymetrics.io/docs/usage/application-declaration/). diff --git a/docs/docs/installation/debian10.md b/docs/docs/installation/debian10.md index 23ecee09f08..af7d9e3da40 100644 --- a/docs/docs/installation/debian10.md +++ b/docs/docs/installation/debian10.md @@ -3,6 +3,8 @@ id: debian10 title: Debian 10 --- +This steps explain how to install Erxes on Debian 10. + ## Installing erxes on Debian 10 To have erxes up and running quickly, you can follow the following steps. @@ -15,11 +17,37 @@ To have erxes up and running quickly, you can follow the following steps. **Note**: you will be asked to provide a domain for nginx server to set up config for erxes -3. This installation will create a new user `erxes`. Run the following command to change password. +3. Log in to your domain DNS and create A record based on your new server IP. + +## Create an admin user + +Switch to user `erxes` and run the following commands based on your needs. + +```sh +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 +``` - `passwd erxes` +If do not want to load sample data then you can run following command just to load permissions. -4. Log in to your domain DNS and create A record based on your new server IP. +``` +yarn loadPermission +``` Now you have erxes up and running! @@ -33,21 +61,21 @@ Once you have installed your ssl certificate, you need to update env vars. 2. Edit `erxes/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: "https://your_domain/api", - REACT_APP_API_SUBSCRIPTION_URL: "wss://your_domain/api/subscriptions", - REACT_APP_CDN_HOST: "https://your_domain/widgets" - }; - ``` +```javascript +window.env = { + PORT: 3000, + NODE_ENV: "production", + REACT_APP_API_URL: "https://your_domain/api", + REACT_APP_API_SUBSCRIPTION_URL: "wss://your_domain/api/subscriptions", + REACT_APP_CDN_HOST: "https://your_domain/widgets" +}; +``` 3. Update all env vars with HTTPS url in the `ecosystem.json` file. 4. Finally, you need to restart pm2 erxes processes by running the following command: - ```sh - pm2 restart ecosystem.json - ``` +```sh +pm2 restart ecosystem.json +``` - If you need more information about pm2, please go to official documentation [here](https://pm2.keymetrics.io/docs/usage/application-declaration/). +If you need more information about pm2, please go to official documentation [here](https://pm2.keymetrics.io/docs/usage/application-declaration/). 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/installation/docker.md b/docs/docs/installation/docker.md index 89c0377b068..3e3e1879b00 100644 --- a/docs/docs/installation/docker.md +++ b/docs/docs/installation/docker.md @@ -3,6 +3,8 @@ id: docker title: Docker --- +This steps explain how to install Erxes on Docker Hub, to do it, follow the instructions. + [erxes on docker hub](https://hub.docker.com/u/erxes/) ## Prerequisites diff --git a/docs/docs/installation/heroku.md b/docs/docs/installation/heroku.md index 327708e53ce..6be56fcd5fb 100644 --- a/docs/docs/installation/heroku.md +++ b/docs/docs/installation/heroku.md @@ -3,7 +3,9 @@ id: heroku title: Heroku --- -## Deploying erxes on Heroku +Learn how to deploy Erxes on Heroku. + +## Deploying Erxes on Heroku Heroku is a container-based cloud Platform as a Service (PaaS). diff --git a/docs/docs/installation/ubuntu.md b/docs/docs/installation/ubuntu.md index e55fd45ae4e..9ed54ca112b 100644 --- a/docs/docs/installation/ubuntu.md +++ b/docs/docs/installation/ubuntu.md @@ -3,11 +3,11 @@ id: ubuntu title: Ubuntu 16.04/18.04 LTS --- -Manual installation on Ubuntu 16.04/18.04 LTS. +This document explains how to manually install Ubuntu 16.04 / 18.04 LTS. ## Prerequisites -There are a couple of pre-reqs for running erxes. This page explain how to quickly install the requirenment needed on an Ubuntu 16.04/18.04 server. +There are a couple of pre-reqs for running erxes. This page explains how to quickly install the requirement needed on an Ubuntu 16.04/18.04 server. This guide assumes that the server does not have any other services running on it. ### Install MongoDB v3.6.x diff --git a/docs/docs/installation/upgrade.md b/docs/docs/installation/upgrade.md index fa7eb116d68..37c2aadc483 100644 --- a/docs/docs/installation/upgrade.md +++ b/docs/docs/installation/upgrade.md @@ -4,10 +4,12 @@ title: Upgrade sidebar_label: Upgrade --- +Following the steps in this document you can upgrade the system version. + ## Upgrading from v0.9+ to the latest release vx.x.x ### Breaking Changes -- Since version `latest vx.x.x` erxes started using RabbitMQ as message broker service. To update, please see example changes at docker [installation guide.](docker) +- Since version `latest vx.x.x` Erxes started using RabbitMQ as message broker service. To update, please see example changes at docker [installation guide.](docker) - Engage module is moved to [separate repository](https://github.com/erxes/erxes-engages-email-sender). Also docker [installation guide](docker) is updated to reflect related changes. ### Env changes diff --git a/docs/docs/overview/architecture-overview.md b/docs/docs/overview/architecture-overview.md index febde68b788..d2770101b50 100644 --- a/docs/docs/overview/architecture-overview.md +++ b/docs/docs/overview/architecture-overview.md @@ -5,15 +5,16 @@ sidebar_label: Architecture Overview --- +This document describes the main architecture overview of Erxes. Erxes at its core is a repository that collects all customer requests from various channels including web chats, Facebook, Twitter, Gmail and provides the ability to respond to those requests from one unified location. In order to accomplish these goals, we have 4 main projects.
+ web Widgets, iOS SDK, Android SDK -react, apollo, graphql clients responsible for sending information to erxes-api +react, apollo, graphql clients responsible for sending information to erxes-api. + Erxes apollo, react, graphql -responsible for writing widgets data to database, sending a notification to erxes-api +responsible for writing widgets data to database, sending a notification to erxes-api.
+ Erxes-api nodejs, graphql, apollo responsible for writing erxes fontend project's data to the database, receiving notifications from widgets, talking to external APIs @@ -22,7 +23,7 @@ responsible for writing widgets data to database, sending a notification to erxe + Erxes-integrations (nodejs) responsible for receiving all external api webhook updates including Facebook, Twitter, Gmail, Nylas etc ...
-+ Redis server responsible for handling real-time communications ++ Redis server responsible for handling real-time communications. + MongoDB
diff --git a/docs/docs/overview/deployment-overview.md b/docs/docs/overview/deployment-overview.md index a1e31f041c0..ba3698fb038 100644 --- a/docs/docs/overview/deployment-overview.md +++ b/docs/docs/overview/deployment-overview.md @@ -3,11 +3,15 @@ id: deployment-overview title: Deployment Overview --- +This document details the steps to perform the Erxes installation. + ## Installing erxes Modern server architectures and configurations are managed in many different ways. Some people still put new software somewhere in `opt` manually for each server while others have already jumped on the configuration management train and fully automated reproducible setups. -Erxes can be installed in many different ways so you can pick whatever works best for you. We recommend to start with the [docker images](installation/docker.md) for the fastest way to get started and then pick one of the other, more flexible installation methods to build an easier to scale setup. +Erxes can be installed in many different ways so you can pick whatever works best for you. + +We recommend to start with the [docker images](installation/docker.md) for the fastest way to get started and then pick one of the other, more flexible installation methods to build an easier to scale setup. ### Choose an installation method @@ -15,8 +19,10 @@ Erxes can be installed in many different ways so you can pick whatever works bes - [Debian 10](installation/debian10.md) - [CentOS 8](installation/centos8.md) - [RHEL 8](installation/redhat8.md) -- [Heroku](installation/heroku.md) - [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 93b1844dd6d..185861f062a 100644 --- a/docs/docs/overview/getting-started.md +++ b/docs/docs/overview/getting-started.md @@ -20,19 +20,18 @@ 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 @@ -43,14 +42,15 @@ title: Getting Started - RHEL 8 - Docker - Heroku +- AWS Marketplace +- DigitalOcean Marketplace - Upgrade ### Administrator's guide - Creating first user - Environment Variables -- File Upload -- Integrations +- System configs - Migration ### Developer's guide @@ -58,9 +58,10 @@ title: Getting Started - GraphQL API - Coding standards - Contributing -- Push Notification +- Push Notification - Android SDK - iOS SDK -- Extend +- Troubleshooting +
diff --git a/docs/docs/overview/integrations-overview.md b/docs/docs/overview/integrations-overview.md index c7e6caeb52f..31187323c85 100644 --- a/docs/docs/overview/integrations-overview.md +++ b/docs/docs/overview/integrations-overview.md @@ -8,26 +8,40 @@ Avoid all the switching between apps like Twitter, Instagram and Facebook with o There are some integrations that we have developed. -* [Facebook integration guide](../administrator/integrations#facebook-integration) +* [Facebook integration guide](../administrator/system-config#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. -* [Twitter integration guide](../administrator/integrations#twitter-integration) +* [Twitter integration guide](../administrator/system-config#twitter) 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/integrations#gmail-integration) +* [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. -* [AWS S3 integration](../administrator/integrations#aws-s3-integration) +* [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. -* [AWS SES integration](../administrator/integrations#aws-ses-integration) +* [AWS SES integration](../administrator/system-config#aws-ses) 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/integrations#nylas-integration) +* [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 Whatsapp accounts messages directly into our erxes app's inbox. + +* [Sunshine Conversation API integration guide](../administrator/system-config#sunshine-conversations-api-integration) + +Erxes app can be integrated with Sunshine Conversation API and that means we can receive Viber, Telegram, Line, Twilio accounts messages directly into our erxes app's inbox. + diff --git a/docs/docs/overview/overview.md b/docs/docs/overview/overview.md index 89fc12a2d88..9f553f22d04 100644 --- a/docs/docs/overview/overview.md +++ b/docs/docs/overview/overview.md @@ -3,7 +3,7 @@ id: overview title: Overview --- -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. +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. We offers an all-in-one messaging platform for teams and individuals to take care of their customers, from leads, to engagement, and support. And the best part is that erxes is an Open (Source) platform. diff --git a/docs/docs/user/contacts.md b/docs/docs/user/contacts.md index 838f5b470d6..5c17371a243 100644 --- a/docs/docs/user/contacts.md +++ b/docs/docs/user/contacts.md @@ -4,7 +4,7 @@ title: Contacts --- -Coordinate and manage your companies and customers in one go from company database. It enables you to filter companies and customers by websites, size, plan industry, session count in an more realistic way +Coordinate and manage your companies and customers in one go from company database. It enables you to filter companies and customers by websites, size, plan industry, session count in an more realistic way. --- @@ -141,48 +141,4 @@ You can view full contact details by clicking on the company from company’s fe
-
- - ---- - -## Setup segment -Segment is a customer data management and analytics solution that helps you make sense of customer and company data coming from multiple various sources. - -+ Please follow the steps for setup: Certain Features ->Segment > Add Segment - -1. Click on top right corner of segment -2. Click New segment - -
- -
- - - ---- - -## Create segment -Segment is a customer data management and analytics solution that helps you make sense of customer and company data coming from multiple various sources. - -+ Please follow the steps for setup: Certain Features ->Segment > Add Segment - -
- -
- -1. Add condition for the Segment -2. Click Add Condition -3. Insert your condition “30 online customers” -4. Insert Name for the Segment -5. Insert Description for the Segment -6. Choose Sub Segment -7. Choose Color for the Segment -8. Click Save to create Segment -9. The numbers of customers equals to created segment - - \ No newline at end of file + \ No newline at end of file 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/engage.md b/docs/docs/user/engage.md index 15cb82ab7b2..beda55b0415 100644 --- a/docs/docs/user/engage.md +++ b/docs/docs/user/engage.md @@ -3,7 +3,7 @@ id: engage title: Engage --- -Start converting your prospects into potential customers through email, SMS, messenger or more interactions to drive them to a successful close. +Start converting your prospects into potential customers through Email, SMS, Messenger or more interactions to drive them to a successful close. - Please follow the steps for setup: **Engage** diff --git a/docs/docs/user/general-settings.md b/docs/docs/user/general-settings.md index 059c310ffda..1157de548b7 100644 --- a/docs/docs/user/general-settings.md +++ b/docs/docs/user/general-settings.md @@ -5,6 +5,8 @@ title: General Settings +This document details how the system configuration is, what features it includes, how to configure an account, etc. + ## General Settings - General settings include following features: @@ -239,3 +241,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/initial-setup.md b/docs/docs/user/initial-setup.md index c24bc0a383b..b8671c69230 100644 --- a/docs/docs/user/initial-setup.md +++ b/docs/docs/user/initial-setup.md @@ -3,7 +3,7 @@ id: initial-setup title: Initial Setup --- -Brands, Channels and Integrations are the key components of erxes platform +Brands, Channels and Integrations are the key components of erxes platform. --- diff --git a/docs/docs/user/knowledge-base.md b/docs/docs/user/knowledge-base.md index 8ec3e97a3ef..7cb6b024f18 100644 --- a/docs/docs/user/knowledge-base.md +++ b/docs/docs/user/knowledge-base.md @@ -7,9 +7,7 @@ sidebar_label: Knowledge Base -Real-time & two-way help center - -Educate both your customers and staff by creating a help center related to your brands, products and services to reach higher level of satisfactions +Real-time & two-way help center. Educate both your customers and staff by creating a help center related to your brands, products and services to reach higher level of satisfactions. @@ -90,7 +88,7 @@ Knowledge Base is a part of your widget section where you address common concern diff --git a/docs/docs/user/mobile-apps.md b/docs/docs/user/mobile-apps.md index 58585f7d36f..33783639a4f 100644 --- a/docs/docs/user/mobile-apps.md +++ b/docs/docs/user/mobile-apps.md @@ -4,6 +4,8 @@ title: Mobile Apps sidebar_label: Mobile Apps --- +This document details how is the configuration of the system in mobile equipment according to the operating system. + ## iOS Setup diff --git a/docs/docs/user/notification.md b/docs/docs/user/notification.md index 7432619e48b..043d3c5d59a 100644 --- a/docs/docs/user/notification.md +++ b/docs/docs/user/notification.md @@ -5,7 +5,7 @@ title: Notifications -Notifications in Erxes alert you to channels and conversations events +Notifications in Erxes alert you to channels and conversations events. + Please follow the steps for : Click on the top right corner of the page diff --git a/docs/docs/user/popups.md b/docs/docs/user/popups.md index a279e5bbd29..3cc70cb377c 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,9 +131,9 @@ Turn regular visitors into qualified leads by capturing them with a customizable diff --git a/docs/docs/user/sales-pipeline-settings.md b/docs/docs/user/sales-pipeline-settings.md deleted file mode 100644 index fb5d1ada577..00000000000 --- a/docs/docs/user/sales-pipeline-settings.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -id: sales-pipeline-settings -title: 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/script-install.md b/docs/docs/user/script-install.md index de2391391e0..3bccc8b4e34 100644 --- a/docs/docs/user/script-install.md +++ b/docs/docs/user/script-install.md @@ -589,6 +589,102 @@ bio:””, +### Manipulate your messenger function + +A messenger can be launched programmatically on some user interaction, "like clicking on request for help button". Rather than attaching to document, there should be an option to attach the erxes launcher to a specific element, as it causes the problem in single-page apps since it is not ideal to display the launcher icon in every page.You are now able to manipulate your messenger functions such as change the appearance of a messenger style, a position as well as you can set up the button on your website to call the messenger on specified page. + +#### Button submit +Ability to call submit from outside (parent website), which means listen for callSubmit action from outside to force submit action. For example, you can add any button to call action to open your messenger. There is a window.Erxes.showMessenger() function available on the window object. You can use this function to show messenger programmatically. Insert the following script inside your messenger script code. + +``` + document.getElementById('button').onclick = () => { + window.Erxes.showMessenger() + } +``` + +First you need uncheck show launcher check to hide default handler. When you check show launcher, the widget section will invisible but you can still callSubmit action to get messenger. + + + +Insert the following script inside your messenger script code. + + + +Once you click submit button on your website, messenger will open. + + + + + + +#### Messenger position + +As part of the support system inside the web, it is always difficult to fix the position of launcher icon to the bottom, or right left position, especially on mobile, since it overlaps with other elements, especially bottom navbar. + +You can manipulate the position of the messenger body like following. The messenger is show left side of your website. +``` + +``` + + + + +#### CSS style +Ability to change form css from parent. In some cases, the developer wants to hide form title, button or modify some auto-generated CSS. Refer the following example of css file. + +``` +.erxes-embed-iframe { +margin-top: 100px !important; +margin-bottom: 100px !important; +height: 500px !important; +} + +#erxes-messenger-container { +left: 0; +} + +#erxes-messenger-iframe { +left: 24px; +} + +#erxes-messenger-container:after { +left: -300px; +transform: scaleX(-1); +} + +#erxes-messenger-container.erxes-messenger-shown:after { +left: -20px; +} + +``` +The messenger position is now left side. + ## Advanced combination installation @@ -746,7 +842,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). @@ -788,7 +884,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). @@ -826,7 +922,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). @@ -1172,7 +1268,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). @@ -1243,7 +1339,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). @@ -1297,7 +1393,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). @@ -1415,14 +1511,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 @@ -1444,7 +1540,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. @@ -1452,7 +1548,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 @@ -1466,7 +1562,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). @@ -1522,7 +1618,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). diff --git a/docs/docs/user/segments.md b/docs/docs/user/segments.md new file mode 100644 index 00000000000..5ceba1e2b50 --- /dev/null +++ b/docs/docs/user/segments.md @@ -0,0 +1,107 @@ +--- +id: segments +title: Segments +--- + + +Coordinate and manage your companies and customers in one go from company database. It enables you to filter companies and customers by websites, size, plan industry, session count in an more realistic way. + +--- + +## Setup segment +Segment is a customer data management and analytics solution that helps you make sense of customer and company data coming from multiple sources. + ++ Please follow the steps for setup: Certain Features ->Segment > Add Segment + +1. Click on top right corner of segment +2. Click New segment + +
+ +
+ + + +--- + +## Create segment +Segment is a customer data management and analytics solution that helps you make sense of customer and company data coming from multiple sources. + ++ Please follow the steps for setup: Certain Features ->Segment > Add Segment + +
+ +
+ +1. Add condition for the Segment +2. Click Add Condition +3. Insert your condition “30 online customers” +4. Insert Name for the Segment +5. Insert Description for the Segment +6. Choose Sub Segment +7. Choose Color for the Segment +8. Click Save to create Segment +9. The numbers of customers equals to created segment + + + +--- + +## Create event + +#### Copy script +``` + + + +``` + +#### Register events +``` + + + + + +``` + +#### Create segments using registered events +
+ +
+ + +#### Update customer properties +``` + + + + + +``` + +#### Check registered attribute +
+ +
\ No newline at end of file diff --git a/docs/docs/user/signing-in.md b/docs/docs/user/signing-in.md deleted file mode 100644 index 423855faca6..00000000000 --- a/docs/docs/user/signing-in.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: signing-in -title: Signing in ---- - - - -To sign in, navigate to the erxes sign-in page.
-If you don't have account? Create a new one from https://erxes.io/. - - - -## Email Address or Username Sign In - -When enabled by your System Admin, you can sign in with the username or email address used to create your account. - -
- - -
-
- -1. Insert an __email address__ or __username__ -2. Insert __password__ -3. Click __Sign in__ button to sign in -4. If you have forgotten your password, you can reset it by clicking __Forgot password?__ -5. Insert an __email address__ that used to create your account. -6. By clicking __Email me the instruction__, we sent an email reset password link to your email address. - ---- - - -## 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/subscription-getting-started.md b/docs/docs/user/subscription-getting-started.md index 81e46824aef..4a68b87d9e8 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
@@ -17,8 +19,6 @@ title: Subscription getting started 3. Insert **confirmation code** sent to the email address 4. Click **confirm** ---- -
@@ -28,6 +28,30 @@ title: Subscription getting started 2. Click **Create** to confirm 3. Click for **Initial Setup** +--- +## Signing in + +To sign in, navigate to the erxes sign-in page.
+If you don't have account? Create a new one from https://erxes.io/. + + + +### Email Address or Username Sign In + +When enabled by your System Admin, you can sign in with the username or email address used to create your account. + +
+ + +
+
+ +1. Insert an __email address__ or __username__ +2. Insert __password__ +3. Click __Sign in__ button to sign in +4. If you have forgotten your password, you can reset it by clicking __Forgot password?__ +5. Insert an __email address__ that used to create your account. +6. By clicking __Email me the instruction__, we sent an email reset password link to your email address. --- ## Initial Setup @@ -116,7 +140,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** @@ -136,7 +160,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** @@ -144,3 +168,16 @@ Create your Password 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/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/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/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 diff --git a/docs/website/sidebars.json b/docs/website/sidebars.json index 07bf77ceb15..3f19fb0c9c7 100644 --- a/docs/website/sidebars.json +++ b/docs/website/sidebars.json @@ -14,9 +14,12 @@ "installation/redhat8", "installation/docker", "installation/heroku", + "installation/aws", + "installation/digitalocean", "installation/upgrade" ], "User's Guide": [ + "user/subscription-getting-started", "user/initial-setup", "user/general-settings", "user/team-inbox", @@ -24,23 +27,18 @@ "user/popups", "user/script-install", "user/contacts", + "user/segments", "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", "administrator/environment-variables", - "administrator/file-upload", - "administrator/integrations", + "administrator/system-config", "administrator/migration" ], "Developer's guide": [ @@ -49,7 +47,8 @@ "developer/graphql-api", "developer/push-notifications", "developer/android-sdk", - "developer/ios-sdk" + "developer/ios-sdk", + "developer/troubleshooting" ] } } 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 }, ], diff --git a/package.json b/package.json new file mode 100644 index 00000000000..103b0ea93ee --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "erxes", + "version": "0.13.0", + "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", + "bugs": "https://github.com/erxes/erxes/issues", + "keywords": [ + "node", + "graphql", + "apollo", + "react" + ], + "license": "MIT", + "private": true, + "collective": { + "type": "opencollective", + "url": "https://opencollective.com/erxes", + "logo": "https://opencollective.com/opencollective/logo.txt" + }, + "scripts": { + "format": "prettier --single-quote --write 'src/**/*.@(ts|tsx)'", + "precommit": "lint-staged", + "lint": "tslint '@(ui|widgets)/**/*.@(ts|tsx)'", + "postinstall": "opencollective postinstall", + "generateVersion": "node commands/generateVersion.js", + "release": "release-it" + }, + "devDependencies": { + "@release-it/conventional-changelog": "^1.1.0", + "opencollective": "^1.0.3" + }, + "dependencies": {} +} diff --git a/scripts/install/centos8.sh b/scripts/install/centos8.sh index b20202997cc..ba8484701bd 100644 --- a/scripts/install/centos8.sh +++ b/scripts/install/centos8.sh @@ -27,18 +27,18 @@ done # Dependencies # yum -qqy update -yum -qqy install -y wget gnupg +yum -qqy install -y wget gnupg python3 python3-pip # MongoDB -cat </etc/yum.repos.d/mongodb-org-4.2.repo -[mongodb-org-4.2] +cat </etc/yum.repos.d/mongodb-org-3.6.repo +[mongodb-org-3.6] name=MongoDB Repository -baseurl=https://repo.mongodb.org/yum/redhat/\$releasever/mongodb-org/4.2/x86_64/ +baseurl=https://repo.mongodb.org/yum/redhat/8/mongodb-org/3.6/x86_64/ gpgcheck=1 enabled=1 -gpgkey=https://www.mongodb.org/static/pgp/server-4.2.asc +gpgkey=https://www.mongodb.org/static/pgp/server-3.6.asc EOF -yum -qqy install -y mongodb-org +yum -qqy install mongodb-org systemctl enable mongod systemctl start mongod @@ -93,6 +93,27 @@ systemctl enable rabbitmq-server rabbitmq-plugins enable rabbitmq_management systemctl start rabbitmq-server +# Java +yum -qqy install java-11-openjdk -y + +# Elasticsearch +rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch +cat </dev/null || useradd -m -s /bin/bash -U -G wheel $username cd /home/$username -erxes_dir=/home/$username/erxes +# erxes repo +erxes_root_dir=/home/$username/erxes +erxes_dir=$erxes_root_dir/ui +erxes_widgets_dir=$erxes_root_dir/widgets + +# erxes-api repo erxes_api_dir=/home/$username/erxes-api -erxes_widgets_dir=/home/$username/erxes-widgets -erxes_engages_dir=/home/$username/erxes-engages -erxes_logger_dir=/home/$username/erxes-logger +erxes_engages_dir=$erxes_api_dir/engages-email-sender +erxes_logger_dir=$erxes_api_dir/logger +erxes_syncer_dir=$erxes_api_dir/elkSyncer +#erxes_email_verifier_dir=$erxes_api_dir/email-verifier + +# erxes-integrations repo erxes_integrations_dir=/home/$username/erxes-integrations -su $username -c "mkdir -p $erxes_dir $erxes_api_dir $erxes_widgets_dir $erxes_engages_dir $erxes_logger_dir $erxes_integrations_dir" +su $username -c "mkdir -p $erxes_dir $erxes_api_dir $erxes_integrations_dir" # download erxes -su $username -c "curl -L https://github.com/erxes/erxes/archive/0.12.0.tar.gz | tar --strip-components=1 -xz -C $erxes_dir" +su $username -c "curl -L https://github.com/erxes/erxes/archive/0.13.0.tar.gz | tar --strip-components=1 -xz -C $erxes_root_dir" # download erxes-api -su $username -c "curl -L https://github.com/erxes/erxes-api/archive/0.12.3.tar.gz | tar --strip-components=1 -xz -C $erxes_api_dir" - -# download erxes-widgets -su $username -c "curl -L https://github.com/erxes/erxes-widgets/archive/0.12.0.tar.gz | tar --strip-components=1 -xz -C $erxes_widgets_dir" - -# download engages email sender -su $username -c "curl -L https://github.com/erxes/erxes-engages-email-sender/archive/0.12.0.tar.gz | tar --strip-components=1 -xz -C $erxes_engages_dir" - -# download logger -su $username -c "curl -L https://github.com/erxes/erxes-logger/archive/0.12.0.tar.gz | tar --strip-components=1 -xz -C $erxes_logger_dir" +su $username -c "curl -L https://github.com/erxes/erxes-api/archive/0.13.0.tar.gz | tar --strip-components=1 -xz -C $erxes_api_dir" # download integrations -su $username -c "curl -L https://github.com/erxes/erxes-integrations/archive/0.12.0.tar.gz | tar --strip-components=1 -xz -C $erxes_integrations_dir" +su $username -c "curl -L https://github.com/erxes/erxes-integrations/archive/0.13.0.tar.gz | tar --strip-components=1 -xz -C $erxes_integrations_dir" # install packages and build erxes su $username -c "cd $erxes_dir && yarn install && yarn build" -# install erxes api packages -su $username -c "cd $erxes_api_dir && yarn install && yarn build" - # install erxes widgets packages su $username -c "cd $erxes_widgets_dir && yarn install && yarn build" +# install erxes api packages +su $username -c "cd $erxes_api_dir && yarn install && yarn build" + # install erxes engages packages su $username -c "cd $erxes_engages_dir && yarn install && yarn build" # install erxes logger packages su $username -c "cd $erxes_logger_dir && yarn install && yarn build" +# install erxes email verifier packages +# su $username -c "cd $erxes_email_verifier_dir && yarn install && yarn build" + # install erxes integrations packages su $username -c "cd $erxes_integrations_dir && yarn install && yarn build" @@ -171,7 +194,7 @@ cat </home/$username/ecosystem.json "apps": [ { "name": "erxes-api", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { @@ -187,46 +210,45 @@ cat </home/$username/ecosystem.json "WORKERS_API_DOMAIN": "http://127.0.0.1:3700", "LOGS_API_DOMAIN": "http://127.0.0.1:3800", "ENGAGES_API_DOMAIN": "http://127.0.0.1:3900", - "MONGO_URL": "mongodb://localhost/erxes", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", "REDIS_HOST": "localhost", "REDIS_PORT": 6379, "REDIS_PASSWORD": "", "RABBITMQ_HOST": "amqp://localhost", "PORT_CRONS": 3600, "PORT_WORKERS": 3700, - "JWT_TOKEN_SECRET": "$JWT_TOKEN_SECRET" + "JWT_TOKEN_SECRET": "$JWT_TOKEN_SECRET", + "ELASTICSEARCH_URL": "http://localhost:9200" } }, { "name": "erxes-api-cronjob", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist/cronJobs", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT_CRONS": 3600, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", + "RABBITMQ_HOST": "amqp://localhost", "DEBUG": "erxes-crons:*" } }, { "name": "erxes-api-worker", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist/workers", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT_WORKERS": 3700, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes", - "REDIS_HOST": "localhost", - "REDIS_PORT": 6379, - "REDIS_PASSWORD": "", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", "DEBUG": "erxes-workers:*" } }, { "name": "erxes-widgets", - "cwd": "erxes-widgets", + "cwd": "$erxes_widgets_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { @@ -234,48 +256,55 @@ cat </home/$username/ecosystem.json "NODE_ENV": "production", "ROOT_URL": "http://$erxes_domain/widgets", "API_URL": "http://$erxes_domain/api", - "API_SUBSCRIPTIONS_URL": "ws://$erxes_domain/api/subscriptions" + "API_SUBSCRIPTIONS_URL": "ws://$erxes_domain/api/subscriptions?replicaSet=rs0" } }, { "name": "erxes-engages", - "cwd": "erxes-engages", + "cwd": "$erxes_engages_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3900, "NODE_ENV": "production", "MAIN_API_DOMAIN": "http://$erxes_domain/api", - "MONGO_URL": "mongodb://localhost/erxes-engages", + "MONGO_URL": "mongodb://localhost/erxes-engages?replicaSet=rs0", "RABBITMQ_HOST": "amqp://localhost", + "REDIS_HOST": "localhost", + "REDIS_PORT": 6379, + "REDIS_PASSWORD": "", "DEBUG": "erxes-engages:*" } }, { "name": "erxes-logger", - "cwd": "erxes-logger", + "cwd": "$erxes_logger_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3800, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes_logs", + "MONGO_URL": "mongodb://localhost/erxes_logs?replicaSet=rs0", + "RABBITMQ_HOST": "amqp://localhost", "DEBUG_PREFIX": "erxes-logs" } }, { "name": "erxes-integrations", - "cwd": "erxes-integrations", + "cwd": "$erxes_integrations_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3400, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes_integrations", + "MONGO_URL": "mongodb://localhost/erxes_integrations?replicaSet=rs0", "DOMAIN": "http://$erxes_domain/integrations", "MAIN_APP_DOMAIN": "http://$erxes_domain", "MAIN_API_DOMAIN": "http://$erxes_domain/api", - "RABBITMQ_HOST": "amqp://localhost" + "RABBITMQ_HOST": "amqp://localhost", + "REDIS_HOST": "localhost", + "REDIS_PORT": 6379, + "REDIS_PASSWORD": "" } } ] @@ -285,6 +314,41 @@ EOF chown $username:$username /home/$username/ecosystem.json chmod 644 /home/$username/ecosystem.json + +# set up mongod ReplicaSet +systemctl stop mongod +mv /etc/mongod.conf /etc/mongod.conf.bak +cat</etc/mongod.conf +storage: + dbPath: /var/lib/mongo + journal: + enabled: true +systemLog: + destination: file + logAppend: true + path: /var/log/mongodb/mongod.log +net: + bindIp: localhost +processManagement: + fork: true # fork and run in background + pidFilePath: /var/run/mongodb/mongod.pid + timeZoneInfo: /usr/share/zoneinfo +replication: + replSetName: "rs0" +EOF +systemctl start mongod +curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /usr/local/bin/wait-for-it.sh +chmod +x /usr/local/bin/wait-for-it.sh +/usr/local/bin/wait-for-it.sh --timeout=0 localhost:27017 +while true; do + healt=$(mongo --eval "rs.initiate().ok" --quiet) + if [ $healt -eq 0 ]; then + break + fi +done +echo "Started MongoDB ReplicaSet successfully" + + # generate env.js cat <$erxes_dir/build/js/env.js window.env = { @@ -298,9 +362,6 @@ EOF chown $username:$username $erxes_dir/build/js/env.js chmod 664 $erxes_dir/build/js/env.js -# erxes api load initial data -su $username -c "cd $erxes_api_dir && yarn loadInitialData && yarn loadPermission" - # make pm2 starts on boot pm2 startup -u $username --hp /home/$username systemctl enable pm2-$username @@ -308,6 +369,41 @@ systemctl enable pm2-$username # start erxes pm2 and save current processes su $username -c "cd /home/$username && pm2 start ecosystem.json && pm2 save" +# pip3 packages for elkSyncer +pip3 install mongo-connector==3.1.1 \ + && pip3 install elasticsearch==7.5.1 \ + && pip3 install elastic2-doc-manager==1.0.0 \ + && pip3 install python-dotenv==0.11.0 + + +# elkSyncer env +cat <$erxes_syncer_dir/.env +MONGO_URL=mongodb://localhost/erxes?replicaSet=rs0 +ELASTICSEARCH_URL=http://localhost:9200 +EOF + +cat </lib/systemd/system/erxes-api-elk-syncer.service +[Unit] +Description=erxes-api-elk-syncer +Documentation=https://docs.erxes.io +After=network.target + +[Service] +WorkingDirectory=$erxes_syncer_dir +ExecStart=/usr/bin/python3 $erxes_syncer_dir/main.py +ExecStop=/bin/kill -INT $MAINPID +ExecReload=/bin/kill -TERM $MAINPID +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target +EOF +chmod 644 /lib/systemd/system/erxes-api-elk-syncer.service +systemctl daemon-reload +systemctl enable erxes-api-elk-syncer.service +systemctl start erxes-api-elk-syncer.service + # Nginx erxes config cat </etc/nginx/conf.d/erxes.conf @@ -322,7 +418,7 @@ server { error_log /var/log/nginx/erxes.error.log; location / { - root /home/erxes/erxes/build; + root $erxes_dir/build; index index.html; error_log /var/log/nginx/erxes.error.log; @@ -368,7 +464,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 diff --git a/scripts/install/debian10.sh b/scripts/install/debian10.sh index 098f16180c4..2ac9068b46d 100644 --- a/scripts/install/debian10.sh +++ b/scripts/install/debian10.sh @@ -27,22 +27,27 @@ done # Dependencies # apt-get -qqy update -apt-get -qqy install -y curl wget gnupg apt-transport-https +apt-get -qqy install -y curl wget gnupg apt-transport-https software-properties-common python3-pip # MongoDB -wget -qO - https://www.mongodb.org/static/pgp/server-4.2.asc | apt-key add - -echo "deb http://repo.mongodb.org/apt/debian buster/mongodb-org/4.2 main" | tee /etc/apt/sources.list.d/mongodb-org-4.2.list +echo "Installing MongoDB" +wget -qO - https://www.mongodb.org/static/pgp/server-3.6.asc | apt-key add - +echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list apt-get -qqy update -apt-get -qqy install -y mongodb-org +apt-get install -y mongodb-org +echo "Installed MongoDB successfully" systemctl enable mongod systemctl start mongod # Redis server +echo "Installing Redis" apt -qqy install -y redis-server systemctl enable redis-server systemctl start redis-server +echo "Installed Redis successfully" # RabbitMQ +echo "Installing RabbitMQ" curl -fsSL https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc | apt-key add - tee /etc/apt/sources.list.d/bintray.rabbitmq.list </home/$username/ecosystem.json { "apps": [ { "name": "erxes-api", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { @@ -156,46 +181,45 @@ cat </home/$username/ecosystem.json "WORKERS_API_DOMAIN": "http://127.0.0.1:3700", "LOGS_API_DOMAIN": "http://127.0.0.1:3800", "ENGAGES_API_DOMAIN": "http://127.0.0.1:3900", - "MONGO_URL": "mongodb://localhost/erxes", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", "REDIS_HOST": "localhost", "REDIS_PORT": 6379, "REDIS_PASSWORD": "", "RABBITMQ_HOST": "amqp://localhost", "PORT_CRONS": 3600, "PORT_WORKERS": 3700, - "JWT_TOKEN_SECRET": "$JWT_TOKEN_SECRET" + "JWT_TOKEN_SECRET": "$JWT_TOKEN_SECRET", + "ELASTICSEARCH_URL": "http://localhost:9200" } }, { "name": "erxes-api-cronjob", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist/cronJobs", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT_CRONS": 3600, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", + "RABBITMQ_HOST": "amqp://localhost", "DEBUG": "erxes-crons:*" } }, { "name": "erxes-api-worker", - "cwd": "erxes-api", + "cwd": "$erxes_api_dir", "script": "dist/workers", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT_WORKERS": 3700, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes", - "REDIS_HOST": "localhost", - "REDIS_PORT": 6379, - "REDIS_PASSWORD": "", + "MONGO_URL": "mongodb://localhost/erxes?replicaSet=rs0", "DEBUG": "erxes-workers:*" } }, { "name": "erxes-widgets", - "cwd": "erxes-widgets", + "cwd": "$erxes_widgets_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { @@ -208,43 +232,50 @@ cat </home/$username/ecosystem.json }, { "name": "erxes-engages", - "cwd": "erxes-engages", + "cwd": "$erxes_engages_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3900, "NODE_ENV": "production", "MAIN_API_DOMAIN": "http://$erxes_domain/api", - "MONGO_URL": "mongodb://localhost/erxes-engages", + "MONGO_URL": "mongodb://localhost/erxes-engages?replicaSet=rs0", "RABBITMQ_HOST": "amqp://localhost", + "REDIS_HOST": "localhost", + "REDIS_PORT": 6379, + "REDIS_PASSWORD": "", "DEBUG": "erxes-engages:*" } }, { "name": "erxes-logger", - "cwd": "erxes-logger", + "cwd": "$erxes_logger_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3800, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes_logs", + "MONGO_URL": "mongodb://localhost/erxes_logs?replicaSet=rs0", + "RABBITMQ_HOST": "amqp://localhost", "DEBUG_PREFIX": "erxes-logs" } }, { "name": "erxes-integrations", - "cwd": "erxes-integrations", + "cwd": "$erxes_integrations_dir", "script": "dist", "log_date_format": "YYYY-MM-DD HH:mm Z", "env": { "PORT": 3400, "NODE_ENV": "production", - "MONGO_URL": "mongodb://localhost/erxes_integrations", + "MONGO_URL": "mongodb://localhost/erxes_integrations?replicaSet=rs0", "DOMAIN": "http://$erxes_domain/integrations", "MAIN_APP_DOMAIN": "http://$erxes_domain", "MAIN_API_DOMAIN": "http://$erxes_domain/api", - "RABBITMQ_HOST": "amqp://localhost" + "RABBITMQ_HOST": "amqp://localhost", + "REDIS_HOST": "localhost", + "REDIS_PORT": 6379, + "REDIS_PASSWORD": "" } } ] @@ -254,6 +285,39 @@ EOF chown $username:$username /home/$username/ecosystem.json chmod 644 /home/$username/ecosystem.json + +# set up mongod ReplicaSet +systemctl stop mongod +mv /etc/mongod.conf /etc/mongod.conf.bak +cat</etc/mongod.conf +storage: + dbPath: /var/lib/mongodb + journal: + enabled: true +systemLog: + destination: file + logAppend: true + path: /var/log/mongodb/mongod.log +net: + bindIp: localhost,$(hostname),$(hostname -I | sed 's/ /,/') +processManagement: + timeZoneInfo: /usr/share/zoneinfo +replication: + replSetName: "rs0" +EOF +systemctl start mongod +curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > /usr/local/bin/wait-for-it.sh +chmod +x /usr/local/bin/wait-for-it.sh +/usr/local/bin/wait-for-it.sh --timeout=0 localhost:27017 +while true; do + healt=$(mongo --eval "rs.initiate().ok" --quiet) + if [ $healt -eq 0 ]; then + break + fi +done +echo "Started MongoDB ReplicaSet successfully" + + # generate env.js cat <$erxes_dir/build/js/env.js window.env = { @@ -267,9 +331,6 @@ EOF chown $username:$username $erxes_dir/build/js/env.js chmod 664 $erxes_dir/build/js/env.js -# erxes api load initial data -su $username -c "cd $erxes_api_dir && yarn loadInitialData && yarn loadPermission" - # make pm2 starts on boot pm2 startup -u $username --hp /home/$username systemctl enable pm2-$username @@ -277,6 +338,41 @@ systemctl enable pm2-$username # start erxes pm2 and save current processes su $username -c "cd /home/$username && pm2 start ecosystem.json && pm2 save" +# pip3 packages for elkSyncer +pip3 install mongo-connector==3.1.1 \ + && pip3 install elasticsearch==7.5.1 \ + && pip3 install elastic2-doc-manager==1.0.0 \ + && pip3 install python-dotenv==0.11.0 + + +# elkSyncer env +cat <$erxes_syncer_dir/.env +MONGO_URL=mongodb://localhost/erxes?replicaSet=rs0 +ELASTICSEARCH_URL=http://localhost:9200 +EOF + +cat </lib/systemd/system/erxes-api-elk-syncer.service +[Unit] +Description=erxes-api-elk-syncer +Documentation=https://docs.erxes.io +After=network.target + +[Service] +WorkingDirectory=$erxes_syncer_dir +ExecStart=/usr/bin/python3 $erxes_syncer_dir/main.py +ExecStop=/bin/kill -INT $MAINPID +ExecReload=/bin/kill -TERM $MAINPID +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target +EOF +chmod 644 /lib/systemd/system/erxes-api-elk-syncer.service +systemctl daemon-reload +systemctl enable erxes-api-elk-syncer.service +systemctl start erxes-api-elk-syncer.service + # Nginx erxes config cat </etc/nginx/sites-available/default @@ -291,7 +387,7 @@ server { error_log /var/log/nginx/erxes.error.log; location / { - root /home/erxes/erxes/build; + root $erxes_dir/build; index index.html; error_log /var/log/nginx/erxes.error.log; 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/.gitignore b/ui/.gitignore deleted file mode 100644 index ac0b08c215a..00000000000 --- a/ui/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# globals -*.un~ -*.swp -*.swo - -# dependencies -/node_modules - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env -.env.local -.env.development.local -.env.development -.env.test.local -.env.production.local -package-lock.json - -version.json - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -dump.rdb 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: diff --git a/ui/package.json b/ui/package.json index 5ef8f6afa8c..18fd4da480c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,19 +1,5 @@ { "name": "erxes", - "version": "0.12.0", - "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", - "bugs": "https://github.com/erxes/erxes/issues", - "keywords": [ - "node", - "graphql", - "apollo", - "mongodb", - "react", - "create-react-app" - ], - "license": "MIT", "private": true, "dependencies": { "@daily-co/daily-js": "^0.9.97", @@ -44,25 +30,23 @@ "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", - "history": "4.7.2", "i18n-react": "^0.6.4", "juice": "^5.2.0", "lodash.flowright": "^3.5.0", "query-string": "^5.0.0", "react": "^16.11.0", "react-apollo": "^3.1.3", - "react-beautiful-dnd": "^10.1.0", + "react-beautiful-dnd": "^13.0.0", "react-bootstrap": "^1.0.0-beta.14", "react-color": "^2.17.3", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.11.0", "react-markdown": "^3.3.0", - "react-router": "^4.2.0", - "react-router-dom": "^4.2.2", + "react-router-dom": "^5.1.2", "react-select-plus": "^1.0.0-rc.5", "react-toggle": "^4.1.1", "react-transition-group": "^2.5.1", @@ -86,11 +70,6 @@ ] } }, - "collective": { - "type": "opencollective", - "url": "https://opencollective.com/erxes", - "logo": "https://opencollective.com/opencollective/logo.txt" - }, "scripts": { "format": "prettier --single-quote --write 'src/**/*.@(ts|tsx)'", "precommit": "lint-staged", @@ -100,15 +79,12 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "lint": "tslint 'src/**/*.@(ts|tsx)'", - "postinstall": "opencollective postinstall", "generateVersion": "node commands/generateVersion.js", - "release": "release-it", "componentTest": "jest --runInBand --forceExit --detectOpenHandles --coverage", "snyk-protect": "snyk protect", "prepare": "yarn run snyk-protect" }, "devDependencies": { - "@release-it/conventional-changelog": "^1.1.0", "@types/draft-js": "^0.10.24", "@types/enzyme": "^3.1.12", "@types/enzyme-adapter-react-16": "^1.0.2", @@ -116,8 +92,7 @@ "@types/node": "^10.9.4", "@types/react": "^16.4.13", "@types/react-dom": "^16.0.7", - "@types/react-router": "^4.0.30", - "@types/react-router-dom": "^4.3.0", + "@types/react-router-dom": "^5.1.3", "@types/styled-components": "^3.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", @@ -125,7 +100,6 @@ "git-repo-info": "^2.1.0", "husky": "^0.14.3", "lint-staged": "^4.3.0", - "opencollective": "^1.0.3", "prettier": "^1.8.2", "react-scripts": "^3.0.1", "react-test-renderer": "^16.3.2", @@ -147,4 +121,4 @@ "engines": { "node": "10.x.x" } -} \ No newline at end of file +} 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/integrations/telegram.png b/ui/public/images/integrations/telegram.png new file mode 100644 index 00000000000..893212c1bfe Binary files /dev/null and b/ui/public/images/integrations/telegram.png differ diff --git a/ui/public/images/reactions.png b/ui/public/images/reactions.png deleted file mode 100644 index e0de78cf766..00000000000 Binary files a/ui/public/images/reactions.png and /dev/null differ 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 72f8010dc8b..00000000000 Binary files a/ui/public/images/sandbox-banner-send-statistics.png and /dev/null differ diff --git a/ui/public/index.html b/ui/public/index.html index 08c2c49905b..0fa838b6357 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -30,6 +30,33 @@ You need to enable JavaScript to run this app. + + %REACT_APP_API_URL%
diff --git a/ui/src/__tests__/common/components/ModifiableSelect.test.tsx b/ui/src/__tests__/common/components/ModifiableSelect.test.tsx index 998bb516cd9..19a92e699f0 100644 --- a/ui/src/__tests__/common/components/ModifiableSelect.test.tsx +++ b/ui/src/__tests__/common/components/ModifiableSelect.test.tsx @@ -6,6 +6,7 @@ import ModifiableSelect from '../../../modules/common/components/ModifiableSelec describe('Testing ModifiableSelect component', () => { let select; const defaultProps = { + name: 'name', options: ['11111111', '22222222'], value: '80', placeholder: 'phone', 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/apolloClient.ts b/ui/src/apolloClient.ts index 44e71b36df8..815c94c74b2 100644 --- a/ui/src/apolloClient.ts +++ b/ui/src/apolloClient.ts @@ -5,21 +5,18 @@ import { onError } from 'apollo-link-error'; import { createHttpLink } from 'apollo-link-http'; import { WebSocketLink } from 'apollo-link-ws'; import { getMainDefinition } from 'apollo-utilities'; -import { Alert } from 'modules/common/utils'; +import { Alert, getCookie } from 'modules/common/utils'; import { __ } from 'modules/common/utils'; // get env config from process.env or window.env -export const getEnv = () => { - const wenv = (window as any).env || {}; +export const getEnv = (): any => { + const envs = {}; - const getItem = name => wenv[name] || process.env[name]; + for (const envMap of (window as any).envMaps) { + envs[envMap.name] = getCookie(envMap.name); + } - return { - 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') - }; + return envs; }; const { REACT_APP_API_URL, REACT_APP_API_SUBSCRIPTION_URL } = getEnv(); diff --git a/ui/src/browserHistory.ts b/ui/src/browserHistory.ts deleted file mode 100644 index d71883efeaa..00000000000 --- a/ui/src/browserHistory.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createBrowserHistory } from 'history'; - -export default createBrowserHistory({}); diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index b52ae204190..474ae714cdb 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", @@ -798,6 +796,7 @@ "Get access to your Knowledge Base right in your Widget": "Get access to your Knowledge Base right in your Widget", "Soon you'll be able to connect Viber straight to your Team Inbox": "Soon you'll be able to connect Viber straight to your Team Inbox", "Get a hold of your Whatsapp messages through your Team Inbox": "Get a hold of your Whatsapp messages through your Team Inbox", + "Cloud-based mobile and desktop messaging app with a focus on speed and security": "Cloud-based mobile and desktop messaging app with a focus on speed and security", "Connect with Wechat and start messaging right from your Team Inbox": "Connect with Wechat and start messaging right from your Team Inbox", "Find feedback that has been gathered from various customer engagement channels.": "Find feedback that has been gathered from various customer engagement channels.", "A report on the total number of customer feedback responses given by team members.": "A report on the total number of customer feedback responses given by team members.", 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/locales/mn.json b/ui/src/locales/mn.json index 337e3a497ad..8c49f7f8791 100644 --- a/ui/src/locales/mn.json +++ b/ui/src/locales/mn.json @@ -345,7 +345,7 @@ "Knowledge base": "Мэдлэгийн сан", "Notification": "Мэдэгдэл", "Notifications": "Мэдэгдлүүд", - "Show Unread":"Уншаагүйг харах", + "Show Unread": "Уншаагүйг харах", "Unread": "Уншаагүйг харах", "Recent": "Жагсаалт харах", "Mark all as read": "Бүгдийг уншсан гэж тэмдэглэ", diff --git a/ui/src/modules/activityLogs/components/ActivityItem.tsx b/ui/src/modules/activityLogs/components/ActivityItem.tsx index d415b22d8fe..9f1c7d6303a 100644 --- a/ui/src/modules/activityLogs/components/ActivityItem.tsx +++ b/ui/src/modules/activityLogs/components/ActivityItem.tsx @@ -32,7 +32,7 @@ class ActivityItem extends React.Component { return ( - + @@ -66,12 +66,12 @@ class ActivityItem extends React.Component { case 'engage-email': return this.renderDetail( 'email', - + ); case 'email': return this.renderDetail( 'email', - + ); case 'comment': return this.renderDetail( diff --git a/ui/src/modules/activityLogs/components/items/ConvertLog.tsx b/ui/src/modules/activityLogs/components/items/ConvertLog.tsx index 3fd4ee632b6..20d6e55f435 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,9 +27,7 @@ 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 = ( 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..96917d22563 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; diff --git a/ui/src/modules/activityLogs/components/items/boardItems/AssigneeLog.tsx b/ui/src/modules/activityLogs/components/items/boardItems/AssigneeLog.tsx index 00edb0789fc..3b68d17d075 100644 --- a/ui/src/modules/activityLogs/components/items/boardItems/AssigneeLog.tsx +++ b/ui/src/modules/activityLogs/components/items/boardItems/AssigneeLog.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'; @@ -24,7 +25,7 @@ class AssigneeLog extends React.Component { const { content } = createdByDetail; if (content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; + userName = renderUserFullName(createdByDetail.content); } } @@ -34,7 +35,7 @@ class AssigneeLog extends React.Component { return (  {user.details.fullName || user.email}  @@ -46,7 +47,7 @@ class AssigneeLog extends React.Component { return (  {user.details.fullName || user.email}  diff --git a/ui/src/modules/activityLogs/components/items/boardItems/MovementLog.tsx b/ui/src/modules/activityLogs/components/items/boardItems/MovementLog.tsx index fb935f839a5..a74c6e5fe0a 100644 --- a/ui/src/modules/activityLogs/components/items/boardItems/MovementLog.tsx +++ b/ui/src/modules/activityLogs/components/items/boardItems/MovementLog.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'; @@ -24,7 +25,7 @@ class MovementLog extends React.Component { const { content } = createdByDetail; if (content.details) { - userName = createdByDetail.content.details.fullName || 'Unknown'; + userName = renderUserFullName(createdByDetail.content); } } 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..429f83b1d14 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 = ( @@ -34,9 +31,9 @@ class BoardItemCreate extends React.Component { to={`/${contentType}/board?_id=${activity._id}&itemId=${ contentTypeDetail._id }`} - target='_blank' + target="_blank" > - {contentTypeDetail.name} + {contentTypeDetail.name} ); 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/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/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/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/activityLogs/styles.ts b/ui/src/modules/activityLogs/styles.ts index f96e0d66327..6a8ca7e80ed 100644 --- a/ui/src/modules/activityLogs/styles.ts +++ b/ui/src/modules/activityLogs/styles.ts @@ -119,9 +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; @@ -133,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)` @@ -263,14 +263,14 @@ const IconWrapper = styledTS<{ isComplete?: boolean }>(styled.div)` > i { background: ${props => - props.isComplete ? colors.colorCoreGreen : colors.bgLight}; + props.isComplete ? colors.colorCoreGreen : colors.bgLight}; color: ${props => - props.isComplete ? colors.colorWhite : colors.colorShadowGray}; + props.isComplete ? colors.colorWhite : colors.colorShadowGray}; border-radius: 25px; display: inline-block; line-height: 25px; border: 2px solid ${props => - props.isComplete ? colors.colorCoreGreen : colors.colorShadowGray}; + props.isComplete ? colors.colorCoreGreen : colors.colorShadowGray}; transition: all ease 0.3s; } `; 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/auth/containers/ResetPassword.tsx b/ui/src/modules/auth/containers/ResetPassword.tsx index f3bd5c2186e..b48afc3f361 100755 --- a/ui/src/modules/auth/containers/ResetPassword.tsx +++ b/ui/src/modules/auth/containers/ResetPassword.tsx @@ -3,7 +3,7 @@ import * as compose from 'lodash.flowright'; import { Alert, withProps } from 'modules/common/utils'; import React from 'react'; import { graphql } from 'react-apollo'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import { IRouterProps } from '../../common/types'; import ResetPassword from '../components/ResetPassword'; import { mutations } from '../graphql'; diff --git a/ui/src/modules/auth/containers/SignIn.tsx b/ui/src/modules/auth/containers/SignIn.tsx index 590284a728f..03a80096ced 100755 --- a/ui/src/modules/auth/containers/SignIn.tsx +++ b/ui/src/modules/auth/containers/SignIn.tsx @@ -1,7 +1,7 @@ import apolloClient from 'apolloClient'; import { __ } from 'modules/common/utils'; import React from 'react'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import ButtonMutate from '../../common/components/ButtonMutate'; import { IButtonMutateProps, IRouterProps } from '../../common/types'; import SignIn from '../components/SignIn'; diff --git a/ui/src/modules/auth/containers/Unsubscribe.tsx b/ui/src/modules/auth/containers/Unsubscribe.tsx new file mode 100644 index 00000000000..85dbf3f6940 --- /dev/null +++ b/ui/src/modules/auth/containers/Unsubscribe.tsx @@ -0,0 +1,26 @@ +import { getEnv } from 'apolloClient'; + +const redirect = (name, value) => { + const { REACT_APP_API_URL } = getEnv(); + window.location.href = `${REACT_APP_API_URL}/unsubscribe?${name}=${value}`; +}; + +const Unsubscribe = props => { + const { queryParams } = props; + + if (queryParams) { + const { uid, cid } = queryParams; + + if (cid) { + redirect('cid', queryParams.cid); + } + + if (uid) { + redirect('uid', queryParams.uid); + } + } + + return null; +}; + +export default Unsubscribe; diff --git a/ui/src/modules/boards/components/Archive.tsx b/ui/src/modules/boards/components/Archive.tsx index b80fa2a3564..995cb8b31c6 100644 --- a/ui/src/modules/boards/components/Archive.tsx +++ b/ui/src/modules/boards/components/Archive.tsx @@ -7,47 +7,48 @@ import { ArchiveWrapper, TopBar } from '../styles/rightMenu'; import { IOptions } from '../types'; type Props = { - options: IOptions; - queryParams: any; + options: IOptions; + queryParams: any; }; function Archive(props: Props) { - const [ type, changeType ] = useState('item'); - const [ searchValue, onSearch ] = useState(''); - const { options, queryParams } = props; + const [type, changeType] = useState('item'); + const [searchValue, onSearch] = useState(''); + const { options, queryParams } = props; - const switchType = (): string => type === 'list' ? 'item' : 'list'; + const switchType = (): string => (type === 'list' ? 'item' : 'list'); - const toggleType = () => changeType(switchType()); + const toggleType = () => changeType(switchType()); - const onEnterSearch = (e: React.KeyboardEvent) => { + const onEnterSearch = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { const target = e.currentTarget as HTMLInputElement; - onSearch(target.value || ''); + onSearch(target.value || ''); } }; return ( - - - - {__('Switch To')} {switchType()}{'s'} - - - - - + + + + {__('Switch To')} {switchType()} + {'s'} + + + + + ); } 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/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/components/MainActionBar.tsx b/ui/src/modules/boards/components/MainActionBar.tsx index f067ba565b1..4482b44796e 100644 --- a/ui/src/modules/boards/components/MainActionBar.tsx +++ b/ui/src/modules/boards/components/MainActionBar.tsx @@ -8,7 +8,13 @@ import React from 'react'; import Dropdown from 'react-bootstrap/Dropdown'; import { Link } from 'react-router-dom'; import PipelineWatch from '../containers/PipelineWatch'; -import { HeaderButton, HeaderItems, HeaderLabel, HeaderLink, PageHeader } from '../styles/header'; +import { + HeaderButton, + HeaderItems, + HeaderLabel, + HeaderLink, + PageHeader +} from '../styles/header'; import { IBoard, IOptions, IPipeline } from '../types'; import RightMenu from './RightMenu'; @@ -204,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/RightMenu.tsx b/ui/src/modules/boards/components/RightMenu.tsx index 629603e91f3..edc718bfb8d 100644 --- a/ui/src/modules/boards/components/RightMenu.tsx +++ b/ui/src/modules/boards/components/RightMenu.tsx @@ -9,7 +9,13 @@ import React from 'react'; import Select from 'react-select-plus'; import RTG from 'react-transition-group'; import { PRIORITIES } from '../constants'; -import { FilterBox, FilterButton, MenuFooter, RightMenuContainer, TabContent } from '../styles/rightMenu'; +import { + FilterBox, + FilterButton, + MenuFooter, + RightMenuContainer, + TabContent +} from '../styles/rightMenu'; import { IOptions } from '../types'; import Archive from './Archive'; import SelectLabel from './label/SelectLabel'; @@ -61,10 +67,14 @@ export default class RightMenu extends React.Component { } handleClickOutside = event => { - if (this.wrapperRef && !this.wrapperRef.contains(event.target) && this.state.currentTab === 'Filter') { + if ( + this.wrapperRef && + !this.wrapperRef.contains(event.target) && + this.state.currentTab === 'Filter' + ) { this.setState({ showMenu: false }); } - } + }; toggleMenu = () => { this.setState({ showMenu: !this.state.showMenu }); @@ -167,7 +177,7 @@ export default class RightMenu extends React.Component { /> {extraFilter} - {this.renderDates()} + {this.renderDates()} ); } @@ -175,17 +185,17 @@ export default class RightMenu extends React.Component { renderTabContent() { if (this.state.currentTab === 'Filter') { const { isFiltered, clearFilter } = this.props; - + return ( <> {this.renderFilter()} {isFiltered && ( - - + void, msg?: string) => void; removeItem: (itemId: string, callback: () => void) => void; copyItem: (itemId: string, callback: () => void, msg?: string) => void; - beforePopupClose: () => void; + beforePopupClose: (afterPopupClose?: () => void) => void; amount?: () => React.ReactNode; formContent: ({ state, copy, remove }: IEditFormContent) => React.ReactNode; onUpdate: (item: IItem, prevStageId?) => void; @@ -68,22 +68,24 @@ class EditForm extends React.Component { copyItem(item._id, this.closeModal, options.texts.copySuccessText); }; - closeModal = () => { + closeModal = (afterPopupClose?: () => void) => { const { beforePopupClose } = this.props; if (beforePopupClose) { - beforePopupClose(); + beforePopupClose(afterPopupClose); + } else if (afterPopupClose) { + afterPopupClose(); } }; onHideModal = () => { - const { updatedItem, prevStageId } = this.state; + this.closeModal(() => { + const { updatedItem, prevStageId } = this.state; - if (updatedItem && this.props.onUpdate) { - this.props.onUpdate(updatedItem, prevStageId); - } - - this.closeModal(); + if (updatedItem && this.props.onUpdate) { + this.props.onUpdate(updatedItem, prevStageId); + } + }); }; renderArchiveStatus() { diff --git a/ui/src/modules/boards/components/editForm/Left.tsx b/ui/src/modules/boards/components/editForm/Left.tsx index cebb7a179b9..83c5c71e7fd 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'; @@ -15,6 +15,51 @@ import { LeftContainer, TitleRow } from '../../styles/item'; import Labels from '../label/Labels'; import Actions from './Actions'; +type DescProps = { + item: IItem; + saveItem: (doc: { [key: string]: any }) => void; +}; + +const Description = (props: DescProps) => { + const { item, saveItem } = 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); + }; + + return ( + + + + + {__('Description')} + + + + + + ); +}; + type Props = { item: IItem; options: IOptions; @@ -26,111 +71,85 @@ 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')} - - - - - - )} - +const Left = (props: Props) => { + const { + item, + saveItem, + options, + copyItem, + removeItem, + onUpdate, + addItem, + sendToBoard + } = props; + + const onChangeAttachment = (files: IAttachment[]) => + saveItem({ attachments: files }); + + const attachments = + (item.attachments && extractAttachment(item.attachments)) || []; + + return ( + + + + {item.labels.length > 0 && ( - - {__('Attachments')} + + {__('Labels')} - + - - - - - - {__('Description')} - - - - - - - - - - - - - ); - } -} + )} + + + + + + {__('Attachments')} + + + + + + + + + + + + + + + ); +}; export default Left; diff --git a/ui/src/modules/boards/components/editForm/Move.tsx b/ui/src/modules/boards/components/editForm/Move.tsx index 25a15c93449..4166a94597e 100644 --- a/ui/src/modules/boards/components/editForm/Move.tsx +++ b/ui/src/modules/boards/components/editForm/Move.tsx @@ -81,9 +81,9 @@ class Move extends React.Component { const item = ( - + - + 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/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/boards/components/label/SelectLabel.tsx b/ui/src/modules/boards/components/label/SelectLabel.tsx index c7923d4c2a7..34c553f1b17 100644 --- a/ui/src/modules/boards/components/label/SelectLabel.tsx +++ b/ui/src/modules/boards/components/label/SelectLabel.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { queries } from '../../graphql/index'; import { IPipelineLabel } from '../../types'; - // get user options for react-select-plus export function generateLabelOptions(array: IPipelineLabel[] = []): IOption[] { return array.map(item => { diff --git a/ui/src/modules/boards/components/portable/Items.tsx b/ui/src/modules/boards/components/portable/Items.tsx index b30b57c3278..c0f0f5c9047 100644 --- a/ui/src/modules/boards/components/portable/Items.tsx +++ b/ui/src/modules/boards/components/portable/Items.tsx @@ -42,7 +42,7 @@ class Items extends React.Component { const { onChangeItem, items, data } = this.props; if (items.length === 0) { - return ; + return ; } const Item = data.options.Item; @@ -75,7 +75,7 @@ class Items extends React.Component { const trigger = ( ); diff --git a/ui/src/modules/boards/components/stage/Item.tsx b/ui/src/modules/boards/components/stage/Item.tsx index 1c18e327d39..de4f8d9970f 100644 --- a/ui/src/modules/boards/components/stage/Item.tsx +++ b/ui/src/modules/boards/components/stage/Item.tsx @@ -1,10 +1,11 @@ import { IItem, IOptions } from 'modules/boards/types'; +import { IRouterProps } from 'modules/common/types'; import routerUtils from 'modules/common/utils/router'; import { IDeal } from 'modules/deals/types'; import { ITicket } from 'modules/tickets/types'; import queryString from 'query-string'; import React from 'react'; -import history from '../../../../browserHistory'; +import { withRouter } from 'react-router-dom'; type Props = { stageId?: string; @@ -12,7 +13,7 @@ type Props = { beforePopupClose?: () => void; onClick?: () => void; options: IOptions; -}; +} & IRouterProps; class Item extends React.PureComponent { unlisten?: () => void; @@ -20,7 +21,7 @@ class Item extends React.PureComponent { constructor(props) { super(props); - const { item } = props; + const { item, history } = props; const itemIdQueryParam = routerUtils.getParam(history, 'itemId'); @@ -36,7 +37,7 @@ class Item extends React.PureComponent { } componentDidMount() { - this.unlisten = history.listen(location => { + this.unlisten = this.props.history.listen(location => { const queryParams = queryString.parse(location.search); if (queryParams.itemId === this.props.item._id) { @@ -51,8 +52,8 @@ class Item extends React.PureComponent { } } - beforePopupClose = () => { - const { beforePopupClose } = this.props; + beforePopupClose = (afterPopupClose?: () => void) => { + const { beforePopupClose, history } = this.props; this.setState({ isFormVisible: false }, () => { const itemIdQueryParam = routerUtils.getParam(history, 'itemId'); @@ -64,6 +65,10 @@ class Item extends React.PureComponent { if (beforePopupClose) { beforePopupClose(); } + + if (afterPopupClose) { + afterPopupClose(); + } }); }; @@ -82,4 +87,4 @@ class Item extends React.PureComponent { } } -export default Item; +export default withRouter(Item); diff --git a/ui/src/modules/boards/components/stage/ItemList.tsx b/ui/src/modules/boards/components/stage/ItemList.tsx index 2ccacaf6ae0..c98b161497a 100644 --- a/ui/src/modules/boards/components/stage/ItemList.tsx +++ b/ui/src/modules/boards/components/stage/ItemList.tsx @@ -2,11 +2,12 @@ import client from 'apolloClient'; import gql from 'graphql-tag'; import EmptyState from 'modules/common/components/EmptyState'; import Icon from 'modules/common/components/Icon'; +import { IRouterProps } from 'modules/common/types'; import routerUtils from 'modules/common/utils/router'; import { mutations as notificationMutations } from 'modules/notifications/graphql'; import React from 'react'; import { Draggable, Droppable } from 'react-beautiful-dnd'; -import history from '../../../../browserHistory'; +import { withRouter } from 'react-router-dom'; import { DropZone, EmptyContainer, @@ -35,7 +36,7 @@ type DraggableContainerProps = { index: number; options: IOptions; onRemoveItem: (itemId: string, stageId: string) => void; -}; +} & IRouterProps; class DraggableContainer extends React.Component< DraggableContainerProps, @@ -45,7 +46,7 @@ class DraggableContainer extends React.Component< super(props); // if popup shows, draggable will disable - const itemIdQueryParam = routerUtils.getParam(history, 'itemId'); + const itemIdQueryParam = routerUtils.getParam(props.history, 'itemId'); this.state = { isDragDisabled: Boolean(itemIdQueryParam), @@ -54,10 +55,10 @@ class DraggableContainer extends React.Component< } onClick = () => { - const { item } = this.props; + const { item, history } = this.props; this.setState({ isDragDisabled: true }, () => { - routerUtils.setParams(history, { itemId: item._id }); + routerUtils.setParams(history, { itemId: item._id, key: '' }); }); if (!this.state.hasNotified) { @@ -126,6 +127,10 @@ class DraggableContainer extends React.Component< } } +const DraggableContainerWithRouter = withRouter( + DraggableContainer +); + class InnerItemList extends React.PureComponent<{ stageId: string; items: IItem[]; @@ -136,7 +141,7 @@ class InnerItemList extends React.PureComponent<{ const { stageId, items, options, onRemoveItem } = this.props; return items.map((item, index: number) => ( - { if (items.length === 0) { return ( - + ); } diff --git a/ui/src/modules/boards/components/stage/Stage.tsx b/ui/src/modules/boards/components/stage/Stage.tsx index acc058d6467..7e87ba52b1d 100644 --- a/ui/src/modules/boards/components/stage/Stage.tsx +++ b/ui/src/modules/boards/components/stage/Stage.tsx @@ -1,3 +1,4 @@ +import { PIPELINE_UPDATE_STATUSES } from 'modules/boards/constants'; import { ActionButton, ActionList, @@ -26,7 +27,7 @@ import { renderAmount } from '../../utils'; import ItemList from '../stage/ItemList'; type Props = { - loadingItems: boolean; + loadingItems: () => boolean; index: number; stage: IStage; length: number; @@ -37,6 +38,7 @@ type Props = { options: IOptions; archiveItems: () => void; archiveList: () => void; + onChangeRealTimeStageIds: (stageId: string) => void; }; export default class Stage extends React.Component { private bodyRef; @@ -51,6 +53,10 @@ export default class Stage extends React.Component { componentDidMount() { // Load items until scroll created const handle = setInterval(() => { + if (this.props.loadingItems()) { + return; + } + const { current } = this.bodyRef; if (!current) { @@ -60,19 +66,54 @@ export default class Stage extends React.Component { const isScrolled = current.scrollHeight > current.clientHeight; if (isScrolled) { - clearInterval(handle); + return clearInterval(handle); } - const { items, stage, loadMore } = this.props; + const { items, stage } = this.props; if (items.length < stage.itemsTotalCount) { - loadMore(); + return this.props.loadMore(); } else { - clearInterval(handle); + return clearInterval(handle); } }, 1000); } + componentDidUpdate(prevProps) { + const { current } = this.bodyRef; + + if (!current) { + return; + } + + const { stage, onChangeRealTimeStageIds } = this.props; + const pipelineUpdate = sessionStorage.getItem('pipelineUpdate'); + + if ( + (pipelineUpdate === PIPELINE_UPDATE_STATUSES.START || + pipelineUpdate === PIPELINE_UPDATE_STATUSES.NEW_REQUEST) && + stage.itemsTotalCount !== prevProps.stage.itemsTotalCount + ) { + onChangeRealTimeStageIds(stage._id); + } + } + + shouldComponentUpdate(nextProps: Props) { + const { stage, index, length, items, loadingItems } = this.props; + + if ( + index !== nextProps.index || + loadingItems() !== nextProps.loadingItems() || + length !== nextProps.length || + JSON.stringify(stage) !== JSON.stringify(nextProps.stage) || + JSON.stringify(items) !== JSON.stringify(nextProps.items) + ) { + return true; + } + + return false; + } + onScroll = (e: React.UIEvent) => { const target = e.currentTarget; const bottom = @@ -121,22 +162,6 @@ export default class Stage extends React.Component { return data; } - shouldComponentUpdate(nextProps: Props) { - const { stage, index, length, items, loadingItems } = this.props; - - if ( - index !== nextProps.index || - loadingItems !== nextProps.loadingItems || - length !== nextProps.length || - JSON.stringify(stage) !== JSON.stringify(nextProps.stage) || - JSON.stringify(items) !== JSON.stringify(nextProps.items) - ) { - return true; - } - - return false; - } - onClosePopover = () => { this.overlayTrigger.hide(); }; @@ -144,7 +169,7 @@ export default class Stage extends React.Component { renderItemList() { const { stage, items, loadingItems, options, onRemoveItem } = this.props; - if (loadingItems) { + if (loadingItems()) { return ( Loading @@ -211,7 +236,7 @@ export default class Stage extends React.Component { const { index, stage } = this.props; if (!stage) { - return ; + return ; } return ( diff --git a/ui/src/modules/boards/constants.ts b/ui/src/modules/boards/constants.ts index d7f9bce1dc6..4d03bc2996d 100644 --- a/ui/src/modules/boards/constants.ts +++ b/ui/src/modules/boards/constants.ts @@ -26,3 +26,9 @@ export const REMINDER_MINUTES = [ { _id: '1440', name: '1 Day Before' }, { _id: '2880', name: '2 Day Before' } ]; + +export const PIPELINE_UPDATE_STATUSES = { + START: 'start', + END: 'end', + NEW_REQUEST: 'newRequest' +}; diff --git a/ui/src/modules/boards/containers/ArchivedItems.tsx b/ui/src/modules/boards/containers/ArchivedItems.tsx index cd105e4b885..c5458594556 100644 --- a/ui/src/modules/boards/containers/ArchivedItems.tsx +++ b/ui/src/modules/boards/containers/ArchivedItems.tsx @@ -9,7 +9,7 @@ import { } from 'modules/common/utils'; import React from 'react'; import { graphql } from 'react-apollo'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import ArchivedItems from '../components/ArchivedItems'; import { mutations, queries } from '../graphql'; import { IItemParams, IOptions, RemoveMutation, SaveMutation } from '../types'; diff --git a/ui/src/modules/boards/containers/MainActionBar.tsx b/ui/src/modules/boards/containers/MainActionBar.tsx index cbd7a3b275b..689e49e6c59 100644 --- a/ui/src/modules/boards/containers/MainActionBar.tsx +++ b/ui/src/modules/boards/containers/MainActionBar.tsx @@ -8,10 +8,14 @@ import { router as routerUtils, withProps } from 'modules/common/utils'; import queryString from 'query-string'; import React from 'react'; import { graphql } from 'react-apollo'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import { STORAGE_BOARD_KEY, STORAGE_PIPELINE_KEY } from '../constants'; import { queries } from '../graphql'; -import { BoardDetailQueryResponse, BoardsGetLastQueryResponse, BoardsQueryResponse } from '../types'; +import { + BoardDetailQueryResponse, + BoardsGetLastQueryResponse, + BoardsQueryResponse +} from '../types'; type Props = { type: string; @@ -25,6 +29,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); }; @@ -44,25 +60,27 @@ class Main extends React.Component { if (!search) { return routerUtils.removeParams(this.props.history, 'search'); } - + routerUtils.setParams(this.props.history, { search }); }; onSelect = (values: string[] | string, name: string) => { const params = generateQueryParams(this.props.history); - if(params.closeDateType === values) { + if (params.closeDateType === values) { return routerUtils.removeParams(this.props.history, name); - } - + } + return routerUtils.setParams(this.props.history, { [name]: values }); }; 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; diff --git a/ui/src/modules/boards/containers/Pipeline.tsx b/ui/src/modules/boards/containers/Pipeline.tsx index 8d4cf2cd8a2..fbef453e5e0 100644 --- a/ui/src/modules/boards/containers/Pipeline.tsx +++ b/ui/src/modules/boards/containers/Pipeline.tsx @@ -2,11 +2,14 @@ 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 React from 'react'; +import { IRouterProps } from 'modules/common/types'; +import { router as routerUtils, withProps } from 'modules/common/utils'; +import React, { Component } from 'react'; import { graphql } from 'react-apollo'; import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; +import { PIPELINE_UPDATE_STATUSES } from '../constants'; import { queries } from '../graphql'; import { IItemMap, @@ -31,20 +34,18 @@ type Props = { options: IOptions; }; -class WithStages extends React.Component { +class WithStages extends Component { componentWillReceiveProps(nextProps: Props) { const { stagesQuery, queryParams } = this.props; const { pipelineId } = queryParams; - if (this.queryParamsChanged(queryParams, nextProps)) { + if (this.queryParamsChanged(queryParams, nextProps.queryParams)) { stagesQuery.refetch({ pipelineId }); } } - queryParamsChanged = (queryParams, nextProps: Props) => { - const nextQueryParams = nextProps.queryParams; - - if (nextQueryParams.itemId || queryParams.itemId) { + queryParamsChanged = (queryParams: any, nextQueryParams: any) => { + if (nextQueryParams.itemId || (!queryParams.key && queryParams.itemId)) { return false; } @@ -59,6 +60,19 @@ class WithStages extends React.Component { return Object.keys(obj).length; } + afterFinish = () => { + const pipelineUpdate = sessionStorage.getItem('pipelineUpdate'); + + // if there is a newRequest + if (pipelineUpdate === PIPELINE_UPDATE_STATUSES.NEW_REQUEST) { + sessionStorage.setItem('pipelineUpdate', PIPELINE_UPDATE_STATUSES.START); + + routerUtils.setParams(this.props.history, { key: Math.random() }); + } else { + sessionStorage.setItem('pipelineUpdate', PIPELINE_UPDATE_STATUSES.END); + } + }; + render() { const { initialItemMap, @@ -89,9 +103,20 @@ class WithStages extends React.Component { queryParams={queryParams} options={options} queryParamsChanged={this.queryParamsChanged} + afterFinish={this.afterFinish} > - {({ stageLoadMap, itemMap, onDragEnd, stageIds }) => ( + {({ + stageLoadMap, + itemMap, + onDragEnd, + stageIds, + scheduleStage, + onLoadStage, + onAddItem, + onRemoveItem, + onChangeRealTimeStageIds + }) => ( { queryParams={queryParams} loadingState={stageLoadMap[stageId]} refetchStages={stagesQuery.refetch} + scheduleStage={scheduleStage} + onLoad={onLoadStage} + onAddItem={onAddItem} + onRemoveItem={onRemoveItem} + onChangeRealTimeStageIds={onChangeRealTimeStageIds} /> ); })} @@ -137,11 +167,12 @@ class WithStages extends React.Component { } } -type WithStatesQueryProps = { +type WithStagesQueryProps = { stagesQuery: StagesQueryResponse; -} & Props; +} & IRouterProps & + Props; -const WithStatesQuery = (props: WithStatesQueryProps) => { +const WithStagesQuery = (props: WithStagesQueryProps) => { const { stagesQuery } = props; if (stagesQuery.loading) { @@ -179,5 +210,5 @@ export default withProps( } }) }) - )(WithStatesQuery) + )(withRouter(WithStagesQuery)) ); diff --git a/ui/src/modules/boards/containers/PipelineContext.tsx b/ui/src/modules/boards/containers/PipelineContext.tsx index 7ae40c72af8..72d515262bf 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,8 +20,12 @@ type Props = { pipeline: IPipeline; initialItemMap?: IItemMap; options: IOptions; - queryParams: IFilterParams; - queryParamsChanged: (queryParams: IFilterParams, args: any) => boolean; + queryParams: IFilterParams & INonFilterParams; + queryParamsChanged: ( + queryParams: IFilterParams, + nextQueryParams: IFilterParams + ) => boolean; + afterFinish: () => void; }; type StageLoadMap = { @@ -32,6 +37,7 @@ type State = { stageLoadMap: StageLoadMap; stageIds: string[]; isShowLabel: boolean; + realTimeStageIds: string[]; }; interface IStore { @@ -47,6 +53,7 @@ interface IStore { onUpdateItem: (item: IItem, prevStageId?: string) => void; isShowLabel: boolean; toggleLabels: () => void; + onChangeRealTimeStageIds: (stageId: string) => void; } const PipelineContext = React.createContext({} as IStore); @@ -68,11 +75,14 @@ export class PipelineProvider extends React.Component { const { initialItemMap } = props; + const stageIds = Object.keys(initialItemMap || {}); + this.state = { itemMap: initialItemMap || {}, stageLoadMap: {}, - stageIds: Object.keys(initialItemMap || {}), - isShowLabel: false + stageIds, + isShowLabel: false, + realTimeStageIds: [] }; PipelineProvider.tasks = []; @@ -82,7 +92,7 @@ export class PipelineProvider extends React.Component { componentWillReceiveProps(nextProps: Props) { const { queryParams, queryParamsChanged, initialItemMap } = this.props; - if (queryParamsChanged(queryParams, nextProps)) { + if (queryParamsChanged(queryParams, nextProps.queryParams)) { const { stageIds } = this.state; PipelineProvider.tasks = []; @@ -126,6 +136,24 @@ export class PipelineProvider extends React.Component { } } + componentDidUpdate() { + const { realTimeStageIds } = this.state; + + if (realTimeStageIds.length >= 2) { + this.setState({ realTimeStageIds: [] }); + + this.props.afterFinish(); + } + } + + onChangeRealTimeStageIds = (stageId: string) => { + this.setState(prevState => { + return { + realTimeStageIds: [...prevState.realTimeStageIds, stageId] + }; + }); + }; + onDragEnd = result => { // dropped nowhere if (!result.destination) { @@ -157,6 +185,9 @@ export class PipelineProvider extends React.Component { return this.saveStageOrders(stageIds); } + // to avoid to refetch current tab + sessionStorage.setItem('currentTab', 'true'); + const { itemMap } = reorderItemMap({ itemMap: this.state.itemMap, source, @@ -164,7 +195,8 @@ export class PipelineProvider extends React.Component { }); // update item to database - this.itemChange(result.draggableId, destination.droppableId); + const itemId = result.draggableId.split('-')[0]; + this.itemChange(itemId, destination.droppableId); this.setState({ itemMap @@ -399,7 +431,8 @@ export class PipelineProvider extends React.Component { stageLoadMap, stageIds, isShowLabel, - toggleLabels: this.toggleLabels + toggleLabels: this.toggleLabels, + onChangeRealTimeStageIds: this.onChangeRealTimeStageIds }} > {this.props.children} diff --git a/ui/src/modules/boards/containers/Stage.tsx b/ui/src/modules/boards/containers/Stage.tsx index db0b577603b..3fccc6b440d 100644 --- a/ui/src/modules/boards/containers/Stage.tsx +++ b/ui/src/modules/boards/containers/Stage.tsx @@ -1,7 +1,6 @@ import client from 'apolloClient'; import gql from 'graphql-tag'; import * as compose from 'lodash.flowright'; -import { PipelineConsumer } from 'modules/boards/containers/PipelineContext'; import { queries } from 'modules/boards/graphql'; import { Alert, confirm, withProps } from 'modules/common/utils'; import React from 'react'; @@ -17,7 +16,7 @@ import { SaveItemMutation } from '../types'; -type WrapperProps = { +type StageProps = { stage: IStage; index: number; loadingState: 'readyToLoad' | 'loaded'; @@ -26,14 +25,12 @@ type WrapperProps = { queryParams: IFilterParams; options: IOptions; refetchStages: ({ pipelineId }: { pipelineId?: string }) => Promise; -}; - -type StageProps = { onLoad: (stageId: string, items: IItem[]) => void; scheduleStage: (stageId: string) => void; onAddItem: (stageId: string, item: IItem) => void; onRemoveItem: (itemId: string, stageId: string) => void; -} & WrapperProps; + onChangeRealTimeStageIds: (stageId: string) => void; +}; type FinalStageProps = { addMutation: SaveItemMutation; @@ -47,7 +44,8 @@ class StageContainer extends React.PureComponent { if (itemsQuery && !itemsQuery.loading && loadingState !== 'loaded') { // Send loaded items to PipelineContext so that context is able to set it // to global itemsMap - onLoad(stage._id, itemsQuery[options.queriesName.itemsQuery] || []); + const items = itemsQuery[options.queriesName.itemsQuery] || []; + onLoad(stage._id, items); } } @@ -117,7 +115,7 @@ class StageContainer extends React.PureComponent { Alert.error(e.message); }); }); - } + }; archiveList = () => { const { stage, refetchStages, options } = this.props; @@ -157,10 +155,18 @@ class StageContainer extends React.PureComponent { itemsQuery, options, onAddItem, - onRemoveItem + onRemoveItem, + loadingState, + onChangeRealTimeStageIds } = this.props; - const loadingItems = (itemsQuery ? itemsQuery.loading : null) || false; + const loadingItems = () => { + if ((itemsQuery && !itemsQuery.loading) || loadingState !== 'loaded') { + return true; + } + + return false; + }; return ( { loadMore={this.loadMore} onAddItem={onAddItem} onRemoveItem={onRemoveItem} + onChangeRealTimeStageIds={onChangeRealTimeStageIds} /> ); } @@ -237,20 +244,4 @@ class WithData extends React.Component { } } -export default (props: WrapperProps) => { - return ( - - {({ onAddItem, onLoadStage, scheduleStage, onRemoveItem }) => { - return ( - - ); - }} - - ); -}; +export default withProps(WithData); diff --git a/ui/src/modules/boards/containers/editForm/EditForm.tsx b/ui/src/modules/boards/containers/editForm/EditForm.tsx index 6266249b3a3..54191549eab 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); } + componentDidMount() { + 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; @@ -257,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} /> ); }} diff --git a/ui/src/modules/boards/containers/portable/AddForm.tsx b/ui/src/modules/boards/containers/portable/AddForm.tsx index b99dfe8bd13..0ede24ef62b 100644 --- a/ui/src/modules/boards/containers/portable/AddForm.tsx +++ b/ui/src/modules/boards/containers/portable/AddForm.tsx @@ -69,7 +69,7 @@ class AddFormContainer extends React.Component { callback(data[options.mutationsName.addMutation]); if (getAssociatedItem) { - getAssociatedItem(data[options.mutationsName.addMutation]._id); + getAssociatedItem(data[options.mutationsName.addMutation]); } if (refetch) { diff --git a/ui/src/modules/boards/containers/portable/ItemChooser.tsx b/ui/src/modules/boards/containers/portable/ItemChooser.tsx index 14448f366fd..2c01e6ce0e8 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'; @@ -34,16 +36,20 @@ type FinalProps = { class ItemChooserContainer extends React.Component< WrapperProps & FinalProps, - { newItemId?: string } + { newItem?: string } > { constructor(props) { super(props); this.state = { - newItemId: undefined + newItem: undefined }; } + resetAssociatedItem = () => { + return this.setState({ newItem: undefined }); + }; + render() { const { data, itemsQuery, search } = this.props; @@ -51,8 +57,8 @@ class ItemChooserContainer extends React.Component< return item.name || 'Unknown'; }; - const getAssociatedItem = (newItemId: string) => { - this.setState({ newItemId }); + const getAssociatedItem = (newItem: string) => { + this.setState({ newItem }); }; const updatedProps = { @@ -84,11 +90,20 @@ class ItemChooserContainer extends React.Component< /> ), hasBoardChooser: true, - newItemId: this.state.newItemId, + newItem: this.state.newItem, + resetAssociatedItem: this.resetAssociatedItem, clearState: () => search(''), refetchQuery: data.options.queries.itemsQuery }; + if (itemsQuery.loading) { + return ; + } + + if (updatedProps.datas.length === 0) { + return ; + } + return ; } } diff --git a/ui/src/modules/boards/containers/withPipeline.tsx b/ui/src/modules/boards/containers/withPipeline.tsx index 763a136f2ed..5c2b4f26dce 100644 --- a/ui/src/modules/boards/containers/withPipeline.tsx +++ b/ui/src/modules/boards/containers/withPipeline.tsx @@ -1,24 +1,68 @@ 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-dom'; +import { PIPELINE_UPDATE_STATUSES } from '../constants'; +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) { + const pipelineUpdate = sessionStorage.getItem('pipelineUpdate'); + + routerUtils.setParams(history, { key: Math.random() }); + + if ( + !pipelineUpdate || + pipelineUpdate === PIPELINE_UPDATE_STATUSES.END + ) { + sessionStorage.setItem( + 'pipelineUpdate', + PIPELINE_UPDATE_STATUSES.START + ); + } else { + // if last subscription is not end + sessionStorage.setItem( + 'pipelineUpdate', + PIPELINE_UPDATE_STATUSES.NEW_REQUEST + ); + } + } else { + sessionStorage.removeItem('currentTab'); + } + } + }) + ); + }); const pipeline = pipelineDetailQuery && pipelineDetailQuery.pipelineDetail; @@ -53,7 +97,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/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 379e9e2742d..32962d805dd 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,8 @@ export const ActionContainer = styled(MoveContainer)` export const MoveFormContainer = styled.div` margin-right: 20px; position: relative; + z-index: 100; + will-change: transform; `; export const PipelineName = styled.div` @@ -259,6 +264,14 @@ export const StageItem = styledTS<{ isPass: boolean }>(styled.li)` &:before { display: none; } + + i { + margin-left: 0; + } + } + + &:last-child i { + margin-right: 0; } &:before { @@ -268,8 +281,8 @@ export const StageItem = styledTS<{ isPass: boolean }>(styled.li)` props.isPass ? colors.colorSecondary : colors.colorShadowGray}; width: 100%; top: 50%; - margin-top: -2px; - left: -0; + margin-top: 0; + left: 0; position: absolute; } @@ -283,6 +296,7 @@ export const StageItem = styledTS<{ isPass: boolean }>(styled.li)` i { font-size: 30px; + margin: 0 -3px; color: ${props => props.isPass ? colors.colorSecondary : colors.colorShadowGray}; } 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/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/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 +197,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 +211,9 @@ class List extends React.Component { /> ); - }; - - renderProgressBar = () => { - const { item } = this.props; + } + function renderProgressBar() { return ( {item.percent.toFixed(0)}% @@ -229,59 +224,52 @@ 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/Item.tsx b/ui/src/modules/checklists/containers/Item.tsx index 3a864d97fe8..4fb3dd39e71 100644 --- a/ui/src/modules/checklists/containers/Item.tsx +++ b/ui/src/modules/checklists/containers/Item.tsx @@ -57,18 +57,16 @@ class ItemContainer extends React.Component { } } -const options = (props: Props) => { - return { - refetchQueries: [ - { - query: gql(queries.checklistDetail), - variables: { - _id: props.item.checklistId - } +const options = (props: Props) => ({ + refetchQueries: [ + { + query: gql(queries.checklistDetail), + variables: { + _id: props.item.checklistId } - ] - }; -}; + } + ] +}); export default withProps( compose( diff --git a/ui/src/modules/checklists/containers/List.tsx b/ui/src/modules/checklists/containers/List.tsx index 674cc2a027b..bbdf4efdd47 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,88 +55,76 @@ 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 ; +} // end ListContainer() -const options = (props: Props) => { - return { - refetchQueries: [ - { - query: gql(queries.checklistDetail), - variables: { - _id: props.listId - } - } - ] - }; -}; +const options = (props: Props) => ({ + refetchQueries: [ + { + query: gql(queries.checklistDetail), + variables: { _id: props.listId } + } + ] +}); export default withProps( compose( 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/ActionButtons.tsx b/ui/src/modules/common/components/ActionButtons.tsx index 58d811325db..7ed30c1ed61 100755 --- a/ui/src/modules/common/components/ActionButtons.tsx +++ b/ui/src/modules/common/components/ActionButtons.tsx @@ -6,7 +6,7 @@ const ActionButton = styled.div` * { padding: 0; - margin-left: 10px; + margin-left: 8px; &:first-child { margin-left: 0; diff --git a/ui/src/modules/common/components/Attachment.tsx b/ui/src/modules/common/components/Attachment.tsx index bb77b69c0f5..c5719a354c3 100644 --- a/ui/src/modules/common/components/Attachment.tsx +++ b/ui/src/modules/common/components/Attachment.tsx @@ -30,15 +30,17 @@ const ItemInfo = styled.div` h5 { margin: 0 0 5px; - display: flex; font-weight: bold; } + + video { + width: 100%; + } `; const Download = styled.a` color: ${colors.colorCoreGray}; - padding: 0 5px; - margin-left: 5px; + margin-left: 10px; &:hover { color: ${colors.colorCoreBlack}; @@ -54,9 +56,11 @@ const PreviewWrapper = styled.div` align-items: center; border-radius: 4px; overflow: hidden; + align-self: center; i { - font-size: 26px; + font-size: 36px; + color: ${colors.colorSecondary}; } `; @@ -71,10 +75,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 = { @@ -97,7 +100,7 @@ class Attachment extends React.Component { href={readFile(attachment.url)} target="_blank" > - + @@ -178,33 +181,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/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/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/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/Chooser.tsx b/ui/src/modules/common/components/Chooser.tsx index 8d1161ba1ea..d4f508b656c 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,8 @@ export type CommonProps = { clearState: () => void; limit?: number; add?: any; - newItemId?: string; + newItem?: string | ICompany | ICustomer; + resetAssociatedItem?: () => void; closeModal: () => void; }; @@ -59,14 +62,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); + const { resetAssociatedItem } = this.props; - items.map(data => this.setState({ datas: [...this.state.datas, data] })); + if (newItem) { + this.setState({ datas: [...this.state.datas, newItem] }, () => { + if (resetAssociatedItem) { + resetAssociatedItem(); + } + }); } } @@ -125,7 +132,7 @@ class CommonChooser extends React.Component { ); } - return ; + return ; } render() { diff --git a/ui/src/modules/common/components/CollapseContent.tsx b/ui/src/modules/common/components/CollapseContent.tsx index 25c47f32638..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; @@ -22,11 +22,15 @@ const Title = styled.div` } `; -const Container = styledTS<{open: boolean}>(styled.div)` +const Container = styledTS<{ open: boolean }>(styled.div)` margin-bottom: 10px; box-shadow: 0 0 6px 1px rgba(0,0,0,0.08); border-radius: 4px; - background: ${props => props.open ? colors.bgLight : colors.colorWhite}; + background: ${props => (props.open ? colors.bgLight : colors.colorWhite)}; + + &:last-child { + margin-bottom: 5px; + } ${Title} i { font-size: 20px; @@ -47,29 +51,27 @@ type Props = { title: string; children: React.ReactNode; open?: boolean; + compact?: boolean; }; -function CollapseContent (props: Props) { - const [ open, toggleCollapse ] = useState(props.open || false); +function CollapseContent(props: Props) { + const [open, toggleCollapse] = useState(props.open || false); - const onClick = () => toggleCollapse(!open); + const onClick = () => toggleCollapse(!open); return ( - + <Title onClick={onClick} compact={props.compact}> <h4>{props.title}</h4> <Icon icon="angle-down" />
- - {props.children} - + {props.children}
); - } export default CollapseContent; diff --git a/ui/src/modules/common/components/CountsByTag.tsx b/ui/src/modules/common/components/CountsByTag.tsx index feefcde5f23..6d960cf6b92 100755 --- a/ui/src/modules/common/components/CountsByTag.tsx +++ b/ui/src/modules/common/components/CountsByTag.tsx @@ -2,7 +2,7 @@ import FilterByParams from 'modules/common/components/FilterByParams'; import Icon from 'modules/common/components/Icon'; import { __, router } from 'modules/common/utils'; import React from 'react'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import { Link } from 'react-router-dom'; import { IRouterProps } from '../../common/types'; import { ITag } from '../../tags/types'; @@ -23,12 +23,12 @@ function CountsByTag({ history, tags, counts, manageUrl, loading }: IProps) { const extraButtons = ( <> - + {router.getParam(history, 'tag') && ( - + )} @@ -38,7 +38,7 @@ function CountsByTag({ history, tags, counts, manageUrl, loading }: IProps) { 5} + collapsible={tags.length > 7} name="showFilterByTags" > { + 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/common/components/FilterByParams.tsx b/ui/src/modules/common/components/FilterByParams.tsx index 1a655e787dd..26d87db4e4d 100644 --- a/ui/src/modules/common/components/FilterByParams.tsx +++ b/ui/src/modules/common/components/FilterByParams.tsx @@ -4,7 +4,7 @@ import Icon from 'modules/common/components/Icon'; import { router } from 'modules/common/utils'; import { FieldStyle, SidebarCounter, SidebarList } from 'modules/layout/styles'; import React from 'react'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; import { IRouterProps } from '../types'; import Filter from './filterableList/Filter'; diff --git a/ui/src/modules/common/components/IntegrationIcon.tsx b/ui/src/modules/common/components/IntegrationIcon.tsx index 610111579aa..9e7235a0739 100644 --- a/ui/src/modules/common/components/IntegrationIcon.tsx +++ b/ui/src/modules/common/components/IntegrationIcon.tsx @@ -23,8 +23,13 @@ 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}; + (props.type.includes('telegram') && colors.socialTelegram) || + (props.type.includes('viber') && colors.socialViber) || + (props.type.includes('line') && colors.socialLine) || + (props.type.includes('twilio') && colors.socialTwilio) || + colors.colorCoreRed}; i { color: ${colors.colorWhite}; @@ -57,29 +62,37 @@ 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 'smooch-line': + icon = 'line'; + break; + case 'smooch-telegram': + icon = 'telegram-alt'; + break; + case 'smooch-viber': + icon = 'viber'; + break; + case 'smooch-twilio': + icon = 'twilio'; + break; + + case 'whatsapp': + icon = 'whatsapp-fill'; + break; default: icon = 'doc-text-inv-1'; } diff --git a/ui/src/modules/common/components/LoadMore.tsx b/ui/src/modules/common/components/LoadMore.tsx index 1e076a8f1a0..b545dc9d0a5 100755 --- a/ui/src/modules/common/components/LoadMore.tsx +++ b/ui/src/modules/common/components/LoadMore.tsx @@ -2,7 +2,7 @@ import Button from 'modules/common/components/Button'; import { IRouterProps } from 'modules/common/types'; import { router } from 'modules/common/utils'; import React from 'react'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; interface IProps extends IRouterProps { perPage?: number; diff --git a/ui/src/modules/common/components/ModalTrigger.tsx b/ui/src/modules/common/components/ModalTrigger.tsx index f61c0dea034..829aa6f698a 100755 --- a/ui/src/modules/common/components/ModalTrigger.tsx +++ b/ui/src/modules/common/components/ModalTrigger.tsx @@ -1,7 +1,7 @@ import { __ } from 'modules/common/utils'; import React from 'react'; import { Modal } from 'react-bootstrap'; -import { withRouter } from 'react-router'; +import { withRouter } from 'react-router-dom'; import RTG from 'react-transition-group'; import { CloseModal } from '../styles/main'; import { IRouterProps } from '../types'; diff --git a/ui/src/modules/common/components/ModifiableList.tsx b/ui/src/modules/common/components/ModifiableList.tsx index d7cda3d5ad8..8c783a42f1d 100644 --- a/ui/src/modules/common/components/ModifiableList.tsx +++ b/ui/src/modules/common/components/ModifiableList.tsx @@ -145,7 +145,7 @@ class ModifiableList extends React.Component { } return ( - ); 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 + { value={currentEventName} onChange={this.onChangeEvents} options={eventsData} - placeholder={__("Select event")} + placeholder={__('Select event')} /> ); } @@ -155,15 +185,22 @@ class Condition extends React.Component { return ( <> - + - + - ) + ); } render() { @@ -171,25 +208,22 @@ class Condition extends React.Component { <> - - {this.renderNames()} - + {this.renderNames()} {this.renderOccurence()} - { - this.state.currentEventName && - - } - + )} + @@ -203,9 +237,7 @@ class Condition extends React.Component { - - {this.renderAttributeFilters()} - + {this.renderAttributeFilters()} ); } diff --git a/ui/src/modules/segments/components/common/Filter.tsx b/ui/src/modules/segments/components/common/Filter.tsx index 82a2ca73649..d7a3fa5b07d 100644 --- a/ui/src/modules/segments/components/common/Filter.tsx +++ b/ui/src/modules/segments/components/common/Filter.tsx @@ -1,12 +1,12 @@ -import Button from "modules/common/components/Button"; -import { FormControl } from "modules/common/components/form"; -import { __ } from "modules/common/utils"; -import { operators } from "modules/customers/constants"; -import { FlexRightItem } from "modules/layout/styles"; -import React from "react"; +import Button from 'modules/common/components/Button'; +import { FormControl } from 'modules/common/components/form'; +import { __ } from 'modules/common/utils'; +import { operators } from 'modules/customers/constants'; +import { FlexRightItem } from 'modules/layout/styles'; +import React from 'react'; import Select from 'react-select-plus'; -import { IConditionFilter, IField } from "../../types"; -import { ConditionItem, FilterProperty, FilterRow } from "../styles"; +import { IConditionFilter, IField } from '../../types'; +import { ConditionItem, FilterProperty, FilterRow } from '../styles'; type Props = { fields: IField[]; @@ -33,7 +33,7 @@ class Filter extends React.Component { key: filter.key || '', currentName: filter.name, currentOperator: filter.operator, - currentValue: filter.value, + currentValue: filter.value }; } @@ -45,32 +45,43 @@ class Filter extends React.Component { key: filter.key, name: currentName, operator: currentOperator, - value: currentValue, + value: currentValue }); - } + }; onChangeValue = (e: React.FormEvent) => { - this.setState({ currentValue: (e.currentTarget as HTMLInputElement).value }, this.onChange); - } + this.setState( + { currentValue: (e.currentTarget as HTMLInputElement).value }, + this.onChange + ); + }; onChangeNames = (e: React.FormEvent) => { - this.setState({ currentName: (e.currentTarget as HTMLInputElement).value }, this.onChange); - } + this.setState( + { currentName: (e.currentTarget as HTMLInputElement).value }, + this.onChange + ); + }; onChangeOperators = (e: React.FormEvent) => { - this.setState({ currentOperator: (e.currentTarget as HTMLInputElement).value }, this.onChange); - } + this.setState( + { currentOperator: (e.currentTarget as HTMLInputElement).value }, + this.onChange + ); + }; onChangeField = ({ value }: { value: string }) => { this.setState({ currentName: value }, this.onChange); - } + }; groupByType = () => { const { fields = [] } = this.props; - + return fields.reduce((acc, field) => { const value = field.value; - const key = value.includes('.') ? value.substr(0, value.indexOf('.')) : 'general'; + const key = value && value.includes('.') + ? value.substr(0, value.indexOf('.')) + : 'general'; if (!acc[key]) { acc[key] = []; @@ -92,7 +103,7 @@ class Filter extends React.Component { }); return array; - } + }; renderNames() { const { fields, groupData } = this.props; @@ -105,7 +116,7 @@ class Filter extends React.Component { clearable={false} value={currentName} onChange={this.onChangeField} - placeholder={__("Select property")} + placeholder={__('Select property')} /> ); } @@ -114,8 +125,12 @@ class Filter extends React.Component { const { currentOperator } = this.state; return ( - - + + {operators.map(c => (