Added sorting to msgs sub and added some styling for messaging items. BOD-309 BOD-310

This commit is contained in:
Patrick Fic
2020-08-28 12:53:19 -07:00
parent fbde4ddc2a
commit 364cf6c7bb
20 changed files with 165 additions and 134 deletions

View File

@@ -10,11 +10,13 @@ import "./chat-affix.styles.scss";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
}); });
export function ChatAffixContainer({ bodyshop }) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { loading, error, data } = useSubscription( const { loading, error, data } = useSubscription(
CONVERSATION_LIST_SUBSCRIPTION, CONVERSATION_LIST_SUBSCRIPTION,
{ {
@@ -26,7 +28,7 @@ export function ChatAffixContainer({ bodyshop }) {
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<Affix className="chat-affix"> <Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
<div> <div>
{bodyshop && bodyshop.messagingservicesid ? ( {bodyshop && bodyshop.messagingservicesid ? (
<ChatAffixComponent <ChatAffixComponent

View File

@@ -2,3 +2,9 @@
position: absolute; position: absolute;
bottom: 2vh; bottom: 2vh;
} }
.chat-affix-open {
-webkit-box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
-moz-box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
box-shadow: 0px 0px 10px 0px rgba(69, 69, 69, 1);
}

View File

@@ -1,6 +1,5 @@
import { Badge, List } from "antd"; import { Badge, List, Tag } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
@@ -22,43 +21,46 @@ export function ChatConversationListComponent({
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
}) { }) {
const { t } = useTranslation();
return ( return (
<List <div className="chat-list-container">
bordered <List
size="small" bordered
dataSource={conversationList} size="small"
renderItem={(item) => ( dataSource={conversationList}
<List.Item renderItem={(item) => (
onClick={() => setSelectedConversation(item.id)} <List.Item
className={`chat-list-item ${ onClick={() => setSelectedConversation(item.id)}
item.id === selectedConversation className={`chat-list-item ${
? "chat-list-selected-conversation" item.id === selectedConversation
: null ? "chat-list-selected-conversation"
}`} : null
> }`}
<List.Item.Meta >
title={<PhoneFormatter>{item.phone_num}</PhoneFormatter>} {item.job_conversations.length > 0 ? (
description={ <div className="chat-name">
item.job_conversations.length > 0 ? ( {item.job_conversations.map((j, idx) => (
<div> <span key={idx}>
{item.job_conversations.map( {`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
(j) => j.job.ownr_co_nm || ""
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${ } `}
j.job.ownr_co_nm || "" </span>
}` ))}
)} </div>
</div> ) : (
) : ( <PhoneFormatter>{item.phone_num}</PhoneFormatter>
t("messaging.labels.nojobs") )}
) {item.job_conversations.length > 0
} ? item.job_conversations.map((j, idx) => (
/> <Tag key={idx} className="ro-number-tag">
<Badge count={item.messages_aggregate.aggregate.count || 0} /> {j.job.ro_number}
</List.Item> </Tag>
)} ))
/> : null}
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
)}
/>
</div>
); );
} }
export default connect( export default connect(

View File

@@ -1,10 +1,24 @@
.chat-list-selected-conversation { .chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2); background-color: rgba(128, 128, 128, 0.2);
} }
.chat-list-container {
flex: 1;
overflow: auto;
height: 100%;
}
.chat-list-item { .chat-list-item {
display: flex;
flex-direction: row;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
color: #ff7a00; color: #ff7a00;
} }
.chat-name {
flex: 1;
display: inline;
}
.ro-number-tag {
align-self: baseline;
}
} }

View File

@@ -29,11 +29,14 @@ export default function ChatConversationTitleTags({ jobConversations }) {
<Tag <Tag
key={item.job.id} key={item.job.id}
closable closable
color='blue' color="blue"
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
onClose={() => handleRemoveTag(item.job.id)}> onClose={() => handleRemoveTag(item.job.id)}
>
<Link to={`/manage/jobs/${item.job.id}`}> <Link to={`/manage/jobs/${item.job.id}`}>
{item.job.ro_number || "?"} {`${item.job.ro_number || "?"} | ${item.job.ownr_fn || ""} ${
item.job.ownr_ln || ""
} ${item.job.ownr_co_nm || ""}`}
</Link> </Link>
</Tag> </Tag>
))} ))}

View File

@@ -1,32 +1,23 @@
import { Space } from "antd";
import React from "react"; import React from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component"; import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
export default function ChatConversationTitle({ conversation }) { export default function ChatConversationTitle({ conversation }) {
return ( return (
<div> <div>
<Space> <div className="imex-flex-row">
<strong>{conversation && conversation.phone_num}</strong>
<span>
{conversation &&
conversation.job_conversations.map(
(j) =>
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
j.job.ownr_co_nm || ""
} | `
)}
</span>
</Space>
<div className="imex-flex-row imex-flex-row__margin">
<ChatConversationTitleTags <ChatConversationTitleTags
jobConversations={ jobConversations={
(conversation && conversation.job_conversations) || [] (conversation && conversation.job_conversations) || []
} }
/> />
<ChatTagRoContainer conversation={conversation || []} /> <ChatTagRoContainer conversation={conversation || []} />
<ChatPresetsComponent /> </div>
<div className="imex-flex-row">
<PhoneNumberFormatter>
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
</div> </div>
</div> </div>
); );

View File

@@ -3,9 +3,12 @@
// bottom: 0rem; // bottom: 0rem;
color: whitesmoke; color: whitesmoke;
border: #000000; border: #000000;
margin-left: 0.2rem; position: absolute;
margin-right: 0rem; margin: 0 0.1rem;
// z-index: 5; bottom: 0.1rem;
right: 0.3rem;
z-index: 5;
} }
.chat { .chat {
@@ -84,6 +87,7 @@
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%); background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
background-attachment: fixed; background-attachment: fixed;
position: relative; position: relative;
padding-bottom: 0.6rem;
} }
.mine .message.last:before { .mine .message.last:before {

View File

@@ -33,7 +33,7 @@ export function ChatPopupComponent({
style={{ position: "absolute", right: ".5rem", top: ".5rem" }} style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
/> />
<Row className="chat-popup-content"> <Row gutter={[8, 8]} className="chat-popup-content">
<Col span={8}> <Col span={8}>
<ChatConversationListComponent conversationList={conversationList} /> <ChatConversationListComponent conversationList={conversationList} />
</Col> </Col>

View File

@@ -6,8 +6,12 @@
} }
.chat-popup-content { .chat-popup-content {
//height: 50vh; min-height: 0px; /* IMPORTANT: you need this for non-chrome browsers */
flex: 1; flex: 1;
//height: 100%;
.ant-col {
height: 100%;
}
} }
@media only screen and (min-width: 992px) { @media only screen and (min-width: 992px) {

View File

@@ -1,7 +1,6 @@
import { DownOutlined } from "@ant-design/icons"; import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown, Menu } from "antd"; import { Dropdown, Menu } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setMessage } from "../../redux/messaging/messaging.actions"; import { setMessage } from "../../redux/messaging/messaging.actions";
@@ -16,9 +15,7 @@ const mapDispatchToProps = (dispatch) => ({
setMessage: (message) => dispatch(setMessage(message)), setMessage: (message) => dispatch(setMessage(message)),
}); });
export function ChatPresetsComponent({ bodyshop, setMessage }) { export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const { t } = useTranslation();
const menu = ( const menu = (
<Menu> <Menu>
{bodyshop.md_messaging_presets.map((i, idx) => ( {bodyshop.md_messaging_presets.map((i, idx) => (
@@ -30,15 +27,9 @@ export function ChatPresetsComponent({ bodyshop, setMessage }) {
); );
return ( return (
<div> <div className={className}>
<Dropdown trigger={["click"]} overlay={menu}> <Dropdown trigger={["click"]} overlay={menu}>
<a <PlusCircleOutlined />
className="ant-dropdown-link"
href="# "
onClick={(e) => e.preventDefault()}
>
{t("messaging.labels.presets")} <DownOutlined />
</a>
</Dropdown> </Dropdown>
</div> </div>
); );

View File

@@ -14,6 +14,7 @@ import {
selectMessage, selectMessage,
} from "../../redux/messaging/messaging.selectors"; } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -53,6 +54,8 @@ function ChatSendMessageComponent({
return ( return (
<div className="imex-flex-row"> <div className="imex-flex-row">
<ChatPresetsComponent className="imex-flex-row__margin" />
<Input.TextArea <Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow" className="imex-flex-row__margin imex-flex-row__grow"
allowClear allowClear
@@ -68,6 +71,7 @@ function ChatSendMessageComponent({
if (!!!event.shiftKey) handleEnter(); if (!!!event.shiftKey) handleEnter();
}} }}
/> />
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} /> <SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
<Spin <Spin
style={{ display: `${isSending ? "" : "none"}` }} style={{ display: `${isSending ? "" : "none"}` }}

View File

@@ -1,51 +1,46 @@
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import { Select, Empty } from "antd";
import React from "react"; import React from "react";
import { AutoComplete } from "antd";
import { LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function ChatTagRoComponent({ export default function ChatTagRoComponent({
searchQueryState,
roOptions, roOptions,
loading, loading,
executeSearch, handleSearch,
handleInsertTag, handleInsertTag,
setVisible, setVisible,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const setSearchQuery = searchQueryState[1];
const handleSearchQuery = (value) => {
setSearchQuery(value);
};
const handleKeyDown = (event) => {
if (event.key === "Enter") {
executeSearch();
}
};
return ( return (
<span> <div>
<AutoComplete <Select
style={{ width: 100 }} showSearch
onSearch={handleSearchQuery} autoFocus
onSelect={handleInsertTag} style={{
width: 300,
}}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onKeyDown={handleKeyDown} filterOption={false}
onSearch={handleSearch}
onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
> >
{roOptions.map((item, idx) => ( {roOptions.map((item, idx) => (
<AutoComplete.Option key={item.id || idx}> <Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${ {` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
item.ownr_ln || "" item.ownr_ln || ""
} ${item.ownr_co_nm || ""}`} } ${item.ownr_co_nm || ""}`}
</AutoComplete.Option> </Select.Option>
))} ))}
</AutoComplete> </Select>
{loading ? <LoadingOutlined /> : null}
{loading ? ( {loading ? (
<LoadingOutlined /> <LoadingOutlined />
) : ( ) : (
<CloseCircleOutlined onClick={() => setVisible(false)} /> <CloseCircleOutlined onClick={() => setVisible(false)} />
)} )}
</span> </div>
); );
} }

View File

@@ -1,32 +1,29 @@
import React, { useState } from "react";
import ChatTagRo from "./chat-tag-ro.component";
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import { Tag } from "antd";
import { useTranslation } from "react-i18next";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/react-hooks";
import { Tag } from "antd";
import _ from "lodash";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
import ChatTagRo from "./chat-tag-ro.component";
export default function ChatTagRoContainer({ conversation }) { export default function ChatTagRoContainer({ conversation }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const searchQueryState = useState("");
const searchText = searchQueryState[0];
const [loadRo, { called, loading, data, refetch }] = useLazyQuery( const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
SEARCH_FOR_JOBS,
{
variables: { search: `%${searchText}%` },
}
);
const executeSearch = () => { const executeSearch = (v) => {
logImEXEvent("messaging_search_job_tag", { searchTerm: searchText }); logImEXEvent("messaging_search_job_tag", { searchTerm: v });
if (called) refetch(); loadRo(v);
else { };
loadRo();
} const debouncedExecuteSearch = _.debounce(executeSearch, 800);
const handleSearch = (value) => {
debouncedExecuteSearch({ variables: { search: value } });
}; };
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, { const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
@@ -45,7 +42,7 @@ export default function ChatTagRoContainer({ conversation }) {
conversation.job_conversations.map((i) => i.jobid); conversation.job_conversations.map((i) => i.jobid);
const roOptions = data const roOptions = data
? data.jobs.filter((job) => !existingJobTags.includes(job.id)) ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
: []; : [];
return ( return (
@@ -53,9 +50,8 @@ export default function ChatTagRoContainer({ conversation }) {
{visible ? ( {visible ? (
<ChatTagRo <ChatTagRo
loading={loading} loading={loading}
searchQueryState={searchQueryState}
roOptions={roOptions} roOptions={roOptions}
executeSearch={executeSearch} handleSearch={handleSearch}
handleInsertTag={handleInsertTag} handleInsertTag={handleInsertTag}
setVisible={setVisible} setVisible={setVisible}
/> />

View File

@@ -1,12 +1,12 @@
import { LoadingOutlined } from "@ant-design/icons";
import { useLazyQuery } from "@apollo/react-hooks"; import { useLazyQuery } from "@apollo/react-hooks";
import { Select } from "antd"; import { Empty, Select } from "antd";
import _ from "lodash";
import React, { forwardRef, useEffect, useState } from "react"; import React, { forwardRef, useEffect, useState } from "react";
import { import {
SEARCH_JOBS_FOR_AUTOCOMPLETE,
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
SEARCH_JOBS_FOR_AUTOCOMPLETE,
} from "../../graphql/jobs.queries"; } from "../../graphql/jobs.queries";
import { LoadingOutlined } from "@ant-design/icons";
import _ from "lodash";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
const { Option } = Select; const { Option } = Select;
@@ -33,7 +33,6 @@ const JobSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
useEffect(() => { useEffect(() => {
if (value === option) { if (value === option) {
console.log("Job ID Provided, searching...");
callIdSearch({ variables: { id: value } }); callIdSearch({ variables: { id: value } });
} }
}, [value, option, callIdSearch]); }, [value, option, callIdSearch]);
@@ -71,7 +70,7 @@ const JobSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
onSearch={handleSearch} onSearch={handleSearch}
// onChange={setOption} // onChange={setOption}
onSelect={handleSelect} onSelect={handleSelect}
notFoundContent={loading ? <LoadingOutlined /> : null} notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
onBlur={onBlur} onBlur={onBlur}
> >
{theOptions {theOptions

View File

@@ -2,7 +2,7 @@ import gql from "graphql-tag";
export const CONVERSATION_LIST_SUBSCRIPTION = gql` export const CONVERSATION_LIST_SUBSCRIPTION = gql`
subscription CONVERSATION_LIST_SUBSCRIPTION { subscription CONVERSATION_LIST_SUBSCRIPTION {
conversations { conversations(order_by: { updated_at: desc }, limit: 100) {
phone_num phone_num
id id
job_conversations { job_conversations {

View File

@@ -711,7 +711,7 @@ export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
export const SEARCH_FOR_JOBS = gql` export const SEARCH_FOR_JOBS = gql`
query SEARCH_FOR_JOBS($search: String!) { query SEARCH_FOR_JOBS($search: String!) {
jobs(where: { ro_number: { _ilike: $search } }) { search_jobs(args: { search: $search }) {
id id
ro_number ro_number
ownr_fn ownr_fn

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,9 @@
- args:
cascade: true
read_only: false
sql: "CREATE OR REPLACE FUNCTION update_conversation_on_message()\nRETURNS TRIGGER
AS $$\nBEGIN\n UPDATE conversations SET last_updated = now() WHERE id = NEW.\"conversationid\";\n
\ RETURN NEW; \nEND;\n$$ language 'plpgsql';\n\nDROP TRIGGER IF EXISTS trigger_update_conversation_on_message
ON messages;\nCREATE TRIGGER trigger_update_conversation_on_message AFTER INSERT
ON messages FOR EACH ROW EXECUTE PROCEDURE update_conversation_on_message();"
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,9 @@
- args:
cascade: true
read_only: false
sql: "CREATE OR REPLACE FUNCTION update_conversation_on_message()\nRETURNS TRIGGER
AS $$\nBEGIN\n UPDATE conversations SET updated_at = now() WHERE id = NEW.\"conversationid\";\n
\ RETURN NEW; \nEND;\n$$ language 'plpgsql';\n\nDROP TRIGGER IF EXISTS trigger_update_conversation_on_message
ON messages;\nCREATE TRIGGER trigger_update_conversation_on_message AFTER INSERT
ON messages FOR EACH ROW EXECUTE PROCEDURE update_conversation_on_message();"
type: run_sql