Added sorting to msgs sub and added some styling for messaging items. BOD-309 BOD-310
This commit is contained in:
@@ -10,11 +10,13 @@ import "./chat-affix.styles.scss";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
chatVisible: selectChatVisible,
|
||||
});
|
||||
|
||||
export function ChatAffixContainer({ bodyshop }) {
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { loading, error, data } = useSubscription(
|
||||
CONVERSATION_LIST_SUBSCRIPTION,
|
||||
{
|
||||
@@ -26,7 +28,7 @@ export function ChatAffixContainer({ bodyshop }) {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<Affix className="chat-affix">
|
||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||
<div>
|
||||
{bodyshop && bodyshop.messagingservicesid ? (
|
||||
<ChatAffixComponent
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Badge, List } from "antd";
|
||||
import { Badge, List, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
@@ -22,43 +21,46 @@ export function ChatConversationListComponent({
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<List
|
||||
bordered
|
||||
size="small"
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<PhoneFormatter>{item.phone_num}</PhoneFormatter>}
|
||||
description={
|
||||
item.job_conversations.length > 0 ? (
|
||||
<div>
|
||||
{item.job_conversations.map(
|
||||
(j) =>
|
||||
`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
t("messaging.labels.nojobs")
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<div className="chat-list-container">
|
||||
<List
|
||||
bordered
|
||||
size="small"
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<div className="chat-name">
|
||||
{item.job_conversations.map((j, idx) => (
|
||||
<span key={idx}>
|
||||
{`${j.job.ownr_fn || ""} ${j.job.ownr_ln || ""} ${
|
||||
j.job.ownr_co_nm || ""
|
||||
} `}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
)}
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
.chat-list-selected-conversation {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
.chat-list-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #ff7a00;
|
||||
}
|
||||
.chat-name {
|
||||
flex: 1;
|
||||
display: inline;
|
||||
}
|
||||
.ro-number-tag {
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,14 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
||||
<Tag
|
||||
key={item.job.id}
|
||||
closable
|
||||
color='blue'
|
||||
color="blue"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClose={() => handleRemoveTag(item.job.id)}>
|
||||
onClose={() => handleRemoveTag(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>
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { Space } from "antd";
|
||||
import React from "react";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
return (
|
||||
<div>
|
||||
<Space>
|
||||
<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">
|
||||
<div className="imex-flex-row">
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
}
|
||||
/>
|
||||
<ChatTagRoContainer conversation={conversation || []} />
|
||||
<ChatPresetsComponent />
|
||||
</div>
|
||||
<div className="imex-flex-row">
|
||||
<PhoneNumberFormatter>
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// bottom: 0rem;
|
||||
color: whitesmoke;
|
||||
border: #000000;
|
||||
margin-left: 0.2rem;
|
||||
margin-right: 0rem;
|
||||
// z-index: 5;
|
||||
position: absolute;
|
||||
margin: 0 0.1rem;
|
||||
bottom: 0.1rem;
|
||||
right: 0.3rem;
|
||||
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.chat {
|
||||
@@ -84,6 +87,7 @@
|
||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||
background-attachment: fixed;
|
||||
position: relative;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.mine .message.last:before {
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ChatPopupComponent({
|
||||
style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
|
||||
/>
|
||||
|
||||
<Row className="chat-popup-content">
|
||||
<Row gutter={[8, 8]} className="chat-popup-content">
|
||||
<Col span={8}>
|
||||
<ChatConversationListComponent conversationList={conversationList} />
|
||||
</Col>
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
}
|
||||
|
||||
.chat-popup-content {
|
||||
//height: 50vh;
|
||||
min-height: 0px; /* IMPORTANT: you need this for non-chrome browsers */
|
||||
flex: 1;
|
||||
//height: 100%;
|
||||
.ant-col {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setMessage } from "../../redux/messaging/messaging.actions";
|
||||
@@ -16,9 +15,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setMessage: (message) => dispatch(setMessage(message)),
|
||||
});
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
||||
@@ -30,15 +27,9 @@ export function ChatPresetsComponent({ bodyshop, setMessage }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
{t("messaging.labels.presets")} <DownOutlined />
|
||||
</a>
|
||||
<PlusCircleOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
selectMessage,
|
||||
} from "../../redux/messaging/messaging.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -53,6 +54,8 @@ function ChatSendMessageComponent({
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row">
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
|
||||
<Input.TextArea
|
||||
className="imex-flex-row__margin imex-flex-row__grow"
|
||||
allowClear
|
||||
@@ -68,6 +71,7 @@ function ChatSendMessageComponent({
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SendOutlined className="imex-flex-row__margin" onClick={handleEnter} />
|
||||
<Spin
|
||||
style={{ display: `${isSending ? "" : "none"}` }}
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { Select, Empty } from "antd";
|
||||
import React from "react";
|
||||
import { AutoComplete } from "antd";
|
||||
import { LoadingOutlined, CloseCircleOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChatTagRoComponent({
|
||||
searchQueryState,
|
||||
roOptions,
|
||||
loading,
|
||||
executeSearch,
|
||||
handleSearch,
|
||||
handleInsertTag,
|
||||
setVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setSearchQuery = searchQueryState[1];
|
||||
const handleSearchQuery = (value) => {
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
executeSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<AutoComplete
|
||||
style={{ width: 100 }}
|
||||
onSearch={handleSearchQuery}
|
||||
onSelect={handleInsertTag}
|
||||
<div>
|
||||
<Select
|
||||
showSearch
|
||||
autoFocus
|
||||
style={{
|
||||
width: 300,
|
||||
}}
|
||||
placeholder={t("general.labels.search")}
|
||||
onKeyDown={handleKeyDown}
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleInsertTag}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
>
|
||||
{roOptions.map((item, idx) => (
|
||||
<AutoComplete.Option key={item.id || idx}>
|
||||
<Select.Option key={item.id || idx}>
|
||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||
item.ownr_ln || ""
|
||||
} ${item.ownr_co_nm || ""}`}
|
||||
</AutoComplete.Option>
|
||||
</Select.Option>
|
||||
))}
|
||||
</AutoComplete>
|
||||
</Select>
|
||||
{loading ? <LoadingOutlined /> : null}
|
||||
|
||||
{loading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { 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 { 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 }) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const searchQueryState = useState("");
|
||||
const searchText = searchQueryState[0];
|
||||
|
||||
const [loadRo, { called, loading, data, refetch }] = useLazyQuery(
|
||||
SEARCH_FOR_JOBS,
|
||||
{
|
||||
variables: { search: `%${searchText}%` },
|
||||
}
|
||||
);
|
||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||
|
||||
const executeSearch = () => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: searchText });
|
||||
if (called) refetch();
|
||||
else {
|
||||
loadRo();
|
||||
}
|
||||
const executeSearch = (v) => {
|
||||
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
||||
loadRo(v);
|
||||
};
|
||||
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 800);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
|
||||
@@ -45,7 +42,7 @@ export default function ChatTagRoContainer({ conversation }) {
|
||||
conversation.job_conversations.map((i) => i.jobid);
|
||||
|
||||
const roOptions = data
|
||||
? data.jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -53,9 +50,8 @@ export default function ChatTagRoContainer({ conversation }) {
|
||||
{visible ? (
|
||||
<ChatTagRo
|
||||
loading={loading}
|
||||
searchQueryState={searchQueryState}
|
||||
roOptions={roOptions}
|
||||
executeSearch={executeSearch}
|
||||
handleSearch={handleSearch}
|
||||
handleInsertTag={handleInsertTag}
|
||||
setVisible={setVisible}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
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 {
|
||||
SEARCH_JOBS_FOR_AUTOCOMPLETE,
|
||||
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
||||
SEARCH_JOBS_FOR_AUTOCOMPLETE,
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import _ from "lodash";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -33,7 +33,6 @@ const JobSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (value === option) {
|
||||
console.log("Job ID Provided, searching...");
|
||||
callIdSearch({ variables: { id: value } });
|
||||
}
|
||||
}, [value, option, callIdSearch]);
|
||||
@@ -71,7 +70,7 @@ const JobSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => {
|
||||
onSearch={handleSearch}
|
||||
// onChange={setOption}
|
||||
onSelect={handleSelect}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : null}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{theOptions
|
||||
|
||||
@@ -2,7 +2,7 @@ import gql from "graphql-tag";
|
||||
|
||||
export const CONVERSATION_LIST_SUBSCRIPTION = gql`
|
||||
subscription CONVERSATION_LIST_SUBSCRIPTION {
|
||||
conversations {
|
||||
conversations(order_by: { updated_at: desc }, limit: 100) {
|
||||
phone_num
|
||||
id
|
||||
job_conversations {
|
||||
|
||||
@@ -711,7 +711,7 @@ export const SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE = gql`
|
||||
|
||||
export const SEARCH_FOR_JOBS = gql`
|
||||
query SEARCH_FOR_JOBS($search: String!) {
|
||||
jobs(where: { ro_number: { _ilike: $search } }) {
|
||||
search_jobs(args: { search: $search }) {
|
||||
id
|
||||
ro_number
|
||||
ownr_fn
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user