@apollo/client#useApolloClient TypeScript Examples

The following examples show how to use @apollo/client#useApolloClient. 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: logout.tsx    From bouncecode-cms with GNU General Public License v3.0 6 votes vote down vote up
function SignInPage() {
  const client = useApolloClient();

  useEffect(() => {
    resetToken(client);
    Router.push('/');
  }, []);

  return <div />;
}
Example #2
Source File: useUserTableDataCallback.tsx    From bouncecode-cms with GNU General Public License v3.0 6 votes vote down vote up
useUserTableDataCallback = (
  options: Partial<QueryOptions<OperationVariables>> = {},
) => {
  const client = useApolloClient();
  // const { enqueueSnackbar } = useSnackbar();

  return useCallback<ITableDataCallback>(async query => {
    const {data} = await client.query<UsersQuery>({
      ...options,
      query: UsersDocument,
      variables: {
        ...options.variables,
        take: query.pageSize,
        skip: query.page * query.pageSize,
      },
      // onError: (e) => {
      //   console.error(e);
      //   enqueueSnackbar(e.message, { variant: "error" });
      // },
    });

    console.log(data?.users);

    return {
      data: data?.users.map(user => ({...user})) || [],
      page: query.page,
      totalCount: 100, // TODO: totalCount
    };
  }, []);
}
Example #3
Source File: SignIn.tsx    From bouncecode-cms with GNU General Public License v3.0 6 votes vote down vote up
function SignIn() {
  const client = useApolloClient();
  const {enqueueSnackbar} = useSnackbar();

  const [signInMutation] = useCreateTokenMutation({
    onCompleted: data => {
      const {access_token, refresh_token} = data.createToken;
      storeToken(client, {access_token, refresh_token});
      enqueueSnackbar('로그인 했습니다.', {variant: 'success'});
      Router.push('/dashboard');
    },
    onError: e => {
      console.error(e);
      enqueueSnackbar(e.message, {variant: 'error'});
    },
  });

  const onSubmit = async values => {
    return signInMutation({
      variables: {
        data: values,
      },
    });
  };

  return <SignInView onSubmit={onSubmit} />;
}
Example #4
Source File: useFragmentTypePolicyFieldName.ts    From apollo-cache-policies with Apache License 2.0 6 votes vote down vote up
useFragmentTypePolicyFieldName = (): string => {
  const { current: fieldName } = useRef(generateFragmentFieldName());
  const client = useApolloClient();

  useEffect(() =>
    // @ts-ignore After the component using the hook is torn down, remove the dynamically added type policy
    // for this hook from the type policies list.
    () => delete (client.cache as InvalidationPolicyCache).policies.typePolicies.Query.fields[fieldName],
    [],
  );

  return fieldName;
}
Example #5
Source File: index.tsx    From nextjs-hasura-fullstack with MIT License 5 votes vote down vote up
IndexPage: NextPageWithLayout<IndexPageProps> = ({ session }) => {
  const apolloClient = useApolloClient()

  const authButtonNode = () => {
    if (session) {
      apolloClient.resetStore()
      return (
        <Link href="/api/auth/signout">
          <Button
            variant="light"
            onClick={(e) => {
              e.preventDefault()
              signOut()
            }}
          >
            Sign Out
          </Button>
        </Link>
      )
    }

    return (
      <Link href="/api/auth/signin">
        <Button
          variant="light"
          onClick={(e) => {
            e.preventDefault()
            signIn()
          }}
        >
          Create an account
        </Button>
      </Link>
    )
  }

  return (
    <div className="py-6 mx-auto">
      <article className="mx-auto prose text-white lg:prose-xl">
        <h1 className={`!text-gray-100 text-center`}>
          Nextjs Hasura Fullstack
        </h1>
        <p className={`break-words`}>
          A boilerplate that uses{' '}
          <ItemLink href="https://hasura.io/">Hasura</ItemLink> and{' '}
          <ItemLink href="https://nextjs.org/">Next.js</ItemLink> to develop web
          applications This demo has been built using{' '}
          <ItemLink href="https://tailwindcss.com/">Tailwindcss</ItemLink>,{' '}
          <ItemLink href="https://next-auth.js.org/">NextAuth.js</ItemLink> and{' '}
          <ItemLink href="https://www.apollographql.com/docs/react/">
            ApolloClient
          </ItemLink>
        </p>
      </article>
      <div className={`mt-12`}>
        <Book />
      </div>
      <div className="mt-6 text-center">{authButtonNode()}</div>
    </div>
  )
}
Example #6
Source File: useMultiSelectionTable.ts    From jmix-frontend with Apache License 2.0 5 votes vote down vote up
export function useMultiSelectionTable<
  TEntity = unknown,
  TData extends Record<string, any> = Record<string, any>,
  TQueryVars extends ListQueryVars = ListQueryVars,
  TMutationVars extends HasId = HasId
