@apollo/client#useLazyQuery TypeScript Examples
The following examples show how to use
@apollo/client#useLazyQuery.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: useRefreshReduxMe.ts From Full-Stack-React-TypeScript-and-Node with MIT License | 7 votes |
useRefreshReduxMe = (): UseRefreshReduxMeResult => {
const [execMe, { data }] = useLazyQuery(Me);
const reduxDispatcher = useDispatch();
const deleteMe = () => {
reduxDispatcher({
type: UserProfileSetType,
payload: null,
});
};
const updateMe = () => {
if (data && data.me && data.me.userName) {
reduxDispatcher({
type: UserProfileSetType,
payload: data.me,
});
}
};
return { execMe, deleteMe, updateMe };
}
Example #2
Source File: useCustomer.ts From magento_react_native_graphql with MIT License | 6 votes |
useCustomer = (): Result => {
const [
getCustomer,
{ data, loading, error },
] = useLazyQuery<GetCustomerDataType>(GET_CUSTOMER);
return {
getCustomer,
data,
loading,
error,
};
}
Example #3
Source File: index.tsx From surveyo with Apache License 2.0 | 6 votes |
function DownloadCsv(props: any) {
const [getCsv, {loading, error, data}] = useLazyQuery<
GetCsvResponses,
GetCsvResponsesVariables
>(GET_CSV);
if (error) {
console.error(error);
message.error('Internal error: could not generate CSV');
}
return data ? (
<Tooltip title="Download CSV">
<CSVLink data={makeCsv(data)} filename={`${props.title}.csv`}>
<Button type="link" icon={<DownloadOutlined />} />
</CSVLink>
</Tooltip>
) : (
<Tooltip title="Generate CSV">
<Button
type="link"
icon={loading ? <LoadingOutlined /> : <FileExcelOutlined />}
onClick={() => getCsv({variables: {id: props.id}})}
/>
</Tooltip>
);
}
Example #4
Source File: useEntityListData.ts From jmix-frontend with Apache License 2.0 | 5 votes |
export function useEntityListData<
TEntity,
TData extends Record<string, any> = Record<string, any>,
TListQueryVars = any
>({
entityList,
listQuery,
listQueryOptions,
filter,
sortOrder,
pagination,
entityName,
lazyLoading
}: EntityListDataHookOptions<TEntity, TData, TListQueryVars>): EntityListDataHookResult<TEntity, TData, TListQueryVars> {
const optsWithVars = {
variables: {
filter,
orderBy: sortOrder,
limit: pagination?.pageSize,
offset: calcOffset(pagination?.current, pagination?.pageSize),
} as TListQueryVars & ListQueryVars,
...listQueryOptions
};
const [executeListQuery, listQueryResult] = useLazyQuery<TData, TListQueryVars>(listQuery, optsWithVars);
// Load items
useEffect(() => {
// We execute the list query unless `entityList` has been passed directly.
// We don't need relation options in this case as filters will be disabled.
// If we implement client-side filtering then we'll need to obtain the relation options from backend.
if (!lazyLoading && (entityList == null)) {
executeListQuery();
}
}, [executeListQuery, lazyLoading, entityList]);
const items = entityList == null
? listQueryResult.data?.[getListQueryName(entityName)]
: getDisplayedItems(entityList, pagination, sortOrder, filter);
const count = entityList == null
? listQueryResult.data?.[getCountQueryName(entityName)]
: entityList.length;
const relationOptions = getRelationOptions<TData>(entityName, listQueryResult.data);
return {
items,
count,
relationOptions,
executeListQuery,
listQueryResult
}
}
Example #5
Source File: useSearch.ts From magento_react_native_graphql with MIT License | 5 votes |
useSearch = (): Result => {
const [searchText, handleChange] = useState<string>('');
const [currentPage, setCurrentPage] = useState<number>(1);
const [
getSearchProducts,
{ called, loading, error, networkStatus, fetchMore, data },
] = useLazyQuery<SearchProductsDataType, GetSearchProductsVars>(
GET_SEARCH_PRODUCTS,
{
notifyOnNetworkStatusChange: true,
},
);
useEffect(() => {
if (searchText.trim().length < LIMITS.searchTextMinLength) {
// Don't do anything
return;
}
const task = setTimeout(() => {
getSearchProducts({
variables: {
searchText,
pageSize: LIMITS.searchScreenPageSize,
currentPage: 1,
},
});
setCurrentPage(1);
}, LIMITS.autoSearchApiTimeDelay);
// eslint-disable-next-line consistent-return
return () => {
clearTimeout(task);
};
}, [searchText]);
useEffect(() => {
if (currentPage === 1) return;
fetchMore?.({
variables: {
currentPage,
},
});
}, [currentPage]);
const loadMore = () => {
if (loading) {
return;
}
if (
currentPage * LIMITS.searchScreenPageSize ===
data?.products?.items?.length &&
data?.products?.items.length < data?.products?.totalCount
) {
setCurrentPage(prevPage => prevPage + 1);
}
};
return {
data,
networkStatus,
called,
error,
searchText,
loadMore,
handleChange,
getSearchProducts,
};
}
Example #6
Source File: useCart.ts From magento_react_native_graphql with MIT License | 5 votes |
useCart = (): Result => {
const { data: { isLoggedIn = false } = {} } = useQuery<IsLoggedInDataType>(
IS_LOGGED_IN,
);
const [
fetchCart,
{ data: cartData, loading: cartLoading, error: cartError },
] = useLazyQuery<GetCartDataType>(GET_CART);
const [_addProductsToCart, { loading: addToCartLoading }] = useMutation<
AddProductsToCartDataType,
AddProductsToCartVars
>(ADD_PRODUCTS_TO_CART, {
onCompleted() {
showMessage({
message: translate('common.success'),
description: translate('productDetailsScreen.addToCartSuccessful'),
type: 'success',
});
},
onError(_error) {
showMessage({
message: translate('common.error'),
description:
_error.message || translate('productDetailsScreen.addToCartError'),
type: 'danger',
});
},
});
const cartCount: string = getCartCount(cartData?.customerCart?.items?.length);
useEffect(() => {
if (isLoggedIn) {
fetchCart();
}
}, [isLoggedIn]);
const addProductsToCart = (productToAdd: CartItemInputType) => {
if (isLoggedIn && cartData?.customerCart.id) {
_addProductsToCart({
variables: {
cartId: cartData.customerCart.id,
cartItems: [productToAdd],
},
});
}
};
return {
addProductsToCart,
isLoggedIn,
cartCount,
cartData,
cartLoading,
cartError,
addToCartLoading,
};
}
Example #7
Source File: useEntityEditorData.ts From jmix-frontend with Apache License 2.0 | 5 votes |
export function useEntityEditorData<
TEntity = unknown,
TData extends Record<string, any> = Record<string, any>,
TQueryVars extends LoadQueryVars = LoadQueryVars,
>({
loadQuery,
loadQueryOptions,
entityInstance,
entityId,
entityName,
cloneEntity
}: EntityEditorDataHookOptions<TEntity, TData, TQueryVars>): EntityEditorDataHookResult<TEntity, TData, TQueryVars> {
const queryName = `${dollarsToUnderscores(entityName)}ById`;
const hasAssociations = editorQueryIncludesRelationOptions(loadQuery);
const loadItem = (entityInstance == null && entityId != null);
const optsWithVars = {
variables: {
id: entityId,
loadItem
} as TQueryVars,
...loadQueryOptions
};
const [executeLoadQuery, loadQueryResult] = useLazyQuery<TData, TQueryVars>(loadQuery, optsWithVars);
// Fetch the entity (if editing) and association options from backend
useEffect(() => {
if (loadItem || hasAssociations) {
executeLoadQuery();
}
}, [loadItem, hasAssociations]);
const {data} = loadQueryResult;
let item = entityInstance != null
? entityInstance
: data?.[queryName];
if (cloneEntity && item != null) {
item = {
...item,
id: undefined
}
}
const relationOptions = getRelationOptions<TData>(entityName, loadQueryResult.data, true);
return {
item,
relationOptions,
executeLoadQuery,
loadQueryResult
};
}
Example #8
Source File: Apollo.ts From graphql-ts-client with MIT License | 5 votes |
export function useTypedLazyQuery<
TData extends object,
TVariables extends object
>(
fetcher: Fetcher<"Query", TData, TVariables>,
options?: QueryHookOptions<TData, TVariables> & {
readonly operationName?: string,
readonly registerDependencies?: boolean | { readonly fieldDependencies: readonly Fetcher<string, object, object>[] }
}
) : QueryTuple<TData, TVariables> {
const body = requestBody(fetcher);
const [operationName, request] = useMemo<[string, DocumentNode]>(() => {
const operationName = options?.operationName ?? `query_${util.toMd5(body)}`;
return [operationName, gql`query ${operationName}${body}`];
}, [body, options?.operationName]);
const [dependencyManager, config] = useContext(dependencyManagerContext);
const register = options?.registerDependencies !== undefined ? !!options.registerDependencies : config?.defaultRegisterDependencies ?? false;
if (register && dependencyManager === undefined) {
throw new Error("The property 'registerDependencies' of options requires <DependencyManagerProvider/>");
}
useEffect(() => {
if (register) {
dependencyManager!.register(
operationName,
fetcher,
typeof options?.registerDependencies === 'object' ? options?.registerDependencies?.fieldDependencies : undefined
);
return () => { dependencyManager!.unregister(operationName); };
}// eslint-disable-next-line
}, [register, dependencyManager, operationName, options?.registerDependencies, request]); // Eslint disable is required, becasue 'fetcher' is replaced by 'request' here.
const response = useLazyQuery<TData, TVariables>(request, options);
const responseData = response[1].data;
const newResponseData = useMemo(() => util.exceptNullValues(responseData), [responseData]);
return newResponseData === responseData ? response : [
response[0],
{ ...response[1], data: newResponseData }
] as QueryTuple<TData, TVariables>;
}
Example #9
Source File: UploadContactsDialog.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
UploadContactsDialog: React.FC<UploadContactsDialogProps> = ({
organizationDetails,
setDialog,
}) => {
const [error, setError] = useState<any>(false);
const [csvContent, setCsvContent] = useState<String | null | ArrayBuffer>('');
const [uploadingContacts, setUploadingContacts] = useState(false);
const [fileName, setFileName] = useState<string>('');
const { t } = useTranslation();
const [collection] = useState();
const [optedIn] = useState(false);
const [getCollections, { data: collections, loading }] = useLazyQuery(
GET_ORGANIZATION_COLLECTIONS
);
useEffect(() => {
if (organizationDetails.id) {
getCollections({
variables: {
organizationGroupsId: organizationDetails.id,
},
});
}
}, [organizationDetails]);
const [importContacts] = useMutation(IMPORT_CONTACTS, {
onCompleted: (data: any) => {
if (data.errors) {
setNotification(data.errors[0].message, 'warning');
} else {
setUploadingContacts(false);
setNotification(t('Contacts have been uploaded'));
}
setDialog(false);
},
onError: (errors) => {
setDialog(false);
setNotification(errors.message, 'warning');
setUploadingContacts(false);
},
});
const addAttachment = (event: any) => {
const media = event.target.files[0];
const reader = new FileReader();
reader.readAsText(media);
reader.onload = () => {
const mediaName = media.name;
const extension = mediaName.slice((Math.max(0, mediaName.lastIndexOf('.')) || Infinity) + 1);
if (extension !== 'csv') {
setError(true);
} else {
const shortenedName = mediaName.length > 15 ? `${mediaName.slice(0, 15)}...` : mediaName;
setFileName(shortenedName);
setCsvContent(reader.result);
}
};
};
const uploadContacts = (details: any) => {
importContacts({
variables: {
type: 'DATA',
data: csvContent,
groupLabel: details.collection.label,
importContactsId: organizationDetails.id,
},
});
};
if (loading || !collections) {
return <Loading />;
}
const validationSchema = Yup.object().shape({
collection: Yup.object().nullable().required(t('Collection is required')),
});
const formFieldItems: any = [
{
component: AutoComplete,
name: 'collection',
placeholder: t('Select collection'),
options: collections.organizationGroups,
multiple: false,
optionLabel: 'label',
textFieldProps: {
label: t('Collection'),
variant: 'outlined',
},
},
{
component: Checkbox,
name: 'optedIn',
title: t('Are these contacts opted in?'),
darkCheckbox: true,
},
];
const form = (
<Formik
enableReinitialize
validationSchema={validationSchema}
initialValues={{ collection, optedIn }}
onSubmit={(itemData) => {
uploadContacts(itemData);
setUploadingContacts(true);
}}
>
{({ submitForm }) => (
<Form data-testid="formLayout">
<DialogBox
titleAlign="left"
title={`${t('Upload contacts')}: ${organizationDetails.name}`}
handleOk={() => {
submitForm();
}}
handleCancel={() => {
setDialog(false);
}}
skipCancel
buttonOkLoading={uploadingContacts}
buttonOk={t('Upload')}
alignButtons="left"
>
<div className={styles.Fields}>
{formFieldItems.map((field: any) => (
<Field {...field} key={field.name} />
))}
</div>
<div className={styles.UploadContainer}>
<label
className={`${styles.UploadEnabled} ${fileName ? styles.Uploaded : ''}`}
htmlFor="uploadFile"
>
<span>
{fileName !== '' ? (
<>
<span>{fileName}</span>
<CrossIcon
className={styles.CrossIcon}
onClick={(event) => {
event.preventDefault();
setFileName('');
}}
/>
</>
) : (
<>
<UploadIcon className={styles.UploadIcon} />
Select .csv
</>
)}
<input
type="file"
id="uploadFile"
disabled={fileName !== ''}
data-testid="uploadFile"
onChange={(event) => {
setError(false);
addAttachment(event);
}}
/>
</span>
</label>
</div>
<div className={styles.Sample}>
<a href={UPLOAD_CONTACTS_SAMPLE}>Download Sample</a>
</div>
{error && (
<div className={styles.Error}>
1. Please make sure the file format matches the sample
</div>
)}
</DialogBox>
</Form>
)}
</Formik>
);
return form;
}
Example #10
Source File: Main.tsx From Full-Stack-React-TypeScript-and-Node with MIT License | 4 votes |
Main = () => {
const [
execGetThreadsByCat,
{
//error: threadsByCatErr,
//called: threadsByCatCalled,
data: threadsByCatData,
},
] = useLazyQuery(GetThreadsByCategoryId);
const [
execGetThreadsLatest,
{
//error: threadsLatestErr,
//called: threadsLatestCalled,
data: threadsLatestData,
},
] = useLazyQuery(GetThreadsLatest);
const { categoryId } = useParams();
const [category, setCategory] = useState<Category | undefined>();
const [threadCards, setThreadCards] = useState<Array<JSX.Element> | null>(
null
);
const history = useHistory();
useEffect(() => {
if (categoryId && categoryId > 0) {
execGetThreadsByCat({
variables: {
categoryId,
},
});
} else {
execGetThreadsLatest();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [categoryId]);
useEffect(() => {
console.log("main threadsByCatData", threadsByCatData);
if (
threadsByCatData &&
threadsByCatData.getThreadsByCategoryId &&
threadsByCatData.getThreadsByCategoryId.threads
) {
const threads = threadsByCatData.getThreadsByCategoryId.threads;
const cards = threads.map((th: any) => {
return <ThreadCard key={`thread-${th.id}`} thread={th} />;
});
setCategory(threads[0].category);
setThreadCards(cards);
} else {
setCategory(undefined);
setThreadCards(null);
}
}, [threadsByCatData]);
useEffect(() => {
if (
threadsLatestData &&
threadsLatestData.getThreadsLatest &&
threadsLatestData.getThreadsLatest.threads
) {
const threads = threadsLatestData.getThreadsLatest.threads;
const cards = threads.map((th: any) => {
return <ThreadCard key={`thread-${th.id}`} thread={th} />;
});
setCategory(new Category("0", "Latest"));
setThreadCards(cards);
}
}, [threadsLatestData]);
const onClickPostThread = () => {
history.push("/thread");
};
return (
<main className="content">
<button className="action-btn" onClick={onClickPostThread}>
Post
</button>
<MainHeader category={category} />
<div>{threadCards}</div>
</main>
);
}
Example #11
Source File: Thread.tsx From Full-Stack-React-TypeScript-and-Node with MIT License | 4 votes |
Thread = () => {
const { width } = useWindowDimensions();
const [execGetThreadById, { data: threadData }] = useLazyQuery(
GetThreadById,
{ fetchPolicy: "no-cache" }
);
const [thread, setThread] = useState<ThreadModel | undefined>();
const { id } = useParams();
const [readOnly, setReadOnly] = useState(false);
const user = useSelector((state: AppState) => state.user);
const [
{ userId, category, title, bodyNode },
threadReducerDispatch,
] = useReducer(threadReducer, {
userId: user ? user.id : "0",
category: undefined,
title: "",
body: "",
bodyNode: undefined,
});
const [postMsg, setPostMsg] = useState("");
const [execCreateThread] = useMutation(CreateThread);
const history = useHistory();
const refreshThread = () => {
if (id && id > 0) {
execGetThreadById({
variables: {
id,
},
});
}
};
useEffect(() => {
if (id && id > 0) {
execGetThreadById({
variables: {
id,
},
});
}
}, [id, execGetThreadById]);
useEffect(() => {
threadReducerDispatch({
type: "userId",
payload: user ? user.id : "0",
});
}, [user]);
useEffect(() => {
if (threadData && threadData.getThreadById) {
setThread(threadData.getThreadById);
setReadOnly(true);
} else {
setThread(undefined);
setReadOnly(false);
}
}, [threadData]);
const receiveSelectedCategory = (cat: Category) => {
threadReducerDispatch({
type: "category",
payload: cat,
});
};
const receiveTitle = (updatedTitle: string) => {
threadReducerDispatch({
type: "title",
payload: updatedTitle,
});
};
const receiveBody = (body: Node[]) => {
threadReducerDispatch({
type: "bodyNode",
payload: body,
});
threadReducerDispatch({
type: "body",
payload: getTextFromNodes(body),
});
};
const onClickPost = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.preventDefault();
console.log("bodyNode", getTextFromNodes(bodyNode));
// gather updated data
if (!userId || userId === "0") {
setPostMsg("You must be logged in before you can post.");
} else if (!category) {
setPostMsg("Please select a category for your post.");
} else if (!title) {
setPostMsg("Please enter a title.");
} else if (!bodyNode) {
setPostMsg("Please enter a body.");
} else {
setPostMsg("");
const newThread = {
userId,
categoryId: category?.id,
title,
body: JSON.stringify(bodyNode),
};
console.log("newThread", newThread);
// send to server to save
const { data: createThreadMsg } = await execCreateThread({
variables: newThread,
});
console.log("createThreadMsg", createThreadMsg);
if (
createThreadMsg.createThread &&
createThreadMsg.createThread.messages &&
!isNaN(createThreadMsg.createThread.messages[0])
) {
setPostMsg("Thread posted successfully.");
history.push(`/thread/${createThreadMsg.createThread.messages[0]}`);
} else {
setPostMsg(createThreadMsg.createThread.messages[0]);
}
}
};
return (
<div className="screen-root-container">
<div className="thread-nav-container">
<Nav />
</div>
<div className="thread-content-container">
<div className="thread-content-post-container">
{width <= 768 && thread ? (
<ThreadPointsInline
points={thread?.points || 0}
threadId={thread?.id}
refreshThread={refreshThread}
allowUpdatePoints={true}
/>
) : null}
<ThreadHeader
userName={thread ? thread.user.userName : user?.userName}
lastModifiedOn={thread ? thread.lastModifiedOn : new Date()}
title={thread ? thread.title : title}
/>
<ThreadCategory
category={thread ? thread.category : category}
sendOutSelectedCategory={receiveSelectedCategory}
/>
<ThreadTitle
title={thread ? thread.title : ""}
readOnly={thread ? readOnly : false}
sendOutTitle={receiveTitle}
/>
<ThreadBody
body={thread ? thread.body : ""}
readOnly={thread ? readOnly : false}
sendOutBody={receiveBody}
/>
{thread ? null : (
<>
<div style={{ marginTop: ".5em" }}>
<button className="action-btn" onClick={onClickPost}>
Post
</button>
</div>
<strong>{postMsg}</strong>
</>
)}
</div>
<div className="thread-content-points-container">
<ThreadPointsBar
points={thread?.points || 0}
responseCount={
(thread && thread.threadItems && thread.threadItems.length) || 0
}
threadId={thread?.id || "0"}
allowUpdatePoints={true}
refreshThread={refreshThread}
/>
</div>
</div>
{thread ? (
<div className="thread-content-response-container">
<hr className="thread-section-divider" />
<div style={{ marginBottom: ".5em" }}>
<strong>Post Response</strong>
</div>
<ThreadResponse
body={""}
userName={user?.userName}
lastModifiedOn={new Date()}
points={0}
readOnly={false}
threadItemId={"0"}
threadId={thread.id}
refreshThread={refreshThread}
/>
</div>
) : null}
{thread ? (
<div className="thread-content-response-container">
<hr className="thread-section-divider" />
<ThreadResponsesBuilder
threadItems={thread?.threadItems}
readOnly={readOnly}
refreshThread={refreshThread}
/>
</div>
) : null}
</div>
);
}
Example #12
Source File: BuildPage.tsx From amplication with Apache License 2.0 | 4 votes |
BuildPage = ({ match }: Props) => {
const { application, buildId } = match.params;
const truncatedId = useMemo(() => {
return truncateId(buildId);
}, [buildId]);
useNavigationTabs(
application,
`${NAVIGATION_KEY}_${buildId}`,
match.url,
`Build ${truncatedId}`
);
const [error, setError] = useState<Error>();
const [getCommit, { data: commitData }] = useLazyQuery<{
commit: models.Commit;
}>(GET_COMMIT);
const { data, error: errorLoading } = useQuery<{
build: models.Build;
}>(GET_BUILD, {
variables: {
buildId: buildId,
},
onCompleted: (data) => {
getCommit({ variables: { commitId: data.build.commitId } });
},
});
const actionLog = useMemo<LogData | null>(() => {
if (!data?.build) return null;
if (!data.build.action) return null;
return {
action: data.build.action,
title: "Build log",
versionNumber: data.build.version,
};
}, [data]);
const errorMessage =
formatError(errorLoading) || (error && formatError(error));
return (
<>
<PageContent className={CLASS_NAME}>
{!data ? (
"loading..."
) : (
<>
<div className={`${CLASS_NAME}__header`}>
<h2>
Build <TruncatedId id={data.build.id} />
</h2>
{commitData && (
<ClickableId
label="Commit"
to={`/${application}/commits/${commitData.commit.id}`}
id={commitData.commit.id}
eventData={{
eventName: "commitHeaderIdClick",
}}
/>
)}
</div>
<div className={`${CLASS_NAME}__build-details`}>
<BuildSteps build={data.build} onError={setError} />
<aside className="log-container">
<ActionLog
action={actionLog?.action}
title={actionLog?.title || ""}
versionNumber={actionLog?.versionNumber || ""}
/>
</aside>
</div>
</>
)}
</PageContent>
<Snackbar open={Boolean(error || errorLoading)} message={errorMessage} />
</>
);
}
Example #13
Source File: BuildingsTable.tsx From condo with MIT License | 4 votes |
export default function BuildingsTable (props: BuildingTableProps) {
const intl = useIntl()
const ExportAsExcel = intl.formatMessage({ id: 'ExportAsExcel' })
const CreateLabel = intl.formatMessage({ id: 'pages.condo.property.index.CreatePropertyButtonLabel' })
const SearchPlaceholder = intl.formatMessage({ id: 'filters.FullSearch' })
const PageTitleMsg = intl.formatMessage({ id: 'pages.condo.property.id.PageTitle' })
const ServerErrorMsg = intl.formatMessage({ id: 'ServerError' })
const PropertiesMessage = intl.formatMessage({ id: 'menu.Property' })
const DownloadExcelLabel = intl.formatMessage({ id: 'pages.condo.property.id.DownloadExcelLabel' })
const PropertyTitle = intl.formatMessage({ id: 'pages.condo.property.ImportTitle' })
const EmptyListLabel = intl.formatMessage({ id: 'pages.condo.property.index.EmptyList.header' })
const EmptyListMessage = intl.formatMessage({ id: 'pages.condo.property.index.EmptyList.text' })
const CreateProperty = intl.formatMessage({ id: 'pages.condo.property.index.CreatePropertyButtonLabel' })
const { role, searchPropertiesQuery, tableColumns, sortBy } = props
const { isSmall } = useLayoutContext()
const router = useRouter()
const { filters, offset } = parseQuery(router.query)
const currentPageIndex = getPageIndexFromOffset(offset, PROPERTY_PAGE_SIZE)
const { loading, error, refetch, objs: properties, count: total } = Property.useObjects({
sortBy,
where: { ...searchPropertiesQuery },
skip: (currentPageIndex - 1) * PROPERTY_PAGE_SIZE,
first: PROPERTY_PAGE_SIZE,
}, {
fetchPolicy: 'network-only',
onCompleted: () => {
props.onSearch && props.onSearch(properties)
},
})
const handleRowAction = (record) => {
return {
onClick: () => {
router.push(`/property/${record.id}/`)
},
}
}
const [downloadLink, setDownloadLink] = useState(null)
const [exportToExcel, { loading: isXlsLoading }] = useLazyQuery(
EXPORT_PROPERTIES_TO_EXCEL,
{
onError: error => {
const message = get(error, ['graphQLErrors', 0, 'extensions', 'messageForUser']) || error.message
notification.error({ message })
},
onCompleted: data => {
setDownloadLink(data.result.linkToFile)
},
},
)
const [columns, propertyNormalizer, propertyValidator, propertyCreator] = useImporterFunctions()
const [search, handleSearchChange] = useSearch<IFilters>(loading)
const isNoBuildingsData = isEmpty(properties) && isEmpty(filters) && !loading
const canManageProperties = get(role, 'canManageProperties', false)
function onExportToExcelButtonClicked () {
exportToExcel({
variables: {
data: {
where: { ...searchPropertiesQuery },
sortBy,
},
},
})
}
if (error) {
return <LoadingOrErrorPage title={PageTitleMsg} loading={loading} error={error ? ServerErrorMsg : null}/>
}
return (
<>
<EmptyListView
label={EmptyListLabel}
message={EmptyListMessage}
button={(
<ImportWrapper
objectsName={PropertiesMessage}
accessCheck={canManageProperties}
onFinish={refetch}
columns={columns}
rowNormalizer={propertyNormalizer}
rowValidator={propertyValidator}
domainTranslate={PropertyTitle}
objectCreator={propertyCreator}
>
<Button
type={'sberPrimary'}
icon={<DiffOutlined/>}
secondary
/>
</ImportWrapper>
)}
createRoute="/property/create"
createLabel={CreateProperty}
containerStyle={{ display: isNoBuildingsData ? 'flex' : 'none' }}
/>
<Row justify={'space-between'} gutter={ROW_VERTICAL_GUTTERS} hidden={isNoBuildingsData}>
<Col span={24}>
<TableFiltersContainer>
<Row justify="space-between" gutter={ROW_VERTICAL_GUTTERS}>
<Col xs={24} lg={12}>
<Row align={'middle'} gutter={ROW_BIG_HORIZONTAL_GUTTERS}>
<Col xs={24} lg={13}>
<Input
placeholder={SearchPlaceholder}
onChange={(e) => {
handleSearchChange(e.target.value)
}}
value={search}
allowClear={true}
/>
</Col>
<Col hidden={isSmall}>
{
downloadLink
? (
<Button
type={'inlineLink'}
icon={<DatabaseFilled/>}
loading={isXlsLoading}
target="_blank"
href={downloadLink}
rel="noreferrer">
{DownloadExcelLabel}
</Button>
)
: (
<Button
type={'inlineLink'}
icon={<DatabaseFilled/>}
loading={isXlsLoading}
onClick={onExportToExcelButtonClicked}>
{ExportAsExcel}
</Button>
)
}
</Col>
</Row>
</Col>
<Col xs={24} lg={6}>
<Row justify={'end'} gutter={ROW_SMALL_HORIZONTAL_GUTTERS}>
<Col hidden={isSmall}>
{
canManageProperties && (
<ImportWrapper
objectsName={PropertiesMessage}
accessCheck={canManageProperties}
onFinish={refetch}
columns={columns}
rowNormalizer={propertyNormalizer}
rowValidator={propertyValidator}
domainTranslate={PropertyTitle}
objectCreator={propertyCreator}
>
<Button
type={'sberPrimary'}
icon={<DiffOutlined/>}
secondary
/>
</ImportWrapper>
)
}
</Col>
<Col>
{
canManageProperties
? (
<Button type="sberPrimary" onClick={() => router.push('/property/create')}>
{CreateLabel}
</Button>
)
: null
}
</Col>
</Row>
</Col>
</Row>
</TableFiltersContainer>
</Col>
<Col span={24}>
<Table
scroll={getTableScrollConfig(isSmall)}
totalRows={total}
loading={loading}
dataSource={properties}
onRow={handleRowAction}
columns={tableColumns}
pageSize={PROPERTY_PAGE_SIZE}
/>
</Col>
</Row>
</>
)
}
Example #14
Source File: SearchForm.tsx From game-store-monorepo-app with MIT License | 4 votes |
SearchForm: React.FC<SearchFormProps> = ({ className, ...rest }) => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = React.useState('');
const [searchVisible, setSearchVisible] = React.useState(false);
const debouncedSearchTerm = useDebounce(searchTerm);
const [searchGames, { data, loading }] = useLazyQuery<SearchGamesQueryResponse>(SEARCH_GAMES);
const listClassName = cn({
hidden: !searchVisible,
});
React.useEffect(() => {
if (debouncedSearchTerm) {
setSearchVisible(true);
} else {
setSearchVisible(false);
}
}, [debouncedSearchTerm]);
React.useEffect(() => {
if (!debouncedSearchTerm) {
return;
}
const queryParams: GamesQueryParams = {
variables: {
page: 1,
pageSize: 10,
search: debouncedSearchTerm,
},
};
searchGames(queryParams);
}, [debouncedSearchTerm, searchGames, searchVisible]);
const gameResults = data?.searchGames?.results;
const listData: ListItem[] = React.useMemo(() => {
if (!gameResults) {
return [];
}
return gameResults.map((item): ListItem => {
return {
id: item.id,
avatarUrl: item.thumbnailImage,
title: item.name,
content: (
<div>
<PlatformLogos data={item.parentPlatforms} className="mt-1" />
<p className="mt-2 text-sm text-base-content-secondary truncate">{`${getMultipleItemNames(
item.genres,
3,
)}`}</p>
</div>
),
};
});
}, [gameResults]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
};
const handleBlur = () => {
setTimeout(() => {
setSearchVisible(false);
}, 200);
};
const handleFocus = () => {
if (!gameResults?.length) {
return;
}
setSearchVisible(true);
};
const onItemClick = (value: ListItem) => {
navigate(`${ROUTES.GAMES}/${value.id}`);
};
const onSearchButtonClick = () => {
navigate(`${ROUTES.GAMES}?search=${searchTerm}`);
};
return (
<div className={cn(className)} {...rest}>
<FormInput
value={searchTerm}
onChange={handleChange}
placeholder="Search games..."
addonElement={
<Button variant="primary" className="font-bold btn-addon-right" onClick={onSearchButtonClick}>
<AiOutlineSearch size={16} />
</Button>
}
onBlur={handleBlur}
onFocus={handleFocus}
/>
<List
data={listData}
onItemClick={onItemClick}
isLoading={loading}
className={cn(
'absolute w-full max-h-96 min-h-12 top-[55px] bg-base-100 overflow-y-auto rounded-xl shadow-2xl',
listClassName,
)}
/>
</div>
);
}
Example #15
Source File: Template.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
Template: React.SFC<TemplateProps> = (props) => {
const {
match,
listItemName,
redirectionLink,
icon,
defaultAttribute = { isHsm: false },
formField,
getSessionTemplatesCallBack,
customStyle,
getUrlAttachmentAndType,
getShortcode,
getExample,
setCategory,
category,
onExampleChange = () => {},
languageStyle = 'dropdown',
} = props;
const [label, setLabel] = useState('');
const [body, setBody] = useState(EditorState.createEmpty());
const [example, setExample] = useState(EditorState.createEmpty());
const [filterLabel, setFilterLabel] = useState('');
const [shortcode, setShortcode] = useState('');
const [language, setLanguageId] = useState<any>({});
const [type, setType] = useState<any>(null);
const [translations, setTranslations] = useState<any>();
const [attachmentURL, setAttachmentURL] = useState<any>();
const [languageOptions, setLanguageOptions] = useState<any>([]);
const [isActive, setIsActive] = useState<boolean>(true);
const [warning, setWarning] = useState<any>();
const [isUrlValid, setIsUrlValid] = useState<any>();
const [templateType, setTemplateType] = useState<string | null>(null);
const [templateButtons, setTemplateButtons] = useState<
Array<CallToActionTemplate | QuickReplyTemplate>
>([]);
const [isAddButtonChecked, setIsAddButtonChecked] = useState(false);
const [nextLanguage, setNextLanguage] = useState<any>('');
const { t } = useTranslation();
const history = useHistory();
const location: any = useLocation();
const states = {
language,
label,
body,
type,
attachmentURL,
shortcode,
example,
category,
isActive,
templateButtons,
isAddButtonChecked,
};
const setStates = ({
isActive: isActiveValue,
language: languageIdValue,
label: labelValue,
body: bodyValue,
example: exampleValue,
type: typeValue,
translations: translationsValue,
MessageMedia: MessageMediaValue,
shortcode: shortcodeValue,
category: categoryValue,
buttonType: templateButtonType,
buttons,
hasButtons,
}: any) => {
if (languageOptions.length > 0 && languageIdValue) {
if (location.state) {
const selectedLangauge = languageOptions.find(
(lang: any) => lang.label === location.state.language
);
history.replace(location.pathname, null);
setLanguageId(selectedLangauge);
} else if (!language.id) {
const selectedLangauge = languageOptions.find(
(lang: any) => lang.id === languageIdValue.id
);
setLanguageId(selectedLangauge);
} else {
setLanguageId(language);
}
}
setLabel(labelValue);
setIsActive(isActiveValue);
if (typeof bodyValue === 'string') {
setBody(getEditorFromContent(bodyValue));
}
if (exampleValue) {
let exampleBody: any;
if (hasButtons) {
setTemplateType(templateButtonType);
const { buttons: buttonsVal, template } = getTemplateAndButtons(
templateButtonType,
exampleValue,
buttons
);
exampleBody = template;
setTemplateButtons(buttonsVal);
} else {
exampleBody = exampleValue;
}
const editorStateBody = getEditorFromContent(exampleValue);
setTimeout(() => setExample(editorStateBody), 0);
setTimeout(() => onExampleChange(exampleBody), 10);
}
if (hasButtons) {
setIsAddButtonChecked(hasButtons);
}
if (typeValue && typeValue !== 'TEXT') {
setType({ id: typeValue, label: typeValue });
} else {
setType('');
}
if (translationsValue) {
const translationsCopy = JSON.parse(translationsValue);
const currentLanguage = language.id || languageIdValue.id;
if (
Object.keys(translationsCopy).length > 0 &&
translationsCopy[currentLanguage] &&
!location.state
) {
const content = translationsCopy[currentLanguage];
setLabel(content.label);
setBody(getEditorFromContent(content.body));
}
setTranslations(translationsValue);
}
if (MessageMediaValue) {
setAttachmentURL(MessageMediaValue.sourceUrl);
} else {
setAttachmentURL('');
}
if (shortcodeValue) {
setTimeout(() => setShortcode(shortcodeValue), 0);
}
if (categoryValue) {
setCategory({ label: categoryValue, id: categoryValue });
}
};
const updateStates = ({
language: languageIdValue,
label: labelValue,
body: bodyValue,
type: typeValue,
MessageMedia: MessageMediaValue,
}: any) => {
if (languageIdValue) {
setLanguageId(languageIdValue);
}
setLabel(labelValue);
if (typeof bodyValue === 'string') {
setBody(getEditorFromContent(bodyValue));
}
if (typeValue && typeValue !== 'TEXT') {
setType({ id: typeValue, label: typeValue });
} else {
setType('');
}
if (MessageMediaValue) {
setAttachmentURL(MessageMediaValue.sourceUrl);
} else {
setAttachmentURL('');
}
};
const { data: languages } = useQuery(USER_LANGUAGES, {
variables: { opts: { order: 'ASC' } },
});
const [getSessionTemplates, { data: sessionTemplates }] = useLazyQuery<any>(FILTER_TEMPLATES, {
variables: {
filter: { languageId: language ? parseInt(language.id, 10) : null },
opts: {
order: 'ASC',
limit: null,
offset: 0,
},
},
});
const [getSessionTemplate, { data: template, loading: templateLoading }] =
useLazyQuery<any>(GET_TEMPLATE);
// create media for attachment
const [createMediaMessage] = useMutation(CREATE_MEDIA_MESSAGE);
useEffect(() => {
if (Object.prototype.hasOwnProperty.call(match.params, 'id') && match.params.id) {
getSessionTemplate({ variables: { id: match.params.id } });
}
}, [match.params]);
useEffect(() => {
if (languages) {
const lang = languages.currentUser.user.organization.activeLanguages.slice();
// sort languages by their name
lang.sort((first: any, second: any) => (first.label > second.label ? 1 : -1));
setLanguageOptions(lang);
if (!Object.prototype.hasOwnProperty.call(match.params, 'id')) setLanguageId(lang[0]);
}
}, [languages]);
useEffect(() => {
if (filterLabel && language && language.id) {
getSessionTemplates();
}
}, [filterLabel, language, getSessionTemplates]);
useEffect(() => {
setShortcode(getShortcode);
}, [getShortcode]);
useEffect(() => {
if (getExample) {
setExample(getExample);
}
}, [getExample]);
const validateTitle = (value: any) => {
let error;
if (value) {
setFilterLabel(value);
let found = [];
if (sessionTemplates) {
if (getSessionTemplatesCallBack) {
getSessionTemplatesCallBack(sessionTemplates);
}
// need to check exact title
found = sessionTemplates.sessionTemplates.filter((search: any) => search.label === value);
if (match.params.id && found.length > 0) {
found = found.filter((search: any) => search.id !== match.params.id);
}
}
if (found.length > 0) {
error = t('Title already exists.');
}
}
return error;
};
const updateTranslation = (value: any) => {
const translationId = value.id;
// restore if selected language is same as template
if (template && template.sessionTemplate.sessionTemplate.language.id === value.id) {
updateStates({
language: value,
label: template.sessionTemplate.sessionTemplate.label,
body: template.sessionTemplate.sessionTemplate.body,
type: template.sessionTemplate.sessionTemplate.type,
MessageMedia: template.sessionTemplate.sessionTemplate.MessageMedia,
});
} else if (translations && !defaultAttribute.isHsm) {
const translationsCopy = JSON.parse(translations);
// restore if translations present for selected language
if (translationsCopy[translationId]) {
updateStates({
language: value,
label: translationsCopy[translationId].label,
body: translationsCopy[translationId].body,
type: translationsCopy[translationId].MessageMedia
? translationsCopy[translationId].MessageMedia.type
: null,
MessageMedia: translationsCopy[translationId].MessageMedia,
});
} else {
updateStates({
language: value,
label: '',
body: '',
type: null,
MessageMedia: null,
});
}
}
};
const handleLanguageChange = (value: any) => {
const selected = languageOptions.find(
({ label: languageLabel }: any) => languageLabel === value
);
if (selected && Object.prototype.hasOwnProperty.call(match.params, 'id')) {
updateTranslation(selected);
} else if (selected) {
setLanguageId(selected);
}
};
const getLanguageId = (value: any) => {
let result = value;
if (languageStyle !== 'dropdown') {
const selected = languageOptions.find((option: any) => option.label === value);
result = selected;
}
// create translations only while updating
if (result && Object.prototype.hasOwnProperty.call(match.params, 'id')) {
updateTranslation(result);
}
if (result) setLanguageId(result);
};
const validateURL = (value: string) => {
if (value && type) {
validateMedia(value, type.id).then((response: any) => {
if (!response.data.is_valid) {
setIsUrlValid(response.data.message);
} else {
setIsUrlValid('');
}
});
}
};
useEffect(() => {
if ((type === '' || type) && attachmentURL) {
validateURL(attachmentURL);
if (getUrlAttachmentAndType) {
getUrlAttachmentAndType(type.id || 'TEXT', { url: attachmentURL });
}
}
}, [type, attachmentURL]);
const displayWarning = () => {
if (type && type.id === 'STICKER') {
setWarning(
<div className={styles.Warning}>
<ol>
<li>{t('Animated stickers are not supported.')}</li>
<li>{t('Captions along with stickers are not supported.')}</li>
</ol>
</div>
);
} else if (type && type.id === 'AUDIO') {
setWarning(
<div className={styles.Warning}>
<ol>
<li>{t('Captions along with audio are not supported.')}</li>
</ol>
</div>
);
} else {
setWarning(null);
}
};
useEffect(() => {
displayWarning();
}, [type]);
let timer: any = null;
const attachmentField = [
{
component: AutoComplete,
name: 'type',
options,
optionLabel: 'label',
multiple: false,
textFieldProps: {
variant: 'outlined',
label: t('Attachment Type'),
},
helperText: warning,
onChange: (event: any) => {
const val = event || '';
if (!event) {
setIsUrlValid(val);
}
setType(val);
},
},
{
component: Input,
name: 'attachmentURL',
type: 'text',
placeholder: t('Attachment URL'),
validate: () => isUrlValid,
inputProp: {
onBlur: (event: any) => {
setAttachmentURL(event.target.value);
},
onChange: (event: any) => {
clearTimeout(timer);
timer = setTimeout(() => setAttachmentURL(event.target.value), 1000);
},
},
},
];
const langOptions = languageOptions && languageOptions.map((val: any) => val.label);
const onLanguageChange = (option: string, form: any) => {
setNextLanguage(option);
const { values } = form;
if (values.label || values.body.getCurrentContent().getPlainText()) {
return;
}
handleLanguageChange(option);
};
const languageComponent =
languageStyle === 'dropdown'
? {
component: AutoComplete,
name: 'language',
options: languageOptions,
optionLabel: 'label',
multiple: false,
textFieldProps: {
variant: 'outlined',
label: t('Language*'),
},
disabled: !!(defaultAttribute.isHsm && match.params.id),
onChange: getLanguageId,
}
: {
component: LanguageBar,
options: langOptions || [],
selectedLangauge: language && language.label,
onLanguageChange,
};
const formFields = [
languageComponent,
{
component: Input,
name: 'label',
placeholder: t('Title*'),
validate: validateTitle,
disabled: !!(defaultAttribute.isHsm && match.params.id),
helperText: defaultAttribute.isHsm
? t('Define what use case does this template serve eg. OTP, optin, activity preference')
: null,
inputProp: {
onBlur: (event: any) => setLabel(event.target.value),
},
},
{
component: EmojiInput,
name: 'body',
placeholder: t('Message*'),
rows: 5,
convertToWhatsApp: true,
textArea: true,
disabled: !!(defaultAttribute.isHsm && match.params.id),
helperText: defaultAttribute.isHsm
? 'You can also use variable and interactive actions. Variable format: {{1}}, Button format: [Button text,Value] Value can be a URL or a phone number.'
: null,
getEditorValue: (value: any) => {
setBody(value);
},
},
];
const addTemplateButtons = (addFromTemplate: boolean = true) => {
let buttons: any = [];
const buttonType: any = {
QUICK_REPLY: { value: '' },
CALL_TO_ACTION: { type: '', title: '', value: '' },
};
if (templateType) {
buttons = addFromTemplate
? [...templateButtons, buttonType[templateType]]
: [buttonType[templateType]];
}
setTemplateButtons(buttons);
};
const removeTemplateButtons = (index: number) => {
const result = templateButtons.filter((val, idx) => idx !== index);
setTemplateButtons(result);
};
useEffect(() => {
if (templateType) {
addTemplateButtons(false);
}
}, [templateType]);
const getTemplateAndButton = (text: string) => {
const exp = /(\|\s\[)|(\|\[)/;
const areButtonsPresent = text.search(exp);
let message: any = text;
let buttons: any = null;
if (areButtonsPresent !== -1) {
buttons = text.substr(areButtonsPresent);
message = text.substr(0, areButtonsPresent);
}
return { message, buttons };
};
// Removing buttons when checkbox is checked or unchecked
useEffect(() => {
if (getExample) {
const { message }: any = getTemplateAndButton(getPlainTextFromEditor(getExample));
onExampleChange(message || '');
}
}, [isAddButtonChecked]);
// Converting buttons to template and vice-versa to show realtime update on simulator
useEffect(() => {
if (templateButtons.length > 0) {
const parse = convertButtonsToTemplate(templateButtons, templateType);
const parsedText = parse.length ? `| ${parse.join(' | ')}` : null;
const { message }: any = getTemplateAndButton(getPlainTextFromEditor(example));
const sampleText: any = parsedText && message + parsedText;
if (sampleText) {
onExampleChange(sampleText);
}
}
}, [templateButtons]);
const handeInputChange = (event: any, row: any, index: any, eventType: any) => {
const { value } = event.target;
const obj = { ...row };
obj[eventType] = value;
const result = templateButtons.map((val: any, idx: number) => {
if (idx === index) return obj;
return val;
});
setTemplateButtons(result);
};
const templateRadioOptions = [
{
component: Checkbox,
title: <Typography variant="h6">Add buttons</Typography>,
name: 'isAddButtonChecked',
disabled: !!(defaultAttribute.isHsm && match.params.id),
handleChange: (value: boolean) => setIsAddButtonChecked(value),
},
{
component: TemplateOptions,
isAddButtonChecked,
templateType,
inputFields: templateButtons,
disabled: !!match.params.id,
onAddClick: addTemplateButtons,
onRemoveClick: removeTemplateButtons,
onInputChange: handeInputChange,
onTemplateTypeChange: (value: string) => setTemplateType(value),
},
];
const hsmFields = formField && [
...formField.slice(0, 1),
...templateRadioOptions,
...formField.slice(1),
];
const fields = defaultAttribute.isHsm
? [formIsActive, ...formFields, ...hsmFields, ...attachmentField]
: [...formFields, ...attachmentField];
// Creating payload for button template
const getButtonTemplatePayload = () => {
const buttons = templateButtons.reduce((result: any, button) => {
const { type: buttonType, value, title }: any = button;
if (templateType === CALL_TO_ACTION) {
const typeObj: any = {
phone_number: 'PHONE_NUMBER',
url: 'URL',
};
const obj: any = { type: typeObj[buttonType], text: title, [buttonType]: value };
result.push(obj);
}
if (templateType === QUICK_REPLY) {
const obj: any = { type: QUICK_REPLY, text: value };
result.push(obj);
}
return result;
}, []);
// get template body
const templateBody = getTemplateAndButton(getPlainTextFromEditor(body));
const templateExample = getTemplateAndButton(getPlainTextFromEditor(example));
return {
hasButtons: true,
buttons: JSON.stringify(buttons),
buttonType: templateType,
body: getEditorFromContent(templateBody.message),
example: getEditorFromContent(templateExample.message),
};
};
const setPayload = (payload: any) => {
let payloadCopy = payload;
let translationsCopy: any = {};
if (template) {
if (template.sessionTemplate.sessionTemplate.language.id === language.id) {
payloadCopy.languageId = language.id;
if (payloadCopy.type) {
payloadCopy.type = payloadCopy.type.id;
// STICKER is a type of IMAGE
if (payloadCopy.type.id === 'STICKER') {
payloadCopy.type = 'IMAGE';
}
} else {
payloadCopy.type = 'TEXT';
}
delete payloadCopy.language;
if (payloadCopy.isHsm) {
payloadCopy.category = payloadCopy.category.label;
if (isAddButtonChecked && templateType) {
const templateButtonData = getButtonTemplatePayload();
Object.assign(payloadCopy, { ...templateButtonData });
}
} else {
delete payloadCopy.example;
delete payloadCopy.isActive;
delete payloadCopy.shortcode;
delete payloadCopy.category;
}
if (payloadCopy.type === 'TEXT') {
delete payloadCopy.attachmentURL;
}
// Removing unnecessary fields
delete payloadCopy.isAddButtonChecked;
delete payloadCopy.templateButtons;
} else if (!defaultAttribute.isHsm) {
let messageMedia = null;
if (payloadCopy.type && payloadCopy.attachmentURL) {
messageMedia = {
type: payloadCopy.type.id,
sourceUrl: payloadCopy.attachmentURL,
};
}
// Update template translation
if (translations) {
translationsCopy = JSON.parse(translations);
translationsCopy[language.id] = {
status: 'approved',
languageId: language,
label: payloadCopy.label,
body: getPlainTextFromEditor(payloadCopy.body),
MessageMedia: messageMedia,
...defaultAttribute,
};
}
payloadCopy = {
translations: JSON.stringify(translationsCopy),
};
}
} else {
// Create template
payloadCopy.languageId = payload.language.id;
if (payloadCopy.type) {
payloadCopy.type = payloadCopy.type.id;
// STICKER is a type of IMAGE
if (payloadCopy.type.id === 'STICKER') {
payloadCopy.type = 'IMAGE';
}
} else {
payloadCopy.type = 'TEXT';
}
if (payloadCopy.isHsm) {
payloadCopy.category = payloadCopy.category.label;
if (isAddButtonChecked && templateType) {
const templateButtonData = getButtonTemplatePayload();
Object.assign(payloadCopy, { ...templateButtonData });
}
} else {
delete payloadCopy.example;
delete payloadCopy.isActive;
delete payloadCopy.shortcode;
delete payloadCopy.category;
}
delete payloadCopy.isAddButtonChecked;
delete payloadCopy.templateButtons;
delete payloadCopy.language;
if (payloadCopy.type === 'TEXT') {
delete payloadCopy.attachmentURL;
}
payloadCopy.translations = JSON.stringify(translationsCopy);
}
return payloadCopy;
};
// create media for attachment
const getMediaId = async (payload: any) => {
const data = await createMediaMessage({
variables: {
input: {
caption: payload.body,
sourceUrl: payload.attachmentURL,
url: payload.attachmentURL,
},
},
});
return data;
};
const validation: any = {
language: Yup.object().nullable().required('Language is required.'),
label: Yup.string().required(t('Title is required.')).max(50, t('Title length is too long.')),
body: Yup.string()
.transform((current, original) => original.getCurrentContent().getPlainText())
.required(t('Message is required.')),
type: Yup.object()
.nullable()
.when('attachmentURL', {
is: (val: string) => val && val !== '',
then: Yup.object().nullable().required(t('Type is required.')),
}),
attachmentURL: Yup.string()
.nullable()
.when('type', {
is: (val: any) => val && val.id,
then: Yup.string().required(t('Attachment URL is required.')),
}),
};
if (defaultAttribute.isHsm && isAddButtonChecked) {
if (templateType === CALL_TO_ACTION) {
validation.templateButtons = Yup.array()
.of(
Yup.object().shape({
type: Yup.string().required('Required'),
title: Yup.string().required('Required'),
value: Yup.string()
.required('Required')
.when('type', {
is: (val: any) => val === 'phone_number',
then: Yup.string().matches(/^\d{10,12}$/, 'Please enter valid phone number.'),
})
.when('type', {
is: (val: any) => val === 'url',
then: Yup.string().matches(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/gi,
'Please enter valid url.'
),
}),
})
)
.min(1)
.max(2);
} else {
validation.templateButtons = Yup.array()
.of(
Yup.object().shape({
value: Yup.string().required('Required'),
})
)
.min(1)
.max(3);
}
}
const validationObj = defaultAttribute.isHsm ? { ...validation, ...HSMValidation } : validation;
const FormSchema = Yup.object().shape(validationObj, [['type', 'attachmentURL']]);
const afterSave = (data: any, saveClick: boolean) => {
if (saveClick) {
return;
}
if (match.params?.id) {
handleLanguageChange(nextLanguage);
} else {
const { sessionTemplate } = data.createSessionTemplate;
history.push(`/speed-send/${sessionTemplate.id}/edit`, {
language: nextLanguage,
});
}
};
if (languageOptions.length < 1 || templateLoading) {
return <Loading />;
}
return (
<FormLayout
{...queries}
match={match}
states={states}
setStates={setStates}
setPayload={setPayload}
validationSchema={FormSchema}
listItemName={listItemName}
dialogMessage={dialogMessage}
formFields={fields}
redirectionLink={redirectionLink}
listItem="sessionTemplate"
icon={icon}
defaultAttribute={defaultAttribute}
getLanguageId={getLanguageId}
languageSupport={false}
isAttachment
getMediaId={getMediaId}
getQueryFetchPolicy="cache-and-network"
button={defaultAttribute.isHsm && !match.params.id ? t('Submit for Approval') : t('Save')}
customStyles={customStyle}
saveOnPageChange={false}
afterSave={!defaultAttribute.isHsm ? afterSave : undefined}
/>
);
}
Example #16
Source File: Tag.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
Tag: React.SFC<TagProps> = ({ match }) => {
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const [keywords, setKeywords] = useState('');
const [colorCode, setColorCode] = useState('#0C976D');
const [parentId, setParentId] = useState<any>(null);
const [filterLabel, setFilterLabel] = useState('');
const [languageId, setLanguageId] = useState<any>(null);
const { t } = useTranslation();
const states = { label, description, keywords, colorCode, parentId };
const { data } = useQuery(GET_TAGS, {
variables: setVariables(),
});
const [getTags, { data: dataTag }] = useLazyQuery<any>(GET_TAGS, {
variables: {
filter: { label: filterLabel, languageId: parseInt(languageId, 10) },
},
});
const setStates = ({
label: labelValue,
description: descriptionValue,
keywords: keywordsValue,
colorCode: colorCodeValue,
parent: parentValue,
}: any) => {
setLabel(labelValue);
setDescription(descriptionValue);
setKeywords(keywordsValue);
setColorCode(colorCodeValue);
if (parentValue) {
setParentId(getObject(data.tags, [parentValue.id])[0]);
}
};
useEffect(() => {
if (filterLabel && languageId) getTags();
}, [filterLabel, languageId, getTags]);
if (!data) return <Loading />;
let tags = [];
tags = data.tags;
// remove the self tag from list
if (match && match.params.id) {
tags = data.tags.filter((tag: any) => tag.id !== match.params.id);
}
const validateTitle = (value: any) => {
let error;
if (value) {
setFilterLabel(value);
let found = [];
if (dataTag) {
// need to check exact title
found = dataTag.tags.filter((search: any) => search.label === value);
if (match.params.id && found.length > 0) {
found = found.filter((search: any) => search.id !== match.params.id);
}
}
if (found.length > 0) {
error = t('Title already exists.');
}
}
return error;
};
const getLanguageId = (value: any) => {
setLanguageId(value);
};
const setPayload = (payload: any) => {
const payloadCopy = payload;
if (payloadCopy.parentId) {
payloadCopy.parentId = payloadCopy.parentId.id;
}
return payloadCopy;
};
const FormSchema = Yup.object().shape({
label: Yup.string().required(t('Title is required.')).max(50, t('Title is too long.')),
description: Yup.string().required(t('Description is required.')),
});
const dialogMessage = t("You won't be able to use this for tagging messages.");
const tagIcon = <TagIcon className={styles.TagIcon} />;
const queries = {
getItemQuery: GET_TAG,
createItemQuery: CREATE_TAG,
updateItemQuery: UPDATE_TAG,
deleteItemQuery: DELETE_TAG,
};
const formFields = (validateTitleCallback: any, tagsList: any, colorCodeValue: string) => [
{
component: Input,
name: 'label',
type: 'text',
placeholder: t('Title'),
validate: validateTitleCallback,
},
{
component: Input,
name: 'description',
type: 'text',
placeholder: t('Description'),
rows: 3,
textArea: true,
},
{
component: Input,
name: 'keywords',
type: 'text',
placeholder: t('Keywords'),
rows: 3,
helperText: t('Use commas to separate the keywords'),
textArea: true,
},
{
component: AutoComplete,
name: 'parentId',
placeholder: t('Parent tag'),
options: tagsList,
optionLabel: 'label',
multiple: false,
textFieldProps: {
label: t('Parent tag'),
variant: 'outlined',
},
},
{
component: ColorPicker,
name: 'colorCode',
colorCode: colorCodeValue,
helperText: t('Tag color'),
},
];
return (
<FormLayout
{...queries}
match={match}
refetchQueries={[
{
query: FILTER_TAGS_NAME,
variables: setVariables(),
},
]}
states={states}
setStates={setStates}
setPayload={setPayload}
validationSchema={FormSchema}
listItemName="tag"
dialogMessage={dialogMessage}
formFields={formFields(validateTitle, tags, colorCode)}
redirectionLink="tag"
listItem="tag"
icon={tagIcon}
getLanguageId={getLanguageId}
/>
);
}
Example #17
Source File: Organisation.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
Organisation: React.SFC = () => {
const client = useApolloClient();
const [name, setName] = useState('');
const [hours, setHours] = useState(true);
const [enabledDays, setEnabledDays] = useState<any>([]);
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const [defaultFlowId, setDefaultFlowId] = useState<any>(null);
const [flowId, setFlowId] = useState<any>(null);
const [isDisabled, setIsDisable] = useState(false);
const [isFlowDisabled, setIsFlowDisable] = useState(true);
const [organizationId, setOrganizationId] = useState(null);
const [newcontactFlowId, setNewcontactFlowId] = useState(null);
const [newcontactFlowEnabled, setNewcontactFlowEnabled] = useState(false);
const [allDayCheck, setAllDayCheck] = useState(false);
const [activeLanguages, setActiveLanguages] = useState([]);
const [defaultLanguage, setDefaultLanguage] = useState<any>(null);
const [signaturePhrase, setSignaturePhrase] = useState();
const [phone, setPhone] = useState<string>('');
const { t } = useTranslation();
const States = {
name,
hours,
startTime,
endTime,
enabledDays,
defaultFlowId,
flowId,
activeLanguages,
newcontactFlowEnabled,
defaultLanguage,
signaturePhrase,
newcontactFlowId,
allDayCheck,
phone,
};
// get the published flow list
const { data: flow } = useQuery(GET_FLOWS, {
variables: setVariables({
status: FLOW_STATUS_PUBLISHED,
}),
fetchPolicy: 'network-only', // set for now, need to check cache issue
});
const { data: languages } = useQuery(GET_LANGUAGES, {
variables: { opts: { order: 'ASC' } },
});
const [getOrg, { data: orgData }] = useLazyQuery<any>(GET_ORGANIZATION);
const getEnabledDays = (data: any) => data.filter((option: any) => option.enabled);
const setOutOfOffice = (data: any) => {
setStartTime(data.startTime);
setEndTime(data.endTime);
setEnabledDays(getEnabledDays(data.enabledDays));
};
const getFlow = (id: string) => flow.flows.filter((option: any) => option.id === id)[0];
const setStates = ({
name: nameValue,
outOfOffice: outOfOfficeValue,
activeLanguages: activeLanguagesValue,
defaultLanguage: defaultLanguageValue,
signaturePhrase: signaturePhraseValue,
contact: contactValue,
newcontactFlowId: newcontactFlowIdValue,
}: any) => {
setName(nameValue);
setHours(outOfOfficeValue.enabled);
setIsDisable(!outOfOfficeValue.enabled);
setOutOfOffice(outOfOfficeValue);
if (outOfOfficeValue.startTime === '00:00:00' && outOfOfficeValue.endTime === '23:59:00') {
setAllDayCheck(true);
}
if (outOfOfficeValue.defaultFlowId) {
// set the value only if default flow is not null
setDefaultFlowId(getFlow(outOfOfficeValue.defaultFlowId));
}
if (newcontactFlowIdValue) {
setNewcontactFlowEnabled(true);
setNewcontactFlowId(getFlow(newcontactFlowIdValue));
}
// set the value only if out of office flow is not null
if (outOfOfficeValue.flowId) {
setFlowId(getFlow(outOfOfficeValue.flowId));
}
setSignaturePhrase(signaturePhraseValue);
if (activeLanguagesValue) setActiveLanguages(activeLanguagesValue);
if (defaultLanguageValue) setDefaultLanguage(defaultLanguageValue);
setPhone(contactValue.phone);
};
useEffect(() => {
getOrg();
}, [getOrg]);
useEffect(() => {
if (orgData) {
const data = orgData.organization.organization;
// get login OrganizationId
setOrganizationId(data.id);
const days = orgData.organization.organization.outOfOffice.enabledDays;
const selectedDays = Object.keys(days).filter((k) => days[k].enabled === true);
// show another flow if days are selected
if (selectedDays.length > 0) setIsFlowDisable(false);
}
}, [orgData]);
if (!flow || !languages) return <Loading />;
const handleChange = (value: any) => {
setIsDisable(!value);
};
let activeLanguage: any = [];
const validateActiveLanguages = (value: any) => {
activeLanguage = value;
if (!value || value.length === 0) {
return t('Supported language is required.');
}
return null;
};
const validateDefaultLanguage = (value: any) => {
let error;
if (!value) {
error = t('Default language is required.');
}
if (value) {
const IsPresent = activeLanguage.filter((language: any) => language.id === value.id);
if (IsPresent.length === 0) error = t('Default language needs to be an active language.');
}
return error;
};
const validateOutOfOfficeFlow = (value: any) => {
let error;
if (!isDisabled && !value) {
error = t('Please select default flow ');
}
return error;
};
const validateDaysSelection = (value: any) => {
let error;
if (!isDisabled && value.length === 0) {
error = t('Please select days');
}
return error;
};
const handleAllDayCheck = (addDayCheck: boolean) => {
if (!allDayCheck) {
setStartTime('00:00:00');
setEndTime('23:59:00');
}
setAllDayCheck(addDayCheck);
};
const handleChangeInDays = (value: any) => {
if (value.length > 0) {
setIsFlowDisable(false);
}
};
const validation = {
name: Yup.string().required(t('Organisation name is required.')),
activeLanguages: Yup.array().required(t('Supported Languages is required.')),
defaultLanguage: Yup.object().nullable().required(t('Default Language is required.')),
signaturePhrase: Yup.string().nullable().required(t('Webhook signature is required.')),
endTime: Yup.string()
.test('is-midnight', t('End time cannot be 12 AM'), (value) => value !== 'T00:00:00')
.test('is-valid', t('Not a valid time'), (value) => value !== 'Invalid date'),
startTime: Yup.string().test(
'is-valid',
t('Not a valid time'),
(value) => value !== 'Invalid date'
),
newcontactFlowId: Yup.object()
.nullable()
.when('newcontactFlowEnabled', {
is: (val: string) => val,
then: Yup.object().nullable().required(t('New contact flow is required.')),
}),
};
const FormSchema = Yup.object().shape(validation);
const formFields: any = [
{
component: Input,
name: 'name',
type: 'text',
placeholder: t('Organisation name'),
},
{
component: AutoComplete,
name: 'activeLanguages',
options: languages.languages,
optionLabel: 'label',
textFieldProps: {
variant: 'outlined',
label: t('Supported languages'),
},
validate: validateActiveLanguages,
},
{
component: AutoComplete,
name: 'defaultLanguage',
options: languages.languages,
optionLabel: 'label',
multiple: false,
textFieldProps: {
variant: 'outlined',
label: t('Default language'),
},
validate: validateDefaultLanguage,
},
{
component: Input,
name: 'signaturePhrase',
type: 'text',
placeholder: t('Webhook signature'),
},
{
component: Input,
name: 'phone',
type: 'text',
placeholder: t('Organisation phone number'),
disabled: true,
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="phone number"
data-testid="phoneNumber"
onClick={() => copyToClipboard(phone)}
edge="end"
>
<CopyIcon />
</IconButton>
</InputAdornment>
),
},
{
component: Checkbox,
name: 'hours',
title: <Typography className={styles.CheckboxLabel}>{t('Default flow')}</Typography>,
handleChange,
},
{
component: Checkbox,
name: 'newcontactFlowEnabled',
title: <Typography className={styles.CheckboxLabel}>{t('New contact flow')}</Typography>,
handleChange: setNewcontactFlowEnabled,
},
{
component: AutoComplete,
name: 'defaultFlowId',
options: flow.flows,
optionLabel: 'name',
multiple: false,
textFieldProps: {
variant: 'outlined',
label: t('Select flow'),
},
disabled: isDisabled,
helperText: t(
'The selected flow will trigger when end-users aren’t in any flow, their message doesn’t match any keyword, and the time of their message is as defined above. Note that the default flow is executed only once a day.'
),
validate: validateOutOfOfficeFlow,
},
{
component: AutoComplete,
name: 'newcontactFlowId',
options: flow.flows,
optionLabel: 'name',
multiple: false,
disabled: !newcontactFlowEnabled,
textFieldProps: {
variant: 'outlined',
label: t('Select flow'),
},
helperText: t('For new contacts messaging your chatbot for the first time'),
},
{
component: AutoComplete,
name: 'enabledDays',
options: dayList,
optionLabel: 'label',
textFieldProps: {
variant: 'outlined',
label: t('Select days'),
},
disabled: isDisabled,
onChange: handleChangeInDays,
validate: validateDaysSelection,
},
{
component: Checkbox,
disabled: isDisabled,
name: 'allDayCheck',
title: <Typography className={styles.AddDayLabel}>{t('All day')}</Typography>,
handleChange: handleAllDayCheck,
},
{
component: TimePicker,
name: 'startTime',
placeholder: t('Start'),
disabled: isDisabled || allDayCheck,
helperText: t('Note: The next day begins after 12AM.'),
},
{
component: TimePicker,
name: 'endTime',
placeholder: t('Stop'),
disabled: isDisabled || allDayCheck,
},
];
if (isFlowDisabled === false) {
formFields.push({
component: AutoComplete,
name: 'flowId',
options: flow.flows,
optionLabel: 'name',
multiple: false,
textFieldProps: {
variant: 'outlined',
label: t('Select flow'),
},
disabled: isDisabled,
questionText: t('Would you like to trigger a flow for all the other days & times?'),
});
}
const assignDays = (enabledDay: any) => {
const array: any = [];
for (let i = 0; i < 7; i += 1) {
array[i] = { id: i + 1, enabled: false };
enabledDay.forEach((days: any) => {
if (i + 1 === days.id) {
array[i] = { id: i + 1, enabled: true };
}
});
}
return array;
};
const saveHandler = (data: any) => {
// update organization details in the cache
client.writeQuery({
query: GET_ORGANIZATION,
data: data.updateOrganization,
});
};
const setPayload = (payload: any) => {
let object: any = {};
// set active Language Ids
const activeLanguageIds = payload.activeLanguages.map((language: any) => language.id);
let newContactFlowId = null;
if (newcontactFlowEnabled) {
newContactFlowId = payload.newcontactFlowId.id;
}
const defaultLanguageId = payload.defaultLanguage.id;
object = {
name: payload.name,
outOfOffice: {
defaultFlowId: payload.defaultFlowId ? payload.defaultFlowId.id : null,
enabled: payload.hours,
enabledDays: assignDays(payload.enabledDays),
endTime: payload.endTime,
flowId: payload.flowId ? payload.flowId.id : null,
startTime: payload.startTime,
},
defaultLanguageId,
activeLanguageIds,
newcontactFlowId: newContactFlowId,
signaturePhrase: payload.signaturePhrase,
};
return object;
};
return (
<FormLayout
backLinkButton={{ text: t('Back to settings'), link: '/settings' }}
{...queries}
title="organization settings"
match={{ params: { id: organizationId } }}
states={States}
setStates={setStates}
validationSchema={FormSchema}
setPayload={setPayload}
listItemName="Settings"
dialogMessage=""
formFields={formFields}
refetchQueries={[{ query: USER_LANGUAGES }]}
redirectionLink="settings"
cancelLink="settings"
linkParameter="id"
listItem="organization"
icon={SettingIcon}
languageSupport={false}
type="settings"
redirect
afterSave={saveHandler}
customStyles={styles.organization}
/>
);
}
Example #18
Source File: Billing.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
BillingForm: React.FC<BillingProps> = () => {
const stripe = useStripe();
const elements = useElements();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [disable, setDisable] = useState(false);
const [paymentMethodId, setPaymentMethodId] = useState('');
const [cardError, setCardError] = useState<any>('');
const [alreadySubscribed, setAlreadySubscribed] = useState(false);
const [pending, setPending] = useState(false);
const [couponApplied, setCouponApplied] = useState(false);
const [coupon] = useState('');
const { t } = useTranslation();
const validationSchema = Yup.object().shape({
name: Yup.string().required(t('Name is required.')),
email: Yup.string().email().required(t('Email is required.')),
});
// get organization billing details
const {
data: billData,
loading: billLoading,
refetch,
} = useQuery(GET_ORGANIZATION_BILLING, {
fetchPolicy: 'network-only',
});
const [getCouponCode, { data: couponCode, loading: couponLoading, error: couponError }] =
useLazyQuery(GET_COUPON_CODE, {
onCompleted: ({ getCouponCode: couponCodeResult }) => {
if (couponCodeResult.code) {
setCouponApplied(true);
}
},
});
const [getCustomerPortal, { loading: portalLoading }] = useLazyQuery(GET_CUSTOMER_PORTAL, {
fetchPolicy: 'network-only',
onCompleted: (customerPortal: any) => {
window.open(customerPortal.customerPortal.url, '_blank');
},
});
const formFieldItems = [
{
component: Input,
name: 'name',
type: 'text',
placeholder: 'Your Organization Name',
disabled: alreadySubscribed || pending || disable,
},
{
component: Input,
name: 'email',
type: 'text',
placeholder: 'Email ID',
disabled: alreadySubscribed || pending || disable,
},
];
useEffect(() => {
// Set name and email if a customer is already created
if (billData && billData.getOrganizationBilling?.billing) {
const billing = billData.getOrganizationBilling?.billing;
setName(billing?.name);
setEmail(billing?.email);
if (billing?.stripeSubscriptionStatus === null) {
setPending(false);
}
}
}, [billData]);
const [updateBilling] = useMutation(UPDATE_BILLING);
const [createBilling] = useMutation(CREATE_BILLING);
const [createSubscription] = useMutation(CREATE_BILLING_SUBSCRIPTION, {
onCompleted: (data) => {
const result = JSON.parse(data.createBillingSubscription.subscription);
// needs additional security (3d secure)
if (result.status === 'pending') {
if (stripe) {
stripe
.confirmCardSetup(result.client_secret, {
payment_method: paymentMethodId,
})
.then((securityResult: any) => {
if (securityResult.error?.message) {
setNotification(securityResult.error?.message, 'warning');
setLoading(false);
refetch().then(({ data: refetchedData }) => {
updateBilling({
variables: {
id: refetchedData.getOrganizationBilling?.billing?.id,
input: {
stripeSubscriptionId: null,
stripeSubscriptionStatus: null,
},
},
}).then(() => {
refetch();
});
});
} else if (securityResult.setupIntent.status === 'succeeded') {
setDisable(true);
setLoading(false);
setNotification('Your billing account is setup successfully');
}
});
}
} // successful subscription
else if (result.status === 'active') {
refetch();
setDisable(true);
setLoading(false);
setNotification('Your billing account is setup successfully');
}
},
onError: (error) => {
refetch();
setNotification(error.message, 'warning');
setLoading(false);
},
});
if (billLoading || portalLoading) {
return <Loading />;
}
// check if the organization is already subscribed or in pending state
if (billData && !alreadySubscribed && !pending) {
const billingDetails = billData.getOrganizationBilling?.billing;
if (billingDetails) {
const { stripeSubscriptionId, stripeSubscriptionStatus } = billingDetails;
if (stripeSubscriptionId && stripeSubscriptionStatus === 'pending') {
setPending(true);
} else if (stripeSubscriptionId && stripeSubscriptionStatus === 'active') {
setAlreadySubscribed(true);
}
}
}
const stripePayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet. Make sure to disable
// form submission until Stripe.js has loaded.
return;
}
// Get a reference to a mounted CardElement. Elements knows how
// to find your CardElement because there can only ever be one of
// each type of element.
const cardElement: any = elements.getElement(CardElement);
// create a payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
setLoading(false);
refetch();
setNotification(error.message ? error.message : 'An error occurred', 'warning');
} else if (paymentMethod) {
setPaymentMethodId(paymentMethod.id);
const variables: any = {
stripePaymentMethodId: paymentMethod.id,
};
if (couponApplied) {
variables.couponCode = couponCode.getCouponCode.id;
}
await createSubscription({
variables,
});
}
};
const handleSubmit = async (itemData: any) => {
const { name: billingName, email: billingEmail } = itemData;
setLoading(true);
if (billData) {
const billingDetails = billData.getOrganizationBilling?.billing;
if (billingDetails) {
// Check if customer needs to be updated
if (billingDetails.name !== billingName || billingDetails.email !== billingEmail) {
updateBilling({
variables: {
id: billingDetails.id,
input: {
name: billingName,
email: billingEmail,
currency: 'inr',
},
},
})
.then(() => {
stripePayment();
})
.catch((error) => {
setNotification(error.message, 'warning');
});
} else {
stripePayment();
}
} else {
// There is no customer created. Creating a customer first
createBilling({
variables: {
input: {
name: billingName,
email: billingEmail,
currency: 'inr',
},
},
})
.then(() => {
stripePayment();
})
.catch((error) => {
setNotification(error.message, 'warning');
});
}
}
};
const backLink = (
<div className={styles.BackLink}>
<Link to="/settings">
<BackIcon />
Back to settings
</Link>
</div>
);
const cardElements = (
<>
<CardElement
options={{ hidePostalCode: true }}
className={styles.Card}
onChange={(e) => {
setCardError(e.error?.message);
}}
/>
<div className={styles.Error}>
<small>{cardError}</small>
</div>
<div className={styles.Helper}>
<small>Once subscribed you will be charged on basis of your usage automatically</small>
</div>
</>
);
const subscribed = (
<div>
<div className={styles.Subscribed}>
<ApprovedIcon />
You have an active subscription
<div>
Please <span>contact us</span> to deactivate
<br />
*Note that we do not store your credit card details, as Stripe securely does.
</div>
</div>
<div
aria-hidden
className={styles.Portal}
data-testid="customerPortalButton"
onClick={() => {
getCustomerPortal();
}}
>
Visit Stripe portal <CallMadeIcon />
</div>
</div>
);
let paymentBody = alreadySubscribed || disable ? subscribed : cardElements;
if (pending) {
paymentBody = (
<div>
<div className={styles.Pending}>
<PendingIcon className={styles.PendingIcon} />
Your payment is in pending state
</div>
<div
aria-hidden
className={styles.Portal}
data-testid="customerPortalButton"
onClick={() => {
getCustomerPortal();
}}
>
Visit Stripe portal <CallMadeIcon />
</div>
</div>
);
}
const couponDescription = couponCode && JSON.parse(couponCode.getCouponCode.metadata);
const processIncomplete = !alreadySubscribed && !pending && !disable;
return (
<div className={styles.Form}>
<Typography variant="h5" className={styles.Title}>
<IconButton disabled className={styles.Icon}>
<Settingicon />
</IconButton>
Billing
</Typography>
{backLink}
<div className={styles.Description}>
<div className={styles.UpperSection}>
<div className={styles.Setup}>
<div>
<div className={styles.Heading}>One time setup</div>
<div className={styles.Pricing}>
<span>INR 15000</span> ($220)
</div>
<div className={styles.Pricing}>+ taxes</div>
<ul className={styles.List}>
<li>5hr consulting</li>
<li>1 hr onboarding session</li>
</ul>
</div>
<div>
<div className={styles.Heading}>Monthly Recurring</div>
<div className={styles.Pricing}>
<span>INR 7,500</span> ($110)
</div>
<div className={styles.Pricing}>+ taxes</div>
<ul className={styles.List}>
<li>upto 250k messages</li>
<li>1-10 users</li>
</ul>
</div>
</div>
<div className={styles.Additional}>
<div className={styles.Heading}>Variable charges as usage increases</div>
<div>For every staff member over 10 users – INR 150 ($2)</div>
<div>For every 1K messages upto 1Mn messages – INR 10 ($0.14)</div>
<div>For every 1K messages over 1Mn messages – INR 5 ($0.07)</div>
</div>
</div>
<div className={styles.DottedSpaced} />
<div className={styles.BottomSection}>
<div className={styles.InactiveHeading}>
Suspended or inactive accounts:{' '}
<span className={styles.Amount}> INR 4,500/mo + taxes</span>
</div>
</div>
</div>
{couponApplied && (
<div className={styles.CouponDescription}>
<div className={styles.CouponHeading}>Coupon Applied!</div>
<div>{couponDescription.description}</div>
</div>
)}
{processIncomplete && couponError && (
<div className={styles.CouponError}>
<div>Invalid Coupon!</div>
</div>
)}
<div>
<Formik
enableReinitialize
validateOnBlur={false}
initialValues={{
name,
email,
coupon,
}}
validationSchema={validationSchema}
onSubmit={(itemData) => {
handleSubmit(itemData);
}}
>
{({ values, setFieldError, setFieldTouched }) => (
<Form>
{processIncomplete && (
<Field
component={Input}
name="coupon"
type="text"
placeholder="Coupon Code"
disabled={couponApplied}
endAdornment={
<InputAdornment position="end">
{couponLoading ? (
<CircularProgress />
) : (
<div
aria-hidden
className={styles.Apply}
onClick={() => {
if (values.coupon === '') {
setFieldError('coupon', 'Please input coupon code');
setFieldTouched('coupon');
} else {
getCouponCode({ variables: { code: values.coupon } });
}
}}
>
{couponApplied ? (
<CancelOutlinedIcon
className={styles.CrossIcon}
onClick={() => setCouponApplied(false)}
/>
) : (
' APPLY'
)}
</div>
)}
</InputAdornment>
}
/>
)}
{formFieldItems.map((field, index) => {
const key = index;
return <Field key={key} {...field} />;
})}
{paymentBody}
{processIncomplete && (
<Button
variant="contained"
data-testid="submitButton"
color="primary"
type="submit"
className={styles.Button}
disabled={!stripe || disable}
loading={loading}
>
Subscribe for monthly billing
</Button>
)}
</Form>
)}
</Formik>
</div>
</div>
);
}
Example #19
Source File: List.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
List: React.SFC<ListProps> = ({
columnNames = [],
countQuery,
listItem,
listIcon,
filterItemsQuery,
deleteItemQuery,
listItemName,
dialogMessage = '',
secondaryButton,
pageLink,
columns,
columnStyles,
title,
dialogTitle,
filterList,
listOrder = 'asc',
removeSortBy = null,
button = {
show: true,
label: 'Add New',
},
showCheckbox,
deleteModifier = { icon: 'normal', variables: null, label: 'Delete' },
editSupport = true,
searchParameter = ['label'],
filters = null,
displayListType = 'list',
cardLink = null,
additionalAction = [],
backLinkButton,
restrictedAction,
collapseOpen = false,
collapseRow = undefined,
defaultSortBy,
noItemText = null,
isDetailsPage = false,
customStyles,
}: ListProps) => {
const { t } = useTranslation();
// DialogBox states
const [deleteItemID, setDeleteItemID] = useState<number | null>(null);
const [deleteItemName, setDeleteItemName] = useState<string>('');
const [newItem, setNewItem] = useState(false);
const [searchVal, setSearchVal] = useState('');
// check if the user has access to manage collections
const userRolePermissions = getUserRolePermissions();
const capitalListItemName = listItemName
? listItemName[0].toUpperCase() + listItemName.slice(1)
: '';
let defaultColumnSort = columnNames[0];
// check if there is a default column set for sorting
if (defaultSortBy) {
defaultColumnSort = defaultSortBy;
}
// get the last sort column value from local storage if exist else set the default column
const getSortColumn = (listItemNameValue: string, columnName: string) => {
// set the column name
let columnnNameValue;
if (columnName) {
columnnNameValue = columnName;
}
// check if we have sorting stored in local storage
const sortValue = getLastListSessionValues(listItemNameValue, false);
// update column name from the local storage
if (sortValue) {
columnnNameValue = sortValue;
}
return setColumnToBackendTerms(listItemName, columnnNameValue);
};
// get the last sort direction value from local storage if exist else set the default order
const getSortDirection = (listItemNameValue: string) => {
let sortDirection: any = listOrder;
// check if we have sorting stored in local storage
const sortValue = getLastListSessionValues(listItemNameValue, true);
if (sortValue) {
sortDirection = sortValue;
}
return sortDirection;
};
// Table attributes
const [tableVals, setTableVals] = useState<TableVals>({
pageNum: 0,
pageRows: 50,
sortCol: getSortColumn(listItemName, defaultColumnSort),
sortDirection: getSortDirection(listItemName),
});
let userRole: any = getUserRole();
const handleTableChange = (attribute: string, newVal: any) => {
let updatedList;
let attributeValue = newVal;
if (attribute === 'sortCol') {
attributeValue = setColumnToBackendTerms(listItemName, newVal);
updatedList = getUpdatedList(listItemName, newVal, false);
} else {
updatedList = getUpdatedList(listItemName, newVal, true);
}
// set the sort criteria in local storage
setListSession(JSON.stringify(updatedList));
setTableVals({
...tableVals,
[attribute]: attributeValue,
});
};
let filter: any = {};
if (searchVal !== '') {
searchParameter.forEach((parameter: string) => {
filter[parameter] = searchVal;
});
}
filter = { ...filter, ...filters };
const filterPayload = useCallback(() => {
let order = 'ASC';
if (tableVals.sortDirection) {
order = tableVals.sortDirection.toUpperCase();
}
return {
filter,
opts: {
limit: tableVals.pageRows,
offset: tableVals.pageNum * tableVals.pageRows,
order,
orderWith: tableVals.sortCol,
},
};
}, [searchVal, tableVals, filters]);
// Get the total number of items here
const {
loading: l,
error: e,
data: countData,
refetch: refetchCount,
} = useQuery(countQuery, {
variables: { filter },
});
// Get item data here
const [fetchQuery, { loading, error, data, refetch: refetchValues }] = useLazyQuery(
filterItemsQuery,
{
variables: filterPayload(),
fetchPolicy: 'cache-and-network',
}
);
// Get item data here
const [fetchUserCollections, { loading: loadingCollections, data: userCollections }] =
useLazyQuery(GET_CURRENT_USER);
const checkUserRole = () => {
userRole = getUserRole();
};
useEffect(() => {
refetchCount();
}, [filterPayload, searchVal, filters]);
useEffect(() => {
if (userRole.length === 0) {
checkUserRole();
} else {
if (!userRolePermissions.manageCollections && listItem === 'collections') {
// if user role staff then display collections related to login user
fetchUserCollections();
}
fetchQuery();
}
}, [userRole]);
let deleteItem: any;
// Make a new count request for a new count of the # of rows from this query in the back-end.
if (deleteItemQuery) {
[deleteItem] = useMutation(deleteItemQuery, {
onCompleted: () => {
checkUserRole();
refetchCount();
if (refetchValues) {
refetchValues(filterPayload());
}
},
});
}
const showDialogHandler = (id: any, label: string) => {
setDeleteItemName(label);
setDeleteItemID(id);
};
const closeDialogBox = () => {
setDeleteItemID(null);
};
const deleteHandler = (id: number) => {
const variables = deleteModifier.variables ? deleteModifier.variables(id) : { id };
deleteItem({ variables });
setNotification(`${capitalListItemName} deleted successfully`);
};
const handleDeleteItem = () => {
if (deleteItemID !== null) {
deleteHandler(deleteItemID);
}
setDeleteItemID(null);
};
const useDelete = (message: string | any) => {
let component = {};
const props = { disableOk: false, handleOk: handleDeleteItem };
if (typeof message === 'string') {
component = message;
} else {
/**
* Custom component to render
* message should contain 3 params
* 1. component: Component to render
* 2. isConfirm: To check true or false value
* 3. handleOkCallback: Callback action to delete item
*/
const {
component: componentToRender,
isConfirmed,
handleOkCallback,
} = message(deleteItemID, deleteItemName);
component = componentToRender;
props.disableOk = !isConfirmed;
props.handleOk = () => handleOkCallback({ refetch: fetchQuery, setDeleteItemID });
}
return {
component,
props,
};
};
let dialogBox;
if (deleteItemID) {
const { component, props } = useDelete(dialogMessage);
dialogBox = (
<DialogBox
title={
dialogTitle || `Are you sure you want to delete the ${listItemName} "${deleteItemName}"?`
}
handleCancel={closeDialogBox}
colorOk="secondary"
alignButtons="center"
{...props}
>
<div className={styles.DialogText}>
<div>{component}</div>
</div>
</DialogBox>
);
}
if (newItem) {
return <Redirect to={`/${pageLink}/add`} />;
}
if (loading || l || loadingCollections) return <Loading />;
if (error || e) {
if (error) {
setErrorMessage(error);
} else if (e) {
setErrorMessage(e);
}
return null;
}
// Reformat all items to be entered in table
function getIcons(
// id: number | undefined,
item: any,
label: string,
isReserved: boolean | null,
listItems: any,
allowedAction: any | null
) {
// there might be a case when we might want to allow certain actions for reserved items
// currently we don't allow edit or delete for reserved items. hence return early
const { id } = item;
if (isReserved) {
return null;
}
let editButton = null;
if (editSupport) {
editButton = allowedAction.edit && (
<Link to={`/${pageLink}/${id}/edit`}>
<IconButton aria-label={t('Edit')} color="default" data-testid="EditIcon">
<Tooltip title={t('Edit')} placement="top">
<EditIcon />
</Tooltip>
</IconButton>
</Link>
);
}
const deleteButton = (Id: any, text: string) =>
allowedAction.delete ? (
<IconButton
aria-label={t('Delete')}
color="default"
data-testid="DeleteIcon"
onClick={() => showDialogHandler(Id, text)}
>
<Tooltip title={`${deleteModifier.label}`} placement="top">
{deleteModifier.icon === 'cross' ? <CrossIcon /> : <DeleteIcon />}
</Tooltip>
</IconButton>
) : null;
if (id) {
return (
<div className={styles.Icons}>
{additionalAction.map((action: any, index: number) => {
if (allowedAction.restricted) {
return null;
}
// check if we are dealing with nested element
let additionalActionParameter: any;
const params: any = additionalAction[index].parameter.split('.');
if (params.length > 1) {
additionalActionParameter = listItems[params[0]][params[1]];
} else {
additionalActionParameter = listItems[params[0]];
}
const key = index;
if (action.link) {
return (
<Link to={`${action.link}/${additionalActionParameter}`} key={key}>
<IconButton
color="default"
className={styles.additonalButton}
data-testid="additionalButton"
>
<Tooltip title={`${action.label}`} placement="top">
{action.icon}
</Tooltip>
</IconButton>
</Link>
);
}
if (action.dialog) {
return (
<IconButton
color="default"
data-testid="additionalButton"
className={styles.additonalButton}
id="additionalButton-icon"
onClick={() => action.dialog(additionalActionParameter, item)}
key={key}
>
<Tooltip title={`${action.label}`} placement="top" key={key}>
{action.icon}
</Tooltip>
</IconButton>
);
}
if (action.button) {
return action.button(listItems, action, key, fetchQuery);
}
return null;
})}
{/* do not display edit & delete for staff role in collection */}
{userRolePermissions.manageCollections || listItems !== 'collections' ? (
<>
{editButton}
{deleteButton(id, label)}
</>
) : null}
</div>
);
}
return null;
}
function formatList(listItems: Array<any>) {
return listItems.map(({ ...listItemObj }) => {
const label = listItemObj.label ? listItemObj.label : listItemObj.name;
const isReserved = listItemObj.isReserved ? listItemObj.isReserved : null;
// display only actions allowed to the user
const allowedAction = restrictedAction
? restrictedAction(listItemObj)
: { chat: true, edit: true, delete: true };
return {
...columns(listItemObj),
operations: getIcons(listItemObj, label, isReserved, listItemObj, allowedAction),
recordId: listItemObj.id,
};
});
}
const resetTableVals = () => {
setTableVals({
pageNum: 0,
pageRows: 50,
sortCol: getSortColumn(listItemName, defaultColumnSort),
sortDirection: getSortDirection(listItemName),
});
};
const handleSearch = (searchError: any) => {
searchError.preventDefault();
const searchValInput = searchError.target.querySelector('input').value.trim();
setSearchVal(searchValInput);
resetTableVals();
};
// Get item data and total number of items.
let itemList: any = [];
if (data) {
itemList = formatList(data[listItem]);
}
if (userCollections) {
if (listItem === 'collections') {
itemList = formatList(userCollections.currentUser.user.groups);
}
}
let itemCount: number = tableVals.pageRows;
if (countData) {
itemCount = countData[`count${listItem[0].toUpperCase()}${listItem.slice(1)}`];
}
let displayList;
if (displayListType === 'list') {
displayList = (
<Pager
columnStyles={columnStyles}
removeSortBy={removeSortBy !== null ? removeSortBy : []}
columnNames={columnNames}
data={itemList}
listItemName={listItemName}
totalRows={itemCount}
handleTableChange={handleTableChange}
tableVals={tableVals}
showCheckbox={showCheckbox}
collapseOpen={collapseOpen}
collapseRow={collapseRow}
/>
);
} else if (displayListType === 'card') {
/* istanbul ignore next */
displayList = (
<>
<ListCard data={itemList} link={cardLink} />
<table>
<TableFooter className={styles.TableFooter} data-testid="tableFooter">
<TableRow>
<TablePagination
className={styles.FooterRow}
colSpan={columnNames.length}
count={itemCount}
onPageChange={(event, newPage) => {
handleTableChange('pageNum', newPage);
}}
onRowsPerPageChange={(event) => {
handleTableChange('pageRows', parseInt(event.target.value, 10));
}}
page={tableVals.pageNum}
rowsPerPage={tableVals.pageRows}
rowsPerPageOptions={[50, 75, 100, 150, 200]}
/>
</TableRow>
</TableFooter>
</table>
</>
);
}
const backLink = backLinkButton ? (
<div className={styles.BackLink}>
<Link to={backLinkButton.link}>
<BackIcon />
{backLinkButton.text}
</Link>
</div>
) : null;
let buttonDisplay;
if (button.show) {
let buttonContent;
if (button.action) {
buttonContent = (
<Button
color="primary"
variant="contained"
onClick={() => button.action && button.action()}
>
{button.label}
</Button>
);
} else if (!button.link) {
buttonContent = (
<Button
color="primary"
variant="contained"
onClick={() => setNewItem(true)}
data-testid="newItemButton"
>
{button.label}
</Button>
);
} else {
buttonContent = (
<Link to={button.link}>
<Button color="primary" variant="contained" data-testid="newItemLink">
{button.label}
</Button>
</Link>
);
}
buttonDisplay = <div className={styles.AddButton}>{buttonContent}</div>;
}
const noItemsText = (
<div className={styles.NoResults}>
{searchVal ? (
<div>{t('Sorry, no results found! Please try a different search.')}</div>
) : (
<div>
There are no {noItemText || listItemName}s right now.{' '}
{button.show && t('Please create one.')}
</div>
)}
</div>
);
let headerSize = styles.Header;
if (isDetailsPage) {
headerSize = styles.DetailsPageHeader;
}
return (
<>
<div className={headerSize} data-testid="listHeader">
<Typography variant="h5" className={styles.Title}>
<IconButton disabled className={styles.Icon}>
{listIcon}
</IconButton>
{title}
</Typography>
{filterList}
<div className={styles.Buttons}>
<SearchBar
handleSubmit={handleSearch}
onReset={() => {
setSearchVal('');
resetTableVals();
}}
searchVal={searchVal}
handleChange={(err: any) => {
// reset value only if empty
if (!err.target.value) setSearchVal('');
}}
searchMode
/>
</div>
<div>
{dialogBox}
<div className={styles.ButtonGroup}>
{buttonDisplay}
{secondaryButton}
</div>
</div>
</div>
<div className={`${styles.Body} ${customStyles}`}>
{backLink}
{/* Rendering list of items */}
{itemList.length > 0 ? displayList : noItemsText}
</div>
</>
);
}
Example #20
Source File: InteractiveMessage.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
InteractiveMessage: React.SFC<FlowProps> = ({ match }) => {
const location: any = useLocation();
const history = useHistory();
const [title, setTitle] = useState('');
const [body, setBody] = useState(EditorState.createEmpty());
const [templateType, setTemplateType] = useState<string>(QUICK_REPLY);
const [templateButtons, setTemplateButtons] = useState<Array<any>>([{ value: '' }]);
const [globalButton, setGlobalButton] = useState('');
const [isUrlValid, setIsUrlValid] = useState<any>();
const [type, setType] = useState<any>(null);
const [attachmentURL, setAttachmentURL] = useState<any>();
const [contactVariables, setContactVariables] = useState([]);
const [defaultLanguage, setDefaultLanguage] = useState<any>({});
const [sendWithTitle, setSendWithTitle] = useState<boolean>(true);
const [language, setLanguage] = useState<any>({});
const [languageOptions, setLanguageOptions] = useState<any>([]);
const [translations, setTranslations] = useState<any>('{}');
const [previousState, setPreviousState] = useState<any>({});
const [nextLanguage, setNextLanguage] = useState<any>('');
const [warning, setWarning] = useState<any>();
const { t } = useTranslation();
// alter header & update/copy queries
let header;
const stateType = location.state;
if (stateType === 'copy') {
queries.updateItemQuery = COPY_INTERACTIVE;
header = t('Copy Interactive Message');
} else {
queries.updateItemQuery = UPDATE_INTERACTIVE;
}
const { data: languages } = useQuery(USER_LANGUAGES);
const [getInteractiveTemplateById, { data: template, loading: loadingTemplate }] =
useLazyQuery<any>(GET_INTERACTIVE_MESSAGE);
useEffect(() => {
getVariableOptions(setContactVariables);
}, []);
useEffect(() => {
if (languages) {
const lang = languages.currentUser.user.organization.activeLanguages.slice();
// sort languages by their name
lang.sort((first: any, second: any) => (first.id > second.id ? 1 : -1));
setLanguageOptions(lang);
if (!Object.prototype.hasOwnProperty.call(match.params, 'id')) {
setLanguage(lang[0]);
}
}
}, [languages]);
useEffect(() => {
if (Object.prototype.hasOwnProperty.call(match.params, 'id') && match.params.id) {
getInteractiveTemplateById({ variables: { id: match.params.id } });
}
}, [match.params]);
const states = {
language,
title,
body,
globalButton,
templateButtons,
sendWithTitle,
templateType,
type,
attachmentURL,
};
const updateStates = ({
language: languageVal,
type: typeValue,
interactiveContent: interactiveContentValue,
}: any) => {
const content = JSON.parse(interactiveContentValue);
const data = convertJSONtoStateData(content, typeValue, title);
if (languageOptions.length > 0 && languageVal) {
const selectedLangauge = languageOptions.find((lang: any) => lang.id === languageVal.id);
setLanguage(selectedLangauge);
}
setTitle(data.title);
setBody(getEditorFromContent(data.body));
setTemplateType(typeValue);
setTimeout(() => setTemplateButtons(data.templateButtons), 100);
if (typeValue === LIST) {
setGlobalButton(data.globalButton);
}
if (typeValue === QUICK_REPLY && data.type && data.attachmentURL) {
setType({ id: data.type, label: data.type });
setAttachmentURL(data.attachmentURL);
}
};
const setStates = ({
label: labelValue,
language: languageVal,
type: typeValue,
interactiveContent: interactiveContentValue,
translations: translationsVal,
sendWithTitle: sendInteractiveTitleValue,
}: any) => {
let content;
if (translationsVal) {
const translationsCopy = JSON.parse(translationsVal);
// restore if translations present for selected language
if (
Object.keys(translationsCopy).length > 0 &&
translationsCopy[language.id || languageVal.id] &&
!location.state?.language
) {
content =
JSON.parse(translationsVal)[language.id || languageVal.id] ||
JSON.parse(interactiveContentValue);
} else if (template) {
content = getDefaultValuesByTemplate(template.interactiveTemplate.interactiveTemplate);
}
}
const data = convertJSONtoStateData(content, typeValue, labelValue);
setDefaultLanguage(languageVal);
if (languageOptions.length > 0 && languageVal) {
if (location.state?.language) {
const selectedLangauge = languageOptions.find(
(lang: any) => lang.label === location.state.language
);
history.replace(location.pathname, null);
setLanguage(selectedLangauge);
} else if (!language.id) {
const selectedLangauge = languageOptions.find((lang: any) => lang.id === languageVal.id);
setLanguage(selectedLangauge);
} else {
setLanguage(language);
}
}
let titleText = data.title;
if (location.state === 'copy') {
titleText = `Copy of ${data.title}`;
}
setTitle(titleText);
setBody(getEditorFromContent(data.body));
setTemplateType(typeValue);
setTimeout(() => setTemplateButtons(data.templateButtons), 100);
if (typeValue === LIST) {
setGlobalButton(data.globalButton);
}
if (typeValue === QUICK_REPLY && data.type && data.attachmentURL) {
setType({ id: data.type, label: data.type });
setAttachmentURL(data.attachmentURL);
}
if (translationsVal) {
setTranslations(translationsVal);
}
setSendWithTitle(sendInteractiveTitleValue);
};
const validateURL = (value: string) => {
if (value && type) {
validateMedia(value, type.id).then((response: any) => {
if (!response.data.is_valid) {
setIsUrlValid(response.data.message);
} else {
setIsUrlValid('');
}
});
}
};
useEffect(() => {
if ((type === '' || type) && attachmentURL) {
validateURL(attachmentURL);
}
}, [type, attachmentURL]);
const handleAddInteractiveTemplate = (
addFromTemplate: boolean,
templateTypeVal: string,
stateToRestore: any = null
) => {
let buttons: any = [];
const buttonType: any = {
QUICK_REPLY: { value: '' },
LIST: { title: '', options: [{ title: '', description: '' }] },
};
const templateResult = stateToRestore || [buttonType[templateTypeVal]];
buttons = addFromTemplate ? [...templateButtons, buttonType[templateTypeVal]] : templateResult;
setTemplateButtons(buttons);
};
const handleRemoveInteractiveTemplate = (index: number) => {
const buttons = [...templateButtons];
const result = buttons.filter((row, idx: number) => idx !== index);
setTemplateButtons(result);
};
const handleAddListItem = (rowNo: number, oldOptions: Array<any>) => {
const buttons = [...templateButtons];
const newOptions = [...oldOptions, { title: '', description: '' }];
const result = buttons.map((row: any, idx: number) =>
rowNo === idx ? { ...row, options: newOptions } : row
);
setTemplateButtons(result);
};
const handleRemoveListItem = (rowIdx: number, idx: number) => {
const buttons = [...templateButtons];
const result = buttons.map((row: any, index: number) => {
if (index === rowIdx) {
const newOptions = row.options.filter((r: any, itemIdx: number) => itemIdx !== idx);
return { ...row, options: newOptions };
}
return row;
});
setTemplateButtons(result);
};
const handleInputChange = (
interactiveMessageType: string,
index: number,
value: string,
payload: any,
setFieldValue: any
) => {
const { key, itemIndex, isOption } = payload;
const buttons = [...templateButtons];
let result = [];
if (interactiveMessageType === QUICK_REPLY) {
result = buttons.map((row: any, idx: number) => {
if (idx === index) {
const newRow = { ...row };
newRow[key] = value;
return newRow;
}
return row;
});
}
if (interactiveMessageType === LIST) {
result = buttons.map((row: any, idx: number) => {
const { options }: { options: Array<any> } = row;
if (idx === index) {
// for options
if (isOption) {
const updatedOptions = options.map((option: any, optionIdx: number) => {
if (optionIdx === itemIndex) {
const newOption = { ...option };
newOption[key] = value;
return newOption;
}
return option;
});
const updatedRowWithOptions = { ...row, options: updatedOptions };
return updatedRowWithOptions;
}
// for title
const newRow = { ...row };
newRow[key] = value;
return newRow;
}
return row;
});
}
setFieldValue('templateButtons', result);
setTemplateButtons(result);
};
const updateTranslation = (value: any) => {
const Id = value.id;
// restore if selected language is same as template
if (translations) {
const translationsCopy = JSON.parse(translations);
// restore if translations present for selected language
if (Object.keys(translationsCopy).length > 0 && translationsCopy[Id]) {
updateStates({
language: value,
type: template.interactiveTemplate.interactiveTemplate.type,
interactiveContent: JSON.stringify(translationsCopy[Id]),
});
} else if (template) {
const fillDataWithEmptyValues = getDefaultValuesByTemplate(
template.interactiveTemplate.interactiveTemplate
);
updateStates({
language: value,
type: template.interactiveTemplate.interactiveTemplate.type,
interactiveContent: JSON.stringify(fillDataWithEmptyValues),
});
}
return;
}
updateStates({
language: value,
type: template.interactiveTemplate.interactiveTemplate.type,
interactiveContent: template.interactiveTemplate.interactiveTemplate.interactiveContent,
});
};
const handleLanguageChange = (value: any) => {
const selected = languageOptions.find(({ label }: any) => label === value);
if (selected && Object.prototype.hasOwnProperty.call(match.params, 'id')) {
updateTranslation(selected);
} else if (selected) {
setLanguage(selected);
}
};
const afterSave = (data: any, saveClick: boolean) => {
if (!saveClick) {
if (match.params.id) {
handleLanguageChange(nextLanguage);
} else {
const { interactiveTemplate } = data.createInteractiveTemplate;
history.push(`/interactive-message/${interactiveTemplate.id}/edit`, {
language: nextLanguage,
});
}
}
};
const displayWarning = () => {
if (type && type.id === 'DOCUMENT') {
setWarning(
<div className={styles.Warning}>
<ol>
<li>{t('Body is not supported for document.')}</li>
</ol>
</div>
);
} else {
setWarning(null);
}
};
useEffect(() => {
handleAddInteractiveTemplate(false, QUICK_REPLY);
}, []);
useEffect(() => {
displayWarning();
}, [type]);
const dialogMessage = t("You won't be able to use this again.");
const options = MEDIA_MESSAGE_TYPES.filter(
(msgType: string) => !['AUDIO', 'STICKER'].includes(msgType)
).map((option: string) => ({ id: option, label: option }));
let timer: any = null;
const langOptions = languageOptions && languageOptions.map(({ label }: any) => label);
const onLanguageChange = (option: string, form: any) => {
setNextLanguage(option);
const { values, errors } = form;
if (values.type?.label === 'TEXT') {
if (values.title || values.body.getCurrentContent().getPlainText()) {
if (errors) {
setNotification(t('Please check the errors'), 'warning');
}
} else {
handleLanguageChange(option);
}
}
if (values.body.getCurrentContent().getPlainText()) {
if (Object.keys(errors).length !== 0) {
setNotification(t('Please check the errors'), 'warning');
}
} else {
handleLanguageChange(option);
}
};
const hasTranslations = match.params?.id && defaultLanguage?.id !== language?.id;
const fields = [
{
field: 'languageBar',
component: LanguageBar,
options: langOptions || [],
selectedLangauge: language && language.label,
onLanguageChange,
},
{
translation:
hasTranslations && getTranslation(templateType, 'title', translations, defaultLanguage),
component: Input,
name: 'title',
type: 'text',
placeholder: t('Title*'),
inputProp: {
onBlur: (event: any) => setTitle(event.target.value),
},
helperText: t('Only alphanumeric characters and spaces are allowed'),
},
// checkbox is not needed in media types
{
skip: type && type.label,
component: Checkbox,
name: 'sendWithTitle',
title: t('Show title in message'),
handleChange: (value: boolean) => setSendWithTitle(value),
addLabelStyle: false,
},
{
translation:
hasTranslations && getTranslation(templateType, 'body', translations, defaultLanguage),
component: EmojiInput,
name: 'body',
placeholder: t('Message*'),
rows: 5,
convertToWhatsApp: true,
textArea: true,
helperText: t('You can also use variables in message enter @ to see the available list'),
getEditorValue: (value: any) => {
setBody(value);
},
inputProp: {
suggestions: contactVariables,
},
},
{
translation:
hasTranslations && getTranslation(templateType, 'options', translations, defaultLanguage),
component: InteractiveOptions,
isAddButtonChecked: true,
templateType,
inputFields: templateButtons,
disabled: false,
disabledType: match.params.id !== undefined,
onAddClick: handleAddInteractiveTemplate,
onRemoveClick: handleRemoveInteractiveTemplate,
onInputChange: handleInputChange,
onListItemAddClick: handleAddListItem,
onListItemRemoveClick: handleRemoveListItem,
onTemplateTypeChange: (value: string) => {
const stateToRestore = previousState[value];
setTemplateType(value);
setPreviousState({ [templateType]: templateButtons });
handleAddInteractiveTemplate(false, value, stateToRestore);
},
onGlobalButtonInputChange: (value: string) => setGlobalButton(value),
},
];
const getTemplateButtonPayload = (typeVal: string, buttons: Array<any>) => {
if (typeVal === QUICK_REPLY) {
return buttons.map((button: any) => ({ type: 'text', title: button.value }));
}
return buttons.map((button: any) => {
const { title: sectionTitle, options: sectionOptions } = button;
const sectionOptionsObject = sectionOptions?.map((option: any) => ({
type: 'text',
title: option.title,
description: option.description,
}));
return {
title: sectionTitle,
subtitle: sectionTitle,
options: sectionOptionsObject,
};
});
};
const convertStateDataToJSON = (
payload: any,
titleVal: string,
templateTypeVal: string,
templateButtonVal: Array<any>,
globalButtonVal: any
) => {
const updatedPayload: any = { type: null, interactiveContent: null };
const { language: selectedLanguage } = payload;
Object.assign(updatedPayload);
if (selectedLanguage) {
Object.assign(updatedPayload, { languageId: selectedLanguage.id });
}
if (templateTypeVal === QUICK_REPLY) {
const content = getPayloadByMediaType(type?.id, payload);
const quickReplyOptions = getTemplateButtonPayload(templateTypeVal, templateButtonVal);
const quickReplyJSON = { type: 'quick_reply', content, options: quickReplyOptions };
Object.assign(updatedPayload, {
type: QUICK_REPLY,
interactiveContent: JSON.stringify(quickReplyJSON),
});
}
if (templateTypeVal === LIST) {
const bodyText = getPlainTextFromEditor(payload.body);
const items = getTemplateButtonPayload(templateTypeVal, templateButtonVal);
const globalButtons = [{ type: 'text', title: globalButtonVal }];
const listJSON = { type: 'list', title: titleVal, body: bodyText, globalButtons, items };
Object.assign(updatedPayload, {
type: LIST,
interactiveContent: JSON.stringify(listJSON),
});
}
return updatedPayload;
};
const isTranslationsPresentForEnglish = (...langIds: any) => {
const english = languageOptions.find(({ label }: { label: string }) => label === 'English');
return !!english && langIds.includes(english.id);
};
const setPayload = (payload: any) => {
const {
templateType: templateTypeVal,
templateButtons: templateButtonVal,
title: titleVal,
globalButton: globalButtonVal,
language: selectedLanguage,
} = payload;
const payloadData: any = convertStateDataToJSON(
payload,
titleVal,
templateTypeVal,
templateButtonVal,
globalButtonVal
);
let translationsCopy: any = {};
if (translations) {
translationsCopy = JSON.parse(translations);
translationsCopy[language.id] = JSON.parse(payloadData.interactiveContent);
}
const langIds = Object.keys(translationsCopy);
const isPresent = isTranslationsPresentForEnglish(...langIds);
// Update label anyway if selected language is English
if (selectedLanguage.label === 'English') {
payloadData.label = titleVal;
}
// Update label if there is no translation present for english
if (!isPresent) {
payloadData.label = titleVal;
}
// While editing preserve original langId and content
if (template) {
/**
* Restoring template if language is different
*/
const dataToRestore = template.interactiveTemplate.interactiveTemplate;
if (selectedLanguage.id !== dataToRestore?.language.id) {
payloadData.languageId = dataToRestore?.language.id;
payloadData.interactiveContent = dataToRestore?.interactiveContent;
}
}
payloadData.sendWithTitle = sendWithTitle;
payloadData.translations = JSON.stringify(translationsCopy);
return payloadData;
};
const attachmentInputs = [
{
component: AutoComplete,
name: 'type',
options,
optionLabel: 'label',
multiple: false,
helperText: warning,
textFieldProps: {
variant: 'outlined',
label: t('Attachment type'),
},
onChange: (event: any) => {
const val = event || '';
if (!event) {
setIsUrlValid(val);
}
setType(val);
},
},
{
component: Input,
name: 'attachmentURL',
type: 'text',
placeholder: t('Attachment URL'),
validate: () => isUrlValid,
inputProp: {
onBlur: (event: any) => {
setAttachmentURL(event.target.value);
},
onChange: (event: any) => {
clearTimeout(timer);
timer = setTimeout(() => setAttachmentURL(event.target.value), 1000);
},
},
},
];
const formFields = templateType === LIST ? [...fields] : [...fields, ...attachmentInputs];
const validation = validator(templateType, t);
const validationScheme = Yup.object().shape(validation, [['type', 'attachmentURL']]);
const getPreviewData = () => {
if (!(templateButtons && templateButtons.length)) {
return null;
}
const isButtonPresent = templateButtons.some((button: any) => {
const { value, options: opts } = button;
return !!value || !!(opts && opts.some((o: any) => !!o.title));
});
if (!isButtonPresent) {
return null;
}
const payload = {
title,
body,
attachmentURL,
language,
};
const { interactiveContent } = convertStateDataToJSON(
payload,
title,
templateType,
templateButtons,
globalButton
);
const data = { templateType, interactiveContent };
return data;
};
const previewData = useMemo(getPreviewData, [
title,
body,
templateType,
templateButtons,
globalButton,
type,
attachmentURL,
]);
if (languageOptions.length < 1 || loadingTemplate) {
return <Loading />;
}
return (
<>
<FormLayout
{...queries}
match={match}
states={states}
setStates={setStates}
setPayload={setPayload}
title={header}
type={stateType}
validationSchema={validationScheme}
listItem="interactiveTemplate"
listItemName="interactive msg"
dialogMessage={dialogMessage}
formFields={formFields}
redirectionLink="interactive-message"
cancelLink="interactive-message"
icon={interactiveMessageIcon}
languageSupport={false}
getQueryFetchPolicy="cache-and-network"
afterSave={afterSave}
saveOnPageChange={false}
/>
<div className={styles.Simulator}>
<Simulator
setSimulatorId={0}
showSimulator
isPreviewMessage
message={{}}
showHeader={sendWithTitle}
interactiveMessage={previewData}
simulatorIcon={false}
/>
</div>
</>
);
}
Example #21
Source File: FlowList.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
FlowList: React.SFC<FlowListProps> = () => {
const history = useHistory();
const { t } = useTranslation();
const inputRef = useRef<any>(null);
const [flowName, setFlowName] = useState('');
const [importing, setImporting] = useState(false);
const [releaseFlow] = useLazyQuery(RELEASE_FLOW);
useEffect(() => {
releaseFlow();
}, []);
const [importFlow] = useMutation(IMPORT_FLOW, {
onCompleted: (result: any) => {
const { success } = result.importFlow;
if (!success) {
setNotification(
t(
'Sorry! An error occurred! This could happen if the flow is already present or error in the import file.'
),
'error'
);
} else {
setNotification(t('The flow has been imported successfully.'));
}
setImporting(false);
},
});
const [exportFlowMutation] = useLazyQuery(EXPORT_FLOW, {
fetchPolicy: 'network-only',
onCompleted: async ({ exportFlow }) => {
const { exportData } = exportFlow;
await exportFlowMethod(exportData, flowName);
},
});
const setDialog = (id: any) => {
history.push({ pathname: `/flow/${id}/edit`, state: 'copy' });
};
const exportFlow = (id: any, item: any) => {
setFlowName(item.name);
exportFlowMutation({ variables: { id } });
};
const changeHandler = (event: any) => {
const fileReader: any = new FileReader();
fileReader.onload = function setImport() {
importFlow({ variables: { flow: fileReader.result } });
};
setImporting(true);
fileReader.readAsText(event.target.files[0]);
};
const importButton = (
<span>
<input
type="file"
ref={inputRef}
hidden
name="file"
onChange={changeHandler}
data-testid="import"
/>
<Button
onClick={() => {
if (inputRef.current) inputRef.current.click();
}}
variant="contained"
color="primary"
>
{t('Import flow')}
<ImportIcon />
</Button>
</span>
);
const additionalAction = [
{
label: t('Configure'),
icon: configureIcon,
parameter: 'uuid',
link: '/flow/configure',
},
{
label: t('Make a copy'),
icon: <DuplicateIcon />,
parameter: 'id',
dialog: setDialog,
},
{
label: t('Export flow'),
icon: <ExportIcon />,
parameter: 'id',
dialog: exportFlow,
},
];
const getColumns = ({ name, keywords, lastChangedAt, lastPublishedAt }: any) => ({
name: getName(name, keywords),
lastPublishedAt: getLastPublished(lastPublishedAt, t('Not published yet')),
lastChangedAt: getDate(lastChangedAt, t('Nothing in draft')),
});
const columnNames = ['TITLE', 'LAST PUBLISHED', 'LAST SAVED IN DRAFT', 'ACTIONS'];
const dialogMessage = t("You won't be able to use this flow.");
const columnAttributes = {
columnNames,
columns: getColumns,
columnStyles,
};
if (importing) {
return <Loading message="Uploading" />;
}
return (
<>
<List
title={t('Flows')}
listItem="flows"
listItemName="flow"
pageLink="flow"
listIcon={flowIcon}
dialogMessage={dialogMessage}
{...queries}
{...columnAttributes}
searchParameter={['nameOrKeyword']}
removeSortBy={['LAST PUBLISHED', 'LAST SAVED IN DRAFT']}
additionalAction={additionalAction}
button={{ show: true, label: t('+ Create Flow') }}
secondaryButton={importButton}
/>
<Link to="/webhook-logs" className={styles.Webhook}>
<WebhookLogsIcon />
{t('View webhook logs')}
</Link>
<Link to="/contact-fields" className={styles.ContactFields}>
<ContactVariable />
{t('View contact variables')}
</Link>
</>
);
}
Example #22
Source File: ExportConsulting.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ExportConsulting: React.FC<ExportConsultingPropTypes> = ({
setFilters,
}: ExportConsultingPropTypes) => {
const { data: organizationList } = useQuery(FILTER_ORGANIZATIONS, {
variables: setVariables(),
});
const { t } = useTranslation();
const [getConsultingDetails] = useLazyQuery(EXPORT_CONSULTING_HOURS, {
fetchPolicy: 'network-only',
onCompleted: ({ fetchConsultingHours }) => {
downloadFile(
`data:attachment/csv,${encodeURIComponent(fetchConsultingHours)}`,
'consulting-hours.csv'
);
},
});
const formFields = [
{
component: AutoComplete,
name: 'organization',
placeholder: t('Select Organization'),
options: organizationList ? organizationList.organizations : [],
optionLabel: 'name',
multiple: false,
textFieldProps: {
label: t('Select Organization'),
variant: 'outlined',
},
},
{
component: Calendar,
name: 'dateFrom',
type: 'date',
placeholder: t('Date from'),
label: t('Date range'),
},
{
component: Calendar,
name: 'dateTo',
type: 'date',
placeholder: t('Date to'),
},
];
const validationSchema = Yup.object().shape({
organization: Yup.object().test(
'organization',
'Organization is required',
(val) => val.name !== undefined
),
dateTo: Yup.string().when('dateFrom', (startDateValue: any, schema: any) =>
schema.test({
test: (endDateValue: any) =>
!(startDateValue !== undefined && !moment(endDateValue).isAfter(startDateValue)),
message: t('End date should be greater than the start date'),
})
),
});
return (
<div className={styles.FilterContainer}>
<Formik
initialValues={{ organization: { name: '', id: '' }, dateFrom: '', dateTo: '' }}
onSubmit={(values) => {
const organizationFilter: any = { organizationName: values.organization.name };
if (values.dateFrom && values.dateTo) {
organizationFilter.startDate = formatDate(values.dateFrom);
organizationFilter.endDate = formatDate(values.dateTo);
}
setFilters(organizationFilter);
}}
validationSchema={validationSchema}
>
{({ values, submitForm }) => (
<div className={styles.FormContainer}>
<Form className={styles.Form}>
{formFields.map((field) => (
<Field className={styles.Field} {...field} key={field.name} />
))}
<div className={styles.Buttons}>
<Button
variant="outlined"
color="primary"
onClick={() => {
submitForm();
}}
>
Filter
</Button>
<ExportIcon
className={styles.ExportIcon}
onClick={() => {
getConsultingDetails({
variables: {
filter: {
clientId: values.organization.id,
startDate: formatDate(values.dateFrom),
endDate: formatDate(values.dateTo),
},
},
});
}}
/>
</div>
</Form>
</div>
)}
</Formik>
</div>
);
}
Example #23
Source File: CollectionList.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
CollectionList: React.SFC<CollectionListProps> = () => {
const [addContactsDialogShow, setAddContactsDialogShow] = useState(false);
const [contactSearchTerm, setContactSearchTerm] = useState('');
const [collectionId, setCollectionId] = useState();
const { t } = useTranslation();
const [getContacts, { data: contactsData }] = useLazyQuery(CONTACT_SEARCH_QUERY, {
variables: setVariables({ name: contactSearchTerm }, 50),
});
const [getCollectionContacts, { data: collectionContactsData }] =
useLazyQuery(GET_COLLECTION_CONTACTS);
const [updateCollectionContacts] = useMutation(UPDATE_COLLECTION_CONTACTS, {
onCompleted: (data) => {
const { numberDeleted, groupContacts } = data.updateGroupContacts;
const numberAdded = groupContacts.length;
if (numberDeleted > 0 && numberAdded > 0) {
setNotification(
`${numberDeleted} contact${
numberDeleted === 1 ? '' : 's were'
} removed and ${numberAdded} contact${numberAdded === 1 ? '' : 's were'} added`
);
} else if (numberDeleted > 0) {
setNotification(`${numberDeleted} contact${numberDeleted === 1 ? '' : 's were'} removed`);
} else {
setNotification(`${numberAdded} contact${numberAdded === 1 ? '' : 's were'} added`);
}
setAddContactsDialogShow(false);
},
refetchQueries: [{ query: GET_COLLECTION_CONTACTS, variables: { id: collectionId } }],
});
const dialogMessage = t("You won't be able to use this collection again.");
let contactOptions: any = [];
let collectionContacts: Array<any> = [];
if (contactsData) {
contactOptions = contactsData.contacts;
}
if (collectionContactsData) {
collectionContacts = collectionContactsData.group.group.contacts;
}
let dialog = null;
const setContactsDialog = (id: any) => {
getCollectionContacts({ variables: { id } });
getContacts();
setCollectionId(id);
setAddContactsDialogShow(true);
};
const handleCollectionAdd = (value: any) => {
const selectedContacts = value.filter(
(contact: any) =>
!collectionContacts.map((collectionContact: any) => collectionContact.id).includes(contact)
);
const unselectedContacts = collectionContacts
.map((collectionContact: any) => collectionContact.id)
.filter((contact: any) => !value.includes(contact));
if (selectedContacts.length === 0 && unselectedContacts.length === 0) {
setAddContactsDialogShow(false);
} else {
updateCollectionContacts({
variables: {
input: {
addContactIds: selectedContacts,
groupId: collectionId,
deleteContactIds: unselectedContacts,
},
},
});
}
};
if (addContactsDialogShow) {
dialog = (
<SearchDialogBox
title={t('Add contacts to the collection')}
handleOk={handleCollectionAdd}
handleCancel={() => setAddContactsDialogShow(false)}
options={contactOptions}
optionLabel="name"
additionalOptionLabel="phone"
asyncSearch
disableClearable
selectedOptions={collectionContacts}
renderTags={false}
searchLabel="Search contacts"
textFieldPlaceholder="Type here"
onChange={(value: any) => {
if (typeof value === 'string') {
setContactSearchTerm(value);
}
}}
/>
);
}
const addContactIcon = <AddContactIcon />;
const additionalAction = [
{
label: t('Add contacts to collection'),
icon: addContactIcon,
parameter: 'id',
dialog: setContactsDialog,
},
];
const getRestrictedAction = () => {
const action: any = { edit: true, delete: true };
if (getUserRole().includes('Staff')) {
action.edit = false;
action.delete = false;
}
return action;
};
const cardLink = { start: 'collection', end: 'contacts' };
// check if the user has access to manage collections
const userRolePermissions = getUserRolePermissions();
return (
<>
<List
restrictedAction={getRestrictedAction}
title={t('Collections')}
listItem="groups"
columnNames={['TITLE']}
listItemName="collection"
displayListType="card"
button={{ show: userRolePermissions.manageCollections, label: t('+ Create Collection') }}
pageLink="collection"
listIcon={collectionIcon}
dialogMessage={dialogMessage}
additionalAction={additionalAction}
cardLink={cardLink}
{...queries}
{...columnAttributes}
/>
{dialog}
</>
);
}
Example #24
Source File: CollectionInformation.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
CollectionInformation: React.SFC<CollectionInformationProps> = ({
collectionId,
staff = true,
displayPopup,
setDisplayPopup,
handleSendMessage,
}) => {
const { t } = useTranslation();
const displayObj: any = { 'Session messages': 0, 'Only templates': 0, 'No messages': 0 };
const [display, setDisplay] = useState(displayObj);
const [getCollectionInfo, { data: collectionInfo }] = useLazyQuery(GET_COLLECTION_INFO);
const [selectedUsers, { data: collectionUsers }] = useLazyQuery(GET_COLLECTION_USERS, {
fetchPolicy: 'cache-and-network',
});
useEffect(() => {
if (collectionId) {
getCollectionInfo({ variables: { id: collectionId } });
selectedUsers({ variables: { id: collectionId } });
// reset to zero on collection change
setDisplay({ 'Session messages': 0, 'Only templates': 0, 'No messages': 0 });
}
}, [collectionId]);
useEffect(() => {
if (collectionInfo) {
const info = JSON.parse(collectionInfo.groupInfo);
const displayCopy = { ...displayObj };
Object.keys(info).forEach((key) => {
if (key === 'session_and_hsm') {
displayCopy['Session messages'] += info[key];
displayCopy['Only templates'] += info[key];
} else if (key === 'session') {
displayCopy['Session messages'] += info[key];
} else if (key === 'hsm') {
displayCopy['Only templates'] += info[key];
} else if (key === 'none') {
displayCopy['No messages'] = info[key];
}
});
setDisplay(displayCopy);
}
}, [collectionInfo]);
let assignedToCollection: any = [];
if (collectionUsers) {
assignedToCollection = collectionUsers.group.group.users.map((user: any) => user.name);
assignedToCollection = Array.from(new Set([].concat(...assignedToCollection)));
if (assignedToCollection.length > 2) {
assignedToCollection = `${assignedToCollection.slice(0, 2).join(', ')} +${(
assignedToCollection.length - 2
).toString()}`;
} else {
assignedToCollection = assignedToCollection.join(', ');
}
}
// display collection contact status before sending message to a collection
if (displayPopup) {
const dialogBox = (
<DialogBox
title={t('Contact status')}
handleOk={() => handleSendMessage()}
handleCancel={() => setDisplayPopup()}
buttonOk={t('Ok, Send')}
alignButtons="center"
>
<div className={styles.DialogBox} data-testid="description">
<div className={styles.Message}>
{t('Custom messages will not be sent to the opted out/session expired contacts.')}
</div>
<div className={styles.Message}>
{t('Only HSM template can be sent to the session expired contacts.')}{' '}
</div>
<div className={styles.Message}>
{t('Total Contacts:')} {collectionInfo ? JSON.parse(collectionInfo.groupInfo).total : 0}
<div>
{t('Contacts qualified for')}-
{Object.keys(display).map((data: any) => (
<span key={data} className={styles.Count}>
{data}: <span> {display[data]}</span>
</span>
))}
</div>
</div>
</div>
</DialogBox>
);
return dialogBox;
}
return (
<div className={styles.InfoWrapper}>
<div className={styles.CollectionInformation} data-testid="CollectionInformation">
<div>{t('Contacts qualified for')}-</div>
{Object.keys(display).map((data: any) => (
<div key={data} className={styles.SessionInfo}>
{data}: <span className={styles.SessionCount}> {display[data]}</span>
</div>
))}
</div>
<div className={styles.CollectionAssigned}>
{assignedToCollection && staff ? (
<>
<span className={styles.CollectionHeading}>{t('Assigned to')}</span>
<span className={styles.CollectionsName}>{assignedToCollection}</span>
</>
) : null}
</div>
</div>
);
}
Example #25
Source File: Collection.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
Collection: React.SFC<CollectionProps> = ({ match }) => {
const [selectedUsers, { data: collectionUsers }] = useLazyQuery(GET_COLLECTION_USERS, {
fetchPolicy: 'cache-and-network',
});
const collectionId = match.params.id ? match.params.id : null;
const [label, setLabel] = useState('');
const [description, setDescription] = useState('');
const [users, setUsers] = useState([]);
const [selected, setSelected] = useState([]);
const { t } = useTranslation();
const [updateCollectionUsers] = useMutation(UPDATE_COLLECTION_USERS);
const updateUsers = (collectionIdValue: any) => {
const initialSelectedUsers = users.map((user: any) => user.id);
const finalSelectedUsers = selected.map((user: any) => user.id);
const selectedUsersData = finalSelectedUsers.filter(
(user: any) => !initialSelectedUsers.includes(user)
);
const removedUsers = initialSelectedUsers.filter(
(contact: any) => !finalSelectedUsers.includes(contact)
);
if (selectedUsersData.length > 0 || removedUsers.length > 0) {
updateCollectionUsers({
variables: {
input: {
addUserIds: selectedUsersData,
groupId: collectionIdValue,
deleteUserIds: removedUsers,
},
},
});
}
};
const { data } = useQuery(GET_USERS, {
variables: setVariables(),
});
const { data: collectionList } = useQuery(GET_COLLECTIONS);
useEffect(() => {
if (collectionId) {
selectedUsers({ variables: { id: collectionId } });
}
}, [selectedUsers, collectionId]);
useEffect(() => {
if (collectionUsers) setUsers(collectionUsers.group.group.users);
}, [collectionUsers]);
const states = { label, description, users };
const setStates = ({ label: labelValue, description: descriptionValue }: any) => {
setLabel(labelValue);
setDescription(descriptionValue);
};
const additionalState = (user: any) => {
setSelected(user);
};
const refetchQueries = [
{
query: GET_COLLECTIONS,
variables: setVariables(),
},
{
query: SEARCH_QUERY,
variables: COLLECTION_SEARCH_QUERY_VARIABLES,
},
];
const validateTitle = (value: any) => {
let error;
if (value) {
let found = [];
if (collectionList) {
found = collectionList.groups.filter((search: any) => search.label === value);
if (collectionId && found.length > 0) {
found = found.filter((search: any) => search.id !== collectionId);
}
}
if (found.length > 0) {
error = t('Title already exists.');
}
}
return error;
};
const FormSchema = Yup.object().shape({
label: Yup.string().required(t('Title is required.')).max(50, t('Title is too long.')),
});
const dialogMessage = t("You won't be able to use this collection again.");
const formFields = [
{
component: Input,
name: 'label',
type: 'text',
placeholder: t('Title'),
validate: validateTitle,
},
{
component: Input,
name: 'description',
type: 'text',
placeholder: t('Description'),
rows: 3,
textArea: true,
},
{
component: AutoComplete,
name: 'users',
additionalState: 'users',
options: data ? data.users : [],
optionLabel: 'name',
textFieldProps: {
label: t('Assign staff to collection'),
variant: 'outlined',
},
skipPayload: true,
icon: <ContactIcon className={styles.ContactIcon} />,
helperText: t(
'Assigned staff members will be responsible to chat with contacts in this collection'
),
},
];
const collectionIcon = <CollectionIcon className={styles.CollectionIcon} />;
const queries = {
getItemQuery: GET_COLLECTION,
createItemQuery: CREATE_COLLECTION,
updateItemQuery: UPDATE_COLLECTION,
deleteItemQuery: DELETE_COLLECTION,
};
return (
<FormLayout
refetchQueries={refetchQueries}
additionalQuery={updateUsers}
{...queries}
match={match}
states={states}
additionalState={additionalState}
languageSupport={false}
setStates={setStates}
validationSchema={FormSchema}
listItemName="collection"
dialogMessage={dialogMessage}
formFields={formFields}
redirectionLink="collection"
listItem="group"
icon={collectionIcon}
/>
);
}
Example #26
Source File: ChatSubscription.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ChatSubscription: React.SFC<ChatSubscriptionProps> = ({ setDataLoaded }) => {
const queryVariables = SEARCH_QUERY_VARIABLES;
let refetchTimer: any = null;
const [triggerRefetch, setTriggerRefetch] = useState(false);
let subscriptionToRefetchSwitchHappened = false;
const [getContactQuery] = useLazyQuery(SEARCH_QUERY, {
onCompleted: (conversation) => {
if (conversation && conversation.search.length > 0) {
// save the conversation and update cache
// temporary fix for cache. need to check why query variables change
saveConversation(conversation, queryVariables);
}
},
});
const updateConversations = useCallback(
(cachedConversations: any, subscriptionData: any, action: string) => {
// if there is no message data then return previous conversations
if (!subscriptionData.data || subscriptionToRefetchSwitchHappened) {
return cachedConversations;
}
// let's return early incase we don't have cached conversations
// TODO: Need to investigate why this happens
if (!cachedConversations) {
return null;
}
let fetchMissingContact = false;
// let's record message sent and received subscriptions
if (action === 'SENT' || action === 'RECEIVED') {
// set fetch missing contact flag
fetchMissingContact = true;
// build the request array
recordRequests();
// determine if we should use subscriptions or refetch the query
if (switchSubscriptionToRefetch() && !subscriptionToRefetchSwitchHappened) {
// when switch happens
// 1. get the random time as defined in constant
// 2. set the refetch action for that duration
// 3. if we are still in fetch mode repeat the same.
// set the switch flag
subscriptionToRefetchSwitchHappened = true;
// let's get the random wait time
const waitTime =
randomIntFromInterval(REFETCH_RANDOM_TIME_MIN, REFETCH_RANDOM_TIME_MAX) * 1000;
// let's clear the timeout if exists
if (refetchTimer) {
clearTimeout(refetchTimer);
}
refetchTimer = setTimeout(() => {
// reset the switch flag
subscriptionToRefetchSwitchHappened = false;
// let's trigger refetch action
setTriggerRefetch(true);
}, waitTime);
return cachedConversations;
}
}
const { newMessage, contactId, collectionId, tagData, messageStatusData } =
getSubscriptionDetails(action, subscriptionData);
// loop through the cached conversations and find if contact exists
let conversationIndex = 0;
let conversationFound = false;
if (action === 'COLLECTION') {
cachedConversations.search.forEach((conversation: any, index: any) => {
if (conversation.group.id === collectionId) {
conversationIndex = index;
conversationFound = true;
}
});
} else {
cachedConversations.search.forEach((conversation: any, index: any) => {
if (conversation.contact.id === contactId) {
conversationIndex = index;
conversationFound = true;
}
});
}
// we should fetch missing contact only when we receive message subscriptions
// this means contact is not cached, so we need to fetch the conversations and add
// it to the cached conversations
// let's also skip fetching contact when we trigger this via group subscriptions
// let's skip fetch contact when we switch to refetch mode from subscription
if (
!conversationFound &&
newMessage &&
!newMessage.groupId &&
fetchMissingContact &&
!triggerRefetch
) {
const variables = {
contactOpts: {
limit: DEFAULT_CONTACT_LIMIT,
},
filter: { id: contactId },
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
},
};
addLogs(
`${action}-contact is not cached, so we need to fetch the conversations and add to cache`,
variables
);
getContactQuery({
variables,
});
return cachedConversations;
}
// we need to handle 2 scenarios:
// 1. Add new message if message is sent or received
// 2. Add/Delete message tags for a message
// let's start by parsing existing conversations
const updatedConversations = JSON.parse(JSON.stringify(cachedConversations));
let updatedConversation = updatedConversations.search;
// get the conversation for the contact that needs to be updated
updatedConversation = updatedConversation.splice(conversationIndex, 1);
// update contact last message at when receiving a new Message
if (action === 'RECEIVED') {
updatedConversation[0].contact.lastMessageAt = newMessage.insertedAt;
}
// Add new message and move the conversation to the top
if (newMessage) {
updatedConversation[0].messages.unshift(newMessage);
} else {
// let's add/delete tags for the message
// tag object: tagData.tag
updatedConversation[0].messages.forEach((message: any) => {
if (tagData && message.id === tagData.message.id) {
// let's add tag if action === "TAG_ADDED"
if (action === 'TAG_ADDED') {
message.tags.push(tagData.tag);
} else {
// handle delete of selected tags
// disabling eslint compile error for this until we find better solution
// eslint-disable-next-line
message.tags = message.tags.filter((tag: any) => tag.id !== tagData.tag.id);
}
}
if (messageStatusData && message.id === messageStatusData.id) {
// eslint-disable-next-line
message.errors = messageStatusData.errors;
}
});
}
// update the conversations
updatedConversations.search = [...updatedConversation, ...updatedConversations.search];
// return the updated object
const returnConversations = { ...cachedConversations, ...updatedConversations };
return returnConversations;
},
[getContactQuery]
);
const [loadCollectionData, { subscribeToMore: collectionSubscribe, data: collectionData }] =
useLazyQuery<any>(SEARCH_QUERY, {
variables: COLLECTION_SEARCH_QUERY_VARIABLES,
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-only',
onCompleted: () => {
const subscriptionVariables = { organizationId: getUserSession('organizationId') };
if (collectionSubscribe) {
// collection sent subscription
collectionSubscribe({
document: COLLECTION_SENT_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'COLLECTION'),
});
}
},
});
const [loadData, { loading, error, subscribeToMore, data, refetch }] = useLazyQuery<any>(
SEARCH_QUERY,
{
variables: queryVariables,
fetchPolicy: 'network-only',
nextFetchPolicy: 'cache-only',
onCompleted: () => {
const subscriptionVariables = { organizationId: getUserSession('organizationId') };
if (subscribeToMore) {
// message received subscription
subscribeToMore({
document: MESSAGE_RECEIVED_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'RECEIVED'),
});
// message sent subscription
subscribeToMore({
document: MESSAGE_SENT_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'SENT'),
});
// message status subscription
subscribeToMore({
document: MESSAGE_STATUS_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'STATUS'),
onError: () => {},
});
// tag added subscription
subscribeToMore({
document: ADD_MESSAGE_TAG_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'TAG_ADDED'),
});
// tag delete subscription
subscribeToMore({
document: DELETE_MESSAGE_TAG_SUBSCRIPTION,
variables: subscriptionVariables,
updateQuery: (prev, { subscriptionData }) =>
updateConversations(prev, subscriptionData, 'TAG_DELETED'),
});
}
},
}
);
useEffect(() => {
if (data && collectionData) {
setDataLoaded(true);
}
}, [data, collectionData]);
useEffect(() => {
loadData();
loadCollectionData();
}, []);
// lets return empty if we are still loading
if (loading) return <div />;
if (error) {
setErrorMessage(error);
return null;
}
if (triggerRefetch) {
// lets refetch here
if (refetch) {
addLogs('refetch for subscription', queryVariables);
refetch();
}
setTriggerRefetch(false);
}
return null;
}
Example #27
Source File: ContactBar.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ContactBar: React.SFC<ContactBarProps> = (props) => {
const {
contactId,
collectionId,
contactBspStatus,
lastMessageTime,
contactStatus,
displayName,
handleAction,
isSimulator,
} = props;
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const history = useHistory();
const [showCollectionDialog, setShowCollectionDialog] = useState(false);
const [showFlowDialog, setShowFlowDialog] = useState(false);
const [showBlockDialog, setShowBlockDialog] = useState(false);
const [showClearChatDialog, setClearChatDialog] = useState(false);
const [addContactsDialogShow, setAddContactsDialogShow] = useState(false);
const [showTerminateDialog, setShowTerminateDialog] = useState(false);
const { t } = useTranslation();
// get collection list
const [getCollections, { data: collectionsData }] = useLazyQuery(GET_COLLECTIONS, {
variables: setVariables(),
});
// get the published flow list
const [getFlows, { data: flowsData }] = useLazyQuery(GET_FLOWS, {
variables: setVariables({
status: FLOW_STATUS_PUBLISHED,
}),
fetchPolicy: 'network-only', // set for now, need to check cache issue
});
// get contact collections
const [getContactCollections, { data }] = useLazyQuery(GET_CONTACT_COLLECTIONS, {
variables: { id: contactId },
fetchPolicy: 'cache-and-network',
});
useEffect(() => {
if (contactId) {
getContactCollections();
}
}, [contactId]);
// mutation to update the contact collections
const [updateContactCollections] = useMutation(UPDATE_CONTACT_COLLECTIONS, {
onCompleted: (result: any) => {
const { numberDeleted, contactGroups } = result.updateContactGroups;
const numberAdded = contactGroups.length;
let notification = `Added to ${numberAdded} collection${numberAdded === 1 ? '' : 's'}`;
if (numberDeleted > 0 && numberAdded > 0) {
notification = `Added to ${numberDeleted} collection${
numberDeleted === 1 ? '' : 's and'
} removed from ${numberAdded} collection${numberAdded === 1 ? '' : 's '}`;
} else if (numberDeleted > 0) {
notification = `Removed from ${numberDeleted} collection${numberDeleted === 1 ? '' : 's'}`;
}
setNotification(notification);
},
refetchQueries: [{ query: GET_CONTACT_COLLECTIONS, variables: { id: contactId } }],
});
const [blockContact] = useMutation(UPDATE_CONTACT, {
onCompleted: () => {
setNotification(t('Contact blocked successfully.'));
},
refetchQueries: [{ query: SEARCH_QUERY, variables: SEARCH_QUERY_VARIABLES }],
});
const [addFlow] = useMutation(ADD_FLOW_TO_CONTACT, {
onCompleted: () => {
setNotification(t('Flow started successfully.'));
},
onError: (error) => {
setErrorMessage(error);
},
});
const [addFlowToCollection] = useMutation(ADD_FLOW_TO_COLLECTION, {
onCompleted: () => {
setNotification(t('Your flow will start in a couple of minutes.'));
},
});
// mutation to clear the chat messages of the contact
const [clearMessages] = useMutation(CLEAR_MESSAGES, {
variables: { contactId },
onCompleted: () => {
setClearChatDialog(false);
setNotification(t('Conversation cleared for this contact.'), 'warning');
},
});
let collectionOptions = [];
let flowOptions = [];
let initialSelectedCollectionIds: Array<any> = [];
let selectedCollectionsName;
let selectedCollections: any = [];
let assignedToCollection: any = [];
if (data) {
const { groups } = data.contact.contact;
initialSelectedCollectionIds = groups.map((group: any) => group.id);
selectedCollections = groups.map((group: any) => group.label);
selectedCollectionsName = shortenMultipleItems(selectedCollections);
assignedToCollection = groups.map((group: any) => group.users.map((user: any) => user.name));
assignedToCollection = Array.from(new Set([].concat(...assignedToCollection)));
assignedToCollection = shortenMultipleItems(assignedToCollection);
}
if (collectionsData) {
collectionOptions = collectionsData.groups;
}
if (flowsData) {
flowOptions = flowsData.flows;
}
let dialogBox = null;
const handleCollectionDialogOk = (selectedCollectionIds: any) => {
const finalSelectedCollections = selectedCollectionIds.filter(
(selectedCollectionId: any) => !initialSelectedCollectionIds.includes(selectedCollectionId)
);
const finalRemovedCollections = initialSelectedCollectionIds.filter(
(gId: any) => !selectedCollectionIds.includes(gId)
);
if (finalSelectedCollections.length > 0 || finalRemovedCollections.length > 0) {
updateContactCollections({
variables: {
input: {
contactId,
addGroupIds: finalSelectedCollections,
deleteGroupIds: finalRemovedCollections,
},
},
});
}
setShowCollectionDialog(false);
};
const handleCollectionDialogCancel = () => {
setShowCollectionDialog(false);
};
if (showCollectionDialog) {
dialogBox = (
<SearchDialogBox
selectedOptions={initialSelectedCollectionIds}
title={t('Add contact to collection')}
handleOk={handleCollectionDialogOk}
handleCancel={handleCollectionDialogCancel}
options={collectionOptions}
/>
);
}
const handleFlowSubmit = (flowId: any) => {
const flowVariables: any = {
flowId,
};
if (contactId) {
flowVariables.contactId = contactId;
addFlow({
variables: flowVariables,
});
}
if (collectionId) {
flowVariables.groupId = collectionId;
addFlowToCollection({
variables: flowVariables,
});
}
setShowFlowDialog(false);
};
const closeFlowDialogBox = () => {
setShowFlowDialog(false);
};
if (showFlowDialog) {
dialogBox = (
<DropdownDialog
title={t('Select flow')}
handleOk={handleFlowSubmit}
handleCancel={closeFlowDialogBox}
options={flowOptions}
placeholder={t('Select flow')}
description={t('The contact will be responded as per the messages planned in the flow.')}
/>
);
}
const handleClearChatSubmit = () => {
clearMessages();
setClearChatDialog(false);
handleAction();
};
if (showClearChatDialog) {
const bodyContext =
'All the conversation data for this contact will be deleted permanently from Glific. This action cannot be undone. However, you should be able to access it in reports if you have backup configuration enabled.';
dialogBox = (
<DialogBox
title="Are you sure you want to clear all conversation for this contact?"
handleOk={handleClearChatSubmit}
handleCancel={() => setClearChatDialog(false)}
alignButtons="center"
buttonOk="YES, CLEAR"
colorOk="secondary"
buttonCancel="MAYBE LATER"
>
<p className={styles.DialogText}>{bodyContext}</p>
</DialogBox>
);
}
const handleBlock = () => {
blockContact({
variables: {
id: contactId,
input: {
status: 'BLOCKED',
},
},
});
};
if (showBlockDialog) {
dialogBox = (
<DialogBox
title="Do you want to block this contact"
handleOk={handleBlock}
handleCancel={() => setShowBlockDialog(false)}
alignButtons="center"
colorOk="secondary"
>
<p className={styles.DialogText}>
You will not be able to view their chats and interact with them again
</p>
</DialogBox>
);
}
if (showTerminateDialog) {
dialogBox = <TerminateFlow contactId={contactId} setDialog={setShowTerminateDialog} />;
}
let flowButton: any;
const blockContactButton = contactId ? (
<Button
data-testid="blockButton"
className={styles.ListButtonDanger}
color="secondary"
disabled={isSimulator}
onClick={() => setShowBlockDialog(true)}
>
{isSimulator ? (
<BlockDisabledIcon className={styles.Icon} />
) : (
<BlockIcon className={styles.Icon} />
)}
Block Contact
</Button>
) : null;
if (collectionId) {
flowButton = (
<Button
data-testid="flowButton"
className={styles.ListButtonPrimary}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
<FlowIcon className={styles.Icon} />
Start a flow
</Button>
);
} else if (
contactBspStatus &&
status.includes(contactBspStatus) &&
!is24HourWindowOver(lastMessageTime)
) {
flowButton = (
<Button
data-testid="flowButton"
className={styles.ListButtonPrimary}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
<FlowIcon className={styles.Icon} />
Start a flow
</Button>
);
} else {
let toolTip = 'Option disabled because the 24hr window expired';
let disabled = true;
// if 24hr window expired & contact type HSM. we can start flow with template msg .
if (contactBspStatus === 'HSM') {
toolTip =
'Since the 24-hour window has passed, the contact will only receive a template message.';
disabled = false;
}
flowButton = (
<Tooltip title={toolTip} placement="right">
<span>
<Button
data-testid="disabledFlowButton"
className={styles.ListButtonPrimary}
disabled={disabled}
onClick={() => {
getFlows();
setShowFlowDialog(true);
}}
>
{disabled ? (
<FlowUnselectedIcon className={styles.Icon} />
) : (
<FlowIcon className={styles.Icon} />
)}
Start a flow
</Button>
</span>
</Tooltip>
);
}
const terminateFLows = contactId ? (
<Button
data-testid="terminateButton"
className={styles.ListButtonPrimary}
onClick={() => {
setShowTerminateDialog(!showTerminateDialog);
}}
>
<TerminateFlowIcon className={styles.Icon} />
Terminate flows
</Button>
) : null;
const viewDetails = contactId ? (
<Button
className={styles.ListButtonPrimary}
disabled={isSimulator}
data-testid="viewProfile"
onClick={() => {
history.push(`/contact-profile/${contactId}`);
}}
>
{isSimulator ? (
<ProfileDisabledIcon className={styles.Icon} />
) : (
<ProfileIcon className={styles.Icon} />
)}
View contact profile
</Button>
) : (
<Button
className={styles.ListButtonPrimary}
data-testid="viewContacts"
onClick={() => {
history.push(`/collection/${collectionId}/contacts`);
}}
>
<ProfileIcon className={styles.Icon} />
View details
</Button>
);
const addMember = contactId ? (
<>
<Button
data-testid="collectionButton"
className={styles.ListButtonPrimary}
onClick={() => {
getCollections();
setShowCollectionDialog(true);
}}
>
<AddContactIcon className={styles.Icon} />
Add to collection
</Button>
<Button
className={styles.ListButtonPrimary}
data-testid="clearChatButton"
onClick={() => setClearChatDialog(true)}
>
<ClearConversation className={styles.Icon} />
Clear conversation
</Button>
</>
) : (
<Button
data-testid="collectionButton"
className={styles.ListButtonPrimary}
onClick={() => {
setAddContactsDialogShow(true);
}}
>
<AddContactIcon className={styles.Icon} />
Add contact
</Button>
);
if (addContactsDialogShow) {
dialogBox = (
<AddContactsToCollection collectionId={collectionId} setDialog={setAddContactsDialogShow} />
);
}
const popper = (
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom-start"
transition
className={styles.Popper}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper elevation={3} className={styles.Container}>
{viewDetails}
{flowButton}
{addMember}
{terminateFLows}
{blockContactButton}
</Paper>
</Fade>
)}
</Popper>
);
const handleConfigureIconClick = (event: any) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
let contactCollections: any;
if (selectedCollections.length > 0) {
contactCollections = (
<div className={styles.ContactCollections}>
<span className={styles.CollectionHeading}>Collections</span>
<span className={styles.CollectionsName} data-testid="collectionNames">
{selectedCollectionsName}
</span>
</div>
);
}
const getTitleAndIconForSmallScreen = (() => {
const { location } = history;
if (location.pathname.includes('collection')) {
return CollectionIcon;
}
if (location.pathname.includes('saved-searches')) {
return SavedSearchIcon;
}
return ChatIcon;
})();
// CONTACT: display session timer & Assigned to
const IconComponent = getTitleAndIconForSmallScreen;
const sessionAndCollectionAssignedTo = (
<>
{contactId ? (
<div className={styles.SessionTimerContainer}>
<div className={styles.SessionTimer} data-testid="sessionTimer">
<span>Session Timer</span>
<Timer
time={lastMessageTime}
contactStatus={contactStatus}
contactBspStatus={contactBspStatus}
/>
</div>
<div>
{assignedToCollection ? (
<>
<span className={styles.CollectionHeading}>Assigned to</span>
<span className={styles.CollectionsName}>{assignedToCollection}</span>
</>
) : null}
</div>
</div>
) : null}
<div className={styles.Chat} onClick={() => showChats()} aria-hidden="true">
<IconButton className={styles.MobileIcon}>
<IconComponent />
</IconButton>
</div>
</>
);
// COLLECTION: display contact info & Assigned to
let collectionStatus: any;
if (collectionId) {
collectionStatus = <CollectionInformation collectionId={collectionId} />;
}
return (
<Toolbar className={styles.ContactBar} color="primary">
<div className={styles.ContactBarWrapper}>
<div className={styles.ContactInfoContainer}>
<div className={styles.ContactInfoWrapper}>
<div className={styles.InfoWrapperRight}>
<div className={styles.ContactDetails}>
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
<div
className={styles.Configure}
data-testid="dropdownIcon"
onClick={handleConfigureIconClick}
onKeyPress={handleConfigureIconClick}
aria-hidden
>
<DropdownIcon />
</div>
</ClickAwayListener>
<Typography
className={styles.Title}
variant="h6"
noWrap
data-testid="beneficiaryName"
>
{displayName}
</Typography>
</div>
{contactCollections}
</div>
{collectionStatus}
{sessionAndCollectionAssignedTo}
</div>
</div>
</div>
{popper}
{dialogBox}
</Toolbar>
);
}
Example #28
Source File: ChatMessages.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ChatMessages: React.SFC<ChatMessagesProps> = ({
contactId,
collectionId,
startingHeight,
}) => {
// create an instance of apollo client
// const [loadAllTags, allTags] = useLazyQuery(FILTER_TAGS_NAME, {
// variables: setVariables(),
// });
const urlString = new URL(window.location.href);
let messageParameterOffset: any = 0;
let searchMessageNumber: any;
// get the message number from url
if (urlString.searchParams.get('search')) {
searchMessageNumber = urlString.searchParams.get('search');
// check if the message number is greater than 10 otherwise set the initial offset to 0
messageParameterOffset =
searchMessageNumber && parseInt(searchMessageNumber, 10) - 10 < 0
? 1
: parseInt(searchMessageNumber, 10) - 10;
}
const [editTagsMessageId, setEditTagsMessageId] = useState<number | null>(null);
const [dialog, setDialogbox] = useState<string>();
// const [selectedMessageTags, setSelectedMessageTags] = useState<any>(null);
// const [previousMessageTags, setPreviousMessageTags] = useState<any>(null);
const [showDropdown, setShowDropdown] = useState<any>(null);
const [reducedHeight, setReducedHeight] = useState(0);
const [showLoadMore, setShowLoadMore] = useState(true);
const [scrolledToMessage, setScrolledToMessage] = useState(false);
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
const [conversationInfo, setConversationInfo] = useState<any>({});
const [collectionVariables, setCollectionVariables] = useState<any>({});
const { t } = useTranslation();
let dialogBox;
useEffect(() => {
setShowLoadMore(true);
setScrolledToMessage(false);
setShowJumpToLatest(false);
}, [contactId]);
useEffect(() => {
setTimeout(() => {
const messageContainer: any = document.querySelector('.messageContainer');
if (messageContainer) {
messageContainer.addEventListener('scroll', (event: any) => {
const messageContainerTarget = event.target;
if (
Math.round(messageContainerTarget.scrollTop) ===
messageContainerTarget.scrollHeight - messageContainerTarget.offsetHeight
) {
setShowJumpToLatest(false);
} else if (showJumpToLatest === false) {
setShowJumpToLatest(true);
}
});
}
}, 1000);
}, [setShowJumpToLatest, contactId, reducedHeight]);
const scrollToLatestMessage = () => {
const container: any = document.querySelector('.messageContainer');
if (container) {
const scroll = container.scrollHeight - container.clientHeight;
if (scroll) {
container.scrollTo(0, scroll);
}
}
};
// Instantiate these to be used later.
let conversationIndex: number = -1;
// create message mutation
const [createAndSendMessage] = useMutation(CREATE_AND_SEND_MESSAGE_MUTATION, {
onCompleted: () => {
scrollToLatestMessage();
},
onError: (error: any) => {
const { message } = error;
if (message) {
setNotification(message, 'warning');
}
return null;
},
});
useEffect(() => {
const clickListener = () => setShowDropdown(null);
if (editTagsMessageId) {
// need to check why we are doing this
window.addEventListener('click', clickListener, true);
}
return () => {
window.removeEventListener('click', clickListener);
};
}, [editTagsMessageId]);
// get the conversations stored from the cache
let queryVariables = SEARCH_QUERY_VARIABLES;
if (collectionId) {
queryVariables = COLLECTION_SEARCH_QUERY_VARIABLES;
}
const {
loading: conversationLoad,
error: conversationError,
data: allConversations,
}: any = useQuery(SEARCH_QUERY, {
variables: queryVariables,
fetchPolicy: 'cache-only',
});
const scrollToMessage = (messageNumberToScroll: any) => {
setTimeout(() => {
const scrollElement = document.querySelector(`#search${messageNumberToScroll}`);
if (scrollElement) {
scrollElement.scrollIntoView();
}
}, 1000);
};
// scroll to the particular message after loading
const getScrollToMessage = () => {
if (!scrolledToMessage) {
scrollToMessage(urlString.searchParams.get('search'));
setScrolledToMessage(true);
}
};
/* istanbul ignore next */
const [
getSearchParameterQuery,
{ called: parameterCalled, data: parameterdata, loading: parameterLoading },
] = useLazyQuery<any>(SEARCH_QUERY, {
onCompleted: (searchData) => {
if (searchData && searchData.search.length > 0) {
// get the conversations from cache
const conversations = getCachedConverations(queryVariables);
const conversationCopy = JSON.parse(JSON.stringify(searchData));
conversationCopy.search[0].messages
.sort((currentMessage: any, nextMessage: any) => currentMessage.id - nextMessage.id)
.reverse();
const conversationsCopy = JSON.parse(JSON.stringify(conversations));
conversationsCopy.search = conversationsCopy.search.map((conversation: any) => {
const conversationObj = conversation;
if (collectionId) {
// If the collection(group) is present in the cache
if (conversationObj.group?.id === collectionId.toString()) {
conversationObj.messages = conversationCopy.search[0].messages;
}
// If the contact is present in the cache
} else if (conversationObj.contact?.id === contactId?.toString()) {
conversationObj.messages = conversationCopy.search[0].messages;
}
return conversationObj;
});
// update the conversation cache
updateConversationsCache(conversationsCopy, queryVariables);
// need to display Load more messages button
setShowLoadMore(true);
}
},
});
const [getSearchQuery, { called, data, loading, error }] = useLazyQuery<any>(SEARCH_QUERY, {
onCompleted: (searchData) => {
if (searchData && searchData.search.length > 0) {
// get the conversations from cache
const conversations = getCachedConverations(queryVariables);
const conversationCopy = JSON.parse(JSON.stringify(searchData));
conversationCopy.search[0].messages
.sort((currentMessage: any, nextMessage: any) => currentMessage.id - nextMessage.id)
.reverse();
let conversationsCopy: any = { search: [] };
// check for the cache
if (JSON.parse(JSON.stringify(conversations))) {
conversationsCopy = JSON.parse(JSON.stringify(conversations));
}
let isContactCached = false;
conversationsCopy.search = conversationsCopy.search.map((conversation: any) => {
const conversationObj = conversation;
// If the collection(group) is present in the cache
if (collectionId) {
if (conversationObj.group?.id === collectionId.toString()) {
isContactCached = true;
conversationObj.messages = [
...conversationObj.messages,
...conversationCopy.search[0].messages,
];
}
}
// If the contact is present in the cache
else if (conversationObj.contact?.id === contactId?.toString()) {
isContactCached = true;
conversationObj.messages = [
...conversationObj.messages,
...conversationCopy.search[0].messages,
];
}
return conversationObj;
});
// If the contact is NOT present in the cache
if (!isContactCached) {
conversationsCopy.search = [...conversationsCopy.search, searchData.search[0]];
}
// update the conversation cache
updateConversationsCache(conversationsCopy, queryVariables);
if (searchData.search[0].messages.length === 0) {
setShowLoadMore(false);
}
}
},
});
useEffect(() => {
// scroll to the particular message after loading
if (data || parameterdata) getScrollToMessage();
}, [data, parameterdata]);
let messageList: any;
// let unselectedTags: Array<any> = [];
// // tagging message mutation
// const [createMessageTag] = useMutation(UPDATE_MESSAGE_TAGS, {
// onCompleted: () => {
// setNotification(client, t('Tags added successfully'));
// setDialogbox('');
// },
// });
const [sendMessageToCollection] = useMutation(CREATE_AND_SEND_MESSAGE_TO_COLLECTION_MUTATION, {
refetchQueries: [{ query: SEARCH_QUERY, variables: SEARCH_QUERY_VARIABLES }],
onCompleted: () => {
scrollToLatestMessage();
},
onError: (collectionError: any) => {
const { message } = collectionError;
if (message) {
setNotification(message, 'warning');
}
return null;
},
});
const updatePayload = (payload: any, selectedTemplate: any, variableParam: any) => {
const payloadCopy = payload;
// add additional param for template
if (selectedTemplate) {
payloadCopy.isHsm = selectedTemplate.isHsm;
payloadCopy.templateId = parseInt(selectedTemplate.id, 10);
payloadCopy.params = variableParam;
}
return payloadCopy;
};
const handleSendMessage = () => {
setDialogbox('');
sendMessageToCollection({
variables: collectionVariables,
});
};
// this function is called when the message is sent collection
const sendCollectionMessageHandler = (
body: string,
mediaId: string,
messageType: string,
selectedTemplate: any,
variableParam: any
) => {
// display collection info popup
setDialogbox('collection');
const payload: any = {
body,
senderId: 1,
mediaId,
type: messageType,
flow: 'OUTBOUND',
};
setCollectionVariables({
groupId: collectionId,
input: updatePayload(payload, selectedTemplate, variableParam),
});
};
// this function is called when the message is sent
const sendMessageHandler = useCallback(
(
body: any,
mediaId: string,
messageType: string,
selectedTemplate: any,
variableParam: any,
interactiveTemplateId: any
) => {
const payload: any = {
body,
senderId: 1,
mediaId,
receiverId: contactId,
type: messageType,
flow: 'OUTBOUND',
interactiveTemplateId,
};
createAndSendMessage({
variables: { input: updatePayload(payload, selectedTemplate, variableParam) },
});
},
[createAndSendMessage, contactId]
);
// HOOKS ESTABLISHED ABOVE
// Run through these cases to ensure data always exists
if (called && error) {
setErrorMessage(error);
return null;
}
if (conversationError) {
setErrorMessage(conversationError);
return null;
}
// loop through the cached conversations and find if contact/Collection exists
const updateConversationInfo = (type: string, Id: any) => {
allConversations.search.map((conversation: any, index: any) => {
if (conversation[type].id === Id.toString()) {
conversationIndex = index;
setConversationInfo(conversation);
}
return null;
});
};
const findContactInAllConversations = () => {
if (allConversations && allConversations.search) {
// loop through the cached conversations and find if contact exists
// need to check - updateConversationInfo('contact', contactId);
allConversations.search.map((conversation: any, index: any) => {
if (conversation.contact.id === contactId?.toString()) {
conversationIndex = index;
setConversationInfo(conversation);
}
return null;
});
}
// if conversation is not present then fetch for contact
if (conversationIndex < 0) {
if ((!loading && !called) || (data && data.search[0].contact.id !== contactId)) {
const variables = {
filter: { id: contactId },
contactOpts: { limit: 1 },
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
offset: messageParameterOffset,
},
};
addLogs(`if conversation is not present then search for contact-${contactId}`, variables);
getSearchQuery({
variables,
});
}
// lets not get from cache if parameter is present
} else if (conversationIndex > -1 && messageParameterOffset) {
if (
(!parameterLoading && !parameterCalled) ||
(parameterdata && parameterdata.search[0].contact.id !== contactId)
) {
const variables = {
filter: { id: contactId },
contactOpts: { limit: 1 },
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
offset: messageParameterOffset,
},
};
addLogs(`if search message is not present then search for contact-${contactId}`, variables);
getSearchParameterQuery({
variables,
});
}
}
};
const findCollectionInAllConversations = () => {
// loop through the cached conversations and find if collection exists
if (allConversations && allConversations.search) {
if (collectionId === -1) {
conversationIndex = 0;
setConversationInfo(allConversations.search);
} else {
updateConversationInfo('group', collectionId);
}
}
// if conversation is not present then fetch the collection
if (conversationIndex < 0) {
if (!loading && !data) {
const variables = {
filter: { id: collectionId, searchGroup: true },
contactOpts: { limit: DEFAULT_CONTACT_LIMIT },
messageOpts: { limit: DEFAULT_MESSAGE_LIMIT, offset: 0 },
};
addLogs(
`if conversation is not present then search for collection-${collectionId}`,
variables
);
getSearchQuery({
variables,
});
}
}
};
// find if contact/Collection present in the cached
useEffect(() => {
if (contactId) {
findContactInAllConversations();
} else if (collectionId) {
findCollectionInAllConversations();
}
}, [contactId, collectionId, allConversations]);
useEffect(() => {
if (searchMessageNumber) {
const element = document.querySelector(`#search${searchMessageNumber}`);
if (element) {
element.scrollIntoView();
} else {
// need to check if message is not present fetch message from selected contact
}
}
}, [searchMessageNumber]);
// const closeDialogBox = () => {
// setDialogbox('');
// setShowDropdown(null);
// };
// check if the search API results nothing for a particular contact ID and redirect to chat
if (contactId && data) {
if (data.search.length === 0 || data.search[0].contact.status === 'BLOCKED') {
return <Redirect to="/chat" />;
}
}
/* istanbul ignore next */
// const handleSubmit = (tags: any) => {
// const selectedTags = tags.filter((tag: any) => !previousMessageTags.includes(tag));
// unselectedTags = previousMessageTags.filter((tag: any) => !tags.includes(tag));
// if (selectedTags.length === 0 && unselectedTags.length === 0) {
// setDialogbox('');
// setShowDropdown(null);
// } else {
// createMessageTag({
// variables: {
// input: {
// messageId: editTagsMessageId,
// addTagIds: selectedTags,
// deleteTagIds: unselectedTags,
// },
// },
// });
// }
// };
// const tags = allTags.data ? allTags.data.tags : [];
// if (dialog === 'tag') {
// dialogBox = (
// <SearchDialogBox
// selectedOptions={selectedMessageTags}
// title={t('Assign tag to message')}
// handleOk={handleSubmit}
// handleCancel={closeDialogBox}
// options={tags}
// icon={<TagIcon />}
// />
// );
// }
const showEditTagsDialog = (id: number) => {
setEditTagsMessageId(id);
setShowDropdown(id);
};
// on reply message scroll to replied message or fetch if not present
const jumpToMessage = (messageNumber: number) => {
const element = document.querySelector(`#search${messageNumber}`);
if (element) {
element.scrollIntoView();
} else {
const offset = messageNumber - 10 <= 0 ? 1 : messageNumber - 10;
const variables: any = {
filter: { id: contactId?.toString() },
contactOpts: { limit: 1 },
messageOpts: {
limit: conversationInfo.messages[conversationInfo.messages.length - 1].messageNumber,
offset,
},
};
addLogs(`fetch reply message`, variables);
getSearchQuery({
variables,
});
scrollToMessage(messageNumber);
}
};
const showDaySeparator = (currentDate: string, nextDate: string) => {
// if it's last message and its date is greater than current date then show day separator
if (!nextDate && moment(currentDate).format('YYYY-MM-DD') < moment().format('YYYY-MM-DD')) {
return true;
}
// if the day is changed then show day separator
if (
nextDate &&
moment(currentDate).format('YYYY-MM-DD') > moment(nextDate).format('YYYY-MM-DD')
) {
return true;
}
return false;
};
if (conversationInfo && conversationInfo.messages && conversationInfo.messages.length > 0) {
let reverseConversation = [...conversationInfo.messages];
reverseConversation = reverseConversation.map((message: any, index: number) => (
<ChatMessage
{...message}
contactId={contactId}
key={message.id}
popup={message.id === showDropdown}
onClick={() => showEditTagsDialog(message.id)}
// setDialog={() => {
// loadAllTags();
// let messageTags = conversationInfo.messages.filter(
// (messageObj: any) => messageObj.id === editTagsMessageId
// );
// if (messageTags.length > 0) {
// messageTags = messageTags[0].tags;
// }
// const messageTagId = messageTags.map((tag: any) => tag.id);
// setSelectedMessageTags(messageTagId);
// setPreviousMessageTags(messageTagId);
// setDialogbox('tag');
// }}
focus={index === 0}
jumpToMessage={jumpToMessage}
daySeparator={showDaySeparator(
reverseConversation[index].insertedAt,
reverseConversation[index + 1] ? reverseConversation[index + 1].insertedAt : null
)}
/>
));
messageList = reverseConversation
.sort((currentMessage: any, nextMessage: any) => currentMessage.id - nextMessage.id)
.reverse();
}
const loadMoreMessages = () => {
const { messageNumber } = conversationInfo.messages[conversationInfo.messages.length - 1];
const variables: any = {
filter: { id: contactId?.toString() },
contactOpts: { limit: 1 },
messageOpts: {
limit:
messageNumber > DEFAULT_MESSAGE_LOADMORE_LIMIT
? DEFAULT_MESSAGE_LOADMORE_LIMIT - 1
: messageNumber - 2,
offset:
messageNumber - DEFAULT_MESSAGE_LOADMORE_LIMIT <= 0
? 1
: messageNumber - DEFAULT_MESSAGE_LOADMORE_LIMIT,
},
};
if (collectionId) {
variables.filter = { id: collectionId.toString(), searchGroup: true };
}
addLogs(`load More Messages-${collectionId}`, variables);
getSearchQuery({
variables,
});
// keep scroll at last message
const element = document.querySelector(`#search${messageNumber}`);
if (element) {
element.scrollIntoView();
}
};
let messageListContainer;
// Check if there are conversation messages else display no messages
if (messageList) {
const loadMoreOption =
conversationInfo.messages.length > DEFAULT_MESSAGE_LIMIT - 1 ||
(searchMessageNumber && searchMessageNumber > 19);
messageListContainer = (
<Container
className={`${styles.MessageList} messageContainer `}
style={{ height: `calc(100% - 195px - ${reducedHeight}px)` }}
maxWidth={false}
data-testid="messageContainer"
>
{showLoadMore && loadMoreOption && (
<div className={styles.LoadMore}>
{(called && loading) || conversationLoad ? (
<CircularProgress className={styles.Loading} />
) : (
<div
className={styles.LoadMoreButton}
onClick={loadMoreMessages}
onKeyDown={loadMoreMessages}
aria-hidden="true"
data-testid="loadMoreMessages"
>
{t('Load more messages')}
</div>
)}
</div>
)}
{messageList}
</Container>
);
} else {
messageListContainer = (
<div className={styles.NoMessages} data-testid="messageContainer">
{t('No messages.')}
</div>
);
}
const handleHeightChange = (newHeight: number) => {
setReducedHeight(newHeight);
};
const handleChatClearedAction = () => {
const conversationInfoCopy = JSON.parse(JSON.stringify(conversationInfo));
conversationInfoCopy.messages = [];
let allConversationsCopy: any = [];
allConversationsCopy = JSON.parse(JSON.stringify(allConversations));
const index = conversationIndex === -1 ? 0 : conversationIndex;
allConversationsCopy.search[index] = conversationInfoCopy;
// update allConversations in the cache
updateConversationsCache(allConversationsCopy, queryVariables);
};
// conversationInfo should not be empty
if (!Object.prototype.hasOwnProperty.call(conversationInfo, 'contact')) {
return (
<div className={styles.LoadMore}>
<CircularProgress className={styles.Loading} />
</div>
);
}
let topChatBar;
let chatInputSection;
if (contactId) {
const displayName = getDisplayName(conversationInfo);
topChatBar = (
<ContactBar
displayName={displayName}
isSimulator={conversationInfo.contact.phone.startsWith(SIMULATOR_NUMBER_START)}
contactId={contactId.toString()}
lastMessageTime={conversationInfo.contact.lastMessageAt}
contactStatus={conversationInfo.contact.status}
contactBspStatus={conversationInfo.contact.bspStatus}
handleAction={() => handleChatClearedAction()}
/>
);
chatInputSection = (
<ChatInput
handleHeightChange={handleHeightChange}
onSendMessage={sendMessageHandler}
lastMessageTime={conversationInfo.contact.lastMessageAt}
contactStatus={conversationInfo.contact.status}
contactBspStatus={conversationInfo.contact.bspStatus}
/>
);
} else if (collectionId) {
topChatBar = (
<ContactBar
collectionId={collectionId.toString()}
displayName={conversationInfo.group.label}
handleAction={handleChatClearedAction}
/>
);
chatInputSection = (
<ChatInput
handleHeightChange={handleHeightChange}
onSendMessage={sendCollectionMessageHandler}
isCollection
/>
);
}
const showLatestMessage = () => {
setShowJumpToLatest(false);
// check if we have offset 0 (messageNumber === offset)
if (conversationInfo.messages[0].messageNumber !== 0) {
// set limit to default message limit
const limit = DEFAULT_MESSAGE_LIMIT;
// set variable for contact chats
const variables: any = {
contactOpts: { limit: 1 },
filter: { id: contactId?.toString() },
messageOpts: { limit, offset: 0 },
};
// if collection, replace id with collection id
if (collectionId) {
variables.filter = { id: collectionId.toString(), searchGroup: true };
}
addLogs(`show Latest Message for contact-${contactId}`, variables);
getSearchParameterQuery({
variables,
});
}
scrollToLatestMessage();
};
const jumpToLatest = (
<div
data-testid="jumpToLatest"
className={styles.JumpToLatest}
onClick={() => showLatestMessage()}
onKeyDown={() => showLatestMessage()}
aria-hidden="true"
>
{t('Jump to latest')}
<ExpandMoreIcon />
</div>
);
return (
<Container
className={styles.ChatMessages}
style={{
height: startingHeight,
}}
maxWidth={false}
disableGutters
>
{dialogBox}
{dialog === 'collection' ? (
<CollectionInformation
collectionId={collectionId}
displayPopup
setDisplayPopup={() => setDialogbox('')}
handleSendMessage={() => handleSendMessage()}
/>
) : null}
{topChatBar}
<StatusBar />
{messageListContainer}
{conversationInfo.messages.length && showJumpToLatest ? jumpToLatest : null}
{chatInputSection}
</Container>
);
}
Example #29
Source File: ConversationList.tsx From glific-frontend with GNU Affero General Public License v3.0 | 4 votes |
ConversationList: React.SFC<ConversationListProps> = (props) => {
const {
searchVal,
selectedContactId,
setSelectedContactId,
savedSearchCriteria,
savedSearchCriteriaId,
searchParam,
searchMode,
selectedCollectionId,
setSelectedCollectionId,
entityType = 'contact',
} = props;
const client = useApolloClient();
const [loadingOffset, setLoadingOffset] = useState(DEFAULT_CONTACT_LIMIT);
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
const [showLoadMore, setShowLoadMore] = useState(true);
const [showLoading, setShowLoading] = useState(false);
const [searchMultiData, setSearchMultiData] = useState<any>();
const scrollHeight = useQuery(SCROLL_HEIGHT);
const { t } = useTranslation();
let queryVariables = SEARCH_QUERY_VARIABLES;
if (selectedCollectionId) {
queryVariables = COLLECTION_SEARCH_QUERY_VARIABLES;
}
if (savedSearchCriteria) {
const variables = JSON.parse(savedSearchCriteria);
queryVariables = variables;
}
// check if there is a previous scroll height
useEffect(() => {
if (scrollHeight.data && scrollHeight.data.height) {
const container = document.querySelector('.contactsContainer');
if (container) {
container.scrollTop = scrollHeight.data.height;
}
}
}, [scrollHeight.data]);
useEffect(() => {
const contactsContainer: any = document.querySelector('.contactsContainer');
if (contactsContainer) {
contactsContainer.addEventListener('scroll', (event: any) => {
const contactContainer = event.target;
if (contactContainer.scrollTop === 0) {
setShowJumpToLatest(false);
} else if (showJumpToLatest === false) {
setShowJumpToLatest(true);
}
});
}
});
// reset offset value on saved search changes
useEffect(() => {
if (savedSearchCriteriaId) {
setLoadingOffset(DEFAULT_CONTACT_LIMIT + 10);
}
}, [savedSearchCriteriaId]);
const {
loading: conversationLoading,
error: conversationError,
data,
} = useQuery<any>(SEARCH_QUERY, {
variables: queryVariables,
fetchPolicy: 'cache-only',
});
const filterVariables = () => {
if (savedSearchCriteria && Object.keys(searchParam).length === 0) {
const variables = JSON.parse(savedSearchCriteria);
if (searchVal) variables.filter.term = searchVal;
return variables;
}
const filter: any = {};
if (searchVal) {
filter.term = searchVal;
}
const params = searchParam;
if (params) {
// if (params.includeTags && params.includeTags.length > 0)
// filter.includeTags = params.includeTags.map((obj: any) => obj.id);
if (params.includeGroups && params.includeGroups.length > 0)
filter.includeGroups = params.includeGroups.map((obj: any) => obj.id);
if (params.includeUsers && params.includeUsers.length > 0)
filter.includeUsers = params.includeUsers.map((obj: any) => obj.id);
if (params.includeLabels && params.includeLabels.length > 0)
filter.includeLabels = params.includeLabels.map((obj: any) => obj.id);
if (params.dateFrom) {
filter.dateRange = {
from: moment(params.dateFrom).format('YYYY-MM-DD'),
to: moment(params.dateTo).format('YYYY-MM-DD'),
};
}
}
// If tab is collection then add appropriate filter
if (selectedCollectionId) {
filter.searchGroup = true;
if (searchVal) {
delete filter.term;
filter.groupLabel = searchVal;
}
}
return {
contactOpts: {
limit: DEFAULT_CONTACT_LIMIT,
},
filter,
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
},
};
};
const filterSearch = () => ({
contactOpts: {
limit: DEFAULT_CONTACT_LIMIT,
order: 'DESC',
},
searchFilter: {
term: searchVal,
},
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
offset: 0,
order: 'ASC',
},
});
const [loadMoreConversations, { data: contactsData }] = useLazyQuery<any>(SEARCH_QUERY, {
onCompleted: (searchData) => {
if (searchData && searchData.search.length === 0) {
setShowLoadMore(false);
} else {
// Now if there is search string and tab is collection then load more will return appropriate data
const variables: any = queryVariables;
if (selectedCollectionId && searchVal) {
variables.filter.groupLabel = searchVal;
}
// save the conversation and update cache
updateConversations(searchData, variables);
setShowLoadMore(true);
setLoadingOffset(loadingOffset + DEFAULT_CONTACT_LOADMORE_LIMIT);
}
setShowLoading(false);
},
});
useEffect(() => {
if (contactsData) {
setShowLoading(false);
}
}, [contactsData]);
const [getFilterConvos, { called, loading, error, data: searchData }] =
useLazyQuery<any>(SEARCH_QUERY);
// fetch data when typing for search
const [getFilterSearch] = useLazyQuery<any>(SEARCH_MULTI_QUERY, {
onCompleted: (multiSearch) => {
setSearchMultiData(multiSearch);
},
});
// load more messages for multi search load more
const [getLoadMoreFilterSearch, { loading: loadingSearch }] = useLazyQuery<any>(
SEARCH_MULTI_QUERY,
{
onCompleted: (multiSearch) => {
if (!searchMultiData) {
setSearchMultiData(multiSearch);
} else if (multiSearch && multiSearch.searchMulti.messages.length !== 0) {
const searchMultiDataCopy = JSON.parse(JSON.stringify(searchMultiData));
// append new messages to existing messages
searchMultiDataCopy.searchMulti.messages = [
...searchMultiData.searchMulti.messages,
...multiSearch.searchMulti.messages,
];
setSearchMultiData(searchMultiDataCopy);
} else {
setShowLoadMore(false);
}
setShowLoading(false);
},
}
);
useEffect(() => {
// Use multi search when has search value and when there is no collection id
if (searchVal && Object.keys(searchParam).length === 0 && !selectedCollectionId) {
addLogs(`Use multi search when has search value`, filterSearch());
getFilterSearch({
variables: filterSearch(),
});
} else {
// This is used for filtering the searches, when you click on it, so only call it
// when user clicks and savedSearchCriteriaId is set.
addLogs(`filtering the searches`, filterVariables());
getFilterConvos({
variables: filterVariables(),
});
}
}, [searchVal, searchParam, savedSearchCriteria]);
// Other cases
if ((called && loading) || conversationLoading) return <Loading />;
if ((called && error) || conversationError) {
if (error) {
setErrorMessage(error);
} else if (conversationError) {
setErrorMessage(conversationError);
}
return null;
}
const setSearchHeight = () => {
client.writeQuery({
query: SCROLL_HEIGHT,
data: { height: document.querySelector('.contactsContainer')?.scrollTop },
});
};
let conversations: any = null;
// Retrieving all convos or the ones searched by.
if (data) {
conversations = data.search;
}
// If no cache, assign conversations data from search query.
if (called && (searchVal || savedSearchCriteria || searchParam)) {
conversations = searchData.search;
}
const buildChatConversation = (index: number, header: any, conversation: any) => {
// We don't have the contact data in the case of contacts.
let contact = conversation;
if (conversation.contact) {
contact = conversation.contact;
}
let selectedRecord = false;
if (selectedContactId === contact.id) {
selectedRecord = true;
}
return (
<>
{index === 0 ? header : null}
<ChatConversation
key={contact.id}
selected={selectedRecord}
onClick={() => {
setSearchHeight();
if (entityType === 'contact' && setSelectedContactId) {
setSelectedContactId(contact.id);
}
}}
entityType={entityType}
index={index}
contactId={contact.id}
contactName={contact.name || contact.maskedPhone}
lastMessage={conversation}
senderLastMessage={contact.lastMessageAt}
contactStatus={contact.status}
contactBspStatus={contact.bspStatus}
contactIsOrgRead={contact.isOrgRead}
highlightSearch={searchVal}
messageNumber={conversation.messageNumber}
searchMode={searchMode}
/>
</>
);
};
let conversationList: any;
// If a search term is used, use the SearchMulti API. For searches term, this is not applicable.
if (searchVal && searchMultiData && Object.keys(searchParam).length === 0) {
conversations = searchMultiData.searchMulti;
// to set search response sequence
const searchArray = { contacts: [], tags: [], messages: [], labels: [] };
let conversationsData;
Object.keys(searchArray).forEach((dataArray: any) => {
const header = (
<div className={styles.Title}>
<Typography className={styles.TitleText}>{dataArray}</Typography>
</div>
);
conversationsData = conversations[dataArray].map((conversation: any, index: number) =>
buildChatConversation(index, header, conversation)
);
// Check if its not empty
if (conversationsData.length > 0) {
if (!conversationList) conversationList = [];
conversationList.push(conversationsData);
}
});
}
// build the conversation list only if there are conversations
if (!conversationList && conversations && conversations.length > 0) {
// TODO: Need to check why test is not returning correct result
conversationList = conversations.map((conversation: any, index: number) => {
let lastMessage = [];
if (conversation.messages.length > 0) {
[lastMessage] = conversation.messages;
}
const key = index;
let entityId: any;
let senderLastMessage = '';
let displayName = '';
let contactStatus = '';
let contactBspStatus = '';
let contactIsOrgRead = false;
let selectedRecord = false;
if (conversation.contact) {
if (selectedContactId === conversation.contact.id) {
selectedRecord = true;
}
entityId = conversation.contact.id;
displayName = getDisplayName(conversation);
senderLastMessage = conversation.contact.lastMessageAt;
contactStatus = conversation.contact.status;
contactBspStatus = conversation.contact.bspStatus;
contactIsOrgRead = conversation.contact.isOrgRead;
} else if (conversation.group) {
if (selectedCollectionId === conversation.group.id) {
selectedRecord = true;
}
entityId = conversation.group.id;
displayName = conversation.group.label;
}
return (
<ChatConversation
key={key}
selected={selectedRecord}
onClick={() => {
setSearchHeight();
showMessages();
if (entityType === 'contact' && setSelectedContactId) {
setSelectedContactId(conversation.contact.id);
} else if (entityType === 'collection' && setSelectedCollectionId) {
setSelectedCollectionId(conversation.group.id);
}
}}
index={index}
contactId={entityId}
entityType={entityType}
contactName={displayName}
lastMessage={lastMessage}
senderLastMessage={senderLastMessage}
contactStatus={contactStatus}
contactBspStatus={contactBspStatus}
contactIsOrgRead={contactIsOrgRead}
/>
);
});
}
if (!conversationList) {
conversationList = (
<p data-testid="empty-result" className={styles.EmptySearch}>
{t(`Sorry, no results found!
Please try a different search.`)}
</p>
);
}
const loadMoreMessages = () => {
setShowLoading(true);
// load more for multi search
if (searchVal && !selectedCollectionId) {
const variables = filterSearch();
variables.messageOpts = {
limit: DEFAULT_MESSAGE_LOADMORE_LIMIT,
offset: conversations.messages.length,
order: 'ASC',
};
getLoadMoreFilterSearch({
variables,
});
} else {
let filter: any = {};
// for saved search use filter value of selected search
if (savedSearchCriteria) {
const variables = JSON.parse(savedSearchCriteria);
filter = variables.filter;
}
if (searchVal) {
filter = { term: searchVal };
}
// Adding appropriate data if selected tab is collection
if (selectedCollectionId) {
filter = { searchGroup: true };
if (searchVal) {
filter.groupLabel = searchVal;
}
}
const conversationLoadMoreVariables = {
contactOpts: {
limit: DEFAULT_CONTACT_LOADMORE_LIMIT,
offset: loadingOffset,
},
filter,
messageOpts: {
limit: DEFAULT_MESSAGE_LIMIT,
},
};
loadMoreConversations({
variables: conversationLoadMoreVariables,
});
}
};
const showLatestContact = () => {
const container: any = document.querySelector('.contactsContainer');
if (container) {
container.scrollTop = 0;
}
};
let scrollTopStyle = selectedContactId
? styles.ScrollToTopContacts
: styles.ScrollToTopCollections;
scrollTopStyle = entityType === 'savedSearch' ? styles.ScrollToTopSearches : scrollTopStyle;
const scrollToTop = (
<div
className={scrollTopStyle}
onClick={showLatestContact}
onKeyDown={showLatestContact}
aria-hidden="true"
>
{t('Go to top')}
<KeyboardArrowUpIcon />
</div>
);
const loadMore = (
<div className={styles.LoadMore}>
{showLoading || loadingSearch ? (
<CircularProgress className={styles.Progress} />
) : (
<div
onClick={loadMoreMessages}
onKeyDown={loadMoreMessages}
className={styles.LoadMoreButton}
aria-hidden="true"
>
{t('Load more')}
</div>
)}
</div>
);
const entityStyles: any = {
contact: styles.ChatListingContainer,
collection: styles.CollectionListingContainer,
savedSearch: styles.SaveSearchListingContainer,
};
const entityStyle = entityStyles[entityType];
return (
<Container className={`${entityStyle} contactsContainer`} disableGutters>
{showJumpToLatest && !showLoading ? scrollToTop : null}
<List className={styles.StyledList}>
{conversationList}
{showLoadMore &&
conversations &&
(conversations.length > DEFAULT_CONTACT_LIMIT - 1 ||
conversations.messages?.length > DEFAULT_MESSAGE_LIMIT - 1)
? loadMore
: null}
</List>
</Container>
);
}