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