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