diff --git a/public/ckeditor/config.js b/public/ckeditor/config.js index 28c56d8df8a..24afab22464 100644 --- a/public/ckeditor/config.js +++ b/public/ckeditor/config.js @@ -3,9 +3,12 @@ * For licensing, see https://ckeditor.com/legal/ckeditor-oss-license */ + CKEDITOR.editorConfig = function( config ) { // Define changes to default configuration here. For example: // config.language = 'fr'; config.font_names = 'Helvetica Neue;Helvetica;Arial;Verdana;Georgia;Times New Roman;Monospace;Comic Sans MS;Courier New;Tahoma'; // config.uiColor = '#AADC6E'; }; + +CKEDITOR.disableAutoInline = true; diff --git a/src/__tests__/forms/FormFieldPreview.test.tsx b/src/__tests__/forms/FormFieldPreview.test.tsx deleted file mode 100644 index de9f58d0a8e..00000000000 --- a/src/__tests__/forms/FormFieldPreview.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { shallow } from 'enzyme'; -import FormFieldPreview from 'modules/forms/components/step/preview/FormFieldPreview'; -import React from 'react'; - -describe('FormFieldPreview component', () => { - const defaultProps = { - onChange: (name: string, fields: any) => false - }; - - test('renders shallow successfully', () => { - const wrapper = shallow(); - expect(wrapper).not.toBe(''); - }); -}); diff --git a/src/__tests__/forms/CalloutPreview.test.tsx b/src/__tests__/leads/CalloutPreview.test.tsx similarity index 84% rename from src/__tests__/forms/CalloutPreview.test.tsx rename to src/__tests__/leads/CalloutPreview.test.tsx index 4eda24b6508..3654aa6e28a 100644 --- a/src/__tests__/forms/CalloutPreview.test.tsx +++ b/src/__tests__/leads/CalloutPreview.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import CalloutPreview from 'modules/forms/components/step/preview/CalloutPreview'; +import CalloutPreview from 'modules/leads/components/step/preview/CalloutPreview'; import React from 'react'; describe('CalloutPreview component', () => { diff --git a/src/__tests__/forms/ChooseType.test.tsx b/src/__tests__/leads/ChooseType.test.tsx similarity index 86% rename from src/__tests__/forms/ChooseType.test.tsx rename to src/__tests__/leads/ChooseType.test.tsx index eb8234200c1..92addca11d0 100644 --- a/src/__tests__/forms/ChooseType.test.tsx +++ b/src/__tests__/leads/ChooseType.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import ChooseType from 'modules/forms/components/step/ChooseType'; +import ChooseType from 'modules/leads/components/step/ChooseType'; import React from 'react'; describe('ChooseType component', () => { diff --git a/src/__tests__/forms/CommonPreview.test.tsx b/src/__tests__/leads/CommonPreview.test.tsx similarity index 83% rename from src/__tests__/forms/CommonPreview.test.tsx rename to src/__tests__/leads/CommonPreview.test.tsx index f28909016f5..b64d61652e6 100644 --- a/src/__tests__/forms/CommonPreview.test.tsx +++ b/src/__tests__/leads/CommonPreview.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import CommonPreview from 'modules/forms/components/step/preview/CommonPreview'; +import CommonPreview from 'modules/leads/components/step/preview/CommonPreview'; import React from 'react'; describe('CommonPreview component', () => { diff --git a/src/__tests__/forms/FormPreview.test.tsx b/src/__tests__/leads/FormPreview.test.tsx similarity index 77% rename from src/__tests__/forms/FormPreview.test.tsx rename to src/__tests__/leads/FormPreview.test.tsx index 80e623bd6ec..ec35cdb24bb 100644 --- a/src/__tests__/forms/FormPreview.test.tsx +++ b/src/__tests__/leads/FormPreview.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import FormPreview from 'modules/forms/components/step/preview/FormPreview'; +import FormPreview from 'modules/leads/components/step/preview/FormPreview'; import React from 'react'; describe('FormPreview component', () => { @@ -8,7 +8,8 @@ describe('FormPreview component', () => { color: 'red', theme: 'default', onChange: (name: any, fields: string) => null, - type: 'string' + type: 'string', + previewRenderer: () =>
}; test('renders shallow successfully', () => { diff --git a/src/__tests__/forms/FullPreviewStep.test.tsx b/src/__tests__/leads/FullPreviewStep.test.tsx similarity index 70% rename from src/__tests__/forms/FullPreviewStep.test.tsx rename to src/__tests__/leads/FullPreviewStep.test.tsx index 16fcfd8e9b6..7a066913136 100644 --- a/src/__tests__/forms/FullPreviewStep.test.tsx +++ b/src/__tests__/leads/FullPreviewStep.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import FullPreviewStep from 'modules/forms/components/step/FullPreviewStep'; +import FullPreviewStep from 'modules/leads/components/step/FullPreviewStep'; import React from 'react'; describe('FullPreviewStep component', () => { @@ -8,7 +8,12 @@ describe('FullPreviewStep component', () => { color: 'red', theme: 'default', onChange: (name: 'carousel', value: string) => null, - carousel: 'carousel' + carousel: 'carousel', + formData: { + title: 'Title', + btnText: 'Save', + desc: 'desc' + } }; test('renders shallow successfully', () => { diff --git a/src/__tests__/forms/SuccessPreview.test.tsx b/src/__tests__/leads/SuccessPreview.test.tsx similarity index 86% rename from src/__tests__/forms/SuccessPreview.test.tsx rename to src/__tests__/leads/SuccessPreview.test.tsx index 5bac5d9cbea..b477920cc53 100644 --- a/src/__tests__/forms/SuccessPreview.test.tsx +++ b/src/__tests__/leads/SuccessPreview.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import SuccessPreview from 'modules/forms/components/step/preview/SuccessPreview'; +import SuccessPreview from 'modules/leads/components/step/preview/SuccessPreview'; import React from 'react'; describe('SuccessPreview component', () => { diff --git a/src/__tests__/forms/SuccessStep.test.tsx b/src/__tests__/leads/SuccessStep.test.tsx similarity index 91% rename from src/__tests__/forms/SuccessStep.test.tsx rename to src/__tests__/leads/SuccessStep.test.tsx index 18f97affeef..825e0ae8020 100644 --- a/src/__tests__/forms/SuccessStep.test.tsx +++ b/src/__tests__/leads/SuccessStep.test.tsx @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import SuccessStep from 'modules/forms/components/step/SuccessStep'; +import SuccessStep from 'modules/leads/components/step/SuccessStep'; import React from 'react'; describe('SuccessStep component', () => { diff --git a/src/modules/activityLogs/components/ActivityList.tsx b/src/modules/activityLogs/components/ActivityList.tsx index 22dfc89917b..43c0bdc5fc2 100644 --- a/src/modules/activityLogs/components/ActivityList.tsx +++ b/src/modules/activityLogs/components/ActivityList.tsx @@ -23,7 +23,7 @@ class ActivityList extends React.Component { return (
- {data} + {data} {data.map(key => this.renderItem(activity[key]))}
); diff --git a/src/modules/activityLogs/components/ActivityRow.tsx b/src/modules/activityLogs/components/ActivityRow.tsx index 95938e8d4f2..0585a1cb258 100644 --- a/src/modules/activityLogs/components/ActivityRow.tsx +++ b/src/modules/activityLogs/components/ActivityRow.tsx @@ -10,6 +10,7 @@ import { import Icon from 'modules/common/components/Icon'; import NameCard from 'modules/common/components/nameCard/NameCard'; import Tip from 'modules/common/components/Tip'; +import { colors } from 'modules/common/styles'; import React from 'react'; type Props = { @@ -23,8 +24,8 @@ const ActivityRowComponent = (props: Props) => { return ( - - + + @@ -34,7 +35,7 @@ const ActivityRowComponent = (props: Props) => { {body} - {dayjs(data.createdAt).format('MMM Do, h:mm A')} + {dayjs(data.createdAt).format('MMM D, h:mm A')} diff --git a/src/modules/activityLogs/styles.ts b/src/modules/activityLogs/styles.ts index 24b693de3ff..a1ac553a285 100644 --- a/src/modules/activityLogs/styles.ts +++ b/src/modules/activityLogs/styles.ts @@ -20,9 +20,10 @@ const Timeline = styled.div` } `; -const ActivityTitle = styled.h3` - padding: ${dimensions.unitSpacing}px 0; - font-weight: 300; +const ActivityTitle = styled.h4` + margin: 0; + padding: ${dimensions.coreSpacing * 1.5}px 0 ${dimensions.coreSpacing}px; + font-weight: 400; color: ${colors.textPrimary}; `; @@ -31,7 +32,7 @@ const ActivityRow = styled(WhiteBox)` position: relative; overflow: visible; margin-bottom: ${dimensions.coreSpacing}px; - border-radius: 3px; + border-radius: 2px; &:last-of-type { margin-bottom: 0; @@ -41,6 +42,7 @@ const ActivityRow = styled(WhiteBox)` const FlexContent = styled.div` display: flex; justify-content: space-between; + align-items: center; `; const FlexBody = styled.div` @@ -63,7 +65,7 @@ const FlexBody = styled.div` `; const AvatarWrapper = styledTS<{ isUser?: boolean }>(styled.div)` - margin-right: ${dimensions.coreSpacing}px; + margin-right: ${dimensions.unitSpacing}px; position: relative; a { @@ -114,7 +116,7 @@ const ActivityIcon = styledTS<{ color?: string }>(styled.span)` const ActivityDate = styled.div` color: ${colors.colorCoreGray}; font-weight: ${typography.fontWeightLight}; - font-size: 12px; + font-size: 11px; margin-left: 5px; cursor: help; `; diff --git a/src/modules/boards/components/DueDateChanger.tsx b/src/modules/boards/components/DueDateChanger.tsx new file mode 100644 index 00000000000..ac20c3e556d --- /dev/null +++ b/src/modules/boards/components/DueDateChanger.tsx @@ -0,0 +1,78 @@ +import Datetime from '@nateradebaugh/react-datetime'; +import Icon from 'modules/common/components/Icon'; +import { rgba } from 'modules/common/styles/color'; +import colors from 'modules/common/styles/colors'; +import * as React from 'react'; +import styled from 'styled-components'; +import styledTS from 'styled-components-ts'; + +export const DateWrapper = styledTS<{ color: string }>(styled.div)` + position: relative; + + input { + background-color: ${props => rgba(props.color, 0.1)}; + color: ${props => props.color}; + border: none; + box-shadow: none; + outline: 0; + padding: 0 10px 0 25px; + height: 25px; + border-radius: 2px; + font-weight: 500; + line-height: 25px; + width: 110px; + font-size: 12px; + + &:hover { + background: ${props => rgba(props.color, 0.15)}; + cursor: pointer; + } + + &:focus { + box-shadow: none; + } + + ::placeholder { + color: ${props => props.color}; + font-weight: 500; + opacity: 1; + } + } + + > i { + color: ${props => props.color}; + line-height: 25px; + position: absolute; + left: 7px; + } +`; + +type IProps = { + onChange: ((value?: string | Date) => void); + value: Date; + isWarned?: boolean; +}; + +class DueDateChanger extends React.Component { + render() { + const { onChange, value, isWarned } = this.props; + const color = isWarned ? colors.colorCoreRed : colors.colorPrimaryDark; + + return ( + + + + + ); + } +} + +export default DueDateChanger; diff --git a/src/modules/boards/components/MainActionBar.tsx b/src/modules/boards/components/MainActionBar.tsx index 73f65e539a6..16dafab7f33 100644 --- a/src/modules/boards/components/MainActionBar.tsx +++ b/src/modules/boards/components/MainActionBar.tsx @@ -15,8 +15,6 @@ import FormControl from 'modules/common/components/form/Control'; import Icon from 'modules/common/components/Icon'; import Tip from 'modules/common/components/Tip'; import { __ } from 'modules/common/utils'; -import SelectCompanies from 'modules/companies/containers/SelectCompanies'; -import SelectCustomers from 'modules/customers/containers/common/SelectCustomers'; import Participators from 'modules/inbox/components/conversationDetail/workarea/Participators'; import { PopoverHeader } from 'modules/notifications/components/styles'; import SelectTeamMembers from 'modules/settings/team/containers/SelectTeamMembers'; @@ -89,10 +87,6 @@ class MainActionBar extends React.Component { } }; - toggleFilter = () => { - this.setState({ show: !this.state.show }); - }; - hideFilter = () => { this.setState({ show: false }); }; @@ -219,18 +213,6 @@ class MainActionBar extends React.Component { {__('Filter')} {extraFilter} - - { + - + ); } diff --git a/src/modules/boards/components/editForm/Watch.tsx b/src/modules/boards/components/editForm/Watch.tsx index bffdb153ba3..3b1622c1cf1 100644 --- a/src/modules/boards/components/editForm/Watch.tsx +++ b/src/modules/boards/components/editForm/Watch.tsx @@ -1,33 +1,44 @@ -import { WatchIndicator } from 'modules/boards/styles/item'; +import { ColorButton } from 'modules/boards/styles/common'; import { IItem } from 'modules/boards/types'; -import Button from 'modules/common/components/Button'; import Icon from 'modules/common/components/Icon'; import { __ } from 'modules/common/utils'; import * as React from 'react'; +import { RightButton, WatchIndicator } from '../../styles/item'; type IProps = { item: IItem; onChangeWatch: (isAdd: boolean) => void; + isSmall?: boolean; }; class Watch extends React.Component { render() { const { onChangeWatch, - item: { isWatched } + item: { isWatched }, + isSmall } = this.props; const onClick = () => onChangeWatch(!isWatched); + if (isSmall) { + return ( + + + {__('Watch')} + + ); + } + return ( - + ); } } diff --git a/src/modules/boards/components/portable/AddForm.tsx b/src/modules/boards/components/portable/AddForm.tsx index 9f3e137f328..3ab0c76f0f3 100644 --- a/src/modules/boards/components/portable/AddForm.tsx +++ b/src/modules/boards/components/portable/AddForm.tsx @@ -121,7 +121,7 @@ class AddForm extends React.Component { - Name + Name diff --git a/src/modules/boards/components/stage/ItemList.tsx b/src/modules/boards/components/stage/ItemList.tsx index 082b750bf39..a8843f130f6 100644 --- a/src/modules/boards/components/stage/ItemList.tsx +++ b/src/modules/boards/components/stage/ItemList.tsx @@ -149,11 +149,7 @@ class InnerList extends React.PureComponent { if (items.length === 0) { return ( - + ); } diff --git a/src/modules/boards/containers/editForm/EditForm.tsx b/src/modules/boards/containers/editForm/EditForm.tsx index 106a78a1116..e0aba125d91 100644 --- a/src/modules/boards/containers/editForm/EditForm.tsx +++ b/src/modules/boards/containers/editForm/EditForm.tsx @@ -26,6 +26,7 @@ type WrapperProps = { onAdd?: (stageId: string, item: IItem) => void; onRemove?: (itemId: string, stageId: string) => void; onUpdate?: (item: IItem, prevStageId: string) => void; + hideHeader?: boolean; }; type ContainerProps = { diff --git a/src/modules/boards/containers/editForm/Watch.tsx b/src/modules/boards/containers/editForm/Watch.tsx index a42e2012495..3eabab3ab08 100644 --- a/src/modules/boards/containers/editForm/Watch.tsx +++ b/src/modules/boards/containers/editForm/Watch.tsx @@ -13,6 +13,7 @@ import { Watch } from '../../components/editForm'; type IProps = { item: IItem; options: IOptions; + isSmall?: boolean; }; type FinalProps = { diff --git a/src/modules/boards/styles/common.ts b/src/modules/boards/styles/common.ts index 070e5871e5e..f7ff65a3ebc 100644 --- a/src/modules/boards/styles/common.ts +++ b/src/modules/boards/styles/common.ts @@ -1,4 +1,5 @@ import { colors } from 'modules/common/styles'; +import { rgba } from 'modules/common/styles/color'; import { Contents, MainContent } from 'modules/layout/styles'; import styled from 'styled-components'; import styledTS from 'styled-components-ts'; @@ -67,6 +68,31 @@ export const SelectContainer = styled.div` background: ${colors.colorWhite}; `; +export const ColorButton = styledTS<{ color?: string }>(styled.div)` + height: 25px; + border-radius: 2px; + font-weight: 500; + line-height: 25px; + font-size: 12px; + background-color: ${props => rgba(props.color || colors.colorPrimary, 0.1)}; + color: ${props => props.color || colors.colorPrimaryDark}; + padding: 0 10px; + transition: background 0.3s ease; + + > i { + margin-right: 5px; + } + + > span { + margin-right: 10px; + } + + &:hover { + cursor: pointer; + background-color: ${props => rgba(props.color || colors.colorPrimary, 0.2)}; + } +`; + export const FormContainer = styled.div` padding-right: 20px; `; diff --git a/src/modules/boards/styles/filter.ts b/src/modules/boards/styles/filter.ts index 74b29a1e4fb..73582c18316 100644 --- a/src/modules/boards/styles/filter.ts +++ b/src/modules/boards/styles/filter.ts @@ -79,19 +79,20 @@ const ClearDate = styledTS<{ selected: boolean }>(styled.div)` `; const FilterBtn = styledTS<{ active?: boolean }>(styled.div)` - box-shadow: ${props => - props.active - ? `0 2px 16px 0 ${lighten(colors.colorCoreGreen, 25)}` - : 'none'}; - background: ${props => - props.active ? colors.colorCoreGreen : 'transparent'}; - border-radius: 30px; + box-shadow: ${props => + props.active + ? `0 2px 16px 0 ${lighten(colors.colorCoreGreen, 25)}` + : 'none'}; + background: ${props => + props.active ? colors.colorCoreGreen : 'transparent'}; + border-radius: 30px; + transition: background-color 0.3s ease; + + button { + color: ${colors.colorWhite}; transition: background-color 0.3s ease; - button{ - font-size:10px; - color: ${colors.colorWhite}; - transition: background-color 0.3s ease; - } + } + span { margin-left: 0; } diff --git a/src/modules/boards/styles/header.ts b/src/modules/boards/styles/header.ts index a3ef00536a3..3bb5505c0b8 100644 --- a/src/modules/boards/styles/header.ts +++ b/src/modules/boards/styles/header.ts @@ -57,19 +57,17 @@ export const HeaderButton = styledTS<{ rightIconed?: boolean; isActive?: boolean; }>(styled.div)` - padding: 0 12px; - line-height: 32px; - height: 34px; + padding: 0 10px; + line-height: 30px; + height: 32px; border-radius: 4px; transition: background 0.3s ease; background: ${props => props.hasBackground && 'rgba(0, 0, 0, 0.04)'}; - font-size: 14px; font-weight: 500; display: inline-block; vertical-align: middle; > i { - font-size: 14px; color: ${props => props.isActive ? colors.colorSecondary : colors.colorCoreGray}; margin-right: 5px; @@ -110,9 +108,9 @@ export const HeaderLink = styled(HeaderButton)` a { color: ${colors.colorCoreGray}; - padding: 0 11px; + padding: 0 10px; display: block; - line-height: 34px; + line-height: 32px; &:hover { color: ${colors.colorCoreDarkGray}; @@ -129,9 +127,10 @@ export const HeaderLink = styled(HeaderButton)` .filter-link { color: ${colors.colorCoreGray}; - padding: 0 12px; + padding: 0 10px; display: block; - line-height: 34px; + line-height: 32px; + span { margin-left: 0; } diff --git a/src/modules/boards/styles/item.ts b/src/modules/boards/styles/item.ts index 396334af811..668e61c9e81 100644 --- a/src/modules/boards/styles/item.ts +++ b/src/modules/boards/styles/item.ts @@ -1,5 +1,6 @@ import { SelectContainer } from 'modules/boards/styles/common'; -import { colors } from 'modules/common/styles'; +import Button from 'modules/common/components/Button'; +import { colors, dimensions } from 'modules/common/styles'; import { rgba } from 'modules/common/styles/color'; import styled from 'styled-components'; import styledTS from 'styled-components-ts'; @@ -39,7 +40,7 @@ const Footer = styled.div` `; const HeaderRow = styled(FlexContent)` - margin-bottom: 40px; + margin-bottom: 30px; `; const HeaderContent = styled.div` @@ -49,20 +50,41 @@ const HeaderContent = styled.div` const TitleRow = styled.div` display: flex; align-items: center; - font-size: 18px; + font-size: 16px; i { - margin-right: 10px; + margin-right: 8px; } label { - font-size: 15px; + font-size: 13px; text-transform: initial; } input { font-weight: bold; } + + textarea { + font-weight: bold; + border-bottom: none; + min-height: auto; + padding: 5px 0; + + &:focus { + border-bottom: 1px solid ${colors.colorSecondary}; + } + } +`; + +const MetaInfo = styled.div` + display: flex; + align-items: center; + margin-top: 5px; + + > * { + margin-right: 5px; + } `; const HeaderContentSmall = styled.div` @@ -98,22 +120,6 @@ const HeaderContentSmall = styled.div` } `; -const Button = styled.div` - padding: 7px 10px; - background: ${colors.colorWhite}; - cursor: pointer; - border-bottom: 1px solid ${colors.borderDarker}; - transition: all 0.3s ease; - - &:hover { - background: ${colors.bgLight}; - } - - i { - float: right; - } -`; - const FormFooter = styled.div` text-align: right; margin-top: 20px; @@ -129,7 +135,7 @@ const FooterContent = styled.div` `; const LeftContainer = styled.div` - margin-right: 20px; + margin-right: ${dimensions.coreSpacing}px; flex: 1; textarea { @@ -158,40 +164,47 @@ const WatchIndicator = styled.span` const RightContent = styled.div` width: 280px; flex-shrink: 0; +`; - > button { - width: 100%; - margin-bottom: 5px; - margin-left: 0; - padding: 8px 40px 8px 20px; - background: ${rgba(buttonColor, 0.04)}; - color: ${colors.textPrimary}; - text-align: left; - border-radius: ${borderRadius}; - text-transform: none; - font-size: 13px; - box-shadow: none; - position: relative; +const RightButton = styled(Button)` + width: 100%; + margin-bottom: 5px; + margin-left: 0 !important; + padding: 8px 40px 8px 20px; + background: ${rgba(buttonColor, 0.04)}; + color: ${colors.textPrimary}; + text-align: left; + border-radius: ${borderRadius}; + text-transform: none; + font-size: 13px; + box-shadow: none; + position: relative; - > i { - color: ${colors.textPrimary}; - margin-right: 5px; - } + > i { + color: ${colors.textPrimary}; + margin-right: 5px; + } - &:hover { - color: ${colors.colorCoreDarkGray}; - background: ${rgba(buttonColor, 0.08)}; - box-shadow: none; - } + &:hover { + color: ${colors.colorCoreDarkGray}; + background: ${rgba(buttonColor, 0.08)}; + box-shadow: none; } `; const MoveContainer = styled(FlexContent)` - display: flex; margin-bottom: 20px; align-items: center; `; +const ActionContainer = styled(MoveContainer)` + flex-wrap: wrap; + + > div { + margin: 0 ${dimensions.unitSpacing / 2}px ${dimensions.unitSpacing / 2}px 0; + } +`; + const MoveFormContainer = styled.div` margin-right: 20px; position: relative; @@ -341,9 +354,10 @@ export { FooterContent, HeaderRow, TitleRow, + MetaInfo, HeaderContent, HeaderContentSmall, - Button, + RightButton, MoveFormContainer, PipelineName, FormFooter, @@ -362,5 +376,6 @@ export { PriceContainer, Right, Footer, - WatchIndicator + WatchIndicator, + ActionContainer }; diff --git a/src/modules/boards/styles/stage.ts b/src/modules/boards/styles/stage.ts index 406a290e733..503991be578 100644 --- a/src/modules/boards/styles/stage.ts +++ b/src/modules/boards/styles/stage.ts @@ -60,7 +60,7 @@ const ItemIndicator = styledTS<{ color: string }>(styled.span)` height: 8px; border-radius: 4px; margin: 6px 6px 0 0; - background-color: ${props => props.color} + background-color: ${props => props.color}; `; const StageFooter = styled.div` diff --git a/src/modules/boards/types.ts b/src/modules/boards/types.ts index 0ec75c286ec..4ecd56ae2fa 100644 --- a/src/modules/boards/types.ts +++ b/src/modules/boards/types.ts @@ -66,6 +66,8 @@ export interface IPipeline { memberIds?: string[]; bgColor?: string; isWatched: boolean; + hackScoringType?: string; + templateId?: string; } interface IStageComparisonInfo { @@ -86,6 +88,7 @@ export interface IStage { inProcessDealsTotalCount: number; stayedDealsTotalCount: number; compareNextStage: IStageComparisonInfo; + formId: string; } export interface IItem { @@ -146,6 +149,11 @@ export type PipelinesQueryResponse = { refetch: ({ boardId }: { boardId?: string }) => Promise; }; +export type TemplatesQueryResponse = { + growthHackTemplates: IPipeline[]; + loading: boolean; +}; + export type StagesQueryResponse = { stages: IStage[]; loading: boolean; @@ -230,5 +238,5 @@ export interface IEditFormContent { ) => void; copy: () => void; remove: (id: string) => void; - onBlurFields: (name: 'description' | 'name', value: string) => void; + onBlurFields: (name: string, value: string) => void; } diff --git a/src/modules/common/components/Icon.tsx b/src/modules/common/components/Icon.tsx index 9e301689b7b..b0d3ed1dd42 100644 --- a/src/modules/common/components/Icon.tsx +++ b/src/modules/common/components/Icon.tsx @@ -11,21 +11,26 @@ type Props = { icon: string; size?: number; style?: any; + color?: string; isActive?: boolean; onClick?: (e: React.MouseEvent) => void; }; function Icon(props: Props) { - const { isActive } = props; + const { isActive, color } = props; - let color; + let changedColor = color || ''; if (isActive) { - color = 'black'; + changedColor = 'black'; } return ( - + ); } diff --git a/src/modules/common/components/ModalTrigger.tsx b/src/modules/common/components/ModalTrigger.tsx index de2dfd6e6e0..a6d1e49c422 100755 --- a/src/modules/common/components/ModalTrigger.tsx +++ b/src/modules/common/components/ModalTrigger.tsx @@ -2,6 +2,8 @@ import { __ } from 'modules/common/utils'; import React from 'react'; import { Modal } from 'react-bootstrap'; import RTG from 'react-transition-group'; +import { CloseModal } from '../styles/main'; +import Icon from './Icon'; type Props = { title: string; @@ -12,6 +14,8 @@ type Props = { dialogClassName?: string; backDrop?: string; enforceFocus?: boolean; + hideHeader?: boolean; + isOpen?: boolean; }; type State = { @@ -22,7 +26,7 @@ class ModalTrigger extends React.Component { constructor(props) { super(props); - this.state = { isOpen: false }; + this.state = { isOpen: props.isOpen || false }; } openModal = () => { @@ -33,12 +37,28 @@ class ModalTrigger extends React.Component { this.setState({ isOpen: false }); }; + renderHeader = () => { + if (this.props.hideHeader) { + return ( + + + + ); + } + + const { title, ignoreTrans } = this.props; + + return ( + + {ignoreTrans ? title : __(title)} + + ); + }; + render() { const { - title, trigger, size, - ignoreTrans, dialogClassName, content, backDrop, @@ -67,9 +87,7 @@ class ModalTrigger extends React.Component { backdrop={backDrop} enforceFocus={enforceFocus} > - - {ignoreTrans ? title : __(title)} - + {this.renderHeader()} {content({ closeModal: this.closeModal })} diff --git a/src/modules/common/components/SortableList.tsx b/src/modules/common/components/SortableList.tsx index d4b4c4a843b..0edf167b6ec 100644 --- a/src/modules/common/components/SortableList.tsx +++ b/src/modules/common/components/SortableList.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragHandler, SortableWrapper, SortItem } from '../styles/sort'; import { reorder } from '../utils'; +import EmptyState from './EmptyState'; type Props = { fields: any[]; @@ -11,9 +12,14 @@ type Props = { isModal?: boolean; showDragHandler?: boolean | true; isDragDisabled?: boolean; + droppableId?: string; }; class SortableList extends React.Component { + static defaultProps = { + droppableId: 'droppableId' + }; + onDragEnd = result => { const { destination, source } = result; @@ -42,56 +48,49 @@ class SortableList extends React.Component { return ( - + ); } - renderField(field, index) { - const { child, isModal, isDragDisabled } = this.props; - - return ( - - {(provided, snapshot) => ( - <> - - {this.renderDragHandler()} - - {child(field)} - - {provided.placeholder} - - )} - - ); - } - - renderFields(provided) { - const { fields } = this.props; + render() { + const { fields, child, isDragDisabled, droppableId } = this.props; - return ( - - {fields.map((field, index) => this.renderField(field, index))} - - ); - } + if (fields.length === 0) { + return ; + } - render() { return ( - - {provided => this.renderFields(provided)} + + {provided => ( + + {fields.map((field, index) => ( + + {(dragProvided, snapshot) => ( + + {this.renderDragHandler()} + {child(field)} + + )} + + ))} + {provided.placeholder} + + )} ); diff --git a/src/modules/common/components/Spinner.tsx b/src/modules/common/components/Spinner.tsx index bc7fc5482de..5d4869bf17b 100755 --- a/src/modules/common/components/Spinner.tsx +++ b/src/modules/common/components/Spinner.tsx @@ -27,7 +27,7 @@ export const MainLoader = styledTS(styled.div)` width: ${props => props.size}px; height: ${props => props.size}px; margin-left: -${props => props.size}px; - margin-top: -${props => props.size}px; + margin-top: -${props => props.size && props.size / 2}px; animation: ${rotate} 0.75s linear infinite; border: 2px solid ${colors.borderDarker}; border-top-color: ${colors.colorSecondary}; diff --git a/src/modules/common/components/filterableList/styles.ts b/src/modules/common/components/filterableList/styles.ts index eeac157d945..99a4cd02375 100644 --- a/src/modules/common/components/filterableList/styles.ts +++ b/src/modules/common/components/filterableList/styles.ts @@ -46,7 +46,7 @@ const PopoverList = styledTS<{ selectable?: boolean }>(styled.ul)` &:before { font-family: 'erxes'; - font-size: ${dimensions.unitSpacing}px; + font-size: 12px; width: 15px; height: 15px; z-index: 30; @@ -59,11 +59,11 @@ const PopoverList = styledTS<{ selectable?: boolean }>(styled.ul)` } &.all:before { - content: '\\e80f'; + content: '\\ea3f'; } &.some:before { - content: '\\e856'; + content: '\\ebe8'; } } `; @@ -85,10 +85,10 @@ const PopoverFooter = styled.div` `; const AvatarImg = styled.img` - width: ${dimensions.coreSpacing + 10}px; - height: ${dimensions.coreSpacing + 10}px; - line-height: ${dimensions.coreSpacing + 10}px; - border-radius: ${(dimensions.coreSpacing + 10) / 2}px; + width: ${dimensions.coreSpacing + 6}px; + height: ${dimensions.coreSpacing + 6}px; + line-height: ${dimensions.coreSpacing + 6}px; + border-radius: ${(dimensions.coreSpacing + 6) / 2}px; vertical-align: middle; background: ${colors.bgActive}; margin-right: ${dimensions.unitSpacing}px; diff --git a/src/modules/common/components/form/styles.tsx b/src/modules/common/components/form/styles.tsx index 5597e9e6f76..71207778a2d 100644 --- a/src/modules/common/components/form/styles.tsx +++ b/src/modules/common/components/form/styles.tsx @@ -297,6 +297,6 @@ export { FormLabel, Label, Formgroup, - Error, - FlexWrapper + FlexWrapper, + Error }; diff --git a/src/modules/common/components/step/styles.ts b/src/modules/common/components/step/styles.ts index b8c878aa005..05853b4a4a8 100644 --- a/src/modules/common/components/step/styles.ts +++ b/src/modules/common/components/step/styles.ts @@ -157,6 +157,9 @@ const FlexItem = styledTS<{ const FlexPad = styled(FlexItem)` padding: ${dimensions.coreSpacing}px; + flex: 1; + border-right: ${colors.borderPrimary}; + padding: ${dimensions.coreSpacing}px; `; const LeftItem = styledTS<{ deactive?: boolean }>(styled.div)` diff --git a/src/modules/common/styles/colors.ts b/src/modules/common/styles/colors.ts index 87878099f04..6b71554ee46 100644 --- a/src/modules/common/styles/colors.ts +++ b/src/modules/common/styles/colors.ts @@ -43,7 +43,7 @@ const textPrimary = '#444'; const textSecondary = rgba(textPrimary, 0.8); // Shadow colors -const shadowPrimary = rgba(colorShadowGray, 0.7); +const shadowPrimary = rgba(colorBlack, 0.08); const darkShadow = rgba(colorBlack, 0.2); // Social colors diff --git a/src/modules/common/styles/global-styles.ts b/src/modules/common/styles/global-styles.ts index 34f0f0fc046..1e6b301ed14 100644 --- a/src/modules/common/styles/global-styles.ts +++ b/src/modules/common/styles/global-styles.ts @@ -79,6 +79,10 @@ a:hover { width: 85%; } +.modal-1000w { + width: 1000px; +} + .modal-content { border-radius: 2px; border: 0; @@ -189,7 +193,8 @@ a:hover { display: block; } -.dropdown-menu li a { +.dropdown-menu li a, +.dropdown-menu li button { display: block; padding: 3px 20px; color: ${colors.textPrimary}; @@ -206,7 +211,9 @@ a:hover { .dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover, .dropdown-menu li a:focus, -.dropdown-menu li a:hover { +.dropdown-menu li a:hover, +.dropdown-menu li button:focus, +.dropdown-menu li button:hover { color: ${colors.colorCoreDarkGray}; background: ${colors.bgActive}; outline: 0; @@ -565,9 +572,9 @@ a:hover { /* react datetime */ .rdtPicker { - box-shadow: 0 5px 15px -3px rgba(0, 0, 0, 0.15); + box-shadow: 0 5px 15px -3px rgba(0, 0, 0, 0.15) !important; width: 100%; - border-color: ${colors.colorShadowGray}; + border: none !important; min-width: 220px; max-width: 260px; } diff --git a/src/modules/common/styles/main.ts b/src/modules/common/styles/main.ts index 5148984c5ff..a3f78c2d6b8 100644 --- a/src/modules/common/styles/main.ts +++ b/src/modules/common/styles/main.ts @@ -19,15 +19,20 @@ const MiddleContent = styledTS<{ transparent?: boolean }>(styled.div)` margin: 10px 0; `; -const BoxRoot = styled.div` +const BoxRoot = styledTS<{ selected?: boolean }>(styled.div)` text-align: center; float: left; background: ${colors.colorLightBlue}; - box-shadow: 0 6px 10px 1px ${rgba(colors.colorCoreGray, 0.08)}; + box-shadow: ${props => + props.selected + ? `0 10px 20px ${rgba(colors.colorCoreDarkGray, 0.12)}` + : `0 6px 10px 1px ${rgba(colors.colorCoreGray, 0.08)}`} ; margin-right: ${dimensions.coreSpacing}px; margin-bottom: ${dimensions.coreSpacing}px; border-radius: ${dimensions.unitSpacing / 2 - 1}px; transition: all 0.25s ease; + border: 1px solid + ${props => (props.selected ? colors.colorSecondary : colors.borderPrimary)}; > a { display: block; @@ -160,6 +165,23 @@ const DropIcon = styledTS<{ isOpen: boolean }>(styled.span)` } `; +const CloseModal = styled.div` + position: absolute; + right: -40px; + width: 30px; + height: 30px; + background: ${rgba(colors.colorBlack, 0.3)}; + line-height: 30px; + border-radius: 15px; + text-align: center; + color: ${colors.colorWhite}; + + &:hover { + background: ${rgba(colors.colorBlack, 0.4)}; + cursor: pointer; + } +`; + export { BoxRoot, FullContent, @@ -172,5 +194,6 @@ export { CenterContent, ActivityContent, DropIcon, - MiddleContent + MiddleContent, + CloseModal }; diff --git a/src/modules/common/styles/sort.ts b/src/modules/common/styles/sort.ts index fc7e63843f1..5cb9f76b320 100644 --- a/src/modules/common/styles/sort.ts +++ b/src/modules/common/styles/sort.ts @@ -6,28 +6,32 @@ const SortItem = styledTS<{ isDragging: boolean; isModal: boolean }>( styled.div )` background: ${colors.colorWhite}; - border: 1px solid ${colors.borderPrimary}; - border-radius: 2px; display: block; - padding: 10px 15px; - margin-bottom: 5px; + padding: 5px; + margin-bottom: 10px; z-index: 2000; position: relative; display: flex; justify-content: space-between; + border-left: 2px solid transparent; box-shadow: ${props => props.isDragging ? `0 2px 8px ${colors.shadowPrimary}` : 'none'}; left: ${props => props.isDragging && props.isModal ? '40px!important' : 'auto'}; + &:last-child { - margin: 0; + margin-bottom: 0; + } + + &:hover { + box-shadow: 0 2px 8px ${colors.shadowPrimary}; + border-color: ${colors.colorSecondary}; } `; const SortableWrapper = styled.div` width: 100%; - max-height: 420px; - overflow: auto; + flex: 1; label { margin: 0; @@ -37,9 +41,10 @@ const SortableWrapper = styled.div` const DragHandler = styled.div` display: flex; width: 20px; - margin-right: 10px; + margin-right: 5px; align-items: center; justify-content: center; + margin-top: 2px; i { color: ${colors.colorLightGray}; diff --git a/src/modules/companies/components/list/CompaniesList.tsx b/src/modules/companies/components/list/CompaniesList.tsx index bc4ea79172a..8dc88323f71 100755 --- a/src/modules/companies/components/list/CompaniesList.tsx +++ b/src/modules/companies/components/list/CompaniesList.tsx @@ -254,7 +254,6 @@ class CompaniesList extends React.Component { title="Manage Columns" trigger={editColumns} content={manageColumns} - dialogClassName="transform" />
  • diff --git a/src/modules/customers/components/list/CustomersList.tsx b/src/modules/customers/components/list/CustomersList.tsx index 8553bb1ffe3..5527be643a9 100755 --- a/src/modules/customers/components/list/CustomersList.tsx +++ b/src/modules/customers/components/list/CustomersList.tsx @@ -226,7 +226,6 @@ class CustomersList extends React.Component { title="Manage Columns" trigger={editColumns} content={manageColumns} - dialogClassName="transform" />
  • diff --git a/src/modules/customers/components/list/FormFilter.tsx b/src/modules/customers/components/list/LeadFilter.tsx similarity index 87% rename from src/modules/customers/components/list/FormFilter.tsx rename to src/modules/customers/components/list/LeadFilter.tsx index eab9e7aff56..f880a3b8818 100644 --- a/src/modules/customers/components/list/FormFilter.tsx +++ b/src/modules/customers/components/list/LeadFilter.tsx @@ -13,7 +13,7 @@ interface IProps extends IRouterProps { loading: boolean; } -function Forms({ history, counts, integrations, loading }: IProps) { +function Leads({ history, counts, integrations, loading }: IProps) { const { Section, Header } = Wrapper.Sidebar; const onClick = formId => { @@ -46,13 +46,13 @@ function Forms({ history, counts, integrations, loading }: IProps) { return (
    5}> -
    {__('Filter by form')}
    +
    {__('Filter by lead')}
    (Forms); +export default withRouter(Leads); diff --git a/src/modules/customers/components/list/Sidebar.tsx b/src/modules/customers/components/list/Sidebar.tsx index 7febc9f1842..0267c1c3100 100755 --- a/src/modules/customers/components/list/Sidebar.tsx +++ b/src/modules/customers/components/list/Sidebar.tsx @@ -1,8 +1,8 @@ import Wrapper from 'modules/layout/components/Wrapper'; import React from 'react'; import BrandFilter from '../../containers/filters/BrandFilter'; -import FormFilter from '../../containers/filters/FormFilter'; import IntegrationFilter from '../../containers/filters/IntegrationFilter'; +import LeadFilter from '../../containers/filters/LeadFilter'; import LeadStatusFilter from '../../containers/filters/LeadStatusFilter'; import LifecycleStateFilter from '../../containers/filters/LifecycleStateFilter'; import SegmentFilter from '../../containers/filters/SegmentFilter'; @@ -15,7 +15,7 @@ function Sidebar({ loadingMainQuery }: { loadingMainQuery: boolean }) { - + diff --git a/src/modules/customers/containers/filters/FormFilter.tsx b/src/modules/customers/containers/filters/LeadFilter.tsx similarity index 88% rename from src/modules/customers/containers/filters/FormFilter.tsx rename to src/modules/customers/containers/filters/LeadFilter.tsx index 01d5ec45d18..f108a2bdbbd 100644 --- a/src/modules/customers/containers/filters/FormFilter.tsx +++ b/src/modules/customers/containers/filters/LeadFilter.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { compose, graphql } from 'react-apollo'; import { withProps } from '../../../common/utils'; import { IntegrationsQueryResponse } from '../../../settings/integrations/types'; -import FormFilter from '../../components/list/FormFilter'; +import LeadFilter from '../../components/list/LeadFilter'; import { queries } from '../../graphql'; import { CountQueryResponse } from '../../types'; @@ -12,7 +12,7 @@ type Props = { customersCountQuery?: CountQueryResponse; }; -const FormFilterContainer = (props: Props) => { +const LeadFilterContainer = (props: Props) => { const { integrationsQuery, customersCountQuery } = props; const counts = (customersCountQuery @@ -28,7 +28,7 @@ const FormFilterContainer = (props: Props) => { loading: integrationsQuery ? integrationsQuery.loading : false }; - return ; + return ; }; export default withProps<{ loadingMainQuery: boolean }>( @@ -36,7 +36,7 @@ export default withProps<{ loadingMainQuery: boolean }>( graphql<{ loadingMainQuery: boolean }, IntegrationsQueryResponse, {}>( gql` query integrations { - integrations(kind: "form") { + integrations(kind: "lead") { _id name form { @@ -61,5 +61,5 @@ export default withProps<{ loadingMainQuery: boolean }>( variables: { only: 'byForm' } } }) - )(FormFilterContainer) + )(LeadFilterContainer) ); diff --git a/src/modules/customers/containers/filters/index.ts b/src/modules/customers/containers/filters/index.ts index 043524d07d0..08f79487805 100644 --- a/src/modules/customers/containers/filters/index.ts +++ b/src/modules/customers/containers/filters/index.ts @@ -1,6 +1,6 @@ import BrandFilter from './BrandFilter'; -import FormFilter from './FormFilter'; import IntegrationFilter from './IntegrationFilter'; +import LeadFilter from './LeadFilter'; import LeadStatusFilter from './LeadStatusFilter'; import LifecycleStateFilter from './LifecycleStateFilter'; import SegmentFilter from './SegmentFilter'; @@ -8,7 +8,7 @@ import TagFilter from './TagFilter'; export { BrandFilter, - FormFilter, + LeadFilter, SegmentFilter, LeadStatusFilter, LifecycleStateFilter, diff --git a/src/modules/deals/components/DealItem.tsx b/src/modules/deals/components/DealItem.tsx index e3147569e39..968cfc4741f 100644 --- a/src/modules/deals/components/DealItem.tsx +++ b/src/modules/deals/components/DealItem.tsx @@ -37,6 +37,7 @@ class DealItem extends React.PureComponent { options={options} stageId={stageId} itemId={item._id} + hideHeader={true} /> ); }; diff --git a/src/modules/deals/components/DealMainActionBar.tsx b/src/modules/deals/components/DealMainActionBar.tsx index c06daf755ed..64b42c55680 100644 --- a/src/modules/deals/components/DealMainActionBar.tsx +++ b/src/modules/deals/components/DealMainActionBar.tsx @@ -4,6 +4,8 @@ import { IBoard, IPipeline } from 'modules/boards/types'; import Icon from 'modules/common/components/Icon'; import Tip from 'modules/common/components/Tip'; import { __ } from 'modules/common/utils'; +import SelectCompanies from 'modules/companies/containers/SelectCompanies'; +import SelectCustomers from 'modules/customers/containers/common/SelectCustomers'; import SelectProducts from 'modules/settings/productService/containers/SelectProducts'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -80,12 +82,26 @@ const DealMainActionBar = (props: Props) => { }; const extraFilter = ( - + <> + + + + ); const extendedProps = { diff --git a/src/modules/deals/components/product/ProductItemForm.tsx b/src/modules/deals/components/product/ProductItemForm.tsx index 2c484a9c417..1bccd14e306 100644 --- a/src/modules/deals/components/product/ProductItemForm.tsx +++ b/src/modules/deals/components/product/ProductItemForm.tsx @@ -1,4 +1,3 @@ -import { Button as DealButton } from 'modules/boards/styles/item'; import Button from 'modules/common/components/Button'; import FormControl from 'modules/common/components/form/Control'; import ControlLabel from 'modules/common/components/form/Label'; @@ -15,6 +14,7 @@ import { ContentColumn, ContentRow, ItemText, + ProductButton, ProductItem, TotalAmount } from '../../styles'; @@ -109,7 +109,7 @@ class ProductItemForm extends React.Component { ); } - return {content}; + return {content}; } renderProductModal(productData: IProductData) { diff --git a/src/modules/deals/styles.ts b/src/modules/deals/styles.ts index 3afa6443f30..6c63b3619b4 100644 --- a/src/modules/deals/styles.ts +++ b/src/modules/deals/styles.ts @@ -89,6 +89,22 @@ const ItemText = styledTS<{ align?: string }>(styled.div)` } `; +const ProductButton = styled.div` + padding: 7px 10px; + background: ${colors.colorWhite}; + cursor: pointer; + border-bottom: 1px solid ${colors.borderDarker}; + transition: all 0.3s ease; + + &:hover { + background: ${colors.bgLight}; + } + + i { + float: right; + } +`; + export { FormContainer, FooterInfo, @@ -97,5 +113,6 @@ export { ProductItem, ContentRow, ContentColumn, - TotalAmount + TotalAmount, + ProductButton }; diff --git a/src/modules/engage/components/step/ChannelStep.tsx b/src/modules/engage/components/step/ChannelStep.tsx index f5c693e3c6d..c64e5dcc1fd 100644 --- a/src/modules/engage/components/step/ChannelStep.tsx +++ b/src/modules/engage/components/step/ChannelStep.tsx @@ -3,15 +3,11 @@ import { colors } from 'modules/common/styles'; import { BoxRoot, FullContent } from 'modules/common/styles/main'; import { __ } from 'modules/common/utils'; import { METHODS } from 'modules/engage/constants'; -import styledTS from 'styled-components-ts'; - import React from 'react'; import styled from 'styled-components'; -const Box = styledTS<{ selected: boolean }>(styled(BoxRoot))` +const Box = styled(BoxRoot)` width: 320px; - border: 1px solid - ${props => (props.selected ? colors.colorSecondary : colors.borderPrimary)}; padding: 40px; background: ${colors.bgLight}; diff --git a/src/modules/forms/components/FieldChoices.tsx b/src/modules/forms/components/FieldChoices.tsx new file mode 100644 index 00000000000..a0f7532fd9e --- /dev/null +++ b/src/modules/forms/components/FieldChoices.tsx @@ -0,0 +1,82 @@ +import Icon from 'modules/common/components/Icon'; +import { __ } from 'modules/common/utils'; +import React from 'react'; +import { FieldWrapper, Options } from '../styles'; + +type Props = { + onChoiceClick: (choice: string) => void; +}; + +type FieldProps = { + icon: string; + type: string; + text: string; +}; + +function FieldChoice(props: Props & FieldProps) { + const { icon, type, text, onChoiceClick } = props; + + const onClick = () => { + onChoiceClick(type); + }; + + return ( + + + {text || type} + + ); +} + +function FieldChoices(props: Props) { + return ( + + + + + + + + + + + + ); +} + +export default FieldChoices; diff --git a/src/modules/forms/components/FieldForm.tsx b/src/modules/forms/components/FieldForm.tsx new file mode 100644 index 00000000000..3595370931b --- /dev/null +++ b/src/modules/forms/components/FieldForm.tsx @@ -0,0 +1,262 @@ +import Button from 'modules/common/components/Button'; +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 Icon from 'modules/common/components/Icon'; +import { FlexItem } from 'modules/common/components/step/styles'; +import { __ } from 'modules/common/utils'; +import { IField } from 'modules/settings/properties/types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import Toggle from 'react-toggle'; +import { + FlexRow, + LeftSection, + Preview, + PreviewSection, + ShowPreview +} from '../styles'; +import FieldPreview from './FieldPreview'; + +type Props = { + onSubmit: (field: IField) => void; + onDelete: (field: IField) => void; + onCancel: () => void; + mode: 'create' | 'update'; + field: IField; +}; + +type State = { + field: IField; +}; + +class FieldForm extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + field: props.field + }; + } + + onFieldChange = (name: string, value: string | boolean | string[]) => { + this.setFieldAttrChanges(name, value); + }; + + onSubmit = e => { + e.persist(); + + const { field } = this.state; + const { onSubmit } = this.props; + + onSubmit(field); + }; + + setFieldAttrChanges( + attributeName: string, + value: string | boolean | string[] + ) { + const { field } = this.state; + + field[attributeName] = value; + + this.setState({ field }); + } + + renderValidation() { + const { field } = this.state; + + const validation = e => + this.onFieldChange( + 'validation', + (e.currentTarget as HTMLInputElement).value + ); + + return ( + + Validation: + + + + + + + + + ); + } + + renderOptions() { + const { field } = this.state; + + const onChange = e => + this.onFieldChange( + 'options', + (e.currentTarget as HTMLInputElement).value.split('\n') + ); + + if (!['select', 'check', 'radio'].includes(field.type)) { + return null; + } + + return ( + + Options: + + + + ); + } + + renderExtraButton() { + const { mode, field } = this.props; + + if (mode === 'create') { + return null; + } + + const onDelete = e => { + e.preventDefault(); + + this.props.onDelete(field); + }; + + return ( + + ); + } + + renderLeftContent() { + const { mode, onCancel } = this.props; + const { field } = this.state; + + const text = e => + this.onFieldChange('text', (e.currentTarget as HTMLInputElement).value); + + const desc = e => + this.onFieldChange( + 'description', + (e.currentTarget as HTMLInputElement).value + ); + + const toggle = e => + this.onFieldChange( + 'isRequired', + (e.currentTarget as HTMLInputElement).checked + ); + + return ( + <> + {this.renderValidation()} + + + + Field Label + + + + + + Field description + + + + {this.renderOptions()} + + + + Yes, + unchecked: No + }} + onChange={toggle} + /> + + + + + + {this.renderExtraButton()} + + + + + ); + } + + renderContent() { + const { field } = this.state; + + return ( + + {this.renderLeftContent()} + + + + + + + Field preview + + + + + ); + } + + render() { + const { mode, field, onCancel } = this.props; + + return ( + + + + {mode === 'create' ? 'Add' : 'Edit'} {field.type} field + + + {this.renderContent()} + + ); + } +} + +export default FieldForm; diff --git a/src/modules/forms/components/step/preview/FieldPreview.tsx b/src/modules/forms/components/FieldPreview.tsx similarity index 57% rename from src/modules/forms/components/step/preview/FieldPreview.tsx rename to src/modules/forms/components/FieldPreview.tsx index e8f6dc350f5..f24b93fff81 100644 --- a/src/modules/forms/components/step/preview/FieldPreview.tsx +++ b/src/modules/forms/components/FieldPreview.tsx @@ -1,29 +1,28 @@ import GenerateField from 'modules/settings/properties/components/GenerateField'; +import { IField } from 'modules/settings/properties/types'; import React from 'react'; -import { IField } from '../../../../settings/properties/types'; -import { FieldItem } from './styles'; +import { FieldItem } from '../styles'; type Props = { field: IField; - onEdit?: (field: IField) => void; + onClick?: (field: IField) => void; }; class FieldPreview extends React.Component { - onEdit = () => { - const { onEdit } = this.props; - - if (onEdit) { - onEdit(this.props.field); - } - }; render() { - const { field } = this.props; + const { field, onClick } = this.props; + + const onClickItem = () => { + if (onClick) { + onClick(field); + } + }; return ( diff --git a/src/modules/forms/components/step/preview/FormFieldPreview.tsx b/src/modules/forms/components/FieldsPreview.tsx similarity index 64% rename from src/modules/forms/components/step/preview/FormFieldPreview.tsx rename to src/modules/forms/components/FieldsPreview.tsx index e3f2ea147e5..610f712afd9 100644 --- a/src/modules/forms/components/step/preview/FormFieldPreview.tsx +++ b/src/modules/forms/components/FieldsPreview.tsx @@ -4,22 +4,22 @@ import React from 'react'; import FieldPreview from './FieldPreview'; type Props = { - fields?: IField[]; + fields: IField[]; formDesc?: string; - onFieldEdit?: (field: IField) => void; - onChange: (name: string, fields: any) => void; + onFieldClick?: (field: IField) => void; + onChangeFieldsOrder?: (fields: IField[]) => void; }; type State = { fields?: IField[]; }; -class FormFieldPreview extends React.Component { +class FieldsPreview extends React.Component { constructor(props: Props) { super(props); this.state = { - fields: props.fields + fields: [...props.fields] }; } @@ -34,6 +34,8 @@ class FormFieldPreview extends React.Component { onChangeFields = (reOrderedFields: IField[]) => { const fields: IField[] = []; + const { onChangeFieldsOrder } = this.props; + reOrderedFields.forEach((field, index) => { fields.push({ ...field, @@ -41,41 +43,47 @@ class FormFieldPreview extends React.Component { }); }); - this.setState({ fields }); - - this.props.onChange('fields', this.state.fields || []); + this.setState({ fields }, () => { + if (onChangeFieldsOrder) { + onChangeFieldsOrder(this.state.fields || []); + } + }); }; renderFormDesc() { - if (!this.props.formDesc) { + const { formDesc } = this.props; + + if (!formDesc) { return null; } - return

    {this.props.formDesc}

    ; + return

    {formDesc}

    ; } render() { const child = field => { return ( ); }; return ( - + <> {this.renderFormDesc()} + - + ); } } -export default FormFieldPreview; +export default FieldsPreview; diff --git a/src/modules/forms/components/Form.tsx b/src/modules/forms/components/Form.tsx index 8e0b98b1319..5971687d61c 100644 --- a/src/modules/forms/components/Form.tsx +++ b/src/modules/forms/components/Form.tsx @@ -1,348 +1,205 @@ -import Button from 'modules/common/components/Button'; import FormControl from 'modules/common/components/form/Control'; -import ConditionsRule from 'modules/common/components/rule/ConditionsRule'; -import { Step, Steps } from 'modules/common/components/step'; -import { - StepWrapper, - TitleContainer -} from 'modules/common/components/step/styles'; -import { IConditionsRule } from 'modules/common/types'; -import { Alert } from 'modules/common/utils'; +import ControlLabel from 'modules/common/components/form/Label'; +import { LeftItem } from 'modules/common/components/step/styles'; import { __ } from 'modules/common/utils'; -import { IFormIntegration } from 'modules/forms/types'; -import Wrapper from 'modules/layout/components/Wrapper'; -import { IFormData } from 'modules/settings/integrations/types'; +import { FlexContent } from 'modules/layout/styles'; import { IField } from 'modules/settings/properties/types'; - import React from 'react'; -import { Link } from 'react-router-dom'; - -import { ImportLoader } from 'modules/common/components/ButtonMutate'; -import { - CallOut, - ChooseType, - FormStep, - FullPreviewStep, - OptionStep, - SuccessStep -} from './step'; +import FormGroup from '../../common/components/form/Group'; +import { Title } from '../styles'; +import { IForm, IFormData } from '../types'; +import FieldChoices from './FieldChoices'; +import FieldForm from './FieldForm'; +import FieldsPreview from './FieldsPreview'; type Props = { - integration?: IFormIntegration; fields: IField[]; - loading?: boolean; - isActionLoading: boolean; - save: ( - params: { - name: string; - brandId: string; - languageCode?: string; - formData: IFormData; - form: any; - fields?: IField[]; - } - ) => void; + renderPreviewWrapper: (previewRenderer, fields: IField[]) => void; + onDocChange?: (doc: IFormData) => void; + saveForm: (params: IFormData) => void; + isReadyToSave: boolean; + type: string; + form?: IForm; + hideOptionalFields?: boolean; }; type State = { - activeStep?: number; - type: string; - brand?: string; - language?: string; - title?: string; - calloutTitle?: string; - formTitle?: string; - bodyValue?: string; - formDesc?: string; - formBtnText?: string; - calloutBtnText?: string; - theme: string; - logoPreviewUrl?: string; - fields?: IField[]; - isSkip?: boolean; - color: string; - logoPreviewStyle?: { opacity?: string }; - defaultValue: { [key: string]: boolean }; - logo?: string; - rules?: IConditionsRule[]; - - successAction?: string; - fromEmail?: string; - userEmailTitle?: string; - userEmailContent?: string; - adminEmails?: string[]; - adminEmailTitle?: string; - adminEmailContent?: string; - thankContent?: string; - redirectUrl?: string; - carousel?: string; + fields: IField[]; + currentMode: 'create' | 'update' | undefined; + currentField?: IField; + title: string; + desc: string; + btnText: string; }; class Form extends React.Component { constructor(props: Props) { super(props); - const integration = props.integration || ({} as IFormIntegration); - - const formData = integration.formData || ({} as IFormData); - const form = integration.form || {}; - const callout = form.callout || {}; - const fields = props.fields; + const { form = {} as IForm } = props; this.state = { - activeStep: 1, - - type: formData.loadType || 'shoutbox', - successAction: formData.successAction || '', - fromEmail: formData.fromEmail || '', - userEmailTitle: formData.userEmailTitle || '', - userEmailContent: formData.userEmailContent || '', - adminEmails: formData.adminEmails || [], - adminEmailTitle: formData.adminEmailTitle || '', - adminEmailContent: formData.adminEmailContent || '', - thankContent: formData.thankContent || 'Thank you.', - redirectUrl: formData.redirectUrl || '', - rules: integration.form ? integration.form.rules : [], - - brand: integration.brandId, - language: integration.languageCode, - title: integration.name, - calloutTitle: callout.title || 'Title', - formTitle: form.title || '', - bodyValue: callout.body || '', - formDesc: form.description || '', - formBtnText: form.buttonText || 'Send', - calloutBtnText: callout.buttonText || 'Start', - color: '', - logoPreviewStyle: {}, - defaultValue: {}, - logo: '', - theme: form.themeColor || '#6569DF', - logoPreviewUrl: callout.featuredImage, - fields: fields || [], - isSkip: callout.skip && true + fields: props.fields || [], + title: form.title || '', + desc: form.description || '', + btnText: form.buttonText || 'Send', + currentMode: undefined, + currentField: undefined }; } - handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const { brand, calloutTitle, title, rules } = this.state; + componentWillReceiveProps(nextProps: Props) { + const { saveForm, type, isReadyToSave } = this.props; + const { title, btnText, desc, fields } = this.state; + + if (nextProps.isReadyToSave && isReadyToSave !== nextProps.isReadyToSave) { + saveForm({ + title, + desc, + btnText, + fields, + type + }); + } + } - if (!title) { - return Alert.error('Write title'); + renderOptionalFields = () => { + if (this.props.hideOptionalFields) { + return null; } - if (!brand) { - return Alert.error('Choose a brand'); + const { onDocChange } = this.props; + const { title, btnText, desc } = this.state; + + const onChangeField = e => { + const name: keyof State = e.target.name; + const value = (e.currentTarget as HTMLInputElement).value; + + this.setState({ [name]: value } as any, () => { + if (onDocChange) { + onDocChange(this.state); + } + }); + }; + + return ( + <> + + {__('Form title')} + + + + + {__('Form description')} + + + + + {__('Form button text')} + + + + ); + }; + + onChoiceClick = (choice: string) => { + this.setState({ + currentMode: 'create', + currentField: { + _id: Math.random().toString(), + contentType: 'form', + type: choice + } + }); + }; + + onFieldClick = (field: IField) => { + this.setState({ currentMode: 'update', currentField: field }); + }; + + onFieldSubmit = (field: IField) => { + const { onDocChange } = this.props; + const { fields, currentMode } = this.state; + + let selector = { fields, currentField: undefined }; + + if (currentMode === 'create') { + selector = { + fields: [...fields, field], + currentField: undefined + }; } - this.props.save({ - name: title, - brandId: brand, - languageCode: this.state.language, - formData: { - loadType: this.state.type, - successAction: this.state.successAction, - fromEmail: this.state.fromEmail, - userEmailTitle: this.state.userEmailTitle, - userEmailContent: this.state.userEmailContent, - adminEmails: this.state.adminEmails, - adminEmailTitle: this.state.adminEmailTitle, - adminEmailContent: this.state.adminEmailContent, - thankContent: this.state.thankContent, - redirectUrl: this.state.redirectUrl - }, - form: { - title: this.state.formTitle, - description: this.state.formDesc, - buttonText: this.state.formBtnText, - themeColor: this.state.theme || this.state.color, - callout: { - title: calloutTitle, - body: this.state.bodyValue, - buttonText: this.state.calloutBtnText, - featuredImage: this.state.logoPreviewUrl, - skip: this.state.isSkip - }, - rules: (rules || []).map(rule => ({ - _id: rule._id, - kind: rule.kind, - text: rule.text, - condition: rule.condition, - value: rule.value - })) - }, - fields: this.state.fields + this.setState(selector, () => { + if (onDocChange) { + onDocChange(this.state); + } }); }; - renderSaveButton = () => { - const { isActionLoading } = this.props; + onFieldDelete = (field: IField) => { + // remove field from state + const fields = this.state.fields.filter(f => f._id !== field._id); - const cancelButton = ( - - - - ); + this.setState({ fields, currentField: undefined }); + }; - return ( - - {cancelButton} - - - ); + onFieldFormCancel = () => { + this.setState({ currentField: undefined }); }; - onChange = (key: T, value: State[T]) => { - this.setState({ [key]: value } as Pick); + onChangeFieldsOrder = fields => { + this.setState({ fields }); }; render() { - const { - activeStep, - calloutTitle, - formTitle, - type, - calloutBtnText, - bodyValue, - formDesc, - color, - theme, - logoPreviewUrl, - thankContent, - fields, - carousel, - language, - title, - successAction, - formBtnText, - isSkip, - rules - } = this.state; - - const { integration } = this.props; - - const formData = integration && integration.formData; - const brand = integration && integration.brand; - const breadcrumb = [{ title: __('Leads'), link: '/forms' }]; - const constant = isSkip ? 'form' : 'callout'; - - const onChange = e => - this.onChange('title', (e.currentTarget as HTMLInputElement).value); + const { renderPreviewWrapper } = this.props; + const { currentMode, currentField, fields, desc } = this.state; + + if (currentField) { + return ( + + ); + } + + const renderer = () => { + return ( + + ); + }; return ( - - - -
    {__('Title')}
    - - {this.renderSaveButton()} -
    - - - - - - - - - - - - - - - - - - - - - - - -
    + + + {this.renderOptionalFields()} + + {__('New field')} + + + + + {renderPreviewWrapper(renderer, fields)} + ); } } diff --git a/src/modules/forms/components/step/FormStep.tsx b/src/modules/forms/components/step/FormStep.tsx deleted file mode 100644 index 2e11ae5f615..00000000000 --- a/src/modules/forms/components/step/FormStep.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import Button from 'modules/common/components/Button'; -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 { LeftItem, Preview } from 'modules/common/components/step/styles'; -import { __ } from 'modules/common/utils'; -import ActionBar from 'modules/layout/components/ActionBar'; -import { IField } from 'modules/settings/properties/types'; -import React from 'react'; -import FormPreview from './preview/FormPreview'; -import { FlexColumn, FlexItem } from './style'; - -type Props = { - type: string; - formTitle?: string; - formBtnText?: string; - formDesc?: string; - color: string; - theme: string; - onChange: ( - name: 'fields' | 'formTitle' | 'formDesc' | 'formBtnText' | 'type', - fields: IField[] | string - ) => void; - fields?: IField[]; -}; - -type State = { - fields?: IField[]; - chosenFieldType?: string; - editingField?: IField; -}; - -class FormStep extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - fields: props.fields, - chosenFieldType: '', - editingField: undefined - }; - } - - componentWillReceiveProps(nextProps: Props) { - this.setState({ fields: nextProps.fields }); - } - - onChangeType = (e: React.FormEvent) => { - this.setState({ - chosenFieldType: (e.currentTarget as HTMLInputElement).value - }); - this.setFieldAttrChanges( - 'type', - (e.currentTarget as HTMLInputElement).value - ); - }; - - onFieldEdit = (field: IField) => { - this.setState({ editingField: field }); - }; - - onFieldAttrChange = (name: string, value: string | boolean | string[]) => { - this.setFieldAttrChanges(name, value); - }; - - onChangeState = (name: any, value: string) => { - this.setState({ [name]: value }); - this.props.onChange(name, value); - }; - - onSubmit = e => { - e.preventDefault(); - - const editingField = this.state.editingField || ({} as IField); - - const doc = { - contentType: 'form', - type: editingField.type, - validation: editingField.validation, - text: editingField.text, - description: editingField.description, - options: editingField.options, - order: 0, - isRequired: editingField.isRequired - }; - - // newly created field to fields state - (this.state.fields || []).push({ - _id: Math.random().toString(), - ...doc - }); - - this.setState({ fields: this.state.fields, editingField: undefined }); - - this.props.onChange('fields', this.state.fields || []); - }; - - setFieldAttrChanges( - attributeName: string, - value: string | boolean | string[] - ) { - const { fields = [] } = this.state; - - const editingField = this.state.editingField || ({} as IField); - - editingField[attributeName] = value; - - this.setState({ editingField }); - - this.props.onChange('fields', fields); - } - - renderButtons() { - const { editingField } = this.state; - - if (editingField && editingField._id) { - const _id = editingField._id; - - // reset editing field state - const reset = () => { - this.setState({ editingField: undefined }); - }; - - const onDelete = e => { - e.preventDefault(); - - // remove field from state - const fields = (this.state.fields || []).filter( - field => field._id !== _id - ); - - this.setState({ fields }); - - reset(); - - this.props.onChange('fields', fields); - }; - - return ( - - - - - ); - } - - return ( - - ); - } - - footerActions = () => { - const editingField = this.state.editingField || ({} as IField); - - const onChange = e => - this.onFieldAttrChange( - 'isRequired', - (e.currentTarget as HTMLInputElement).checked - ); - - return ( - - - {__('This item is required')} - -   {this.renderButtons()} - - } - /> - ); - }; - - renderOptionsTextArea() { - const { chosenFieldType = '' } = this.state; - const editingField = this.state.editingField || ({} as IField); - - const onChange = e => - this.onFieldAttrChange( - 'options', - (e.currentTarget as HTMLInputElement).value.split('\n') - ); - - if ( - !['select', 'check', 'radio'].includes( - chosenFieldType || editingField.type || '' - ) - ) { - return null; - } - - return ( - - Options: - - - - ); - } - - renderOptions() { - const editingField = this.state.editingField || ({} as IField); - - const formTitle = e => - this.onChangeState( - 'formTitle', - (e.currentTarget as HTMLInputElement).value - ); - - const formDesc = e => - this.onChangeState( - 'formDesc', - (e.currentTarget as HTMLInputElement).value - ); - - const validation = e => - this.onFieldAttrChange( - 'validation', - (e.currentTarget as HTMLInputElement).value - ); - - const text = e => - this.onFieldAttrChange( - 'text', - (e.currentTarget as HTMLInputElement).value - ); - - const desc = e => - this.onFieldAttrChange( - 'description', - (e.currentTarget as HTMLInputElement).value - ); - - const formBtnText = e => - this.onChangeState( - 'formBtnText', - (e.currentTarget as HTMLInputElement).value - ); - - return ( - - - {__('Form title')} - - - - - {__('Form description')} - - - - - Type: - - - - - - - - - - - - - - - - Validation: - - - - - - - - - - - Text: - - - - - Description: - - - - {this.renderOptionsTextArea()} - - - {__('Form button text')} - - - - ); - } - - render() { - return ( - - - {this.renderOptions()} - {this.footerActions()} - - - - - - - ); - } -} - -export default FormStep; diff --git a/src/modules/forms/components/step/preview/FormPreview.tsx b/src/modules/forms/components/step/preview/FormPreview.tsx deleted file mode 100644 index 1ccee2b2bcc..00000000000 --- a/src/modules/forms/components/step/preview/FormPreview.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { IField } from 'modules/settings/properties/types'; -import React from 'react'; -import CommonPreview from './CommonPreview'; -import FormFieldPreview from './FormFieldPreview'; - -type Props = { - formTitle?: string; - formDesc?: string; - formBtnText?: string; - color: string; - theme: string; - fields?: IField[]; - onFieldEdit?: (field: IField) => void; - onChange: (name: any, fields: string) => void; - type: string; -}; - -class FormPreview extends React.Component { - render() { - const { - formTitle, - formDesc, - formBtnText, - color, - theme, - fields, - onFieldEdit, - onChange, - type - } = this.props; - - return ( - - - - ); - } -} - -export default FormPreview; diff --git a/src/modules/forms/components/step/preview/index.ts b/src/modules/forms/components/step/preview/index.ts deleted file mode 100644 index 73b05d25cda..00000000000 --- a/src/modules/forms/components/step/preview/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import CalloutPreview from './CalloutPreview'; -import CommonPreview from './CommonPreview'; -import FieldPreview from './FieldPreview'; -import FormFieldPreview from './FormFieldPreview'; -import FormPreview from './FormPreview'; -import SuccessPreview from './SuccessPreview'; - -export { - CommonPreview, - CalloutPreview, - FormPreview, - SuccessPreview, - FormFieldPreview, - FieldPreview -}; diff --git a/src/modules/forms/containers/CreateForm.tsx b/src/modules/forms/containers/CreateForm.tsx index 0e8f237192a..faaf814c14d 100644 --- a/src/modules/forms/containers/CreateForm.tsx +++ b/src/modules/forms/containers/CreateForm.tsx @@ -1,12 +1,9 @@ import gql from 'graphql-tag'; import { Alert, withProps } from 'modules/common/utils'; -import { - AddIntegrationMutationResponse, - AddIntegrationMutationVariables -} from 'modules/settings/integrations/types'; import { AddFieldsMutationResponse, - AddFieldsMutationVariables + AddFieldsMutationVariables, + IField } from 'modules/settings/properties/types'; import React from 'react'; import { compose, graphql } from 'react-apollo'; @@ -14,47 +11,45 @@ import { withRouter } from 'react-router'; import { IRouterProps } from '../../common/types'; import Form from '../components/Form'; import { mutations } from '../graphql'; -import { AddFormMutationResponse, AddFormMutationVariables } from '../types'; - -type Props = {} & IRouterProps & - AddIntegrationMutationResponse & +import { + AddFormMutationResponse, + AddFormMutationVariables, + IFormData +} from '../types'; + +type Props = { + renderPreviewWrapper: (previewRenderer, fields: IField[]) => void; + afterDbSave: (formId: string) => void; + onDocChange?: (doc: IFormData) => void; + type: string; + isReadyToSave: boolean; +}; + +type FinalProps = {} & Props & + IRouterProps & AddFieldsMutationResponse & AddFormMutationResponse; -class CreateFormContainer extends React.Component< - Props, - { isLoading: boolean } -> { - constructor(props: Props) { - super(props); - - this.state = { isLoading: false }; - } - +class CreateFormContainer extends React.Component { render() { - const { - addIntegrationMutation, - addFormMutation, - addFieldsMutation, - history - } = this.props; + const { addFormMutation, addFieldsMutation, afterDbSave } = this.props; - const save = doc => { + const saveForm = doc => { let formId; - - const { form, brandId, name, languageCode, formData, fields } = doc; - - this.setState({ isLoading: true }); + const { title, desc, btnText, fields, type } = doc; addFormMutation({ - variables: form + variables: { + title, + description: desc, + buttonText: btnText, + type + } }) .then(({ data }) => { formId = data.formsAdd._id; - return addIntegrationMutation({ - variables: { formData, brandId, name, languageCode, formId } - }); + afterDbSave(formId); }) .then(() => { @@ -64,7 +59,6 @@ class CreateFormContainer extends React.Component< promises.push( addFieldsMutation({ variables: { - contentType: 'form', contentTypeId: formId, ...field } @@ -76,53 +70,40 @@ class CreateFormContainer extends React.Component< }) .then(() => { - Alert.success('You successfully added a lead'); - history.push('/forms'); - - this.setState({ isLoading: false }); + Alert.success('You successfully added a form'); }) .catch(error => { Alert.error(error.message); - - this.setState({ isLoading: false }); }); }; const updatedProps = { ...this.props, fields: [], - save, - isActionLoading: this.state.isLoading + saveForm }; return
    ; } } -export default withProps<{}>( +export default withProps( compose( - graphql< - {}, - AddIntegrationMutationResponse, - AddIntegrationMutationVariables - >(gql(mutations.integrationsCreateFormIntegration), { - name: 'addIntegrationMutation', - options: { - refetchQueries: ['formIntegrations', 'formIntegrationCounts'] - } - }), - graphql<{}, AddFormMutationResponse, AddFormMutationVariables>( + graphql( gql(mutations.addForm), { - name: 'addFormMutation' + name: 'addFormMutation', + options: { + refetchQueries: ['fields'] + } } ), - graphql<{}, AddFieldsMutationResponse, AddFieldsMutationVariables>( + graphql( gql(mutations.fieldsAdd), { name: 'addFieldsMutation' } ) - )(withRouter(CreateFormContainer)) + )(withRouter(CreateFormContainer)) ); diff --git a/src/modules/forms/containers/EditForm.tsx b/src/modules/forms/containers/EditForm.tsx index 092c302c5b6..cf93794b129 100644 --- a/src/modules/forms/containers/EditForm.tsx +++ b/src/modules/forms/containers/EditForm.tsx @@ -1,10 +1,6 @@ import gql from 'graphql-tag'; import { Alert, withProps } from 'modules/common/utils'; -import { - EditIntegrationMutationResponse, - EditIntegrationMutationVariables, - FormIntegrationDetailQueryResponse -} from 'modules/settings/integrations/types'; +import { IIntegration } from 'modules/settings/integrations/types'; import { FieldsQueryResponse, IField } from 'modules/settings/properties/types'; import React from 'react'; import { compose, graphql } from 'react-apollo'; @@ -19,78 +15,73 @@ import { EditFieldMutationVariables, EditFormMutationResponse, EditFormMutationVariables, + FormDetailQueryResponse, + IFormData, RemoveFieldMutationResponse, RemoveFieldMutationVariables } from '../types'; type Props = { - contentTypeId: string; + renderPreviewWrapper: (previewRenderer, fields: IField[]) => void; + afterDbSave: (formId: string) => void; + onDocChange?: (doc: IFormData) => void; + onInit?: (fields: IField[]) => void; + type: string; + isReadyToSave: boolean; formId: string; - queryParams: any; + integration?: IIntegration; }; type FinalProps = { fieldsQuery: FieldsQueryResponse; - integrationDetailQuery: FormIntegrationDetailQueryResponse; + formDetailQuery: FormDetailQueryResponse; } & Props & - EditIntegrationMutationResponse & EditFormMutationResponse & AddFieldMutationResponse & EditFieldMutationResponse & RemoveFieldMutationResponse & IRouterProps; -class EditFormContainer extends React.Component< - FinalProps, - { isLoading: boolean } -> { - constructor(props: FinalProps) { - super(props); +class EditFormContainer extends React.Component { + componentWillReceiveProps(nextProps: FinalProps) { + const { onInit, fieldsQuery } = this.props; - this.state = { isLoading: false }; + if (fieldsQuery.loading && !nextProps.fieldsQuery.loading && onInit) { + onInit(nextProps.fieldsQuery.fields || []); + } } render() { const { formId, - integrationDetailQuery, - editIntegrationMutation, + afterDbSave, addFieldMutation, editFieldMutation, - removeFieldMutation, editFormMutation, + removeFieldMutation, fieldsQuery, - history + formDetailQuery } = this.props; - if (fieldsQuery.loading || integrationDetailQuery.loading) { + if (fieldsQuery.loading || formDetailQuery.loading) { return false; } const dbFields = fieldsQuery.fields || []; - const integration = integrationDetailQuery.integrationDetail || {}; - - const save = doc => { - const { form, brandId, name, languageCode, formData, fields } = doc; - - this.setState({ isLoading: true }); - - // edit form - editFormMutation({ variables: { _id: formId, ...form } }) - .then(() => - // edit integration - editIntegrationMutation({ - variables: { - _id: integration._id, - formData, - brandId, - name, - languageCode, - formId - } - }) - ) - + const form = formDetailQuery.formDetail || {}; + + const saveForm = doc => { + const { title, desc, btnText, fields, type } = doc; + + editFormMutation({ + variables: { + _id: formId, + title, + description: desc, + buttonText: btnText, + type + } + }) .then(() => { const dbFieldIds = dbFields.map(field => field._id); const existingIds: string[] = []; @@ -109,6 +100,7 @@ class EditFormContainer extends React.Component< // collect fields to create delete field._id; + delete field.key; createFieldsData.push({ ...field, @@ -119,8 +111,8 @@ class EditFormContainer extends React.Component< // collect fields to remove for (const dbFieldId of dbFieldIds) { - if (!existingIds.includes(dbFieldId)) { - removeFieldsData.push({ _id: dbFieldId }); + if (!existingIds.includes(dbFieldId || '')) { + removeFieldsData.push({ _id: dbFieldId || '' }); } } @@ -144,28 +136,26 @@ class EditFormContainer extends React.Component< }) .then(() => { - Alert.success('You successfully updated a lead'); + Alert.success('You successfully updated a form'); fieldsQuery.refetch().then(() => { - history.push('/forms'); + afterDbSave(formId); }); - - this.setState({ isLoading: false }); }) .catch(error => { Alert.error(error.message); - - this.setState({ isLoading: false }); }); }; const updatedProps = { ...this.props, - integration, - fields: dbFields.map(field => ({ ...field })), - save, - isActionLoading: this.state.isLoading + fields: dbFields.map(field => ({ + ...field, + key: Math.random().toString() + })), + saveForm, + form }; return ; @@ -185,37 +175,34 @@ export default withProps( variables: { contentType: 'form', contentTypeId: formId - } + }, + fetchPolicy: 'network-only' }; } }), - graphql( - gql(queries.integrationDetail), + graphql( + gql(queries.formDetail), { - name: 'integrationDetailQuery', - options: ({ contentTypeId }) => ({ + name: 'formDetailQuery', + options: ({ formId }) => ({ variables: { - _id: contentTypeId + _id: formId } }) } ), - graphql< - Props, - EditIntegrationMutationResponse, - EditIntegrationMutationVariables - >(gql(mutations.integrationsEditFormIntegration), { - name: 'editIntegrationMutation', - options: { - refetchQueries: ['formIntegrations', 'formIntegrationCounts'] - } - }), graphql( gql(mutations.fieldsAdd), { name: 'addFieldMutation' } ), + graphql( + gql(mutations.editForm), + { + name: 'editFormMutation' + } + ), graphql( gql(mutations.fieldsEdit), { @@ -227,12 +214,6 @@ export default withProps( { name: 'removeFieldMutation' } - ), - graphql( - gql(mutations.editForm), - { - name: 'editFormMutation' - } ) )(withRouter(EditFormContainer)) ); diff --git a/src/modules/forms/graphql/mutations.ts b/src/modules/forms/graphql/mutations.ts index a7aa76af357..afff7a007ea 100644 --- a/src/modules/forms/graphql/mutations.ts +++ b/src/modules/forms/graphql/mutations.ts @@ -1,38 +1,34 @@ -const commonFormParamsDef = ` - $name: String!, - $brandId: String!, - $formId: String!, - $languageCode: String, - $formData: IntegrationFormData! -`; - -const commonFormParams = ` - name: $name, - brandId: $brandId, - formId: $formId, - languageCode: $languageCode, - formData: $formData -`; - const commonParamsDef = ` $title: String, $description: String, $buttonText: String, - $themeColor: String, - $callout: JSON, - $rules:[InputRule] + $type: String! `; const commonParams = ` title: $title, description: $description, buttonText: $buttonText, - themeColor: $themeColor, - callout: $callout - rules: $rules + type: $type `; -const commonVariables = ` +const addForm = ` + mutation formsAdd(${commonParamsDef}) { + formsAdd(${commonParams}) { + _id + } + } +`; + +const editForm = ` + mutation formsEdit($_id: String!, ${commonParamsDef}) { + formsEdit(_id: $_id, ${commonParams}) { + _id + } + } +`; + +const commonFieldParamsDef = ` $type: String, $validation: String, $text: String, @@ -52,49 +48,11 @@ const commonFieldParams = ` order: $order `; -const integrationRemove = ` - mutation integrationsRemove($_id: String!) { - integrationsRemove(_id: $_id) - } -`; - -const integrationsCreateFormIntegration = ` - mutation integrationsCreateFormIntegration(${commonFormParamsDef}) { - integrationsCreateFormIntegration(${commonFormParams}) { - _id - } - } -`; - -const integrationsEditFormIntegration = ` - mutation integrationsEditFormIntegration($_id: String!, ${commonFormParamsDef}) { - integrationsEditFormIntegration(_id: $_id, ${commonFormParams}) { - _id - } - } -`; - -const addForm = ` - mutation formsAdd(${commonParamsDef}) { - formsAdd(${commonParams}) { - _id - } - } -`; - -const editForm = ` - mutation formsEdit($_id: String!, ${commonParamsDef}) { - formsEdit(_id: $_id, ${commonParams}) { - _id - } - } -`; - const fieldsAdd = ` mutation fieldsAdd( $contentType: String!, $contentTypeId: String, - ${commonVariables} + ${commonFieldParamsDef} ) { fieldsAdd( contentType: $contentType, @@ -108,7 +66,7 @@ const fieldsAdd = ` `; const fieldsEdit = ` - mutation fieldsEdit($_id: String!, ${commonVariables}) { + mutation fieldsEdit($_id: String!, ${commonFieldParamsDef}) { fieldsEdit(_id: $_id, ${commonFieldParams}) { _id contentTypeId @@ -124,13 +82,31 @@ const fieldsRemove = ` } `; +const commonFormSubmissionParamsDef = ` + $formId: String, + $contentType: String, + $contentTypeId: String, + $formSubmissions: JSON, +`; + +const commonFormSubmissionParams = ` + formId: $formId, + contentType: $contentType, + contentTypeId: $contentTypeId, + formSubmissions: $formSubmissions +`; + +const formSubmissionsSave = ` + mutation formSubmissionsSave(${commonFormSubmissionParamsDef}) { + formSubmissionsSave(${commonFormSubmissionParams}) + } +`; + export default { - integrationRemove, - integrationsEditFormIntegration, - integrationsCreateFormIntegration, addForm, editForm, fieldsAdd, fieldsEdit, - fieldsRemove + fieldsRemove, + formSubmissionsSave }; diff --git a/src/modules/forms/graphql/queries.ts b/src/modules/forms/graphql/queries.ts index a7b11cacf8a..22558c87d58 100644 --- a/src/modules/forms/graphql/queries.ts +++ b/src/modules/forms/graphql/queries.ts @@ -11,19 +11,17 @@ const integrations = ` code } languageCode - formData - formId + leadData + leadId tags { _id name colorCode } tagIds - form { + lead { _id - title - code - description + formId createdDate createdUserId createdUser { @@ -34,7 +32,6 @@ const integrations = ` position } } - buttonText themeColor contactsGathered viewCount @@ -52,6 +49,24 @@ const integrations = ` condition value } + form { + _id + title + code + description + type + buttonText + createdDate + createdUserId + createdUser { + _id + details { + avatar + fullName + position + } + } + } } } } @@ -71,7 +86,7 @@ const integrationDetail = ` brandId code formId - formData + leadData tagIds tags { _id @@ -108,6 +123,29 @@ const integrationDetail = ` } `; +const formDetail = ` + query formDetail($_id: String!) { + formDetail(_id: $_id) { + _id + title + code + type + description + buttonText + createdDate + createdUserId + createdUser { + _id + details { + avatar + fullName + position + } + } + } + } +`; + const integrationsTotalCount = ` query integrationsTotalCount { integrationsTotalCount { @@ -173,6 +211,7 @@ export default { integrationDetail, integrationsTotalCount, fields, + formDetail, tags, forms, fieldsCombinedByContentType, diff --git a/src/modules/forms/styles.tsx b/src/modules/forms/styles.tsx new file mode 100644 index 00000000000..869c8661fbc --- /dev/null +++ b/src/modules/forms/styles.tsx @@ -0,0 +1,155 @@ +import { colors, dimensions } from 'modules/common/styles'; +import { rgba } from 'modules/common/styles/color'; +import styled from 'styled-components'; +import styledTS from 'styled-components-ts'; +import { SelectWrapper } from '../common/components/form/styles'; + +const FlexRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + > label { + font-weight: 500; + } +`; + +const FlexWrapper = styled.span` + flex: 1; +`; + +const FieldWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + width: 48%; + float: left; + min-height: 110px; + border: 1px solid ${colors.borderPrimary}; + border-radius: 2px; + margin-bottom: 4%; + padding: 20px; + transition: all ease 0.3s; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); + + &:nth-of-type(even) { + margin-left: 4%; + } + + > i { + margin-bottom: 10px; + } + + &:hover { + cursor: pointer; + box-shadow: 0 10px 20px ${rgba(colors.colorCoreDarkGray, 0.12)}; + } +`; + +const FieldItem = styledTS<{ selectType?: boolean; noPadding?: boolean }>( + styled.div +)` + padding: ${props => !props.noPadding && `0 ${dimensions.unitSpacing}px`}; + flex: 1; + + input, + textarea, + select { + box-sizing: border-box; + transition: all 0.3s ease-in-out; + background: #faf9fb; + border: 1px solid ${colors.colorShadowGray}; + border-radius: 5px !important; + box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.07); + color: #1a1a1a; + display: block; + font-size: 14px; + height: 36px; + line-height: 1.42857143; + margin-top: ${props => !props.selectType && `${dimensions.unitSpacing}px`}; + outline: 0; + padding: 6px 15px; + width: 100%; + + + &:focus { + border-color: ${colors.colorShadowGray}; + background: ${colors.colorWhite}; + } + + &:after { + top: 22px; + } + } + + textarea { + overflow: auto; + height: auto; + } + + .required { + color: ${colors.colorCoreRed}; + } + + ${SelectWrapper} { + margin-top: ${dimensions.unitSpacing}px; + } +`; + +const Options = styled.div` + display: inline-block; + width: 100%; + margin-top: 10px; +`; + +const LeftSection = styled.div` + border-right: 1px solid ${colors.borderDarker}; + padding-right: ${dimensions.coreSpacing}px; + width: 100%; +`; + +const PreviewSection = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: ${dimensions.coreSpacing}px; +`; + +const Preview = styled.div` + width: 80%; +`; + +const ShowPreview = styled.div` + display: flex; + align-items: center; + justify-content: center; + margin-top: ${dimensions.coreSpacing}px; + color: ${colors.colorCoreGray}; + font-size: 14px; + + > i { + margin-right: ${dimensions.unitSpacing}px; + } +`; + +const Title = styled.h4` + font-size: 16px; + margin-top: 10px; + font-weight: 500; +`; + +export { + FieldWrapper, + FieldItem, + Options, + Preview, + ShowPreview, + LeftSection, + PreviewSection, + FlexRow, + FlexWrapper, + Title +}; diff --git a/src/modules/forms/types.ts b/src/modules/forms/types.ts index a4f5d5bd823..33748e6aee0 100644 --- a/src/modules/forms/types.ts +++ b/src/modules/forms/types.ts @@ -1,50 +1,38 @@ -import { IConditionsRule } from 'modules/common/types'; import { IUser } from '../auth/types'; -import { IBrand } from '../settings/brands/types'; -import { IIntegration } from '../settings/integrations/types'; import { IField } from '../settings/properties/types'; -import { ITag } from '../tags/types'; - -export interface ICallout { - title?: string; - body?: string; - buttonText?: string; - featuredImage?: string; - skip?: boolean; -} export interface IForm { _id: string; title?: string; code?: string; + type?: string; description?: string; buttonText?: string; - themeColor?: string; - callout?: ICallout; - rules?: IConditionsRule[]; createdUserId?: string; createdUser?: IUser; createdDate?: Date; - viewCount?: number; - contactsGathered?: number; - tagIds?: string[]; - getTags?: ITag[]; } -export interface IFormIntegration extends IIntegration { - brand: IBrand; - form: IForm; - tags: ITag[]; - createdUser: IUser; +export interface IFormSubmission { + formId: string; + formSubmissions: string; + contentTypeId: string; +} + +export interface IFormData { + title?: string; + desc?: string; + btnText?: string; + fields?: IField[]; + type?: string; } // mutation types export type AddFormMutationVariables = { - title: string; - description: string; - buttonText: string; - themeColor: string; - callout: ICallout; + title?: string; + description?: string; + buttonText?: string; + type: string; }; export type AddFormMutationResponse = { @@ -55,11 +43,10 @@ export type AddFormMutationResponse = { export type EditFormMutationVariables = { _id: string; - title: string; - description: string; - buttonText: string; - themeColor: string; - callout: ICallout; + title?: string; + description?: string; + buttonText?: string; + type: string; }; export type EditFormMutationResponse = { @@ -70,16 +57,6 @@ export type EditFormMutationResponse = { ) => Promise; }; -export type RemoveMutationVariables = { - _id: string; -}; - -export type RemoveMutationResponse = { - removeMutation: ( - params: { variables: RemoveMutationVariables } - ) => Promise; -}; - export type AddFieldMutationVariables = { createFieldsData: IField[]; }; @@ -114,28 +91,24 @@ export type RemoveFieldMutationResponse = { ) => Promise; }; -// query types - -export type FormIntegrationsQueryResponse = { - integrations: IFormIntegration; +export type FormDetailQueryResponse = { + formDetail: IForm; loading: boolean; refetch: () => void; }; -export type Counts = { - [key: string]: number; +export type FormsQueryResponse = { + forms: IForm[]; + loading: boolean; }; -export type IntegrationsCount = { - total: number; - byTag: Counts; - byChannel: Counts; - byBrand: Counts; - byKind: Counts; -}; +export interface IFormSubmissionParams { + contentTypeId: string; + contentType: string; + formId: string; + formField: JSON; +} -export type CountQueryResponse = { - integrationsTotalCount: IntegrationsCount; - loading: boolean; - refetch: () => void; -}; +export type SaveFormSubmissionMutation = ( + { variables: IFormSubmissionParams } +) => Promise; diff --git a/src/modules/growthHacks/components/FormFields.tsx b/src/modules/growthHacks/components/FormFields.tsx new file mode 100644 index 00000000000..04181bf86f9 --- /dev/null +++ b/src/modules/growthHacks/components/FormFields.tsx @@ -0,0 +1,26 @@ +import GenerateField from 'modules/settings/properties/components/GenerateField'; +import React from 'react'; + +type Props = { + formId: string; + fields: any; + formSubmissions?: any; + onChangeFormField: (field: any) => void; +}; + +class FormFields extends React.Component { + render() { + const { fields, formSubmissions, onChangeFormField } = this.props; + + return fields.map(field => ( + + )); + } +} + +export default FormFields; diff --git a/src/modules/growthHacks/components/GrowthHackAddTrigger.tsx b/src/modules/growthHacks/components/GrowthHackAddTrigger.tsx new file mode 100644 index 00000000000..2766fc165f6 --- /dev/null +++ b/src/modules/growthHacks/components/GrowthHackAddTrigger.tsx @@ -0,0 +1,19 @@ +import AddTrigger from 'modules/boards/components/portable/AddTrigger'; +import React from 'react'; +import options from '../options'; + +type Props = { + customerIds?: string[]; +}; + +export default (props: Props) => { + const { customerIds } = props; + + const extendedProps = { + options, + relType: 'growthHack', + relTypeIds: customerIds + }; + + return ; +}; diff --git a/src/modules/growthHacks/components/GrowthHackBoard.tsx b/src/modules/growthHacks/components/GrowthHackBoard.tsx new file mode 100644 index 00000000000..78f657b00d0 --- /dev/null +++ b/src/modules/growthHacks/components/GrowthHackBoard.tsx @@ -0,0 +1,41 @@ +import Board from 'modules/boards/containers/Board'; +import MainActionBar from 'modules/boards/containers/MainActionBar'; +import { BoardContainer, BoardContent } from 'modules/boards/styles/common'; +import { __ } from 'modules/common/utils'; +import Header from 'modules/layout/components/Header'; +import React from 'react'; +import options from '../options'; +import GrowthHackMainActionBar from './GrowthHackMainActionBar'; + +type Props = { + queryParams: any; +}; +class GrowthHackBoard extends React.Component { + renderContent() { + const { queryParams } = this.props; + + return ; + } + + renderActionBar() { + return ( + + ); + } + + render() { + const breadcrumb = [{ title: __('Growth hack') }]; + + return ( + +
    + + {this.renderActionBar()} + {this.renderContent()} + + + ); + } +} + +export default GrowthHackBoard; diff --git a/src/modules/growthHacks/components/GrowthHackEditForm.tsx b/src/modules/growthHacks/components/GrowthHackEditForm.tsx new file mode 100644 index 00000000000..42955361ebc --- /dev/null +++ b/src/modules/growthHacks/components/GrowthHackEditForm.tsx @@ -0,0 +1,209 @@ +import { IUser } from 'modules/auth/types'; +import DueDateChanger from 'modules/boards/components/DueDateChanger'; +import EditForm from 'modules/boards/components/editForm/EditForm'; +import { FlexContent, LeftContainer } from 'modules/boards/styles/item'; +import { IEditFormContent, IOptions } from 'modules/boards/types'; +import { IFormSubmission } from 'modules/forms/types'; +import React from 'react'; +import { IGrowthHack, IGrowthHackParams } from '../types'; +import { Left, StageForm, Top } from './editForm/'; +import Actions from './editForm/Actions'; +import Score from './Score'; + +const reactiveFields = ['priority', 'hackStages']; + +type Props = { + options: IOptions; + item: IGrowthHack; + users: IUser[]; + addItem: (doc: IGrowthHackParams, callback: () => void, msg?: string) => void; + saveFormSubmission: (doc: IFormSubmission) => void; + saveItem: (doc: IGrowthHackParams, callback?: (item) => void) => void; + onUpdate: (item, prevStageId?: string) => void; + removeItem: (itemId: string, callback: () => void) => void; + beforePopupClose: () => void; +}; + +type State = { + hackDescription: string; + goal: string; + formSubmissions: JSON; + formId: string; + priority: string; + hackStages: string[]; + impact: number; + ease: number; + confidence: number; + reach: number; +}; + +export default class GrowthHackEditForm extends React.Component { + constructor(props) { + super(props); + + const item = props.item; + + this.state = { + hackDescription: item.hackDescription || '', + goal: item.goal || '', + formSubmissions: item.formSubmissions || {}, + priority: item.priority || '', + hackStages: item.hackStages || [], + formId: item.formId || '', + impact: item.impact || 0, + confidence: item.confidence || 0, + ease: item.ease || 0, + reach: item.reach || 0 + }; + } + + onChangeExtraField = (name: T, value: State[T]) => { + this.setState({ [name]: value } as Pick, () => { + if (reactiveFields.includes(name)) { + this.props.saveItem({ [name]: value }, updatedItem => { + this.props.onUpdate(updatedItem); + }); + } + }); + }; + + renderDueDate = (closeDate, onDateChange: (date) => void) => { + if (!closeDate) { + return null; + } + + return ( + + ); + }; + + renderScore = () => { + const { reach, impact, confidence, ease } = this.state; + const { saveItem, item } = this.props; + + const onChange = e => { + const value = Number((e.target as HTMLInputElement).value); + const confirmedValue = value > 10 ? 10 : value; + + const changedValue = { [e.target.name]: confirmedValue }; + + this.setState(changedValue as Pick); + }; + + const onExited = () => { + saveItem( + { + impact, + confidence, + ease, + reach + }, + updatedItem => { + this.props.onUpdate(updatedItem); + } + ); + }; + + return ( + + ); + }; + + renderFormContent = ({ + state, + onChangeAttachment, + onChangeField, + copy, + remove, + onBlurFields + }: IEditFormContent) => { + const { item, options, saveFormSubmission } = this.props; + const { formSubmissions, priority, hackStages, formId } = this.state; + + const { + name, + stageId, + description, + closeDate, + attachments, + assignedUserIds + } = state; + + const dateOnChange = date => onChangeField('closeDate', date); + + return ( + <> + + + + + + + + + + + + ); + }; + + render() { + const extendedProps = { + ...this.props, + formContent: this.renderFormContent, + extraFields: this.state + }; + + return ; + } +} diff --git a/src/modules/growthHacks/components/GrowthHackItem.tsx b/src/modules/growthHacks/components/GrowthHackItem.tsx new file mode 100644 index 00000000000..953fd10404c --- /dev/null +++ b/src/modules/growthHacks/components/GrowthHackItem.tsx @@ -0,0 +1,96 @@ +import dayjs from 'dayjs'; +import EditForm from 'modules/boards/containers/editForm/EditForm'; +import { ItemDate } from 'modules/boards/styles/common'; +import { Footer, PriceContainer, Right } from 'modules/boards/styles/item'; +import { Content } from 'modules/boards/styles/stage'; +import { IOptions } from 'modules/boards/types'; +import { renderPriority } from 'modules/boards/utils'; +import { __, getUserAvatar } from 'modules/common/utils'; +import React from 'react'; +import { ScoreAmount } from '../styles'; +import { IGrowthHack } from '../types'; +import Score from './Score'; + +type Props = { + stageId: string; + item: IGrowthHack; + beforePopupClose: () => void; + onClick: () => void; + options: IOptions; +}; + +export default class GrowthHackItem extends React.PureComponent { + renderDate(date) { + if (!date) { + return null; + } + + return {dayjs(date).format('MMM D, h:mm a')}; + } + + renderForm = () => { + const { stageId, item, options, beforePopupClose } = this.props; + + return ( + + ); + }; + + render() { + const { item, onClick } = this.props; + const { + scoringType, + reach = 0, + impact = 0, + confidence = 0, + ease = 0 + } = item; + + return ( + <> + +
    + {renderPriority(item.priority)} + {item.name} +
    + + + + + + + {(item.assignedUsers || []).map((user, index) => ( + Avatar + ))} + + + +
    + {__('Last updated')}: + {this.renderDate(item.modifiedAt)} +
    +
    + {this.renderForm()} + + ); + } +} diff --git a/src/modules/growthHacks/components/GrowthHackMainActionBar.tsx b/src/modules/growthHacks/components/GrowthHackMainActionBar.tsx new file mode 100644 index 00000000000..181ebd8795b --- /dev/null +++ b/src/modules/growthHacks/components/GrowthHackMainActionBar.tsx @@ -0,0 +1,31 @@ +import MainActionBar from 'modules/boards/components/MainActionBar'; +import { IBoard, IPipeline } from 'modules/boards/types'; +import React from 'react'; + +type Props = { + onSearch: (search: string) => void; + onSelect: (values: string[] | string, name: string) => void; + onDateFilterSelect: (name: string, value: string) => void; + onClear: (name: string, values) => void; + isFiltered: () => boolean; + clearFilter: () => void; + currentBoard?: IBoard; + currentPipeline?: IPipeline; + boards: IBoard[]; + middleContent?: () => React.ReactNode; + history: any; + queryParams: any; + assignedUserIds?: string[]; + type: string; +}; + +const GrowthHackMainActionBar = (props: Props) => { + const extendedProps = { + ...props, + link: '/growthHack/board' + }; + + return ; +}; + +export default GrowthHackMainActionBar; diff --git a/src/modules/growthHacks/components/PortableGrowthHack.tsx b/src/modules/growthHacks/components/PortableGrowthHack.tsx new file mode 100644 index 00000000000..d68e74e7c76 --- /dev/null +++ b/src/modules/growthHacks/components/PortableGrowthHack.tsx @@ -0,0 +1,93 @@ +import dayjs from 'dayjs'; +import UserCounter from 'modules/boards/components/portable/UserCounter'; +import EditForm from 'modules/boards/containers/editForm/EditForm'; +import { ItemContainer, ItemDate } from 'modules/boards/styles/common'; +import { + Footer, + PriceContainer, + Right, + SpaceContent +} from 'modules/boards/styles/item'; +import { Content } from 'modules/boards/styles/stage'; +import { IOptions } from 'modules/boards/types'; +import ModalTrigger from 'modules/common/components/ModalTrigger'; +import Tip from 'modules/common/components/Tip'; +import { __ } from 'modules/common/utils'; +import React from 'react'; +import { IGrowthHack } from '../types'; + +type Props = { + item: IGrowthHack; + onAdd?: (stageId: string, item: IGrowthHack) => void; + onRemove?: (dealId: string, stageId: string) => void; + onUpdate?: (item: IGrowthHack) => void; + options: IOptions; +}; + +class GrowthHack extends React.Component { + renderFormTrigger = (trigger: React.ReactNode) => { + const { item, onAdd, onRemove, onUpdate, options } = this.props; + + const content = props => ( + + ); + + return ( + + ); + }; + + renderDate = (date, format = 'YYYY-MM-DD') => { + if (!date) { + return null; + } + + return ( + + {dayjs(date).format('lll')} + + ); + }; + + render() { + const { item } = this.props; + + const content = ( + + + +
    {item.name}
    + {this.renderDate(item.closeDate)} +
    +
    + + + + + + +
    + {__('Last updated')}:{this.renderDate(item.modifiedAt)} +
    +
    + ); + + return this.renderFormTrigger(content); + } +} + +export default GrowthHack; diff --git a/src/modules/growthHacks/components/PortableGrowthHacks.tsx b/src/modules/growthHacks/components/PortableGrowthHacks.tsx new file mode 100644 index 00000000000..08145f728bf --- /dev/null +++ b/src/modules/growthHacks/components/PortableGrowthHacks.tsx @@ -0,0 +1,23 @@ +import PortableItems from 'modules/boards/components/portable/Items'; +import GetConformity from 'modules/conformity/containers/GetConformity'; +import React from 'react'; +import options from '../options'; + +type IProps = { + mainType?: string; + mainTypeId?: string; + isOpen?: boolean; +}; + +export default (props: IProps) => { + return ( + + ); +}; diff --git a/src/modules/growthHacks/components/Score.tsx b/src/modules/growthHacks/components/Score.tsx new file mode 100644 index 00000000000..056b0859bac --- /dev/null +++ b/src/modules/growthHacks/components/Score.tsx @@ -0,0 +1,141 @@ +import FormControl from 'modules/common/components/form/Control'; +import React from 'react'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { + AmountItem, + Amounts, + CalculatedAmount, + Factor, + ScoreWrapper +} from '../styles'; + +type Props = { + impact: number; + ease: number; + confidence: number; + reach: number; + scoringType?: string; + onChange: (e) => void; + onExited: () => void; +}; + +function Amount({ + type, + r, + i, + c, + e +}: { + type?: string; + r: number; + i: number; + c: number; + e: number; +}) { + const roundToTwo = value => { + if (!value) { + return 0; + } + + return Math.round(value * 100) / 100; + }; + + const calculateScore = () => { + if (type === 'rice') { + if (e === 0) { + return 0; + } + + return roundToTwo((r * i * c) / e); + } + + return i * c * e; + }; + + return {calculateScore()}; +} + +class Score extends React.Component { + static Amount = Amount; + + renderInput = (name: string, value: number) => { + return ( + + {name} + + + ); + }; + + renderInputs = () => { + const { reach, impact, confidence, ease, scoringType } = this.props; + + if (scoringType === 'rice') { + return ( + + {this.renderInput('reach', reach)} + {this.renderInput('impact', impact)} + {this.renderInput('confidence', confidence)} + {this.renderInput('effort', ease)} + + ); + } + + return ( + + {this.renderInput('impact', impact)} + {this.renderInput('confidence', confidence)} + {this.renderInput('ease', ease)} + + ); + }; + + renderPopover = () => { + return ( + + {this.renderInputs()} + + ); + }; + + render() { + const { + scoringType, + reach, + impact, + confidence, + ease, + onExited + } = this.props; + + return ( + + + + + + + + ); + } +} + +export default Score; diff --git a/src/modules/growthHacks/components/editForm/Actions.tsx b/src/modules/growthHacks/components/editForm/Actions.tsx new file mode 100644 index 00000000000..20c1d290367 --- /dev/null +++ b/src/modules/growthHacks/components/editForm/Actions.tsx @@ -0,0 +1,102 @@ +import DueDateChanger from 'modules/boards/components/DueDateChanger'; +import SelectItem from 'modules/boards/components/SelectItem'; +import { PRIORITIES } from 'modules/boards/constants'; +import { Watch } from 'modules/boards/containers/editForm/'; +import { ColorButton } from 'modules/boards/styles/common'; +import { ActionContainer } from 'modules/boards/styles/item'; +import { IOptions } from 'modules/boards/types'; +import Icon from 'modules/common/components/Icon'; +import { IGrowthHack } from 'modules/growthHacks/types'; +import React from 'react'; +import { HACKSTAGES } from '../../constants'; + +type Props = { + item: IGrowthHack; + onChangeField: (name: 'priority' | 'hackStages', value: any) => void; + closeDate: Date; + priority: string; + hackStages: string[]; + dateOnChange: (date) => void; + options: IOptions; + copy: () => void; + remove: (id: string) => void; +}; + +class Actions extends React.Component { + render() { + const { + item, + onChangeField, + closeDate, + priority, + hackStages, + options, + copy, + remove, + dateOnChange + } = this.props; + + const priorityOnChange = (value: string) => { + onChangeField('priority', value); + }; + + const hackStageOnChange = (value: string) => { + if (hackStages.includes(value)) { + const remainedValues = hackStages.filter(i => { + return i !== value; + }); + + return onChangeField('hackStages', remainedValues); + } + + const values = hackStages.concat(value); + + return onChangeField('hackStages', values); + }; + + const onRemove = () => remove(item._id); + + const priorityTrigger = ( + + + Priority + + ); + const hackStageTrigger = ( + + + Hack Stage + + ); + + return ( + + + + + + + + Copy + + + + Delete + + + ); + } +} + +export default Actions; diff --git a/src/modules/growthHacks/components/editForm/Left.tsx b/src/modules/growthHacks/components/editForm/Left.tsx new file mode 100644 index 00000000000..2ecacd4d05a --- /dev/null +++ b/src/modules/growthHacks/components/editForm/Left.tsx @@ -0,0 +1,159 @@ +import ActivityInputs from 'modules/activityLogs/components/ActivityInputs'; +import ActivityLogs from 'modules/activityLogs/containers/ActivityLogs'; +import { TitleRow } from 'modules/boards/styles/item'; +import { IItem, IOptions } from 'modules/boards/types'; +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 Icon from 'modules/common/components/Icon'; +import Uploader from 'modules/common/components/Uploader'; +import { IAttachment } from 'modules/common/types'; +import SelectTeamMembers from 'modules/settings/team/containers/SelectTeamMembers'; +import React from 'react'; + +type Props = { + item: IItem; + onChangeField: ( + name: 'description' | 'closeDate' | 'assignedUserIds', + value: any + ) => void; + onChangeExtraField: (name: 'hackDescription' | 'goal', value: any) => void; + onBlurFields: (name: 'description' | 'name', value: string) => void; + type: string; + assignedUserIds: string[]; + description: string; + hackDescription: string; + goal: string; + onChangeAttachment: (attachments: IAttachment[]) => void; + attachments: IAttachment[]; + options: IOptions; +}; + +class Left extends React.Component { + render() { + const { + item, + onChangeField, + onChangeExtraField, + onBlurFields, + attachments, + onChangeAttachment, + description, + hackDescription, + goal, + type, + assignedUserIds + } = this.props; + + const onChange = e => + onChangeField(e.target.name, (e.target as HTMLInputElement).value); + + const onChangeExtra = e => + onChangeExtraField(e.target.name, (e.target as HTMLInputElement).value); + + const onSave = e => { + onBlurFields(e.target.name, e.target.value); + }; + + const userOnChange = usrs => onChangeField('assignedUserIds', usrs); + + return ( + <> + + + + + Assign to + + + + + + + + + Hack Description + + + + + + + + + + + Description + + + + + + + + + + + Goal + + + + + + + + + + + Attachments + + + + + + + + + + + ); + } +} + +export default Left; diff --git a/src/modules/growthHacks/components/editForm/StageForm.tsx b/src/modules/growthHacks/components/editForm/StageForm.tsx new file mode 100644 index 00000000000..1482beeb835 --- /dev/null +++ b/src/modules/growthHacks/components/editForm/StageForm.tsx @@ -0,0 +1,79 @@ +import Button from 'modules/common/components/Button'; +import colors from 'modules/common/styles/colors'; +import { IFormSubmission } from 'modules/forms/types'; +import FormFields from 'modules/growthHacks/containers/FormFields'; +import { IGrowthHack } from 'modules/growthHacks/types'; +import React from 'react'; +import styled from 'styled-components'; + +const RightContainer = styled.div` + background: #fff; + padding: 30px; + box-shadow: 0 0 6px 1px rgba(221, 221, 221, 0.7); + align-self: baseline; + flex-basis: 450px; +`; + +const CurrentStage = styled.div` + margin-bottom: 20px; + color: ${colors.colorCoreGray}; + font-weight: 500; + font-size: 12px; + + h4 { + color: ${colors.colorCoreDarkGray}; + margin: 2px 0 0; + font-size: 16px; + } +`; + +type Props = { + item: IGrowthHack; + onChangeExtraField: (name: 'formSubmissions', value: any) => void; + save: (doc: IFormSubmission) => void; + formSubmissions: any; + formId: string; +}; + +class StageForm extends React.Component { + render() { + const { item, onChangeExtraField, formSubmissions, formId } = this.props; + + const stageName = item.stage && item.stage.name; + + const onChangeFormField = field => { + formSubmissions[field._id] = field.value; + onChangeExtraField('formSubmissions', formSubmissions); + }; + + const save = () => { + this.props.save({ contentTypeId: item._id, formId, formSubmissions }); + }; + + return ( + + + Currently on

    {stageName}

    +
    + {formId ? ( + + ) : null} + +
    + ); + } +} + +export default StageForm; diff --git a/src/modules/growthHacks/components/editForm/Top.tsx b/src/modules/growthHacks/components/editForm/Top.tsx new file mode 100644 index 00000000000..a0ed60d00fa --- /dev/null +++ b/src/modules/growthHacks/components/editForm/Top.tsx @@ -0,0 +1,122 @@ +import { PriorityIndicator } from 'modules/boards/components/editForm'; +import Move from 'modules/boards/containers/editForm/Move'; +import { ColorButton } from 'modules/boards/styles/common'; +import { + HeaderContent, + HeaderRow, + MetaInfo, + TitleRow +} from 'modules/boards/styles/item'; +import { IItem, IOptions } from 'modules/boards/types'; +import FormControl from 'modules/common/components/form/Control'; +import Participators from 'modules/inbox/components/conversationDetail/workarea/Participators'; +import React from 'react'; + +type Props = { + item: IItem; + options: IOptions; + name: string; + stageId: string; + priority: string; + hackStages: string[]; + onChangeField: (name: 'name' | 'stageId', value: any) => void; + onBlurFields: (name: 'description' | 'name', value: string) => void; + score?: () => React.ReactNode; + dueDate?: React.ReactNode; + formSubmissions: JSON; +}; + +class Top extends React.Component { + onChangeStage = stageId => { + const { onChangeField } = this.props; + + onChangeField('stageId', stageId); + }; + + renderMove() { + const { item, stageId, options } = this.props; + + return ( + + ); + } + + renderHackStage() { + const { hackStages } = this.props; + + if (hackStages.length === 0) { + return null; + } + + return ( + + {hackStages.map(i => ( + + + {i} + + ))} + + ); + } + + render() { + const { + name, + onChangeField, + score, + dueDate, + priority, + onBlurFields, + item + } = this.props; + + const { assignedUsers = [] } = item; + + const nameOnChange = e => + onChangeField('name', (e.target as HTMLInputElement).value); + + const onSaveName = e => { + onBlurFields('name', e.target.value); + }; + + return ( + <> + + + + {priority && } + + + + {assignedUsers.length > 0 && ( + + )} + {dueDate} + {this.renderHackStage()} + + + + {score && score()} + + + + {this.renderMove()} + + + ); + } +} + +export default Top; diff --git a/src/modules/growthHacks/components/editForm/index.tsx b/src/modules/growthHacks/components/editForm/index.tsx new file mode 100644 index 00000000000..f204361b7fe --- /dev/null +++ b/src/modules/growthHacks/components/editForm/index.tsx @@ -0,0 +1,5 @@ +import Left from './Left'; +import StageForm from './StageForm'; +import Top from './Top'; + +export { Left, StageForm, Top }; diff --git a/src/modules/growthHacks/constants.ts b/src/modules/growthHacks/constants.ts new file mode 100644 index 00000000000..9f0c6074830 --- /dev/null +++ b/src/modules/growthHacks/constants.ts @@ -0,0 +1,8 @@ +export const HACKSTAGES = [ + 'Awareness', + 'Acquisition', + 'Activation', + 'Retention', + 'Revenue', + 'Referrals' +]; diff --git a/src/modules/growthHacks/containers/FormFields.tsx b/src/modules/growthHacks/containers/FormFields.tsx new file mode 100644 index 00000000000..ac213d588d7 --- /dev/null +++ b/src/modules/growthHacks/containers/FormFields.tsx @@ -0,0 +1,48 @@ +import gql from 'graphql-tag'; +import { withProps } from 'modules/common/utils'; +import { queries } from 'modules/forms/graphql'; +import { FieldsQueryResponse } from 'modules/settings/properties/types'; +import React from 'react'; +import { compose, graphql } from 'react-apollo'; +import FormFields from '../components/FormFields'; + +type IProps = { + formId: string; + formSubmissions?: any; + onChangeFormField: (field: any) => void; +}; + +type FinalProps = { + fieldsQuery: FieldsQueryResponse; +} & IProps; + +class FormFieldsContainer extends React.Component { + render() { + const { fieldsQuery } = this.props; + + const dbFields = fieldsQuery.fields || []; + + const extendedProps = { + ...this.props, + fields: dbFields.map(field => ({ ...field })) + }; + + return ; + } +} + +export default withProps( + compose( + graphql(gql(queries.fields), { + name: 'fieldsQuery', + options: ({ formId }) => { + return { + variables: { + contentType: 'form', + contentTypeId: formId + } + }; + } + }) + )(FormFieldsContainer) +); diff --git a/src/modules/growthHacks/containers/GrowthHackEditForm.tsx b/src/modules/growthHacks/containers/GrowthHackEditForm.tsx new file mode 100644 index 00000000000..75539f2c58b --- /dev/null +++ b/src/modules/growthHacks/containers/GrowthHackEditForm.tsx @@ -0,0 +1,80 @@ +import gql from 'graphql-tag'; +import { IUser } from 'modules/auth/types'; +import { IOptions } from 'modules/boards/types'; +import { Alert, withProps } from 'modules/common/utils'; +import { mutations } from 'modules/forms/graphql'; +import { + IFormSubmission, + IFormSubmissionParams, + SaveFormSubmissionMutation +} from 'modules/forms/types'; +import React from 'react'; +import { compose, graphql } from 'react-apollo'; +import GrowthHackEditForm from '../components/GrowthHackEditForm'; +import { IGrowthHack, IGrowthHackParams } from '../types'; + +type Props = { + options: IOptions; + item: IGrowthHack; + users: IUser[]; + addItem: (doc: IGrowthHackParams, callback: () => void, msg?: string) => void; + saveItem: (doc: IGrowthHackParams, callback?: (item) => void) => void; + onUpdate: (item, prevStageId?: string) => void; + removeItem: (itemId: string, callback: () => void) => void; + beforePopupClose: () => void; +}; + +type FinalProps = { + saveFormSubmissionMutation: SaveFormSubmissionMutation; +} & Props; + +class GrowthHackEditFormContainer extends React.Component { + constructor(props) { + super(props); + + this.saveFormSubmission = this.saveFormSubmission.bind(this); + } + + saveFormSubmission = ({ + formId, + formSubmissions, + contentTypeId + }: IFormSubmission) => { + const { saveFormSubmissionMutation } = this.props; + + saveFormSubmissionMutation({ + variables: { + formId, + formSubmissions, + contentTypeId, + contentType: 'growthHack' + } + }) + .then(() => { + Alert.success('You successfully updated'); + }) + .catch(error => { + Alert.error(error.message); + }); + }; + + render() { + const extendedProps = { + ...this.props, + saveFormSubmission: this.saveFormSubmission + }; + + return ; + } +} + +export default withProps( + compose( + graphql( + gql(mutations.formSubmissionsSave), + { + name: 'saveFormSubmissionMutation' + } + ) + )(GrowthHackEditFormContainer) +); diff --git a/src/modules/growthHacks/graphql/index.ts b/src/modules/growthHacks/graphql/index.ts new file mode 100644 index 00000000000..8cff0604ab6 --- /dev/null +++ b/src/modules/growthHacks/graphql/index.ts @@ -0,0 +1,4 @@ +import mutations from './mutations'; +import queries from './queries'; + +export { queries, mutations }; diff --git a/src/modules/growthHacks/graphql/mutations.ts b/src/modules/growthHacks/graphql/mutations.ts new file mode 100644 index 00000000000..59d663ce4ce --- /dev/null +++ b/src/modules/growthHacks/graphql/mutations.ts @@ -0,0 +1,119 @@ +const commonVariables = ` + $name: String, + $stageId: String, + $closeDate: Date, + $description: String, + $assignedUserIds: [String], + $order: Int, + $hackDescription: String, + $goal: String, + $hackStages: [String], + $priority: String, + $reach: Int, + $impact: Int, + $confidence: Int, + $ease: Int, + $attachments: [AttachmentInput] +`; + +const commonParams = ` + name: $name, + stageId: $stageId, + closeDate: $closeDate, + description: $description, + assignedUserIds: $assignedUserIds, + order: $order, + hackDescription: $hackDescription, + goal: $goal, + hackStages: $hackStages, + priority: $priority, + attachments: $attachments, + reach: $reach, + impact: $impact, + confidence: $confidence, + ease: $ease +`; + +const commonReturn = ` + _id + name + stageId + closeDate + description + assignedUsers { + _id + email + details { + fullName + avatar + } + } + hackDescription + priority + reach + impact + confidence + ease + scoringType + goal + modifiedAt + modifiedBy +`; + +const growthHacksAdd = ` + mutation growthHacksAdd(${commonVariables}) { + growthHacksAdd(${commonParams}) { + ${commonReturn} + } + } +`; + +const growthHacksEdit = ` + mutation growthHacksEdit($_id: String!, ${commonVariables}) { + growthHacksEdit(_id: $_id, ${commonParams}) { + ${commonReturn} + } + } +`; + +const growthHacksRemove = ` + mutation growthHacksRemove($_id: String!) { + growthHacksRemove(_id: $_id) { + _id + } + } +`; + +const growthHacksChange = ` + mutation growthHacksChange($_id: String!, $destinationStageId: String!) { + growthHacksChange(_id: $_id, destinationStageId: $destinationStageId) { + _id + } + } +`; + +const growthHacksUpdateOrder = ` + mutation growthHacksUpdateOrder($stageId: String!, $orders: [OrderItem]) { + growthHacksUpdateOrder(stageId: $stageId, orders: $orders) { + _id + } + } +`; + +const growthHacksWatch = ` + mutation growthHacksWatch($_id: String!, $isAdd: Boolean!) { + growthHacksWatch(_id: $_id, isAdd: $isAdd) { + _id + isWatched + } + } +`; + +export default { + growthHacksAdd, + growthHacksEdit, + growthHacksRemove, + growthHacksChange, + growthHacksUpdateOrder, + growthHacksWatch +}; diff --git a/src/modules/growthHacks/graphql/queries.ts b/src/modules/growthHacks/graphql/queries.ts new file mode 100644 index 00000000000..f5b0799431e --- /dev/null +++ b/src/modules/growthHacks/graphql/queries.ts @@ -0,0 +1,97 @@ +const commonParams = ` + $assignedUserIds: [String], + $nextDay: String, + $nextWeek: String, + $nextMonth: String, + $noCloseDate: String, + $overdue: String, +`; + +const commonParamDefs = ` + assignedUserIds: $assignedUserIds, + nextDay: $nextDay, + nextWeek: $nextWeek, + nextMonth: $nextMonth, + noCloseDate: $noCloseDate, + overdue: $overdue, +`; + +const growthHackFields = ` + _id + name + stageId + pipeline { + _id + name + } + boardId + closeDate + description + hackDescription + hackStages + priority + goal + reach + impact + confidence + ease + scoringType + assignedUsers { + _id + email + details { + fullName + avatar + } + } + stage { + probability + name + } + isWatched + attachments { + name + url + type + size + } + formSubmissions + formId + modifiedAt + modifiedBy +`; + +const growthHacks = ` + query growthHacks( + $pipelineId: String, + $stageId: String, + $date: ItemDate, + $skip: Int, + $search: String, + ${commonParams} + ) { + growthHacks( + pipelineId: $pipelineId, + stageId: $stageId, + date: $date, + skip: $skip, + search: $search, + ${commonParamDefs} + ) { + ${growthHackFields} + } + } +`; + +const growthHackDetail = ` + query growthHackDetail($_id: String!) { + growthHackDetail(_id: $_id) { + ${growthHackFields} + } + } +`; + +export default { + growthHacks, + growthHackDetail +}; diff --git a/src/modules/growthHacks/options.ts b/src/modules/growthHacks/options.ts new file mode 100644 index 00000000000..6a15fbde9ef --- /dev/null +++ b/src/modules/growthHacks/options.ts @@ -0,0 +1,57 @@ +import { toArray } from 'modules/boards/utils'; +import PortableGrowthHack from 'modules/growthHacks/components/PortableGrowthHack'; +import GrowthHackEditForm from 'modules/growthHacks/containers/GrowthHackEditForm'; +import GrowthHackItem from './components/GrowthHackItem'; +import { mutations, queries } from './graphql'; + +const options = { + EditForm: GrowthHackEditForm, + PortableItem: PortableGrowthHack, + Item: GrowthHackItem, + type: 'growthHack', + title: 'Growth hack', + queriesName: { + itemsQuery: 'growthHacks', + detailQuery: 'growthHackDetail' + }, + mutationsName: { + addMutation: 'growthHacksAdd', + editMutation: 'growthHacksEdit', + removeMutation: 'growthHacksRemove', + changeMutation: 'growthHacksChange', + updateOrderMutation: 'growthHacksUpdateOrder', + watchMutation: 'growthHacksWatch' + }, + queries: { + itemsQuery: queries.growthHacks, + detailQuery: queries.growthHackDetail + }, + mutations: { + addMutation: mutations.growthHacksAdd, + editMutation: mutations.growthHacksEdit, + removeMutation: mutations.growthHacksRemove, + changeMutation: mutations.growthHacksChange, + updateOrderMutation: mutations.growthHacksUpdateOrder, + watchMutation: mutations.growthHacksWatch + }, + texts: { + addText: 'Add a growth hack', + addSuccessText: 'You successfully added a growth hack', + updateSuccessText: 'You successfully updated a growth hack', + deleteSuccessText: 'You successfully deleted a growth hack', + copySuccessText: 'You successfully copied a growth hack', + changeSuccessText: 'You successfully changed a growth hack' + }, + getExtraParams: (queryParams: any) => { + const { priority } = queryParams; + const extraParams: any = {}; + + if (priority) { + extraParams.priority = toArray(priority); + } + + return extraParams; + } +}; + +export default options; diff --git a/src/modules/growthHacks/routes.tsx b/src/modules/growthHacks/routes.tsx new file mode 100644 index 00000000000..93f68943b1c --- /dev/null +++ b/src/modules/growthHacks/routes.tsx @@ -0,0 +1,54 @@ +import { getDefaultBoardAndPipelines } from 'modules/boards/utils'; +import asyncComponent from 'modules/common/components/AsyncComponent'; +import queryString from 'query-string'; +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; + +const GrowthHackBoard = asyncComponent(() => + import(/* webpackChunkName: "GrowthHackBoard" */ './components/GrowthHackBoard') +); + +const growthHacks = () => { + let link = '/growthHack/board'; + + const { defaultBoards, defaultPipelines } = getDefaultBoardAndPipelines(); + + const [defaultBoardId, defaultPipelineId] = [ + defaultBoards.growthHack, + defaultPipelines.growthHack + ]; + + if (defaultBoardId && defaultPipelineId) { + link = `/growthHack/board?id=${defaultBoardId}&pipelineId=${defaultPipelineId}`; + } + + return ; +}; + +const boards = ({ location }) => { + const queryParams = queryString.parse(location.search); + + return ; +}; + +const routes = () => { + return ( + <> + + + + + ); +}; + +export default routes; diff --git a/src/modules/growthHacks/styles.ts b/src/modules/growthHacks/styles.ts new file mode 100644 index 00000000000..807b3ce191d --- /dev/null +++ b/src/modules/growthHacks/styles.ts @@ -0,0 +1,112 @@ +import { colors, dimensions } from 'modules/common/styles'; +import styled from 'styled-components'; + +const ScoreWrapper = styled.div``; + +const CalculatedAmount = styled.div` + font-size: 32px; + font-weight: bold; + color: ${colors.colorPrimaryDark}; + margin-left: 40px; + text-align: right; + position: relative; + + &:after { + content: '\\e945'; + font-family: 'erxes'; + position: absolute; + left: -20px; + color: ${colors.colorSecondary}; + font-size: 12px; + display: none; + top: 15px; + } + + &:hover { + cursor: pointer; + + &:after { + display: block; + } + } +`; + +const Amounts = styled.div` + display: flex; + flex-direction: column; + padding: 40px 30px; +`; + +const Factor = styled.div` + display: flex; +`; + +const AmountItem = styled.div` + margin-left: ${dimensions.coreSpacing}px; + position: relative; + text-align: center; + + > span { + text-transform: capitalize; + } + + input { + text-align: center; + } + + &:nth-of-type(1) { + color: ${colors.colorCoreRed}; + margin: 0; + + &:after { + content: ''; + } + } + + &:nth-of-type(2) { + color: ${colors.colorCoreBlue}; + } + + &:nth-of-type(3) { + color: ${colors.colorCoreGreen}; + } + + &:nth-of-type(4) { + color: ${colors.colorCoreTeal}; + } + + &:nth-of-type(4):after { + content: '\/'; + left: -10px; + } + + &:after { + content: '\\ecdb'; + font-family: 'erxes'; + position: absolute; + left: -17px; + bottom: 7px; + color: ${colors.colorCoreGray}; + } +`; + +const ScoreAmount = styled.div` + position: absolute; + right: 0; + top: 5px; + border-radius: 2px; + background: ${colors.colorCoreTeal}; + padding: 0 5px; + color: ${colors.colorWhite}; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +`; + +export { + ScoreWrapper, + CalculatedAmount, + Amounts, + AmountItem, + Factor, + ScoreAmount +}; diff --git a/src/modules/growthHacks/types.ts b/src/modules/growthHacks/types.ts new file mode 100644 index 00000000000..83d7a18a4ba --- /dev/null +++ b/src/modules/growthHacks/types.ts @@ -0,0 +1,38 @@ +import { IItem, IItemParams } from 'modules/boards/types'; +import { IActivityLogForMonth } from '../activityLogs/types'; + +export type ActivityLogQueryResponse = { + activityLogs: IActivityLogForMonth[]; + loading: boolean; +}; + +export interface IGrowthHack extends IItem { + hackDescription?: string; + goal?: string; + hackStages?: string[]; + formId?: string; + formSubmissions?: any; + scoringType?: string; + reach?: number; + impact?: number; + confidence?: number; + ease?: number; +} + +export interface IGrowthHackParams extends IItemParams { + hackDescription?: string; + goal?: string; + hackStages?: string[]; + priority?: string; + formId?: string; + formSubmissions?: any; + reach?: number; + impact?: number; + confidence?: number; + ease?: number; +} + +export interface IFormField { + name: string; + value: string; +} diff --git a/src/modules/inbox/components/conversationDetail/workarea/Participators.tsx b/src/modules/inbox/components/conversationDetail/workarea/Participators.tsx index e8e5aeeed52..4c23343b7d7 100644 --- a/src/modules/inbox/components/conversationDetail/workarea/Participators.tsx +++ b/src/modules/inbox/components/conversationDetail/workarea/Participators.tsx @@ -1,15 +1,15 @@ +import { IUser } from 'modules/auth/types'; import Tip from 'modules/common/components/Tip'; import { colors } from 'modules/common/styles'; import { __, getUserAvatar } from 'modules/common/utils'; import React from 'react'; import styled from 'styled-components'; -import { IUser } from '../../../../auth/types'; const spacing = 30; const ParticipatorWrapper = styled.div` display: inline-block; - margin-left: ${spacing}px; + margin-left: 10px; &:hover { cursor: pointer; @@ -31,7 +31,7 @@ const More = styled(ParticipatorImg.withComponent('span'))` vertical-align: middle; font-size: 10px; background: ${colors.colorCoreLightGray}; - line-height: ${spacing - 2}px; + line-height: ${spacing - 4}px; `; type Props = { diff --git a/src/modules/inbox/styles.ts b/src/modules/inbox/styles.ts index 7878beb0cc6..2fe975a2aad 100644 --- a/src/modules/inbox/styles.ts +++ b/src/modules/inbox/styles.ts @@ -290,10 +290,6 @@ const AssignTrigger = styled.div` transform: rotate(180deg); } } - - img { - margin: 0; - } `; const MaskWrapper = styled.div` diff --git a/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx b/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx index 1181afe895d..a12a4838b85 100644 --- a/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx +++ b/src/modules/knowledgeBase/components/knowledge/KnowledgeForm.tsx @@ -15,11 +15,13 @@ import { IFormProps } from 'modules/common/types'; import { __ } from 'modules/common/utils'; +import { FlexContent } from 'modules/layout/styles'; import { IBrand } from 'modules/settings/brands/types'; import SelectBrand from 'modules/settings/integrations/containers/SelectBrand'; import { ColorPick, ColorPicker, + ExpandWrapper, MarkdownWrapper } from 'modules/settings/styles'; import React from 'react'; @@ -250,22 +252,40 @@ class KnowledgeForm extends React.Component { onChange={this.handleBrandChange} /> + + + + Language + + + + + + + - - Choose a custom color -
    - - - - - -
    -
    + + Custom color +
    + + + + + +
    +
    +
    Background image: @@ -286,21 +306,6 @@ class KnowledgeForm extends React.Component { /> - - Language - - - - - - - {this.renderInstallCode()} ); diff --git a/src/modules/layout/components/Navigation.tsx b/src/modules/layout/components/Navigation.tsx index 1838e78a05a..eaf28bb8e81 100644 --- a/src/modules/layout/components/Navigation.tsx +++ b/src/modules/layout/components/Navigation.tsx @@ -155,6 +155,13 @@ class Navigation extends React.Component<{ + + + + + + + @@ -171,7 +178,7 @@ class Navigation extends React.Component<{ - + diff --git a/src/modules/layout/styles.ts b/src/modules/layout/styles.ts index 02508ecf05f..217beeb07eb 100644 --- a/src/modules/layout/styles.ts +++ b/src/modules/layout/styles.ts @@ -103,6 +103,10 @@ const BarItems = styled.div` const HeaderItems = styledTS<{ rightAligned?: boolean }>(styled.div)` align-self: center; margin-left: ${props => props.rightAligned && 'auto'}; + + > * + * { + margin-left: ${dimensions.unitSpacing}px; + } `; const SideContent = styledTS<{ diff --git a/src/modules/leads/components/Lead.tsx b/src/modules/leads/components/Lead.tsx new file mode 100644 index 00000000000..86ce137eb04 --- /dev/null +++ b/src/modules/leads/components/Lead.tsx @@ -0,0 +1,347 @@ +import Button from 'modules/common/components/Button'; +import FormControl from 'modules/common/components/form/Control'; +import ConditionsRule from 'modules/common/components/rule/ConditionsRule'; +import { Step, Steps } from 'modules/common/components/step'; +import { + StepWrapper, + TitleContainer +} from 'modules/common/components/step/styles'; +import { IConditionsRule } from 'modules/common/types'; +import { Alert } from 'modules/common/utils'; +import { __ } from 'modules/common/utils'; +import Wrapper from 'modules/layout/components/Wrapper'; +import { ILeadData, ILeadIntegration } from '../types'; + +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { ImportLoader } from 'modules/common/components/ButtonMutate'; +import { IFormData } from 'modules/forms/types'; +import { IField } from 'modules/settings/properties/types'; +import { + CallOut, + ChooseType, + FormStep, + FullPreviewStep, + OptionStep, + SuccessStep +} from './step'; + +type Props = { + integration?: ILeadIntegration; + loading?: boolean; + isActionLoading: boolean; + isReadyToSaveForm: boolean; + afterFormDbSave: (formId: string) => void; + save: ( + params: { + name: string; + brandId: string; + languageCode?: string; + leadData: ILeadData; + } + ) => void; +}; + +type State = { + activeStep?: number; + type: string; + brand?: string; + language?: string; + title?: string; + calloutTitle?: string; + bodyValue?: string; + calloutBtnText?: string; + theme: string; + logoPreviewUrl?: string; + isSkip?: boolean; + color: string; + logoPreviewStyle?: { opacity?: string }; + defaultValue: { [key: string]: boolean }; + logo?: string; + rules?: IConditionsRule[]; + formData: IFormData; + + successAction?: string; + fromEmail?: string; + userEmailTitle?: string; + userEmailContent?: string; + adminEmails?: string[]; + adminEmailTitle?: string; + adminEmailContent?: string; + thankContent?: string; + redirectUrl?: string; + carousel?: string; +}; + +class Lead extends React.Component { + constructor(props: Props) { + super(props); + + const integration = props.integration || ({} as ILeadIntegration); + + const { leadData = {} as ILeadData } = integration; + const callout = leadData.callout || {}; + const form = integration.form || {}; + + this.state = { + activeStep: 1, + + type: leadData.loadType || 'shoutbox', + successAction: leadData.successAction || '', + fromEmail: leadData.fromEmail || '', + userEmailTitle: leadData.userEmailTitle || '', + userEmailContent: leadData.userEmailContent || '', + adminEmails: leadData.adminEmails || [], + adminEmailTitle: leadData.adminEmailTitle || '', + adminEmailContent: leadData.adminEmailContent || '', + thankContent: leadData.thankContent || 'Thank you.', + redirectUrl: leadData.redirectUrl || '', + rules: leadData.rules || [], + + brand: integration.brandId, + language: integration.languageCode, + title: integration.name, + calloutTitle: callout.title || 'Title', + bodyValue: callout.body || '', + calloutBtnText: callout.buttonText || 'Start', + color: '', + logoPreviewStyle: {}, + defaultValue: {}, + logo: '', + formData: { + title: form.title || '', + desc: form.description || '', + btnText: form.buttonText || 'Send', + fields: [], + type: form.type || '' + }, + theme: leadData.themeColor || '#6569DF', + logoPreviewUrl: callout.featuredImage, + isSkip: callout.skip && true + }; + } + + handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const { brand, calloutTitle, title, rules } = this.state; + + if (!title) { + return Alert.error('Write title'); + } + + if (!brand) { + return Alert.error('Choose a brand'); + } + + const doc = { + name: title, + brandId: brand, + languageCode: this.state.language, + leadData: { + loadType: this.state.type, + successAction: this.state.successAction, + fromEmail: this.state.fromEmail, + userEmailTitle: this.state.userEmailTitle, + userEmailContent: this.state.userEmailContent, + adminEmails: this.state.adminEmails, + adminEmailTitle: this.state.adminEmailTitle, + adminEmailContent: this.state.adminEmailContent, + thankContent: this.state.thankContent, + redirectUrl: this.state.redirectUrl, + themeColor: this.state.theme || this.state.color, + callout: { + title: calloutTitle, + body: this.state.bodyValue, + buttonText: this.state.calloutBtnText, + featuredImage: this.state.logoPreviewUrl, + skip: this.state.isSkip + }, + rules: (rules || []).map(rule => ({ + _id: rule._id, + kind: rule.kind, + text: rule.text, + condition: rule.condition, + value: rule.value + })) + } + }; + + this.props.save(doc); + }; + + renderSaveButton = () => { + const { isActionLoading } = this.props; + + const cancelButton = ( + + + + ); + + return ( + + {cancelButton} + + + + ); + }; + + onChange = (key: string, value: any) => { + this.setState({ [key]: value } as any); + }; + + onFormDocChange = formData => { + this.setState({ formData }); + }; + + onFormInit = (fields: IField[]) => { + const formData = this.state.formData; + formData.fields = fields; + + this.setState({ formData }); + }; + + render() { + const { + activeStep, + calloutTitle, + type, + calloutBtnText, + bodyValue, + color, + theme, + logoPreviewUrl, + thankContent, + carousel, + language, + title, + successAction, + isSkip, + rules, + formData + } = this.state; + + const { integration } = this.props; + const leadData = integration && integration.leadData; + const brand = integration && integration.brand; + const breadcrumb = [{ title: __('Leads'), link: '/leads' }]; + const constant = isSkip ? 'form' : 'callout'; + + const onChange = e => + this.onChange('title', (e.currentTarget as HTMLInputElement).value); + + return ( + + + +
    {__('Title')}
    + + {this.renderSaveButton()} +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + ); + } +} + +export default Lead; diff --git a/src/modules/forms/components/List.tsx b/src/modules/leads/components/List.tsx similarity index 93% rename from src/modules/forms/components/List.tsx rename to src/modules/leads/components/List.tsx index 9d13729b1e2..a2d6178f48e 100644 --- a/src/modules/forms/components/List.tsx +++ b/src/modules/leads/components/List.tsx @@ -11,19 +11,19 @@ import TaggerPopover from 'modules/tags/components/TaggerPopover'; import React from 'react'; import { Link } from 'react-router-dom'; import { ITag } from '../../tags/types'; -import { IFormIntegration } from '../types'; +import { ILeadIntegration } from '../types'; import Row from './Row'; type Props = { - integrations: IFormIntegration[]; + integrations: ILeadIntegration[]; tags: ITag[]; - bulk: IFormIntegration[]; + bulk: ILeadIntegration[]; isAllSelected: boolean; emptyBulk: () => void; totalCount: number; tagsCount: { [key: string]: number }; - toggleBulk: (target: IFormIntegration, toAdd: boolean) => void; - toggleAll: (bulk: IFormIntegration[], name: string) => void; + toggleBulk: (target: ILeadIntegration, toAdd: boolean) => void; + toggleAll: (bulk: ILeadIntegration[], name: string) => void; loading: boolean; remove: (integrationId: string, callback: (error: Error) => void) => void; }; @@ -82,7 +82,7 @@ class List extends React.Component { } const actionBarRight = ( - + diff --git a/src/modules/forms/components/Manage.tsx b/src/modules/leads/components/Manage.tsx similarity index 96% rename from src/modules/forms/components/Manage.tsx rename to src/modules/leads/components/Manage.tsx index e1301631bcf..df39432020a 100644 --- a/src/modules/forms/components/Manage.tsx +++ b/src/modules/leads/components/Manage.tsx @@ -8,10 +8,10 @@ import { MarkdownWrapper } from 'modules/settings/styles'; import React from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import ReactMarkdown from 'react-markdown'; -import { IFormIntegration } from '../types'; +import { ILeadIntegration } from '../types'; type Props = { - integration: IFormIntegration; + integration: ILeadIntegration; closeModal: () => void; }; @@ -75,7 +75,7 @@ class Manage extends React.Component { // showed install code automatically in edit mode if (integration._id) { const brand = integration.brand; - const form = integration.form; + const form = integration.form || {}; code = getInstallCode(brand.code, form.code || ''); embedCode = getEmbedCode(form.code || ''); diff --git a/src/modules/forms/components/Row.tsx b/src/modules/leads/components/Row.tsx similarity index 87% rename from src/modules/forms/components/Row.tsx rename to src/modules/leads/components/Row.tsx index e756d08ca2b..1ce2cb82f4c 100644 --- a/src/modules/forms/components/Row.tsx +++ b/src/modules/leads/components/Row.tsx @@ -9,13 +9,13 @@ import Tip from 'modules/common/components/Tip'; import { __, Alert, confirm } from 'modules/common/utils'; import React from 'react'; import { Link } from 'react-router-dom'; -import { IFormIntegration } from '../types'; +import { ILeadIntegration } from '../types'; import Manage from './Manage'; type Props = { - integration: IFormIntegration; + integration: ILeadIntegration; - toggleBulk: (integration: IFormIntegration, checked: boolean) => void; + toggleBulk: (integration: ILeadIntegration, checked: boolean) => void; remove: (integrationId: string, callback: (error: Error) => void) => void; isChecked: boolean; @@ -41,8 +41,10 @@ class Row extends React.Component { }; manageAction(integration) { + const { formId } = integration; + return ( - +
  • @@ -125,7 +130,12 @@ class OptionStep extends React.Component { - + ); diff --git a/src/modules/forms/components/step/SuccessStep.tsx b/src/modules/leads/components/step/SuccessStep.tsx similarity index 88% rename from src/modules/forms/components/step/SuccessStep.tsx rename to src/modules/leads/components/step/SuccessStep.tsx index 58a0ec1132d..cc6c3c90ebe 100644 --- a/src/modules/forms/components/step/SuccessStep.tsx +++ b/src/modules/leads/components/step/SuccessStep.tsx @@ -2,8 +2,8 @@ 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 { LeftItem, Preview } from 'modules/common/components/step/styles'; +import { ILeadData } from 'modules/leads/types'; import React from 'react'; -import { IFormData } from '../../../settings/integrations/types'; import SuccessPreview from './preview/SuccessPreview'; import { FlexItem } from './style'; @@ -25,7 +25,7 @@ type Props = { thankContent?: string; successAction?: string; onChange: (name: Name, value: string) => void; - formData?: IFormData; + leadData?: ILeadData; }; type State = { @@ -36,10 +36,10 @@ class SuccessStep extends React.Component { constructor(props: Props) { super(props); - const formData = props.formData || {}; + const leadData = props.leadData || {}; this.state = { - successAction: formData.successAction || 'onPage' + successAction: leadData.successAction || 'onPage' }; } @@ -58,7 +58,7 @@ class SuccessStep extends React.Component { this.props.onChange(name, value); }; - renderEmailFields(formData: IFormData) { + renderEmailFields(leadData: ILeadData) { if (this.state.successAction !== 'email') { return null; } @@ -106,7 +106,7 @@ class SuccessStep extends React.Component { @@ -116,7 +116,7 @@ class SuccessStep extends React.Component { @@ -126,7 +126,7 @@ class SuccessStep extends React.Component { @@ -138,7 +138,7 @@ class SuccessStep extends React.Component { id="adminEmails" type="text" defaultValue={ - formData.adminEmails ? formData.adminEmails.join(',') : '' + leadData.adminEmails ? leadData.adminEmails.join(',') : '' } onChange={adminEmails} /> @@ -148,7 +148,7 @@ class SuccessStep extends React.Component { Admin email title @@ -159,7 +159,7 @@ class SuccessStep extends React.Component { @@ -168,7 +168,7 @@ class SuccessStep extends React.Component { ); } - renderRedirectUrl(formData) { + renderRedirectUrl(leadData) { if (this.state.successAction !== 'redirect') { return null; } @@ -185,7 +185,7 @@ class SuccessStep extends React.Component { Redirect url @@ -223,7 +223,7 @@ class SuccessStep extends React.Component { } render() { - const formData = this.props.formData || {}; + const leadData = this.props.leadData || {}; const { successAction } = this.state; return ( @@ -244,8 +244,8 @@ class SuccessStep extends React.Component { - {this.renderEmailFields(formData)} - {this.renderRedirectUrl(formData)} + {this.renderEmailFields(leadData)} + {this.renderRedirectUrl(leadData)} {this.renderThankContent()} diff --git a/src/modules/forms/components/step/index.ts b/src/modules/leads/components/step/index.ts similarity index 100% rename from src/modules/forms/components/step/index.ts rename to src/modules/leads/components/step/index.ts diff --git a/src/modules/forms/components/step/preview/CalloutPreview.tsx b/src/modules/leads/components/step/preview/CalloutPreview.tsx similarity index 100% rename from src/modules/forms/components/step/preview/CalloutPreview.tsx rename to src/modules/leads/components/step/preview/CalloutPreview.tsx diff --git a/src/modules/forms/components/step/preview/CommonPreview.tsx b/src/modules/leads/components/step/preview/CommonPreview.tsx similarity index 100% rename from src/modules/forms/components/step/preview/CommonPreview.tsx rename to src/modules/leads/components/step/preview/CommonPreview.tsx diff --git a/src/modules/leads/components/step/preview/FormPreview.tsx b/src/modules/leads/components/step/preview/FormPreview.tsx new file mode 100644 index 00000000000..82f92fd2198 --- /dev/null +++ b/src/modules/leads/components/step/preview/FormPreview.tsx @@ -0,0 +1,42 @@ +import { IField } from 'modules/settings/properties/types'; +import React from 'react'; +import CommonPreview from './CommonPreview'; + +type Props = { + previewRenderer: () => React.ReactNode; + title?: string; + desc?: string; + btnText?: string; + color: string; + theme: string; + fields?: IField[]; + onFieldEdit?: (field: IField, props) => void; + onChange?: (name: any, fields: string) => void; + onFieldChange?: (name: string, value: IField[]) => void; + type: string; +}; + +class FormPreview extends React.Component { + render() { + const { title, btnText, color, theme, type, previewRenderer } = this.props; + + if (!previewRenderer) { + return null; + } + + return ( + + {previewRenderer()} + + ); + } +} + +export default FormPreview; diff --git a/src/modules/forms/components/step/preview/SuccessPreview.tsx b/src/modules/leads/components/step/preview/SuccessPreview.tsx similarity index 100% rename from src/modules/forms/components/step/preview/SuccessPreview.tsx rename to src/modules/leads/components/step/preview/SuccessPreview.tsx diff --git a/src/modules/leads/components/step/preview/index.ts b/src/modules/leads/components/step/preview/index.ts new file mode 100644 index 00000000000..5f74968713e --- /dev/null +++ b/src/modules/leads/components/step/preview/index.ts @@ -0,0 +1,6 @@ +import CalloutPreview from './CalloutPreview'; +import CommonPreview from './CommonPreview'; +import FormPreview from './FormPreview'; +import SuccessPreview from './SuccessPreview'; + +export { CommonPreview, CalloutPreview, FormPreview, SuccessPreview }; diff --git a/src/modules/forms/components/step/preview/styles.ts b/src/modules/leads/components/step/preview/styles.ts similarity index 81% rename from src/modules/forms/components/step/preview/styles.ts rename to src/modules/leads/components/step/preview/styles.ts index 0a12786b717..ce202c84b25 100644 --- a/src/modules/forms/components/step/preview/styles.ts +++ b/src/modules/leads/components/step/preview/styles.ts @@ -1,8 +1,4 @@ -import { - Formgroup, - Label, - SelectWrapper -} from 'modules/common/components/form/styles'; +import { Formgroup, Label } from 'modules/common/components/form/styles'; import { colors, dimensions } from 'modules/common/styles'; import { rgba } from 'modules/common/styles/color'; import { @@ -100,8 +96,6 @@ const SlideRightContent = styled(SlideLeftContent)` const BodyContent = styled.div` span { - padding-left: 5px; - &:after { left: ${dimensions.unitSpacing - 1}px; } @@ -109,7 +103,6 @@ const BodyContent = styled.div` input, textarea { - border-radius: ${dimensions.unitSpacing}px !important; margin-top: 5px !important; } @@ -135,6 +128,7 @@ const BodyContent = styled.div` > div { width: 100%; + flex: 1; } ${DragHandler} { @@ -241,55 +235,6 @@ const Embedded = styled.div` } `; -const FieldItem = styledTS<{ selectType?: boolean; noPadding?: boolean }>( - styled.div -)` - padding: ${props => !props.noPadding && `0 ${dimensions.unitSpacing}px`}; - - input, - textarea, - select { - box-sizing: border-box; - transition: all 0.3s ease-in-out; - background: #faf9fb; - border: 1px solid ${colors.colorShadowGray}; - border-radius: 5px !important; - box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.07); - color: #1a1a1a; - display: block; - font-size: 14px; - height: 36px; - line-height: 1.42857143; - margin-top: ${props => !props.selectType && `${dimensions.unitSpacing}px`}; - outline: 0; - padding: 6px 15px; - width: 100%; - - - &:focus { - border-color: ${colors.colorShadowGray}; - background: ${colors.colorWhite}; - } - - &:after { - top: 22px; - } - } - - textarea { - overflow: auto; - height: auto; - } - - .required { - color: ${colors.colorCoreRed}; - } - - ${SelectWrapper} { - margin-top: ${dimensions.unitSpacing}px; - } -`; - const ThankContent = styled.div` text-align: center; `; @@ -306,7 +251,6 @@ export { PopUpContainer, OverlayTrigger, Embedded, - FieldItem, ThankContent, PreviewContainer }; diff --git a/src/modules/forms/components/step/style.ts b/src/modules/leads/components/step/style.ts similarity index 98% rename from src/modules/forms/components/step/style.ts rename to src/modules/leads/components/step/style.ts index 6c61d5da29e..902bb1a824d 100644 --- a/src/modules/forms/components/step/style.ts +++ b/src/modules/leads/components/step/style.ts @@ -9,8 +9,6 @@ import { Embedded, PreviewContainer, SlideLeftContent } from './preview/styles'; const Space = `${dimensions.unitSpacing + dimensions.coreSpacing}px`; const Box = styledTS<{ selected?: boolean }>(styled(BoxRoot))` - border: 1px solid - ${props => (props.selected ? colors.colorSecondary : colors.borderPrimary)}; padding: ${dimensions.coreSpacing * 2}px; width: 50%; background: ${colors.bgLight}; diff --git a/src/modules/leads/containers/CreateLead.tsx b/src/modules/leads/containers/CreateLead.tsx new file mode 100644 index 00000000000..b75a35d56e3 --- /dev/null +++ b/src/modules/leads/containers/CreateLead.tsx @@ -0,0 +1,102 @@ +import gql from 'graphql-tag'; +import { Alert, withProps } from 'modules/common/utils'; +import { + AddIntegrationMutationResponse, + AddIntegrationMutationVariables +} from 'modules/settings/integrations/types'; +import { AddFieldsMutationResponse } from 'modules/settings/properties/types'; +import React from 'react'; +import { compose, graphql } from 'react-apollo'; +import { withRouter } from 'react-router'; +import { IRouterProps } from '../../common/types'; +import Lead from '../components/Lead'; +import { mutations } from '../graphql'; +import { ILeadData } from '../types'; + +type Props = {} & IRouterProps & + AddIntegrationMutationResponse & + AddFieldsMutationResponse; + +type State = { + isLoading: boolean; + isReadyToSaveForm: boolean; + doc?: { + brandId: string; + name: string; + languageCode: string; + lead: any; + leadData: ILeadData; + }; +}; + +class CreateLeadContainer extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { isLoading: false, isReadyToSaveForm: false }; + } + + render() { + const { addIntegrationMutation, history } = this.props; + + const afterFormDbSave = id => { + this.setState({ isReadyToSaveForm: false }); + + if (this.state.doc) { + const { leadData, brandId, name, languageCode } = this.state.doc; + + addIntegrationMutation({ + variables: { + formId: id, + leadData, + brandId, + name, + languageCode + } + }) + .then(() => { + Alert.success('You successfully added a lead'); + history.push('/leads'); + + this.setState({ isLoading: false }); + }) + + .catch(error => { + Alert.error(error.message); + + this.setState({ isLoading: false }); + }); + } + }; + + const save = doc => { + this.setState({ isLoading: true, isReadyToSaveForm: true, doc }); + }; + + const updatedProps = { + ...this.props, + fields: [], + save, + afterFormDbSave, + isActionLoading: this.state.isLoading, + isReadyToSaveForm: this.state.isReadyToSaveForm + }; + + return ; + } +} + +export default withProps<{}>( + compose( + graphql< + {}, + AddIntegrationMutationResponse, + AddIntegrationMutationVariables + >(gql(mutations.integrationsCreateLeadIntegration), { + name: 'addIntegrationMutation', + options: { + refetchQueries: ['leadIntegrations', 'leadIntegrationCounts'] + } + }) + )(withRouter(CreateLeadContainer)) +); diff --git a/src/modules/leads/containers/EditLead.tsx b/src/modules/leads/containers/EditLead.tsx new file mode 100644 index 00000000000..7e0737b071f --- /dev/null +++ b/src/modules/leads/containers/EditLead.tsx @@ -0,0 +1,136 @@ +import gql from 'graphql-tag'; +import { Alert, withProps } from 'modules/common/utils'; +import { + EditIntegrationMutationResponse, + EditIntegrationMutationVariables, + LeadIntegrationDetailQueryResponse +} from 'modules/settings/integrations/types'; +import React from 'react'; +import { compose, graphql } from 'react-apollo'; +import { withRouter } from 'react-router'; +import { IRouterProps } from '../../common/types'; +import Lead from '../components/Lead'; +import { mutations, queries } from '../graphql'; +import { ILeadData } from '../types'; + +type Props = { + contentTypeId: string; + formId: string; + queryParams: any; +}; + +type State = { + isLoading: boolean; + isReadyToSaveForm: boolean; + doc?: { + brandId: string; + name: string; + languageCode: string; + lead: any; + leadData: ILeadData; + }; +}; + +type FinalProps = { + integrationDetailQuery: LeadIntegrationDetailQueryResponse; +} & Props & + EditIntegrationMutationResponse & + IRouterProps; + +class EditLeadContainer extends React.Component { + constructor(props: FinalProps) { + super(props); + + this.state = { isLoading: false, isReadyToSaveForm: false }; + } + + render() { + const { + formId, + integrationDetailQuery, + editIntegrationMutation, + history + } = this.props; + + if (integrationDetailQuery.loading) { + return false; + } + + const integration = integrationDetailQuery.integrationDetail || {}; + + const afterFormDbSave = () => { + if (this.state.doc) { + const { leadData, brandId, name, languageCode } = this.state.doc; + + editIntegrationMutation({ + variables: { + _id: integration._id, + formId, + leadData, + brandId, + name, + languageCode + } + }) + .then(() => { + Alert.success('You successfully updated a lead'); + + history.push('/leads'); + + this.setState({ isReadyToSaveForm: false, isLoading: false }); + }) + + .catch(error => { + Alert.error(error.message); + + this.setState({ isReadyToSaveForm: false, isLoading: false }); + }); + } + }; + + const save = doc => { + this.setState({ isLoading: true, isReadyToSaveForm: true, doc }); + }; + + const updatedProps = { + ...this.props, + integration, + save, + afterFormDbSave, + isActionLoading: this.state.isLoading, + isReadyToSaveForm: this.state.isReadyToSaveForm + }; + + return ; + } +} + +export default withProps( + compose( + graphql( + gql(queries.integrationDetail), + { + name: 'integrationDetailQuery', + options: ({ contentTypeId }) => ({ + variables: { + _id: contentTypeId + } + }) + } + ), + graphql< + Props, + EditIntegrationMutationResponse, + EditIntegrationMutationVariables + >(gql(mutations.integrationsEditLeadIntegration), { + name: 'editIntegrationMutation', + options: { + refetchQueries: [ + 'leadIntegrations', + 'leadIntegrationCounts', + 'formDetail' + ] + } + }) + )(withRouter(EditLeadContainer)) +); diff --git a/src/modules/forms/containers/List.tsx b/src/modules/leads/containers/List.tsx similarity index 93% rename from src/modules/forms/containers/List.tsx rename to src/modules/leads/containers/List.tsx index 34b0dc53aa5..e1b68330217 100755 --- a/src/modules/forms/containers/List.tsx +++ b/src/modules/leads/containers/List.tsx @@ -9,7 +9,7 @@ import List from '../components/List'; import { mutations, queries } from '../graphql'; import { CountQueryResponse, - FormIntegrationsQueryResponse, + LeadIntegrationsQueryResponse, RemoveMutationResponse, RemoveMutationVariables } from '../types'; @@ -20,7 +20,7 @@ type Props = { type FinalProps = { integrationsTotalCountQuery: CountQueryResponse; - integrationsQuery: FormIntegrationsQueryResponse; + integrationsQuery: LeadIntegrationsQueryResponse; tagsQuery: TagsQueryResponse; } & RemoveMutationResponse & Props; @@ -37,7 +37,7 @@ class ListContainer extends React.Component { const counts = integrationsTotalCountQuery.integrationsTotalCount || { byKind: {} }; - const totalCount = counts.byKind.form || 0; + const totalCount = counts.byKind.lead || 0; const tagsCount = counts.byTag || {}; const integrations = integrationsQuery.integrations || []; @@ -85,7 +85,7 @@ export default withProps( compose( graphql< Props, - FormIntegrationsQueryResponse, + LeadIntegrationsQueryResponse, { page?: number; perPage?: number; tag?: string; kind?: string } >(gql(queries.integrations), { name: 'integrationsQuery', @@ -94,7 +94,7 @@ export default withProps( variables: { ...generatePaginationParams(queryParams), tag: queryParams.tag, - kind: 'form' + kind: 'lead' } }; } diff --git a/src/modules/leads/graphql/index.ts b/src/modules/leads/graphql/index.ts new file mode 100644 index 00000000000..8cff0604ab6 --- /dev/null +++ b/src/modules/leads/graphql/index.ts @@ -0,0 +1,4 @@ +import mutations from './mutations'; +import queries from './queries'; + +export { queries, mutations }; diff --git a/src/modules/leads/graphql/mutations.ts b/src/modules/leads/graphql/mutations.ts new file mode 100644 index 00000000000..e70f2cada17 --- /dev/null +++ b/src/modules/leads/graphql/mutations.ts @@ -0,0 +1,43 @@ +const commonFormParamsDef = ` + $name: String!, + $brandId: String!, + $formId: String!, + $languageCode: String, + $leadData: IntegrationLeadData! +`; + +const commonFormParams = ` + name: $name, + brandId: $brandId, + formId: $formId, + languageCode: $languageCode, + leadData: $leadData +`; + +const integrationRemove = ` + mutation integrationsRemove($_id: String!) { + integrationsRemove(_id: $_id) + } +`; + +const integrationsCreateLeadIntegration = ` + mutation integrationsCreateLeadIntegration(${commonFormParamsDef}) { + integrationsCreateLeadIntegration(${commonFormParams}) { + _id + } + } +`; + +const integrationsEditLeadIntegration = ` + mutation integrationsEditLeadIntegration($_id: String!, ${commonFormParamsDef}) { + integrationsEditLeadIntegration(_id: $_id, ${commonFormParams}) { + _id + } + } +`; + +export default { + integrationRemove, + integrationsEditLeadIntegration, + integrationsCreateLeadIntegration +}; diff --git a/src/modules/leads/graphql/queries.ts b/src/modules/leads/graphql/queries.ts new file mode 100644 index 00000000000..0485d2d105f --- /dev/null +++ b/src/modules/leads/graphql/queries.ts @@ -0,0 +1,123 @@ +const integrations = ` + query leadIntegrations($perPage: Int, $page: Int, $kind: String, $tag: String) { + integrations(perPage: $perPage, page: $page, kind: $kind, tag: $tag) { + _id + brandId + name + kind + code + brand { + _id + name + code + } + languageCode + leadData + formId + tags { + _id + name + colorCode + } + tagIds + form { + _id + title + code + description + type + buttonText + createdDate + createdUserId + createdUser { + _id + details { + avatar + fullName + position + } + } + } + } + } +`; + +const integrationDetail = ` + query integrationDetail($_id: String!) { + integrationDetail(_id: $_id) { + _id + kind + name + brand { + _id + name + code + } + languageCode + brandId + code + formId + leadData + tagIds + tags { + _id + name + colorCode + } + form { + _id + title + code + description + type + buttonText + createdDate + createdUserId + createdUser { + _id + details { + avatar + fullName + position + } + } + } + } + } +`; + +const integrationsTotalCount = ` + query integrationsTotalCount { + integrationsTotalCount { + byKind + byTag + } + } +`; +const tags = ` + query tags($type: String) { + tags(type: $type) { + _id + name + type + colorCode + } + } +`; + +const forms = ` + query forms { + forms { + _id + title + } + } +`; + +export default { + integrations, + integrationDetail, + integrationsTotalCount, + tags, + forms +}; diff --git a/src/modules/forms/routes.tsx b/src/modules/leads/routes.tsx similarity index 58% rename from src/modules/forms/routes.tsx rename to src/modules/leads/routes.tsx index 7552928dd46..8614ef60a68 100644 --- a/src/modules/forms/routes.tsx +++ b/src/modules/leads/routes.tsx @@ -3,12 +3,12 @@ import queryString from 'query-string'; import React from 'react'; import { Route } from 'react-router-dom'; -const CreateForm = asyncComponent(() => - import(/* webpackChunkName: "CreateForm" */ './containers/CreateForm') +const CreateLead = asyncComponent(() => + import(/* webpackChunkName: "CreateLead" */ './containers/CreateLead') ); -const EditForm = asyncComponent(() => - import(/* webpackChunkName: "EditForm" */ './containers/EditForm') +const EditLead = asyncComponent(() => + import(/* webpackChunkName: "EditLead" */ './containers/EditLead') ); const List = asyncComponent(() => @@ -20,19 +20,19 @@ const forms = ({ location }) => { return ; }; -const createForm = () => { - return ; +const createLead = () => { + return ; }; -const editForm = ({ match, location }) => { +const editLead = ({ match, location }) => { const { contentTypeId, formId } = match.params; const queryParams = queryString.parse(location.search); return ( - ); }; @@ -40,20 +40,20 @@ const editForm = ({ match, location }) => { const routes = () => { return ( - + ); diff --git a/src/modules/leads/types.ts b/src/modules/leads/types.ts new file mode 100644 index 00000000000..34942e43ff1 --- /dev/null +++ b/src/modules/leads/types.ts @@ -0,0 +1,79 @@ +import { IConditionsRule } from 'modules/common/types'; +import { IUser } from '../auth/types'; +import { IForm } from '../forms/types'; +import { IBrand } from '../settings/brands/types'; +import { IIntegration } from '../settings/integrations/types'; +import { ITag } from '../tags/types'; + +export interface ICallout { + title?: string; + body?: string; + buttonText?: string; + featuredImage?: string; + skip?: boolean; +} + +export interface ILeadData { + loadType?: string; + successAction?: string; + fromEmail?: string; + userEmailTitle?: string; + userEmailContent?: string; + adminEmails?: string[]; + adminEmailTitle?: string; + adminEmailContent?: string; + thankContent?: string; + redirectUrl?: string; + themeColor?: string; + callout?: ICallout; + rules?: IConditionsRule[]; + createdUserId?: string; + createdUser?: IUser; + createdDate?: Date; + viewCount?: number; + contactsGathered?: number; + tagIds?: string[]; + getTags?: ITag[]; + form?: IForm; +} + +export interface ILeadIntegration extends IIntegration { + brand: IBrand; + tags: ITag[]; + createdUser: IUser; +} + +export type RemoveMutationVariables = { + _id: string; +}; + +export type RemoveMutationResponse = { + removeMutation: ( + params: { variables: RemoveMutationVariables } + ) => Promise; +}; + +// query types +export type LeadIntegrationsQueryResponse = { + integrations: ILeadIntegration; + loading: boolean; + refetch: () => void; +}; + +export type Counts = { + [key: string]: number; +}; + +export type IntegrationsCount = { + total: number; + byTag: Counts; + byChannel: Counts; + byBrand: Counts; + byKind: Counts; +}; + +export type CountQueryResponse = { + integrationsTotalCount: IntegrationsCount; + loading: boolean; + refetch: () => void; +}; diff --git a/src/modules/notifications/graphql/subscriptions.ts b/src/modules/notifications/graphql/subscriptions.ts index 6bf7931b671..94c7cc0068b 100644 --- a/src/modules/notifications/graphql/subscriptions.ts +++ b/src/modules/notifications/graphql/subscriptions.ts @@ -1,6 +1,7 @@ const notificationSubscription = ` subscription notificationInserted($userId: String) { notificationInserted(userId: $userId) { + _id title content } diff --git a/src/modules/settings/boards/components/BoardForm.tsx b/src/modules/settings/boards/components/BoardForm.tsx index a57346894cf..747328e1118 100755 --- a/src/modules/settings/boards/components/BoardForm.tsx +++ b/src/modules/settings/boards/components/BoardForm.tsx @@ -45,6 +45,7 @@ class BoardForm extends React.Component { name="name" defaultValue={object.name} required={true} + autoFocus={true} /> diff --git a/src/modules/settings/boards/components/Boards.tsx b/src/modules/settings/boards/components/Boards.tsx index 3698e245c0d..3ac0bb29b19 100644 --- a/src/modules/settings/boards/components/Boards.tsx +++ b/src/modules/settings/boards/components/Boards.tsx @@ -7,6 +7,7 @@ import { __ } from 'modules/common/utils'; import Sidebar from 'modules/layout/components/Sidebar'; import { HelperButtons, SidebarList as List } from 'modules/layout/styles'; import React from 'react'; +import { IOption } from '../types'; import BoardForm from './BoardForm'; import BoardRow from './BoardRow'; @@ -17,6 +18,7 @@ type Props = { remove: (boardId: string) => void; renderButton: (props: IButtonMutateProps) => JSX.Element; loading: boolean; + options?: IOption; }; class Boards extends React.Component { @@ -40,9 +42,11 @@ class Boards extends React.Component { } renderSidebarHeader() { - const { renderButton, type } = this.props; + const { renderButton, type, options } = this.props; const { Header } = Sidebar; + const boardName = options ? options.boardName : 'Board'; + const addBoard = ( {renderButton({ - name: 'pipeline', + name: pipelineName, values: this.generateDoc(values), isSubmitted, callback: closeModal, @@ -226,12 +252,7 @@ class PipelineForm extends React.Component { } return ( - + ); diff --git a/src/modules/settings/boards/components/PipelineRow.tsx b/src/modules/settings/boards/components/PipelineRow.tsx index 32e7ecfb298..84ef14c9d68 100644 --- a/src/modules/settings/boards/components/PipelineRow.tsx +++ b/src/modules/settings/boards/components/PipelineRow.tsx @@ -6,6 +6,7 @@ import { IButtonMutateProps } from 'modules/common/types'; import React from 'react'; import PipelineForm from '../containers/PipelineForm'; import { PipelineRowContainer } from '../styles'; +import { IOption } from '../types'; type Props = { pipeline: IPipeline; @@ -13,6 +14,7 @@ type Props = { remove: (pipelineId: string) => void; onTogglePopup: () => void; type: string; + options?: IOption; }; type State = { @@ -52,7 +54,7 @@ class PipelineRow extends React.Component { } renderEditForm() { - const { renderButton, type, pipeline } = this.props; + const { renderButton, type, pipeline, options } = this.props; const closeModal = () => { this.setState({ showModal: false }); @@ -62,6 +64,7 @@ class PipelineRow extends React.Component { return ( void; boardId: string; + options?: IOption; + refetch: ({ boardId }: { boardId?: string }) => Promise; }; type State = { @@ -45,12 +48,13 @@ class Pipelines extends React.Component { } renderAddForm = () => { - const { boardId, renderButton, type } = this.props; + const { boardId, renderButton, type, options } = this.props; const closeModal = () => this.setState({ showModal: false }); return ( { }; renderRows() { - const { renderButton, type } = this.props; - - const child = pipeline => { - return ( - - ); - }; - + const { renderButton, type, options } = this.props; const { pipelines } = this.state; - return ( - ( + - ); + )); } renderContent() { - const { pipelines } = this.props; + const { pipelines, options } = this.props; + const pipelineName = options ? options.pipelineName : 'pipeline'; if (pipelines.length === 0) { return ( ); @@ -121,37 +115,63 @@ class Pipelines extends React.Component { return ( -

    {__('Pipeline')}

    +

    {__(pipelineName)}

    {this.renderRows()}
    ); } + renderAdditionalButton = () => { + const { options } = this.props; + + if (options && options.additionalButton) { + return ( + + + + ); + } + + return null; + }; + renderButton() { - if (!this.props.boardId) { + const { options, boardId } = this.props; + const pipelineName = options ? options.pipelineName : 'pipeline'; + + if (!boardId) { return null; } return ( - + <> + {this.renderAdditionalButton()} + + ); } render() { + const { options } = this.props; + const pipelineName = options ? options.pipelineName : 'Pipeline'; + const boardName = options ? options.boardName : 'Board'; + return ( - + <> } @@ -160,7 +180,7 @@ class Pipelines extends React.Component { {this.renderContent()} {this.renderAddForm()} - + ); } } diff --git a/src/modules/settings/boards/components/StageItem.tsx b/src/modules/settings/boards/components/StageItem.tsx index 045bdd88fc4..ac9ec86d35f 100755 --- a/src/modules/settings/boards/components/StageItem.tsx +++ b/src/modules/settings/boards/components/StageItem.tsx @@ -8,7 +8,7 @@ import { StageItemContainer } from '../styles'; type Props = { stage: IStage; remove: (stageId: string) => void; - onChange: (stageId: string, e: any) => void; + onChange: (stageId: string, name: string, value: string) => void; onKeyPress: (e: any) => void; }; @@ -17,6 +17,11 @@ class StageItem extends React.Component { const { stage, onChange, onKeyPress, remove } = this.props; const probabilties = PROBABILITY.ALL; + const onChangeName = (stageId, e) => + onChange(stageId, e.target.name, e.target.value); + const onChangeProbability = (stageId, e) => + onChange(stageId, e.target.name, e.target.value); + return ( { onKeyPress={onKeyPress} autoFocus={true} name="name" - onChange={onChange.bind(this, stage._id)} + onChange={onChangeName.bind(this, stage._id)} /> {probabilties.map((p, index) => ( + + + + {__('Add another stage')} + ); } diff --git a/src/modules/settings/boards/containers/Boards.tsx b/src/modules/settings/boards/containers/Boards.tsx index 0dd5e0a1214..228a2d086be 100644 --- a/src/modules/settings/boards/containers/Boards.tsx +++ b/src/modules/settings/boards/containers/Boards.tsx @@ -10,16 +10,17 @@ import { compose, graphql } from 'react-apollo'; import { withRouter } from 'react-router'; import Boards from '../components/Boards'; import { mutations, queries } from '../graphql'; -import { RemoveBoardMutationResponse } from '../types'; +import { IOption, RemoveBoardMutationResponse } from '../types'; type Props = { history?: any; currentBoardId?: string; type: string; + options?: IOption; }; type FinalProps = { - boardsQuery: any; + boardsQuery: BoardsQueryResponse; } & Props & IRouterProps & RemoveBoardMutationResponse; @@ -55,9 +56,7 @@ class BoardsContainer extends React.Component { Alert.success('You successfully deleted a board'); }) .catch(error => { - Alert.error( - `Please remove all pipelines in this board before delete the board` - ); + Alert.error(error.message); }); }); }; diff --git a/src/modules/settings/boards/containers/Home.tsx b/src/modules/settings/boards/containers/Home.tsx index ec6a8417ad7..35f27075a4d 100644 --- a/src/modules/settings/boards/containers/Home.tsx +++ b/src/modules/settings/boards/containers/Home.tsx @@ -8,17 +8,20 @@ import { compose, graphql } from 'react-apollo'; import { withRouter } from 'react-router'; import Home from '../components/Home'; import { queries } from '../graphql'; +import { IOption } from '../types'; type HomeContainerProps = { history?: any; boardId: string; }; -type TypeProps = { +type Props = { type: string; + title: string; + options?: IOption; }; -class HomeContainer extends React.Component { +class HomeContainer extends React.Component { componentWillReceiveProps(nextProps) { const { history, boardId } = nextProps; @@ -37,7 +40,7 @@ type LastBoardProps = { }; // Getting lastBoard id to currentBoard -const LastBoard = (props: LastBoardProps & TypeProps) => { +const LastBoard = (props: LastBoardProps & Props) => { const { boardGetLastQuery } = props; if (boardGetLastQuery.loading) { @@ -54,7 +57,7 @@ const LastBoard = (props: LastBoardProps & TypeProps) => { return ; }; -type MainProps = IRouterProps & TypeProps; +type MainProps = IRouterProps & Props; const LastBoardContainer = withProps( compose( diff --git a/src/modules/settings/boards/containers/PipelineForm.tsx b/src/modules/settings/boards/containers/PipelineForm.tsx index e2d1f7696b7..854a692c598 100644 --- a/src/modules/settings/boards/containers/PipelineForm.tsx +++ b/src/modules/settings/boards/containers/PipelineForm.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { compose, graphql } from 'react-apollo'; import PipelineForm from '../components/PipelineForm'; import { queries } from '../graphql'; +import { IOption } from '../types'; type Props = { pipeline?: IPipeline; @@ -16,6 +17,7 @@ type Props = { closeModal: () => void; show: boolean; type: string; + options?: IOption; }; type FinalProps = { @@ -24,7 +26,7 @@ type FinalProps = { class PipelineFormContainer extends React.Component { render() { - const { stagesQuery, boardId, renderButton } = this.props; + const { stagesQuery, boardId, renderButton, options } = this.props; if (stagesQuery && stagesQuery.loading) { return ; @@ -39,7 +41,9 @@ class PipelineFormContainer extends React.Component { renderButton }; - return ; + const Form = options ? options.PipelineForm : PipelineForm; + + return ; } } diff --git a/src/modules/settings/boards/containers/Pipelines.tsx b/src/modules/settings/boards/containers/Pipelines.tsx index 48d4bbc76bb..6ab7341db52 100644 --- a/src/modules/settings/boards/containers/Pipelines.tsx +++ b/src/modules/settings/boards/containers/Pipelines.tsx @@ -9,6 +9,7 @@ import { compose, graphql } from 'react-apollo'; import Pipelines from '../components/Pipelines'; import { mutations, queries } from '../graphql'; import { + IOption, RemovePipelineMutationResponse, RemovePipelineMutationVariables, UpdateOrderPipelineMutationResponse, @@ -18,6 +19,7 @@ import { type Props = { boardId: string; type: string; + options?: IOption; }; type FinalProps = { @@ -57,9 +59,7 @@ class PipelinesContainer extends React.Component { Alert.success(msg); }) .catch(error => { - Alert.error( - `Please remove all stages in this pipeline before delete the pipeline` - ); + Alert.error(error.message); }); }); }; diff --git a/src/modules/settings/boards/graphql/mutations.ts b/src/modules/settings/boards/graphql/mutations.ts index b861da6063e..8bc7bf08a29 100644 --- a/src/modules/settings/boards/graphql/mutations.ts +++ b/src/modules/settings/boards/graphql/mutations.ts @@ -38,6 +38,8 @@ const commonPipelineParamsDef = ` $visibility: String!, $memberIds: [String], $bgColor: String, + $hackScoringType: String + $templateId: String `; const commonPipelineParams = ` @@ -47,7 +49,9 @@ const commonPipelineParams = ` type: $type, visibility: $visibility, memberIds: $memberIds, - bgColor: $bgColor + bgColor: $bgColor, + hackScoringType: $hackScoringType, + templateId: $templateId `; const pipelineAdd = ` diff --git a/src/modules/settings/boards/graphql/queries.ts b/src/modules/settings/boards/graphql/queries.ts index 21f6e91b6e8..668657076a2 100644 --- a/src/modules/settings/boards/graphql/queries.ts +++ b/src/modules/settings/boards/graphql/queries.ts @@ -25,6 +25,8 @@ const pipelines = ` visibility memberIds bgColor + hackScoringType + templateId } } `; @@ -36,6 +38,7 @@ const stages = ` name probability pipelineId + formId } } `; diff --git a/src/modules/settings/boards/routes.tsx b/src/modules/settings/boards/routes.tsx index be574de2e85..d82e14e5ee7 100644 --- a/src/modules/settings/boards/routes.tsx +++ b/src/modules/settings/boards/routes.tsx @@ -7,15 +7,15 @@ const Home = asyncComponent(() => ); const DealHome = () => { - return ; + return ; }; const TicketHome = () => { - return ; + return ; }; const TaskHome = () => { - return ; + return ; }; const routes = () => ( diff --git a/src/modules/settings/boards/styles.ts b/src/modules/settings/boards/styles.ts index e70c614f38c..392fb082af6 100644 --- a/src/modules/settings/boards/styles.ts +++ b/src/modules/settings/boards/styles.ts @@ -3,6 +3,7 @@ import { SortItem } from 'modules/common/styles/sort'; import styled from 'styled-components'; import styledTS from 'styled-components-ts'; import { SidebarListItem } from '../styles'; +import { LinkButton } from '../team/styles'; const BoardItem = styledTS<{ isActive: boolean }>(styled(SidebarListItem))` overflow: hidden; @@ -47,18 +48,26 @@ const PipelineRowContainer = styled.div` display: flex; justify-content: space-between; flex: 1; + border-top: 1px solid rgb(238, 238, 238); + padding: 10px 20px; `; const StageList = styled.div` - > button { - margin-top: 10px; + background: ${colors.colorWhite}; + padding: 20px; + margin-top: 10px; + box-shadow: 0 2px 8px ${colors.shadowPrimary}; + + ${LinkButton} { + margin: 20px 0 0 30px; + display: block; } `; const StageItemContainer = styled(PipelineRowContainer)` - background-color: #fff; - margin-bottom: 10px; - padding: 5px 20px 10px; + background-color: ${colors.colorWhite}; + padding: 0; + border-top: 0; align-items: center; > *:not(button) { @@ -66,7 +75,12 @@ const StageItemContainer = styled(PipelineRowContainer)` } button { - padding: 5px 8px; + padding: 3px; + font-size: 16px; + } + + button:hover { + color: ${colors.colorCoreRed}; } `; diff --git a/src/modules/settings/boards/types.ts b/src/modules/settings/boards/types.ts index 8e2a1f8cb42..d26d0c219e1 100644 --- a/src/modules/settings/boards/types.ts +++ b/src/modules/settings/boards/types.ts @@ -62,3 +62,30 @@ export type UpdateOrderPipelineMutationResponse = { } ) => Promise; }; + +export type PipelineCopyMutationVariables = { + _id: string; + boardId: string; + type: string; +}; + +export type PipelineCopyMutationResponse = { + pipelinesCopyMutation: ( + params: { + variables: PipelineCopyMutationVariables; + } + ) => Promise; +}; + +export type PipelineCopyMutation = ( + { variables: PipelineCopyMutationVariables } +) => Promise; + +export type IOption = { + boardName: string; + pipelineName: string; + StageItem: any; + PipelineForm: any; + additionalButton?: string; + additionalButtonText?: string; +}; diff --git a/src/modules/settings/common/components/List.tsx b/src/modules/settings/common/components/List.tsx index 32ef173351e..8e69a8cb7f8 100644 --- a/src/modules/settings/common/components/List.tsx +++ b/src/modules/settings/common/components/List.tsx @@ -17,6 +17,7 @@ type Props = { breadcrumb?: IBreadCrumbItem[]; center?: boolean; renderFilter?: () => any; + additionalButton?: React.ReactNode; }; class List extends React.Component { @@ -36,7 +37,8 @@ class List extends React.Component { save, refetch, center, - remove + remove, + additionalButton } = this.props; const trigger = ( @@ -50,13 +52,17 @@ class List extends React.Component { }; const actionBarRight = ( - + <> + {additionalButton} + + ); return ( diff --git a/src/modules/settings/emailTemplates/components/List.tsx b/src/modules/settings/emailTemplates/components/List.tsx index 2a036249f51..c5043726900 100755 --- a/src/modules/settings/emailTemplates/components/List.tsx +++ b/src/modules/settings/emailTemplates/components/List.tsx @@ -8,8 +8,8 @@ import List from '../../common/components/List'; import { ICommonListProps } from '../../common/types'; import { Actions, - EmailTemplate, IframePreview, + Template, TemplateBox, Templates } from '../styles'; @@ -52,7 +52,7 @@ class EmailTemplateList extends React.Component { renderRow({ objects }) { return objects.map((object, index) => ( - + )); } diff --git a/src/modules/settings/emailTemplates/styles.ts b/src/modules/settings/emailTemplates/styles.ts index 5351f518876..9a0db37f764 100644 --- a/src/modules/settings/emailTemplates/styles.ts +++ b/src/modules/settings/emailTemplates/styles.ts @@ -7,6 +7,7 @@ const Templates = styled.div` background: ${colors.colorWhite}; padding: ${dimensions.coreSpacing}px; overflow: auto; + display: flex; `; const IframePreview = styled.div` @@ -27,6 +28,7 @@ const IframePreview = styled.div` const TemplateBox = styled.div` width: 100%; height: 140px; + border-radius: 2px; border: 1px solid ${colors.borderDarker}; position: relative; `; @@ -39,6 +41,9 @@ const Actions = styled.div` z-index: 3; width: 100%; height: 100%; + left: 0; + top: 0; + border-radius: 2px; transition: opacity ease 0.3s; justify-content: space-evenly; align-items: center; @@ -53,7 +58,7 @@ const Actions = styled.div` } `; -const EmailTemplate = styled.div` +const Template = styled.div` display: flex; flex-direction: column; align-items: center; @@ -79,4 +84,4 @@ const EmailTemplate = styled.div` } `; -export { EmailTemplate, Actions, TemplateBox, Templates, IframePreview }; +export { Template, Actions, TemplateBox, Templates, IframePreview }; diff --git a/src/modules/settings/growthHacks/components/FormBuilder.tsx b/src/modules/settings/growthHacks/components/FormBuilder.tsx new file mode 100644 index 00000000000..28cdff6d498 --- /dev/null +++ b/src/modules/settings/growthHacks/components/FormBuilder.tsx @@ -0,0 +1,132 @@ +import { IStage } from 'modules/boards/types'; +import Button from 'modules/common/components/Button'; +import Icon from 'modules/common/components/Icon'; +import { CloseModal } from 'modules/common/styles/main'; +import { __ } from 'modules/common/utils'; +import CreateForm from 'modules/forms/containers/CreateForm'; +import EditForm from 'modules/forms/containers/EditForm'; +import { ShowPreview } from 'modules/forms/styles'; +import { IField } from 'modules/settings/properties/types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import { ContentWrapper, PreviewWrapper } from '../styles'; + +type Props = { + onChange: (stageId: string, name: string, value: string) => void; + onHide: () => void; + stage: IStage; +}; + +class FormBuilder extends React.Component< + Props, + { isReadyToSaveForm: boolean } +> { + constructor(props: Props) { + super(props); + + this.state = { + isReadyToSaveForm: false + }; + } + + renderFooter = (items: number) => { + if (items === 0) { + return null; + } + + return ( + <> + + {__('Form preview')} + + + + + + + + ); + }; + + renderFormPreviewWrapper = (previewRenderer, fields: IField[]) => { + return ( + + {previewRenderer()} + {this.renderFooter(fields ? fields.length : 0)} + + ); + }; + + saveForm = () => { + this.setState({ isReadyToSaveForm: true }); + }; + + afterFormDbSave = (formId: string) => { + const { stage, onChange, onHide } = this.props; + + onChange(stage._id, 'formId', formId); + onHide(); + + this.setState({ isReadyToSaveForm: false }); + }; + + closeModal = () => { + this.props.onHide(); + }; + + renderFormContent = () => { + const { stage } = this.props; + + const props = { + renderPreviewWrapper: this.renderFormPreviewWrapper, + afterDbSave: this.afterFormDbSave, + isReadyToSave: this.state.isReadyToSaveForm, + hideOptionalFields: true, + type: 'growthHack' + }; + + if (stage.formId) { + return ; + } + + return ; + }; + + renderContent = () => { + return {this.renderFormContent()}; + }; + + render() { + return ( + + + + + {this.renderContent()} + + ); + } +} + +export default FormBuilder; diff --git a/src/modules/settings/growthHacks/components/FormList.tsx b/src/modules/settings/growthHacks/components/FormList.tsx new file mode 100644 index 00000000000..bd581b5843a --- /dev/null +++ b/src/modules/settings/growthHacks/components/FormList.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Select from 'react-select-plus'; +import styled from 'styled-components'; + +const Container = styled.div` + width: 140px; +`; + +type Props = { + forms: any[]; + onChangeForm: (stageId: string, value: string) => void; + stage: any; +}; + +class FormList extends React.Component { + generateForms = forms => + forms.map(form => ({ + value: form._id, + label: form.title + })); + + render() { + const { forms, stage, onChangeForm } = this.props; + + const onChange = form => { + let value = ''; + + if (form) { + value = form.value; + } + + onChangeForm(stage._id, value); + }; + + return ( + + + + ); + } + + renderBox(type, desc, formula) { + const onClick = () => this.onChangeType(type); + + return ( + + {__(type)} +

    + {__(desc)} {formula} +

    +
    + ); + } + + renderContent = (formProps: IFormProps) => { + const { pipeline, renderButton, closeModal } = this.props; + const { values, isSubmitted } = formProps; + const object = pipeline || ({} as IPipeline); + + const popoverTop = ( + + + + ); + + return ( + <> + + {pipeline ? 'Edit project' : 'Add project'} + + + + + Name + + + + + Scoring type + + + {this.renderBox( + 'ice', + 'Set the Impact, Confidence and Ease factors for your tasks. Final score is calculated by the formula:', + 'Impact * Confidence * Ease' + )} + {this.renderBox( + 'rice', + 'Set the Reach, Impact, Confidence and Effort factors for your tasks. Final score is calculated by the formula:', + 'Reach * Impact * Confidence / Effort' + )} + + + + + + + Visibility + + + + + + + + + Background +
    + + + + + +
    +
    +
    + + {this.renderSelectMembers()} + {this.renderTemplates()} + + + + + {renderButton({ + name: 'pipeline', + values: this.generateDoc(values), + isSubmitted, + callback: closeModal, + object: pipeline + })} + +
    + + ); + }; + + render() { + const { show, closeModal } = this.props; + + if (!show) { + return null; + } + + return ( + + + + ); + } +} + +export default PipelineForm; diff --git a/src/modules/settings/growthHacks/components/StageItem.tsx b/src/modules/settings/growthHacks/components/StageItem.tsx new file mode 100755 index 00000000000..8cd809413bb --- /dev/null +++ b/src/modules/settings/growthHacks/components/StageItem.tsx @@ -0,0 +1,62 @@ +import { IStage } from 'modules/boards/types'; +import Button from 'modules/common/components/Button'; +import FormControl from 'modules/common/components/form/Control'; +import Icon from 'modules/common/components/Icon'; +import Tip from 'modules/common/components/Tip'; +import { colors } from 'modules/common/styles'; +import { StageItemContainer } from 'modules/settings/boards/styles'; +import React from 'react'; + +type Props = { + stage: IStage; + remove: (stageId: string) => void; + onChange: (stageId: string, name: string, value: string) => void; + onClick: (stage: IStage) => void; + onKeyPress: (e: any) => void; +}; + +class StageItem extends React.Component { + render() { + const { stage, onChange, onKeyPress, remove, onClick } = this.props; + + const onChangeName = (stageId, e) => + onChange(stageId, e.target.name, e.target.value); + + const onBuildClick = e => { + onClick(stage); + }; + + return ( + + + + + + + + ); + }; + + render() { + return ( + + } + additionalButton={this.renderButton()} + renderForm={this.renderForm} + renderContent={this.renderContent} + {...this.props} + /> + ); + } +} + +export default TemplateList; diff --git a/src/modules/settings/growthHacks/containers/FormList.tsx b/src/modules/settings/growthHacks/containers/FormList.tsx new file mode 100644 index 00000000000..4923bcdcada --- /dev/null +++ b/src/modules/settings/growthHacks/containers/FormList.tsx @@ -0,0 +1,35 @@ +import gql from 'graphql-tag'; +import { queries } from 'modules/forms/graphql'; +import { FormsQueryResponse } from 'modules/forms/types'; +import React from 'react'; +import { compose, graphql } from 'react-apollo'; +import FormList from '../components/FormList'; + +type Props = { + onChangeForm: (stageId: string, value: string) => void; + stage: any; +}; + +type FinalProps = { + formsQuery: any; +} & Props; + +class FormListContainer extends React.Component { + render() { + const { formsQuery } = this.props; + const forms = formsQuery.forms || []; + + const extendProps = { + ...this.props, + forms + }; + + return ; + } +} + +export default compose( + graphql(gql(queries.forms), { + name: 'formsQuery' + }) +)(FormListContainer); diff --git a/src/modules/settings/growthHacks/containers/TemplateList.tsx b/src/modules/settings/growthHacks/containers/TemplateList.tsx new file mode 100755 index 00000000000..9a2c98ad799 --- /dev/null +++ b/src/modules/settings/growthHacks/containers/TemplateList.tsx @@ -0,0 +1,87 @@ +import client from 'apolloClient'; +import gql from 'graphql-tag'; +import { IButtonMutateProps } from 'modules/common/types'; +import { Alert } from 'modules/common/utils'; +import { generatePaginationParams } from 'modules/common/utils/router'; +import { + ICommonFormProps, + ICommonListProps +} from 'modules/settings/common/types'; +import React from 'react'; +import { graphql } from 'react-apollo'; +import { commonListComposer } from '../../utils'; +import TemplateList from '../components/TemplateList'; +import { mutations, queries } from '../graphql'; +import { IPipelineTemplate } from '../types'; + +export type PipelineTemplatesQueryResponse = { + pipelineTemplates: IPipelineTemplate[]; + loading: boolean; + refetch: () => void; +}; + +type Props = ICommonListProps & + ICommonFormProps & { + queryParams: any; + renderButton: (props: IButtonMutateProps) => JSX.Element; + }; + +class TemplateListContainer extends React.Component { + duplicate = (id: string) => { + client + .mutate({ + mutation: gql(mutations.pipelineTemplatesDuplicate), + variables: { _id: id } + }) + .then(() => { + Alert.success('Successfully duplicated a template'); + + this.props.refetch(); + }) + .catch(e => { + Alert.error(e.message); + }); + }; + + render() { + return ; + } +} + +export default commonListComposer({ + text: 'growth hack template', + label: 'pipelineTemplates', + stringEditMutation: mutations.pipelineTemplatesEdit, + stringAddMutation: mutations.pipelineTemplatesAdd, + + gqlListQuery: graphql(gql(queries.pipelineTemplates), { + name: 'listQuery', + options: ({ queryParams }: { queryParams: any }) => { + return { + notifyOnNetworkStatusChange: true, + variables: { + ...generatePaginationParams(queryParams), + type: 'growthHack' + } + }; + } + }), + + gqlTotalCountQuery: graphql(gql(queries.totalCount), { + name: 'totalCountQuery' + }), + + gqlAddMutation: graphql(gql(mutations.pipelineTemplatesAdd), { + name: 'addMutation' + }), + + gqlEditMutation: graphql(gql(mutations.pipelineTemplatesEdit), { + name: 'editMutation' + }), + + gqlRemoveMutation: graphql(gql(mutations.pipelineTemplatesRemove), { + name: 'removeMutation' + }), + + ListComponent: TemplateListContainer +}); diff --git a/src/modules/settings/growthHacks/graphql/index.ts b/src/modules/settings/growthHacks/graphql/index.ts new file mode 100644 index 00000000000..8cff0604ab6 --- /dev/null +++ b/src/modules/settings/growthHacks/graphql/index.ts @@ -0,0 +1,4 @@ +import mutations from './mutations'; +import queries from './queries'; + +export { queries, mutations }; diff --git a/src/modules/settings/growthHacks/graphql/mutations.ts b/src/modules/settings/growthHacks/graphql/mutations.ts new file mode 100644 index 00000000000..ea06222d91c --- /dev/null +++ b/src/modules/settings/growthHacks/graphql/mutations.ts @@ -0,0 +1,50 @@ +const commonParamsDef = ` + $name: String!, + $description: String, + $type: String!, + $stages: [PipelineTemplateStageInput], +`; + +const commonParams = ` + name: $name, + description: $description, + type: $type, + stages: $stages +`; + +const pipelineTemplatesAdd = ` + mutation pipelineTemplatesAdd(${commonParamsDef}) { + pipelineTemplatesAdd(${commonParams}) { + _id + } + } +`; + +const pipelineTemplatesEdit = ` + mutation pipelineTemplatesEdit($_id: String!, ${commonParamsDef}) { + pipelineTemplatesEdit(_id: $_id, ${commonParams}) { + _id + } + } +`; + +const pipelineTemplatesRemove = ` + mutation pipelineTemplatesRemove($_id: String!) { + pipelineTemplatesRemove(_id: $_id) + } +`; + +const pipelineTemplatesDuplicate = ` + mutation pipelineTemplatesDuplicate($_id: String!) { + pipelineTemplatesDuplicate(_id: $_id) { + _id + } + } +`; + +export default { + pipelineTemplatesAdd, + pipelineTemplatesEdit, + pipelineTemplatesRemove, + pipelineTemplatesDuplicate +}; diff --git a/src/modules/settings/growthHacks/graphql/queries.ts b/src/modules/settings/growthHacks/graphql/queries.ts new file mode 100644 index 00000000000..cb2210b0708 --- /dev/null +++ b/src/modules/settings/growthHacks/graphql/queries.ts @@ -0,0 +1,26 @@ +const pipelineTemplates = ` + query pipelineTemplates($type: String!) { + pipelineTemplates(type: $type) { + _id + name + description + stages { + _id + name + formId + } + isDefinedByErxes + } + } +`; + +const totalCount = ` + query pipelineTemplatesTotalCount { + pipelineTemplatesTotalCount + } +`; + +export default { + pipelineTemplates, + totalCount +}; diff --git a/src/modules/settings/growthHacks/options.ts b/src/modules/settings/growthHacks/options.ts new file mode 100644 index 00000000000..942455d6496 --- /dev/null +++ b/src/modules/settings/growthHacks/options.ts @@ -0,0 +1,20 @@ +import PipelineForm from './components/PipelineForm'; +import StageItem from './components/StageItem'; + +const options = { + boardName: 'Campaign', + pipelineName: 'Project', + StageItem, + PipelineForm, + additionalButton: '/settings/boards/growthHackTemplate', + additionalButtonText: 'Go to templates' +}; + +const templateOptions = { + StageItem, + boardName: 'Category', + pipelineName: 'Template', + PipelineForm +}; + +export { options, templateOptions }; diff --git a/src/modules/settings/growthHacks/routes.tsx b/src/modules/settings/growthHacks/routes.tsx new file mode 100644 index 00000000000..19fbbb70fc6 --- /dev/null +++ b/src/modules/settings/growthHacks/routes.tsx @@ -0,0 +1,34 @@ +import asyncComponent from 'modules/common/components/AsyncComponent'; +import queryString from 'query-string'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { options } from './options'; + +const Home = asyncComponent(() => + import(/* webpackChunkName: "Settings - Board Home" */ 'modules/settings/boards/containers/Home') +); + +const GrowthHackHome = () => { + return ; +}; + +const TemplateList = asyncComponent(() => + import(/* webpackChunkName: "Settings - List PipelineTemplate" */ './containers/TemplateList') +); + +const pipelineTemplates = ({ location }) => { + return ; +}; + +const routes = () => ( + + + + + +); + +export default routes; diff --git a/src/modules/settings/growthHacks/styles.ts b/src/modules/settings/growthHacks/styles.ts new file mode 100644 index 00000000000..e0904ab3aff --- /dev/null +++ b/src/modules/settings/growthHacks/styles.ts @@ -0,0 +1,108 @@ +import { LeftItem } from 'modules/common/components/step/styles'; +import { colors, dimensions } from 'modules/common/styles'; +import { BoxRoot } from 'modules/common/styles/main'; +import { WhiteBoxRoot } from 'modules/layout/styles'; +import styled from 'styled-components'; + +const TemplateContainer = styled.div` + display: flex; + padding: 20px 0 20px 20px; + flex-wrap: wrap; +`; + +const TemplateItem = styled.div` + flex-basis: 300px; + padding: 25px 30px; + margin: 0 ${dimensions.coreSpacing}px ${dimensions.coreSpacing}px 0; + display: flex; + flex-direction: column; + justify-content: space-between; + border-radius: 6px; + box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.1); + + h5 { + margin: 0 0 5px; + line-height: 22px; + color: ${colors.colorPrimaryDark}; + } + + p { + margin: 0; + color: ${colors.colorCoreGray}; + } +`; + +const Box = styled(BoxRoot)` + flex: 1; + padding: ${dimensions.coreSpacing}px; + text-align: left; + background: ${colors.colorWhite}; + margin: 10px 20px 0 0; + + b { + font-size: 30px; + text-transform: uppercase; + color: ${colors.colorCoreLightGray}; + } + + p { + margin: 10px 0 0; + font-size: 12px; + color: ${colors.textSecondary}; + } + + &:last-of-type { + margin-right: 0; + } +`; + +const PreviewWrapper = styled(WhiteBoxRoot)` + flex: 1; + padding: 30px; + display: flex; + flex-direction: column; + align-items: center; + margin: 0; + + > div { + max-width: 400px; + } +`; + +const ContentWrapper = styled.div` + ${LeftItem} { + padding: 20px 30px; + flex: 0.5; + min-width: auto; + } +`; + +const Actions = styled.div` + display: flex; + margin-top: ${dimensions.coreSpacing}px; + justify-content: flex-end; + + > div { + padding: 3px 6px; + width: 26px; + height: 26px; + border-radius: 13px; + margin-left: 5px; + background-color: ${colors.bgActive}; + transition: background-color 0.3s ease; + + &:hover { + cursor: pointer; + background-color: ${colors.colorShadowGray}; + } + } +`; + +export { + TemplateItem, + Box, + PreviewWrapper, + ContentWrapper, + TemplateContainer, + Actions +}; diff --git a/src/modules/settings/growthHacks/types.ts b/src/modules/settings/growthHacks/types.ts new file mode 100644 index 00000000000..e94d1dc574a --- /dev/null +++ b/src/modules/settings/growthHacks/types.ts @@ -0,0 +1,13 @@ +export interface IPipelineTemplateStage { + name: string; + formId: string; + order?: number; +} + +export interface IPipelineTemplate { + _id: string; + name: string; + description: string; + type: string; + stages: IPipelineTemplateStage[]; +} diff --git a/src/modules/settings/integrations/components/common/IntegrationList.tsx b/src/modules/settings/integrations/components/common/IntegrationList.tsx index b1d161ea6c7..665b236a2f4 100644 --- a/src/modules/settings/integrations/components/common/IntegrationList.tsx +++ b/src/modules/settings/integrations/components/common/IntegrationList.tsx @@ -34,8 +34,8 @@ class IntegrationList extends React.Component { return 'gmail'; } - if (kind === KIND_CHOICES.FORM) { - return 'form'; + if (kind === KIND_CHOICES.LEAD) { + return 'lead'; } if (kind === KIND_CHOICES.CALLPRO) { diff --git a/src/modules/settings/integrations/components/common/ManageIntegrations.tsx b/src/modules/settings/integrations/components/common/ManageIntegrations.tsx index 88d73864e33..8d7015085a4 100644 --- a/src/modules/settings/integrations/components/common/ManageIntegrations.tsx +++ b/src/modules/settings/integrations/components/common/ManageIntegrations.tsx @@ -98,8 +98,8 @@ class ManageIntegrations extends React.Component { const kind = integration.kind; let type = 'messenger'; - if (kind === KIND_CHOICES.FORM) { - type = 'form'; + if (kind === KIND_CHOICES.LEAD) { + type = 'lead'; } else if (kind === KIND_CHOICES.FACEBOOK_MESSENGER) { type = 'facebook-messenger'; } else if (kind === KIND_CHOICES.FACEBOOK_POST) { @@ -115,7 +115,7 @@ class ManageIntegrations extends React.Component { const kind = integration.kind; let icon = 'comment-alt'; - if (kind === KIND_CHOICES.FORM) { + if (kind === KIND_CHOICES.LEAD) { icon = 'doc-text-inv-1'; } else if (kind === KIND_CHOICES.FACEBOOK_MESSENGER) { icon = 'facebook-official'; diff --git a/src/modules/settings/integrations/constants.ts b/src/modules/settings/integrations/constants.ts index a93dcd286f5..4a760201d58 100755 --- a/src/modules/settings/integrations/constants.ts +++ b/src/modules/settings/integrations/constants.ts @@ -68,13 +68,13 @@ export const KIND_CHOICES = { FACEBOOK_MESSENGER: 'facebook-messenger', FACEBOOK_POST: 'facebook-post', GMAIL: 'gmail', - FORM: 'form', + LEAD: 'lead', CALLPRO: 'callpro', ALL_LIST: [ 'messenger', 'facebook-post', 'facebook-messenger', - 'form', + 'lead', 'callpro' ] }; diff --git a/src/modules/settings/integrations/containers/lead/Form.tsx b/src/modules/settings/integrations/containers/lead/Form.tsx index 95f656c62dc..668614a9416 100755 --- a/src/modules/settings/integrations/containers/lead/Form.tsx +++ b/src/modules/settings/integrations/containers/lead/Form.tsx @@ -99,7 +99,7 @@ export default withProps( notifyOnNetworkStatusChange: true, variables: { ...integrationsListParams(queryParams || {}), - kind: 'form' + kind: 'lead' }, fetchPolicy: 'network-only' }; diff --git a/src/modules/settings/integrations/graphql/queries.ts b/src/modules/settings/integrations/graphql/queries.ts index 7fce0b10540..51e27feaa21 100644 --- a/src/modules/settings/integrations/graphql/queries.ts +++ b/src/modules/settings/integrations/graphql/queries.ts @@ -74,7 +74,7 @@ const integrations = ` name code } - formData + leadData formId tagIds tags { diff --git a/src/modules/settings/integrations/types.ts b/src/modules/settings/integrations/types.ts index dd89f3c8b0a..50272b28398 100644 --- a/src/modules/settings/integrations/types.ts +++ b/src/modules/settings/integrations/types.ts @@ -1,4 +1,5 @@ -import { IForm, IFormIntegration } from 'modules/forms/types'; +import { IForm } from 'modules/forms/types'; +import { ILeadData, ILeadIntegration } from 'modules/leads/types'; import { IBrand } from '../brands/types'; import { IChannel } from '../channels/types'; @@ -66,35 +67,20 @@ export interface IUiOptions { logoPreviewUrl?: string; } -export interface IFormData { - loadType?: string; - successAction?: string; - fromEmail?: string; - userEmailTitle?: string; - userEmailContent?: string; - adminEmails?: string[]; - adminEmailTitle?: string; - adminEmailContent?: string; - thankContent?: string; - redirectUrl?: string; -} - export interface IIntegration { _id: string; kind: string; name: string; brandId?: string; - description?: string; code: string; formId: string; - form: IForm; - logo: string; languageCode?: string; createUrl: string; createModal: string; messengerData?: IMessengerData; + form: IForm; uiOptions?: IUiOptions; - formData?: IFormData; + leadData: ILeadData; brand: IBrand; channels: IChannel[]; } @@ -119,12 +105,6 @@ export type IntegrationsQueryResponse = { refetch: (variables?: QueryVariables) => void; }; -export type LeadsQueryResponse = { - forms: IForm[]; - loading: boolean; - refetch: (variables?: QueryVariables) => void; -}; - export type IntegrationDetailQueryResponse = { integrationDetail: IIntegration; loading: boolean; @@ -190,8 +170,8 @@ export type MessengerAppsCountQueryResponse = { loading: boolean; }; -export type FormIntegrationDetailQueryResponse = { - integrationDetail: IFormIntegration; +export type LeadIntegrationDetailQueryResponse = { + integrationDetail: ILeadIntegration; loading: boolean; refetch: () => void; }; @@ -294,7 +274,7 @@ export type MessengerAppsAddKnowledgebaseMutationResponse = { }; export type AddIntegrationMutationVariables = { - formData: IFormData; + leadData: ILeadData; brandId: string; name: string; languageCode: string; @@ -311,7 +291,7 @@ export type AddIntegrationMutationResponse = { export type EditIntegrationMutationVariables = { _id: string; - formData: IFormData; + leadData: ILeadData; brandId: string; name: string; languageCode: string; diff --git a/src/modules/settings/main/components/Settings.tsx b/src/modules/settings/main/components/Settings.tsx index 2fc41689f34..29aaaf6f01a 100644 --- a/src/modules/settings/main/components/Settings.tsx +++ b/src/modules/settings/main/components/Settings.tsx @@ -43,7 +43,7 @@ class Settings extends React.PureComponent { )} {this.renderBox( 'Permission', - '/images/icons/erxes-20.svg', + '/images/icons/erxes-23.svg', '/settings/permissions' )} {this.renderBox( @@ -120,6 +120,22 @@ class Settings extends React.PureComponent { + + {__('Growth hack Settings')} +
    + {this.renderBox( + 'Campaigns & Projects', + '/images/icons/erxes-20.svg', + '/settings/boards/growthHack' + )} + {this.renderBox( + 'Templates', + '/images/icons/erxes-22.svg', + '/settings/boards/growthHackTemplate' + )} +
    +
    + {__('Deal Settings')}
    diff --git a/src/modules/settings/main/styles.ts b/src/modules/settings/main/styles.ts index 2ee5ee0d136..9761ec8c244 100644 --- a/src/modules/settings/main/styles.ts +++ b/src/modules/settings/main/styles.ts @@ -17,6 +17,7 @@ const Row = styled.div` @media (max-width: 1170px) { flex-direction: column; + padding-left: ${dimensions.coreSpacing}px; } `; @@ -30,15 +31,33 @@ const RowTitle = styled.h3` color: ${colors.colorCoreDarkGray}; flex-shrink: 0; width: ${rowTitleSize}px; + + @media (max-width: 1170px) { + align-self: baseline; + padding: 0; + } `; const Box = styled(BoxRoot)` width: ${boxSize}px; height: ${boxSize}px; + border-color: transparent; img { height: 83px; } + + > a { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + height: 100%; + + &:focus { + text-decoration: none; + } + } `; const Divider = styled.div` @@ -46,10 +65,15 @@ const Divider = styled.div` padding-bottom: ${dimensions.coreSpacing}px; margin: 0 ${dimensions.coreSpacing}px ${dimensions.coreSpacing}px ${rowTitleSize}px; + + @media (max-width: 1170px) { + margin-left: ${dimensions.coreSpacing}px; + } `; const BoxName = styled.span` font-size: ${typography.fontSizeUppercase}px; + margin: 0 !important; `; export { Row, RowTitle, Box, Divider, BoxName, MenusContainer }; diff --git a/src/modules/settings/properties/types.ts b/src/modules/settings/properties/types.ts index 6a05805c1d9..22097393f8a 100644 --- a/src/modules/settings/properties/types.ts +++ b/src/modules/settings/properties/types.ts @@ -5,12 +5,12 @@ export interface IField { contentType: string; contentTypeId?: string; type: string; - validation: string; - text: string; - description: string; - options: string[]; + validation?: string; + text?: string; + description?: string; + options?: string[]; isRequired?: boolean; - order: React.ReactNode; + order?: React.ReactNode; isVisible?: boolean; isDefinedByErxes?: boolean; groupId?: string; diff --git a/src/modules/settings/routes.tsx b/src/modules/settings/routes.tsx index 388cfc1d967..3d30ca3b3aa 100644 --- a/src/modules/settings/routes.tsx +++ b/src/modules/settings/routes.tsx @@ -6,6 +6,7 @@ import ChannelsRoutes from './channels/routes'; import EmailRoutes from './email/routes'; import EmailTemplatesRoutes from './emailTemplates/routes'; import General from './general/routes'; +import GrowthHackRoutes from './growthHacks/routes'; import ImportHistory from './importHistory/routes'; import IntegrationsRoutes from './integrations/routes'; import LogRoutes from './logs/routes'; @@ -39,6 +40,7 @@ const routes = () => ( + ); diff --git a/src/modules/settings/scripts/components/Form.tsx b/src/modules/settings/scripts/components/Form.tsx index bd1229bc12f..d55078f79fb 100755 --- a/src/modules/settings/scripts/components/Form.tsx +++ b/src/modules/settings/scripts/components/Form.tsx @@ -13,7 +13,7 @@ import { IScript } from '../types'; type Props = { object?: IScript; - forms: IIntegration[]; + leads: IIntegration[]; messengers: IIntegration[]; kbTopics: ITopic[]; renderButton: (props: IButtonMutateProps) => JSX.Element; @@ -68,7 +68,7 @@ class Form extends React.Component { }; renderContent = (formProps: IFormProps) => { - const { forms, messengers, kbTopics } = this.props; + const { leads, messengers, kbTopics } = this.props; const object = this.props.object || ({} as IScript); return ( @@ -110,7 +110,7 @@ class Form extends React.Component { placeholder={__('Select leads')} onChange={this.onChangeLeads} value={this.state.leads} - options={this.generateLeadOptions(forms)} + options={this.generateLeadOptions(leads)} multi={true} /> diff --git a/src/modules/settings/scripts/containers/Form.tsx b/src/modules/settings/scripts/containers/Form.tsx index ad638e2daeb..2f6aecaf9d7 100755 --- a/src/modules/settings/scripts/containers/Form.tsx +++ b/src/modules/settings/scripts/containers/Form.tsx @@ -25,12 +25,13 @@ const FormContainer = (props: Props & ICommonFormProps) => { } const integrations = integrationsQuery.integrations; + const kbTopics = kbTopicsQuery.knowledgeBaseTopics; const updatedProps = { ...props, messengers: integrations.filter(i => i.kind === 'messenger'), - forms: integrations.filter(i => i.kind === 'form'), + leads: integrations.filter(i => i.kind === 'lead'), kbTopics }; diff --git a/src/modules/settings/styles.ts b/src/modules/settings/styles.ts index 2e730490f20..bccd8466f58 100644 --- a/src/modules/settings/styles.ts +++ b/src/modules/settings/styles.ts @@ -67,18 +67,17 @@ const LogoContainer = styled.div` `; const ColorPick = styled.div` - border-radius: 50%; + border-radius: 4px; display: inline-block; - padding: 5px; - border: 1px solid ${colors.borderPrimary}; + padding: 3px; + border: 1px solid ${colors.borderDarker}; cursor: pointer; - margin-top: ${dimensions.unitSpacing}px; `; const ColorPicker = styled.div` - width: 30px; - height: 30px; - border-radius: 15px; + width: 80px; + height: 27px; + border-radius: 2px; `; const WidgetApperance = styled.div` @@ -255,6 +254,11 @@ const Description = styled.div` font-size: 12px; `; +const ExpandWrapper = styled.div` + flex: 1; + margin-right: 20px; +`; + export { ContentBox, ModuleBox, @@ -270,5 +274,6 @@ export { LogoContainer, SidebarListItem, ActionButtons, - Description + Description, + ExpandWrapper }; diff --git a/src/modules/tasks/components/TaskMainActionBar.tsx b/src/modules/tasks/components/TaskMainActionBar.tsx index a7746cfdd25..2dca1217cda 100644 --- a/src/modules/tasks/components/TaskMainActionBar.tsx +++ b/src/modules/tasks/components/TaskMainActionBar.tsx @@ -3,6 +3,8 @@ import { PRIORITIES } from 'modules/boards/constants'; import { IBoard, IPipeline } from 'modules/boards/types'; import { IOption } from 'modules/common/types'; import { __ } from 'modules/common/utils'; +import SelectCompanies from 'modules/companies/containers/SelectCompanies'; +import SelectCustomers from 'modules/customers/containers/common/SelectCustomers'; import React from 'react'; import Select from 'react-select-plus'; @@ -34,15 +36,29 @@ const TaskMainActionBar = (props: Props) => { onSelect(ops.map(option => option.value), 'priority'); const extraFilter = ( - + + + ); const extendedProps = { diff --git a/src/modules/tasks/options.ts b/src/modules/tasks/options.ts index 3852ee6b60e..ee040b26083 100644 --- a/src/modules/tasks/options.ts +++ b/src/modules/tasks/options.ts @@ -40,7 +40,7 @@ const options = { updateSuccessText: 'You successfully updated a task', deleteSuccessText: 'You successfully deleted a task', copySuccessText: 'You successfully copied a task', - changeSuccessText: 'You successfully changed a ticket' + changeSuccessText: 'You successfully changed a task' }, getExtraParams: (queryParams: any) => { const { priority } = queryParams; diff --git a/src/modules/tickets/components/TicketEditForm.tsx b/src/modules/tickets/components/TicketEditForm.tsx index a1fa102995e..54f770db5d5 100644 --- a/src/modules/tickets/components/TicketEditForm.tsx +++ b/src/modules/tickets/components/TicketEditForm.tsx @@ -186,7 +186,6 @@ export default class TicketEditForm extends React.Component { const extendedProps = { ...this.props, formContent: this.renderFormContent, - sidebar: this.renderSidebarFields, extraFields: this.state }; diff --git a/src/modules/tickets/components/TicketItem.tsx b/src/modules/tickets/components/TicketItem.tsx index 99d57f9458b..ddb14ec3634 100644 --- a/src/modules/tickets/components/TicketItem.tsx +++ b/src/modules/tickets/components/TicketItem.tsx @@ -34,6 +34,7 @@ class TicketItem extends React.PureComponent { itemId={item._id} beforePopupClose={beforePopupClose} options={options} + hideHeader={true} /> ); }; diff --git a/src/modules/tickets/components/TicketMainActionBar.tsx b/src/modules/tickets/components/TicketMainActionBar.tsx index c5fa49a24e4..bb554e5c530 100644 --- a/src/modules/tickets/components/TicketMainActionBar.tsx +++ b/src/modules/tickets/components/TicketMainActionBar.tsx @@ -3,6 +3,8 @@ import { PRIORITIES } from 'modules/boards/constants'; import { IBoard, IPipeline } from 'modules/boards/types'; import { IOption } from 'modules/common/types'; import { __ } from 'modules/common/utils'; +import SelectCompanies from 'modules/companies/containers/SelectCompanies'; +import SelectCustomers from 'modules/customers/containers/common/SelectCustomers'; import { KIND_CHOICES } from 'modules/settings/integrations/constants'; import React from 'react'; import Select from 'react-select-plus'; @@ -48,7 +50,7 @@ const TicketMainActionBar = (props: Props) => { const extraFilter = ( <> { multi={true} loadingPlaceholder={__('Loading...')} /> + + + + ); diff --git a/src/routes.tsx b/src/routes.tsx index 00af2b2fce7..8d7b78c8d86 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -12,10 +12,11 @@ import CompaniesRoutes from './modules/companies/routes'; import CustomersRoutes from './modules/customers/routes'; import DealsRoutes from './modules/deals/routes'; import EngageRoutes from './modules/engage/routes'; -import FormRoutes from './modules/forms/routes'; +import GrowthHackRoutes from './modules/growthHacks/routes'; import InboxRoutes from './modules/inbox/routes'; import InsightsRoutes from './modules/insights/routes'; import KnowledgeBaseRoutes from './modules/knowledgeBase/routes'; +import LeadRoutes from './modules/leads/routes'; import { NotifProvider } from './modules/notifications/context'; import NotificationRoutes from './modules/notifications/routes'; import OnboardRoutes from './modules/onboard/routes'; @@ -51,12 +52,13 @@ const renderRoutes = currentUser => { - + +