diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx index 4aba129de..ebbfaf985 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx @@ -1,10 +1,12 @@ import { Key } from 'ts-key-enum'; import { AppTooltip, + IconCopy, IconLayout, IconLayoutList, IconList, IconTag, + IconTrash, MenuItem, useIcons, } from 'twenty-ui'; @@ -13,14 +15,20 @@ import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdow import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { ViewType } from '@/views/types/ViewType'; +import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState'; +import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useTheme } from '@emotion/react'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; @@ -66,6 +74,21 @@ export const ObjectOptionsDropdownMenuContent = () => { FeatureFlagKey.IsCommandMenuV2Enabled, ); + const { deleteViewFromCurrentState } = useDeleteViewFromCurrentState(); + const setViewPickerReferenceViewId = useSetRecoilComponentStateV2( + viewPickerReferenceViewIdComponentState, + ); + const handleDelete = () => { + if (!currentView?.id) { + return; + } + setViewPickerReferenceViewId(currentView?.id); + deleteViewFromCurrentState(); + closeDropdown(); + }; + const theme = useTheme(); + const { enqueueSnackBar } = useSnackBar(); + return ( <> @@ -122,6 +145,39 @@ export const ObjectOptionsDropdownMenuContent = () => { width="100%" /> )} + + + { + const currentUrl = window.location.href; + navigator.clipboard.writeText(currentUrl); + enqueueSnackBar('Link copied to clipboard', { + variant: SnackBarVariant.Success, + icon: , + duration: 2000, + }); + }} + LeftIcon={IconCopy} + text={t`Copy link to view`} + /> +
+ handleDelete()} + LeftIcon={IconTrash} + text={t`Delete view`} + disabled={currentView?.key === 'INDEX'} + /> +
+ {currentView?.key === 'INDEX' && ( + + )} ); diff --git a/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-many.pre-query.hook.ts b/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-many.pre-query.hook.ts new file mode 100644 index 000000000..a70fe63b4 --- /dev/null +++ b/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-many.pre-query.hook.ts @@ -0,0 +1,26 @@ +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { + ViewException, + ViewExceptionCode, + ViewExceptionMessage, +} from 'src/modules/view/views.exception'; + +@WorkspaceQueryHook(`view.deleteMany`) +export class ViewDeleteManyPreQueryHook implements WorkspaceQueryHookInstance { + constructor() {} + + async execute( + _authContext: AuthContext, + _objectName: string, + _payload: DeleteManyResolverArgs, + ): Promise { + throw new ViewException( + ViewExceptionMessage.METHOD_NOT_IMPLEMENTED, + ViewExceptionCode.METHOD_NOT_IMPLEMENTED, + ); + } +} diff --git a/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-one.pre-query.hook.ts new file mode 100644 index 000000000..05d2742fa --- /dev/null +++ b/packages/twenty-server/src/modules/view/pre-hooks.ts/view-delete-one.pre-query.hook.ts @@ -0,0 +1,52 @@ +import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { + ViewException, + ViewExceptionCode, + ViewExceptionMessage, +} from 'src/modules/view/views.exception'; + +@WorkspaceQueryHook(`view.deleteOne`) +export class ViewDeleteOnePreQueryHook implements WorkspaceQueryHookInstance { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async execute( + authContext: AuthContext, + _objectName: string, + payload: DeleteOneResolverArgs, + ): Promise { + const targettedViewId = payload.id; + + const viewRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + authContext.workspace.id, + 'view', + ); + + const view = await viewRepository.findOne({ + where: { id: targettedViewId }, + }); + + if (!view) { + throw new ViewException( + ViewExceptionMessage.VIEW_NOT_FOUND, + ViewExceptionCode.VIEW_NOT_FOUND, + ); + } + + if (view.key === 'INDEX') { + throw new ViewException( + ViewExceptionMessage.CANNOT_DELETE_INDEX_VIEW, + ViewExceptionCode.CANNOT_DELETE_INDEX_VIEW, + ); + } + + return payload; + } +} diff --git a/packages/twenty-server/src/modules/view/view.module.ts b/packages/twenty-server/src/modules/view/view.module.ts index 5ae3dbd98..2197de393 100644 --- a/packages/twenty-server/src/modules/view/view.module.ts +++ b/packages/twenty-server/src/modules/view/view.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ViewService } from 'src/modules/view/services/view.service'; +import { ViewDeleteOnePreQueryHook } from './pre-hooks.ts/view-delete-one.pre-query.hook'; @Module({ imports: [], - providers: [ViewService], + providers: [ViewService, ViewDeleteOnePreQueryHook], exports: [ViewService], }) export class ViewModule {} diff --git a/packages/twenty-server/src/modules/view/views.exception.ts b/packages/twenty-server/src/modules/view/views.exception.ts new file mode 100644 index 000000000..46cb651ed --- /dev/null +++ b/packages/twenty-server/src/modules/view/views.exception.ts @@ -0,0 +1,19 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class ViewException extends CustomException { + constructor(message: string, code: ViewExceptionCode) { + super(message, code); + } +} + +export enum ViewExceptionCode { + VIEW_NOT_FOUND = 'VIEW_NOT_FOUND', + CANNOT_DELETE_INDEX_VIEW = 'CANNOT_DELETE_INDEX_VIEW', + METHOD_NOT_IMPLEMENTED = 'METHOD_NOT_IMPLEMENTED', +} + +export enum ViewExceptionMessage { + VIEW_NOT_FOUND = 'View not found', + CANNOT_DELETE_INDEX_VIEW = 'Cannot delete index view', + METHOD_NOT_IMPLEMENTED = 'Method not implemented', +}