From 70a7851dd0ce18bc16892aa8eba688b7e65bba6d Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Tue, 3 Dec 2024 16:45:51 +0100 Subject: [PATCH] Create Subspace Form (#7247) --- src/core/apollo/generated/apollo-hooks.ts | 224 +++++++++++++++++- src/core/apollo/generated/graphql-schema.ts | 82 ++++++- src/core/i18n/en/translation.en.json | 39 +-- src/core/ui/forms/FormikRadiosSwitch.tsx | 36 +++ .../DefaultVisualTypeConstraints.graphql | 17 ++ .../FormikVisualUpload/FormikVisualUpload.tsx | 138 +++++++++++ .../InnovationPacksView.tsx | 2 +- .../LibraryTemplatesView.tsx | 2 +- .../collaboration/callout/CalloutForm.tsx | 4 +- .../visual/EditVisuals/EditVisualsView.tsx | 12 +- .../visual/EditVisuals/VisualDescription.tsx | 3 +- .../components/BasicSpaceCard.tsx | 5 +- .../InnovationHubsAdmin/InnovationHubForm.tsx | 4 +- .../common/JourneyAvatar/JourneyAvatar.tsx | 7 +- .../common/JourneyCard/JourneyCard.tsx | 5 +- .../common/JourneyTile/JourneyTile.tsx | 5 +- .../ChildJourneyPageBanner.tsx | 5 +- .../defaultVisuals/defaultVisualUrls.ts | 12 + .../forms/CreateOpportunityForm.tsx | 121 ---------- .../settings/routes/ChallengeRoute.tsx | 4 +- .../journey/space/layout/SpacePageBanner.tsx | 6 +- .../AdminChallengesPage.graphql | 2 +- .../pages/SpaceSubspaces/SubspaceListView.tsx | 63 ++--- .../space/pages/SpaceSubspacesPage.tsx | 12 +- .../subspace/forms/CreateSubspaceForm.tsx | 134 ++++++----- .../SubspaceProfile/SubspaceProfilePage.tsx | 11 +- .../SubspaceProfile/SubspaceProfileView.tsx | 109 +++------ .../subspaceHome/dialogs/CreateJourney.tsx | 64 +++-- .../opportunity/pages/OpportunityList.tsx | 55 ++--- .../OpportunityProfilePage.tsx | 18 +- .../OpportunityProfileView.tsx | 139 ----------- .../components/JorneyCreationDialog/index.ts | 1 - .../JourneyCreationDialog.tsx | 34 +-- .../JourneyCreationForm.ts | 13 +- .../useJourneyCreation/createSubspace.graphql | 5 - .../createSubspace.graphql | 14 ++ .../useSubspaceCreation.ts} | 88 ++++++- .../PostTemplateSelector.tsx | 0 .../SubspaceTemplateSelector.tsx | 84 +++++++ .../WhiteboardTemplateSelector.tsx | 0 .../graphql/SpaceDefaultTemplates.graphql | 21 ++ .../templates/graphql/TemplateName.graphql | 11 + .../hooks/useCreateCollaborationTemplate.ts | 6 +- .../DashboardSpaces/DashboardSpaces.tsx | 8 +- .../MyLatestContributions.tsx | 5 +- .../myMemberships/ExpandableSpaceTree.tsx | 8 +- 46 files changed, 1014 insertions(+), 624 deletions(-) create mode 100644 src/core/ui/forms/FormikRadiosSwitch.tsx create mode 100644 src/core/ui/upload/FormikVisualUpload/DefaultVisualTypeConstraints.graphql create mode 100644 src/core/ui/upload/FormikVisualUpload/FormikVisualUpload.tsx create mode 100644 src/domain/journey/defaultVisuals/defaultVisualUrls.ts delete mode 100644 src/domain/journey/opportunity/forms/CreateOpportunityForm.tsx delete mode 100644 src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx delete mode 100644 src/domain/shared/components/JorneyCreationDialog/index.ts rename src/domain/shared/components/{JorneyCreationDialog => JourneyCreationDialog}/JourneyCreationDialog.tsx (70%) rename src/domain/shared/components/{JorneyCreationDialog => JourneyCreationDialog}/JourneyCreationForm.ts (60%) delete mode 100644 src/domain/shared/utils/useJourneyCreation/createSubspace.graphql create mode 100644 src/domain/shared/utils/useSubspaceCreation/createSubspace.graphql rename src/domain/shared/utils/{useJourneyCreation/useJourneyCreation.ts => useSubspaceCreation/useSubspaceCreation.ts} (56%) rename src/domain/templates/components/{CalloutForm => TemplateSelectors}/PostTemplateSelector.tsx (100%) create mode 100644 src/domain/templates/components/TemplateSelectors/SubspaceTemplateSelector.tsx rename src/domain/templates/components/{CalloutForm => TemplateSelectors}/WhiteboardTemplateSelector.tsx (100%) create mode 100644 src/domain/templates/graphql/SpaceDefaultTemplates.graphql create mode 100644 src/domain/templates/graphql/TemplateName.graphql diff --git a/src/core/apollo/generated/apollo-hooks.ts b/src/core/apollo/generated/apollo-hooks.ts index 0f65c1ebfc..84dbbc39cc 100644 --- a/src/core/apollo/generated/apollo-hooks.ts +++ b/src/core/apollo/generated/apollo-hooks.ts @@ -4064,6 +4064,80 @@ export type UploadFileMutationOptions = Apollo.BaseMutationOptions< SchemaTypes.UploadFileMutation, SchemaTypes.UploadFileMutationVariables >; +export const DefaultVisualTypeConstraintsDocument = gql` + query DefaultVisualTypeConstraints($visualType: VisualType!) { + platform { + id + configuration { + defaultVisualTypeConstraints(type: $visualType) { + maxHeight + maxWidth + minHeight + minWidth + aspectRatio + allowedTypes + } + } + } + } +`; + +/** + * __useDefaultVisualTypeConstraintsQuery__ + * + * To run a query within a React component, call `useDefaultVisualTypeConstraintsQuery` and pass it any options that fit your needs. + * When your component renders, `useDefaultVisualTypeConstraintsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useDefaultVisualTypeConstraintsQuery({ + * variables: { + * visualType: // value for 'visualType' + * }, + * }); + */ +export function useDefaultVisualTypeConstraintsQuery( + baseOptions: Apollo.QueryHookOptions< + SchemaTypes.DefaultVisualTypeConstraintsQuery, + SchemaTypes.DefaultVisualTypeConstraintsQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery< + SchemaTypes.DefaultVisualTypeConstraintsQuery, + SchemaTypes.DefaultVisualTypeConstraintsQueryVariables + >(DefaultVisualTypeConstraintsDocument, options); +} + +export function useDefaultVisualTypeConstraintsLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + SchemaTypes.DefaultVisualTypeConstraintsQuery, + SchemaTypes.DefaultVisualTypeConstraintsQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery< + SchemaTypes.DefaultVisualTypeConstraintsQuery, + SchemaTypes.DefaultVisualTypeConstraintsQueryVariables + >(DefaultVisualTypeConstraintsDocument, options); +} + +export type DefaultVisualTypeConstraintsQueryHookResult = ReturnType; +export type DefaultVisualTypeConstraintsLazyQueryHookResult = ReturnType< + typeof useDefaultVisualTypeConstraintsLazyQuery +>; +export type DefaultVisualTypeConstraintsQueryResult = Apollo.QueryResult< + SchemaTypes.DefaultVisualTypeConstraintsQuery, + SchemaTypes.DefaultVisualTypeConstraintsQueryVariables +>; +export function refetchDefaultVisualTypeConstraintsQuery( + variables: SchemaTypes.DefaultVisualTypeConstraintsQueryVariables +) { + return { query: DefaultVisualTypeConstraintsDocument, variables: variables }; +} + export const InnovationPackProfilePageDocument = gql` query InnovationPackProfilePage($innovationPackId: UUID!) { lookup { @@ -17474,7 +17548,7 @@ export const AdminSpaceSubspacesPageDocument = gql` id displayName url - cardBanner: visual(type: CARD) { + avatar: visual(type: AVATAR) { ...VisualUri } } @@ -18849,9 +18923,18 @@ export type ShareLinkWithUserMutationOptions = Apollo.BaseMutationOptions< SchemaTypes.ShareLinkWithUserMutationVariables >; export const CreateSubspaceDocument = gql` - mutation createSubspace($input: CreateSubspaceInput!) { + mutation createSubspace($input: CreateSubspaceInput!, $includeVisuals: Boolean = false) { createSubspace(subspaceData: $input) { ...SubspaceCard + visuals: profile @include(if: $includeVisuals) { + id + cardBanner: visual(type: CARD) { + id + } + avatar: visual(type: AVATAR) { + id + } + } } } ${SubspaceCardFragmentDoc} @@ -18875,6 +18958,7 @@ export type CreateSubspaceMutationFn = Apollo.MutationFunction< * const [createSubspaceMutation, { data, loading, error }] = useCreateSubspaceMutation({ * variables: { * input: // value for 'input' + * includeVisuals: // value for 'includeVisuals' * }, * }); */ @@ -19981,6 +20065,82 @@ export function refetchSpaceCollaborationIdQuery(variables: SchemaTypes.SpaceCol return { query: SpaceCollaborationIdDocument, variables: variables }; } +export const SpaceDefaultTemplatesDocument = gql` + query SpaceDefaultTemplates($spaceId: UUID!) { + lookup { + space(ID: $spaceId) { + id + templatesManager { + id + templateDefaults { + id + type + template { + id + profile { + id + displayName + } + } + } + } + } + } + } +`; + +/** + * __useSpaceDefaultTemplatesQuery__ + * + * To run a query within a React component, call `useSpaceDefaultTemplatesQuery` and pass it any options that fit your needs. + * When your component renders, `useSpaceDefaultTemplatesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useSpaceDefaultTemplatesQuery({ + * variables: { + * spaceId: // value for 'spaceId' + * }, + * }); + */ +export function useSpaceDefaultTemplatesQuery( + baseOptions: Apollo.QueryHookOptions< + SchemaTypes.SpaceDefaultTemplatesQuery, + SchemaTypes.SpaceDefaultTemplatesQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery( + SpaceDefaultTemplatesDocument, + options + ); +} + +export function useSpaceDefaultTemplatesLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + SchemaTypes.SpaceDefaultTemplatesQuery, + SchemaTypes.SpaceDefaultTemplatesQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery( + SpaceDefaultTemplatesDocument, + options + ); +} + +export type SpaceDefaultTemplatesQueryHookResult = ReturnType; +export type SpaceDefaultTemplatesLazyQueryHookResult = ReturnType; +export type SpaceDefaultTemplatesQueryResult = Apollo.QueryResult< + SchemaTypes.SpaceDefaultTemplatesQuery, + SchemaTypes.SpaceDefaultTemplatesQueryVariables +>; +export function refetchSpaceDefaultTemplatesQuery(variables: SchemaTypes.SpaceDefaultTemplatesQueryVariables) { + return { query: SpaceDefaultTemplatesDocument, variables: variables }; +} + export const SpaceTemplatesSetIdDocument = gql` query SpaceTemplatesSetId($spaceNameId: UUID_NAMEID!) { space(ID: $spaceNameId) { @@ -20493,6 +20653,66 @@ export type DeleteTemplateMutationOptions = Apollo.BaseMutationOptions< SchemaTypes.DeleteTemplateMutation, SchemaTypes.DeleteTemplateMutationVariables >; +export const TemplateNameDocument = gql` + query TemplateName($templateId: UUID!) { + lookup { + template(ID: $templateId) { + id + profile { + id + displayName + } + } + } + } +`; + +/** + * __useTemplateNameQuery__ + * + * To run a query within a React component, call `useTemplateNameQuery` and pass it any options that fit your needs. + * When your component renders, `useTemplateNameQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useTemplateNameQuery({ + * variables: { + * templateId: // value for 'templateId' + * }, + * }); + */ +export function useTemplateNameQuery( + baseOptions: Apollo.QueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery( + TemplateNameDocument, + options + ); +} + +export function useTemplateNameLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery( + TemplateNameDocument, + options + ); +} + +export type TemplateNameQueryHookResult = ReturnType; +export type TemplateNameLazyQueryHookResult = ReturnType; +export type TemplateNameQueryResult = Apollo.QueryResult< + SchemaTypes.TemplateNameQuery, + SchemaTypes.TemplateNameQueryVariables +>; +export function refetchTemplateNameQuery(variables: SchemaTypes.TemplateNameQueryVariables) { + return { query: TemplateNameDocument, variables: variables }; +} + export const TemplateUrlResolverDocument = gql` query TemplateUrlResolver($templatesSetId: UUID!, $templateNameId: NameID!) { lookupByName { diff --git a/src/core/apollo/generated/graphql-schema.ts b/src/core/apollo/generated/graphql-schema.ts index 7c2184dd76..32e580f726 100644 --- a/src/core/apollo/generated/graphql-schema.ts +++ b/src/core/apollo/generated/graphql-schema.ts @@ -6988,6 +6988,30 @@ export type UploadFileMutationVariables = Exact<{ export type UploadFileMutation = { __typename?: 'Mutation'; uploadFileOnStorageBucket: string }; +export type DefaultVisualTypeConstraintsQueryVariables = Exact<{ + visualType: VisualType; +}>; + +export type DefaultVisualTypeConstraintsQuery = { + __typename?: 'Query'; + platform: { + __typename?: 'Platform'; + id: string; + configuration: { + __typename?: 'Config'; + defaultVisualTypeConstraints: { + __typename?: 'VisualConstraints'; + maxHeight: number; + maxWidth: number; + minHeight: number; + minWidth: number; + aspectRatio: number; + allowedTypes: Array; + }; + }; + }; +}; + export type InnovationPackProfilePageQueryVariables = Exact<{ innovationPackId: Scalars['UUID']; }>; @@ -22731,7 +22755,7 @@ export type AdminSpaceSubspacesPageQuery = { id: string; displayName: string; url: string; - cardBanner?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; + avatar?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; }; }>; templatesManager?: @@ -24226,6 +24250,7 @@ export type PageInfoFragment = { export type CreateSubspaceMutationVariables = Exact<{ input: CreateSubspaceInput; + includeVisuals?: InputMaybe; }>; export type CreateSubspaceMutation = { @@ -24233,6 +24258,12 @@ export type CreateSubspaceMutation = { createSubspace: { __typename?: 'Space'; id: string; + visuals: { + __typename?: 'Profile'; + id: string; + cardBanner?: { __typename?: 'Visual'; id: string } | undefined; + avatar?: { __typename?: 'Visual'; id: string } | undefined; + }; metrics?: Array<{ __typename?: 'NVP'; id: string; name: string; value: string }> | undefined; profile: { __typename?: 'Profile'; @@ -25082,6 +25113,41 @@ export type SpaceCollaborationIdQuery = { }; }; +export type SpaceDefaultTemplatesQueryVariables = Exact<{ + spaceId: Scalars['UUID']; +}>; + +export type SpaceDefaultTemplatesQuery = { + __typename?: 'Query'; + lookup: { + __typename?: 'LookupQueryResults'; + space?: + | { + __typename?: 'Space'; + id: string; + templatesManager?: + | { + __typename?: 'TemplatesManager'; + id: string; + templateDefaults: Array<{ + __typename?: 'TemplateDefault'; + id: string; + type: TemplateDefaultType; + template?: + | { + __typename?: 'Template'; + id: string; + profile: { __typename?: 'Profile'; id: string; displayName: string }; + } + | undefined; + }>; + } + | undefined; + } + | undefined; + }; +}; + export type SpaceTemplatesSetIdQueryVariables = Exact<{ spaceNameId: Scalars['UUID_NAMEID']; }>; @@ -25995,6 +26061,20 @@ export type DeleteTemplateMutation = { deleteTemplate: { __typename?: 'Template'; id: string }; }; +export type TemplateNameQueryVariables = Exact<{ + templateId: Scalars['UUID']; +}>; + +export type TemplateNameQuery = { + __typename?: 'Query'; + lookup: { + __typename?: 'LookupQueryResults'; + template?: + | { __typename?: 'Template'; id: string; profile: { __typename?: 'Profile'; id: string; displayName: string } } + | undefined; + }; +}; + export type TemplateUrlResolverQueryVariables = Exact<{ templatesSetId: Scalars['UUID']; templateNameId: Scalars['NameID']; diff --git a/src/core/i18n/en/translation.en.json b/src/core/i18n/en/translation.en.json index 05b11f3d65..fedf65612e 100644 --- a/src/core/i18n/en/translation.en.json +++ b/src/core/i18n/en/translation.en.json @@ -122,6 +122,7 @@ "submit": "Submit", "remove": "Remove", "cancel": "Cancel", + "upload": "Upload", "explore-and-connect": "Explore and connect", "send-message": "Send message", "edit-members": "Edit members", @@ -829,18 +830,18 @@ }, "subspace": { "displayName": { - "title": "Title", - "description": "Try to keep it short, descriptive and engaging", + "title": "Title *", + "description": "", "placeholder": "" }, "tagline": { - "title": "Subspace Statement", - "description": "Explain in one sentence what this Subspace is about. It is also possible to frame it as a question, starting with 'How might we...'", + "title": "Tagline", + "description": "", "placeholder": "" }, "background": { - "title": "Current Reality", - "description": "Give a brief impression of the current state of the Subspace. Think of the 'What, where, when and who'.", + "title": "A description of the subspace that is visible on the subspace dashboard", + "description": "", "placeholder": "" }, "impact": { @@ -860,17 +861,16 @@ }, "tags": { "title": "Tags", - "description": "Enter words that are key to the Subspace to make it stand out and easier to find.", - "placeholder": "" - }, - "addCallouts": { - "title": "Add default template's collaboration tools" + "description": "", + "placeholder": "", + "tooltip": "Tags" }, "addTutorialCallouts": { "title": "Add tutorials collaboration tools" }, - "innovationFlow": { - "title": "Subspace Flow" + "template": { + "title": "Template:", + "defaultTemplate": "Default Alkemio template for subspaces" } }, "subsubspace": { @@ -1974,7 +1974,8 @@ "notifications": { "subsubspace-created": "Subspace created successfully.", "subsubspace-removed": "Subspace removed successfully.", - "subsubspace-updated": "Subspace updated successfully." + "subsubspace-updated": "Subspace updated successfully.", + "error-creating-subsubspace": "An error ocurred while creating the Subspace." } }, "subspace": { @@ -2129,24 +2130,28 @@ } }, "visualEdit": { - "avatar": { + "AVATAR": { "title": "Avatar", "description1": "Resolution: {{width}} width x {{height}} height (pixels)", "description2": "Usage: Journey card on page banner", "description": "Description: {{alternativeText}}" }, - "banner": { + "BANNER": { "title": "Page banner", "description1": "Resolution: {{width}} width x {{height}} height (pixels)", "description2": "Usage: Banner on dashboard tab", "description": "Description: {{alternativeText}}" }, - "cardBanner": { + "CARD": { "title": "Card banner", "description1": "Resolution: {{width}} width x {{height}} height (pixels)", "description2": "Usage: Banner on cards", "description": "Description: {{alternativeText}}" }, + "BANNER_WIDE": { + "title": "Wide Banner", + "description1": "Resolution: {{width}} width x {{height}} height (pixels)" + }, "form": { "altText": { "placeholder": "Example: Multiple snow covered mountains", diff --git a/src/core/ui/forms/FormikRadiosSwitch.tsx b/src/core/ui/forms/FormikRadiosSwitch.tsx new file mode 100644 index 0000000000..9d4bbab373 --- /dev/null +++ b/src/core/ui/forms/FormikRadiosSwitch.tsx @@ -0,0 +1,36 @@ +import { FormHelperText, InputLabel, Radio } from '@mui/material'; +import { useField } from 'formik'; +import Gutters, { GuttersProps } from '../grid/Gutters'; +import { BlockSectionTitle } from '../typography'; + +interface FormikRadiosSwitchProps extends GuttersProps { + name: string; + label: string; + options: { value: T; label: string }[]; +} + +export const FormikRadiosSwitch = ({ name, label, options, ...containerProps }: FormikRadiosSwitchProps) => { + const [field, meta, helper] = useField(name); + + const handleChange = (value: T) => { + helper.setValue(value); + }; + + return ( + + {label} + {options.map(({ value, label }, index) => ( + + handleChange(value)} + onBlur={field.onBlur} + /> + {label} + + ))} + {meta.touched && meta.error && {meta.error}} + + ); +}; diff --git a/src/core/ui/upload/FormikVisualUpload/DefaultVisualTypeConstraints.graphql b/src/core/ui/upload/FormikVisualUpload/DefaultVisualTypeConstraints.graphql new file mode 100644 index 0000000000..5c931c93bc --- /dev/null +++ b/src/core/ui/upload/FormikVisualUpload/DefaultVisualTypeConstraints.graphql @@ -0,0 +1,17 @@ +query DefaultVisualTypeConstraints( + $visualType: VisualType! +) { + platform { + id + configuration { + defaultVisualTypeConstraints(type: $visualType) { + maxHeight + maxWidth + minHeight + minWidth + aspectRatio + allowedTypes + } + } + } +} diff --git a/src/core/ui/upload/FormikVisualUpload/FormikVisualUpload.tsx b/src/core/ui/upload/FormikVisualUpload/FormikVisualUpload.tsx new file mode 100644 index 0000000000..56e639673f --- /dev/null +++ b/src/core/ui/upload/FormikVisualUpload/FormikVisualUpload.tsx @@ -0,0 +1,138 @@ +import { Box, BoxProps, Skeleton } from '@mui/material'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import 'react-image-crop/dist/ReactCrop.css'; +import UploadButton from '@/core/ui/button/UploadButton'; +import Image from '@/core/ui/image/Image'; +import FileUploadWrapper from '../FileUploadWrapper'; +import { CropDialog } from '../VisualUpload/CropDialog'; +import { useField } from 'formik'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; +import { useDefaultVisualTypeConstraintsQuery } from '@/core/apollo/generated/apollo-hooks'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { Caption } from '../../typography'; +import { gutters } from '../../grid/utils'; + +const DEFAULT_SIZE = 70; + +const ImagePlaceholder = ({ src, alt, ...props }: BoxProps<'img'>) => { + const { t } = useTranslation(); + return src ? {alt} : {t('components.visual-upload.no-data')}; +}; + +const FormikAvatarUploadSkeleton = ({ height = DEFAULT_SIZE, ...containerProps }: { height?: number } & BoxProps) => { + return ( + + + + + + + ); +}; + +interface VisualWithAltText { + file: File; + altText?: string; +} + +export interface FormikAvatarUploadProps extends BoxProps { + name: string; + visualType: VisualType; + height?: number; + altText?: string; +} + +/** + * Dimensions are obtained from the query PlatformVisualsConstraints using the visualType + * If height is provided, width is calculated based on the aspect ratio of the visualType, if not default height is used + */ +const FormikAvatarUpload = ({ + name, + visualType, + height = DEFAULT_SIZE, + altText, + ...containerProps +}: FormikAvatarUploadProps) => { + const { t } = useTranslation(); + + const [cropDialogOpened, setCropDialogOpened] = useState(false); + const [field, , helpers] = useField(name); + const selectedFile = field.value; + + const [imageUrl, setImageUrl] = useState(defaultVisualUrls[visualType]); + + useEffect(() => { + if (selectedFile?.file) { + const objectUrl = URL.createObjectURL(selectedFile.file); + setImageUrl(objectUrl); + return () => { + URL.revokeObjectURL(objectUrl); + }; + } else { + setImageUrl(defaultVisualUrls[visualType]); + } + }, [selectedFile, visualType]); + + const { data: constraintsData, loading } = useDefaultVisualTypeConstraintsQuery({ + variables: { visualType }, + }); + const visualTypeConstraints = constraintsData?.platform.configuration.defaultVisualTypeConstraints; + + if (loading || !visualTypeConstraints) { + return ; + } + const { maxHeight, maxWidth, allowedTypes, aspectRatio } = visualTypeConstraints; + + const onFileSelected = (file: File) => { + helpers.setValue({ file, altText }); + setCropDialogOpened(true); + }; + const handleVisualReady = (file: File, altText: string) => { + helpers.setValue({ file, altText }); + setCropDialogOpened(false); + }; + + const width = height * aspectRatio; + + return ( + + + + theme.palette.grey[400], + }} + src={imageUrl} + alt={altText} + /> + + {t(`pages.visualEdit.${visualType}.title`)} + + {t(`pages.visualEdit.${visualType}.description1`, { width: maxWidth, height: maxHeight })} + + + + + + {cropDialogOpened && ( + setCropDialogOpened(false)} + onSave={handleVisualReady} + config={visualTypeConstraints} + /> + )} + + ); +}; + +export default FormikAvatarUpload; diff --git a/src/domain/InnovationPack/DashboardInnovationPacks/InnovationPacksView.tsx b/src/domain/InnovationPack/DashboardInnovationPacks/InnovationPacksView.tsx index 1b7610943d..4063a9cdc6 100644 --- a/src/domain/InnovationPack/DashboardInnovationPacks/InnovationPacksView.tsx +++ b/src/domain/InnovationPack/DashboardInnovationPacks/InnovationPacksView.tsx @@ -12,7 +12,7 @@ import { CONTRIBUTE_CARD_COLUMNS } from '@/core/ui/card/ContributeCard'; import GridItem from '@/core/ui/grid/GridItem'; import { Skeleton } from '@mui/material'; import { gutters } from '@/core/ui/grid/utils'; -import { useTheme } from '@mui/styles'; +import { useTheme } from '@mui/material'; interface InnovationPacksViewProps extends PageContentBlockProps { filter: string[]; diff --git a/src/domain/InnovationPack/DashboardLibraryTemplates/LibraryTemplatesView.tsx b/src/domain/InnovationPack/DashboardLibraryTemplates/LibraryTemplatesView.tsx index 78c012dc45..e895860be4 100644 --- a/src/domain/InnovationPack/DashboardLibraryTemplates/LibraryTemplatesView.tsx +++ b/src/domain/InnovationPack/DashboardLibraryTemplates/LibraryTemplatesView.tsx @@ -14,7 +14,7 @@ import { TemplateType } from '@/core/apollo/generated/graphql-schema'; import TemplateCard from '@/domain/templates/components/cards/TemplateCard'; import { AnyTemplate, AnyTemplateWithInnovationPack } from '@/domain/templates/models/TemplateBase'; import { gutters } from '@/core/ui/grid/utils'; -import { useTheme } from '@mui/styles'; +import { useTheme } from '@mui/material'; export interface LibraryTemplatesFilter { templateTypes: TemplateType[]; diff --git a/src/domain/collaboration/callout/CalloutForm.tsx b/src/domain/collaboration/callout/CalloutForm.tsx index ae0b72eb3b..9d1fd57100 100644 --- a/src/domain/collaboration/callout/CalloutForm.tsx +++ b/src/domain/collaboration/callout/CalloutForm.tsx @@ -34,8 +34,8 @@ import CalloutWhiteboardField, { import { JourneyTypeName } from '@/domain/journey/JourneyTypeName'; import { JourneyCalloutGroupNameOptions } from './CalloutsInContext/CalloutsGroup'; import { DEFAULT_TAGSET } from '@/domain/common/tags/tagset.constants'; -import PostTemplateSelector from '@/domain/templates/components/CalloutForm/PostTemplateSelector'; -import WhiteboardTemplateSelector from '@/domain/templates/components/CalloutForm/WhiteboardTemplateSelector'; +import PostTemplateSelector from '@/domain/templates/components/TemplateSelectors/PostTemplateSelector'; +import WhiteboardTemplateSelector from '@/domain/templates/components/TemplateSelectors/WhiteboardTemplateSelector'; type FormValueType = { displayName: string; diff --git a/src/domain/common/visual/EditVisuals/EditVisualsView.tsx b/src/domain/common/visual/EditVisuals/EditVisualsView.tsx index ca573fbc94..8c80e815bb 100644 --- a/src/domain/common/visual/EditVisuals/EditVisualsView.tsx +++ b/src/domain/common/visual/EditVisuals/EditVisualsView.tsx @@ -23,33 +23,33 @@ const EditVisualsView = ({ visuals, visualTypes }: EditVisualsViewProps) => { - + )} {(!visualTypes || visualTypes.includes(VisualType.Banner)) && ( - + )} {(!visualTypes || visualTypes.includes(VisualType.Card)) && ( - + )} diff --git a/src/domain/common/visual/EditVisuals/VisualDescription.tsx b/src/domain/common/visual/EditVisuals/VisualDescription.tsx index 62b89f5279..a4885f7c66 100644 --- a/src/domain/common/visual/EditVisuals/VisualDescription.tsx +++ b/src/domain/common/visual/EditVisuals/VisualDescription.tsx @@ -1,9 +1,10 @@ +import { VisualType } from '@/core/apollo/generated/graphql-schema'; import { BlockSectionTitle } from '@/core/ui/typography'; import { Box } from '@mui/material'; import { useTranslation } from 'react-i18next'; type VisualDescriptionProps = { - visualTypeName: 'avatar' | 'banner' | 'cardBanner'; + visualTypeName: VisualType.Avatar | VisualType.Banner | VisualType.Card; visual: | { maxWidth: number; diff --git a/src/domain/community/virtualContributor/components/BasicSpaceCard.tsx b/src/domain/community/virtualContributor/components/BasicSpaceCard.tsx index 83c4b6b0a2..dc24234c86 100644 --- a/src/domain/community/virtualContributor/components/BasicSpaceCard.tsx +++ b/src/domain/community/virtualContributor/components/BasicSpaceCard.tsx @@ -3,7 +3,8 @@ import Avatar from '@/core/ui/avatar/Avatar'; import { BlockSectionTitle } from '@/core/ui/typography'; import { useTranslation } from 'react-i18next'; import RouterLink from '@/core/ui/link/RouterLink'; -import defaultJourneyCardBanner from '@/domain/journey/defaultVisuals/Card.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; // TODO: add cardBanner if we want support of Spaces as BOK export interface BasicSpaceProps { @@ -26,7 +27,7 @@ const BasicSpaceCard = ({ space }: { space: BasicSpaceProps | undefined }) => { {space.displayName} diff --git a/src/domain/innovationHub/InnovationHubsAdmin/InnovationHubForm.tsx b/src/domain/innovationHub/InnovationHubsAdmin/InnovationHubForm.tsx index 538a130f2c..b8aac9181d 100644 --- a/src/domain/innovationHub/InnovationHubsAdmin/InnovationHubForm.tsx +++ b/src/domain/innovationHub/InnovationHubsAdmin/InnovationHubForm.tsx @@ -2,7 +2,7 @@ import { Box, FormGroup } from '@mui/material'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; -import { Tagset, TagsetType, Visual } from '@/core/apollo/generated/graphql-schema'; +import { Tagset, TagsetType, Visual, VisualType } from '@/core/apollo/generated/graphql-schema'; import { NameSegment, nameSegmentSchema } from '@/domain/platform/admin/components/Common/NameSegment'; import FormikAutocomplete from '@/core/ui/forms/FormikAutocomplete'; import FormikMarkdownField from '@/core/ui/forms/MarkdownInput/FormikMarkdownField'; @@ -126,7 +126,7 @@ const InnovationHubForm = ({ {t('components.visualSegment.banner')} diff --git a/src/domain/journey/common/JourneyAvatar/JourneyAvatar.tsx b/src/domain/journey/common/JourneyAvatar/JourneyAvatar.tsx index ff7987673c..9f93bade92 100644 --- a/src/domain/journey/common/JourneyAvatar/JourneyAvatar.tsx +++ b/src/domain/journey/common/JourneyAvatar/JourneyAvatar.tsx @@ -1,7 +1,8 @@ -import React, { forwardRef } from 'react'; +import { forwardRef } from 'react'; import { SxProps, Theme } from '@mui/material'; import Avatar, { AvatarSize, SizeableAvatarProps } from '@/core/ui/avatar/Avatar'; -import defaultJourneyAvatar from '../../defaultVisuals/Avatar.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; interface JourneyAvatarProps extends SizeableAvatarProps { src: string | undefined; @@ -10,7 +11,7 @@ interface JourneyAvatarProps extends SizeableAvatarProps { } const JourneyAvatar = forwardRef(({ src, size = 'large', ...props }, ref) => { - return ; + return ; }); export default JourneyAvatar; diff --git a/src/domain/journey/common/JourneyCard/JourneyCard.tsx b/src/domain/journey/common/JourneyCard/JourneyCard.tsx index e452be34c5..9a449ae6c9 100644 --- a/src/domain/journey/common/JourneyCard/JourneyCard.tsx +++ b/src/domain/journey/common/JourneyCard/JourneyCard.tsx @@ -11,7 +11,8 @@ import ExpandableCardFooter from '@/core/ui/card/ExpandableCardFooter'; import CardBanner from '@/core/ui/card/CardImageHeader'; import { useTranslation } from 'react-i18next'; import { JourneyCardBanner } from './Banner'; -import defaultCardBanner from '../../defaultVisuals/Card.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; import CardTags from '@/core/ui/card/CardTags'; export interface JourneyCardProps extends ContributeCardProps { @@ -65,7 +66,7 @@ const JourneyCard = ({ diff --git a/src/domain/journey/common/JourneyTile/JourneyTile.tsx b/src/domain/journey/common/JourneyTile/JourneyTile.tsx index 3337f32a45..2a4c023cc3 100644 --- a/src/domain/journey/common/JourneyTile/JourneyTile.tsx +++ b/src/domain/journey/common/JourneyTile/JourneyTile.tsx @@ -10,7 +10,8 @@ import { alpha } from '@mui/material/styles'; import webkitLineClamp from '@/core/ui/utils/webkitLineClamp'; import { BlockTitle } from '@/core/ui/typography'; import InsertPhotoOutlinedIcon from '@mui/icons-material/InsertPhotoOutlined'; -import defaultJourneyCardBanner from '@/domain/journey/defaultVisuals/Card.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; import { PrivacyIcon } from './PrivacyIcon'; type JourneyTileProps = { @@ -54,7 +55,7 @@ const JourneyTile = ({ journey, isPrivate, columns = 3 }: JourneyTileProps) => { {isPrivate && } diff --git a/src/domain/journey/common/childJourneyPageBanner/ChildJourneyPageBanner.tsx b/src/domain/journey/common/childJourneyPageBanner/ChildJourneyPageBanner.tsx index 67d6b79070..ee122aeb7b 100644 --- a/src/domain/journey/common/childJourneyPageBanner/ChildJourneyPageBanner.tsx +++ b/src/domain/journey/common/childJourneyPageBanner/ChildJourneyPageBanner.tsx @@ -1,7 +1,8 @@ import JourneyPageBannerCard from '../PageBanner/JourneyPageBannerCard/JourneyPageBannerCard'; import PageBanner, { PageBannerProps } from '@/core/ui/layout/pageBanner/PageBanner'; import { useMemo } from 'react'; -import defaultJourneyBanner from '../../defaultVisuals/Banner.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; import { useChildJourneyPageBannerQuery } from '@/core/apollo/generated/apollo-hooks'; import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; import { getVisualByType } from '@/domain/common/visual/utils/visuals.utils'; @@ -21,7 +22,7 @@ const ChildJourneyPageBanner = ({ journeyId, ...props }: ChildJourneyPageBannerP } return { ...spaceBanner, - uri: defaultJourneyBanner, + uri: defaultVisualUrls[VisualType.Banner], }; }, [spaceBanner]); diff --git a/src/domain/journey/defaultVisuals/defaultVisualUrls.ts b/src/domain/journey/defaultVisuals/defaultVisualUrls.ts new file mode 100644 index 0000000000..2529fa4b37 --- /dev/null +++ b/src/domain/journey/defaultVisuals/defaultVisualUrls.ts @@ -0,0 +1,12 @@ +import { VisualType } from '@/core/apollo/generated/graphql-schema'; +import defaultJourneyAvatar from '@/domain/journey/defaultVisuals/Avatar.jpg'; +import defaultJourneyBanner from '@/domain/journey/defaultVisuals/Banner.jpg'; +import defaultJourneyCard from '@/domain/journey/defaultVisuals/Card.jpg'; + +export const defaultVisualUrls = { + [VisualType.Avatar]: defaultJourneyAvatar, + [VisualType.Banner]: defaultJourneyBanner, + [VisualType.Card]: defaultJourneyCard, + // It's never shown as an uploadable, only replaced when saving a whiteboard, so it doesn't really need a default, but it is useful for typescript validation to define it + [VisualType.BannerWide]: defaultJourneyBanner, +} as const; diff --git a/src/domain/journey/opportunity/forms/CreateOpportunityForm.tsx b/src/domain/journey/opportunity/forms/CreateOpportunityForm.tsx deleted file mode 100644 index c8f87cf690..0000000000 --- a/src/domain/journey/opportunity/forms/CreateOpportunityForm.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { useTranslation } from 'react-i18next'; -import * as yup from 'yup'; -import { Form, Formik } from 'formik'; -import { MessageWithPayload } from '@/domain/shared/i18n/ValidationMessageTranslation'; -import FormikInputField from '@/core/ui/forms/FormikInputField/FormikInputField'; -import { SMALL_TEXT_LENGTH, MARKDOWN_TEXT_LENGTH } from '@/core/ui/forms/field-length.constants'; -import FormikMarkdownField from '@/core/ui/forms/MarkdownInput/FormikMarkdownField'; -import Gutters from '@/core/ui/grid/Gutters'; -import { TagsetField } from '@/domain/platform/admin/components/Common/TagsetSegment'; -import FormikEffectFactory from '@/core/ui/forms/FormikEffect'; -import { JourneyCreationForm } from '@/domain/shared/components/JorneyCreationDialog/JourneyCreationForm'; -import MarkdownValidator from '@/core/ui/forms/MarkdownInput/MarkdownValidator'; -import { FormikSwitch } from '@/core/ui/forms/FormikSwitch'; - -const FormikEffect = FormikEffectFactory(); - -type FormValues = { - displayName: string; - tagline: string; - vision: string; - tags: string[]; - addTutorialCallouts: boolean; - addCallouts: boolean; -}; - -interface CreateOpportunityFormProps extends JourneyCreationForm {} - -export const CreateOpportunityForm = ({ - isSubmitting, - onValidChanged, - onChanged, -}: PropsWithChildren) => { - const { t } = useTranslation(); - - const validationRequiredString = t('forms.validations.required'); - - const handleChanged = (value: FormValues) => - onChanged({ - displayName: value.displayName, - tagline: value.tagline, - vision: value.vision, - tags: value.tags, - addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, - }); - - const initialValues: FormValues = { - displayName: '', - tagline: '', - vision: '', - tags: [], - addTutorialCallouts: true, - addCallouts: true, - }; - - const validationSchema = yup.object().shape({ - displayName: yup - .string() - .trim() - .min(3, MessageWithPayload('forms.validations.minLength')) - .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')) - .required(validationRequiredString), - tagline: yup - .string() - .trim() - .min(3, MessageWithPayload('forms.validations.minLength')) - .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')) - .required(validationRequiredString), - vision: MarkdownValidator(MARKDOWN_TEXT_LENGTH).trim().required(validationRequiredString), - tags: yup.array().of(yup.string().min(2)).notRequired(), - }); - - return ( - {}} - > - {() => ( -
- - - - - - - - - -
- )} -
- ); -}; diff --git a/src/domain/journey/settings/routes/ChallengeRoute.tsx b/src/domain/journey/settings/routes/ChallengeRoute.tsx index 71b82b16a2..b8720003a1 100644 --- a/src/domain/journey/settings/routes/ChallengeRoute.tsx +++ b/src/domain/journey/settings/routes/ChallengeRoute.tsx @@ -4,7 +4,7 @@ import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; import { useSubSpace } from '@/domain/journey/subspace/hooks/useSubSpace'; import { Error404 } from '@/core/pages/Errors/Error404'; import SubspaceCommunicationsPage from '@/domain/journey/subspace/pages/SubspaceCommunications/SubspaceCommunicationsPage'; -import ChallengeProfilePage from '@/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage'; +import SubspaceProfilePage from '@/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage'; import { ApplicationsAdminRoutes } from '@/domain/platform/admin/community/routes/ApplicationsAdminRoutes'; import ChallengeAuthorizationRoute from '@/domain/platform/admin/subspace/routing/ChallengeAuthorizationRoute'; @@ -27,7 +27,7 @@ export const ChallengeRoute: FC = () => { } /> - } /> + } /> } /> } /> setImageLoading(false)} onError={imageLoadError} diff --git a/src/domain/journey/space/pages/SpaceSubspaces/AdminChallengesPage.graphql b/src/domain/journey/space/pages/SpaceSubspaces/AdminChallengesPage.graphql index b552457f76..11f904562b 100644 --- a/src/domain/journey/space/pages/SpaceSubspaces/AdminChallengesPage.graphql +++ b/src/domain/journey/space/pages/SpaceSubspaces/AdminChallengesPage.graphql @@ -7,7 +7,7 @@ query AdminSpaceSubspacesPage($spaceId: UUID_NAMEID!) { id displayName url - cardBanner: visual(type: CARD) { + avatar: visual(type: AVATAR) { ...VisualUri } } diff --git a/src/domain/journey/space/pages/SpaceSubspaces/SubspaceListView.tsx b/src/domain/journey/space/pages/SpaceSubspaces/SubspaceListView.tsx index 83d6ad16f6..5d18d3bd98 100644 --- a/src/domain/journey/space/pages/SpaceSubspaces/SubspaceListView.tsx +++ b/src/domain/journey/space/pages/SpaceSubspaces/SubspaceListView.tsx @@ -9,7 +9,6 @@ import { refetchAdminSpaceSubspacesPageQuery, refetchDashboardWithMembershipsQuery, useAdminSpaceSubspacesPageQuery, - useCreateSubspaceMutation, useDeleteSpaceMutation, useSpaceCollaborationIdLazyQuery, useSpaceTemplatesSetIdQuery, @@ -17,9 +16,9 @@ import { } from '@/core/apollo/generated/apollo-hooks'; import { useNotification } from '@/core/ui/notifications/useNotification'; import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; -import { JourneyCreationDialog } from '@/domain/shared/components/JorneyCreationDialog'; -import { SubspaceIcon } from '@/domain/journey/subspace/icon/SubspaceIcon'; -import { JourneyFormValues } from '@/domain/shared/components/JorneyCreationDialog/JourneyCreationForm'; +import { JourneyCreationDialog } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog'; +import SubspaceIcon2 from '@/main/ui/icons/SubspaceIcon2'; +import { JourneyFormValues } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationForm'; import { buildSettingsUrl } from '@/main/routing/urlBuilders'; import { CreateSubspaceForm } from '@/domain/journey/subspace/forms/CreateSubspaceForm'; import PageContentBlock from '@/core/ui/content/PageContentBlock'; @@ -38,6 +37,7 @@ import CreateTemplateDialog from '@/domain/templates/components/Dialogs/CreateEd import { AuthorizationPrivilege, TemplateDefaultType, TemplateType } from '@/core/apollo/generated/graphql-schema'; import { CollaborationTemplateFormSubmittedValues } from '@/domain/templates/components/Forms/CollaborationTemplateForm'; import { useCreateCollaborationTemplate } from '@/domain/templates/hooks/useCreateCollaborationTemplate'; +import { useSubspaceCreation } from '@/domain/shared/utils/useSubspaceCreation/useSubspaceCreation'; export const SubspaceListView = () => { const { t } = useTranslation(); @@ -53,8 +53,9 @@ export const SubspaceListView = () => { const { data, loading } = useAdminSpaceSubspacesPageQuery({ variables: { - spaceId: spaceNameId, + spaceId: spaceId, }, + skip: !spaceId, }); const [fetchCollaborationId] = useSpaceCollaborationIdLazyQuery(); @@ -71,7 +72,7 @@ export const SubspaceListView = () => { displayName: s.profile.displayName, url: buildSettingsUrl(s.profile.url), avatar: { - uri: s.profile.cardBanner?.uri ?? '', + uri: s.profile.avatar?.uri ?? '', }, }, })) || [] @@ -81,7 +82,7 @@ export const SubspaceListView = () => { const [deleteSubspace] = useDeleteSpaceMutation({ refetchQueries: [ refetchAdminSpaceSubspacesPageQuery({ - spaceId: spaceNameId, + spaceId, }), ], awaitRefetchQueries: true, @@ -98,48 +99,34 @@ export const SubspaceListView = () => { }); }; - const [createSubspace] = useCreateSubspaceMutation({ + const { createSubspace } = useSubspaceCreation({ onCompleted: () => { notify(t('pages.admin.subspace.notifications.subspace-created'), 'success'); }, - refetchQueries: [ - refetchAdminSpaceSubspacesPageQuery({ spaceId: spaceNameId }), - refetchDashboardWithMembershipsQuery(), - ], + refetchQueries: [refetchAdminSpaceSubspacesPageQuery({ spaceId }), refetchDashboardWithMembershipsQuery()], awaitRefetchQueries: true, }); const handleCreate = useCallback( async (value: JourneyFormValues) => { - const { data } = await createSubspace({ - variables: { - input: { - spaceID: spaceNameId, - profileData: { - displayName: value.displayName, - description: value.background, - tagline: value.tagline, - }, - context: { - vision: value.vision, - }, - tags: value.tags, - collaborationData: { - addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, - }, - }, - }, + const result = await createSubspace({ + spaceID: spaceId, + displayName: value.displayName, + tagline: value.tagline, + background: value.background ?? '', + vision: value.vision, + tags: value.tags, + addTutorialCallouts: value.addTutorialCallouts, + collaborationTemplateId: value.collaborationTemplateId, + visuals: value.visuals, }); - if (!data?.createSubspace) { + if (!result) { return; } - if (data?.createSubspace.profile.url) { - navigate(buildSettingsUrl(data?.createSubspace.profile.url)); - } + navigate(buildSettingsUrl(result.profile.url)); }, - [navigate, createSubspace, spaceNameId] + [navigate, createSubspace, spaceId] ); const [updateTemplateDefault] = useUpdateTemplateDefaultMutation(); @@ -154,7 +141,7 @@ export const SubspaceListView = () => { }, refetchQueries: [ refetchAdminSpaceSubspacesPageQuery({ - spaceId: spaceNameId, + spaceId, }), ], awaitRefetchQueries: true, @@ -282,7 +269,7 @@ export const SubspaceListView = () => { /> } + icon={} journeyName={t('common.subspace')} onClose={() => setJourneyCreationDialogOpen(false)} onCreate={handleCreate} diff --git a/src/domain/journey/space/pages/SpaceSubspacesPage.tsx b/src/domain/journey/space/pages/SpaceSubspacesPage.tsx index 0d477d2846..b8cbdfea65 100644 --- a/src/domain/journey/space/pages/SpaceSubspacesPage.tsx +++ b/src/domain/journey/space/pages/SpaceSubspacesPage.tsx @@ -2,10 +2,10 @@ import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import useNavigate from '@/core/routing/useNavigate'; import { journeyCardTagsGetter, journeyCardValueGetter } from '@/domain/journey/common/utils/journeyCardValueGetter'; -import { JourneyCreationDialog } from '@/domain/shared/components/JorneyCreationDialog'; -import { JourneyFormValues } from '@/domain/shared/components/JorneyCreationDialog/JourneyCreationForm'; +import { JourneyCreationDialog } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog'; +import { JourneyFormValues } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationForm'; import { EntityPageSection } from '@/domain/shared/layout/EntityPageSection'; -import { useSubspaceCreation } from '@/domain/shared/utils/useJourneyCreation/useJourneyCreation'; +import { useSubspaceCreation } from '@/domain/shared/utils/useSubspaceCreation/useSubspaceCreation'; import ChildJourneyView from '@/domain/journey/common/tabs/Subentities/ChildJourneyView'; import SubspacesContainer from '../containers/SubspacesContainer'; import { useSpace } from '../SpaceContext/useSpace'; @@ -17,6 +17,7 @@ import { SubspaceIcon } from '@/domain/journey/subspace/icon/SubspaceIcon'; import SubspaceCard from '@/domain/journey/subspace/subspaceCard/SubspaceCard'; import { CreateSubspaceForm } from '@/domain/journey/subspace/forms/CreateSubspaceForm'; import useCallouts from '@/domain/collaboration/callout/useCallouts/useCallouts'; +import SubspaceIcon2 from '@/main/ui/icons/SubspaceIcon2'; const SpaceSubspacesPage = () => { const { t } = useTranslation(); @@ -38,7 +39,8 @@ const SpaceSubspacesPage = () => { vision: value.vision, tags: value.tags, addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, + collaborationTemplateId: value.collaborationTemplateId, + visuals: value.visuals, }); if (!result) { @@ -88,7 +90,7 @@ const SpaceSubspacesPage = () => { createSubentityDialog={ } + icon={} journeyName={t('common.subspace')} onClose={() => setCreateDialogOpen(false)} onCreate={handleCreate} diff --git a/src/domain/journey/subspace/forms/CreateSubspaceForm.tsx b/src/domain/journey/subspace/forms/CreateSubspaceForm.tsx index d158438d6e..7f8ac09c06 100644 --- a/src/domain/journey/subspace/forms/CreateSubspaceForm.tsx +++ b/src/domain/journey/subspace/forms/CreateSubspaceForm.tsx @@ -6,24 +6,28 @@ import { MessageWithPayload } from '@/domain/shared/i18n/ValidationMessageTransl import FormikInputField from '@/core/ui/forms/FormikInputField/FormikInputField'; import { SMALL_TEXT_LENGTH, MARKDOWN_TEXT_LENGTH } from '@/core/ui/forms/field-length.constants'; import FormikMarkdownField from '@/core/ui/forms/MarkdownInput/FormikMarkdownField'; -import Gutters from '@/core/ui/grid/Gutters'; import { TagsetField } from '@/domain/platform/admin/components/Common/TagsetSegment'; import FormikEffectFactory from '@/core/ui/forms/FormikEffect'; -import { JourneyCreationForm } from '@/domain/shared/components/JorneyCreationDialog/JourneyCreationForm'; +import { + JourneyCreationForm, + JourneyFormValues, +} from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationForm'; import MarkdownValidator from '@/core/ui/forms/MarkdownInput/MarkdownValidator'; -import { FormikSwitch } from '@/core/ui/forms/FormikSwitch'; +import { FormikRadiosSwitch } from '@/core/ui/forms/FormikRadiosSwitch'; +import SubspaceTemplateSelector from '@/domain/templates/components/TemplateSelectors/SubspaceTemplateSelector'; +import Gutters from '@/core/ui/grid/Gutters'; +import PageContentBlock from '@/core/ui/content/PageContentBlock'; +import FormikVisualUpload from '@/core/ui/upload/FormikVisualUpload/FormikVisualUpload'; +import { VisualType } from '@/core/apollo/generated/graphql-schema'; +import { Theme, useMediaQuery } from '@mui/material'; +import { gutters } from '@/core/ui/grid/utils'; const FormikEffect = FormikEffectFactory(); -type FormValues = { - displayName: string; - tagline: string; - background: string; - vision: string; - tags: string[]; - addTutorialCallouts: boolean; - addCallouts: boolean; -}; +type FormValues = Pick< + JourneyFormValues, + 'displayName' | 'tagline' | 'background' | 'tags' | 'addTutorialCallouts' | 'collaborationTemplateId' | 'visuals' +>; interface CreateSubspaceFormProps extends JourneyCreationForm {} @@ -33,6 +37,7 @@ export const CreateSubspaceForm = ({ onChanged, }: PropsWithChildren) => { const { t } = useTranslation(); + const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm')); const validationRequiredString = t('forms.validations.required'); @@ -41,20 +46,23 @@ export const CreateSubspaceForm = ({ displayName: value.displayName, tagline: value.tagline, background: value.background, - vision: value.vision, tags: value.tags, addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, + collaborationTemplateId: value.collaborationTemplateId, + visuals: value.visuals, }); const initialValues: FormValues = { displayName: '', tagline: '', background: '', - vision: '', tags: [], addTutorialCallouts: false, - addCallouts: true, + collaborationTemplateId: undefined, + visuals: { + avatar: { file: undefined, altText: '' }, + cardBanner: { file: undefined, altText: '' }, + }, }; const validationSchema = yup.object().shape({ @@ -68,11 +76,10 @@ export const CreateSubspaceForm = ({ .string() .trim() .min(3, MessageWithPayload('forms.validations.minLength')) - .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')) - .required(validationRequiredString), - background: MarkdownValidator(MARKDOWN_TEXT_LENGTH).trim().required(validationRequiredString), - vision: MarkdownValidator(MARKDOWN_TEXT_LENGTH).trim().required(validationRequiredString), + .max(SMALL_TEXT_LENGTH, MessageWithPayload('forms.validations.maxLength')), + background: MarkdownValidator(MARKDOWN_TEXT_LENGTH), tags: yup.array().of(yup.string().min(2)).notRequired(), + collaborationTemplateId: yup.string().nullable(), }); return ( @@ -85,48 +92,53 @@ export const CreateSubspaceForm = ({ > {() => (
- - - - - - - + + + + + `${gutters()(theme)} 0 0 0`}> + + + + + + - - )} diff --git a/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage.tsx b/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage.tsx index 29fbdc623b..a51fa07fea 100644 --- a/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage.tsx +++ b/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfilePage.tsx @@ -3,15 +3,18 @@ import React, { FC } from 'react'; import { SettingsSection } from '@/domain/platform/admin/layout/EntitySettingsLayout/constants'; import { SettingsPageProps } from '@/domain/platform/admin/layout/EntitySettingsLayout/types'; import SubspaceProfileView from './SubspaceProfileView'; -import FormMode from '@/domain/platform/admin/components/FormMode'; import SubspaceSettingsLayout from '@/domain/platform/admin/subspace/SubspaceSettingsLayout'; +import { useRouteResolver } from '@/main/routing/resolvers/RouteResolver'; + +const SubspaceProfilePage: FC = ({ routePrefix = '../' }) => { + const { journeyPath } = useRouteResolver(); + const subspaceId = journeyPath[journeyPath.length - 1]; -const ChallengeProfilePage: FC = ({ routePrefix = '../' }) => { return ( - + ); }; -export default ChallengeProfilePage; +export default SubspaceProfilePage; diff --git a/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfileView.tsx b/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfileView.tsx index 06bd393d15..7c14541ef3 100644 --- a/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfileView.tsx +++ b/src/domain/journey/subspace/pages/SubspaceProfile/SubspaceProfileView.tsx @@ -2,127 +2,76 @@ import { Grid } from '@mui/material'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useNotification } from '@/core/ui/notifications/useNotification'; -import { useUrlParams } from '@/core/routing/useUrlParams'; import { - refetchAdminSpaceSubspacesPageQuery, - refetchDashboardWithMembershipsQuery, refetchSubspaceProfileInfoQuery, - useCreateSubspaceMutation, useSubspaceProfileInfoQuery, useUpdateSpaceMutation, } from '@/core/apollo/generated/apollo-hooks'; import SaveButton from '@/core/ui/actions/SaveButton'; import WrapperTypography from '@/core/ui/typography/deprecated/WrapperTypography'; -import FormMode from '@/domain/platform/admin/components/FormMode'; import ProfileForm, { ProfileFormValues } from '@/domain/common/profile/ProfileForm'; import EditVisualsView from '@/domain/common/visual/EditVisuals/EditVisualsView'; import { formatDatabaseLocation } from '@/domain/common/location/LocationUtils'; import Gutters from '@/core/ui/grid/Gutters'; import { VisualType } from '@/core/apollo/generated/graphql-schema'; -import { useRouteResolver } from '@/main/routing/resolvers/RouteResolver'; -import useNavigate from '@/core/routing/useNavigate'; -import { buildSettingsUrl } from '@/main/routing/urlBuilders'; interface ChallengeProfileViewProps { - mode: FormMode; + subspaceId: string; } -const SubspaceProfileView: FC = ({ mode }) => { +const SubspaceProfileView: FC = ({ subspaceId }) => { const { t } = useTranslation(); - const navigate = useNavigate(); const notify = useNotification(); const onSuccess = (message: string) => notify(message, 'success'); - const { spaceNameId = '' } = useUrlParams(); - - const { subSpaceId: challengeId } = useRouteResolver(); - - const [createSubspace, { loading: isCreating }] = useCreateSubspaceMutation({ - onCompleted: data => { - onSuccess('Successfully created'); - navigate(buildSettingsUrl(data.createSubspace.profile.url), { replace: true }); - }, - refetchQueries: [ - refetchAdminSpaceSubspacesPageQuery({ spaceId: spaceNameId }), - refetchDashboardWithMembershipsQuery(), - ], - awaitRefetchQueries: true, - }); - const [updateSubspace, { loading: isUpdating }] = useUpdateSpaceMutation({ onCompleted: () => onSuccess('Successfully updated'), - refetchQueries: [refetchSubspaceProfileInfoQuery({ subspaceId: challengeId! })], + refetchQueries: [refetchSubspaceProfileInfoQuery({ subspaceId: subspaceId! })], awaitRefetchQueries: true, }); const { data: subspaceProfile } = useSubspaceProfileInfoQuery({ - variables: { subspaceId: challengeId! }, - skip: mode === FormMode.create || !challengeId, + variables: { subspaceId: subspaceId! }, + skip: !subspaceId, }); const challenge = subspaceProfile?.lookup.space; - const isLoading = isCreating || isUpdating; + const isLoading = isUpdating; const onSubmit = async (values: ProfileFormValues) => { const { name: displayName, nameID, tagsets, tagline, references } = values; - // TODO: We need to select the template for the user if they want and put it in the collaborationData passing what we get using the createInput service - - switch (mode) { - case FormMode.create: - createSubspace({ - variables: { - input: { - nameID: nameID, - profileData: { - displayName, - tagline, - location: formatDatabaseLocation(values.location), - }, - spaceID: spaceNameId, - tags: tagsets.flatMap(x => x.tags), - collaborationData: {}, - }, - }, - }); - break; - case FormMode.update: { - if (!challengeId) { - throw new Error('Challenge ID is required for update'); - } - updateSubspace({ - variables: { - input: { - ID: challengeId, - nameID: nameID, - profileData: { - displayName, - tagline, - location: formatDatabaseLocation(values.location), - tagsets: tagsets.map(tagset => ({ ID: tagset.id, name: tagset.name, tags: tagset.tags })), - references: references.map(reference => ({ - ID: reference.id, - name: reference.name, - description: reference.description, - uri: reference.uri, - })), - }, - }, - }, - }); - break; - } - default: - throw new Error(`Submit mode expected: (${mode}) found`); + if (!subspaceId) { + throw new Error('Challenge ID is required for update'); } + updateSubspace({ + variables: { + input: { + ID: subspaceId, + nameID: nameID, + profileData: { + displayName, + tagline, + location: formatDatabaseLocation(values.location), + tagsets: tagsets.map(tagset => ({ ID: tagset.id, name: tagset.name, tags: tagset.tags })), + references: references.map(reference => ({ + ID: reference.id, + name: reference.name, + description: reference.description, + uri: reference.uri, + })), + }, + }, + }, + }); }; let submitWired; return ( { - const { data } = await createSubspace({ - variables: { - input: { - spaceID: parentSpaceId, - context: { - vision: value.vision, - }, - profileData: { - displayName: value.displayName, - tagline: value.tagline, - }, - tags: value.tags, - collaborationData: { - addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, - }, - }, - }, + const result = await createSubspace({ + spaceID: parentSpaceId, + displayName: value.displayName, + tagline: value.tagline, + background: value.background ?? '', + vision: value.vision, + tags: value.tags, + addTutorialCallouts: value.addTutorialCallouts, + collaborationTemplateId: value.collaborationTemplateId, + visuals: value.visuals, }); - if (!data?.createSubspace) { + if (!result) { return; } - if (data?.createSubspace.profile.url) { - navigate(data?.createSubspace.profile.url); - onClose(); - } + navigate(result.profile.url); + onClose(); }, [navigate, createSubspace, parentSpaceId] ); return ( - <> - - + } + open={isVisible} + journeyName={t('common.subspace')} + onClose={onClose} + onCreate={handleCreate} + formComponent={CreateSubspaceForm} + /> ); }; diff --git a/src/domain/platform/admin/opportunity/pages/OpportunityList.tsx b/src/domain/platform/admin/opportunity/pages/OpportunityList.tsx index 32b9e8bbc5..27471bf092 100644 --- a/src/domain/platform/admin/opportunity/pages/OpportunityList.tsx +++ b/src/domain/platform/admin/opportunity/pages/OpportunityList.tsx @@ -9,15 +9,12 @@ import Loading from '@/core/ui/loading/Loading'; import { useNotification } from '@/core/ui/notifications/useNotification'; import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; import { useSubSpace } from '@/domain/journey/subspace/hooks/useSubSpace'; -import { useUrlParams } from '@/core/routing/useUrlParams'; -import { JourneyCreationDialog } from '@/domain/shared/components/JorneyCreationDialog'; -import { CreateOpportunityForm } from '@/domain/journey/opportunity/forms/CreateOpportunityForm'; +import { JourneyCreationDialog } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog'; import { buildSettingsUrl } from '@/main/routing/urlBuilders'; -import { JourneyFormValues } from '@/domain/shared/components/JorneyCreationDialog/JourneyCreationForm'; +import { JourneyFormValues } from '@/domain/shared/components/JourneyCreationDialog/JourneyCreationForm'; import { OpportunityIcon } from '@/domain/journey/opportunity/icon/OpportunityIcon'; import { refetchSubspacesInSpaceQuery, - useCreateSubspaceMutation, useDeleteSpaceMutation, useSpaceCollaborationIdLazyQuery, useSpaceTemplatesSetIdQuery, @@ -31,13 +28,14 @@ import { useCreateCollaborationTemplate } from '@/domain/templates/hooks/useCrea import { CollaborationTemplateFormSubmittedValues } from '@/domain/templates/components/Forms/CollaborationTemplateForm'; import CreateTemplateDialog from '@/domain/templates/components/Dialogs/CreateEditTemplateDialog/CreateTemplateDialog'; import { AuthorizationPrivilege, TemplateType } from '@/core/apollo/generated/graphql-schema'; +import { CreateSubspaceForm } from '@/domain/journey/subspace/forms/CreateSubspaceForm'; +import { useSubspaceCreation } from '@/domain/shared/utils/useSubspaceCreation/useSubspaceCreation'; export const OpportunityList: FC = () => { const { t } = useTranslation(); const notify = useNotification(); const { spaceNameId } = useSpace(); const { subspaceId } = useSubSpace(); - const { challengeNameId = '' } = useUrlParams(); const navigate = useNavigate(); const [open, setOpen] = useState(false); const [saveAsTemplateDialogSelectedItem, setSaveAsTemplateDialogSelectedItem] = useState(); @@ -82,44 +80,35 @@ export const OpportunityList: FC = () => { }); }; - const [createSubspace] = useCreateSubspaceMutation({ - refetchQueries: [refetchSubspacesInSpaceQuery({ spaceId: subspaceId })], - awaitRefetchQueries: true, + const { createSubspace } = useSubspaceCreation({ onCompleted: () => { notify(t('pages.admin.subsubspace.notifications.subsubspace-created'), 'success'); }, + refetchQueries: [refetchSubspacesInSpaceQuery({ spaceId: subspaceId })], + awaitRefetchQueries: true, }); const handleCreate = useCallback( async (value: JourneyFormValues) => { - const { data } = await createSubspace({ - variables: { - input: { - spaceID: subspaceId, - context: { - vision: value.vision, - }, - profileData: { - displayName: value.displayName, - tagline: value.tagline, - }, - tags: value.tags, - collaborationData: { - addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, - }, - }, - }, + const result = await createSubspace({ + spaceID: subspaceId, + displayName: value.displayName, + tagline: value.tagline, + background: value.background ?? '', + vision: value.vision, + tags: value.tags, + addTutorialCallouts: value.addTutorialCallouts, + collaborationTemplateId: value.collaborationTemplateId, + visuals: value.visuals, }); - if (!data?.createSubspace) { + if (!result?.profile.url) { + notify(t('pages.admin.subsubspace.notifications.error-creating-subsubspace'), 'error'); return; } - if (data?.createSubspace.profile.url) { - navigate(buildSettingsUrl(data?.createSubspace.profile.url)); - } + navigate(buildSettingsUrl(result.profile.url)); }, - [navigate, createSubspace, spaceNameId, subspaceId, challengeNameId] + [navigate, createSubspace, subspaceId] ); // check for TemplateCreation privileges @@ -210,7 +199,7 @@ export const OpportunityList: FC = () => { journeyName={t('common.subspace')} onClose={() => setOpen(false)} onCreate={handleCreate} - formComponent={CreateOpportunityForm} + formComponent={CreateSubspaceForm} /> ( - - - -); +const OpportunityProfilePage = ({ routePrefix = '../' }: SettingsPageProps) => { + const { journeyPath } = useRouteResolver(); + const subspaceId = journeyPath[journeyPath.length - 1]; + return ( + + + + ); +}; export default OpportunityProfilePage; diff --git a/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx b/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx deleted file mode 100644 index 84ec260f09..0000000000 --- a/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { Grid, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import FormMode from '@/domain/platform/admin/components/FormMode'; -import ProfileForm, { ProfileFormValues } from '@/domain/common/profile/ProfileForm'; -import { useNotification } from '@/core/ui/notifications/useNotification'; -import EditVisualsView from '@/domain/common/visual/EditVisuals/EditVisualsView'; -import { formatDatabaseLocation } from '@/domain/common/location/LocationUtils'; -import SaveButton from '@/core/ui/actions/SaveButton'; -import Gutters from '@/core/ui/grid/Gutters'; -import { VisualType } from '@/core/apollo/generated/graphql-schema'; -import { useRouteResolver } from '@/main/routing/resolvers/RouteResolver'; -import useNavigate from '@/core/routing/useNavigate'; -import { - refetchSubspaceProfileInfoQuery, - refetchSubspacesInSpaceQuery, - useCreateSubspaceMutation, - useSubspaceProfileInfoQuery, - useUpdateSpaceMutation, -} from '@/core/apollo/generated/apollo-hooks'; - -// TODO: Probably this file should be removed (subspace?) -/** - * @deprecated - */ -const OpportunityProfileView = ({ mode }: { mode: FormMode }) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const notify = useNotification(); - const onSuccess = (message: string) => notify(message, 'success'); - - const { subSpaceId: challengeId, subSubSpaceId: opportunityId } = useRouteResolver(); - - const [createSubspace, { loading: isCreating }] = useCreateSubspaceMutation({ - refetchQueries: [refetchSubspacesInSpaceQuery({ spaceId: challengeId! })], - awaitRefetchQueries: true, - onCompleted: data => { - onSuccess('Successfully created'); - navigate(data.createSubspace.profile.url, { replace: true }); - }, - }); - - const [updateSubspace, { loading: isUpdating }] = useUpdateSpaceMutation({ - onCompleted: () => onSuccess('Successfully updated'), - refetchQueries: [refetchSubspaceProfileInfoQuery({ subspaceId: opportunityId! })], - awaitRefetchQueries: true, - }); - - const { data: opportunityProfile } = useSubspaceProfileInfoQuery({ - variables: { subspaceId: opportunityId! }, - skip: !opportunityId || mode === FormMode.create, - }); - - const opportunity = opportunityProfile?.lookup.space; - - const isLoading = isCreating || isUpdating; - - const onSubmit = async (values: ProfileFormValues) => { - const { name: displayName, tagline, nameID, tagsets, references } = values; - - if (!challengeId) { - throw new Error('Challenge ID is required'); - } - // TODO: We need to select the template for the user if they want and put it in the collaborationData passing what we get using the createInput service - switch (mode) { - case FormMode.create: - createSubspace({ - variables: { - input: { - nameID: nameID, - profileData: { - displayName, - location: formatDatabaseLocation(values.location), - }, - spaceID: challengeId, - tags: tagsets.flatMap(x => x.tags), - collaborationData: {}, - }, - }, - }); - break; - case FormMode.update: { - if (!opportunity) { - throw new Error('Opportunity is not loaded'); - } - - updateSubspace({ - variables: { - input: { - nameID: nameID, - ID: opportunity.id, - profileData: { - displayName, - tagline, - location: formatDatabaseLocation(values.location), - tagsets: tagsets.map(tagset => ({ ID: tagset.id, name: tagset.name, tags: tagset.tags })), - references: references.map(reference => ({ - ID: reference.id, - name: reference.name, - description: reference.description, - uri: reference.uri, - })), - }, - }, - }, - }); - break; - } - default: - throw new Error(`Submit mode expected: (${mode}) found`); - } - }; - - let submitWired; - return ( - - (submitWired = submit)} - /> - - submitWired()} /> - - - - {t('components.visualSegment.title')} - - - - - ); -}; - -export default OpportunityProfileView; diff --git a/src/domain/shared/components/JorneyCreationDialog/index.ts b/src/domain/shared/components/JorneyCreationDialog/index.ts deleted file mode 100644 index 97ae740605..0000000000 --- a/src/domain/shared/components/JorneyCreationDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './JourneyCreationDialog'; diff --git a/src/domain/shared/components/JorneyCreationDialog/JourneyCreationDialog.tsx b/src/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog.tsx similarity index 70% rename from src/domain/shared/components/JorneyCreationDialog/JourneyCreationDialog.tsx rename to src/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog.tsx index c643d11c28..7088720f25 100644 --- a/src/domain/shared/components/JorneyCreationDialog/JourneyCreationDialog.tsx +++ b/src/domain/shared/components/JourneyCreationDialog/JourneyCreationDialog.tsx @@ -1,15 +1,16 @@ -import React, { FC, useState } from 'react'; +import React, { FC, ReactElement, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Dialog } from '@mui/material'; -import Box from '@mui/material/Box'; +import { DialogActions, SvgIconProps } from '@mui/material'; import Button from '@mui/material/Button'; import LoadingButton from '@mui/lab/LoadingButton'; -import { DialogActions, DialogContent, DialogTitle } from '@/core/ui/dialog/deprecated'; +import { DialogContent } from '@/core/ui/dialog/deprecated'; import { JourneyCreationForm, JourneyFormValues } from './JourneyCreationForm'; +import DialogHeader from '@/core/ui/dialog/DialogHeader'; +import DialogWithGrid from '@/core/ui/dialog/DialogWithGrid'; interface JourneyCreationDialogProps { open: boolean; - icon?: React.ReactNode; + icon?: ReactElement; journeyName: string; onClose: () => void; onCreate: (value: JourneyFormValues) => Promise; @@ -30,10 +31,13 @@ export const JourneyCreationDialog: FC = ({ const [value, setValue] = useState({ displayName: '', tagline: '', - vision: '', tags: [], addTutorialCallouts: false, - addCallouts: true, + collaborationTemplateId: undefined, + visuals: { + avatar: { file: undefined, altText: '' }, + cardBanner: { file: undefined, altText: '' }, + }, }); const handleChange = (value: JourneyFormValues) => setValue(value); @@ -45,14 +49,11 @@ export const JourneyCreationDialog: FC = ({ }; return ( - - - - {icon} - {t('journey-creation.dialog-title', { entity: journeyName })} - - - + + + {t('journey-creation.dialog-title', { entity: journeyName })} + + @@ -63,13 +64,12 @@ export const JourneyCreationDialog: FC = ({ onClick={handleCreate} variant="contained" loading={submitting} - loadingIndicator={`${t('buttons.create')}...`} disabled={formInvalid} sx={{ alignSelf: 'end' }} > {t('buttons.create')} - + ); }; diff --git a/src/domain/shared/components/JorneyCreationDialog/JourneyCreationForm.ts b/src/domain/shared/components/JourneyCreationDialog/JourneyCreationForm.ts similarity index 60% rename from src/domain/shared/components/JorneyCreationDialog/JourneyCreationForm.ts rename to src/domain/shared/components/JourneyCreationDialog/JourneyCreationForm.ts index 226b56f449..058e48200c 100644 --- a/src/domain/shared/components/JorneyCreationDialog/JourneyCreationForm.ts +++ b/src/domain/shared/components/JourneyCreationDialog/JourneyCreationForm.ts @@ -1,11 +1,20 @@ +interface VisualUpload { + file: File | undefined; + altText?: string; +} + export interface JourneyFormValues { displayName: string; tagline: string; - vision: string; + vision?: string; tags: string[]; background?: string; addTutorialCallouts: boolean; - addCallouts: boolean; + collaborationTemplateId?: string; + visuals: { + avatar: VisualUpload; + cardBanner: VisualUpload; + }; } export interface JourneyCreationForm { diff --git a/src/domain/shared/utils/useJourneyCreation/createSubspace.graphql b/src/domain/shared/utils/useJourneyCreation/createSubspace.graphql deleted file mode 100644 index 054116bcfd..0000000000 --- a/src/domain/shared/utils/useJourneyCreation/createSubspace.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation createSubspace($input: CreateSubspaceInput!) { - createSubspace(subspaceData: $input) { - ...SubspaceCard - } -} diff --git a/src/domain/shared/utils/useSubspaceCreation/createSubspace.graphql b/src/domain/shared/utils/useSubspaceCreation/createSubspace.graphql new file mode 100644 index 0000000000..8b854429a2 --- /dev/null +++ b/src/domain/shared/utils/useSubspaceCreation/createSubspace.graphql @@ -0,0 +1,14 @@ +mutation createSubspace($input: CreateSubspaceInput!, $includeVisuals: Boolean = false) { + createSubspace(subspaceData: $input) { + ...SubspaceCard + visuals: profile @include(if: $includeVisuals) { + id + cardBanner: visual(type: CARD) { + id + } + avatar: visual(type: AVATAR) { + id + } + } + } +} diff --git a/src/domain/shared/utils/useJourneyCreation/useJourneyCreation.ts b/src/domain/shared/utils/useSubspaceCreation/useSubspaceCreation.ts similarity index 56% rename from src/domain/shared/utils/useJourneyCreation/useJourneyCreation.ts rename to src/domain/shared/utils/useSubspaceCreation/useSubspaceCreation.ts index b890315c51..59760d0d1e 100644 --- a/src/domain/shared/utils/useJourneyCreation/useJourneyCreation.ts +++ b/src/domain/shared/utils/useSubspaceCreation/useSubspaceCreation.ts @@ -1,9 +1,11 @@ import { useCallback } from 'react'; import { + CreateSubspaceMutationOptions, refetchDashboardWithMembershipsQuery, refetchUserProviderQuery, SubspaceCardFragmentDoc, useCreateSubspaceMutation, + useUploadVisualMutation, } from '@/core/apollo/generated/apollo-hooks'; import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; import { useConfig } from '@/domain/platform/config/useConfig'; @@ -13,6 +15,7 @@ import { SpacePrivacyMode, TagsetType, } from '@/core/apollo/generated/graphql-schema'; +import { error as logError } from '@/core/logging/sentry/log'; import { DEFAULT_TAGSET } from '@/domain/common/tags/tagset.constants'; interface SubspaceCreationInput { @@ -20,19 +23,35 @@ interface SubspaceCreationInput { displayName: string; tagline: string; background?: string; - vision: string; + vision?: string; tags: string[]; addTutorialCallouts: boolean; - addCallouts: boolean; + collaborationTemplateId?: string; + visuals: { + avatar: { + file: File | undefined; + altText?: string; + }; + cardBanner: { + file: File | undefined; + altText?: string; + }; + }; } -export const useSubspaceCreation = () => { +export const useSubspaceCreation = (mutationOptions: CreateSubspaceMutationOptions = {}) => { const { spaceId } = useSpace(); const { isFeatureEnabled } = useConfig(); const subscriptionsEnabled = isFeatureEnabled(PlatformFeatureFlagName.Subscriptions); + const [uploadVisual] = useUploadVisualMutation(); - const [createSubspaceLazy] = useCreateSubspaceMutation({ + const { + refetchQueries = [refetchUserProviderQuery(), refetchDashboardWithMembershipsQuery()], // default to refetching user provider and dashboard + ...restMutationOptions + } = mutationOptions; + + const [createSubspaceLazy, { loading }] = useCreateSubspaceMutation({ update: (cache, { data }) => { if (subscriptionsEnabled || !data) { return; @@ -63,13 +82,15 @@ export const useSubspaceCreation = () => { }, }); }, - - refetchQueries: [refetchUserProviderQuery(), refetchDashboardWithMembershipsQuery()], + refetchQueries, + ...restMutationOptions, }); // add useCallback const createSubspace = useCallback( async (value: SubspaceCreationInput) => { + const includeVisuals = Boolean(value.visuals.cardBanner.file) || Boolean(value.visuals.avatar.file); + const { data } = await createSubspaceLazy({ variables: { input: { @@ -85,9 +106,11 @@ export const useSubspaceCreation = () => { tags: value.tags, collaborationData: { addTutorialCallouts: value.addTutorialCallouts, - addCallouts: value.addCallouts, + addCallouts: true, // Always add Callouts from the template + collaborationTemplateID: value.collaborationTemplateId, }, }, + includeVisuals, }, optimisticResponse: { createSubspace: { @@ -133,14 +156,59 @@ export const useSubspaceCreation = () => { mode: SpacePrivacyMode.Public, }, }, + visuals: { + id: '', + cardBanner: { + id: '', + }, + avatar: { + id: '', + }, + }, }, }, }); - + try { + const uploadPromises: Promise[] = []; + if (value.visuals.avatar.file && data?.createSubspace.visuals.avatar?.id) { + uploadPromises.push( + uploadVisual({ + variables: { + file: value.visuals.avatar.file, + uploadData: { + visualID: data.createSubspace.visuals.avatar.id, + alternativeText: value.visuals.avatar.altText, + }, + }, + }) + ); + } + if (value.visuals.cardBanner.file && data?.createSubspace.visuals.cardBanner?.id) { + uploadPromises.push( + uploadVisual({ + variables: { + file: value.visuals.cardBanner.file, + uploadData: { + visualID: data.createSubspace.visuals.cardBanner.id, + alternativeText: value.visuals.cardBanner.altText, + }, + }, + }) + ); + } + await Promise.all(uploadPromises); + } catch (error) { + // Subspace is created anyway, just the images failed log the error and continue + if (error instanceof Error) { + logError(error); + } else { + logError(`Error uploading visuals for subspace: ${error}`, { label: 'TempStorage' }); + } + } return data?.createSubspace; }, - [createSubspaceLazy] + [createSubspaceLazy, uploadVisual] ); - return { createSubspace }; + return { createSubspace, loading }; }; diff --git a/src/domain/templates/components/CalloutForm/PostTemplateSelector.tsx b/src/domain/templates/components/TemplateSelectors/PostTemplateSelector.tsx similarity index 100% rename from src/domain/templates/components/CalloutForm/PostTemplateSelector.tsx rename to src/domain/templates/components/TemplateSelectors/PostTemplateSelector.tsx diff --git a/src/domain/templates/components/TemplateSelectors/SubspaceTemplateSelector.tsx b/src/domain/templates/components/TemplateSelectors/SubspaceTemplateSelector.tsx new file mode 100644 index 0000000000..c1d9685a72 --- /dev/null +++ b/src/domain/templates/components/TemplateSelectors/SubspaceTemplateSelector.tsx @@ -0,0 +1,84 @@ +import { Box, Button, Skeleton } from '@mui/material'; +import { useField } from 'formik'; +import { FC, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BlockSectionTitle, Text } from '@/core/ui/typography'; +import ImportTemplatesDialog from '../Dialogs/ImportTemplateDialog/ImportTemplatesDialog'; +import { LoadingButton } from '@mui/lab'; +import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; +import { LibraryIcon } from '@/domain/templates/LibraryIcon'; +import { TemplateDefaultType, TemplateType } from '@/core/apollo/generated/graphql-schema'; +import { useSpaceDefaultTemplatesQuery, useTemplateNameQuery } from '@/core/apollo/generated/apollo-hooks'; +import { Identifiable } from '@/core/utils/Identifiable'; +import Gutters, { GuttersProps } from '@/core/ui/grid/Gutters'; +import { useSpace } from '@/domain/journey/space/SpaceContext/useSpace'; + +interface SubspaceTemplateSelectorProps extends GuttersProps { + name: string; +} + +export const SubspaceTemplateSelector: FC = ({ name, ...rest }) => { + const { t } = useTranslation(); + const { spaceId, loading: loadingSpace } = useSpace(); + const [isDialogOpen, setDialogOpen] = useState(false); + const [field, , helpers] = useField(name); + + const templateId: string | undefined = typeof field.value === 'string' ? field.value : undefined; + + const { data: templateData, loading: loadingTemplate } = useTemplateNameQuery({ + variables: { templateId: templateId! }, + skip: !templateId, + }); + + const { data: defaultSpaceTemplatesData, loading: loadingSpaceTemplate } = useSpaceDefaultTemplatesQuery({ + variables: { spaceId: spaceId! }, + skip: !spaceId, + }); + + const templateName = useMemo(() => { + const selectedTemplate = templateData?.lookup.template?.profile.displayName; + const defaultSpaceTemplate = defaultSpaceTemplatesData?.lookup.space?.templatesManager?.templateDefaults.find( + templateDefault => templateDefault.type === TemplateDefaultType.SpaceSubspace + )?.template?.profile.displayName; + const defaultPlatformTemplate = t('context.subspace.template.defaultTemplate'); + return selectedTemplate ?? defaultSpaceTemplate ?? defaultPlatformTemplate; + }, [templateId, templateData, defaultSpaceTemplatesData, t]); + + const handleSelectTemplate = async ({ id: templateId }: Identifiable): Promise => { + helpers.setValue(templateId); + setDialogOpen(false); + }; + + const loading = loadingSpace || loadingTemplate || loadingSpaceTemplate; + + return ( + + {t('context.subspace.template.title')} + {loading ? : {templateName}} + + + } variant="contained"> + {t('buttons.use')} + + } + open={isDialogOpen} + onSelectTemplate={handleSelectTemplate} + onClose={() => setDialogOpen(false)} + enablePlatformTemplates + /> + + + ); +}; + +export default SubspaceTemplateSelector; diff --git a/src/domain/templates/components/CalloutForm/WhiteboardTemplateSelector.tsx b/src/domain/templates/components/TemplateSelectors/WhiteboardTemplateSelector.tsx similarity index 100% rename from src/domain/templates/components/CalloutForm/WhiteboardTemplateSelector.tsx rename to src/domain/templates/components/TemplateSelectors/WhiteboardTemplateSelector.tsx diff --git a/src/domain/templates/graphql/SpaceDefaultTemplates.graphql b/src/domain/templates/graphql/SpaceDefaultTemplates.graphql new file mode 100644 index 0000000000..529f016d68 --- /dev/null +++ b/src/domain/templates/graphql/SpaceDefaultTemplates.graphql @@ -0,0 +1,21 @@ +query SpaceDefaultTemplates ($spaceId: UUID!) { + lookup { + space(ID: $spaceId) { + id + templatesManager { + id + templateDefaults { + id + type + template { + id + profile { + id + displayName + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/domain/templates/graphql/TemplateName.graphql b/src/domain/templates/graphql/TemplateName.graphql new file mode 100644 index 0000000000..26f47dcd2f --- /dev/null +++ b/src/domain/templates/graphql/TemplateName.graphql @@ -0,0 +1,11 @@ +query TemplateName($templateId: UUID!) { + lookup { + template(ID: $templateId) { + id + profile { + id + displayName + } + } + } +} \ No newline at end of file diff --git a/src/domain/templates/hooks/useCreateCollaborationTemplate.ts b/src/domain/templates/hooks/useCreateCollaborationTemplate.ts index c2fb3701c0..6353b8be1f 100644 --- a/src/domain/templates/hooks/useCreateCollaborationTemplate.ts +++ b/src/domain/templates/hooks/useCreateCollaborationTemplate.ts @@ -9,7 +9,7 @@ import { toCreateTemplateFromCollaborationMutationVariables } from '../component export interface CollaborationCreationUtils { handleCreateCollaborationTemplate: ( values: CollaborationTemplateFormSubmittedValues, - spaceNameId: string + destinationSpaceNameId: string ) => Promise; } @@ -18,8 +18,8 @@ export const useCreateCollaborationTemplate = (): CollaborationCreationUtils => const [fetchTemplatesSetId] = useSpaceTemplatesSetIdLazyQuery(); const handleCreateCollaborationTemplate = useCallback( - async (values: CollaborationTemplateFormSubmittedValues, spaceNameId: string) => { - const { data: templatesData } = await fetchTemplatesSetId({ variables: { spaceNameId } }); + async (values: CollaborationTemplateFormSubmittedValues, destinationSpaceNameId: string) => { + const { data: templatesData } = await fetchTemplatesSetId({ variables: { spaceNameId: destinationSpaceNameId } }); const templatesSetId = templatesData?.space.templatesManager?.templatesSet?.id; if (!templatesSetId) { throw new TypeError('TemplateSet not found!'); diff --git a/src/main/topLevelPages/myDashboard/DashboardWithMemberships/DashboardSpaces/DashboardSpaces.tsx b/src/main/topLevelPages/myDashboard/DashboardWithMemberships/DashboardSpaces/DashboardSpaces.tsx index a6a595b3eb..4c94660ef6 100644 --- a/src/main/topLevelPages/myDashboard/DashboardWithMemberships/DashboardSpaces/DashboardSpaces.tsx +++ b/src/main/topLevelPages/myDashboard/DashboardWithMemberships/DashboardSpaces/DashboardSpaces.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'; import { Paper, Button, Avatar } from '@mui/material'; import { Card } from '@mui/material'; import { DoubleArrowOutlined } from '@mui/icons-material'; - import Gutters from '@/core/ui/grid/Gutters'; import GridItem from '@/core/ui/grid/GridItem'; import Loading from '@/core/ui/loading/Loading'; @@ -12,11 +11,10 @@ import { Caption, Tagline } from '@/core/ui/typography'; import { MyMembershipsDialog } from '@/main/topLevelPages/myDashboard/myMemberships/MyMembershipsDialog'; import PageContentBlock from '@/core/ui/content/PageContentBlock'; import JourneyTile from '@/domain/journey/common/JourneyTile/JourneyTile'; -import defaultJourneyBanner from '@/domain/journey/defaultVisuals/Banner.jpg'; - +import { SpacePrivacyMode, VisualType } from '@/core/apollo/generated/graphql-schema'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; import { useDashboardSpaces } from './useDashboardSpaces'; import { gutters } from '@/core/ui/grid/utils'; -import { SpacePrivacyMode } from '@/core/apollo/generated/graphql-schema'; const DashboardSpaces = () => { const { @@ -61,7 +59,7 @@ const DashboardSpaces = () => { variant="square" sx={spaceCardMedia} alt={profile?.displayName} - src={profile?.spaceBanner?.uri || defaultJourneyBanner} + src={profile?.spaceBanner?.uri || defaultVisualUrls[VisualType.Banner]} /> diff --git a/src/main/topLevelPages/myDashboard/latestContributions/myLatestContributions/MyLatestContributions.tsx b/src/main/topLevelPages/myDashboard/latestContributions/myLatestContributions/MyLatestContributions.tsx index 19b5f84d4c..a5a8dbcf9d 100644 --- a/src/main/topLevelPages/myDashboard/latestContributions/myLatestContributions/MyLatestContributions.tsx +++ b/src/main/topLevelPages/myDashboard/latestContributions/myLatestContributions/MyLatestContributions.tsx @@ -6,6 +6,7 @@ import { ActivityEventType, ActivityLogCalloutWhiteboardContentModifiedFragment, ActivityLogCalloutWhiteboardCreatedFragment, + VisualType, } from '@/core/apollo/generated/graphql-schema'; import { Box, SelectChangeEvent } from '@mui/material'; import { @@ -13,7 +14,7 @@ import { ActivityViewChooser, } from '@/domain/collaboration/activity/ActivityLog/ActivityComponent'; import { CaptionSmall } from '@/core/ui/typography/components'; -import defaultJourneyAvatar from '@/domain/journey/defaultVisuals/Avatar.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; import { LatestContributionsProps, SPACE_OPTION_ALL } from '../LatestContributionsProps'; import { SelectOption } from '@mui/base'; import SeamlessSelect from '@/core/ui/forms/select/SeamlessSelect'; @@ -113,7 +114,7 @@ const MyLatestContributions = ({ spaceMemberships }: LatestContributionsProps) = ); })} diff --git a/src/main/topLevelPages/myDashboard/myMemberships/ExpandableSpaceTree.tsx b/src/main/topLevelPages/myDashboard/myMemberships/ExpandableSpaceTree.tsx index 0936117b0a..4f1723f2b2 100644 --- a/src/main/topLevelPages/myDashboard/myMemberships/ExpandableSpaceTree.tsx +++ b/src/main/topLevelPages/myDashboard/myMemberships/ExpandableSpaceTree.tsx @@ -16,9 +16,9 @@ import { gutters } from '@/core/ui/grid/utils'; import { MembershipProps } from './MyMembershipsDialog.model'; import { useColumns } from '@/core/ui/grid/GridContext'; import webkitLineClamp from '@/core/ui/utils/webkitLineClamp'; -import { SpaceLevel, CommunityRoleType } from '@/core/apollo/generated/graphql-schema'; +import { SpaceLevel, CommunityRoleType, VisualType } from '@/core/apollo/generated/graphql-schema'; -import defaultCardBanner from '@/domain/journey/defaultVisuals/Card.jpg'; +import { defaultVisualUrls } from '@/domain/journey/defaultVisuals/defaultVisualUrls'; const VISIBLE_COMMUNITY_ROLES = [CommunityRoleType.Admin, CommunityRoleType.Lead]; @@ -68,7 +68,7 @@ export const ExpandableSpaceTree = ({ membership }: { membership: MembershipProp component={RouterLink} visual={ @@ -99,7 +99,7 @@ export const ExpandableSpaceTree = ({ membership }: { membership: MembershipProp endIcon={isExpanded ? : } onClick={toggleExpanded} aria-expanded={isExpanded} - area-label={isExpanded ? t('buttons.collapse') : t('buttons.expand')} + aria-label={isExpanded ? t('buttons.collapse') : t('buttons.expand')} />