diff --git a/src/core/apollo/generated/apollo-helpers.ts b/src/core/apollo/generated/apollo-helpers.ts index 05d013fe0f..39863dda46 100644 --- a/src/core/apollo/generated/apollo-helpers.ts +++ b/src/core/apollo/generated/apollo-helpers.ts @@ -14,6 +14,7 @@ export type AccountKeySpecifier = ( | 'innovationPacks' | 'spaces' | 'storageAggregator' + | 'subscriptions' | 'type' | 'updatedDate' | 'virtualContributors' @@ -29,10 +30,16 @@ export type AccountFieldPolicy = { innovationPacks?: FieldPolicy | FieldReadFunction; spaces?: FieldPolicy | FieldReadFunction; storageAggregator?: FieldPolicy | FieldReadFunction; + subscriptions?: FieldPolicy | FieldReadFunction; type?: FieldPolicy | FieldReadFunction; updatedDate?: FieldPolicy | FieldReadFunction; virtualContributors?: FieldPolicy | FieldReadFunction; }; +export type AccountSubscriptionKeySpecifier = ('expires' | 'name' | AccountSubscriptionKeySpecifier)[]; +export type AccountSubscriptionFieldPolicy = { + expires?: FieldPolicy | FieldReadFunction; + name?: FieldPolicy | FieldReadFunction; +}; export type ActivityCreatedSubscriptionResultKeySpecifier = ( | 'activity' | ActivityCreatedSubscriptionResultKeySpecifier @@ -1398,6 +1405,7 @@ export type DocumentKeySpecifier = ( | 'mimeType' | 'size' | 'tagset' + | 'temporaryLocation' | 'updatedDate' | 'uploadedDate' | 'url' @@ -1412,6 +1420,7 @@ export type DocumentFieldPolicy = { mimeType?: FieldPolicy | FieldReadFunction; size?: FieldPolicy | FieldReadFunction; tagset?: FieldPolicy | FieldReadFunction; + temporaryLocation?: FieldPolicy | FieldReadFunction; updatedDate?: FieldPolicy | FieldReadFunction; uploadedDate?: FieldPolicy | FieldReadFunction; url?: FieldPolicy | FieldReadFunction; @@ -3595,6 +3604,10 @@ export type StrictTypedTypePolicies = { keyFields?: false | AccountKeySpecifier | (() => undefined | AccountKeySpecifier); fields?: AccountFieldPolicy; }; + AccountSubscription?: Omit & { + keyFields?: false | AccountSubscriptionKeySpecifier | (() => undefined | AccountSubscriptionKeySpecifier); + fields?: AccountSubscriptionFieldPolicy; + }; ActivityCreatedSubscriptionResult?: Omit & { keyFields?: | false diff --git a/src/core/apollo/generated/apollo-hooks.ts b/src/core/apollo/generated/apollo-hooks.ts index 091177dd23..7afb2971e7 100644 --- a/src/core/apollo/generated/apollo-hooks.ts +++ b/src/core/apollo/generated/apollo-hooks.ts @@ -4491,6 +4491,10 @@ export const AccountInformationDocument = gql` spaces { id level + authorization { + id + myPrivileges + } profile { ...AccountItemProfile cardBanner: visual(type: CARD) { @@ -4500,6 +4504,10 @@ export const AccountInformationDocument = gql` } community { id + authorization { + id + myPrivileges + } } subspaces { id @@ -11444,21 +11452,152 @@ export function refetchRolesOrganizationQuery(variables: SchemaTypes.RolesOrgani return { query: RolesOrganizationDocument, variables: variables }; } +export const AssignLicensePlanToAccountDocument = gql` + mutation AssignLicensePlanToAccount($licensePlanId: UUID!, $accountId: UUID!, $licensingId: UUID!) { + assignLicensePlanToAccount( + planData: { accountID: $accountId, licensePlanID: $licensePlanId, licensingID: $licensingId } + ) { + id + } + } +`; +export type AssignLicensePlanToAccountMutationFn = Apollo.MutationFunction< + SchemaTypes.AssignLicensePlanToAccountMutation, + SchemaTypes.AssignLicensePlanToAccountMutationVariables +>; + +/** + * __useAssignLicensePlanToAccountMutation__ + * + * To run a mutation, you first call `useAssignLicensePlanToAccountMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAssignLicensePlanToAccountMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [assignLicensePlanToAccountMutation, { data, loading, error }] = useAssignLicensePlanToAccountMutation({ + * variables: { + * licensePlanId: // value for 'licensePlanId' + * accountId: // value for 'accountId' + * licensingId: // value for 'licensingId' + * }, + * }); + */ +export function useAssignLicensePlanToAccountMutation( + baseOptions?: Apollo.MutationHookOptions< + SchemaTypes.AssignLicensePlanToAccountMutation, + SchemaTypes.AssignLicensePlanToAccountMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + SchemaTypes.AssignLicensePlanToAccountMutation, + SchemaTypes.AssignLicensePlanToAccountMutationVariables + >(AssignLicensePlanToAccountDocument, options); +} + +export type AssignLicensePlanToAccountMutationHookResult = ReturnType; +export type AssignLicensePlanToAccountMutationResult = + Apollo.MutationResult; +export type AssignLicensePlanToAccountMutationOptions = Apollo.BaseMutationOptions< + SchemaTypes.AssignLicensePlanToAccountMutation, + SchemaTypes.AssignLicensePlanToAccountMutationVariables +>; +export const RevokeLicensePlanFromAccountDocument = gql` + mutation RevokeLicensePlanFromAccount($licensePlanId: UUID!, $accountId: UUID!, $licensingId: UUID!) { + revokeLicensePlanFromAccount( + planData: { accountID: $accountId, licensePlanID: $licensePlanId, licensingID: $licensingId } + ) { + id + } + } +`; +export type RevokeLicensePlanFromAccountMutationFn = Apollo.MutationFunction< + SchemaTypes.RevokeLicensePlanFromAccountMutation, + SchemaTypes.RevokeLicensePlanFromAccountMutationVariables +>; + +/** + * __useRevokeLicensePlanFromAccountMutation__ + * + * To run a mutation, you first call `useRevokeLicensePlanFromAccountMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRevokeLicensePlanFromAccountMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [revokeLicensePlanFromAccountMutation, { data, loading, error }] = useRevokeLicensePlanFromAccountMutation({ + * variables: { + * licensePlanId: // value for 'licensePlanId' + * accountId: // value for 'accountId' + * licensingId: // value for 'licensingId' + * }, + * }); + */ +export function useRevokeLicensePlanFromAccountMutation( + baseOptions?: Apollo.MutationHookOptions< + SchemaTypes.RevokeLicensePlanFromAccountMutation, + SchemaTypes.RevokeLicensePlanFromAccountMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + SchemaTypes.RevokeLicensePlanFromAccountMutation, + SchemaTypes.RevokeLicensePlanFromAccountMutationVariables + >(RevokeLicensePlanFromAccountDocument, options); +} + +export type RevokeLicensePlanFromAccountMutationHookResult = ReturnType; +export type RevokeLicensePlanFromAccountMutationResult = + Apollo.MutationResult; +export type RevokeLicensePlanFromAccountMutationOptions = Apollo.BaseMutationOptions< + SchemaTypes.RevokeLicensePlanFromAccountMutation, + SchemaTypes.RevokeLicensePlanFromAccountMutationVariables +>; export const AdminGlobalOrganizationsListDocument = gql` query adminGlobalOrganizationsList($first: Int!, $after: UUID, $filter: OrganizationFilterInput) { organizationsPaginated(first: $first, after: $after, filter: $filter) { organization { id + account { + id + subscriptions { + name + } + } profile { id url displayName } + verification { + id + lifecycle { + id + state + } + } } pageInfo { ...PageInfo } } + platform { + id + licensing { + id + plans { + id + name + type + licenseCredential + } + } + } } ${PageInfoFragmentDoc} `; @@ -11521,6 +11660,59 @@ export function refetchAdminGlobalOrganizationsListQuery( return { query: AdminGlobalOrganizationsListDocument, variables: variables }; } +export const AdminOrganizationVerifyDocument = gql` + mutation adminOrganizationVerify($input: OrganizationVerificationEventInput!) { + eventOnOrganizationVerification(organizationVerificationEventData: $input) { + id + lifecycle { + id + nextEvents + state + } + } + } +`; +export type AdminOrganizationVerifyMutationFn = Apollo.MutationFunction< + SchemaTypes.AdminOrganizationVerifyMutation, + SchemaTypes.AdminOrganizationVerifyMutationVariables +>; + +/** + * __useAdminOrganizationVerifyMutation__ + * + * To run a mutation, you first call `useAdminOrganizationVerifyMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAdminOrganizationVerifyMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [adminOrganizationVerifyMutation, { data, loading, error }] = useAdminOrganizationVerifyMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useAdminOrganizationVerifyMutation( + baseOptions?: Apollo.MutationHookOptions< + SchemaTypes.AdminOrganizationVerifyMutation, + SchemaTypes.AdminOrganizationVerifyMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + SchemaTypes.AdminOrganizationVerifyMutation, + SchemaTypes.AdminOrganizationVerifyMutationVariables + >(AdminOrganizationVerifyDocument, options); +} + +export type AdminOrganizationVerifyMutationHookResult = ReturnType; +export type AdminOrganizationVerifyMutationResult = Apollo.MutationResult; +export type AdminOrganizationVerifyMutationOptions = Apollo.BaseMutationOptions< + SchemaTypes.AdminOrganizationVerifyMutation, + SchemaTypes.AdminOrganizationVerifyMutationVariables +>; export const OrganizationInfoDocument = gql` query organizationInfo($organizationId: UUID_NAMEID!, $includeAssociates: Boolean = false) { organization(ID: $organizationId) { @@ -12719,6 +12911,12 @@ export const UserListDocument = gql` usersPaginated(first: $first, after: $after, filter: $filter) { users { id + account { + id + subscriptions { + name + } + } profile { id url @@ -12731,6 +12929,18 @@ export const UserListDocument = gql` hasNextPage } } + platform { + id + licensing { + id + plans { + id + name + type + licenseCredential + } + } + } } `; @@ -22756,12 +22966,20 @@ export const NewVirtualContributorMySpacesDocument = gql` id community { id + authorization { + id + myPrivileges + } } profile { id displayName url } + authorization { + id + myPrivileges + } subspaces { id type diff --git a/src/core/apollo/generated/graphql-schema.ts b/src/core/apollo/generated/graphql-schema.ts index 13a577d506..1be670da28 100644 --- a/src/core/apollo/generated/graphql-schema.ts +++ b/src/core/apollo/generated/graphql-schema.ts @@ -54,6 +54,8 @@ export type Account = { spaces: Array; /** The StorageAggregator in use by this Account */ storageAggregator: StorageAggregator; + /** The subscriptions active for this Account. */ + subscriptions: Array; /** A type of entity that this Account is being used with. */ type?: Maybe; /** The date at which the entity was last updated. */ @@ -67,6 +69,14 @@ export type AccountAuthorizationResetInput = { accountID: Scalars['UUID_NAMEID']; }; +export type AccountSubscription = { + __typename?: 'AccountSubscription'; + /** The expiry date of this subscription, null if it does never expire. */ + expires?: Maybe; + /** The name of the Subscription. */ + name: LicenseCredential; +}; + export enum AccountType { Organization = 'ORGANIZATION', User = 'USER', @@ -2419,6 +2429,8 @@ export type Document = { size: Scalars['Float']; /** The tagset in use on this Document. */ tagset: Tagset; + /** Whether this Document is in its end location or not. */ + temporaryLocation: Scalars['Boolean']; /** The date at which the entity was last updated. */ updatedDate?: Maybe; /** The uploaded date of this Document */ @@ -5568,6 +5580,8 @@ export type StorageBucketParent = { export type StorageBucketUploadFileInput = { storageBucketId: Scalars['String']; + /** Is this a temporary Document that will be moved later to another StorageBucket. */ + temporaryLocation?: InputMaybe; }; export type StorageBucketUploadFileOnLinkInput = { @@ -7059,6 +7073,9 @@ export type AccountInformationQuery = { __typename?: 'Space'; id: string; level: number; + authorization?: + | { __typename?: 'Authorization'; id: string; myPrivileges?: Array | undefined } + | undefined; profile: { __typename?: 'Profile'; tagline?: string | undefined; @@ -7069,7 +7086,13 @@ export type AccountInformationQuery = { cardBanner?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; avatar?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; }; - community: { __typename?: 'Community'; id: string }; + community: { + __typename?: 'Community'; + id: string; + authorization?: + | { __typename?: 'Authorization'; id: string; myPrivileges?: Array | undefined } + | undefined; + }; subspaces: Array<{ __typename?: 'Space'; id: string; @@ -16244,6 +16267,28 @@ export type RolesOrganizationQuery = { }; }; +export type AssignLicensePlanToAccountMutationVariables = Exact<{ + licensePlanId: Scalars['UUID']; + accountId: Scalars['UUID']; + licensingId: Scalars['UUID']; +}>; + +export type AssignLicensePlanToAccountMutation = { + __typename?: 'Mutation'; + assignLicensePlanToAccount: { __typename?: 'Account'; id: string }; +}; + +export type RevokeLicensePlanFromAccountMutationVariables = Exact<{ + licensePlanId: Scalars['UUID']; + accountId: Scalars['UUID']; + licensingId: Scalars['UUID']; +}>; + +export type RevokeLicensePlanFromAccountMutation = { + __typename?: 'Mutation'; + revokeLicensePlanFromAccount: { __typename?: 'Account'; id: string }; +}; + export type AdminGlobalOrganizationsListQueryVariables = Exact<{ first: Scalars['Int']; after?: InputMaybe; @@ -16257,7 +16302,19 @@ export type AdminGlobalOrganizationsListQuery = { organization: Array<{ __typename?: 'Organization'; id: string; + account?: + | { + __typename?: 'Account'; + id: string; + subscriptions: Array<{ __typename?: 'AccountSubscription'; name: LicenseCredential }>; + } + | undefined; profile: { __typename?: 'Profile'; id: string; url: string; displayName: string }; + verification: { + __typename?: 'OrganizationVerification'; + id: string; + lifecycle: { __typename?: 'Lifecycle'; id: string; state?: string | undefined }; + }; }>; pageInfo: { __typename?: 'PageInfo'; @@ -16266,6 +16323,39 @@ export type AdminGlobalOrganizationsListQuery = { hasNextPage: boolean; }; }; + platform: { + __typename?: 'Platform'; + id: string; + licensing: { + __typename?: 'Licensing'; + id: string; + plans: Array<{ + __typename?: 'LicensePlan'; + id: string; + name: string; + type: LicensePlanType; + licenseCredential: LicenseCredential; + }>; + }; + }; +}; + +export type AdminOrganizationVerifyMutationVariables = Exact<{ + input: OrganizationVerificationEventInput; +}>; + +export type AdminOrganizationVerifyMutation = { + __typename?: 'Mutation'; + eventOnOrganizationVerification: { + __typename?: 'OrganizationVerification'; + id: string; + lifecycle: { + __typename?: 'Lifecycle'; + id: string; + nextEvents?: Array | undefined; + state?: string | undefined; + }; + }; }; export type OrganizationInfoFragment = { @@ -17067,10 +17157,32 @@ export type UserListQuery = { __typename?: 'User'; id: string; email: string; + account?: + | { + __typename?: 'Account'; + id: string; + subscriptions: Array<{ __typename?: 'AccountSubscription'; name: LicenseCredential }>; + } + | undefined; profile: { __typename?: 'Profile'; id: string; url: string; displayName: string }; }>; pageInfo: { __typename?: 'PageInfo'; endCursor?: string | undefined; hasNextPage: boolean }; }; + platform: { + __typename?: 'Platform'; + id: string; + licensing: { + __typename?: 'Licensing'; + id: string; + plans: Array<{ + __typename?: 'LicensePlan'; + id: string; + name: string; + type: LicensePlanType; + licenseCredential: LicenseCredential; + }>; + }; + }; }; export type UserAvatarsQueryVariables = Exact<{ @@ -29111,8 +29223,25 @@ export type NewVirtualContributorMySpacesQuery = { spaces: Array<{ __typename?: 'Space'; id: string; - community: { __typename?: 'Community'; id: string }; + community: { + __typename?: 'Community'; + id: string; + authorization?: + | { + __typename?: 'Authorization'; + id: string; + myPrivileges?: Array | undefined; + } + | undefined; + }; profile: { __typename?: 'Profile'; id: string; displayName: string; url: string }; + authorization?: + | { + __typename?: 'Authorization'; + id: string; + myPrivileges?: Array | undefined; + } + | undefined; subspaces: Array<{ __typename?: 'Space'; id: string; diff --git a/src/core/i18n/en/translation.en.json b/src/core/i18n/en/translation.en.json index 23e1e2bc88..e6efe01901 100644 --- a/src/core/i18n/en/translation.en.json +++ b/src/core/i18n/en/translation.en.json @@ -1867,7 +1867,8 @@ "deleteSpace_description": "Click here to delete this Space. Be careful, this action cannot be undone.", "deleteSpace_disallowed": "The Space can only be deleted by the host of this Space.", "moreInfo": "More about Alkemio licenses can be found here.", - "moreInfoUrl": "$t(plansTable.seeMoreUrl)" + "moreInfoUrl": "$t(plansTable.seeMoreUrl)", + "licenseUpdated": "License updated successfully" }, "settings": { "description": "Here you can edit the visibility settings of your {{entity}}." @@ -1885,6 +1886,20 @@ "notifications": { "organization-created": "Organization created successfully.", "organization-removed": "Organization removed successfully." + }, + "verification": { + "confirm": { + "title": "{{action}} {{name}}", + "description": "This organization is currently {{status}}. Do you want to {{action}} it?", + "status": { + "verified": "verified", + "notVerified": "not verified" + }, + "action": { + "verify": "Verify", + "unverify": "Unverify" + } + } } }, "subsubspace": { @@ -3010,7 +3025,7 @@ }, "trySection": { "title": "🎉 Try it out!", - "subTitle": "Your Virtual Contributor is successfully created and we added it to your Personal Space . You can interact with it by typing @{{vcName}} in a comment on any post in your Space. Give it a try in the preview below!", + "subTitle": "Your Virtual Contributor is successfully created and we added it to your Personal Space .
You can interact with it by typing @{{vcName}} in a comment on any post in your Space. Give it a try in the preview below!", "subTitleInfo": "When you create your first VC, we automatically generate a Space for you. This Space functions like any other Space on Alkemio, and you can customize the settings according to your preferences.", "postTitle": "Example post", "postDescription": "At Alkemio, we utilize Posts to facilitate collaborative interactions with AI. Unlike private chats, other members within a Space can learn from and contribute to these interactions.\n\nSimilar to mentioning a human contributor, you can use the \"@\" symbol followed by the Virtual Contributor's name.", @@ -3022,6 +3037,10 @@ "description": "Which Space or Subspace do you want to bring to life with the Virtual Contributor? Below you'll be able to choose from all Spaces and Subspaces that you are host of.", "label": "(Sub)Space", "noSpaces": "You need a Space to proceed with this option. To create a Virtual Contributor, please go back and follow the Written Knowledge flow." + }, + "insufficientPrivileges": { + "title": "Insufficient Privileges", + "description": "You don't have the necessary privileges to complete the creation. Please contact the owners of this account." } } } diff --git a/src/core/ui/dialogs/InfoDialog.tsx b/src/core/ui/dialogs/InfoDialog.tsx new file mode 100644 index 0000000000..2f0b2225eb --- /dev/null +++ b/src/core/ui/dialogs/InfoDialog.tsx @@ -0,0 +1,52 @@ +import React, { FC, ReactNode } from 'react'; +import Dialog from '@mui/material/Dialog'; +import { LoadingButton } from '@mui/lab'; +import { DialogContent } from '../dialog/deprecated'; +import DialogHeader from '../dialog/DialogHeader'; +import { BlockTitle } from '../typography'; +import { Actions } from '../actions/Actions'; +import { gutters } from '../grid/utils'; + +// could be merged with ConfirmationDialog +// however there's no need of this entities, actions, etc. structure +interface InfoDialogProps { + entities: { + title: string | React.ReactNode; + content: ReactNode; + buttonCaption: string; + }; + actions: { + onButtonClick: () => void; + }; + options: { + show: boolean; + }; + state?: { + isLoading: boolean; + }; +} + +const InfoDialog: FC = ({ entities, actions, options, state }) => { + const { title, content, buttonCaption } = entities; + + return ( + + + {title} + + {content} + + + {buttonCaption} + + + + ); +}; + +export default InfoDialog; diff --git a/src/domain/InnovationPack/admin/AdminInnovationPacksPage.tsx b/src/domain/InnovationPack/admin/AdminInnovationPacksPage.tsx index 9a9566c471..9e69625185 100644 --- a/src/domain/InnovationPack/admin/AdminInnovationPacksPage.tsx +++ b/src/domain/InnovationPack/admin/AdminInnovationPacksPage.tsx @@ -6,7 +6,7 @@ import { useDeleteInnovationPackMutation, } from '../../../core/apollo/generated/apollo-hooks'; import SearchableListLayout from '../../shared/components/SearchableList/SearchableListLayout'; -import SimpleSearchableList from '../../shared/components/SearchableList/SimpleSearchableList'; +import SimpleSearchableTable from '../../shared/components/SearchableList/SimpleSearchableTable'; import AdminLayout from '../../platform/admin/layout/toplevel/AdminLayout'; import { AdminSection } from '../../platform/admin/layout/toplevel/constants'; import { buildInnovationPackSettingsUrl } from '../../../main/routing/urlBuilders'; @@ -46,7 +46,7 @@ const AdminInnovationPacksPage: FC = () => { return ( - handleDelete(item.id)} loading={loading} diff --git a/src/domain/account/queries/AccountInformation.graphql b/src/domain/account/queries/AccountInformation.graphql index 66c19dfa84..fb181a8d38 100644 --- a/src/domain/account/queries/AccountInformation.graphql +++ b/src/domain/account/queries/AccountInformation.graphql @@ -12,6 +12,10 @@ query AccountInformation($accountId: UUID!) { spaces { id level + authorization { + id + myPrivileges + } profile { ...AccountItemProfile cardBanner: visual(type: CARD) { @@ -21,6 +25,10 @@ query AccountInformation($accountId: UUID!) { } community { id + authorization { + id + myPrivileges + } } subspaces { id diff --git a/src/domain/community/contributor/organization/adminOrganizations/AdminOrganizationsPage.tsx b/src/domain/community/contributor/organization/adminOrganizations/AdminOrganizationsPage.tsx index 07d54fef72..f346815010 100644 --- a/src/domain/community/contributor/organization/adminOrganizations/AdminOrganizationsPage.tsx +++ b/src/domain/community/contributor/organization/adminOrganizations/AdminOrganizationsPage.tsx @@ -1,21 +1,133 @@ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; +import { useResolvedPath } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import AdminLayout from '../../../../platform/admin/layout/toplevel/AdminLayout'; import { AdminSection } from '../../../../platform/admin/layout/toplevel/constants'; import useAdminGlobalOrganizationsList from './useAdminGlobalOrganizationsList'; import SearchableListLayout from '../../../../shared/components/SearchableList/SearchableListLayout'; -import { useResolvedPath } from 'react-router-dom'; -import SimpleSearchableList from '../../../../shared/components/SearchableList/SimpleSearchableList'; +import SimpleSearchableTable, { + SearchableListItem, +} from '../../../../shared/components/SearchableList/SimpleSearchableTable'; +import { IconButton } from '@mui/material'; +import { TuneOutlined, VerifiedUser, VerifiedUserOutlined } from '@mui/icons-material'; +import ConfirmationDialog from '../../../../../core/ui/dialogs/ConfirmationDialog'; +import LicensePlanDialog from './LicensePlanDialog'; const AdminOrganizationsPage: FC = () => { - const { organizations, ...listProps } = useAdminGlobalOrganizationsList(); + const { t } = useTranslation(); + const { organizations, licensePlans, ...listProps } = useAdminGlobalOrganizationsList(); const { pathname: url } = useResolvedPath('.'); + const [confirmationOpen, setConfirmationOpen] = useState(false); + const [licenseDialogOpen, setLicenseDialogOpen] = useState(false); + const [verificationLoading, setVerificationLoading] = useState(false); + const [selectedItem, setSelectedItem] = useState(undefined); + + const onVerificationClick = (item: SearchableListItem) => { + setConfirmationOpen(true); + setSelectedItem(item); + }; + + const onCloseConfirmation = () => { + setConfirmationOpen(false); + setSelectedItem(undefined); + }; + + const onVerificationConfirmation = async () => { + if (selectedItem) { + setVerificationLoading(true); + await listProps.handleVerification(selectedItem); + setVerificationLoading(false); + onCloseConfirmation(); + } + }; + + const onSettingsClick = (item: SearchableListItem) => { + setSelectedItem(item); + setLicenseDialogOpen(true); + }; + + const assignLicense = async (entityId: string, planId: string) => { + await listProps.assignLicensePlan(entityId, planId); + setLicenseDialogOpen(false); + }; + + const revokeLicense = async (entityId: string, planId: string) => { + await listProps.revokeLicensePlan(entityId, planId); + setLicenseDialogOpen(false); + }; + + const getActions = (item: SearchableListItem) => { + return ( + <> + onSettingsClick(item)} size="large" aria-label={'License'}> + + + onVerificationClick(item)} size="large" aria-label={'Verify'}> + {item?.verified ? : } + + + ); + }; + + const getStatusTranslation = (item: SearchableListItem | undefined) => { + switch (item?.verified) { + case true: + return t('pages.admin.organization.verification.confirm.status.verified'); + default: + return t('pages.admin.organization.verification.confirm.status.notVerified'); + } + }; + + const getActionTranslation = (item: SearchableListItem | undefined) => { + switch (item?.verified) { + case true: + return t('pages.admin.organization.verification.confirm.action.unverify'); + default: + return t('pages.admin.organization.verification.confirm.action.verify'); + } + }; + return ( - + + + {selectedItem?.accountId && ( + setLicenseDialogOpen(false)} + licensePlans={licensePlans} + assignLicensePlan={assignLicense} + revokeLicensePlan={revokeLicense} + activeLicensePlanIds={selectedItem.activeLicensePlanIds} + /> + )} ); }; diff --git a/src/domain/community/contributor/organization/adminOrganizations/LicensePlanDialog.tsx b/src/domain/community/contributor/organization/adminOrganizations/LicensePlanDialog.tsx new file mode 100644 index 0000000000..11ac76bb16 --- /dev/null +++ b/src/domain/community/contributor/organization/adminOrganizations/LicensePlanDialog.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { DialogContent } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import DialogWithGrid from '../../../../../core/ui/dialog/DialogWithGrid'; +import DialogHeader from '../../../../../core/ui/dialog/DialogHeader'; +import PlansTable from '../../../../platform/admin/space/AdminSpaceListPage/PlansTable'; +import AssignPlan from '../../../../platform/admin/space/AdminSpaceListPage/AssignPlan'; + +interface LicensePlanDialogProps { + accountId: string; + activeLicensePlanIds: string[] | undefined; + licensePlans: + | { + id: string; + name: string; + }[] + | undefined; + assignLicensePlan: (accountId: string, planId: string) => Promise; + revokeLicensePlan: (accountId: string, planId: string) => void; + open: boolean; + onClose: () => void; +} + +const LicensePlanDialog = ({ + accountId, + activeLicensePlanIds, + licensePlans, + assignLicensePlan, + revokeLicensePlan, + open, + onClose, +}: LicensePlanDialogProps) => { + const { t } = useTranslation(); + + return ( + + + + {licensePlans && ( + revokeLicensePlan(accountId, plan.id)} + /> + )} + {licensePlans && ( + assignLicensePlan(accountId, licensePlanId)} + licensePlans={licensePlans} + /> + )} + + + ); +}; + +export default LicensePlanDialog; diff --git a/src/domain/community/contributor/organization/adminOrganizations/ManageLicensePlanAccount.graphql b/src/domain/community/contributor/organization/adminOrganizations/ManageLicensePlanAccount.graphql new file mode 100644 index 0000000000..ba141e7395 --- /dev/null +++ b/src/domain/community/contributor/organization/adminOrganizations/ManageLicensePlanAccount.graphql @@ -0,0 +1,19 @@ +mutation AssignLicensePlanToAccount($licensePlanId: UUID!, $accountId: UUID!, $licensingId: UUID!) { + assignLicensePlanToAccount(planData: { + accountID: $accountId + licensePlanID: $licensePlanId + licensingID: $licensingId + }) { + id + } +} + +mutation RevokeLicensePlanFromAccount($licensePlanId: UUID!, $accountId: UUID!, $licensingId: UUID!) { + revokeLicensePlanFromAccount(planData: { + accountID: $accountId + licensePlanID: $licensePlanId + licensingID: $licensingId + }) { + id + } +} \ No newline at end of file diff --git a/src/domain/community/contributor/organization/adminOrganizations/adminGlobalOrganizationsList.graphql b/src/domain/community/contributor/organization/adminOrganizations/adminGlobalOrganizationsList.graphql index f7e0c34980..31bb5ad856 100644 --- a/src/domain/community/contributor/organization/adminOrganizations/adminGlobalOrganizationsList.graphql +++ b/src/domain/community/contributor/organization/adminOrganizations/adminGlobalOrganizationsList.graphql @@ -2,14 +2,50 @@ query adminGlobalOrganizationsList($first: Int!, $after: UUID, $filter: Organiza organizationsPaginated(first: $first, after: $after, filter: $filter) { organization { id + account { + id + subscriptions { + name + } + } profile { id url displayName } + verification { + id + lifecycle { + id + state + } + } } pageInfo { ...PageInfo } } + platform { + id + licensing { + id + plans { + id + name + type + licenseCredential + } + } + } } + +mutation adminOrganizationVerify($input: OrganizationVerificationEventInput!) { + eventOnOrganizationVerification(organizationVerificationEventData: $input) { + id + lifecycle { + id + nextEvents + state + } + } +} \ No newline at end of file diff --git a/src/domain/community/contributor/organization/adminOrganizations/useAdminGlobalOrganizationsList.ts b/src/domain/community/contributor/organization/adminOrganizations/useAdminGlobalOrganizationsList.ts index 16b2bc8b80..8918f769b1 100644 --- a/src/domain/community/contributor/organization/adminOrganizations/useAdminGlobalOrganizationsList.ts +++ b/src/domain/community/contributor/organization/adminOrganizations/useAdminGlobalOrganizationsList.ts @@ -1,18 +1,38 @@ import { useMemo, useState } from 'react'; import { + refetchAdminGlobalOrganizationsListQuery, useAdminGlobalOrganizationsListQuery, + useAdminOrganizationVerifyMutation, + useAssignLicensePlanToAccountMutation, useDeleteOrganizationMutation, + useRevokeLicensePlanFromAccountMutation, } from '../../../../../core/apollo/generated/apollo-hooks'; import { useNotification } from '../../../../../core/ui/notifications/useNotification'; import usePaginatedQuery from '../../../../shared/pagination/usePaginatedQuery'; -import { SearchableListItem } from '../../../../shared/components/SearchableList/SimpleSearchableList'; +import { SearchableListItem } from '../../../../shared/components/SearchableList/SimpleSearchableTable'; import clearCacheForQuery from '../../../../../core/apollo/utils/clearCacheForQuery'; import { useTranslation } from 'react-i18next'; import { buildSettingsUrl } from '../../../../../main/routing/urlBuilders'; +import { LicensePlanType } from '../../../../../core/apollo/generated/graphql-schema'; const PAGE_SIZE = 10; +enum OrgVerificationLifecycleStates { + manuallyVerified = 'manuallyVerified', +} + +enum OrgVerificationLifecycleEvents { + VERIFICATION_REQUEST = 'VERIFICATION_REQUEST', + MANUALLY_VERIFY = 'MANUALLY_VERIFY', + RESET = 'RESET', +} + +export interface ContributorLicensePlan { + id: string; + name: string; +} + export const useAdminGlobalOrganizationsList = () => { const [searchTerm, setSearchTerm] = useState(''); @@ -42,21 +62,123 @@ export const useAdminGlobalOrganizationsList = () => { }, }); + const [verifyOrg] = useAdminOrganizationVerifyMutation(); + + const handleVerification = async (item: SearchableListItem) => { + const orgFullData = data?.organizationsPaginated?.organization?.find(org => org.id === item.id); + + if (!orgFullData) { + return; + } + + if (orgFullData.verification.lifecycle.state === OrgVerificationLifecycleStates.manuallyVerified) { + await verifyOrg({ + variables: { + input: { + eventName: OrgVerificationLifecycleEvents.RESET, + organizationVerificationID: orgFullData.verification.id, + }, + }, + }); + } else { + // in case the VERIFICATION_REQUEST is not available, try to complete with MANUALLY_VERIFY + try { + await verifyOrg({ + variables: { + input: { + eventName: OrgVerificationLifecycleEvents.VERIFICATION_REQUEST, + organizationVerificationID: orgFullData.verification.id, + }, + }, + }); + } catch (e) { + // ignore errors if the verification_request fails we still try to manually verify + } + + await verifyOrg({ + variables: { + input: { + eventName: OrgVerificationLifecycleEvents.MANUALLY_VERIFY, + organizationVerificationID: orgFullData.verification.id, + }, + }, + }); + } + }; + + const [assignLicense] = useAssignLicensePlanToAccountMutation(); + const assignLicensePlan = async (accountId: string, licensePlanId: string) => { + await assignLicense({ + variables: { + accountId, + licensePlanId, + licensingId: data?.platform.licensing.id ?? '', + }, + refetchQueries: [ + refetchAdminGlobalOrganizationsListQuery({ + first: PAGE_SIZE, + filter: { displayName: searchTerm }, + }), + ], + onCompleted: () => notify(t('pages.admin.generic.sections.account.licenseUpdated'), 'success'), + }); + }; + + const [revokeLicense] = useRevokeLicensePlanFromAccountMutation(); + const revokeLicensePlan = async (accountId: string, licensePlanId: string) => { + await revokeLicense({ + variables: { + accountId, + licensePlanId, + licensingId: data?.platform.licensing.id ?? '', + }, + refetchQueries: [ + refetchAdminGlobalOrganizationsListQuery({ + first: PAGE_SIZE, + filter: { displayName: searchTerm }, + }), + ], + onCompleted: () => notify(t('pages.admin.generic.sections.account.licenseUpdated'), 'success'), + }); + }; + const organizations = useMemo( () => data?.organizationsPaginated.organization.map(org => ({ id: org.id, + accountId: org.account?.id, value: org.profile.displayName, url: buildSettingsUrl(org.profile.url), + verified: org.verification.lifecycle.state === OrgVerificationLifecycleStates.manuallyVerified, + activeLicensePlanIds: data?.platform.licensing.plans + .filter(({ licenseCredential }) => + org.account?.subscriptions.map(subscription => subscription.name).includes(licenseCredential) + ) + .map(({ id }) => id), })) || [], [data] ); + const licensePlans = useMemo( + () => + data?.platform.licensing.plans + .filter(plan => plan.type === LicensePlanType.AccountPlan) + .map(licensePlan => ({ + id: licensePlan.id, + name: licensePlan.name, + })) || [], + [data] + ); + return { organizations, searchTerm, onSearchTermChange: setSearchTerm, onDelete: handleDelete, + handleVerification, + licensePlans, + assignLicensePlan, + revokeLicensePlan, ...paginationProvided, }; }; diff --git a/src/domain/community/user/adminUsers/useAdminGlobalUserList.tsx b/src/domain/community/user/adminUsers/useAdminGlobalUserList.tsx index 9db8616583..bbef3c039b 100644 --- a/src/domain/community/user/adminUsers/useAdminGlobalUserList.tsx +++ b/src/domain/community/user/adminUsers/useAdminGlobalUserList.tsx @@ -1,13 +1,24 @@ import { useMemo, useState } from 'react'; import { ApolloError } from '@apollo/client'; import { SearchableListItem } from '../../../platform/admin/components/SearchableList'; -import { useDeleteUserMutation, useUserListQuery } from '../../../../core/apollo/generated/apollo-hooks'; +import { + refetchUserListQuery, + useAssignLicensePlanToAccountMutation, + useDeleteUserMutation, + useRevokeLicensePlanFromAccountMutation, + useUserListQuery, +} from '../../../../core/apollo/generated/apollo-hooks'; import { useNotification } from '../../../../core/ui/notifications/useNotification'; import usePaginatedQuery from '../../../shared/pagination/usePaginatedQuery'; -import { UserListQuery, UserListQueryVariables } from '../../../../core/apollo/generated/graphql-schema'; +import { + LicensePlanType, + UserListQuery, + UserListQueryVariables, +} from '../../../../core/apollo/generated/graphql-schema'; import { useTranslation } from 'react-i18next'; import clearCacheForQuery from '../../../../core/apollo/utils/clearCacheForQuery'; import { buildSettingsUrl } from '../../../../main/routing/urlBuilders'; +import { ContributorLicensePlan } from '../../contributor/organization/adminOrganizations/useAdminGlobalOrganizationsList'; interface Provided { loading: boolean; @@ -20,6 +31,9 @@ interface Provided { pageSize: number; firstPageSize: number; searchTerm: string; + licensePlans: ContributorLicensePlan[]; + assignLicensePlan: (accountId: string, planId: string) => Promise; + revokeLicensePlan: (accountId: string, planId: string) => Promise; onSearchTermChange: (filterTerm: string) => void; } @@ -63,10 +77,16 @@ const useAdminGlobalUserList = ({ const userList = useMemo( () => - (data?.usersPaginated.users ?? []).map(({ id, profile, email }) => ({ + (data?.usersPaginated.users ?? []).map(({ id, profile, email, account }) => ({ id, + accountId: account?.id, value: `${profile.displayName} (${email})`, url: buildSettingsUrl(profile.url), + activeLicensePlanIds: data?.platform.licensing.plans + .filter(({ licenseCredential }) => + account?.subscriptions.map(subscription => subscription.name).includes(licenseCredential) + ) + .map(({ id }) => id), })), [data] ); @@ -86,6 +106,53 @@ const useAdminGlobalUserList = ({ }); }; + const [assignLicense] = useAssignLicensePlanToAccountMutation(); + const assignLicensePlan = async (accountId: string, licensePlanId: string) => { + await assignLicense({ + variables: { + accountId, + licensePlanId, + licensingId: data?.platform.licensing.id ?? '', + }, + refetchQueries: [ + refetchUserListQuery({ + first: pageSize, + filter: { firstName: searchTerm, lastName: searchTerm, email: searchTerm }, + }), + ], + onCompleted: () => notify(t('pages.admin.generic.sections.account.licenseUpdated'), 'success'), + }); + }; + + const [revokeLicense] = useRevokeLicensePlanFromAccountMutation(); + const revokeLicensePlan = async (accountId: string, licensePlanId: string) => { + await revokeLicense({ + variables: { + accountId, + licensePlanId, + licensingId: data?.platform.licensing.id ?? '', + }, + refetchQueries: [ + refetchUserListQuery({ + first: pageSize, + filter: { firstName: searchTerm, lastName: searchTerm, email: searchTerm }, + }), + ], + onCompleted: () => notify(t('pages.admin.generic.sections.account.licenseUpdated'), 'success'), + }); + }; + + const licensePlans = useMemo( + () => + data?.platform.licensing.plans + .filter(plan => plan.type === LicensePlanType.AccountPlan) + .map(licensePlan => ({ + id: licensePlan.id, + name: licensePlan.name, + })) || [], + [data] + ); + return { userList, loading, @@ -97,6 +164,9 @@ const useAdminGlobalUserList = ({ pageSize: actualPageSize, firstPageSize, searchTerm, + licensePlans, + assignLicensePlan, + revokeLicensePlan, onSearchTermChange: setSearchTerm, }; }; diff --git a/src/domain/community/user/adminUsers/userList.graphql b/src/domain/community/user/adminUsers/userList.graphql index 51166aacfa..be9e985bda 100644 --- a/src/domain/community/user/adminUsers/userList.graphql +++ b/src/domain/community/user/adminUsers/userList.graphql @@ -2,6 +2,12 @@ query userList($first: Int!, $after: UUID, $filter: UserFilterInput) { usersPaginated(first: $first, after: $after, filter: $filter) { users { id + account { + id + subscriptions { + name + } + } profile { id url @@ -14,4 +20,16 @@ query userList($first: Int!, $after: UUID, $filter: UserFilterInput) { hasNextPage } } + platform { + id + licensing { + id + plans { + id + name + type + licenseCredential + } + } + } } diff --git a/src/domain/innovationHub/InnovationHubsAdmin/AdminInnovationHubsPage.tsx b/src/domain/innovationHub/InnovationHubsAdmin/AdminInnovationHubsPage.tsx index 07e677a2b0..4b927bad21 100644 --- a/src/domain/innovationHub/InnovationHubsAdmin/AdminInnovationHubsPage.tsx +++ b/src/domain/innovationHub/InnovationHubsAdmin/AdminInnovationHubsPage.tsx @@ -2,13 +2,13 @@ import { FC, useMemo, useState } from 'react'; import { sortBy } from 'lodash'; import AdminLayout from '../../platform/admin/layout/toplevel/AdminLayout'; import SearchableListLayout from '../../shared/components/SearchableList/SearchableListLayout'; -import SimpleSearchableList from '../../shared/components/SearchableList/SimpleSearchableList'; import { AdminSection } from '../../platform/admin/layout/toplevel/constants'; import { refetchAdminInnovationHubsListQuery, useAdminInnovationHubsListQuery, useDeleteInnovationHubMutation, } from '../../../core/apollo/generated/apollo-hooks'; +import SimpleSearchableTable from '../../shared/components/SearchableList/SimpleSearchableTable'; interface AdminInnovationHubsPageProps {} @@ -45,7 +45,7 @@ const AdminInnovationHubsPage: FC = () => { return ( - handleDelete(item.id)} loading={loading} diff --git a/src/domain/platform/admin/components/Organization/OrganizationForm.tsx b/src/domain/platform/admin/components/Organization/OrganizationForm.tsx index a9d8e41202..e24c3faf35 100644 --- a/src/domain/platform/admin/components/Organization/OrganizationForm.tsx +++ b/src/domain/platform/admin/components/Organization/OrganizationForm.tsx @@ -44,6 +44,7 @@ const EmptyOrganization: Omit = { machineDef: '', }, }, + account: undefined, profile: { id: '', displayName: '', @@ -71,7 +72,6 @@ const EmptyOrganization: Omit = { }, }, preferences: [], - account: undefined, }; interface Props { diff --git a/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx b/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx index 28fe702186..caf1416f3c 100644 --- a/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx +++ b/src/domain/platform/admin/opportunity/pages/OpportunityProfile/OpportunityProfileView.tsx @@ -78,8 +78,7 @@ const OpportunityProfileView: FC = ({ mode }) => { }, spaceID: challengeId, tags: tagsets.flatMap(x => x.tags), - collaborationData: { - }, + collaborationData: {}, }, }, }); diff --git a/src/domain/shared/components/SearchableList/SearchableListLayout.tsx b/src/domain/shared/components/SearchableList/SearchableListLayout.tsx index e55e29677f..a7c8dd6270 100644 --- a/src/domain/shared/components/SearchableList/SearchableListLayout.tsx +++ b/src/domain/shared/components/SearchableList/SearchableListLayout.tsx @@ -1,9 +1,10 @@ -import { Grid } from '@mui/material'; +import { Button, Grid } from '@mui/material'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import AddOutlinedIcon from '@mui/icons-material/AddOutlined'; import WrapperTypography from '../../../../core/ui/typography/deprecated/WrapperTypography'; -import WrapperButton from '../../../../core/ui/button/deprecated/WrapperButton'; +import RouterLink from '../../../../core/ui/link/RouterLink'; +import { gutters } from '../../../../core/ui/grid/utils'; interface ListPageProps { title?: string; @@ -16,12 +17,16 @@ export const SearchableListLayout: FC = ({ title, newLink, childr return ( {(title || newLink) && ( - + {title && {title}} - {newLink && } + {newLink && ( + + )} )} diff --git a/src/domain/shared/components/SearchableList/SimpleSearchableList.tsx b/src/domain/shared/components/SearchableList/SimpleSearchableList.tsx deleted file mode 100644 index e603c177dd..0000000000 --- a/src/domain/shared/components/SearchableList/SimpleSearchableList.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { FormControl, InputLabel, List, OutlinedInput } from '@mui/material'; -import Delete from '@mui/icons-material/Delete'; -import React, { forwardRef, ReactNode, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import SearchableListIconButton from './SearchableListIconButton'; -import RemoveModal from '../../../../core/ui/dialogs/RemoveModal'; -import useLazyLoading from '../../pagination/useLazyLoading'; -import LoadingListItem from './LoadingListItem'; -import ListItemLink from './ListItemLink'; -import { times } from 'lodash'; - -export interface SearchableListProps { - data: Item[] | undefined; - active?: number | string; - onDelete?: (item: Item) => void; - loading: boolean; - fetchMore: () => Promise; - pageSize: number; - firstPageSize?: number; - searchTerm: string; - onSearchTermChange: (searchTerm: string) => void; - totalCount?: number; - hasMore: boolean | undefined; - itemActions?: (item: Item) => ReactNode | ReactNode; -} - -export interface SearchableListItem { - id: string; - value: string; - url: string; -} - -const SimpleSearchableList = ({ - data = [], - onDelete, - loading, - fetchMore, - pageSize, - firstPageSize = pageSize, - searchTerm, - onSearchTermChange, - totalCount, - hasMore = false, - itemActions = () => null, -}: SearchableListProps) => { - const { t } = useTranslation(); - const [isModalOpened, setModalOpened] = useState(false); - const [itemToRemove, setItemToRemove] = useState(null); - - const Loader = useMemo( - () => - forwardRef((props, ref) => ( - <> - - {times(pageSize - 1, i => ( - - ))} - - )), - [pageSize] - ); - - const loader = useLazyLoading(Loader, { - hasMore, - loading, - fetchMore, - }); - - const handleSearch = (e: React.ChangeEvent) => { - const value = e.target.value; - onSearchTermChange(value); - }; - - const handleRemoveItem = async () => { - if (onDelete && itemToRemove) { - onDelete(itemToRemove as Item); - closeModal(); - } - }; - - const openModal = (e: Event, item: SearchableListItem): void => { - e.preventDefault(); - setModalOpened(true); - setItemToRemove(item); - }; - - const closeModal = (): void => { - setModalOpened(false); - setItemToRemove(null); - }; - - const renderItemActions = typeof itemActions === 'function' ? itemActions : () => itemActions; - - return ( - <> - - theme.palette.primary.contrastText }} - /> - - {typeof totalCount === 'undefined' ? null : ( - {t('components.searchableList.info', { count: data.length, total: totalCount })} - )} -
- - {loading && !data - ? times(firstPageSize, i => ) - : data.map(item => ( - openModal(e, item)} - size="large" - aria-label={t('buttons.delete')} - > - - - ) - } - actions={renderItemActions(item)} - /> - ))} - {loader} - - - - ); -}; - -export default SimpleSearchableList; diff --git a/src/domain/shared/components/SearchableList/SimpleSearchableTable.tsx b/src/domain/shared/components/SearchableList/SimpleSearchableTable.tsx new file mode 100644 index 0000000000..3c28f91bdc --- /dev/null +++ b/src/domain/shared/components/SearchableList/SimpleSearchableTable.tsx @@ -0,0 +1,191 @@ +import { + FormControl, + IconButton, + InputLabel, + OutlinedInput, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import DeleteOutline from '@mui/icons-material/DeleteOutline'; +import React, { forwardRef, ReactNode, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import RemoveModal from '../../../../core/ui/dialogs/RemoveModal'; +import useLazyLoading from '../../pagination/useLazyLoading'; +import LoadingListItem from './LoadingListItem'; +import { times } from 'lodash'; +import RouterLink from '../../../../core/ui/link/RouterLink'; +import { BlockTitle, CardTitle } from '../../../../core/ui/typography'; +import { Actions } from '../../../../core/ui/actions/Actions'; +import PageContent from '../../../../core/ui/content/PageContent'; + +export interface SearchableListProps { + data: Item[] | undefined; + active?: number | string; + onDelete?: (item: Item) => void; + loading: boolean; + fetchMore: () => Promise; + pageSize: number; + firstPageSize?: number; + searchTerm: string; + onSearchTermChange: (searchTerm: string) => void; + totalCount?: number; + hasMore: boolean | undefined; + itemActions?: (item: Item) => ReactNode | ReactNode; +} + +export interface SearchableListItem { + id: string; + accountId?: string; + value: string; + url: string; + verified?: boolean; + activeLicensePlanIds?: string[]; +} + +const SimpleSearchableList = ({ + data = [], + onDelete, + loading, + fetchMore, + pageSize, + firstPageSize = pageSize, + searchTerm, + onSearchTermChange, + totalCount, + hasMore = false, + itemActions, +}: SearchableListProps) => { + const { t } = useTranslation(); + const [isModalOpened, setModalOpened] = useState(false); + const [itemToRemove, setItemToRemove] = useState(null); + + const Loader = useMemo( + () => + forwardRef((props, ref) => ( + <> + + {times(pageSize - 1, i => ( + + ))} + + )), + [pageSize] + ); + + const loader = useLazyLoading(Loader, { + hasMore, + loading, + fetchMore, + }); + + const handleSearch = (e: React.ChangeEvent) => { + const value = e.target.value; + onSearchTermChange(value); + }; + + const handleRemoveItem = async () => { + if (onDelete && itemToRemove) { + onDelete(itemToRemove as Item); + closeModal(); + } + }; + + const openModal = (e: React.MouseEvent, item: SearchableListItem): void => { + e.preventDefault(); + setModalOpened(true); + setItemToRemove(item); + }; + + const closeModal = (): void => { + setModalOpened(false); + setItemToRemove(null); + }; + + const renderItemActions = typeof itemActions === 'function' ? itemActions : () => itemActions; + + return ( + + + theme.palette.primary.contrastText }} + /> + + {typeof totalCount === 'undefined' ? null : ( + {t('components.searchableList.info', { count: data.length, total: totalCount })} + )} + + + + theme.palette.primary.main, + }} + > + + Name + + +   + + + Actions + + + + + {loading && !data + ? times(firstPageSize, i => ) + : data.map((item, index) => ( + + + + {item.value} + + + + + {renderItemActions(item)} + {onDelete && ( + openModal(e, item)} size="large" aria-label={t('buttons.delete')}> + + + )} + + + + ))} + +
+
+ {loader} + +
+ ); +}; + +export default SimpleSearchableList; diff --git a/src/domain/templates/components/Dialogs/CreateEditTemplateDialog/CreateEditTemplateDialogBase.tsx b/src/domain/templates/components/Dialogs/CreateEditTemplateDialog/CreateEditTemplateDialogBase.tsx index 782b947cce..223b6b5fa1 100644 --- a/src/domain/templates/components/Dialogs/CreateEditTemplateDialog/CreateEditTemplateDialogBase.tsx +++ b/src/domain/templates/components/Dialogs/CreateEditTemplateDialog/CreateEditTemplateDialogBase.tsx @@ -31,7 +31,10 @@ const CreateEditTemplateDialogBase = ({ return ( - + {children?.({ actions: formik => ( diff --git a/src/domain/templates/components/Forms/TemplateForm.tsx b/src/domain/templates/components/Forms/TemplateForm.tsx index fc3c4a5da2..a00b9309ef 100644 --- a/src/domain/templates/components/Forms/TemplateForm.tsx +++ b/src/domain/templates/components/Forms/TemplateForm.tsx @@ -3,7 +3,9 @@ import { FormikProps } from 'formik'; import { TemplateType } from '../../../../core/apollo/generated/graphql-schema'; import { AnyTemplate } from '../../models/TemplateBase'; import CalloutTemplateForm, { CalloutTemplateFormSubmittedValues } from './CalloutTemplateForm'; -import CommunityGuidelinesTemplateForm, { CommunityGuidelinesTemplateFormSubmittedValues } from './CommunityGuidelinesTemplateForm'; +import CommunityGuidelinesTemplateForm, { + CommunityGuidelinesTemplateFormSubmittedValues, +} from './CommunityGuidelinesTemplateForm'; import PostTemplateForm, { PostTemplateFormSubmittedValues } from './PostTemplateForm'; import InnovationFlowTemplateForm, { InnovationFlowTemplateFormSubmittedValues } from './InnovationFlowTemplateForm'; import WhiteboardTemplateForm, { WhiteboardTemplateFormSubmittedValues } from './WhiteboardTemplateForm'; @@ -14,20 +16,25 @@ interface TemplateFormProps { actions: ReactNode | ((formState: FormikProps) => ReactNode); } -export type AnyTemplateFormSubmittedValues = CalloutTemplateFormSubmittedValues | CommunityGuidelinesTemplateFormSubmittedValues | PostTemplateFormSubmittedValues | InnovationFlowTemplateFormSubmittedValues | WhiteboardTemplateFormSubmittedValues; +export type AnyTemplateFormSubmittedValues = + | CalloutTemplateFormSubmittedValues + | CommunityGuidelinesTemplateFormSubmittedValues + | PostTemplateFormSubmittedValues + | InnovationFlowTemplateFormSubmittedValues + | WhiteboardTemplateFormSubmittedValues; const TemplateForm = ({ template, ...rest }: TemplateFormProps) => { switch (template.type) { case TemplateType.Callout: - return + return ; case TemplateType.CommunityGuidelines: - return + return ; case TemplateType.Post: - return + return ; case TemplateType.InnovationFlow: - return + return ; case TemplateType.Whiteboard: - return + return ; } throw new Error('Template type not supported'); }; diff --git a/src/domain/templates/components/Previews/CommunityGuidelinesTemplatePreview.tsx b/src/domain/templates/components/Previews/CommunityGuidelinesTemplatePreview.tsx index fba07e5094..0f20628849 100644 --- a/src/domain/templates/components/Previews/CommunityGuidelinesTemplatePreview.tsx +++ b/src/domain/templates/components/Previews/CommunityGuidelinesTemplatePreview.tsx @@ -16,8 +16,8 @@ interface CommunityGuidelinesTemplatePreviewProps { description?: string; references?: ReferenceWithAuthorization[]; }; - } - } + }; + }; } const CommunityGuidelinesTemplatePreview: FC = ({ template, loading }) => { diff --git a/src/domain/templates/components/Previews/InnovationFlowTemplatePreview.tsx b/src/domain/templates/components/Previews/InnovationFlowTemplatePreview.tsx index cb0cf88e7a..5977963e43 100644 --- a/src/domain/templates/components/Previews/InnovationFlowTemplatePreview.tsx +++ b/src/domain/templates/components/Previews/InnovationFlowTemplatePreview.tsx @@ -10,7 +10,7 @@ interface InnovationFlowTemplatePreviewProps { template?: { innovationFlow?: { states: InnovationFlowState[]; - } + }; }; } diff --git a/src/domain/templates/components/Previews/WhiteboardTemplatePreview.tsx b/src/domain/templates/components/Previews/WhiteboardTemplatePreview.tsx index d31b1664dd..1c90a1c37b 100644 --- a/src/domain/templates/components/Previews/WhiteboardTemplatePreview.tsx +++ b/src/domain/templates/components/Previews/WhiteboardTemplatePreview.tsx @@ -11,7 +11,7 @@ interface WhiteboardTemplatePreviewProps { template?: { whiteboard?: { content: string; - } + }; }; } @@ -21,9 +21,13 @@ const WhiteboardTemplatePreview: FC = ({ templat return ( <> - {loading && } + {loading && ( + + + + )} {!loading && ( - + = ({ template, link, ...rest }) => { case TemplateType.Post: return ; case TemplateType.Whiteboard: - return ; + return ; } return null; }; diff --git a/src/domain/templates/hooks/useCreateCalloutTemplate.ts b/src/domain/templates/hooks/useCreateCalloutTemplate.ts index 375a90421d..91873365e5 100644 --- a/src/domain/templates/hooks/useCreateCalloutTemplate.ts +++ b/src/domain/templates/hooks/useCreateCalloutTemplate.ts @@ -1,8 +1,6 @@ import { useCallback } from 'react'; import { CalloutTemplateFormSubmittedValues } from '../components/Forms/CalloutTemplateForm'; -import { - TemplateType, -} from '../../../core/apollo/generated/graphql-schema'; +import { TemplateType } from '../../../core/apollo/generated/graphql-schema'; import { useCreateTemplateMutation, useSpaceTemplatesSetIdLazyQuery, @@ -10,10 +8,7 @@ import { import { toCreateTemplateMutationVariables } from '../components/Forms/common/mappings'; export interface CalloutCreationUtils { - handleCreateCalloutTemplate: ( - values: CalloutTemplateFormSubmittedValues, - spaceNameId: string - ) => Promise; + handleCreateCalloutTemplate: (values: CalloutTemplateFormSubmittedValues, spaceNameId: string) => Promise; } export const useCreateCalloutTemplate = (): CalloutCreationUtils => { diff --git a/src/domain/templates/models/CollaborationTemplate.ts b/src/domain/templates/models/CollaborationTemplate.ts index 40e4e229bc..dd75b8aae2 100644 --- a/src/domain/templates/models/CollaborationTemplate.ts +++ b/src/domain/templates/models/CollaborationTemplate.ts @@ -7,7 +7,7 @@ export interface CollaborationTemplate extends TemplateBase { framing: { profile: { displayName: string; - } - } + }; + }; }[]; } diff --git a/src/domain/templates/models/CommunityGuidelinesTemplate.tsx b/src/domain/templates/models/CommunityGuidelinesTemplate.tsx index 03475e9356..3d11f7bf26 100644 --- a/src/domain/templates/models/CommunityGuidelinesTemplate.tsx +++ b/src/domain/templates/models/CommunityGuidelinesTemplate.tsx @@ -7,7 +7,7 @@ export interface CommunityGuidelinesTemplate extends TemplateBase { communityGuidelines?: { id: string; profile: { - id?: string; // it is set if editing a template. Is not sent to the server on update. It is used to update the References + id?: string; // it is set if editing a template. Is not sent to the server on update. It is used to update the References displayName: string; description?: string; references?: Reference[]; diff --git a/src/domain/templates/readme.md b/src/domain/templates/readme.md index 569e0243b4..545fb27cd1 100644 --- a/src/domain/templates/readme.md +++ b/src/domain/templates/readme.md @@ -12,5 +12,3 @@ - In the query we retrieve `defaultTagset`, on update is turned into `tagsets` to send it as an UpdateTagsetInput, and on Create is sent as just `tags` at the parent level - Avoid any casting or any ts-ignore, try to keep the types consistent!! Apart from the mappings file there shouldn't be any need for castings. - - diff --git a/src/main/admin/authorizationPolicies/AuthorizationPrivilegesForUser.tsx b/src/main/admin/authorizationPolicies/AuthorizationPrivilegesForUser.tsx index 93c6f0c973..b7c85d9103 100644 --- a/src/main/admin/authorizationPolicies/AuthorizationPrivilegesForUser.tsx +++ b/src/main/admin/authorizationPolicies/AuthorizationPrivilegesForUser.tsx @@ -6,7 +6,7 @@ import Loading from '../../../core/ui/loading/Loading'; import Gutters from '../../../core/ui/grid/Gutters'; import { BlockTitle } from '../../../core/ui/typography'; import useAdminGlobalUserList from '../../../domain/community/user/adminUsers/useAdminGlobalUserList'; -import { SearchableListItem } from '../../../domain/shared/components/SearchableList/SimpleSearchableList'; +import { SearchableListItem } from '../../../domain/platform/admin/components/SearchableList'; interface AuthorizationDialogProps { authorizationPolicyId: string; diff --git a/src/main/admin/users/adminUsers/AdminUsersPage.tsx b/src/main/admin/users/adminUsers/AdminUsersPage.tsx index 78b48b7611..2b90cbb511 100644 --- a/src/main/admin/users/adminUsers/AdminUsersPage.tsx +++ b/src/main/admin/users/adminUsers/AdminUsersPage.tsx @@ -1,18 +1,60 @@ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import AdminLayout from '../../../../domain/platform/admin/layout/toplevel/AdminLayout'; import { AdminSection } from '../../../../domain/platform/admin/layout/toplevel/constants'; import useAdminGlobalUserList from '../../../../domain/community/user/adminUsers/useAdminGlobalUserList'; -import SimpleSearchableList from '../../../../domain/shared/components/SearchableList/SimpleSearchableList'; import SearchableListLayout from '../../../../domain/shared/components/SearchableList/SearchableListLayout'; +import SimpleSearchableTable, { + SearchableListItem, +} from '../../../../domain/shared/components/SearchableList/SimpleSearchableTable'; +import { IconButton } from '@mui/material'; +import { TuneOutlined } from '@mui/icons-material'; +import LicensePlanDialog from '../../../../domain/community/contributor/organization/adminOrganizations/LicensePlanDialog'; const AdminUsersPage: FC = () => { - const { userList, ...listProps } = useAdminGlobalUserList(); + const { userList, licensePlans, ...listProps } = useAdminGlobalUserList(); + + const [licenseDialogOpen, setLicenseDialogOpen] = useState(false); + const [selectedItem, setSelectedItem] = useState(undefined); + + const onSettingsClick = (item: SearchableListItem) => { + setSelectedItem(item); + setLicenseDialogOpen(true); + }; + + const assignLicense = async (entityId: string, planId: string) => { + await listProps.assignLicensePlan(entityId, planId); + setLicenseDialogOpen(false); + }; + + const revokeLicense = async (entityId: string, planId: string) => { + await listProps.revokeLicensePlan(entityId, planId); + setLicenseDialogOpen(false); + }; + + const getActions = (item: SearchableListItem) => { + return ( + onSettingsClick(item)} size="large" aria-label={'License'}> + + + ); + }; return ( - + + {selectedItem?.accountId && ( + setLicenseDialogOpen(false)} + licensePlans={licensePlans} + assignLicensePlan={assignLicense} + revokeLicensePlan={revokeLicense} + activeLicensePlanIds={selectedItem.activeLicensePlanIds} + /> + )} ); }; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVCInfoDialog.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVCInfoDialog.tsx deleted file mode 100644 index 7d8113a9ea..0000000000 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVCInfoDialog.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import DialogWithGrid from '../../../../core/ui/dialog/DialogWithGrid'; -import DialogHeader from '../../../../core/ui/dialog/DialogHeader'; -import { Trans, useTranslation } from 'react-i18next'; -import Gutters from '../../../../core/ui/grid/Gutters'; -import { Box, Button, DialogContent, Tooltip } from '@mui/material'; -import { Caption } from '../../../../core/ui/typography'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import { gutters } from '../../../../core/ui/grid/utils'; - -import { Actions } from '../../../../core/ui/actions/Actions'; - -interface TryVCInfoDialogProps { - spaceId: string; - vcName: string; - open: boolean; - onClose: () => void; -} - -const TryVCInfoDialog: React.FC = ({ vcName, open, onClose }) => { - const { t } = useTranslation(); - - return ( - - - - - - - , - icon: , - tooltip: ( - - <> - - ), - }} - /> - - - - - , - }} - /> - - - - - - - - - ); -}; - -export default TryVCInfoDialog; diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVirtualContributorDialog.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVirtualContributorDialog.tsx index c41d7a4fcb..aa35826caa 100644 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVirtualContributorDialog.tsx +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/TryVirtualContributorDialog.tsx @@ -183,6 +183,7 @@ const TryVirtualContributorDialog: React.FC = values={{ vcName: vcData?.virtualContributor.profile.displayName ?? '' }} components={{ b: , + br:
, i: , icon: , tooltip: ( diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/newVirtualContributorQueries.graphql b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/newVirtualContributorQueries.graphql index cced3bf73c..4c78de3b03 100644 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/newVirtualContributorQueries.graphql +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/newVirtualContributorQueries.graphql @@ -12,12 +12,20 @@ query NewVirtualContributorMySpaces { id community { id + authorization { + id + myPrivileges + } } profile { id displayName url } + authorization { + id + myPrivileges + } subspaces { id type diff --git a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx index ba6321124b..ca39e12744 100644 --- a/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx +++ b/src/main/topLevelPages/myDashboard/newVirtualContributorWizard/useNewVirtualContributorWizard.tsx @@ -14,6 +14,7 @@ import { useAssignCommunityRoleToVirtualContributorMutation, } from '../../../../core/apollo/generated/apollo-hooks'; import { + AuthorizationPrivilege, CalloutGroupName, CalloutState, CalloutType, @@ -39,6 +40,7 @@ import { import SetupVCInfo from './SetupVCInfo'; import { info } from '../../../../core/logging/sentry/log'; import { compact } from 'lodash'; +import InfoDialog from '../../../../core/ui/dialogs/InfoDialog'; const SPACE_LABEL = '(space)'; const entityNamePostfixes = { @@ -46,7 +48,13 @@ const entityNamePostfixes = { SUBSPACE: "'s Knowledge Subspace", }; -type Step = 'initial' | 'createSpace' | 'addKnowledge' | 'existingKnowledge' | 'loadingVCSetup'; +type Step = + | 'initial' + | 'createSpace' + | 'addKnowledge' + | 'existingKnowledge' + | 'loadingVCSetup' + | 'insufficientPrivileges'; export interface UserAccountProps { id: string; @@ -57,17 +65,21 @@ export interface UserAccountProps { id: string; community: { id: string; + authorization?: { + myPrivileges?: AuthorizationPrivilege[] | undefined; + }; }; profile: { - id: string; displayName: string; url: string; }; + authorization?: { + myPrivileges?: AuthorizationPrivilege[] | undefined; + }; subspaces: Array<{ id: string; type: SpaceType; profile: { - id: string; displayName: string; url: string; }; @@ -143,10 +155,11 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide // selectableSpaces are space and subspaces // subspaces has communityId in order to manually add the VC to it - const { selectedExistingSpaceId, myAccountId, selectableSpaces } = useMemo(() => { + const { selectedExistingSpaceId, spacePrivileges, myAccountId, selectableSpaces } = useMemo(() => { const account = targetAccount ?? data?.me.user?.account; const accountId = account?.id; const mySpaces = compact(account?.spaces); + const mySpace = mySpaces?.[0]; // TODO: auto-selecting the first space, not ideal let selectableSpaces: SelectableKnowledgeProps[] = []; if (accountId) { @@ -174,8 +187,14 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide } return { - selectedExistingSpaceId: mySpaces?.[0]?.id, // TODO: auto-selecting the first space, not ideal + selectedExistingSpaceId: mySpace?.id, myAccountId: accountId, + spacePrivileges: { + myPrivileges: mySpace?.authorization?.myPrivileges, + collaboration: { + myPrivileges: mySpace?.community?.authorization?.myPrivileges, + }, + }, selectableSpaces, }; }, [data, user, targetAccount]); @@ -222,6 +241,18 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide [plansData, isPlanAvailable] ); + const hasPrivilegesOnSpaceAndCommunity = () => { + // todo: check create callout privilege (community) if needed + const { myPrivileges: spaceMyPrivileges } = spacePrivileges; + const { myPrivileges: collaborationMyPrivileges } = spacePrivileges.collaboration; + + return ( + spaceMyPrivileges?.includes(AuthorizationPrivilege.CreateSubspace) && + collaborationMyPrivileges?.includes(AuthorizationPrivilege.AccessVirtualContributor) && + collaborationMyPrivileges?.includes(AuthorizationPrivilege.CommunityAddMemberVcFromAccount) + ); + }; + const [CreateNewSpace] = useCreateSpaceMutation({ refetchQueries: ['MyAccount'], }); @@ -231,11 +262,16 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide } setVirtualContributorInput(values); - setStep('createSpace'); // in case of existing space, create subspace as BoK // otherwise create a new space if (selectedExistingSpaceId && myAccountId) { + if (!hasPrivilegesOnSpaceAndCommunity()) { + setStep('insufficientPrivileges'); + return; + } + setStep('createSpace'); + const subspace = await handleSubspaceCreation(selectedExistingSpaceId, values.name); setbokId(subspace?.data?.createSubspace.id); setBokCommunityId(subspace?.data?.createSubspace.community.id); @@ -250,6 +286,7 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide notify('No available plans for this account. Please, contact support@alkem.io.', 'error'); return; } + setStep('createSpace'); const { data: newSpace } = await CreateNewSpace({ variables: { @@ -398,7 +435,7 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide // create VC if (virtualContributorInput && myAccountId && bokId && bokCommunityId) { - await handleCreateVirtualContributor( + const creationSuccess = await handleCreateVirtualContributor( virtualContributorInput, myAccountId, bokId, @@ -406,8 +443,10 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide boKParentCommunityId ); - const { data } = await getNewSpaceUrl(); - navigate(data?.space.profile.url ?? ''); + if (creationSuccess) { + const { data } = await getNewSpaceUrl(); + navigate(data?.space.profile.url ?? ''); + } } }; @@ -427,62 +466,75 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide return; } - const { data } = await createVirtualContributor({ - variables: { - virtualContributorData: { - accountID: accountId, - profileData: { - displayName: values.name, - tagline: values.tagline, - description: - values.description ?? t('createVirtualContributorWizard.createdVirtualContributor.description'), - }, - aiPersona: { - aiPersonaService: { - bodyOfKnowledgeID: vcBoKId, + if (!hasPrivilegesOnSpaceAndCommunity()) { + setStep('insufficientPrivileges'); + return false; + } + + try { + const { data } = await createVirtualContributor({ + variables: { + virtualContributorData: { + accountID: accountId, + profileData: { + displayName: values.name, + tagline: values.tagline, + description: + values.description ?? t('createVirtualContributorWizard.createdVirtualContributor.description'), + }, + aiPersona: { + aiPersonaService: { + bodyOfKnowledgeID: vcBoKId, + }, }, }, }, - }, - }); + }); - const vcId = data?.createVirtualContributor.id; + const virtualContributorId = data?.createVirtualContributor.id; - if (vcId) { - if (parentCommunityId) { - // the VC cannot be added to the BoK community - // if it's not part of the parent community + if (virtualContributorId) { + if (parentCommunityId) { + // the VC cannot be added to the BoK community + // if it's not part of the parent community + await addVirtualContributorToCommunity({ + variables: { + communityId: parentCommunityId, + virtualContributorId, + }, + }); + } + + // add the VC to the BoK community await addVirtualContributorToCommunity({ variables: { - communityId: parentCommunityId, - virtualContributorId: vcId, + communityId: communityId, + virtualContributorId, }, }); - } - // add the VC to the BoK community - await addVirtualContributorToCommunity({ - variables: { - communityId: communityId, - virtualContributorId: vcId, - }, - }); + // add vc's nameId to the cache for the TryVC dialog + if (data?.createVirtualContributor.nameID) { + addVCCreationCache(data?.createVirtualContributor.nameID); + } - // add vc's nameId to the cache for the TryVC dialog - if (data?.createVirtualContributor.nameID) { - addVCCreationCache(data?.createVirtualContributor.nameID); + notify( + t('createVirtualContributorWizard.createdVirtualContributor.successMessage', { name: values.name }), + 'success' + ); + + return true; } - notify( - t('createVirtualContributorWizard.createdVirtualContributor.successMessage', { name: values.name }), - 'success' - ); + return false; + } catch (error) { + return false; } }; const handleCreateVCWithExistingKnowledge = async (selectedKnowledge: SelectableKnowledgeProps) => { if (selectedKnowledge && selectedKnowledge.communityId && virtualContributorInput) { - await handleCreateVirtualContributor( + const creationSuccess = await handleCreateVirtualContributor( virtualContributorInput, selectedKnowledge.accountId, selectedKnowledge.id, @@ -490,7 +542,7 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide selectedKnowledge.parentCommunityId ); - navigate(selectedKnowledge.url ?? ''); + creationSuccess && navigate(selectedKnowledge.url ?? ''); } }; @@ -527,6 +579,17 @@ const useNewVirtualContributorWizard = (): useNewVirtualContributorWizardProvide /> )} {step === 'loadingVCSetup' && } + {step === 'insufficientPrivileges' && ( + + )}
), [dialogOpen, step, loading]