diff --git a/extensions/apps/antenna/src/extensions/beam-editor/beam-editor.tsx b/extensions/apps/antenna/src/extensions/beam-editor/beam-editor.tsx index c729ab6332..68abbb9a72 100644 --- a/extensions/apps/antenna/src/extensions/beam-editor/beam-editor.tsx +++ b/extensions/apps/antenna/src/extensions/beam-editor/beam-editor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, ChangeEvent, KeyboardEvent } from 'react'; +import React, { useEffect, useState, useRef, ChangeEvent, KeyboardEvent, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { hasOwn, useAkashaStore, useRootComponentProps } from '@akashaorg/ui-awf-hooks'; import { type ContentBlock } from '@akashaorg/typings/lib/ui'; @@ -10,6 +10,7 @@ import Pill from '@akashaorg/design-system-core/lib/components/Pill'; import SearchBar from '@akashaorg/design-system-components/lib/components/SearchBar'; import Stack from '@akashaorg/design-system-core/lib/components/Stack'; import Text from '@akashaorg/design-system-core/lib/components/Text'; +import UnsavedChangesModal from '@akashaorg/design-system-components/lib/components/UnsavedChangesModal'; import { EditorBlockExtension } from '@akashaorg/ui-lib-extensions/lib/react/content-block'; import { Header } from './header'; import { Footer } from './footer'; @@ -27,12 +28,15 @@ export const BeamEditor: React.FC = () => { const [errorMessage, setErrorMessage] = useState(null); const [isNsfw, setIsNsfw] = useState(false); const [nsfwBlocks, setNsfwBlocks] = useState(new Map()); + const [disablePublishing, setDisablePublishing] = useState(true); + const [newUrl, setNewUrl] = useState(null); const bottomRef = useRef(null); const { t } = useTranslation('app-antenna'); - const { getCorePlugins } = useRootComponentProps(); + const { singleSpa, cancelNavigation, getCorePlugins } = useRootComponentProps(); + /* * get the logged-in user info and info about their profile's NSFW property */ @@ -70,6 +74,12 @@ export const BeamEditor: React.FC = () => { const { akashaProfile: profileData } = data?.node && hasOwn(data.node, 'akashaProfile') ? data.node : { akashaProfile: null }; + + const disableBeamPublishing = useMemo( + () => isPublishing || disablePublishing, + [disablePublishing, isPublishing], + ); + useEffect(() => { if (profileData?.nsfw) { setIsNsfw(true); @@ -105,6 +115,27 @@ export const BeamEditor: React.FC = () => { } }, [blocksInUse]); + useEffect(() => { + let navigationUnsubscribe: () => void; + /** + * when beam publishing is not disabled; + * 1. call cancel navigation method from routing plugin + * 2. set the new url from the callback fn. + */ + if (!disableBeamPublishing) { + navigationUnsubscribe = cancelNavigation(!disableBeamPublishing, url => { + setNewUrl(url); + }); + } + + return () => { + if (typeof navigationUnsubscribe === 'function') { + navigationUnsubscribe(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [disableBeamPublishing]); + const onBlockSelectAfter = (newSelection: ContentBlock) => { if (!newSelection?.propertyType) { return; @@ -234,7 +265,6 @@ export const BeamEditor: React.FC = () => { setUiState('editor'); }; - const [disablePublishing, setDisablePublishing] = useState(false); const blocksWithActiveNsfw = [...nsfwBlocks].filter(([, value]) => !!value); useEffect(() => { @@ -264,8 +294,31 @@ export const BeamEditor: React.FC = () => { }); }, [blocksInUse, focusedBlock]); + const handleLeavePage = () => { + // reset states + setDisablePublishing(true); + setNewUrl(null); + // navigate away from editor to the desired url using singleSpa. + singleSpa.navigateToUrl(newUrl); + }; + + const handleModalClose = () => setNewUrl(null); + return ( + {!!newUrl && ( + + )}
{ blocksNumber={blocksInUse.length} disableAddBlock={blocksInUse.length === maxAllowedBlocks} disableTagsSave={isPublishing || JSON.stringify(newTags) === JSON.stringify(editorTags)} - disableBeamPublishing={isPublishing || disablePublishing} + disableBeamPublishing={disableBeamPublishing} handleClickTags={handleTagsBtn} handleClickSave={handleClickSave} handleClickCancel={handleClickCancel} diff --git a/libs/design-system-components/src/components/EditInterests/index.tsx b/extensions/apps/profile/src/components/edit-interests/index.tsx similarity index 81% rename from libs/design-system-components/src/components/EditInterests/index.tsx rename to extensions/apps/profile/src/components/edit-interests/index.tsx index 5e85c1fc98..ed3ad041e6 100644 --- a/libs/design-system-components/src/components/EditInterests/index.tsx +++ b/extensions/apps/profile/src/components/edit-interests/index.tsx @@ -1,16 +1,19 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { apply, tw } from '@twind/core'; +import { useTranslation } from 'react-i18next'; +import { ProfileLabeled } from '@akashaorg/typings/lib/sdk/graphql-types-new'; import AutoComplete from '@akashaorg/design-system-core/lib/components/AutoComplete'; +import Button from '@akashaorg/design-system-core/lib/components/Button'; import { CheckIcon, XMarkIcon, } from '@akashaorg/design-system-core/lib/components/Icon/hero-icons-outline'; -import Button from '@akashaorg/design-system-core/lib/components/Button'; import Pill from '@akashaorg/design-system-core/lib/components/Pill'; import Stack from '@akashaorg/design-system-core/lib/components/Stack'; import Text from '@akashaorg/design-system-core/lib/components/Text'; -import { apply, tw } from '@twind/core'; -import { ButtonType } from '../types/common.types'; -import { ProfileLabeled } from '@akashaorg/typings/lib/sdk/graphql-types-new'; +import { ButtonType } from '@akashaorg/design-system-components/lib/components/types/common.types'; +import UnsavedChangesModal from '@akashaorg/design-system-components/lib/components/UnsavedChangesModal'; +import { useRootComponentProps } from '@akashaorg/ui-awf-hooks'; export type EditInterestsProps = { title: string; @@ -62,8 +65,12 @@ const EditInterests: React.FC = ({ const [myActiveInterests, setMyActiveInterests] = useState(new Set(myInterests)); const [allMyInterests, setAllMyInterests] = useState(new Set(myInterests)); const [tags, setTags] = useState(new Set()); + const [newUrl, setNewUrl] = useState(null); + const [isDisabled, setIsDisabled] = useState(true); + const { t } = useTranslation('app-profile'); + const { singleSpa, cancelNavigation } = useRootComponentProps(); - React.useEffect(() => { + useEffect(() => { setMyActiveInterests(new Set(myInterests)); setAllMyInterests(new Set(myInterests)); }, [myInterests]); @@ -97,6 +104,7 @@ const EditInterests: React.FC = ({ !!query; useEffect(() => { + setIsDisabled(!isFormDirty); if (onFormDirty) onFormDirty(isFormDirty); }, [isFormDirty, onFormDirty]); @@ -108,6 +116,7 @@ const EditInterests: React.FC = ({ }, [allMyInterests], ); + const getNewInterest = useCallback(() => { if (query) { const foundInterest = findInterest(query); @@ -120,8 +129,47 @@ const EditInterests: React.FC = ({ const maximumInterestsSelected = myActiveInterests.size + tagsSize >= maxInterests; + useEffect(() => { + let navigationUnsubscribe: () => void; + if (!isDisabled) { + navigationUnsubscribe = cancelNavigation(!isDisabled, url => { + setNewUrl(url); + }); + } + + return () => { + if (typeof navigationUnsubscribe === 'function') { + navigationUnsubscribe(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDisabled]); + + const handleLeavePage = () => { + // reset states + setIsDisabled(true); + setNewUrl(null); + // navigate away from editor to the desired url using singleSpa. + singleSpa.navigateToUrl(newUrl); + }; + + const handleModalClose = () => setNewUrl(null); + return (
+ {!!newUrl && ( + + )} diff --git a/libs/design-system-components/src/components/EditProfile/General/Header/DeleteImageModal/index.tsx b/extensions/apps/profile/src/components/edit-profile/General/Header/DeleteImageModal/index.tsx similarity index 100% rename from libs/design-system-components/src/components/EditProfile/General/Header/DeleteImageModal/index.tsx rename to extensions/apps/profile/src/components/edit-profile/General/Header/DeleteImageModal/index.tsx diff --git a/libs/design-system-components/src/components/EditProfile/General/Header/index.tsx b/extensions/apps/profile/src/components/edit-profile/General/Header/index.tsx similarity index 99% rename from libs/design-system-components/src/components/EditProfile/General/Header/index.tsx rename to extensions/apps/profile/src/components/edit-profile/General/Header/index.tsx index 2b22140a41..37353d4f2d 100644 --- a/libs/design-system-components/src/components/EditProfile/General/Header/index.tsx +++ b/extensions/apps/profile/src/components/edit-profile/General/Header/index.tsx @@ -5,7 +5,7 @@ import Card from '@akashaorg/design-system-core/lib/components/Card'; import Stack from '@akashaorg/design-system-core/lib/components/Stack'; import Text from '@akashaorg/design-system-core/lib/components/Text'; import List, { ListProps } from '@akashaorg/design-system-core/lib/components/List'; -import ImageModal from '../../../ImageModal'; +import ImageModal from '@akashaorg/design-system-components/lib/components/ImageModal'; import { ArrowUpOnSquareIcon, PencilIcon, diff --git a/libs/design-system-components/src/components/EditProfile/General/index.tsx b/extensions/apps/profile/src/components/edit-profile/General/index.tsx similarity index 95% rename from libs/design-system-components/src/components/EditProfile/General/index.tsx rename to extensions/apps/profile/src/components/edit-profile/General/index.tsx index 39bdf43af4..3e19047449 100644 --- a/libs/design-system-components/src/components/EditProfile/General/index.tsx +++ b/extensions/apps/profile/src/components/edit-profile/General/index.tsx @@ -3,7 +3,7 @@ import TextField from '@akashaorg/design-system-core/lib/components/TextField'; import { Controller, Control } from 'react-hook-form'; import { Header, HeaderProps } from './Header'; import { EditProfileFormValues } from '../types'; -import { ButtonType } from '../../types/common.types'; +import { ButtonType } from '@akashaorg/design-system-components/lib/components/types/common.types'; const MAX_BIO_LENGTH = 200; diff --git a/libs/design-system-components/src/components/EditProfile/SocialLinks/index.tsx b/extensions/apps/profile/src/components/edit-profile/SocialLinks/index.tsx similarity index 100% rename from libs/design-system-components/src/components/EditProfile/SocialLinks/index.tsx rename to extensions/apps/profile/src/components/edit-profile/SocialLinks/index.tsx diff --git a/libs/design-system-components/src/components/EditProfile/SocialLinks/social-link.tsx b/extensions/apps/profile/src/components/edit-profile/SocialLinks/social-link.tsx similarity index 100% rename from libs/design-system-components/src/components/EditProfile/SocialLinks/social-link.tsx rename to extensions/apps/profile/src/components/edit-profile/SocialLinks/social-link.tsx diff --git a/libs/design-system-components/src/components/EditProfile/index.tsx b/extensions/apps/profile/src/components/edit-profile/index.tsx similarity index 63% rename from libs/design-system-components/src/components/EditProfile/index.tsx rename to extensions/apps/profile/src/components/edit-profile/index.tsx index a28f0e9564..2f3260e659 100644 --- a/libs/design-system-components/src/components/EditProfile/index.tsx +++ b/extensions/apps/profile/src/components/edit-profile/index.tsx @@ -1,20 +1,20 @@ -import React, { SyntheticEvent, useMemo } from 'react'; +import React, { SyntheticEvent, useEffect, useMemo, useState } from 'react'; import * as z from 'zod'; -import Button from '@akashaorg/design-system-core/lib/components/Button'; -import Stack from '@akashaorg/design-system-core/lib/components/Stack'; -import { SocialLinks, SocialLinksProps } from './SocialLinks'; import { apply, tw } from '@twind/core'; +import { useTranslation } from 'react-i18next'; import { useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { General, GeneralProps } from './General'; -import { - EditProfileFormValues, - isFormExcludingAllExceptLinksDirty, - isFormWithExceptionOfLinksDirty, -} from './types'; -import { ButtonType } from '../types/common.types'; -import { InputType, NSFW } from '../NSFW'; import { PublishProfileData } from '@akashaorg/typings/lib/ui'; +import { useRootComponentProps } from '@akashaorg/ui-awf-hooks'; +import Button from '@akashaorg/design-system-core/lib/components/Button'; +import Stack from '@akashaorg/design-system-core/lib/components/Stack'; +import { InputType, NSFW } from '@akashaorg/design-system-components/lib/components/NSFW'; +import UnsavedChangesModal from '@akashaorg/design-system-components/lib/components/UnsavedChangesModal'; +import { ButtonType } from '@akashaorg/design-system-components/lib/components/types/common.types'; +import { General, GeneralProps } from './General'; +import { SocialLinks, SocialLinksProps } from './SocialLinks'; +import { isFormExcludingAllExceptLinksDirty, isFormWithExceptionOfLinksDirty } from './utils'; +import { EditProfileFormValues } from './types'; const MIN_NAME_CHARACTERS = 3; @@ -29,6 +29,10 @@ type GeneralFormProps = Pick; export type EditProfileProps = { defaultValues?: PublishProfileData; + /** + * modifying the handleClick to have an optional 'canSave' param. + * This determines when the cancel button click handler should show the unsaved changes modal. + */ cancelButton: ButtonType; saveButton: { label: string; @@ -62,7 +66,17 @@ const EditProfile: React.FC = ({ linkLabel, addNewLinkButtonLabel, }) => { - const { control, setValue, getValues, formState } = useForm({ + const [newUrl, setNewUrl] = useState(null); + const [isDisabled, setIsDisabled] = useState(true); + const [isFormValid, setIsFormValid] = useState(false); + const { t } = useTranslation('app-profile'); + const { singleSpa, cancelNavigation } = useRootComponentProps(); + const { + control, + setValue, + getValues, + formState: { dirtyFields, errors }, + } = useForm({ defaultValues: { ...defaultValues, links: defaultValues.links.map(link => ({ id: crypto.randomUUID(), href: link })), @@ -70,7 +84,6 @@ const EditProfile: React.FC = ({ resolver: zodResolver(schema), mode: 'onChange', }); - const { dirtyFields, errors } = formState; const links = useWatch({ name: 'links', control }); @@ -82,22 +95,68 @@ const EditProfile: React.FC = ({ const isFormDirty = isFormWithExceptionOfLinksDirty(dirtyFields) || formExcludingAllExceptLinksDirty; - const isValid = !Object.keys(errors).length; - const onSave = (event: SyntheticEvent) => { event.preventDefault(); const formValues = getValues(); - if (isValid && isFormDirty) { + if (isFormValid && isFormDirty) { saveButton.handleClick({ ...formValues, links: formValues.links?.map(link => link.href?.trim())?.filter(link => link) || [], }); + // reset state, to prevent re-triggering unsaved changes modal + setIsDisabled(true); } }; + useEffect(() => { + const isValid = !Object.keys(errors).length; + const buttonDisabled = isValid ? !isFormDirty : true; + setIsFormValid(isValid); + setIsDisabled(buttonDisabled); + }, [dirtyFields, errors, isFormDirty]); + + useEffect(() => { + let navigationUnsubscribe: () => void; + if (!isDisabled) { + navigationUnsubscribe = cancelNavigation(!isDisabled, url => { + setNewUrl(url); + }); + } + + return () => { + if (typeof navigationUnsubscribe === 'function') { + navigationUnsubscribe(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDisabled]); + + const handleLeavePage = () => { + // reset states + setIsDisabled(true); + setNewUrl(null); + // navigate away from editor to the desired url using singleSpa. + singleSpa.navigateToUrl(newUrl); + }; + + const handleModalClose = () => setNewUrl(null); + return ( + {!!newUrl && ( + + )} = ({ variant="primary" label={saveButton.label} loading={saveButton.loading} - disabled={isValid ? !isFormDirty : true} + disabled={isDisabled} onClick={onSave} type="submit" /> diff --git a/extensions/apps/profile/src/components/edit-profile/types.ts b/extensions/apps/profile/src/components/edit-profile/types.ts new file mode 100644 index 0000000000..0f514aea87 --- /dev/null +++ b/extensions/apps/profile/src/components/edit-profile/types.ts @@ -0,0 +1,15 @@ +import { Image } from '@akashaorg/typings/lib/ui'; + +type FormLink = { + id: string; + href: string; +}; + +export type EditProfileFormValues = { + name?: string; + bio?: string; + nsfw?: boolean; + links: FormLink[]; + avatar?: Image | File; + coverImage?: Image | File; +}; diff --git a/libs/design-system-components/src/components/EditProfile/types.ts b/extensions/apps/profile/src/components/edit-profile/utils.ts similarity index 81% rename from libs/design-system-components/src/components/EditProfile/types.ts rename to extensions/apps/profile/src/components/edit-profile/utils.ts index 291e71c396..928d99493a 100644 --- a/libs/design-system-components/src/components/EditProfile/types.ts +++ b/extensions/apps/profile/src/components/edit-profile/utils.ts @@ -1,18 +1,6 @@ -import { Image } from '@akashaorg/typings/lib/ui'; +import { EditProfileFormValues } from './types'; import { FormState } from 'react-hook-form'; -export type EditProfileFormValues = { - name?: string; - bio?: string; - nsfw?: boolean; - links: { - id: string; - href: string; - }[]; - avatar?: Image | File; - coverImage?: Image | File; -}; - export function isFormWithExceptionOfLinksDirty( dirtyFields: FormState['dirtyFields'], ) { diff --git a/extensions/apps/profile/src/components/pages/edit-profile/index.tsx b/extensions/apps/profile/src/components/pages/edit-profile/index.tsx index bad1d1d92f..9687476c90 100644 --- a/extensions/apps/profile/src/components/pages/edit-profile/index.tsx +++ b/extensions/apps/profile/src/components/pages/edit-profile/index.tsx @@ -4,7 +4,6 @@ import Card from '@akashaorg/design-system-core/lib/components/Card'; import Modal from '@akashaorg/design-system-core/lib/components/Modal'; import Text from '@akashaorg/design-system-core/lib/components/Text'; import ErrorLoader from '@akashaorg/design-system-core/lib/components/ErrorLoader'; -import EditProfile from '@akashaorg/design-system-components/lib/components/EditProfile'; import getSDK from '@akashaorg/core-sdk'; import { useTranslation } from 'react-i18next'; import { @@ -21,6 +20,7 @@ import { } from '@akashaorg/typings/lib/ui'; import { getAvatarImage, getCoverImage } from './get-profile-images'; import { selectProfileData } from '@akashaorg/ui-awf-hooks/lib/selectors/get-profile-by-did-query'; +import EditProfile from '../../edit-profile'; type EditProfilePageProps = { profileDID: string; @@ -269,9 +269,7 @@ const EditProfilePage: React.FC = props => { cancelButton={{ label: t('Cancel'), disabled: isProcessing, - handleClick: () => { - navigateToProfileInfoPage(); - }, + handleClick: navigateToProfileInfoPage, }} saveButton={{ label: t('Save'), diff --git a/extensions/apps/profile/src/components/pages/interests/index.tsx b/extensions/apps/profile/src/components/pages/interests/index.tsx index 3562337235..58c594b390 100644 --- a/extensions/apps/profile/src/components/pages/interests/index.tsx +++ b/extensions/apps/profile/src/components/pages/interests/index.tsx @@ -5,7 +5,6 @@ import Pill from '@akashaorg/design-system-core/lib/components/Pill'; import Stack from '@akashaorg/design-system-core/lib/components/Stack'; import Text from '@akashaorg/design-system-core/lib/components/Text'; import { ProfileInterestsLoading } from '@akashaorg/design-system-components/lib/components/Profile'; -import EditInterests from '@akashaorg/design-system-components/lib/components/EditInterests'; import { useTranslation } from 'react-i18next'; import { useGetInterestsByDidQuery, @@ -17,6 +16,7 @@ import { hasOwn, useRootComponentProps, useAkashaStore } from '@akashaorg/ui-awf import getSDK from '@akashaorg/core-sdk'; import { useApolloClient } from '@apollo/client'; import { ProfileLabeled } from '@akashaorg/typings/lib/sdk/graphql-types-new'; +import EditInterests from '../../edit-interests'; type InterestsPageProps = { profileDID: string; @@ -110,6 +110,13 @@ const InterestsPage: React.FC = props => { }); }; + const navigateToProfileInfoPage = () => { + navigateTo({ + appName: '@akashaorg/app-profile', + getNavigationUrl: () => `/${profileDID}`, + }); + }; + const runMutations = (interests: ProfileLabeled[]) => { setIsProcessing(true); if (interestSubscriptionId) { @@ -213,12 +220,7 @@ const InterestsPage: React.FC = props => { cancelButton={{ label: t('Cancel'), disabled: isProcessing, - handleClick: () => { - navigateTo({ - appName: '@akashaorg/app-profile', - getNavigationUrl: () => `/${profileDID}`, - }); - }, + handleClick: navigateToProfileInfoPage, }} saveButton={{ label: t('Save'), diff --git a/libs/app-loader/src/index.ts b/libs/app-loader/src/index.ts index 1453753087..f70d0dac48 100644 --- a/libs/app-loader/src/index.ts +++ b/libs/app-loader/src/index.ts @@ -80,6 +80,7 @@ export default class AppLoader { appNotFound: boolean; erroredApps: string[]; isLoadingUserExtensions: boolean; + navigationCanceledExtensions: Set; constructor(worldConfig: WorldConfig) { this.worldConfig = worldConfig; this.uiEvents = new Subject(); @@ -96,6 +97,7 @@ export default class AppLoader { this.appNotFound = false; this.erroredApps = []; this.isLoadingUserExtensions = false; + this.navigationCanceledExtensions = new Set(); } start = async () => { @@ -179,11 +181,13 @@ export default class AppLoader { hideNotLoggedIn(this.layoutConfig.extensionSlots.applicationSlotId); } }; + beforeAppChange = (ev: CustomEvent) => { - const { newUrl } = ev.detail; - const appName = extractAppNameFromPath(new URL(newUrl).pathname); - const status = singleSpa.getAppStatus(appName); - if (status === singleSpa.NOT_LOADED) { + const { newUrl, oldUrl } = ev.detail; + const newAppName = extractAppNameFromPath(new URL(newUrl).pathname); + const currentAppName = extractAppNameFromPath(new URL(oldUrl).pathname); + const status = singleSpa.getAppStatus(newAppName); + if (status === singleSpa.NOT_LOADED && !this.navigationCanceledExtensions.has(currentAppName)) { showLoadingCard(this.layoutConfig.extensionSlots.applicationSlotId); } }; @@ -670,6 +674,7 @@ export default class AppLoader { uiEvents: this.uiEvents, logger, plugins: this.plugins, + cancelNavigation: this.handleCancelNavigation(this.worldConfig.layout), }, }); }; @@ -724,6 +729,7 @@ export default class AppLoader { uiEvents: this.uiEvents, logger: this.parentLogger.create(name), plugins: this.plugins, + cancelNavigation: this.handleCancelNavigation(name), }; singleSpa.registerApplication({ name, @@ -735,4 +741,34 @@ export default class AppLoader { }); } }; + + /** + * This method relies on single spa's before-routing-event to determine when to block navigation. + * It is useful in scenarios where the user is allowed to make extra decision before completing the navigation action. + * @param appName - the name of the current app requiring navigation to be canceled. + * @returns a function with the following params: + * @param shouldCancel - boolean value indicating when the navigation should be canceled, after setting the event listener on the window object. + * @param callback - a callback function trigered after the navigation has been canceled. + * + * This function then returns a cleanup function that removes the event listener from the window object + */ + handleCancelNavigation = + (appName: string) => (shouldCancel: boolean, callback: (targetUrl: string) => void) => { + const listenerFn = ({ + detail: { newUrl, oldUrl, cancelNavigation }, + }: CustomEvent) => { + if (shouldCancel && new URL(newUrl).pathname !== new URL(oldUrl).pathname) { + cancelNavigation(); + callback(newUrl); + } + }; + + this.navigationCanceledExtensions.add(appName); + window.addEventListener('single-spa:before-routing-event', listenerFn); + + return () => { + this.navigationCanceledExtensions.delete(appName); + window.removeEventListener('single-spa:before-routing-event', listenerFn); + }; + }; } diff --git a/libs/design-system-components/src/components/UnsavedChangesModal/index.tsx b/libs/design-system-components/src/components/UnsavedChangesModal/index.tsx new file mode 100644 index 0000000000..5b05fd33e3 --- /dev/null +++ b/libs/design-system-components/src/components/UnsavedChangesModal/index.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import Modal from '@akashaorg/design-system-core/lib/components/Modal'; +import Text from '@akashaorg/design-system-core/lib/components/Text'; + +export type UnsavedChangesModalProps = { + showModal: boolean; + cancelButtonLabel: string; + leavePageButtonLabel: string; + title: string; + description: string; + handleModalClose: () => void; + handleLeavePage: () => void; +}; + +/** + * The UnsavedChangesModal composes the Modal component and is used to prompt user of any unsaved changes before navigating away from a page. + * It is currently implemented in: + * - edit profile page + * - beam editor page + * - edit profile interests page + */ +const UnsavedChangesModal: React.FC = props => { + const { + showModal, + cancelButtonLabel, + leavePageButtonLabel, + title, + description, + handleModalClose, + handleLeavePage, + } = props; + return ( + + + {description} + + + ); +}; + +export default UnsavedChangesModal; diff --git a/libs/hooks/src/use-modal-data.ts b/libs/hooks/src/use-modal-data.ts index b667821e60..8aee092b8d 100644 --- a/libs/hooks/src/use-modal-data.ts +++ b/libs/hooks/src/use-modal-data.ts @@ -4,7 +4,7 @@ import { useRootComponentProps } from './use-root-props'; export type theme = 'Light-Theme' | 'Dark-Theme'; /** - * Hook to handle the data supplied to the `LoginModal` extension. + * Hook to handle the data supplied to the modal extension. * @returns { modalData } - Object containing the params passed in the url * @example useModalData hook * ```typescript diff --git a/libs/typings/src/ui/root-component.ts b/libs/typings/src/ui/root-component.ts index 42ce5c8279..c435c22a02 100644 --- a/libs/typings/src/ui/root-component.ts +++ b/libs/typings/src/ui/root-component.ts @@ -32,6 +32,7 @@ export interface IRootComponentProps { encodeAppName: (name: string) => string; decodeAppName: (name: string) => string; children?: React.ReactNode; + cancelNavigation: (shouldCancel: boolean, callback: (targetUrl: string) => void) => () => void; } /** diff --git a/tests/utils/src/data-generator/integrations.ts b/tests/utils/src/data-generator/integrations.ts index b1995b9e47..98b2a52bd3 100644 --- a/tests/utils/src/data-generator/integrations.ts +++ b/tests/utils/src/data-generator/integrations.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/consistent-function-scoping */ import { IRootComponentProps } from '@akashaorg/typings/lib/ui'; import { genWorldConfig } from './world-config'; import { uiEventsMock } from '../mocks/uiEvents'; @@ -9,13 +10,13 @@ const corePluginsMock = { getUrlForApp: () => '', registerRoute: () => {}, unregisterRoute: () => {}, - // eslint-disable-next-line unicorn/consistent-function-scoping subscribe: () => () => {}, getSnapshot: () => ({ all: {}, activeExtensionsNames: {}, byArea: {}, }), + cancelNavigation: () => () => {}, }, contentBlockStore: { registerContentBlocks: () => {}, @@ -44,7 +45,6 @@ const corePluginsMock = { retryFromError: () => Promise.resolve(), // modify statusCodes as needed getStaticStatusCodes: () => ({ status: {} as any, error: {} as any }), - // eslint-disable-next-line unicorn/consistent-function-scoping subscribe: () => () => {}, }, extensionUninstaller: { @@ -84,4 +84,5 @@ export const genAppProps = (): IRootComponentProps & { encodeAppName: name => name, decodeAppName: name => name, getModalFromParams: () => ({ name: 'test-modal' }), + cancelNavigation: () => () => {}, });