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 { 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

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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;
}
}

View File

@@ -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>
))}

View File

@@ -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>
);

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>
);

View File

@@ -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"}` }}

View File

@@ -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>
);
}

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 { 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}
/>

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

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