>(
  options: MultiSelectionTableHookOptions<TEntity, TData, TQueryVars, TMutationVars>
): MultiSelectionTableHookResult<TEntity, TData, TQueryVars, TMutationVars> {
  const entityListData = useEntityList(options);
  const client = useApolloClient();
  const multiSelectionTableStore = useLocalObservable(() => new MultiSelectionTableStore());
  const intl = useIntl();

  type EntityListHookResultType = EntityListHookResult<TEntity, TData, TQueryVars, TMutationVars>;

  const handleSelectionChange: EntityListHookResultType['handleSelectionChange'] = useCallback(
    selectedEntityIds => multiSelectionTableStore.setSelectedEntityIds(selectedEntityIds),
    [multiSelectionTableStore]
  );

  const deleteSelectedEntities = useCallback(async () => {
    if (multiSelectionTableStore.selectedEntityIds != null) {
      const entitiesDeleteMutate = createDeleteMutationForSomeEntities(options.entityName, multiSelectionTableStore.selectedEntityIds);
      try {
        await client.mutate({mutation: entitiesDeleteMutate});
        message.success(intl.formatMessage({id: 'multiSelectionTable.delete.success'}));
        await entityListData.executeListQuery();
      } catch (error) {
        message.error(intl.formatMessage({id: 'multiSelectionTable.delete.error'}));
      }
    }
  }, [multiSelectionTableStore.selectedEntityIds, options.entityName, client, intl, entityListData]);

  const handleDeleteBtnClick = useCallback(() => {
    if (
      multiSelectionTableStore.selectedEntityIds != null
      && multiSelectionTableStore.selectedEntityIds.length > 0
    ) {
      modals.open({
        content: intl.formatMessage({id: 'multiSelectionTable.delete.areYouSure'}),
        okText: intl.formatMessage({id: "common.ok"}),
        cancelText: intl.formatMessage({id: "common.cancel"}),
        onOk: deleteSelectedEntities,
      });
    }
  }, [deleteSelectedEntities, intl, multiSelectionTableStore.selectedEntityIds]);
  
  return {
    ...entityListData,
    multiSelectionTableStore,
    handleSelectionChange,
    handleDeleteBtnClick
  };
}
Example #7
Source File: RedditAuth.tsx    From keycapsets.com with GNU General Public License v3.0 5 votes vote down vote up
function RedditAuth(props: RedditAuthProps): JSX.Element {
    const { text, disabled, asLink = false } = props;
    const router: NextRouter = useRouter();
    const client = useApolloClient();
    const setUser = useStore((state) => state.setUser);

    useEffect(() => {
        const hash = window.location.hash;
        if (hash !== '') {
            const fragments = router.asPath
                .split('#')[1]
                .split('&')
                .reduce<Record<string, string>>((res, fragment) => {
                    const [key, value] = fragment.split('=');
                    return {
                        ...res,
                        [key]: value,
                    };
                }, {});

            getAccesToken(fragments.access_token, fragments.state);
        }
    }, [router.query]);

    async function getAccesToken(token: string, state: unknown) {
        const {
            data: { redditLogin },
        } = await client.mutate({
            mutation: REDDIT_LOGIN,
            variables: {
                token,
            },
        });
        setUser(redditLogin?.user);
        loginUser(redditLogin);
        const routes = {
            next: `${router.query.next}`,
            edit: '/user/edit',
            home: '/',
        };
        const route = router.query.next !== undefined ? 'next' : redditLogin.firstLogin ? 'edit' : 'home';
        console.log(redditLogin, 'after login route...', route);
        router.push(routes[route]);
    }

    return asLink ? (
        <a onClick={handleRedditAuth}>
            <RedditIcon variant="dark" />
            {text}
        </a>
    ) : (
        <Button variant="primary" size="md" onClick={handleRedditAuth} isDisabled={disabled}>
            <RedditIcon variant="white" size={16} />
            {text}
        </Button>
    );
}
Example #8
Source File: GoogleAuth.tsx    From keycapsets.com with GNU General Public License v3.0 5 votes vote down vote up
function GoogleAuth(props: GoogleAuthProps): JSX.Element {
    const { text, disabled, asLink = false, isLogginOut = false } = props;
    const client = useApolloClient();
    const router = useRouter();
    const setUser = useStore((state) => state.setUser);

    async function success(response: GoogleLoginResponse) {
        try {
            const {
                data: { googleLogin },
            } = await client.mutate({
                mutation: GOOGLE_LOGIN,
                variables: {
                    token: response.tokenId,
                },
            });
            setUser(googleLogin.user);
            loginUser(googleLogin);
            const routes = {
                next: `${router.query.next}`,
                edit: '/user/edit?message=Welcome to Keycapsets!',
                home: '/',
            };
            const route = router.query.next !== undefined ? 'next' : googleLogin.firstLogin ? 'edit' : 'home';
            router.push(routes[route]);
        } catch (err) {
            console.error(err);
        }
    }

    function error(response: GoogleLoginResponse) {
        console.error('error', response);
    }

    function logout() {
        logoutUser();
    }

    if (isLogginOut) {
        return <GoogleLogout clientId={CLIENT_ID} buttonText="Logout" onLogoutSuccess={logout} />;
    }

    return (
        <GoogleLogin
            clientId={CLIENT_ID}
            onSuccess={success}
            onFailure={error}
            responseType="id_token"
            cookiePolicy={'single_host_origin'}
            disabled={disabled}
            render={(renderProps) =>
                asLink ? (
                    <a onClick={renderProps.onClick}>
                        <GoogleIcon variant="dark" />
                        {text}
                    </a>
                ) : (
                    <Button onClick={renderProps.onClick} variant="primary" size="md" className="google-button" isDisabled={disabled}>
                        <GoogleIcon variant="white" size={16} />
                        {text}
                    </Button>
                )
            }
        />
    );
}
Example #9
Source File: Logout.tsx    From glific-frontend with GNU Affero General Public License v3.0 5 votes vote down vote up
Logout: React.SFC<LogoutProps> = ({ match }) => {
  const { setAuthenticated } = useContext(SessionContext);
  const [redirect, setRedirect] = useState(false);
  const client = useApolloClient();
  const { t } = useTranslation();
  const location = useLocation();

  // let's notify the backend when user logs out
  const userLogout = () => {
    // get the auth token from session
    axios.defaults.headers.common.authorization = getAuthSession('access_token');
    axios.delete(USER_SESSION);
  };

  const handleLogout = () => {
    userLogout();
    // clear local storage auth session
    clearAuthSession();

    // update the context
    setAuthenticated(false);

    // clear local storage user session
    clearUserSession();

    // clear role & access permissions
    resetRolePermissions();

    // clear local storage list sort session
    clearListSession();

    // clear apollo cache
    client.clearStore();

    setRedirect(true);
  };

  useEffect(() => {
    // if user click on logout menu
    if (match.params.mode === 'user') {
      handleLogout();
    }
  }, []);

  const dialog = (
    <DialogBox
      title={t('Your session has expired!')}
      buttonOk={t('Login')}
      handleOk={() => handleLogout()}
      handleCancel={() => handleLogout()}
      skipCancel
      alignButtons="center"
    >
      <div style={divStyle}>{t('Please login again to continue.')}</div>
    </DialogBox>
  );

  if (redirect) {
    return <Redirect to={{ pathname: '/login', state: location.state }} />;
  }

  return dialog;
}
Example #10
Source File: NavBar.tsx    From lireddit with MIT License 5 votes vote down vote up
NavBar: React.FC<NavBarProps> = ({}) => {
  const router = useRouter();
  const [logout, { loading: logoutFetching }] = useLogoutMutation();
  const apolloClient = useApolloClient();
  const { data, loading } = useMeQuery({
    skip: isServer(),
  });

  let body = null;

  // data is loading
  if (loading) {
    // user not logged in
  } else if (!data?.me) {
    body = (
      <>
        <NextLink href="/login">
          <Link mr={2}>login</Link>
        </NextLink>
        <NextLink href="/register">
          <Link>register</Link>
        </NextLink>
      </>
    );
    // user is logged in
  } else {
    body = (
      <Flex align="center">
        <NextLink href="/create-post">
          <Button as={Link} mr={4}>
            create post
          </Button>
        </NextLink>
        <Box mr={2}>{data.me.username}</Box>
        <Button
          onClick={async () => {
            await logout();
            await apolloClient.resetStore();
          }}
          isLoading={logoutFetching}
          variant="link"
        >
          logout
        </Button>
      </Flex>
    );
  }

  return (
    <Flex zIndex={1} position="sticky" top={0} bg="tan" p={4}>
      <Flex flex={1} m="auto" align="center" maxW={800}>
        <NextLink href="/">
          <Link>
            <Heading>LiReddit</Heading>
          </Link>
        </NextLink>
        <Box ml={"auto"}>{body}</Box>
      </Flex>
    </Flex>
  );
}
Example #11
Source File: MainLayout.tsx    From amplication with Apache License 2.0 5 votes vote down vote up
Menu = ({ children }: MenuProps) => {
  const history = useHistory();
  const { trackEvent } = useTracking();

  const apolloClient = useApolloClient();

  const handleProfileClick = useCallback(() => {
    history.push("/user/profile");
  }, [history]);

  const handleSignOut = useCallback(() => {
    /**@todo: sign out on server */
    unsetToken();
    apolloClient.clearStore();

    history.replace("/");
  }, [history, apolloClient]);

  const handleSupportClick = useCallback(() => {
    trackEvent({
      eventName: "supportButtonClick",
    });
  }, [trackEvent]);

  return (
    <div className={classNames("main-layout__menu")}>
      <div className="main-layout__menu__wrapper">
        <div className="main-layout__menu__wrapper__main-menu">
          <div className="logo-container">
            <Link to="/" className="logo-container__logo">
              <Icon icon="logo" className="main-logo" />
              <LogoTextual />
            </Link>
          </div>

          <div className="menu-container">
            <CommandPalette
              trigger={
                <MenuItem
                  title="Search"
                  icon="search_outline"
                  overrideTooltip={`Search (${isMacOs ? "⌘" : "Ctrl"}+Shift+P)`}
                />
              }
            />
            {children}
          </div>
          <div className="bottom-menu-container">
            <DarkModeToggle />
            <Popover
              className="main-layout__menu__popover"
              content={<SupportMenu />}
              onOpen={handleSupportClick}
              placement="right"
            >
              <MenuItem
                icon="help_outline"
                hideTooltip
                title="Help and support"
              />
            </Popover>
            <MenuItem
              title="Profile"
              icon="plus"
              hideTooltip
              onClick={handleProfileClick}>
              <UserBadge />
            </MenuItem>
            <MenuItem
              title="Sign Out"
              icon="log_out_outline"
              onClick={handleSignOut}
            />
          </div>
        </div>
        <FixedMenuPanel.Target className="main-layout__menu__wrapper__menu-fixed-panel" />
      </div>
    </div>
  );
}
Example #12
Source File: useCreateEditMember.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
useCreateEditMemberForm = ({ prevHandle }: UseCreateEditMemberFormArgs) => {
  const client = useApolloClient()

  const debouncedAvatarValidation = useRef(debouncePromise(imageUrlValidation, 500))
  const debouncedHandleUniqueValidation = useRef(
    debouncePromise(async (value: string, prevValue?: string) => {
      if (value === prevValue) {
        return true
      }
      const {
        data: { membershipByUniqueInput },
      } = await client.query<GetMembershipQuery, GetMembershipQueryVariables>({
        query: GetMembershipDocument,
        variables: { where: { handle: value } },
      })
      return !membershipByUniqueInput
    }, 500)
  )

  const schema = z.object({
    handle: z
      .string()
      .nonempty({ message: 'Member handle cannot be empty' })
      .min(5, {
        message: 'Member handle must be at least 5 characters',
      })
      .max(40, {
        message: `Member handle cannot be longer than 40 characters`,
      })
      .refine((val) => (val ? MEMBERSHIP_NAME_PATTERN.test(val) : true), {
        message: 'Member handle may contain only lowercase letters, numbers and underscores',
      })
      .refine((val) => debouncedHandleUniqueValidation.current(val, prevHandle), {
        message: 'Member handle already in use',
      }),
    avatar: z
      .string()
      .max(400)
      .refine((val) => (val ? URL_PATTERN.test(val) : true), { message: 'Avatar URL must be a valid url' })
      .refine(
        (val) => {
          if (!val) return true
          return debouncedAvatarValidation.current(val)
        },
        { message: 'Image not found' }
      )
      .nullable(),
    about: z.string().max(1000, { message: 'About cannot be longer than 1000 characters' }).nullable(),
  })

  const {
    register,
    handleSubmit,
    setFocus,
    getValues,
    reset,
    watch,
    formState: { errors, isDirty, isValid, dirtyFields, isValidating },
  } = useForm<Inputs>({
    mode: 'onChange',
    resolver: zodResolver(schema),
    shouldFocusError: true,
  })

  return {
    register,
    handleSubmit,
    getValues,
    reset,
    watch,
    setFocus,
    errors,
    isDirty,
    isValid,
    isValidating,
    dirtyFields,
  }
}
Example #13
Source File: NftSaleBottomDrawer.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
NftSaleBottomDrawer: React.FC = () => {
  const { currentAction, currentNftId, closeNftAction } = useNftActions()
  const [formStatus, setFormStatus] = useState<NftFormStatus | null>(null)

  const { activeMemberId } = useUser()
  const { joystream, proxyCallback } = useJoystream()
  const handleTransaction = useTransaction()
  const client = useApolloClient()
  const { displaySnackbar } = useSnackbar()

  const isOpen = currentAction === 'putOnSale'

  const handleSubmit = useCallback(
    async (data: NftFormData) => {
      if (!joystream) {
        ConsoleLogger.error('No Joystream instance! Has webworker been initialized?')
        return
      }

      if (!currentNftId || !activeMemberId) {
        ConsoleLogger.error('Missing NFT or member ID')
        return
      }

      const refetchData = async () => {
        await client.query<GetNftQuery, GetNftQueryVariables>({
          query: GetNftDocument,
          variables: { id: currentNftId },
          fetchPolicy: 'network-only',
        })
      }

      const completed = await handleTransaction({
        txFactory: async (cb) =>
          (await joystream.extrinsics).putNftOnSale(currentNftId, activeMemberId, data, proxyCallback(cb)),
        onTxSync: refetchData,
      })
      if (completed) {
        displaySnackbar({
          customId: currentNftId,
          title: 'NFT put on sale successfully',
          iconType: 'success',
          timeout: SUCCESS_SNACKBAR_TIMEOUT,
          actionText: 'See details',
          onActionClick: () => openInNewTab(absoluteRoutes.viewer.video(currentNftId), true),
        })
        closeNftAction()
      }
    },
    [activeMemberId, client, closeNftAction, currentNftId, displaySnackbar, handleTransaction, joystream, proxyCallback]
  )

  const handleCancel = useCallback(() => {
    closeNftAction()
    setFormStatus(null)
  }, [closeNftAction])

  const actionBarProps: ActionBarProps = {
    variant: 'nft',
    primaryButton: {
      text: !formStatus?.canGoForward ? 'Start sale' : 'Next step',
      disabled: formStatus?.isDisabled,
      onClick: !formStatus?.canGoForward ? formStatus?.triggerSubmit : formStatus?.triggerGoForward,
    },
    secondaryButton: {
      text: !formStatus?.canGoBack ? 'Cancel' : 'Go back',
      onClick: !formStatus?.canGoBack ? handleCancel : formStatus?.triggerGoBack,
      disabled: false,
      visible: true,
    },
  }

  return (
    <BottomDrawer isOpen={isOpen} onClose={closeNftAction} actionBar={actionBarProps}>
      {isOpen && currentNftId && (
        <NftForm onSubmit={handleSubmit} videoId={currentNftId} setFormStatus={setFormStatus} />
      )}
    </BottomDrawer>
  )
}
Example #14
Source File: useDeleteVideo.ts    From atlas with GNU General Public License v3.0 5 votes vote down vote up
useDeleteVideo = () => {
  const { joystream, proxyCallback } = useJoystream()
  const handleTransaction = useTransaction()
  const { activeMemberId } = useAuthorizedUser()
  const removeAssetsWithParentFromUploads = useUploadsStore((state) => state.actions.removeAssetsWithParentFromUploads)
  const [openDeleteVideoDialog, closeDeleteVideoDialog] = useConfirmationModal()

  const client = useApolloClient()

  const confirmDeleteVideo = useCallback(
    async (videoId: string, onTxSync?: () => void) => {
      if (!joystream) {
        return
      }

      handleTransaction({
        txFactory: async (updateStatus) =>
          (await joystream.extrinsics).deleteVideo(videoId, activeMemberId, proxyCallback(updateStatus)),
        onTxSync: async () => {
          removeVideoFromCache(videoId, client)
          removeAssetsWithParentFromUploads('video', videoId)
          onTxSync?.()
        },
      })
    },
    [activeMemberId, client, handleTransaction, joystream, proxyCallback, removeAssetsWithParentFromUploads]
  )

  return useCallback(
    (videoId: string, onDeleteVideo?: () => void) => {
      openDeleteVideoDialog({
        title: 'Delete this video?',
        description:
          'You will not be able to undo this. Deletion requires a blockchain transaction to complete. Currently there is no way to remove uploaded video assets.',
        primaryButton: {
          text: 'Delete video',
          variant: 'destructive',
          onClick: () => {
            confirmDeleteVideo(videoId, () => onDeleteVideo?.())
            closeDeleteVideoDialog()
          },
        },
        secondaryButton: {
          text: 'Cancel',
          onClick: () => {
            closeDeleteVideoDialog()
          },
        },
        iconType: 'warning',
      })
    },
    [closeDeleteVideoDialog, confirmDeleteVideo, openDeleteVideoDialog]
  )
}
Example #15
Source File: Header.tsx    From OpenVolunteerPlatform with MIT License 5 votes vote down vote up
Header: React.FC<{ title: string, backHref?: string, match: any }> = ({ title, backHref, match }) => {

  const { url } = match;
  const client = useApolloClient();
  const { keycloak } = useContext(AuthContext);
  const [showToast, setShowToast] = useState(false);

  const handleLogout = async () => {
    await logout({ keycloak, client });
  }

  // if keycloak is not configured, don't display logout and
  // profile icons. Only show login and profile icons on the home
  // screen
  const buttons = (!keycloak || url !== '/actions') ? <></> : (
    <IonButtons slot="end">
      <Link to="/profile">
        <IonButton>
          <IonIcon slot="icon-only" icon={person} />
        </IonButton>
      </Link>
      <IonButton onClick={handleLogout}>
        <IonIcon slot="icon-only" icon={exit} />
      </IonButton>
    </IonButtons>
  );

  return (
    <>
      <IonHeader>
        <IonToolbar>
          {
            url !== '/actions' &&
            <Link
              to={backHref as string}
              slot="start"
              role="button"
            >
              <IonButtons>
                <IonButton>
                  <IonIcon icon={arrowBack} />
                </IonButton>
              </IonButtons>
            </Link>
          }
          <IonTitle>{title}</IonTitle>
          {buttons}
        </IonToolbar>
      </IonHeader>
      <IonToast
        isOpen={showToast}
        onDidDismiss={() => setShowToast(false)}
        message="You are currently offline. Unable to logout."
        position="top"
        color="danger"
        duration={1000}
      />
    </>
  );
}
Example #16
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>
  );
}
Example #17
Source File: MemberComboBox.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
MemberComboBox: React.FC<MemberComboBoxProps> = ({
  selectedMembers,
  className,
  onSelectMember,
  onRemoveMember,
  disabled,
  error,
  helperText,
}) => {
  const [isLoading, setIsLoading] = useState(false)
  const [members, setMembers] = useState<BasicMembershipFieldsFragment[]>([])
  const client = useApolloClient()
  const [isError, setIsError] = useState(false)

  const debounceFetchMembers = useRef(
    debouncePromise(async (val?: string) => {
      if (!val) {
        setMembers([])
        return
      }
      try {
        const {
          data: { memberships },
        } = await client.query<GetMembershipsQuery, GetMembershipsQueryVariables>({
          query: GetMembershipsDocument,
          variables: { where: { handle_startsWith: val } },
        })
        setIsLoading(false)

        setMembers(memberships)
      } catch (error) {
        SentryLogger.error('Failed to fetch memberships', 'WhiteListTextField', error)
        setIsError(true)
      }
    }, 500)
  )

  const handleSelect = (item?: BasicMembershipFieldsFragment) => {
    if (!item) {
      return
    }
    onSelectMember?.(item)
    setMembers([])
  }

  const handleDeleteClick = (memberId: string) => {
    if (memberId) {
      onRemoveMember?.(memberId)
    }
  }

  const selectedMembersLookup = selectedMembers ? createLookup(selectedMembers) : {}

  const dropdownItems = members
    .map((member) => {
      return {
        ...member,
        nodeStart: <AvatarWithResolvedAsset member={member} />,
        label: member.handle,
      }
    })
    .filter((member) => !selectedMembersLookup[member.id])

  const notFoundNode = {
    label: `We couldn't find this member. Please check if spelling is correct.`,
    nodeStart: <SvgActionCancel />,
  }

  return (
    <div className={className}>
      <ComboBox<BasicMembershipFieldsFragment>
        items={dropdownItems}
        disabled={disabled}
        placeholder={selectedMembers.length ? 'Enter another member handle' : 'Enter member handle'}
        notFoundNode={notFoundNode}
        resetOnSelect
        loading={isLoading}
        error={isError || error}
        onSelectedItemChange={handleSelect}
        helperText={isError ? 'Something went wrong' : helperText}
        onInputValueChange={(val) => {
          setIsError(false)
          setIsLoading(true)
          debounceFetchMembers.current(val)
        }}
      />
      <MemberBadgesWrapper>
        {selectedMembers.length > 0 && <StyledSelectedText variant="t200-strong">Selected: </StyledSelectedText>}
        {selectedMembers.map((member) => (
          <MemberBadgeWithResolvedAsset
            key={member.id}
            member={member}
            onDeleteClick={() => handleDeleteClick(member.id)}
          />
        ))}
      </MemberBadgesWrapper>
    </div>
  )
}
Example #18
Source File: index.tsx    From tinyhouse with MIT License 4 votes vote down vote up
export function Login({ setViewer }: LoginProps) {
    const client = useApolloClient();
    const [logIn, { loading: logInLoading, error: loginError }] = useMutation<
        LogInData,
        LogInVariables
    >(LOG_IN, {
        onCompleted: (data) => {
            if (data && data.logIn && data.logIn.token) {
                setViewer(data.logIn);
                sessionStorage.setItem("token", data.logIn.token);
                displaySuccessNotification("You've successfully logged in!");
                const { id: viewerId } = data.logIn;
                navigate(`/user/${viewerId}`);
            }
        },
    });
    const logInRef = useRef(logIn);
    const navigate = useNavigate();

    useEffect(() => {
        const code = new URL(window.location.href).searchParams.get("code");
        if (code) {
            logInRef.current({
                variables: {
                    input: { code },
                },
            });
        }
    }, []);
    const handleAuthorize = async () => {
        try {
            const { data } = await client.query<AuthUrlData>({
                query: AUTH_URL,
            });
            if (data) {
                window.location.href = data.authUrl;
            }
        } catch {
            displayErrorMessage(
                "Sorry! We weren't able to log you in. Please try again later!"
            );
        }
    };
    if (logInLoading) {
        return (
            <Content className="log-in">
                <Spin size="large" tip="Logging you in..." />
            </Content>
        );
    }

    const logInErrorBanner = loginError ? (
        <ErrorBanner description="Sorry! We weren't able to log you in. Please try again later!" />
    ) : null;
    return (
        <Content className="log-in">
            {logInErrorBanner}
            <Card className="log-in-card">
                <div className="log-in-card__intro-title">
                    <Title level={3} className="log-in-card__intro-title">
                        <span role="img" aria-label="wave">
                            ?
                        </span>
                    </Title>
                    <Title level={3} className="log-in-card__intro-title">
                        Log in to TinyHouse!
                    </Title>
                    <Text>Sign in with Google to book available rentals!</Text>
                </div>
                <button
                    className="log-in-card__google-button"
                    onClick={handleAuthorize}
                >
                    <img
                        alt="Google logo"
                        className="log-in-card__google-button-logo"
                        src={googleLogo}
                    />
                    <span className="log-in-card__google-button-text">
                        Sign in with Google!
                    </span>
                </button>
                <Text type="secondary">
                    Note: By signing in, you'll be redirected to the Google
                    consent form to sign in with your Google account.
                </Text>
            </Card>
        </Content>
    );
}
Example #19
Source File: Providers.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
Providers: React.SFC<ProvidersProps> = ({ match }) => {
  const type = match.params.type ? match.params.type : null;
  const [credentialId, setCredentialId] = useState(null);
  const client = useApolloClient();
  const { t } = useTranslation();

  const param = { params: { id: credentialId, shortcode: type } };
  const [stateValues, setStateValues] = useState({});

  const [formFields, setFormFields] = useState([]);
  const states: any = {};

  const [keys, setKeys] = useState({});
  const [secrets, setSecrets] = useState({});

  const { data: providerData } = useQuery(GET_PROVIDERS, {
    variables: { filter: { shortcode: type } },
  });
  const { data: credential, loading } = useQuery(GET_CREDENTIAL, {
    variables: { shortcode: type },
    fetchPolicy: 'no-cache', // This is required to restore the data after save
  });

  const setCredential = (item: any) => {
    const keysObj = JSON.parse(item.keys);
    const secretsObj = JSON.parse(item.secrets);
    const fields: any = {};
    Object.assign(fields, keysObj);
    Object.assign(fields, secretsObj);
    Object.keys(fields).forEach((key) => {
      // restore value of the field
      states[key] = fields[key];
    });
    states.isActive = item.isActive;
    setStateValues(states);
  };

  if (credential && !credentialId) {
    const data = credential.credential.credential;
    if (data) {
      // to get credential data
      setCredentialId(data.id);
    }
  }

  const setPayload = (payload: any) => {
    let object: any = {};
    const secretsObj: any = {};
    const keysObj: any = {};
    Object.keys(secrets).forEach((key) => {
      if (payload[key]) {
        secretsObj[key] = payload[key];
      }
    });
    Object.keys(keys).forEach((key) => {
      if (payload[key]) {
        keysObj[key] = payload[key];
      }
    });
    object = {
      shortcode: type,
      isActive: payload.isActive,
      keys: JSON.stringify(keysObj),
      secrets: JSON.stringify(secretsObj),
    };
    return object;
  };

  const resetValidation = () => {
    validation = {};
    FormSchema = Yup.object().shape(validation);
  };

  const addValidation = (fields: any, key: string) => {
    validation[key] = Yup.string()
      .nullable()
      .when('isActive', {
        is: true,
        then: Yup.string().nullable().required(`${fields[key].label} is required.`),
      });
    FormSchema = Yup.object().shape(validation);
  };

  const addField = (fields: any) => {
    // reset validation to empty
    resetValidation();

    const formField: any = [
      {
        component: Checkbox,
        name: 'isActive',
        title: (
          <Typography variant="h6" style={{ color: '#073f24' }}>
            {t('Is active?')}
          </Typography>
        ),
      },
    ];
    const defaultStates: any = {};
    Object.keys(fields).forEach((key) => {
      Object.assign(defaultStates, { [key]: fields[key].default });
      const field = {
        component: Input,
        name: key,
        type: 'text',
        placeholder: fields[key].label,
        disabled: fields[key].view_only,
      };
      formField.push(field);

      // create validation object for field
      addValidation(fields, key);

      // add dafault value for the field
      states[key] = fields[key].default;
    });
    setStateValues(states);
    setFormFields(formField);
  };
  useEffect(() => {
    if (providerData) {
      providerData.providers.forEach((provider: any) => {
        const providerKeys = JSON.parse(provider.keys);
        const providerSecrets = JSON.parse(provider.secrets);
        const fields: any = {};
        Object.assign(fields, providerKeys);
        Object.assign(fields, providerSecrets);

        addField(fields);
        setKeys(providerKeys);
        setSecrets(providerSecrets);
      });
    }
  }, [providerData]);

  const saveHandler = (data: any) => {
    if (data)
      // Update the details of the cache. This is required at the time of restoration
      client.writeQuery({
        query: GET_CREDENTIAL,
        variables: { shortcode: type },
        data: data.updateCredential,
      });
  };

  if (!providerData || loading) return <Loading />;

  const title = providerData.providers[0].name;

  return (
    <FormLayout
      backLinkButton={{ text: t('Back to settings'), link: '/settings' }}
      {...queries}
      title={title}
      match={param}
      states={stateValues}
      setStates={setCredential}
      validationSchema={FormSchema}
      setPayload={setPayload}
      listItemName="Settings"
      dialogMessage=""
      formFields={formFields}
      redirectionLink="settings"
      cancelLink="settings"
      linkParameter="id"
      listItem="credential"
      icon={SettingIcon}
      languageSupport={false}
      type="settings"
      redirect
      afterSave={saveHandler}
    />
  );
}
Example #20
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 #21
Source File: NotificationList.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
NotificationList: React.SFC<NotificationListProps> = () => {
  const client = useApolloClient();
  const [open, setOpen] = useState(false);
  const [text, setText] = useState<any>();
  const { t } = useTranslation();
  const history = useHistory();
  const [filters, setFilters] = useState<any>({
    Critical: true,
    Warning: false,
  });

  const menuRef = useRef(null);
  let filterValue: any = '';

  const [markNotificationAsRead] = useMutation(MARK_NOTIFICATIONS_AS_READ, {
    onCompleted: (data) => {
      if (data.markNotificationAsRead) {
        client.writeQuery({
          query: GET_NOTIFICATIONS_COUNT,
          variables: {
            filter: {
              is_read: false,
              severity: 'critical',
            },
          },
          data: { countNotifications: 0 },
        });
      }
    },
  });

  useEffect(() => {
    setTimeout(() => {
      markNotificationAsRead();
    }, 1000);
  }, []);

  const setDialog = (id: any, item: any) => {
    if (item.category === 'Message') {
      const chatID = JSON.parse(item.entity).id;
      history.push({ pathname: `/chat/${chatID}` });
    } else if (item.category === 'Flow') {
      const uuidFlow = JSON.parse(item.entity).flow_uuid;
      history.push({ pathname: `/flow/configure/${uuidFlow}` });
    } else {
      // this is item.category == Partner
      // need to figure out what should be done
    }
  };

  const additionalAction = [
    {
      icon: <ArrowForwardIcon className={styles.RedirectArrow} />,
      parameter: 'id',
      label: 'Check',
      dialog: setDialog,
    },
  ];
  const getCroppedText = (croppedtext: string) => {
    if (!croppedtext) {
      return <div className={styles.TableText}>NULL</div>;
    }

    const entityObj = JSON.parse(croppedtext);

    const Menus = [
      {
        title: t('Copy text'),
        icon: <img src={CopyIcon} alt="copy" />,
        onClick: () => {
          copyToClipboard(croppedtext);
        },
      },
      {
        title: t('View'),
        icon: <ViewIcon />,
        onClick: () => {
          setText(croppedtext);
          setOpen(true);
        },
      },
    ];

    return (
      <Menu menus={Menus}>
        <div
          className={styles.CroppedText}
          data-testid="NotificationRowMenu"
          ref={menuRef}
          aria-hidden="true"
        >
          {entityObj.name ? (
            <span>
              Contact: {entityObj.name}
              <br />
              {croppedtext.slice(0, 25)}...
            </span>
          ) : (
            `${croppedtext.slice(0, 25)}...`
          )}
        </div>
      </Menu>
    );
  };

  const getColumns = ({ category, entity, message, severity, updatedAt, isRead }: any) => ({
    isRead: getDot(isRead),
    updatedAt: getTime(updatedAt),
    category: getText(category),
    severity: getText(severity.replace(/"/g, '')),
    entity: getCroppedText(entity),
    message: getText(message),
  });

  const handleClose = () => {
    setOpen(false);
  };

  const columnNames = ['', 'TIMESTAMP', 'CATEGORY', 'SEVERITY', 'ENTITY', 'MESSAGE'];

  const columnAttributes = {
    columnNames,
    columns: getColumns,
    columnStyles,
  };

  const popover = (
    <Popover open={open} anchorEl={menuRef.current} onClose={handleClose}>
      <div className={styles.PopoverText}>
        <pre>{JSON.stringify(text ? JSON.parse(text) : '', null, 2)}</pre>
      </div>
      <div className={styles.PopoverActions}>
        <span
          onClick={() => copyToClipboard(text)}
          aria-hidden="true"
          data-testid="copyToClipboard"
        >
          <img src={CopyIcon} alt="copy" />
          {t('Copy text')}
        </span>
        <Button variant="contained" color="default" onClick={handleClose}>
          {t('Done')}
        </Button>
      </div>
    </Popover>
  );

  const severityList = ['Critical', 'Warning'];

  const handleCheckedBox = (event: any) => {
    setFilters({ ...filters, [event.target.name]: event.target.checked });
  };

  const filterName = Object.keys(filters).filter((k) => filters[k] === true);
  if (filterName.length === 1) {
    [filterValue] = filterName;
  }

  const filterOnSeverity = (
    <div className={styles.Filters}>
      {severityList.map((label, index) => {
        const key = index;
        return (
          <FormControlLabel
            key={key}
            control={
              <Checkbox
                checked={filters[label]}
                color="primary"
                onChange={handleCheckedBox}
                name={severityList[index]}
              />
            }
            label={severityList[index]}
            classes={{
              label: styles.FilterLabel,
            }}
          />
        );
      })}
    </div>
  );
  return (
    <div>
      <List
        title="Notifications"
        listItem="notifications"
        listItemName="notification"
        pageLink="notifications"
        listIcon={notificationIcon}
        searchParameter={['message']}
        button={{ show: false }}
        dialogMessage=""
        {...queries}
        restrictedAction={restrictedAction}
        additionalAction={additionalAction}
        {...columnAttributes}
        removeSortBy={[t('Entity'), t('Severity'), t('Category')]}
        filters={{ severity: filterValue }}
        filterList={filterOnSeverity}
        listOrder="desc"
      />
      {popover}
    </div>
  );
}
Example #22
Source File: MyAccount.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
MyAccount: React.SFC<MyAccountProps> = () => {
  // set the validation / errors / success message
  const [toastMessageInfo, setToastMessageInfo] = useState({ message: '', severity: '' });

  // set the trigger to show next step
  const [showOTPButton, setShowOTPButton] = useState(true);

  // set redirection to chat
  const [redirectToChat, setRedirectToChat] = useState(false);

  // handle visibility for the password field
  const [showPassword, setShowPassword] = useState(false);

  // user language selection
  const [userLanguage, setUserLanguage] = useState('');

  const [message, setMessage] = useState<string>('');

  const client = useApolloClient();

  // get the information on current user
  const { data: userData, loading: userDataLoading } = useQuery(GET_CURRENT_USER);

  // get available languages for the logged in users organization
  const { data: organizationData, loading: organizationDataLoading } = useQuery(USER_LANGUAGES);

  const { t, i18n } = useTranslation();

  // set the mutation to update the logged in user password
  const [updateCurrentUser] = useMutation(UPDATE_CURRENT_USER, {
    onCompleted: (data) => {
      if (data.updateCurrentUser.errors) {
        if (data.updateCurrentUser.errors[0].message === 'incorrect_code') {
          setToastMessageInfo({ severity: 'error', message: t('Please enter a valid OTP') });
        } else {
          setToastMessageInfo({
            severity: 'error',
            message: t('Too many attempts, please retry after sometime.'),
          });
        }
      } else {
        setShowOTPButton(true);
        setToastMessageInfo({ severity: 'success', message });
      }
    },
  });

  // return loading till we fetch the data
  if (userDataLoading || organizationDataLoading) return <Loading />;

  // filter languages that support localization
  const languageOptions = organizationData.currentUser.user.organization.activeLanguages
    .filter((lang: any) => lang.localized)
    .map((lang: any) => {
      // restructure language array
      const lanObj = { id: lang.locale, label: lang.label };
      return lanObj;
    });

  // callback function to send otp to the logged user
  const sendOTPHandler = () => {
    // set the phone of logged in user that will be used to send the OTP
    const loggedInUserPhone = userData?.currentUser.user.phone;
    sendOTP(loggedInUserPhone)
      .then(() => {
        setShowOTPButton(false);
      })
      .catch(() => {
        setToastMessageInfo({
          severity: 'error',
          message: `Unable to send an OTP to ${loggedInUserPhone}.`,
        });
      });
  };

  // cancel handler if cancel is clicked
  const cancelHandler = () => {
    setRedirectToChat(true);
  };

  // save the form if data is valid
  const saveHandler = (item: any) => {
    setMessage(t('Password updated successfully!'));
    updateCurrentUser({
      variables: { input: item },
    });
  };

  const handlePasswordVisibility = () => {
    setShowPassword(!showPassword);
  };

  // callback function when close icon is clicked
  const closeToastMessage = () => {
    // reset toast information
    setToastMessageInfo({ message: '', severity: '' });
  };

  // set up toast message display, we use this for showing backend validation errors like
  // invalid OTP and also display success message on password update
  let displayToastMessage: any;
  if (toastMessageInfo.message.length > 0) {
    displayToastMessage = (
      <ToastMessage
        message={toastMessageInfo.message}
        severity={toastMessageInfo.severity === 'success' ? 'success' : 'error'}
        handleClose={closeToastMessage}
      />
    );
  }

  // setup form schema base on Yup
  const FormSchema = Yup.object().shape({
    otp: Yup.string().required(t('Input required')),
    password: Yup.string()
      .min(6, t('Password must be at least 8 characters long.'))
      .required(t('Input required')),
  });

  // for configuration that needs to be rendered
  const formFields = [
    {
      component: Input,
      type: 'otp',
      name: 'otp',
      placeholder: 'OTP',
      helperText: t('Please confirm the OTP received at your WhatsApp number.'),
      endAdornmentCallback: sendOTPHandler,
    },
    {
      component: Input,
      name: 'password',
      type: 'password',
      placeholder: t('Change Password'),
      endAdornmentCallback: handlePasswordVisibility,
      togglePassword: showPassword,
    },
  ];

  // redirect to chat
  if (redirectToChat) {
    return <Redirect to="/chat" />;
  }

  // build form fields
  let formFieldLayout: any;
  if (!showOTPButton) {
    formFieldLayout = formFields.map((field: any, index) => {
      const key = index;
      return (
        <React.Fragment key={key}>
          <Field key={key} {...field} />
        </React.Fragment>
      );
    });
  }

  // form component
  const form = (
    <Formik
      enableReinitialize
      initialValues={{ otp: '', password: '' }}
      validationSchema={FormSchema}
      onSubmit={(values, { resetForm }) => {
        saveHandler(values);
        resetForm();
      }}
    >
      {({ submitForm }) => (
        <Form className={styles.Form}>
          {displayToastMessage}
          {formFieldLayout}
          <div className={styles.Buttons}>
            {showOTPButton ? (
              <>
                <Button
                  variant="contained"
                  color="primary"
                  onClick={sendOTPHandler}
                  className={styles.Button}
                  data-testid="generateOTP"
                >
                  {t('Generate OTP')}
                </Button>
                <div className={styles.HelperText}>{t('To change first please generate OTP')}</div>
              </>
            ) : (
              <>
                <Button
                  variant="contained"
                  color="primary"
                  onClick={submitForm}
                  className={styles.Button}
                >
                  {t('Save')}
                </Button>
                <Button variant="contained" color="default" onClick={cancelHandler}>
                  {t('Cancel')}
                </Button>
              </>
            )}
          </div>
        </Form>
      )}
    </Formik>
  );

  // set only for the first time
  if (!userLanguage && userData.currentUser.user.language) {
    setUserLanguage(userData.currentUser.user.language.locale);
  }

  const changeLanguage = (event: any) => {
    setUserLanguage(event.target.value);

    // change the user interface
    i18n.changeLanguage(event.target.value);

    // get language id
    const languageID = organizationData.currentUser.user.organization.activeLanguages.filter(
      (lang: any) => lang.locale === event.target.value
    );

    setMessage(t('Language changed successfully!'));
    // update user's language
    updateCurrentUser({
      variables: { input: { languageId: languageID[0].id } },
    });

    // writing cache to restore value
    const userDataCopy = JSON.parse(JSON.stringify(userData));
    const language = languageID[0];
    userDataCopy.currentUser.user.language = language;

    client.writeQuery({
      query: GET_CURRENT_USER,
      data: userDataCopy,
    });
  };

  const languageField = {
    onChange: changeLanguage,
    value: userLanguage,
  };

  const languageSwitcher = (
    <div className={styles.Form}>
      <Dropdown
        options={languageOptions}
        placeholder={t('Available languages')}
        field={languageField}
      />
    </div>
  );

  return (
    <div className={styles.MyAccount} data-testid="MyAccount">
      <Typography variant="h5" className={styles.Title}>
        <IconButton disabled className={styles.Icon}>
          <UserIcon />
        </IconButton>
        {t('My Account')}
      </Typography>
      <Typography variant="h6" className={styles.Title}>
        {t('Change Interface Language')}
      </Typography>
      {languageSwitcher}
      <Typography variant="h6" className={styles.Title}>
        {t('Change Password')}
      </Typography>
      {form}
    </div>
  );
}
Example #23
Source File: ChatConversation.tsx    From glific-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
ChatConversation: React.SFC<ChatConversationProps> = (props) => {
  // check if message is unread and style it differently
  const client = useApolloClient();
  let chatInfoClass = [styles.ChatInfo, styles.ChatInfoRead];
  let chatBubble = [styles.ChatBubble, styles.ChatBubbleRead];
  const {
    lastMessage,
    selected,
    contactId,
    contactName,
    index,
    highlightSearch,
    searchMode,
    senderLastMessage,
    contactStatus,
    contactBspStatus,
    contactIsOrgRead,
    entityType,
    messageNumber,
    onClick,
  } = props;

  const [markAsRead] = useMutation(MARK_AS_READ, {
    onCompleted: (data) => {
      if (data.markContactMessagesAsRead) {
        updateContactCache(client, contactId);
      }
    },
  });

  // Need to handle following cases:
  // a. there might be some cases when there are no conversations against the contact
  // b. handle unread formatting only if tags array is set
  if (!contactIsOrgRead) {
    chatInfoClass = [styles.ChatInfo, styles.ChatInfoUnread];
    chatBubble = [styles.ChatBubble, styles.ChatBubbleUnread];
  }

  const name = contactName?.length > 20 ? `${contactName.slice(0, 20)}...` : contactName;

  const { type, body } = lastMessage;
  const isTextType = type === 'TEXT';

  let displayMSG: any = <MessageType type={type} body={body} />;

  let originalText = body;
  if (isTextType) {
    // let's shorten the text message to display correctly
    if (originalText.length > COMPACT_MESSAGE_LENGTH) {
      originalText = originalText.slice(0, COMPACT_MESSAGE_LENGTH).concat('...');
    }
    displayMSG = WhatsAppToJsx(originalText);
  }

  // set offset to use that in chatting window to fetch that msg
  const setSearchOffset = (apolloClient: any, offset: number = 0) => {
    apolloClient.writeQuery({
      query: SEARCH_OFFSET,
      data: { offset, search: highlightSearch },
    });
  };

  const msgID = searchMode && messageNumber ? `?search=${messageNumber}` : '';

  let redirectURL = `/chat/${contactId}${msgID}`;
  if (entityType === 'collection') {
    redirectURL = `/chat/collection/${contactId}${msgID}`;
  } else if (entityType === 'savedSearch') {
    redirectURL = `/chat/saved-searches/${contactId}${msgID}`;
  }

  return (
    <ListItem
      key={index}
      data-testid="list"
      button
      disableRipple
      className={clsx(styles.StyledListItem, { [styles.SelectedColor]: selected })}
      component={Link}
      selected={selected}
      onClick={() => {
        if (onClick) onClick(index);
        setSearchOffset(client, messageNumber);
        if (entityType === 'contact') {
          markAsRead({
            variables: { contactId: contactId.toString() },
          });
        }
      }}
      to={redirectURL}
    >
      <div>
        {entityType === 'contact' ? (
          <div className={styles.ChatIcons}>
            <div className={chatBubble.join(' ')} />
            <div className={styles.Timer}>
              <Timer
                time={senderLastMessage}
                contactStatus={contactStatus}
                contactBspStatus={contactBspStatus}
              />
            </div>
          </div>
        ) : (
          ''
        )}
      </div>
      <div className={chatInfoClass.join(' ')}>
        <div className={styles.ChatName} data-testid="name">
          {name}
        </div>
        <div className={styles.MessageContent} data-testid="content">
          {isTextType && highlightSearch ? BoldedText(body, highlightSearch) : displayMSG}
        </div>
        <div className={styles.MessageDate} data-testid="date">
          {moment(lastMessage.insertedAt).format(DATE_FORMAT)}
        </div>
      </div>
    </ListItem>
  );
}
Example #24
Source File: useNftTransactions.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
useNftTransactions = () => {
  const { activeMemberId } = useUser()
  const { joystream, proxyCallback } = useJoystream()
  const handleTransaction = useTransaction()
  const [openModal, closeModal] = useConfirmationModal()
  const client = useApolloClient()

  const _refetchData = useCallback(
    (includeBids = false) =>
      client.refetchQueries({
        include: [GetNftDocument, ...(includeBids ? [GetBidsDocument] : [])],
      }),
    [client]
  )

  const withdrawBid = useCallback(
    (id: string) => {
      if (!joystream || !activeMemberId) {
        return
      }
      handleTransaction({
        snackbarSuccessMessage: {
          title: 'Bid withdrawn successfully',
        },
        txFactory: async (updateStatus) =>
          (await joystream.extrinsics).cancelNftBid(id, activeMemberId, proxyCallback(updateStatus)),
        onTxSync: async () => _refetchData(true),
      })
    },
    [_refetchData, activeMemberId, handleTransaction, joystream, proxyCallback]
  )

  const cancelNftSale = useCallback(
    (id: string, saleType: NftSaleType) => {
      if (!joystream || !activeMemberId) {
        return
      }
      const handleCancelTransaction = () =>
        handleTransaction({
          txFactory: async (updateStatus) =>
            (await joystream.extrinsics).cancelNftSale(id, activeMemberId, saleType, proxyCallback(updateStatus)),
          onTxSync: async () => _refetchData(),
          snackbarSuccessMessage: {
            title: 'NFT removed from sale successfully',
            description: 'You can put it back on sale anytime.',
          },
        })

      openModal({
        title: 'Remove from sale?',
        description: 'Are you sure you want to remove this NFT from sale? You can put it back on sale anytime.',
        primaryButton: {
          variant: 'destructive',
          text: 'Remove',
          onClick: () => {
            handleCancelTransaction()
            closeModal()
          },
        },
        secondaryButton: {
          variant: 'secondary',
          text: 'Cancel',
          onClick: () => closeModal(),
        },
      })
    },
    [_refetchData, activeMemberId, closeModal, handleTransaction, joystream, openModal, proxyCallback]
  )

  const changeNftPrice = useCallback(
    (id: string, price: number) => {
      if (!joystream || !activeMemberId) {
        return
      }
      return handleTransaction({
        txFactory: async (updateStatus) =>
          (await joystream.extrinsics).changeNftPrice(activeMemberId, id, price, proxyCallback(updateStatus)),
        onTxSync: async () => _refetchData(),
        snackbarSuccessMessage: {
          title: 'NFT price changed successfully',
          description: 'You can update the price anytime.',
        },
      })
    },
    [_refetchData, activeMemberId, handleTransaction, joystream, proxyCallback]
  )

  const acceptNftBid = useCallback(
    (ownerId: string, id: string, bidderId: string, price: string) => {
      if (!joystream || !activeMemberId) {
        return
      }
      return handleTransaction({
        txFactory: async (updateStatus) =>
          (await joystream.extrinsics).acceptNftBid(ownerId, id, bidderId, price, proxyCallback(updateStatus)),
        onTxSync: async () => _refetchData(),
        snackbarSuccessMessage: {
          title: 'Bid accepted',
          description: 'Your auction has ended. The ownership has been transferred.',
        },
      })
    },
    [_refetchData, activeMemberId, handleTransaction, joystream, proxyCallback]
  )

  return {
    cancelNftSale,
    changeNftPrice,
    withdrawBid,
    acceptNftBid,
  }
}
Example #25
Source File: operatorsProvider.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
OperatorsContextProvider: React.FC = ({ children }) => {
  const distributionOperatorsMappingPromiseRef = useRef<Promise<BagOperatorsMapping>>()
  const storageOperatorsMappingPromiseRef = useRef<Promise<BagOperatorsMapping>>()
  const lastDistributionOperatorsFetchTimeRef = useRef<number>(Number.MAX_SAFE_INTEGER)
  const isFetchingDistributionOperatorsRef = useRef(false)
  const [distributionOperatorsError, setDistributionOperatorsError] = useState<unknown>(null)
  const [storageOperatorsError, setStorageOperatorsError] = useState<unknown>(null)
  const [failedStorageOperatorIds, setFailedStorageOperatorIds] = useState<string[]>([])

  const client = useApolloClient()

  const fetchDistributionOperators = useCallback(() => {
    const distributionOperatorsPromise = client.query<
      GetDistributionBucketsWithOperatorsQuery,
      GetDistributionBucketsWithOperatorsQueryVariables
    >({
      query: GetDistributionBucketsWithOperatorsDocument,
      fetchPolicy: 'network-only',
    })
    isFetchingDistributionOperatorsRef.current = true
    lastDistributionOperatorsFetchTimeRef.current = new Date().getTime()
    distributionOperatorsMappingPromiseRef.current = distributionOperatorsPromise.then((result) => {
      const mapping: BagOperatorsMapping = {}
      const buckets = result.data.distributionBuckets
      buckets.forEach((bucket) => {
        const bagIds = bucket.bags.map((bag) => bag.id)

        // we need to filter operators manually as query node doesn't support filtering this deep
        const operatorsInfos: OperatorInfo[] = bucket.operators
          .filter((operator) => operator.metadata?.nodeEndpoint?.includes('http') && operator.status === 'ACTIVE')
          .map((operator) => ({ id: operator.id, endpoint: operator.metadata?.nodeEndpoint || '' }))

        bagIds.forEach((bagId) => {
          if (!mapping[bagId]) {
            mapping[bagId] = operatorsInfos
          } else {
            mapping[bagId] = [...mapping[bagId], ...operatorsInfos]
          }
        })
      })
      isFetchingDistributionOperatorsRef.current = false
      return removeBagOperatorsDuplicates(mapping)
    })
    distributionOperatorsPromise.catch((error) => {
      SentryLogger.error('Failed to fetch distribution operators', 'OperatorsContextProvider', error)
      setDistributionOperatorsError(error)
      isFetchingDistributionOperatorsRef.current = false
    })
    return distributionOperatorsMappingPromiseRef.current
  }, [client])

  const fetchStorageOperators = useCallback(() => {
    const storageOperatorsPromise = client.query<GetStorageBucketsQuery, GetStorageBucketsQueryVariables>({
      query: GetStorageBucketsDocument,
      fetchPolicy: 'network-only',
    })
    storageOperatorsMappingPromiseRef.current = storageOperatorsPromise.then((result) => {
      const mapping: BagOperatorsMapping = {}
      const buckets = result.data.storageBuckets
      buckets.forEach((bucket) => {
        const bagIds = bucket.bags.map((bag) => bag.id)

        const endpoint = bucket.operatorMetadata?.nodeEndpoint
        if (!endpoint) {
          return
        }
        const operatorInfo: OperatorInfo = {
          id: bucket.id,
          endpoint,
        }

        bagIds.forEach((bagId) => {
          if (!mapping[bagId]) {
            mapping[bagId] = [operatorInfo]
          } else {
            mapping[bagId] = [...mapping[bagId], operatorInfo]
          }
        })
      })
      return removeBagOperatorsDuplicates(mapping)
    })
    storageOperatorsPromise.catch((error) => {
      SentryLogger.error('Failed to fetch storage operators', 'OperatorsContextProvider', error)
      setStorageOperatorsError(error)
    })
    return storageOperatorsMappingPromiseRef.current
  }, [client])

  const fetchOperators = useCallback(async () => {
    await Promise.all([fetchDistributionOperators(), fetchStorageOperators()])
  }, [fetchDistributionOperators, fetchStorageOperators])

  const tryRefetchDistributionOperators = useCallback(async () => {
    const currentTime = new Date().getTime()

    if (isFetchingDistributionOperatorsRef.current) {
      await distributionOperatorsMappingPromiseRef
      return true
    }

    if (currentTime - lastDistributionOperatorsFetchTimeRef.current < ASSET_MIN_DISTRIBUTOR_REFETCH_TIME) {
      return false
    }

    ConsoleLogger.log('Refetching distribution operators')
    await fetchDistributionOperators()
    return true
  }, [fetchDistributionOperators])

  // runs once - fetch all operators and create associated mappings
  useEffect(() => {
    fetchOperators()
  }, [fetchOperators])

  if (distributionOperatorsError || storageOperatorsError) {
    return <ViewErrorFallback />
  }

  return (
    <OperatorsContext.Provider
      value={{
        distributionOperatorsMappingPromiseRef,
        storageOperatorsMappingPromiseRef,
        failedStorageOperatorIds,
        setFailedStorageOperatorIds,
        fetchOperators,
        tryRefetchDistributionOperators,
      }}
    >
      {children}
    </OperatorsContext.Provider>
  )
}
Example #26
Source File: uploadsManager.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
UploadsManager: React.FC = () => {
  const navigate = useNavigate()
  const { activeChannelId } = useUser()
  const [cachedActiveChannelId, setCachedActiveChannelId] = useState<string | null>(null)
  const videoAssetsRef = useRef<VideoAssets[]>([])

  const { displaySnackbar } = useSnackbar()
  const { assetsFiles, channelUploads, uploadStatuses, isSyncing, processingAssets, newChannelsIds } = useUploadsStore(
    (state) => ({
      channelUploads: state.uploads.filter((asset) => asset.owner === activeChannelId),
      isSyncing: state.isSyncing,
      assetsFiles: state.assetsFiles,
      processingAssets: state.processingAssets,
      uploadStatuses: state.uploadsStatus,
      newChannelsIds: state.newChannelsIds,
    }),
    shallow
  )
  const { addAssetToUploads, removeAssetFromUploads, setIsSyncing, removeProcessingAsset, setUploadStatus } =
    useUploadsStore((state) => state.actions)
  const processingAssetsLookup = createLookup(processingAssets.map((asset) => ({ id: asset.id })))

  const videoAssets = channelUploads
    .filter((asset) => asset.type === 'video')
    .map((asset) => ({ ...asset, uploadStatus: uploadStatuses[asset.id]?.lastStatus }))

  const { getDataObjectsAvailability, dataObjects, startPolling, stopPolling } = useDataObjectsAvailabilityLazy({
    fetchPolicy: 'network-only',
    onCompleted: () => {
      startPolling?.(ASSET_POLLING_INTERVAL)
    },
  })

  // display snackbar when video upload is complete
  useEffect(() => {
    if (videoAssets.length) {
      videoAssets.forEach((video) => {
        const videoObject = videoAssetsRef.current.find(
          (videoRef) => videoRef.uploadStatus !== 'completed' && videoRef.id === video.id
        )
        if (videoObject && video.uploadStatus === 'completed') {
          displaySnackbar({
            customId: video.id,
            title: 'Video ready to be viewed',
            description: video.parentObject?.title || '',
            iconType: 'success',
            timeout: UPLOADED_SNACKBAR_TIMEOUT,
            actionText: 'See on Joystream',
            onActionClick: () => openInNewTab(absoluteRoutes.viewer.video(video.parentObject.id), true),
          })
        }
      })
      videoAssetsRef.current = videoAssets
    }
  }, [assetsFiles, displaySnackbar, navigate, videoAssets])

  const initialRender = useRef(true)
  useEffect(() => {
    if (!initialRender.current) {
      return
    }
    processingAssets.map((processingAsset) => {
      setUploadStatus(processingAsset.id, { progress: 100, lastStatus: 'processing' })
    })
    initialRender.current = false
  }, [processingAssets, setUploadStatus])

  useEffect(() => {
    if (!processingAssets.length) {
      return
    }
    getDataObjectsAvailability(processingAssets.map((asset) => asset.id))
  }, [getDataObjectsAvailability, processingAssets])

  useEffect(() => {
    dataObjects?.forEach((asset) => {
      if (asset.isAccepted) {
        setUploadStatus(asset.id, { lastStatus: 'completed' })
        removeProcessingAsset(asset.id)
      }
    })
    if (dataObjects?.every((entry) => entry.isAccepted)) {
      stopPolling?.()
    }
  }, [dataObjects, removeProcessingAsset, setUploadStatus, stopPolling])

  useEffect(() => {
    if (!processingAssets.length) {
      return
    }
    const interval = setInterval(
      () =>
        processingAssets.forEach((processingAsset) => {
          if (processingAsset.expiresAt < Date.now()) {
            removeProcessingAsset(processingAsset.id)
            setUploadStatus(processingAsset.id, { lastStatus: 'error' })
          }
        }),
      5000
    )
    return () => clearInterval(interval)
  }, [processingAssets, processingAssets.length, removeProcessingAsset, setUploadStatus])

  const client = useApolloClient()

  useEffect(() => {
    // do this only on first render or when active channel changes
    if (
      !activeChannelId ||
      cachedActiveChannelId === activeChannelId ||
      newChannelsIds.includes(activeChannelId) ||
      isSyncing
    ) {
      return
    }
    setCachedActiveChannelId(activeChannelId)
    setIsSyncing(true)

    const init = async () => {
      const [fetchedVideos, fetchedChannel, pendingAssetsLookup] = await fetchMissingAssets(client, activeChannelId)

      // start with assumption that all assets are missing
      const missingLocalAssetsLookup = { ...pendingAssetsLookup }

      // remove assets from local state that weren't returned by the query node
      // mark asset as not missing in local state
      channelUploads.forEach((asset) => {
        if (asset.owner !== activeChannelId) {
          return
        }

        if (!pendingAssetsLookup[asset.id]) {
          removeAssetFromUploads(asset.id)
        } else {
          // mark asset as not missing from local state
          delete missingLocalAssetsLookup[asset.id]
        }
      })

      // add missing video assets
      fetchedVideos.forEach((video) => {
        const media = video.media
        const thumbnail = video.thumbnailPhoto

        if (media && missingLocalAssetsLookup[media.id]) {
          addAssetToUploads({
            id: media.id,
            ipfsHash: media.ipfsHash,
            parentObject: {
              type: 'video',
              id: video.id,
            },
            owner: activeChannelId,
            type: 'video',
            size: media.size,
          })
        }

        if (thumbnail && missingLocalAssetsLookup[thumbnail.id]) {
          addAssetToUploads({
            id: thumbnail.id,
            ipfsHash: thumbnail.ipfsHash,
            parentObject: {
              type: 'video',
              id: video.id,
            },
            owner: activeChannelId,
            type: 'thumbnail',
            size: thumbnail.size,
          })
        }
      })

      // add missing channel assets
      const avatar = fetchedChannel?.avatarPhoto
      const cover = fetchedChannel?.coverPhoto

      if (avatar && missingLocalAssetsLookup[avatar.id]) {
        addAssetToUploads({
          id: avatar.id,
          ipfsHash: avatar.ipfsHash,
          parentObject: {
            type: 'channel',
            id: fetchedChannel?.id || '',
          },
          owner: activeChannelId,
          type: 'avatar',
          size: avatar.size,
        })
      }
      if (cover && missingLocalAssetsLookup[cover.id]) {
        addAssetToUploads({
          id: cover.id,
          ipfsHash: cover.ipfsHash,
          parentObject: {
            type: 'channel',
            id: fetchedChannel?.id || '',
          },
          owner: activeChannelId,
          type: 'cover',
          size: cover.size,
        })
      }

      const missingAssetsNotificationCount = Object.keys(pendingAssetsLookup).filter(
        (key) => !processingAssetsLookup[key]
      ).length

      if (missingAssetsNotificationCount > 0) {
        displaySnackbar({
          title: `${missingAssetsNotificationCount} asset${
            missingAssetsNotificationCount > 1 ? 's' : ''
          } waiting to resume upload`,
          description: 'Reconnect files to fix the issue',
          actionText: 'See',
          onActionClick: () => navigate(absoluteRoutes.studio.uploads(), { state: { highlightFailed: true } }),
          iconType: 'warning',
        })
      }
      setIsSyncing(false)
    }

    init()
  }, [
    activeChannelId,
    channelUploads,
    client,
    displaySnackbar,
    navigate,
    removeAssetFromUploads,
    addAssetToUploads,
    cachedActiveChannelId,
    isSyncing,
    setIsSyncing,
    processingAssets,
    processingAssetsLookup,
    newChannelsIds,
  ])

  return null
}
Example #27
Source File: VideoWorkspace.hooks.ts    From atlas with GNU General Public License v3.0 4 votes vote down vote up
useHandleVideoWorkspaceSubmit = () => {
  const { setIsWorkspaceOpen, editedVideoInfo, setEditedVideo } = useVideoWorkspace()
  const isNftMintDismissed = usePersonalDataStore((state) =>
    state.dismissedMessages.some((message) => message.id === 'first-mint')
  )
  const { setShowFistMintDialog } = useTransactionManagerStore((state) => state.actions)

  const { joystream, proxyCallback } = useJoystream()
  const startFileUpload = useStartFileUpload()
  const { activeChannelId, activeMemberId } = useAuthorizedUser()

  const client = useApolloClient()
  const handleTransaction = useTransaction()
  const addAsset = useAssetStore((state) => state.actions.addAsset)
  const removeDrafts = useDraftStore((state) => state.actions.removeDrafts)
  const { tabData } = useVideoWorkspaceData()

  const isEdit = !editedVideoInfo?.isDraft

  const handleSubmit = useCallback(
    async (data: VideoFormData) => {
      if (!joystream) {
        ConsoleLogger.error('No Joystream instance! Has webworker been initialized?')
        return
      }

      const isNew = !isEdit

      const assets: VideoInputAssets = {}
      const processAssets = async () => {
        if (data.assets.media) {
          const ipfsHash = await data.assets.media.hashPromise
          assets.media = {
            size: data.assets.media.blob.size,
            ipfsHash,
            replacedDataObjectId: tabData?.assets.video.id || undefined,
          }
        }

        if (data.assets.thumbnailPhoto) {
          const ipfsHash = await data.assets.thumbnailPhoto.hashPromise
          assets.thumbnailPhoto = {
            size: data.assets.thumbnailPhoto.blob.size,
            ipfsHash,
            replacedDataObjectId: tabData?.assets.thumbnail.cropId || undefined,
          }
        }
      }

      const uploadAssets = async ({ videoId, assetsIds }: VideoExtrinsicResult) => {
        const uploadPromises: Promise<unknown>[] = []
        if (data.assets.media && assetsIds.media) {
          const uploadPromise = startFileUpload(data.assets.media.blob, {
            id: assetsIds.media,
            owner: activeChannelId,
            parentObject: {
              type: 'video',
              id: videoId,
              title: data.metadata.title,
            },
            type: 'video',
            dimensions: data.assets.media.dimensions,
          })
          uploadPromises.push(uploadPromise)
        }
        if (data.assets.thumbnailPhoto && assetsIds.thumbnailPhoto) {
          const uploadPromise = startFileUpload(data.assets.thumbnailPhoto.blob, {
            id: assetsIds.thumbnailPhoto,
            owner: activeChannelId,
            parentObject: {
              type: 'video',
              id: videoId,
            },
            type: 'thumbnail',
            dimensions: data.assets.thumbnailPhoto.dimensions,
            imageCropData: data.assets.thumbnailPhoto.cropData,
          })
          uploadPromises.push(uploadPromise)
        }
        Promise.all(uploadPromises).catch((e) => SentryLogger.error('Unexpected upload failure', 'VideoWorkspace', e))
      }

      const refetchDataAndUploadAssets = async (result: VideoExtrinsicResult) => {
        const { assetsIds, videoId } = result

        // start asset upload
        uploadAssets(result)

        // add resolution for newly created asset
        if (assetsIds.thumbnailPhoto) {
          addAsset(assetsIds.thumbnailPhoto, { url: data.assets.thumbnailPhoto?.url })
        }

        const fetchedVideo = await client.query<GetVideosConnectionQuery, GetVideosConnectionQueryVariables>({
          query: GetVideosConnectionDocument,
          variables: {
            orderBy: VideoOrderByInput.CreatedAtDesc,
            where: {
              id_eq: videoId,
            },
          },
          fetchPolicy: 'network-only',
        })

        if (isNew) {
          if (fetchedVideo.data.videosConnection?.edges[0]) {
            writeVideoDataInCache({
              edge: fetchedVideo.data.videosConnection.edges[0],
              client,
            })
          }

          setEditedVideo({
            id: videoId,
            isDraft: false,
            isNew: false,
          })
          removeDrafts([editedVideoInfo?.id])
        }
      }

      const completed = await handleTransaction({
        preProcess: processAssets,
        txFactory: async (updateStatus) =>
          isNew
            ? (
                await joystream.extrinsics
              ).createVideo(
                activeMemberId,
                activeChannelId,
                data.metadata,
                data.nftMetadata,
                assets,
                proxyCallback(updateStatus)
              )
            : (
                await joystream.extrinsics
              ).updateVideo(
                editedVideoInfo.id,
                activeMemberId,
                data.metadata,
                data.nftMetadata,
                assets,
                proxyCallback(updateStatus)
              ),
        onTxSync: refetchDataAndUploadAssets,
        snackbarSuccessMessage: isNew ? undefined : { title: 'Changes published successfully' },
      })

      if (completed) {
        setIsWorkspaceOpen(false)
        if (!isNftMintDismissed && data.nftMetadata) {
          setTimeout(() => {
            setShowFistMintDialog(true)
          }, 2000)
        }
      }
    },
    [
      activeChannelId,
      activeMemberId,
      addAsset,
      client,
      editedVideoInfo.id,
      handleTransaction,
      isEdit,
      isNftMintDismissed,
      joystream,
      proxyCallback,
      removeDrafts,
      setEditedVideo,
      setIsWorkspaceOpen,
      setShowFistMintDialog,
      startFileUpload,
      tabData?.assets.thumbnail.cropId,
      tabData?.assets.video.id,
    ]
  )

  return handleSubmit
}
Example #28
Source File: WorkspaceSelector.tsx    From amplication with Apache License 2.0 4 votes vote down vote up
function WorkspaceSelector() {
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const [newWorkspace, setNewWorkspace] = useState<boolean>(false);

  const apolloClient = useApolloClient();
  const history = useHistory();

  const [setCurrentWorkspace, { data: setCurrentData }] = useMutation<TSetData>(
    SET_CURRENT_WORKSPACE
  );

  const handleSetCurrentWorkspace = useCallback(
    (workspace: models.Workspace) => {
      setIsOpen(false);
      setCurrentWorkspace({
        variables: {
          workspaceId: workspace.id,
        },
      }).catch(console.error);
    },
    [setCurrentWorkspace]
  );

  useEffect(() => {
    if (setCurrentData) {
      apolloClient.clearStore();
      setToken(setCurrentData.setCurrentWorkspace.token);
      history.replace("/");
      window.location.reload();
    }
  }, [setCurrentData, history, apolloClient]);

  const handleNewWorkspaceClick = useCallback(() => {
    setNewWorkspace(!newWorkspace);
  }, [newWorkspace, setNewWorkspace]);

  const handleOpen = useCallback(() => {
    setIsOpen((isOpen) => {
      return !isOpen;
    });
  }, [setIsOpen]);

  const { data, loading } = useQuery<TData>(GET_CURRENT_WORKSPACE);

  return (
    <div className={CLASS_NAME}>
      <Dialog
        className="new-entity-dialog"
        isOpen={newWorkspace}
        onDismiss={handleNewWorkspaceClick}
        title="New Workspace"
      >
        <NewWorkspace onWorkspaceCreated={handleSetCurrentWorkspace} />
      </Dialog>
      <div
        className={classNames(`${CLASS_NAME}__current`, {
          [`${CLASS_NAME}__current--active`]: isOpen,
        })}
        onClick={handleOpen}
      >
        {loading ? (
          <CircularProgress />
        ) : (
          <>
            <CircleBadge
              name={data?.currentWorkspace.name || ""}
              color={COLOR}
            />
            <span className={`${CLASS_NAME}__current__name`}>
              {data?.currentWorkspace.name}
            </span>
            <Button
              buttonStyle={EnumButtonStyle.Clear}
              disabled={loading}
              type="button"
              icon="code"
            />
          </>
        )}
      </div>
      {isOpen && data && (
        <WorkspaceSelectorList
          onNewWorkspaceClick={handleNewWorkspaceClick}
          selectedWorkspace={data.currentWorkspace}
          onWorkspaceSelected={handleSetCurrentWorkspace}
        />
      )}
    </div>
  );
}