-
Notifications
You must be signed in to change notification settings - Fork 5.5k
feat: add command menu item to layout customization #18764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
984bf6e
a7bb9b0
9a9cc0c
047e2ae
e08c292
e722f5c
539f182
320c84c
74ca958
2e5bdab
b3a532e
f692427
c0e5923
f372306
3588f7c
239cb7f
bd8c9dc
2d30f24
6c46478
eb937a9
f95f1a9
3b77d8e
3c205de
393bf2b
7c90b2c
442c28d
b1ea211
550b51e
bf5b034
42cb345
9d88056
f09ef69
6e36176
c54e981
2c121b9
7a9ab4f
705b637
a8e68c0
f2a4352
e90865d
7ab77f0
d0ed3b7
35e4109
2181df3
0d30f1e
cab2fd0
6dc63a7
b34acd7
d35e071
b8cd83c
429307a
62888ed
839ad0f
89a2b16
850354f
c3d4708
4ea0b94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { styled } from '@linaria/react'; | ||
| import { motion } from 'framer-motion'; | ||
| import { useContext } from 'react'; | ||
| import { type IconComponent } from 'twenty-ui/display'; | ||
| import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; | ||
|
|
||
| type AnimatedIconCrossfadeProps = { | ||
| isToggled: boolean; | ||
| DefaultIcon: IconComponent; | ||
| ToggledIcon: IconComponent; | ||
| }; | ||
|
|
||
| const StyledContainer = styled.div` | ||
| height: calc(${themeCssVariables.icon.size.sm} * 1px); | ||
| overflow: hidden; | ||
| position: relative; | ||
| width: calc(${themeCssVariables.icon.size.sm} * 1px); | ||
| `; | ||
|
|
||
| const StyledLayer = styled(motion.div)` | ||
| align-items: center; | ||
| display: flex; | ||
| inset: 0; | ||
| justify-content: center; | ||
| position: absolute; | ||
| `; | ||
|
|
||
| export const AnimatedIconCrossfade = ({ | ||
| isToggled, | ||
| DefaultIcon, | ||
| ToggledIcon, | ||
| }: AnimatedIconCrossfadeProps) => { | ||
| const { theme } = useContext(ThemeContext); | ||
|
|
||
| return ( | ||
| <StyledContainer> | ||
| <StyledLayer | ||
| initial={false} | ||
| animate={{ | ||
| opacity: isToggled ? 0 : 1, | ||
| scale: isToggled ? 0.85 : 1, | ||
| }} | ||
| transition={{ | ||
| duration: theme.animation.duration.fast, | ||
| ease: 'easeInOut', | ||
| }} | ||
| > | ||
| <DefaultIcon size={theme.icon.size.sm} /> | ||
| </StyledLayer> | ||
| <StyledLayer | ||
| initial={false} | ||
| animate={{ | ||
| opacity: isToggled ? 1 : 0, | ||
| scale: isToggled ? 1 : 0.85, | ||
| }} | ||
| transition={{ | ||
| duration: theme.animation.duration.fast, | ||
| ease: 'easeInOut', | ||
| }} | ||
| > | ||
| <ToggledIcon size={theme.icon.size.sm} /> | ||
| </StyledLayer> | ||
| </StyledContainer> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { gql } from '@apollo/client'; | ||
|
|
||
| export const FIND_COMMAND_MENU_ITEM_DEFAULT_VALUES = gql` | ||
| query FindCommandMenuItemDefaultValues($ids: [UUID!]!) { | ||
| commandMenuItemDefaultValues(ids: $ids) { | ||
| id | ||
| label | ||
| shortLabel | ||
| isPinned | ||
| position | ||
| } | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider'; | ||
| import { SidePanelCommandMenuItemDisplayPage } from '@/command-menu-item/server-items/display/components/SidePanelCommandMenuItemDisplayPage'; | ||
| import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; | ||
| import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; | ||
| import { SidePanelRootPage } from '@/side-panel/pages/root/components/SidePanelRootPage'; | ||
| import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; | ||
| import { FeatureFlagKey } from '~/generated-metadata/graphql'; | ||
|
|
||
| export const SidePanelCommandMenuDisplayPageEntry = () => { | ||
| const isCommandMenuItemEnabled = useIsFeatureEnabled( | ||
| FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED, | ||
| ); | ||
|
|
||
| const pageContent = isCommandMenuItemEnabled ? ( | ||
| <SidePanelCommandMenuItemDisplayPage /> | ||
| ) : ( | ||
| <SidePanelRootPage /> | ||
| ); | ||
|
|
||
| return ( | ||
| <ContextStoreComponentInstanceContext.Provider | ||
| value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }} | ||
| > | ||
| <CommandMenuContextProvider | ||
| isInSidePanel={true} | ||
| displayType="listItem" | ||
| containerType="command-menu-list" | ||
| > | ||
| {pageContent} | ||
| </CommandMenuContextProvider> | ||
| </ContextStoreComponentInstanceContext.Provider> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider'; | ||
| import { SidePanelCommandMenuItemEditPage } from '@/command-menu-item/server-items/edit/components/SidePanelCommandMenuItemEditPage'; | ||
| import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; | ||
| import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; | ||
| import { SidePanelRootPage } from '@/side-panel/pages/root/components/SidePanelRootPage'; | ||
| import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; | ||
| import { FeatureFlagKey } from '~/generated-metadata/graphql'; | ||
|
|
||
| export const SidePanelCommandMenuEditPageEntry = () => { | ||
| const isCommandMenuItemEnabled = useIsFeatureEnabled( | ||
| FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED, | ||
| ); | ||
|
|
||
| const pageContent = isCommandMenuItemEnabled ? ( | ||
| <SidePanelCommandMenuItemEditPage /> | ||
| ) : ( | ||
| <SidePanelRootPage /> | ||
| ); | ||
|
|
||
| return ( | ||
| <ContextStoreComponentInstanceContext.Provider | ||
| value={{ instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID }} | ||
| > | ||
| <CommandMenuContextProvider | ||
| isInSidePanel={true} | ||
| displayType="listItem" | ||
| containerType="command-menu-list" | ||
| > | ||
| {pageContent} | ||
| </CommandMenuContextProvider> | ||
| </ContextStoreComponentInstanceContext.Provider> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| import { CoreObjectNameSingular } from 'twenty-shared/types'; | ||
| import { isDefined } from 'twenty-shared/utils'; | ||
|
|
||
| import { useCommandMenuContextApi } from '@/command-menu-item/server-items/hooks/useCommandMenuContextApi'; | ||
| import { type CommandMenuContextType } from '@/command-menu-item/contexts/CommandMenuContext'; | ||
| import { useCommandMenuContextApi } from '@/command-menu-item/server-items/common/hooks/useCommandMenuContextApi'; | ||
|
|
||
| import { CommandMenuContextProviderServerItemsContent } from './CommandMenuContextProviderServerItemsContent'; | ||
| import { CommandMenuContextProviderServerItemsWithWorkflowEnrichment } from './CommandMenuContextProviderServerItemsWithWorkflowEnrichment'; | ||
|
|
@@ -20,7 +20,9 @@ export const CommandMenuContextProviderServerItems = ({ | |
| containerType, | ||
| children, | ||
| }: CommandMenuContextProviderServerItemsProps) => { | ||
| const commandMenuContextApi = useCommandMenuContextApi(); | ||
| const commandMenuContextApi = useCommandMenuContextApi({ | ||
| isInSidePanel, | ||
| }); | ||
|
|
||
| const currentObjectNameSingular = | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The code unconditionally accesses Suggested FixAdd a null check before accessing Prompt for AI Agent |
||
| commandMenuContextApi.objectMetadataItem.nameSingular; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { COMMAND_MENU_ITEM_FRAGMENT } from '@/command-menu-item/graphql/fragments/commandMenuItemFragment'; | ||
| import { gql } from '@apollo/client'; | ||
|
|
||
| export const UPDATE_COMMAND_MENU_ITEM = gql` | ||
| ${COMMAND_MENU_ITEM_FRAGMENT} | ||
| mutation UpdateCommandMenuItem($input: UpdateCommandMenuItemInput!) { | ||
| updateCommandMenuItem(input: $input) { | ||
| ...CommandMenuItemFields | ||
| } | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { useCommandMenuFavoriteRecordIds } from '@/command-menu-item/server-items/common/hooks/useCommandMenuFavoriteRecordIds'; | ||
| import { useCommandMenuFeatureFlags } from '@/command-menu-item/server-items/common/hooks/useCommandMenuFeatureFlags'; | ||
| import { useCommandMenuObjectPermissions } from '@/command-menu-item/server-items/common/hooks/useCommandMenuObjectPermissions'; | ||
| import { useCommandMenuPageContext } from '@/command-menu-item/server-items/common/hooks/useCommandMenuPageContext'; | ||
| import { useCommandMenuTargetObjectPermissions } from '@/command-menu-item/server-items/common/hooks/useCommandMenuTargetObjectPermissions'; | ||
| import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; | ||
| import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; | ||
| import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; | ||
| import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; | ||
| import { recordStoreRecordsSelector } from '@/object-record/record-store/states/selectors/recordStoreRecordsSelector'; | ||
| import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; | ||
| import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; | ||
| import { type CommandMenuContextApi } from 'twenty-shared/types'; | ||
|
|
||
| export const useCommandMenuContextApi = ({ | ||
| isInSidePanel, | ||
| }: { | ||
| isInSidePanel: boolean; | ||
| }): CommandMenuContextApi => { | ||
| const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue( | ||
| contextStoreCurrentObjectMetadataItemIdComponentState, | ||
| ); | ||
|
|
||
| const contextStoreTargetedRecordsRule = useAtomComponentStateValue( | ||
| contextStoreTargetedRecordsRuleComponentState, | ||
| ); | ||
|
|
||
| const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue( | ||
| contextStoreNumberOfSelectedRecordsComponentState, | ||
| ); | ||
|
|
||
| const { objectMetadataItems } = useObjectMetadataItems(); | ||
| const objectMetadataItem = objectMetadataItems.find( | ||
| (item) => item.id === contextStoreCurrentObjectMetadataItemId, | ||
| ); | ||
|
|
||
| const recordIds = | ||
| contextStoreTargetedRecordsRule.mode === 'selection' | ||
| ? contextStoreTargetedRecordsRule.selectedRecordIds | ||
| : undefined; | ||
|
|
||
| const selectedRecords = useAtomFamilySelectorValue( | ||
| recordStoreRecordsSelector, | ||
| { | ||
| recordIds: recordIds ?? [], | ||
| }, | ||
| ); | ||
|
|
||
| const favoriteRecordIds = useCommandMenuFavoriteRecordIds({ | ||
| recordIds, | ||
| objectMetadataItemId: objectMetadataItem?.id, | ||
| }); | ||
| const objectPermissions = useCommandMenuObjectPermissions({ | ||
| objectMetadataItemId: objectMetadataItem?.id, | ||
| }); | ||
| const featureFlags = useCommandMenuFeatureFlags(); | ||
| const { pageType, isPageInEditMode, hasAnySoftDeleteFilterOnView } = | ||
| useCommandMenuPageContext(); | ||
| const { targetObjectReadPermissions, targetObjectWritePermissions } = | ||
| useCommandMenuTargetObjectPermissions(); | ||
|
|
||
| const isSelectAll = contextStoreTargetedRecordsRule.mode === 'exclusion'; | ||
|
|
||
| return { | ||
| pageType, | ||
| isInSidePanel, | ||
| isPageInEditMode, | ||
| favoriteRecordIds, | ||
| isSelectAll, | ||
| hasAnySoftDeleteFilterOnView, | ||
| numberOfSelectedRecords: contextStoreNumberOfSelectedRecords, | ||
| objectPermissions, | ||
| selectedRecords, | ||
| featureFlags, | ||
| targetObjectReadPermissions, | ||
| targetObjectWritePermissions, | ||
| objectMetadataItem: objectMetadataItem ?? {}, | ||
| }; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.