Skip to content

Commit

Permalink
refactor(console): block page navigation when uploading custom ui ass…
Browse files Browse the repository at this point in the history
…ets (#6342)
  • Loading branch information
charIeszhao authored Jul 26, 2024
1 parent fb5b02b commit 33a1ac1
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 26 deletions.
2 changes: 1 addition & 1 deletion packages/console/src/components/ImageInputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function ImageInputs<FormContext extends FieldValues>({
actionDescription={t(`sign_in_exp.branding.with_${field.theme}`, {
value: t(`sign_in_exp.branding_uploads.${field.type}.title`),
})}
onCompleted={({ url }) => {
onUploadComplete={({ url }) => {
onChange(url);
}}
// Noop fallback should not be necessary, but for TypeScript to be happy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styles from './index.module.scss';
type Props = {
readonly isOpen: boolean;
readonly isSubmitting: boolean;
readonly isSubmitDisabled?: boolean;
readonly onSubmit: () => Promise<void>;
readonly onDiscard: () => void;
readonly confirmText?: AdminConsoleKey;
Expand All @@ -17,6 +18,7 @@ type Props = {
function SubmitFormChangesActionBar({
isOpen,
isSubmitting,
isSubmitDisabled = false,
confirmText = 'general.save_changes',
onSubmit,
onDiscard,
Expand All @@ -34,6 +36,7 @@ function SubmitFormChangesActionBar({
}}
/>
<Button
disabled={isSubmitDisabled}
isLoading={isSubmitting}
type="primary"
size="medium"
Expand Down
16 changes: 11 additions & 5 deletions packages/console/src/ds-components/Uploader/FileUploader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type Props<T extends Record<string, unknown> = UserAssets> = {
readonly defaultApiInstanceTimeout?: number;
readonly allowedMimeTypes: AllowedUploadMimeType[];
readonly actionDescription?: string;
readonly onCompleted: (response: T) => void;
readonly onUploadStart?: (file: File) => void;
readonly onUploadComplete?: (response: T) => void;
readonly onUploadErrorChange: (errorMessage?: string, files?: File[]) => void;
readonly className?: string;
/**
Expand All @@ -46,7 +47,8 @@ function FileUploader<T extends Record<string, unknown> = UserAssets>({
defaultApiInstanceTimeout,
allowedMimeTypes,
actionDescription,
onCompleted,
onUploadStart,
onUploadComplete,
onUploadErrorChange,
className,
apiInstance,
Expand Down Expand Up @@ -116,17 +118,21 @@ function FileUploader<T extends Record<string, unknown> = UserAssets>({

try {
setIsUploading(true);
onUploadStart?.(acceptedFile);
const uploadApi = apiInstance ?? api;
const response = await uploadApi.post(uploadUrl, { body: formData }).json<T>();

onCompleted(response);
} catch {
onUploadComplete?.(response);
} catch (error: unknown) {
if (error instanceof Error && error.name === 'AbortError') {
return;
}
setUploadError(t('components.uploader.error_upload'));
} finally {
setIsUploading(false);
}
},
[api, apiInstance, allowedMimeTypes, maxSize, onCompleted, t, uploadUrl]
[api, apiInstance, allowedMimeTypes, maxSize, onUploadComplete, onUploadStart, t, uploadUrl]
);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function ImageUploaderField({ onChange, allowedMimeTypes: mimeTypes, ...rest }:
<div>
<ImageUploader
allowedMimeTypes={allowedMimeTypes}
onCompleted={({ url }) => {
onUploadComplete={({ url }) => {
onChange(url);
}}
onUploadErrorChange={setUploadError}
Expand Down
4 changes: 4 additions & 0 deletions packages/console/src/hooks/use-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type StaticApiProps = {
hideErrorToast?: boolean | LogtoErrorCode[];
resourceIndicator: string;
timeout?: number;
signal?: AbortSignal;
};

const useGlobalRequestErrorHandler = (toastDisabledErrorCodes?: LogtoErrorCode[]) => {
Expand Down Expand Up @@ -112,6 +113,7 @@ export const useStaticApi = ({
hideErrorToast,
resourceIndicator,
timeout = requestTimeout,
signal,
}: StaticApiProps): KyInstance => {
const { isAuthenticated, getAccessToken, getOrganizationToken } = useLogto();
const { i18n } = useTranslation(undefined, { keyPrefix: 'admin_console' });
Expand All @@ -128,6 +130,7 @@ export const useStaticApi = ({
ky.create({
prefixUrl,
timeout,
signal,
hooks: {
beforeError: conditionalArray(
!disableGlobalErrorHandling &&
Expand Down Expand Up @@ -159,6 +162,7 @@ export const useStaticApi = ({
getAccessToken,
i18n.language,
timeout,
signal,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function LogosUploader({ isDarkModeEnabled }: Props) {
: 'enterprise_sso_details.branding_logo_context'
)}
allowedMimeTypes={allowedMimeTypes}
onCompleted={({ url }) => {
onUploadComplete={({ url }) => {
onChange(url);
}}
onUploadErrorChange={setUploadLogoError}
Expand All @@ -67,7 +67,7 @@ function LogosUploader({ isDarkModeEnabled }: Props) {
value={value ?? ''}
actionDescription={t('enterprise_sso_details.branding_dark_logo_context')}
allowedMimeTypes={allowedMimeTypes}
onCompleted={({ url }) => {
onUploadComplete={({ url }) => {
onChange(url);
}}
onUploadErrorChange={setUploadDarkLogoError}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useContext } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';

import CustomUiAssetsUploader from '@/components/CustomUiAssetsUploader';
import InlineUpsell from '@/components/InlineUpsell';
import { isDevFeaturesEnabled, isCloud } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
Expand All @@ -12,6 +11,7 @@ import CodeEditor from '@/ds-components/CodeEditor';
import FormField from '@/ds-components/FormField';
import TextLink from '@/ds-components/TextLink';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import CustomUiAssetsUploader from '@/pages/SignInExperience/components/CustomUiAssetsUploader';

import type { SignInExperienceForm } from '../../../types';
import FormSectionTitle from '../../components/FormSectionTitle';
Expand Down
19 changes: 15 additions & 4 deletions packages/console/src/pages/SignInExperience/PageContent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type SignInExperience } from '@logto/schemas';
import classNames from 'classnames';
import { useState } from 'react';
import { useCallback, useContext, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
Expand All @@ -16,6 +16,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
import { trySubmitSafe } from '@/utils/form';

import Preview from '../components/Preview';
import { SignInExperienceContext } from '../contexts/SignInExperienceContextProvider';
import usePreviewConfigs from '../hooks/use-preview-configs';
import { SignInExperienceTab } from '../types';
import { type SignInExperienceForm } from '../types';
Expand Down Expand Up @@ -48,6 +49,7 @@ function PageContent({ data, onSignInExperienceUpdated }: Props) {

const { updateConfigs } = useConfigs();
const { getPathname } = useTenantPathname();
const { isUploading, cancelUpload } = useContext(SignInExperienceContext);

const [dataToCompare, setDataToCompare] = useState<SignInExperience>();

Expand Down Expand Up @@ -106,6 +108,13 @@ function PageContent({ data, onSignInExperienceUpdated }: Props) {
})
);

const onDiscard = useCallback(() => {
reset();
if (isUploading && cancelUpload) {
cancelUpload();
}
}, [isUploading, cancelUpload, reset]);

return (
<>
<TabNav className={styles.tabs}>
Expand Down Expand Up @@ -143,9 +152,10 @@ function PageContent({ data, onSignInExperienceUpdated }: Props) {
)}
</div>
<SubmitFormChangesActionBar
isOpen={isDirty}
isOpen={isDirty || isUploading}
isSubmitDisabled={isUploading}
isSubmitting={isSaving}
onDiscard={reset}
onDiscard={onDiscard}
onSubmit={onSubmit}
/>
</div>
Expand All @@ -162,8 +172,9 @@ function PageContent({ data, onSignInExperienceUpdated }: Props) {
{dataToCompare && <SignUpAndSignInChangePreview before={data} after={dataToCompare} />}
</ConfirmModal>
<UnsavedChangesAlertModal
hasUnsavedChanges={isDirty}
hasUnsavedChanges={isDirty || isUploading}
parentPath={getPathname('/sign-in-experience')}
onConfirm={onDiscard}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { type CustomUiAssets, maxUploadFileSize, type AllowedUploadMimeType } from '@logto/schemas';
import { type Nullable } from '@silverhand/essentials';
import { format } from 'date-fns/fp';
import { useCallback, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import DeleteIcon from '@/assets/icons/delete.svg?react';
import FileIcon from '@/components/FileIcon';
import IconButton from '@/ds-components/IconButton';
import FileUploader from '@/ds-components/Uploader/FileUploader';
import useApi from '@/hooks/use-api';
import { formatBytes } from '@/utils/uploader';

import FileIcon from '../FileIcon';
import { SignInExperienceContext } from '../../contexts/SignInExperienceContextProvider';

import styles from './index.module.scss';

Expand All @@ -28,7 +30,19 @@ function CustomUiAssetsUploader({ disabled, value, onChange }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [file, setFile] = useState<File>();
const [error, setError] = useState<string>();
const [abortController, setAbortController] = useState(new AbortController());
const showUploader = !value?.id && !file && !error;
const { setIsUploading, setCancelUpload } = useContext(SignInExperienceContext);

const api = useApi({ timeout: requestTimeout, signal: abortController.signal });

useEffect(() => {
setCancelUpload(() => {
abortController.abort();
setIsUploading(false);
setAbortController(new AbortController());
});
}, [abortController, setCancelUpload, setIsUploading]);

const onComplete = useCallback(
(id: string) => {
Expand All @@ -53,13 +67,17 @@ function CustomUiAssetsUploader({ disabled, value, onChange }: Props) {
if (showUploader) {
return (
<FileUploader<{ customUiAssetId: string }>
defaultApiInstanceTimeout={requestTimeout}
apiInstance={api}
disabled={disabled}
allowedMimeTypes={allowedMimeTypes}
maxSize={maxUploadFileSize}
uploadUrl="api/sign-in-exp/default/custom-ui-assets"
onCompleted={({ customUiAssetId }) => {
onUploadStart={() => {
setIsUploading(true);
}}
onUploadComplete={({ customUiAssetId }) => {
onComplete(customUiAssetId);
setIsUploading(false);
}}
onUploadErrorChange={onErrorChange}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { noop } from '@silverhand/essentials';
import { createContext, useMemo, useRef, useState } from 'react';

type SignInExperienceContextType = {
isUploading: boolean;
cancelUpload?: () => void;
setIsUploading: (value: boolean) => void;
setCancelUpload: (cancelFunction?: () => void) => void;
};

type Props = {
readonly children?: React.ReactNode;
};

export const SignInExperienceContext = createContext<SignInExperienceContextType>({
isUploading: false,
cancelUpload: noop,
setIsUploading: noop,
setCancelUpload: noop,
});

function SignInExperienceContextProvider({ children }: Props) {
const [isUploading, setIsUploading] = useState(false);
const cancelUploadRef = useRef<() => void>();

const handleSetCancelUpload = (cancelFunction?: () => void) => {
// eslint-disable-next-line @silverhand/fp/no-mutation
cancelUploadRef.current = cancelFunction;
};

const contextValue = useMemo(
() => ({
isUploading,
cancelUpload: cancelUploadRef.current,
setIsUploading,
setCancelUpload: handleSetCancelUpload,
}),
[isUploading]
);

return (
<SignInExperienceContext.Provider value={contextValue}>
{children}
</SignInExperienceContext.Provider>
);
}

export default SignInExperienceContextProvider;
19 changes: 11 additions & 8 deletions packages/console/src/pages/SignInExperience/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useUserAssetsService from '@/hooks/use-user-assets-service';
import PageContent from './PageContent';
import Skeleton from './Skeleton';
import Welcome from './Welcome';
import SignInExperienceContextProvider from './contexts/SignInExperienceContextProvider';
import styles from './index.module.scss';

type PageWrapperProps = {
Expand All @@ -21,14 +22,16 @@ type PageWrapperProps = {

function PageWrapper({ children }: PageWrapperProps) {
return (
<div className={styles.container}>
<CardTitle
title="sign_in_exp.title"
subtitle="sign_in_exp.description"
className={styles.cardTitle}
/>
{children}
</div>
<SignInExperienceContextProvider>
<div className={styles.container}>
<CardTitle
title="sign_in_exp.title"
subtitle="sign_in_exp.description"
className={styles.cardTitle}
/>
{children}
</div>
</SignInExperienceContextProvider>
);
}

Expand Down

0 comments on commit 33a1ac1

Please sign in to comment.