Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a32032b78 | ||
|
|
ea5091f8c0 | ||
|
|
5f5f654602 |
@@ -2524,6 +2524,7 @@ type File {
|
||||
|
||||
type MarketplaceApp {
|
||||
id: String!
|
||||
universalIdentifier: String!
|
||||
name: String!
|
||||
description: String!
|
||||
icon: String!
|
||||
@@ -2651,7 +2652,6 @@ type CommandMenuItem {
|
||||
hotKeys: [String!]
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
pageLayoutId: UUID
|
||||
applicationId: UUID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
@@ -4184,7 +4184,6 @@ input CreateCommandMenuItemInput {
|
||||
conditionalAvailabilityExpression: String
|
||||
availabilityObjectMetadataId: UUID
|
||||
payload: JSON
|
||||
pageLayoutId: UUID
|
||||
}
|
||||
|
||||
input UpdateCommandMenuItemInput {
|
||||
@@ -4198,7 +4197,6 @@ input UpdateCommandMenuItemInput {
|
||||
availabilityObjectMetadataId: UUID
|
||||
engineComponentKey: EngineComponentKey
|
||||
hotKeys: [String!]
|
||||
pageLayoutId: UUID
|
||||
}
|
||||
|
||||
input CreateFrontComponentInput {
|
||||
|
||||
@@ -2235,6 +2235,7 @@ export interface File {
|
||||
|
||||
export interface MarketplaceApp {
|
||||
id: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
icon: Scalars['String']
|
||||
@@ -2368,7 +2369,6 @@ export interface CommandMenuItem {
|
||||
hotKeys?: Scalars['String'][]
|
||||
conditionalAvailabilityExpression?: Scalars['String']
|
||||
availabilityObjectMetadataId?: Scalars['UUID']
|
||||
pageLayoutId?: Scalars['UUID']
|
||||
applicationId?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
@@ -5565,6 +5565,7 @@ export interface FileGenqlSelection{
|
||||
|
||||
export interface MarketplaceAppGenqlSelection{
|
||||
id?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
icon?: boolean | number
|
||||
@@ -5707,7 +5708,6 @@ export interface CommandMenuItemGenqlSelection{
|
||||
hotKeys?: boolean | number
|
||||
conditionalAvailabilityExpression?: boolean | number
|
||||
availabilityObjectMetadataId?: boolean | number
|
||||
pageLayoutId?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
@@ -6744,9 +6744,9 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
|
||||
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
|
||||
|
||||
export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
|
||||
|
||||
|
||||
@@ -5036,6 +5036,9 @@ export default {
|
||||
"id": [
|
||||
1
|
||||
],
|
||||
"universalIdentifier": [
|
||||
1
|
||||
],
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
@@ -5334,9 +5337,6 @@ export default {
|
||||
"availabilityObjectMetadataId": [
|
||||
3
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"applicationId": [
|
||||
3
|
||||
],
|
||||
@@ -10669,9 +10669,6 @@ export default {
|
||||
"payload": [
|
||||
15
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
@@ -10707,9 +10704,6 @@ export default {
|
||||
"hotKeys": [
|
||||
1
|
||||
],
|
||||
"pageLayoutId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -172,14 +172,6 @@ const SettingsApplications = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationDetails = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsApplicationDetails').then(
|
||||
(module) => ({
|
||||
default: module.SettingsApplicationDetails,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail'
|
||||
@@ -188,12 +180,12 @@ const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAvailableApplicationDetails = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/SettingsAvailableApplicationDetails'
|
||||
).then((module) => ({
|
||||
default: module.SettingsAvailableApplicationDetails,
|
||||
})),
|
||||
const SettingsApplicationPage = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsApplicationPage').then(
|
||||
(module) => ({
|
||||
default: module.SettingsApplicationPage,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationRegistrationDetails = lazy(() =>
|
||||
@@ -738,11 +730,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationDetail}
|
||||
element={<SettingsApplicationDetails />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AvailableApplicationDetail}
|
||||
element={<SettingsAvailableApplicationDetails />}
|
||||
element={<SettingsApplicationPage />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationDetail}
|
||||
|
||||
@@ -32,7 +32,6 @@ import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useRes
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
@@ -45,7 +44,6 @@ import { AppBasePath, AppPath, SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AnalyticsType } from '~/generated-metadata/graphql';
|
||||
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
|
||||
import { getPageLayoutIdForLocation } from '~/modules/app/utils/getPageLayoutIdForLocation';
|
||||
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
@@ -148,15 +146,10 @@ export const PageChangeEffect = () => {
|
||||
if (!previousLocation || previousLocation !== location.pathname) {
|
||||
setPreviousLocation(location.pathname);
|
||||
executeTasksOnAnyLocationChange();
|
||||
|
||||
const newPageLayoutId = getPageLayoutIdForLocation({
|
||||
location,
|
||||
store,
|
||||
});
|
||||
|
||||
store.set(currentPageLayoutIdState.atom, newPageLayoutId);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}, [location, previousLocation, executeTasksOnAnyLocationChange, store]);
|
||||
}, [location, previousLocation, executeTasksOnAnyLocationChange]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeQueryParamState();
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { type getDefaultStore } from 'jotai';
|
||||
import { type Location, matchPath } from 'react-router-dom';
|
||||
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
|
||||
const DASHBOARD_NAME_SINGULAR = CoreObjectNameSingular.Dashboard;
|
||||
|
||||
export const getPageLayoutIdForLocation = ({
|
||||
location,
|
||||
store,
|
||||
}: {
|
||||
location: Location;
|
||||
store: ReturnType<typeof getDefaultStore>;
|
||||
}): string | null => {
|
||||
const recordShowMatch = matchPath(AppPath.RecordShowPage, location.pathname);
|
||||
|
||||
if (isDefined(recordShowMatch?.params.objectNameSingular)) {
|
||||
const objectNameSingular = recordShowMatch.params.objectNameSingular;
|
||||
|
||||
if (objectNameSingular === DASHBOARD_NAME_SINGULAR) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectMetadataItem = store.get(
|
||||
objectMetadataItemFamilySelector.selectorFamily({
|
||||
objectName: objectNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordPageLayout = store.get(
|
||||
recordPageLayoutByObjectMetadataIdFamilySelector.selectorFamily({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return isDefined(recordPageLayout)
|
||||
? recordPageLayout.id
|
||||
: getDefaultRecordPageLayoutId({
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
const pageLayoutMatch = matchPath(AppPath.PageLayoutPage, location.pathname);
|
||||
|
||||
if (isDefined(pageLayoutMatch?.params.pageLayoutId)) {
|
||||
return pageLayoutMatch.params.pageLayoutId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { APPLICATION_REGISTRATION_FRAGMENT } from '@/settings/application-registrations/graphql/fragments/applicationRegistrationFragment';
|
||||
|
||||
export const FIND_APPLICATION_REGISTRATION_BY_UNIVERSAL_IDENTIFIER = gql`
|
||||
query findApplicationRegistrationByUniversalIdentifier(
|
||||
$universalIdentifier: String!
|
||||
) {
|
||||
findApplicationRegistrationByUniversalIdentifier(
|
||||
universalIdentifier: $universalIdentifier
|
||||
) {
|
||||
...ApplicationRegistrationFragment
|
||||
}
|
||||
${APPLICATION_REGISTRATION_FRAGMENT}
|
||||
}
|
||||
`;
|
||||
+1
-5
@@ -5,9 +5,7 @@ import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/compon
|
||||
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { doesCommandMenuItemMatchPageLayoutId } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageLayoutId';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
@@ -24,7 +22,6 @@ export const StandalonePageCommandMenu = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const commandMenuContextApi = useMemo<CommandMenuContextApi>(() => {
|
||||
@@ -86,7 +83,6 @@ export const StandalonePageCommandMenu = () => {
|
||||
item.availabilityType !==
|
||||
CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
|
||||
)
|
||||
.filter(doesCommandMenuItemMatchPageLayoutId(currentPageLayoutId))
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
@@ -96,7 +92,7 @@ export const StandalonePageCommandMenu = () => {
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi, currentPageLayoutId]);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
|
||||
+1
-5
@@ -4,9 +4,7 @@ import {
|
||||
} from '@/command-menu-item/contexts/CommandMenuContext';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { doesCommandMenuItemMatchObjectMetadataId } from '@/command-menu-item/utils/doesCommandMenuItemMatchObjectMetadataId';
|
||||
import { doesCommandMenuItemMatchPageLayoutId } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageLayoutId';
|
||||
import { doesCommandMenuItemMatchPageType } from '@/command-menu-item/utils/doesCommandMenuItemMatchPageType';
|
||||
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type CommandMenuContextApi } from 'twenty-shared/types';
|
||||
@@ -26,7 +24,6 @@ export const CommandMenuContextProviderContent = ({
|
||||
commandMenuContextApi,
|
||||
}: CommandMenuContextProviderContentProps) => {
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
|
||||
|
||||
const filteredCommandMenuItems = useMemo(() => {
|
||||
const currentObjectMetadataItemId =
|
||||
@@ -37,7 +34,6 @@ export const CommandMenuContextProviderContent = ({
|
||||
doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId),
|
||||
)
|
||||
.filter(doesCommandMenuItemMatchPageType(commandMenuContextApi.pageType))
|
||||
.filter(doesCommandMenuItemMatchPageLayoutId(currentPageLayoutId))
|
||||
.filter((item) =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
@@ -47,7 +43,7 @@ export const CommandMenuContextProviderContent = ({
|
||||
.sort(
|
||||
(firstItem, secondItem) => firstItem.position - secondItem.position,
|
||||
);
|
||||
}, [commandMenuItems, commandMenuContextApi, currentPageLayoutId]);
|
||||
}, [commandMenuItems, commandMenuContextApi]);
|
||||
|
||||
return (
|
||||
<CommandMenuContext.Provider
|
||||
|
||||
+24
-5
@@ -3,7 +3,10 @@ import { Suspense, lazy } from 'react';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { CommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/CommandComponentInstanceContext';
|
||||
import { isHeadlessFrontComponentCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessFrontComponentCommandContextApi';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const FrontComponentRenderer = lazy(() =>
|
||||
import('@/front-components/components/FrontComponentRenderer').then(
|
||||
@@ -24,18 +27,34 @@ export const HeadlessFrontComponentRendererEngineCommand = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const objectNameSingular = context.objectMetadataItem?.nameSingular;
|
||||
|
||||
const recordId =
|
||||
context.selectedRecords.length === 1
|
||||
? context.selectedRecords[0].id
|
||||
: undefined;
|
||||
|
||||
// TODO: Remove layout rendering provider once we have refactored FrontComponentRenderer to have one headless renderer and a standard renderer
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={context.frontComponentId}
|
||||
commandMenuItemId={commandMenuItemId}
|
||||
recordId={recordId}
|
||||
/>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier:
|
||||
isDefined(objectNameSingular) && isDefined(recordId)
|
||||
? {
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInSidePanel: false,
|
||||
}}
|
||||
>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={context.frontComponentId}
|
||||
commandMenuItemId={commandMenuItemId}
|
||||
/>
|
||||
</LayoutRenderingProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
+117
-6
@@ -9,6 +9,8 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const mockFindOneWorkflowVersion = jest.fn();
|
||||
const mockEnqueueWarningSnackBar = jest.fn();
|
||||
const mockBuildTriggerWorkflowVersionPayloads = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/hooks/useLazyFindOneRecord', () => ({
|
||||
useLazyFindOneRecord: () => ({
|
||||
@@ -16,6 +18,20 @@ jest.mock('@/object-record/hooks/useLazyFindOneRecord', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
|
||||
useSnackBar: () => ({
|
||||
enqueueWarningSnackBar: mockEnqueueWarningSnackBar,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads',
|
||||
() => ({
|
||||
buildTriggerWorkflowVersionPayloads: (...args: unknown[]) =>
|
||||
mockBuildTriggerWorkflowVersionPayloads(...args),
|
||||
}),
|
||||
);
|
||||
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
@@ -42,7 +58,7 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return enriched context with workflow metadata', async () => {
|
||||
it('should return enriched context API with workflow info and payloads', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
@@ -59,6 +75,9 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
},
|
||||
);
|
||||
|
||||
const expectedPayloads = [{ recordId: 'rec-1' }];
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue(expectedPayloads);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
@@ -75,8 +94,7 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
{
|
||||
headlessEngineCommandContextApi,
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: 'obj-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -85,9 +103,7 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
...headlessEngineCommandContextApi,
|
||||
workflowId: 'workflow-1',
|
||||
workflowVersionId: 'wf-version-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: 'obj-1',
|
||||
payloads: expectedPayloads,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,4 +134,99 @@ describe('useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformatio
|
||||
|
||||
expect(enrichedResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for RECORD_SELECTION type when payloads are empty', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const workflowVersionRecord = {
|
||||
id: 'wf-version-1',
|
||||
workflowId: 'workflow-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
__typename: 'WorkflowVersion' as const,
|
||||
};
|
||||
|
||||
mockFindOneWorkflowVersion.mockImplementation(
|
||||
async ({ onCompleted }: { onCompleted: (data: unknown) => void }) => {
|
||||
onCompleted(workflowVersionRecord);
|
||||
},
|
||||
);
|
||||
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue([]);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
let enrichedResult: unknown;
|
||||
|
||||
await act(async () => {
|
||||
enrichedResult =
|
||||
await result.current.enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
|
||||
{
|
||||
headlessEngineCommandContextApi: buildBaseContextApi(),
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(enrichedResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should show warning snackbar when selected records exceed QUERY_MAX_RECORDS', async () => {
|
||||
const store = createStore();
|
||||
const wrapper = getWrapper(store);
|
||||
|
||||
const workflowVersionRecord = {
|
||||
id: 'wf-version-1',
|
||||
workflowId: 'workflow-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
__typename: 'WorkflowVersion' as const,
|
||||
};
|
||||
|
||||
mockFindOneWorkflowVersion.mockImplementation(
|
||||
async ({ onCompleted }: { onCompleted: (data: unknown) => void }) => {
|
||||
onCompleted(workflowVersionRecord);
|
||||
},
|
||||
);
|
||||
|
||||
mockBuildTriggerWorkflowVersionPayloads.mockReturnValue([
|
||||
{ recordId: 'rec-1' },
|
||||
]);
|
||||
|
||||
const selectedRecordIds = Array.from({ length: 201 }, (_, index) =>
|
||||
String(index),
|
||||
);
|
||||
|
||||
const headlessEngineCommandContextApi = buildBaseContextApi({
|
||||
targetedRecordsRule: { mode: 'selection', selectedRecordIds },
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.enrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation(
|
||||
{
|
||||
headlessEngineCommandContextApi,
|
||||
workflowVersionId: 'wf-version-1',
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockEnqueueWarningSnackBar).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-selection-limit',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -84,8 +84,7 @@ describe('useMountCommand', () => {
|
||||
...baseContextApi,
|
||||
workflowId: 'workflow-1',
|
||||
workflowVersionId: 'wf-version-1',
|
||||
trigger: { type: 'MANUAL' },
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
payloads: [{ recordId: 'rec-1' }],
|
||||
};
|
||||
mockEnrichFn.mockResolvedValue(enrichedState);
|
||||
|
||||
|
||||
+73
-8
@@ -4,18 +4,27 @@ import {
|
||||
type HeadlessCommandContextApi,
|
||||
type HeadlessEngineCommandContextApi,
|
||||
} from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
|
||||
import { buildTriggerWorkflowVersionPayloads } from '@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CommandMenuItemAvailabilityType } from '~/generated-metadata/graphql';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType as CommandMenuItemAvailabilityTypeEnum,
|
||||
type CommandMenuItemAvailabilityType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type WorkflowVersionRecord = Pick<
|
||||
WorkflowVersion,
|
||||
'id' | 'workflowId' | 'trigger' | '__typename'
|
||||
>;
|
||||
|
||||
type EnrichParams = {
|
||||
type BuildTriggerWorkflowVersionCommandStateParams = {
|
||||
headlessEngineCommandContextApi: HeadlessEngineCommandContextApi;
|
||||
workflowVersionId: string;
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
@@ -24,6 +33,9 @@ type EnrichParams = {
|
||||
|
||||
export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInformation =
|
||||
() => {
|
||||
const store = useStore();
|
||||
const { enqueueWarningSnackBar } = useSnackBar();
|
||||
|
||||
const { findOneRecord: findOneWorkflowVersion } =
|
||||
useLazyFindOneRecord<WorkflowVersionRecord>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
@@ -53,23 +65,76 @@ export const useEnrichHeadlessCommandContextApiWithWorkflowVersionTriggerInforma
|
||||
workflowVersionId,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
}: EnrichParams): Promise<HeadlessCommandContextApi | undefined> => {
|
||||
}: BuildTriggerWorkflowVersionCommandStateParams): Promise<
|
||||
HeadlessCommandContextApi | undefined
|
||||
> => {
|
||||
const workflowVersion = await fetchWorkflowVersion(workflowVersionId);
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
headlessEngineCommandContextApi.targetedRecordsRule.mode ===
|
||||
'exclusion'
|
||||
) {
|
||||
enqueueWarningSnackBar({
|
||||
message: t`Running workflows on all records is not yet supported. Please select records manually.`,
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-select-all-not-supported',
|
||||
},
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedRecordIds =
|
||||
headlessEngineCommandContextApi.targetedRecordsRule
|
||||
.selectedRecordIds;
|
||||
|
||||
if (selectedRecordIds.length > QUERY_MAX_RECORDS) {
|
||||
const selectedCountFormatted =
|
||||
selectedRecordIds.length.toLocaleString();
|
||||
|
||||
const limitFormatted = QUERY_MAX_RECORDS.toLocaleString();
|
||||
|
||||
enqueueWarningSnackBar({
|
||||
message: t`You selected ${selectedCountFormatted} records but manual triggers can run on at most ${limitFormatted} records at once. Only the first ${limitFormatted} records will be processed.`,
|
||||
options: {
|
||||
dedupeKey: 'workflow-manual-trigger-selection-limit',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const objectMetadataItems = store.get(
|
||||
objectMetadataItemsSelector.atom,
|
||||
);
|
||||
|
||||
const payloads = buildTriggerWorkflowVersionPayloads({
|
||||
store,
|
||||
trigger: workflowVersion.trigger,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
selectedRecordIds,
|
||||
});
|
||||
|
||||
if (
|
||||
availabilityType ===
|
||||
CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION &&
|
||||
!isNonEmptyArray(payloads)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...headlessEngineCommandContextApi,
|
||||
workflowId: workflowVersion.workflowId,
|
||||
workflowVersionId: workflowVersion.id,
|
||||
trigger: workflowVersion.trigger,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
payloads,
|
||||
};
|
||||
},
|
||||
[fetchWorkflowVersion],
|
||||
[store, fetchWorkflowVersion, enqueueWarningSnackBar],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+7
-47
@@ -1,63 +1,23 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { buildTriggerWorkflowVersionPayloads } from '@/command-menu-item/engine-command/utils/buildTriggerWorkflowVersionPayloads';
|
||||
import { isHeadlessTriggerWorkflowVersionCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessTriggerWorkflowVersionCommandContextApi';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
type RecordGqlOperationFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import { isNonEmptyArray } from 'twenty-shared/utils';
|
||||
|
||||
export const TriggerWorkflowVersionEngineCommand = () => {
|
||||
const mountedCommandState = useHeadlessCommandContextApi();
|
||||
const store = useStore();
|
||||
|
||||
if (!isHeadlessTriggerWorkflowVersionCommandContextApi(mountedCommandState)) {
|
||||
throw new Error(
|
||||
'TriggerWorkflowVersionEngineCommand requires a workflow trigger context',
|
||||
);
|
||||
}
|
||||
|
||||
const noMatchFilter: RecordGqlOperationFilter = { id: { in: [] } };
|
||||
|
||||
const { fetchAllRecords } = useLazyFetchAllRecords({
|
||||
objectNameSingular:
|
||||
mountedCommandState.objectMetadataItem?.nameSingular ??
|
||||
CoreObjectNameSingular.Person,
|
||||
filter: mountedCommandState.graphqlFilter ?? noMatchFilter,
|
||||
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||
});
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
let selectedRecords: ObjectRecord[];
|
||||
|
||||
if (mountedCommandState.targetedRecordsRule.mode === 'selection') {
|
||||
selectedRecords = mountedCommandState.selectedRecords;
|
||||
} else {
|
||||
selectedRecords = await fetchAllRecords();
|
||||
if (
|
||||
!isHeadlessTriggerWorkflowVersionCommandContextApi(mountedCommandState)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const payloads = buildTriggerWorkflowVersionPayloads({
|
||||
trigger: mountedCommandState.trigger,
|
||||
availabilityType: mountedCommandState.availabilityType,
|
||||
availabilityObjectMetadataId:
|
||||
mountedCommandState.availabilityObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
selectedRecords,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(payloads)) {
|
||||
if (!isNonEmptyArray(mountedCommandState.payloads)) {
|
||||
await runWorkflowVersion({
|
||||
workflowId: mountedCommandState.workflowId,
|
||||
workflowVersionId: mountedCommandState.workflowVersionId,
|
||||
@@ -66,14 +26,14 @@ export const TriggerWorkflowVersionEngineCommand = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const payload of payloads) {
|
||||
for (const payload of mountedCommandState.payloads) {
|
||||
await runWorkflowVersion({
|
||||
workflowId: mountedCommandState.workflowId,
|
||||
workflowVersionId: mountedCommandState.workflowVersionId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
}, [mountedCommandState, fetchAllRecords, runWorkflowVersion, store]);
|
||||
}, [runWorkflowVersion, mountedCommandState]);
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={execute} />;
|
||||
};
|
||||
|
||||
+1
-5
@@ -1,13 +1,11 @@
|
||||
import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
type Nullable,
|
||||
type RecordGqlOperationFilter,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
type CommandMenuItemAvailabilityType,
|
||||
type EngineComponentKey,
|
||||
type CommandMenuItemPayload,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -33,9 +31,7 @@ export type HeadlessTriggerWorkflowVersionCommandContextApi =
|
||||
HeadlessEngineCommandContextApi & {
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
trigger: WorkflowTrigger | null;
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
availabilityObjectMetadataId?: string | null;
|
||||
payloads: Record<string, any>[];
|
||||
};
|
||||
|
||||
export type HeadlessCommandContextApi =
|
||||
|
||||
+2
-6
@@ -1,9 +1,6 @@
|
||||
import { type HeadlessCommandContextApi } from '@/command-menu-item/engine-command/types/HeadlessCommandContextApi';
|
||||
import { isHeadlessTriggerWorkflowVersionCommandContextApi } from '@/command-menu-item/engine-command/utils/isHeadlessTriggerWorkflowVersionCommandContextApi';
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
EngineComponentKey,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { EngineComponentKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const baseContextApi: HeadlessCommandContextApi = {
|
||||
engineComponentKey: EngineComponentKey.CREATE_NEW_RECORD,
|
||||
@@ -23,8 +20,7 @@ describe('isHeadlessTriggerWorkflowVersionCommandContextApi', () => {
|
||||
...baseContextApi,
|
||||
workflowId: 'wf-1',
|
||||
workflowVersionId: 'wfv-1',
|
||||
trigger: null,
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
payloads: [],
|
||||
};
|
||||
|
||||
expect(
|
||||
|
||||
+29
-5
@@ -1,7 +1,10 @@
|
||||
import type { Store } from 'jotai/vanilla/store';
|
||||
|
||||
import { isBulkRecordsManualTrigger } from '@/command-menu-item/record/utils/isBulkRecordsManualTrigger';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type CommandMenuItemAvailabilityType,
|
||||
@@ -9,31 +12,44 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const buildTriggerWorkflowVersionPayloads = ({
|
||||
store,
|
||||
trigger,
|
||||
availabilityType,
|
||||
availabilityObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
selectedRecords,
|
||||
selectedRecordIds,
|
||||
}: {
|
||||
store: Store;
|
||||
trigger: WorkflowTrigger | null;
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
availabilityObjectMetadataId?: string | null;
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[];
|
||||
selectedRecords: ObjectRecord[];
|
||||
selectedRecordIds: string[];
|
||||
}): Record<string, any>[] => {
|
||||
const payloads: Record<string, any>[] = [];
|
||||
|
||||
switch (availabilityType) {
|
||||
case CommandMenuItemAvailabilityTypeEnum.RECORD_SELECTION: {
|
||||
if (selectedRecords.length === 0) {
|
||||
if (selectedRecordIds.length === 0) {
|
||||
return payloads;
|
||||
}
|
||||
|
||||
const limitedSelectedRecordIds = selectedRecordIds.slice(
|
||||
0,
|
||||
QUERY_MAX_RECORDS,
|
||||
);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(metadata) => metadata.id === availabilityObjectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(trigger) && isBulkRecordsManualTrigger(trigger)) {
|
||||
const selectedRecords = limitedSelectedRecordIds
|
||||
.map((recordId) =>
|
||||
store.get(recordStoreFamilyState.atomFamily(recordId)),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
payloads.push({
|
||||
[objectMetadataItem.namePlural]: selectedRecords,
|
||||
@@ -43,7 +59,15 @@ export const buildTriggerWorkflowVersionPayloads = ({
|
||||
return payloads;
|
||||
}
|
||||
|
||||
for (const selectedRecord of selectedRecords) {
|
||||
for (const selectedRecordId of limitedSelectedRecordIds) {
|
||||
const selectedRecord = store.get(
|
||||
recordStoreFamilyState.atomFamily(selectedRecordId),
|
||||
);
|
||||
|
||||
if (!isDefined(selectedRecord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
payloads.push(selectedRecord);
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -28,6 +28,5 @@ export const COMMAND_MENU_ITEM_FRAGMENT = gql`
|
||||
conditionalAvailabilityExpression
|
||||
availabilityType
|
||||
availabilityObjectMetadataId
|
||||
pageLayoutId
|
||||
}
|
||||
`;
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
|
||||
export const doesCommandMenuItemMatchPageLayoutId =
|
||||
(pageLayoutId: string | null) => (item: CommandMenuItemFieldsFragment) =>
|
||||
!isDefined(item.pageLayoutId) || item.pageLayoutId === pageLayoutId;
|
||||
+1
-7
@@ -18,13 +18,11 @@ import { FindOneFrontComponentDocument } from '~/generated-metadata/graphql';
|
||||
type FrontComponentRendererProps = {
|
||||
frontComponentId: string;
|
||||
commandMenuItemId?: string;
|
||||
recordId?: string;
|
||||
};
|
||||
|
||||
export const FrontComponentRenderer = ({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
recordId,
|
||||
}: FrontComponentRendererProps) => {
|
||||
const { colorScheme } = useContext(ThemeContext);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
@@ -35,11 +33,7 @@ export const FrontComponentRenderer = ({
|
||||
);
|
||||
|
||||
const { executionContext, frontComponentHostCommunicationApi } =
|
||||
useFrontComponentExecutionContext({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
recordId,
|
||||
});
|
||||
useFrontComponentExecutionContext({ frontComponentId, commandMenuItemId });
|
||||
|
||||
const handleError = useCallback(
|
||||
(error?: Error) => {
|
||||
|
||||
+13
-2
@@ -17,6 +17,9 @@ const mockCloseSidePanelMenu = jest.fn();
|
||||
const mockSetCommandMenuItemProgress = jest.fn();
|
||||
|
||||
let mockCurrentUser: { id: string } | null = { id: 'user-123' };
|
||||
let mockTargetRecordIdentifier: { id: string } | undefined = {
|
||||
id: 'record-456',
|
||||
};
|
||||
|
||||
jest.mock('~/hooks/useNavigateApp', () => ({
|
||||
useNavigateApp: () => mockNavigateApp,
|
||||
@@ -83,6 +86,12 @@ jest.mock('@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState', () => ({
|
||||
useSetAtomFamilyState: () => mockSetCommandMenuItemProgress,
|
||||
}));
|
||||
|
||||
jest.mock('@/ui/layout/contexts/LayoutRenderingContext', () => ({
|
||||
useLayoutRenderingContext: () => ({
|
||||
targetRecordIdentifier: mockTargetRecordIdentifier,
|
||||
}),
|
||||
}));
|
||||
|
||||
const FRONT_COMPONENT_ID = 'fc-test-id';
|
||||
const COMMAND_MENU_ITEM_ID = 'cmd-item-1';
|
||||
|
||||
@@ -90,6 +99,7 @@ describe('useFrontComponentExecutionContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCurrentUser = { id: 'user-123' };
|
||||
mockTargetRecordIdentifier = { id: 'record-456' };
|
||||
});
|
||||
|
||||
describe('executionContext', () => {
|
||||
@@ -97,7 +107,6 @@ describe('useFrontComponentExecutionContext', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFrontComponentExecutionContext({
|
||||
frontComponentId: FRONT_COMPONENT_ID,
|
||||
recordId: 'record-456',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -120,7 +129,9 @@ describe('useFrontComponentExecutionContext', () => {
|
||||
expect(result.current.executionContext.userId).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null recordId when no recordId provided', () => {
|
||||
it('should return null recordId when no target record', () => {
|
||||
mockTargetRecordIdentifier = undefined;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useFrontComponentExecutionContext({
|
||||
frontComponentId: FRONT_COMPONENT_ID,
|
||||
|
||||
+4
-3
@@ -14,6 +14,7 @@ import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
@@ -23,11 +24,9 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
export const useFrontComponentExecutionContext = ({
|
||||
frontComponentId,
|
||||
commandMenuItemId,
|
||||
recordId,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
commandMenuItemId?: string;
|
||||
recordId?: string;
|
||||
}): {
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
@@ -124,10 +123,12 @@ export const useFrontComponentExecutionContext = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { targetRecordIdentifier } = useLayoutRenderingContext();
|
||||
|
||||
const executionContext: FrontComponentExecutionContext = {
|
||||
frontComponentId,
|
||||
userId: currentUser?.id ?? null,
|
||||
recordId: recordId ?? null,
|
||||
recordId: targetRecordIdentifier?.id ?? null,
|
||||
};
|
||||
|
||||
const unmountFrontComponent: FrontComponentHostCommunicationApi['unmountFrontComponent'] =
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import gql from 'graphql-tag';
|
||||
export const MARKETPLACE_APP_FRAGMENT = gql`
|
||||
fragment MarketplaceAppFields on MarketplaceApp {
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
description
|
||||
icon
|
||||
|
||||
+8
-27
@@ -3,20 +3,14 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { ViewFieldsSearchDropdownSection } from '@/views/components/ViewFieldsSearchDropdownSection';
|
||||
import { ViewFieldsVisibleDropdownSection } from '@/views/components/ViewFieldsVisibleDropdownSection';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { IconChevronLeft, IconEyeOff } from 'twenty-ui/display';
|
||||
import { MenuItemNavigate } from 'twenty-ui/navigation';
|
||||
|
||||
export const ObjectOptionsDropdownFieldsContent = () => {
|
||||
const { t } = useLingui();
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
const { onContentChange, resetContent } = useObjectOptionsDropdown();
|
||||
|
||||
return (
|
||||
@@ -31,28 +25,15 @@ export const ObjectOptionsDropdownFieldsContent = () => {
|
||||
>
|
||||
{t`Fields`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInput}
|
||||
placeholder={t`Search fields`}
|
||||
onChange={(event) => setSearchInput(event.target.value)}
|
||||
/>
|
||||
<ViewFieldsVisibleDropdownSection />
|
||||
<DropdownMenuSeparator />
|
||||
{isNonEmptyString(searchInput) ? (
|
||||
<ViewFieldsSearchDropdownSection searchInput={searchInput} />
|
||||
) : (
|
||||
<>
|
||||
<ViewFieldsVisibleDropdownSection />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemNavigate
|
||||
onClick={() => onContentChange('hiddenFields')}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={t`Hidden Fields`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemNavigate
|
||||
onClick={() => onContentChange('hiddenFields')}
|
||||
LeftIcon={IconEyeOff}
|
||||
text={t`Hidden Fields`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,9 +95,7 @@ export const useSetIsPageLayoutInEditMode = (pageLayoutIdFromProps: string) => {
|
||||
|
||||
store.set(contextStoreIsFullTabWidgetInEditModeState, value);
|
||||
|
||||
if (value) {
|
||||
store.set(currentPageLayoutIdState.atom, pageLayoutId);
|
||||
}
|
||||
store.set(currentPageLayoutIdState.atom, value ? pageLayoutId : null);
|
||||
},
|
||||
[
|
||||
isDashboardInEditModeState,
|
||||
|
||||
+1
-6
@@ -7,7 +7,6 @@ import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutIn
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
|
||||
import { isWidgetConfigurationOfType } from '@/side-panel/pages/page-layout/utils/isWidgetConfigurationOfType';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
|
||||
const StyledContainer = styled.div<{ isInEditMode: boolean }>`
|
||||
height: 100%;
|
||||
@@ -30,7 +29,6 @@ export const FrontComponentWidgetRenderer = ({
|
||||
widget,
|
||||
}: FrontComponentWidgetRendererProps) => {
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
const { targetRecordIdentifier } = useLayoutRenderingContext();
|
||||
|
||||
const configuration = widget.configuration;
|
||||
|
||||
@@ -46,10 +44,7 @@ export const FrontComponentWidgetRenderer = ({
|
||||
return (
|
||||
<StyledContainer isInEditMode={isPageLayoutInEditMode}>
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={frontComponentId}
|
||||
recordId={targetRecordIdentifier?.id}
|
||||
/>
|
||||
<FrontComponentRenderer frontComponentId={frontComponentId} />
|
||||
</Suspense>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
+4
-1
@@ -102,7 +102,10 @@ export const SettingsAdminApps = () => {
|
||||
key={registration.id}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId: registration.id },
|
||||
{
|
||||
applicationUniversalIdentifier:
|
||||
registration.universalIdentifier,
|
||||
},
|
||||
)}
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import {
|
||||
IconDeviceFloppy,
|
||||
IconPencil,
|
||||
IconRefreshAlert,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
ConfigSource,
|
||||
type ConfigVariable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type ConfigVariableActionButtonsProps = {
|
||||
variable: ConfigVariable;
|
||||
isValueValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
onSave: () => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export const ConfigVariableActionButtons = ({
|
||||
variable,
|
||||
isValueValid,
|
||||
isSubmitting,
|
||||
onSave,
|
||||
onReset,
|
||||
}: ConfigVariableActionButtonsProps) => {
|
||||
const { t } = useLingui();
|
||||
const isConfigVariablesInDbEnabled = useAtomStateValue(
|
||||
isConfigVariablesInDbEnabledState,
|
||||
);
|
||||
const isFromDatabase = variable.source === ConfigSource.DATABASE;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isConfigVariablesInDbEnabled &&
|
||||
variable.source === ConfigSource.DATABASE && (
|
||||
<Button
|
||||
title={t`Reset to Default`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
accent="danger"
|
||||
disabled={isSubmitting}
|
||||
onClick={onReset}
|
||||
Icon={IconRefreshAlert}
|
||||
/>
|
||||
)}
|
||||
{isConfigVariablesInDbEnabled && !variable.isEnvOnly && (
|
||||
<Button
|
||||
title={isFromDatabase ? t`Save` : t`Edit`}
|
||||
variant="primary"
|
||||
size="small"
|
||||
accent="blue"
|
||||
disabled={isSubmitting || !isValueValid}
|
||||
onClick={onSave}
|
||||
type="submit"
|
||||
Icon={isFromDatabase ? IconDeviceFloppy : IconPencil}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconChevronRight } from 'twenty-ui/display';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type ConfigVariable } from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsAdminConfigVariablesRowProps = {
|
||||
variable: ConfigVariable;
|
||||
};
|
||||
|
||||
const StyledTableRowContainer = styled.div`
|
||||
> * {
|
||||
&:hover {
|
||||
background-color: ${themeCssVariables.background.transparent.light};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEllipsisLabel = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SettingsAdminConfigVariablesRow = ({
|
||||
variable,
|
||||
}: SettingsAdminConfigVariablesRowProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const displayValue =
|
||||
variable.value === ''
|
||||
? 'null'
|
||||
: variable.isSensitive
|
||||
? '••••••'
|
||||
: typeof variable.value === 'boolean'
|
||||
? variable.value
|
||||
? 'true'
|
||||
: 'false'
|
||||
: typeof variable.value === 'object' && variable.value !== null
|
||||
? JSON.stringify(variable.value)
|
||||
: variable.value;
|
||||
|
||||
return (
|
||||
<StyledTableRowContainer>
|
||||
<TableRow
|
||||
gridAutoColumns="5fr 3fr 1fr"
|
||||
to={getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
|
||||
variableName: variable.name,
|
||||
})}
|
||||
>
|
||||
<TableCell
|
||||
color={theme.font.color.primary}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</StyledTableRowContainer>
|
||||
);
|
||||
};
|
||||
+24
-31
@@ -1,16 +1,7 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsAdminConfigVariablesRow } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type ConfigVariable } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTableBodyContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
import { ConfigVariableTable } from '@/settings/config-variables/components/ConfigVariableTable';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
|
||||
type SettingsAdminConfigVariablesTableProps = {
|
||||
variables: ConfigVariable[];
|
||||
@@ -19,23 +10,25 @@ type SettingsAdminConfigVariablesTableProps = {
|
||||
export const SettingsAdminConfigVariablesTable = ({
|
||||
variables,
|
||||
}: SettingsAdminConfigVariablesTableProps) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns="5fr 3fr 1fr">
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Value`}</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{variables.map((variable) => (
|
||||
<SettingsAdminConfigVariablesRow
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBodyContainer>
|
||||
</Table>
|
||||
);
|
||||
const configVariables = variables.map((variable) => ({
|
||||
name: variable.name,
|
||||
description: variable.description,
|
||||
value:
|
||||
variable.value === ''
|
||||
? 'null'
|
||||
: variable.isSensitive
|
||||
? '••••••'
|
||||
: typeof variable.value === 'boolean'
|
||||
? variable.value
|
||||
? 'true'
|
||||
: 'false'
|
||||
: typeof variable.value === 'object' && variable.value !== null
|
||||
? JSON.stringify(variable.value)
|
||||
: variable.value,
|
||||
to: getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
|
||||
variableName: variable.name,
|
||||
}),
|
||||
}));
|
||||
|
||||
return <ConfigVariableTable configVariables={configVariables} />;
|
||||
};
|
||||
|
||||
+44
-69
@@ -1,8 +1,5 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { type ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
@@ -13,8 +10,6 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useConfigVariableActions = (variableName: string) => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
|
||||
const [updateDatabaseConfigVariable] = useMutation(
|
||||
@@ -31,65 +26,20 @@ export const useConfigVariableActions = (variableName: string) => {
|
||||
value: ConfigVariableValue,
|
||||
isFromDatabase: boolean,
|
||||
) => {
|
||||
try {
|
||||
if (
|
||||
value === null ||
|
||||
(typeof value === 'string' && value === '') ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
await handleDeleteVariable();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFromDatabase) {
|
||||
await updateDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
value,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
await createDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
value,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await refetchClientConfig();
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable updated successfully.`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to update variable`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteVariable = async (e?: React.MouseEvent<HTMLElement>) => {
|
||||
if (isDefined(e)) {
|
||||
e.preventDefault();
|
||||
if (
|
||||
value === null ||
|
||||
(typeof value === 'string' && value === '') ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
await handleDeleteVariable();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteDatabaseConfigVariable({
|
||||
if (isFromDatabase) {
|
||||
await updateDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
value,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
@@ -98,17 +48,42 @@ export const useConfigVariableActions = (variableName: string) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await refetchClientConfig();
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable deleted successfully.`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to remove override`,
|
||||
} else {
|
||||
await createDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
value,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
await refetchClientConfig();
|
||||
};
|
||||
|
||||
const handleDeleteVariable = async (e?: React.MouseEvent<HTMLElement>) => {
|
||||
if (isDefined(e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
await deleteDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await refetchClientConfig();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { type ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ConfigVariable } from '~/generated-metadata/graphql';
|
||||
|
||||
type FormValues = {
|
||||
value: ConfigVariableValue;
|
||||
};
|
||||
|
||||
const hasMeaningfulValue = (value: ConfigVariableValue): boolean => {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim() !== '';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const useConfigVariableForm = (variable?: ConfigVariable) => {
|
||||
const validationSchema = z.object({
|
||||
value: z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string(), z.unknown()),
|
||||
z.null(),
|
||||
]),
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty },
|
||||
watch,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(validationSchema),
|
||||
values: { value: variable?.value ?? null },
|
||||
});
|
||||
|
||||
const currentValue = watch('value');
|
||||
const isValueValid =
|
||||
variable !== undefined &&
|
||||
!variable.isEnvOnly &&
|
||||
isDirty &&
|
||||
hasMeaningfulValue(currentValue);
|
||||
|
||||
return {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
isSubmitting,
|
||||
currentValue,
|
||||
hasValueChanged: isDirty,
|
||||
isValueValid,
|
||||
};
|
||||
};
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { H3Title, IconCheck, IconPencil, IconX } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { ConfigVariableHelpText } from '@/settings/admin-panel/config-variables/components/ConfigVariableHelpText';
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
|
||||
const RESET_VARIABLE_MODAL_ID =
|
||||
'reset-application-registration-config-variable-modal';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
& > :not(:first-of-type) > button {
|
||||
border-left: none;
|
||||
}
|
||||
`;
|
||||
|
||||
type ConfigVariableEditProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
input: React.ReactNode;
|
||||
isEditing: boolean;
|
||||
setIsEditing: Dispatch<SetStateAction<boolean>>;
|
||||
isSaveDisabled?: boolean;
|
||||
canOpenCancelModal?: boolean;
|
||||
onSave?: () => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onEdit?: () => void;
|
||||
onConfirmReset?: () => Promise<void>;
|
||||
editDisabled?: boolean;
|
||||
helpContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ConfigVariableEdit = ({
|
||||
title,
|
||||
description,
|
||||
input,
|
||||
isEditing,
|
||||
setIsEditing,
|
||||
canOpenCancelModal,
|
||||
isSaveDisabled = false,
|
||||
onSave,
|
||||
onCancel,
|
||||
onEdit,
|
||||
onConfirmReset,
|
||||
editDisabled = false,
|
||||
helpContent,
|
||||
}: ConfigVariableEditProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onSave?.();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable ${title} updated`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error updating variable`,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmReset = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onConfirmReset?.();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable ${title} reset`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error resetting variable`,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (canOpenCancelModal) {
|
||||
openModal(RESET_VARIABLE_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
onCancel?.();
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit?.();
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H3Title title={title} description={description} />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<StyledRow>
|
||||
{input}
|
||||
{!isEditing ? (
|
||||
<Button
|
||||
Icon={IconPencil}
|
||||
variant="primary"
|
||||
onClick={handleEdit}
|
||||
type="button"
|
||||
disabled={editDisabled}
|
||||
/>
|
||||
) : (
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconCheck}
|
||||
variant="secondary"
|
||||
position="left"
|
||||
type={'button'}
|
||||
onClick={handleSave}
|
||||
disabled={isSaveDisabled || isSubmitting}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconX}
|
||||
variant="secondary"
|
||||
position="right"
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_VARIABLE_MODAL_ID}
|
||||
title={t`Reset variable`}
|
||||
subtitle={t`Are you sure you want to reset this variable?`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
</StyledRow>
|
||||
{helpContent}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
);
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { styled } from '@linaria/react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import {
|
||||
IconChevronRight,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const StyledTableBodyContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
const GRID_AUTO_COLUMNS = '5fr 3fr 3fr 1fr';
|
||||
|
||||
type ConfigVariable = {
|
||||
name: string;
|
||||
description?: string;
|
||||
value?: string | React.ReactNode;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type ConfigVariableTableProps = { configVariables: ConfigVariable[] };
|
||||
|
||||
export const ConfigVariableTable = ({
|
||||
configVariables,
|
||||
}: ConfigVariableTableProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns={GRID_AUTO_COLUMNS}>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Description`}</TableHeader>
|
||||
<TableHeader align="right">{t`Value`}</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{configVariables.map((variable, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
gridAutoColumns={GRID_AUTO_COLUMNS}
|
||||
to={variable.to}
|
||||
>
|
||||
<TableCell
|
||||
color={theme.font.color.primary}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<OverflowingTextWithTooltip text={variable.name} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={theme.font.color.secondary}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<OverflowingTextWithTooltip text={variable.description} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={theme.font.color.secondary}
|
||||
align="right"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
{typeof variable.value === 'string' ? (
|
||||
<OverflowingTextWithTooltip text={variable.value} />
|
||||
) : (
|
||||
variable.value
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" color={theme.font.color.secondary}>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBodyContainer>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { LinkChip } from 'twenty-ui/components';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
|
||||
const { applicationId = '' } = useParams<{ applicationId: string }>();
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Environment Variables`}
|
||||
description={t`Accessible in your function via process.env.KEY`}
|
||||
/>
|
||||
<Trans>
|
||||
Environment variables are defined at application level for all
|
||||
functions. Please check{' '}
|
||||
<LinkChip
|
||||
label={t`application detail page`}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'settings',
|
||||
)}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+1
-8
@@ -1,5 +1,4 @@
|
||||
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
|
||||
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
|
||||
export const SettingsLogicFunctionSettingsTab = ({
|
||||
@@ -12,12 +11,6 @@ export const SettingsLogicFunctionSettingsTab = ({
|
||||
) => (value: LogicFunctionFormValues[TKey]) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsLogicFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
<SettingsLogicFunctionNewForm formValues={formValues} onChange={onChange} />
|
||||
);
|
||||
};
|
||||
|
||||
+19
-6
@@ -2,8 +2,10 @@ import { Suspense, lazy } from 'react';
|
||||
|
||||
import { viewableFrontComponentIdComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentIdComponentState';
|
||||
import { viewableFrontComponentRecordContextComponentState } from '@/side-panel/pages/front-component/states/viewableFrontComponentRecordContextComponentState';
|
||||
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const FrontComponentRenderer = lazy(() =>
|
||||
import('@/front-components/components/FrontComponentRenderer').then(
|
||||
@@ -25,11 +27,22 @@ export const SidePanelFrontComponentPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={viewableFrontComponentId}
|
||||
recordId={viewableFrontComponentRecordContext?.recordId}
|
||||
/>
|
||||
</Suspense>
|
||||
<LayoutRenderingProvider
|
||||
value={{
|
||||
targetRecordIdentifier: isDefined(viewableFrontComponentRecordContext)
|
||||
? {
|
||||
id: viewableFrontComponentRecordContext.recordId,
|
||||
targetObjectNameSingular:
|
||||
viewableFrontComponentRecordContext.objectNameSingular,
|
||||
}
|
||||
: undefined,
|
||||
layoutType: PageLayoutType.DASHBOARD,
|
||||
isInSidePanel: true,
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<FrontComponentRenderer frontComponentId={viewableFrontComponentId} />
|
||||
</Suspense>
|
||||
</LayoutRenderingProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user