Files
speckle-server/packages/frontend-2/lib/projects/composables/projectManagement.ts
T
andrewwallacespeckle e72b193f06 feat(fe2): move settings to tab on projects page (#2207)
* Add settings tab. Update style of component

* Structuring of files/components

* Updates to TexInput

* Add RadioGroup

* Last FE work

* FE Updates

* Webhooks Settings Tab

* Styling updates to webhooks

* Title/Description Update

* General Page done

* Collaborators WIP

* Styling updates

* Add custom message to updateProject

* Radio Group Same Height

* Styling updates to radio group. Disabled state

* Updates pre demo

* Updates to icons & post demo changes

* Major Updates

* Unsaved Changes Dialog

* Routing WIP

* Remove StatsBlock

* Auto update discussions on Private

* Routing/Redirects

* New input style

* Invite Dialog

* Fix mobile radiogroup

* Mobile Improvements

* Fix console warning

* Fix build

* Disabled States

* Fix console

* Unhide webhooks

* Updates from testing

* Responsive fixes

* Alignment fixes

* Fix textarea mobile height

* Updates to GraphQL Fragments

* Fix disabled state

* WIP Arrows for scroll

* Update PageTabs - broken

* Fix to PageTabs

* PageTab fix initial scroll

* Hide Scrollbar

* Better underline method

* Fix mobile initial underline

* Webhooks Empty State

* Fix input border

* Fix empty state

* Input Styling updates

* Remove mobile smaller text

* Update disabled state for disabled items

* Updates disabled sates on Settings Block

* Fix build. Disable Invite

* Fixes to invite permissions

* Disable role select when invite is disabled

* Small alignment fix

* Fix webhooks empty state

* cleaning up unnecessary vue files

* story improvement

* Remove DisabledMessage prop

* Fix disabled prop on Button

* Move team to Leave Fragment

* Remove unused Disabled Message props

* Add limit to graphql query

* Updates to BlockDiscussions

* add formatTriggers function to webhooks

* Remove md from button. Improved switch

* Update RadioGroup.stories.ts

* Update RadioGroup to use defineModel

* Various styling and copy updates

- More concise and accurate copy
- More readable
- Works better on mobile

* Updates to Invite Dialog

* Custom success Message

* Update slot names

* Remove md in TextInput. Set h-8 to default

* Changes from call with fabs

* Replace isOwner with composable

* Set SettingsBlock icon as optional

* Comments from PR

* Updates from PR

* Final Tidy Ups

* Fix Title/Description

* Fix spacing issue on Webhooks page

* Update borders and colors to align with Automate

Makes the same changes that I recently made to the unreleased Automate tab

* WEB-869

* Improve styling of radiogroup component

Better borders, hover effect, bigger checkmark icon, more subtle active background color, same across breakpoints.

* Adjust border styling of RadioGroup component

* Improve circle around checkmark in RadioGroup

* Split Tabs into 2 components

* Restyle overflow arrows

* Adjust gap and remove icons from vertical nav

Too many icons on the screen got distracting.

* Ensure active item visible

* Increase gap on vertical nav

* Update copy for Access and Discussions settings

* Input Tidyup

* WEB-877 update-collaborators-block

* Update inputs to new style

* Fix webhooks button hover state

* Fix comment copy

Appears in the access settings before embedding a model

* Remove hover shadow from search input on Dashboard

* Small change from Benjamin

* Change collaborators permissions copy

* Comments from PR #1

* Comments from PR #2

* Fix condition for EditableHeader

* Updates from CR

---------

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
Co-authored-by: Benjamin Ottensten <benjamin.ottensten@gmail.com>
2024-05-10 14:04:11 +02:00

436 lines
12 KiB
TypeScript

import { ApolloCache } from '@apollo/client/core'
import { useApolloClient, useSubscription } from '@vue/apollo-composable'
import type { MaybeRef } from '@vueuse/core'
import { isArray } from 'lodash-es'
import type { Get } from 'type-fest'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useLock } from '~~/lib/common/composables/singleton'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import { ProjectUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
import type {
OnProjectUpdatedSubscription,
ProjectCreateInput,
ProjectInviteCreateInput,
ProjectInviteUseInput,
ProjectUpdateInput,
ProjectUpdateRoleInput,
UpdateProjectMetadataMutation,
AdminPanelProjectsListQuery
} from '~~/lib/common/generated/gql/graphql'
import {
ROOT_QUERY,
convertThrowIntoFetchResult,
getCacheId,
getFirstErrorMessage,
modifyObjectFields
} from '~~/lib/common/helpers/graphql'
import { useNavigateToHome } from '~~/lib/common/helpers/route'
import {
cancelProjectInviteMutation,
createProjectMutation,
deleteProjectMutation,
inviteProjectUserMutation,
leaveProjectMutation,
updateProjectMetadataMutation,
updateProjectRoleMutation,
useProjectInviteMutation
} from '~~/lib/projects/graphql/mutations'
import { onProjectUpdatedSubscription } from '~~/lib/projects/graphql/subscriptions'
export function useProjectUpdateTracking(
projectId: MaybeRef<string>,
handler?: (
data: NonNullable<Get<OnProjectUpdatedSubscription, 'projectUpdated'>>,
cache: ApolloCache<unknown>
) => void,
options?: Partial<{
redirectOnDeletion: boolean
notifyOnUpdate?: boolean
}>
) {
const { redirectOnDeletion, notifyOnUpdate } = options || {}
const goHome = useNavigateToHome()
const { triggerNotification } = useGlobalToast()
const apollo = useApolloClient().client
const { hasLock } = useLock(
computed(() => `useProjectUpdateTracking-${unref(projectId)}`)
)
const isEnabled = computed(() => !!(hasLock.value || handler))
const { onResult: onProjectUpdated } = useSubscription(
onProjectUpdatedSubscription,
() => ({
id: unref(projectId)
}),
{ enabled: isEnabled }
)
onProjectUpdated((res) => {
if (!res.data?.projectUpdated || !hasLock.value) return
const event = res.data.projectUpdated
const cache = apollo.cache
const isDeleted = event.type === ProjectUpdatedMessageType.Deleted
if (isDeleted) {
cache.evict({
id: getCacheId('Project', event.id)
})
if (redirectOnDeletion) {
goHome()
}
if (redirectOnDeletion || notifyOnUpdate) {
triggerNotification({
type: ToastNotificationType.Info,
title: isDeleted ? 'Project deleted' : 'Project updated',
description: isDeleted ? 'Redirecting to home' : undefined
})
}
}
})
onProjectUpdated((res) => {
if (!res.data?.projectUpdated) return
const event = res.data.projectUpdated
handler?.(event, apollo.cache)
})
}
export function useCreateProject() {
const apollo = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const { activeUser } = useActiveUser()
return async (input: ProjectCreateInput) => {
const userId = activeUser.value?.id
if (!userId) return
const res = await apollo
.mutate({
mutation: createProjectMutation,
variables: { input },
update: (cache, { data }) => {
if (data?.projectMutations.create.id) {
modifyObjectFields<
undefined,
{ [key: string]: AdminPanelProjectsListQuery }
>(
cache,
ROOT_QUERY,
(fieldName, _variables, value, details) => {
const projectListFields = Object.keys(value).filter(
(k) =>
details.revolveFieldNameAndVariables(k).fieldName === 'projectList'
)
const newVal: typeof value = { ...value }
for (const field of projectListFields) {
delete newVal[field]
}
return newVal
},
{ fieldNameWhitelist: ['admin'] }
)
}
}
})
.catch(convertThrowIntoFetchResult)
if (!res.data?.projectMutations.create.id) {
const err = getFirstErrorMessage(res.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Project creation failed',
description: err
})
} else {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Project successfully created'
})
}
return res
}
}
export function useUpdateUserRole() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
return async (input: ProjectUpdateRoleInput) => {
const userId = activeUser.value?.id
if (!userId) return
const { data, errors } = await apollo
.mutate({
mutation: updateProjectRoleMutation,
variables: { input }
})
.catch(convertThrowIntoFetchResult)
if (!data?.projectMutations.updateRole.id) {
const err = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Permission update failed',
description: err
})
} else {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Project permissions updated'
})
}
return data?.projectMutations.updateRole
}
}
export function useInviteUserToProject() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
return async (
projectId: string,
input: ProjectInviteCreateInput | ProjectInviteCreateInput[]
) => {
const userId = activeUser.value?.id
if (!userId) return
const { data, errors } = await apollo
.mutate({
mutation: inviteProjectUserMutation,
variables: { input: isArray(input) ? input : [input], projectId }
})
.catch(convertThrowIntoFetchResult)
if (!data?.projectMutations.invites.batchCreate.id) {
const err = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Invitation failed',
description: err
})
} else {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Invite successfully sent'
})
}
return data?.projectMutations.invites.batchCreate
}
}
export function useCancelProjectInvite() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
return async (input: { projectId: string; inviteId: string }) => {
const userId = activeUser.value?.id
if (!userId) return
const { data, errors } = await apollo
.mutate({
mutation: cancelProjectInviteMutation,
variables: input
})
.catch(convertThrowIntoFetchResult)
if (!data?.projectMutations.invites.cancel) {
const err = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Invitation cancelation failed',
description: err
})
} else {
triggerNotification({
type: ToastNotificationType.Info,
title: 'Invitation canceled'
})
}
return data?.projectMutations.invites.cancel
}
}
export function useUpdateProject() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
return async (
update: ProjectUpdateInput,
options?: Partial<{
customSuccessMessage?: string
optimisticResponse: UpdateProjectMetadataMutation
}>
) => {
if (!activeUser.value) return
const successMessage = options?.customSuccessMessage || 'Project updated'
const result = await apollo
.mutate({
mutation: updateProjectMetadataMutation,
variables: { update },
optimisticResponse: options?.optimisticResponse
})
.catch(convertThrowIntoFetchResult)
if (result?.data?.projectMutations.update?.id) {
triggerNotification({
type: ToastNotificationType.Success,
title: successMessage
})
} else {
const errMsg = getFirstErrorMessage(result.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Project update failed',
description: errMsg
})
}
return result.data?.projectMutations.update
}
}
export function useDeleteProject() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
const navigateHome = useNavigateToHome()
return async (id: string, options?: Partial<{ goHome: boolean }>) => {
if (!activeUser.value) return
const { goHome } = options || {}
const result = await apollo
.mutate({
mutation: deleteProjectMutation,
variables: {
id
}
})
.catch(convertThrowIntoFetchResult)
if (result?.data?.projectMutations.delete) {
triggerNotification({
type: ToastNotificationType.Info,
title: 'Project deleted'
})
if (goHome) {
navigateHome()
}
// evict project from cache
apollo.cache.evict({
id: getCacheId('Project', id)
})
} else {
const errMsg = getFirstErrorMessage(result.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Project deletion failed',
description: errMsg
})
}
return !!result.data?.projectMutations.delete
}
}
export function useProcessProjectInvite() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
return async (
input: ProjectInviteUseInput,
options?: Partial<{ inviteId: string }>
) => {
if (!activeUser.value) return
const { data, errors } = await apollo
.mutate({
mutation: useProjectInviteMutation,
variables: { input },
update: (cache, { data }) => {
if (!data?.projectMutations.invites.use || !options?.inviteId) return
// Evict PendingStreamCollaborator
cache.evict({
id: getCacheId('PendingStreamCollaborator', options.inviteId)
})
}
})
.catch(convertThrowIntoFetchResult)
if (data?.projectMutations.invites.use) {
triggerNotification({
type: input.accept ? ToastNotificationType.Success : ToastNotificationType.Info,
title: input.accept ? 'Invite accepted' : 'Invite dismissed'
})
} else {
const errMsg = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: "Couldn't process invite",
description: errMsg
})
}
return data?.projectMutations.invites.use
}
}
export function useLeaveProject() {
const apollo = useApolloClient().client
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
const navigateHome = useNavigateToHome()
return async (projectId: string, options?: Partial<{ goHome: boolean }>) => {
if (!activeUser.value) return
const { data, errors } = await apollo
.mutate({
mutation: leaveProjectMutation,
variables: { projectId },
update: (cache, { data }) => {
if (!data?.projectMutations.leave) return
cache.evict({
id: getCacheId('Project', projectId)
})
}
})
.catch(convertThrowIntoFetchResult)
if (data?.projectMutations.leave) {
triggerNotification({
type: ToastNotificationType.Info,
title: "You've left the project"
})
if (options?.goHome) navigateHome()
} else {
const errMsg = getFirstErrorMessage(errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: "Couldn't leave project",
description: errMsg
})
}
}
}