Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
291b356a8c | ||
|
|
413d1124bb | ||
|
|
ab5fb1f658 | ||
|
|
e4e7137660 | ||
|
|
7fb8cc1c39 | ||
|
|
922bb91f7a | ||
|
|
dfe4b956b9 | ||
|
|
1501b7f7a4 | ||
|
|
82d2a7f4fa | ||
|
|
32212feaa3 | ||
|
|
4eeb26be9b |
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const INSTALL_APPLICATION = gql`
|
||||
mutation InstallApplication($appRegistrationId: String!, $version: String) {
|
||||
installApplication(appRegistrationId: $appRegistrationId, version: $version)
|
||||
}
|
||||
`;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPLOAD_APP_TARBALL = gql`
|
||||
mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) {
|
||||
uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { UPLOAD_APP_TARBALL } from '~/modules/marketplace/graphql/mutations/uploadAppTarball';
|
||||
|
||||
type UploadResult =
|
||||
| {
|
||||
success: true;
|
||||
registrationId: string;
|
||||
universalIdentifier: string;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
};
|
||||
|
||||
export const useUploadAppTarball = () => {
|
||||
const [uploadAppTarball] = useMutation(UPLOAD_APP_TARBALL);
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const upload = async (file: File): Promise<UploadResult> => {
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
const result = await uploadAppTarball({
|
||||
variables: { file },
|
||||
});
|
||||
|
||||
const registration = result.data?.uploadAppTarball;
|
||||
|
||||
if (
|
||||
!isDefined(registration?.id) ||
|
||||
!isDefined(registration?.universalIdentifier)
|
||||
) {
|
||||
enqueueErrorSnackBar({ message: t`Upload failed.` });
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
registrationId: registration.id,
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
};
|
||||
} catch (error) {
|
||||
const graphqlMessage = error instanceof Error ? error.message : undefined;
|
||||
|
||||
enqueueErrorSnackBar({
|
||||
message: graphqlMessage ?? t`Failed to upload tarball.`,
|
||||
});
|
||||
|
||||
return { success: false };
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { upload, isUploading };
|
||||
};
|
||||
+61
-28
@@ -1,5 +1,7 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
IconColumnInsertRight,
|
||||
IconLink,
|
||||
@@ -11,10 +13,10 @@ import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
import {
|
||||
type NavigationMenuItemClickParams,
|
||||
useWorkspaceSectionItems,
|
||||
@@ -30,13 +32,13 @@ import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/comp
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useStore } from 'jotai';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledRightIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -46,6 +48,7 @@ const StyledRightIconsContainer = styled.div`
|
||||
|
||||
export const WorkspaceNavigationMenuItems = () => {
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const store = useStore();
|
||||
const enterEditMode = () => {
|
||||
const prefetchNavigationMenuItems = store.get(
|
||||
@@ -70,6 +73,7 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
const setOpenNavigationMenuItemFolderIds = useSetAtomState(
|
||||
openNavigationMenuItemFolderIdsState,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
@@ -83,36 +87,65 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
enterEditMode();
|
||||
};
|
||||
|
||||
const openFolderAndNavigateToFirstChild = (
|
||||
folderId: string,
|
||||
item: NavigationMenuItemClickParams['item'],
|
||||
) => {
|
||||
setOpenNavigationMenuItemFolderIds((current) =>
|
||||
current.includes(folderId) ? current : [...current, folderId],
|
||||
);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
|
||||
});
|
||||
const firstChild = workspaceNavigationMenuItemsSorted.find(
|
||||
(navItem) =>
|
||||
navItem.folderId === folderId &&
|
||||
navItem.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(navItem.link),
|
||||
);
|
||||
if (firstChild?.link) {
|
||||
navigate(firstChild.link);
|
||||
}
|
||||
};
|
||||
|
||||
const openViewOrRecordEditPanelAndNavigate = (
|
||||
item: NavigationMenuItemClickParams['item'],
|
||||
objectMetadataItem: ObjectMetadataItem | null | undefined,
|
||||
) => {
|
||||
if (objectMetadataItem) {
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle:
|
||||
item.itemType === NavigationMenuItemType.VIEW
|
||||
? item.labelIdentifier
|
||||
: objectMetadataItem.labelSingular,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
}
|
||||
const link = 'link' in item ? item.link : undefined;
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigationMenuItemClick = (
|
||||
params: NavigationMenuItemClickParams,
|
||||
) => {
|
||||
const { item, objectMetadataItem } = params;
|
||||
const id = item.id;
|
||||
setSelectedNavigationMenuItemInEditMode(id);
|
||||
if (item.itemType === NavigationMenuItemType.FOLDER) {
|
||||
setOpenNavigationMenuItemFolderIds((currentOpenFolders) =>
|
||||
currentOpenFolders.includes(id)
|
||||
? currentOpenFolders
|
||||
: [...currentOpenFolders, id],
|
||||
);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
|
||||
});
|
||||
} else if (item.itemType === NavigationMenuItemType.LINK) {
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
});
|
||||
} else if (isDefined(objectMetadataItem)) {
|
||||
const pageTitle =
|
||||
item.itemType === NavigationMenuItemType.VIEW
|
||||
? item.labelIdentifier
|
||||
: objectMetadataItem.labelSingular;
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
setSelectedNavigationMenuItemInEditMode(item.id);
|
||||
|
||||
switch (item.itemType) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
openFolderAndNavigateToFirstChild(item.id, item);
|
||||
break;
|
||||
case NavigationMenuItemType.LINK:
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
openViewOrRecordEditPanelAndNavigate(item, objectMetadataItem);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+29
-10
@@ -1,4 +1,5 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
@@ -11,21 +12,15 @@ import {
|
||||
matchesWorkspaceFolderId,
|
||||
validateAndExtractWorkspaceFolderId,
|
||||
} from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
|
||||
export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
@@ -47,7 +42,9 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
};
|
||||
|
||||
const handleWorkspaceNavigationMenuItemDragAndDrop: OnDragEndResponder = (
|
||||
result,
|
||||
result: Parameters<OnDragEndResponder>[0] & {
|
||||
insertBeforeItemId?: string | null;
|
||||
},
|
||||
) => {
|
||||
const { destination, source, draggableId } = result;
|
||||
|
||||
@@ -70,6 +67,12 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const navigationMenuItemsDraft = store.get(
|
||||
navigationMenuItemsDraftState.atom,
|
||||
);
|
||||
const isNavigationMenuInEditMode = store.get(
|
||||
isNavigationMenuInEditModeState.atom,
|
||||
);
|
||||
if (!isNavigationMenuInEditMode || !navigationMenuItemsDraft) {
|
||||
return;
|
||||
}
|
||||
@@ -122,8 +125,24 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
const listWithoutDragged = sourceList.filter(
|
||||
(item) => item.id !== draggableId,
|
||||
);
|
||||
const prevItem = listWithoutDragged[destination.index - 1];
|
||||
const nextItem = listWithoutDragged[destination.index];
|
||||
const sourceIndexInList = sourceList.findIndex(
|
||||
(item) => item.id === draggableId,
|
||||
);
|
||||
const insertBeforeIndex =
|
||||
result.insertBeforeItemId != null
|
||||
? sourceList.findIndex(
|
||||
(item) => item.id === result.insertBeforeItemId,
|
||||
)
|
||||
: -1;
|
||||
const destinationIndexInFullList =
|
||||
insertBeforeIndex >= 0 ? insertBeforeIndex : destination.index;
|
||||
const destIndexInListWithoutDragged =
|
||||
sourceIndexInList < destinationIndexInFullList &&
|
||||
destinationIndexInFullList <= listWithoutDragged.length
|
||||
? destinationIndexInFullList - 1
|
||||
: destinationIndexInFullList;
|
||||
const prevItem = listWithoutDragged[destIndexInListWithoutDragged - 1];
|
||||
const nextItem = listWithoutDragged[destIndexInListWithoutDragged];
|
||||
const newPosition = getPositionBetween(
|
||||
prevItem?.position,
|
||||
nextItem?.position,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type DragDropProvider } from '@dnd-kit/react';
|
||||
import { isSortable } from '@dnd-kit/react/sortable';
|
||||
import type { ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
@@ -96,6 +96,7 @@ export const useWorkspaceDndKit = (): {
|
||||
id: string,
|
||||
source: DropDestination,
|
||||
destination: DropDestination,
|
||||
insertBeforeItemId?: string | null,
|
||||
) => {
|
||||
const draggedItem = getNavItemById(id);
|
||||
const destFolderId = validateAndExtractWorkspaceFolderId(
|
||||
@@ -118,7 +119,11 @@ export const useWorkspaceDndKit = (): {
|
||||
);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(
|
||||
{ ...result, ...DROP_RESULT_OPTIONS },
|
||||
{
|
||||
...result,
|
||||
...DROP_RESULT_OPTIONS,
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
},
|
||||
provided,
|
||||
);
|
||||
};
|
||||
@@ -284,10 +289,14 @@ export const useWorkspaceDndKit = (): {
|
||||
isWorkspaceDroppableId(initialGroupStr) &&
|
||||
isWorkspaceDroppableId(destGroup);
|
||||
if (bothWorkspace) {
|
||||
const insertBeforeItemId = resolved.isTargetFolder
|
||||
? null
|
||||
: String(target?.id ?? '');
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{ droppableId: initialGroupStr, index: initialIndex },
|
||||
resolved.destination,
|
||||
insertBeforeItemId || undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
+38
-28
@@ -26,6 +26,8 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const LazyWorkspaceSectionListDndKit = lazy(() =>
|
||||
import(
|
||||
@@ -33,6 +35,10 @@ const LazyWorkspaceSectionListDndKit = lazy(() =>
|
||||
).then((m) => ({ default: m.WorkspaceSectionListDndKit })),
|
||||
);
|
||||
|
||||
const StyledWorkspaceSectionContentGapOffset = styled.div`
|
||||
margin-top: calc(-1 * ${themeCssVariables.betweenSiblingsGap});
|
||||
`;
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsProps = {
|
||||
sectionTitle: string;
|
||||
items: FlatWorkspaceItem[];
|
||||
@@ -150,44 +156,48 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={
|
||||
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
|
||||
}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceSectionListEditModeFallback
|
||||
<StyledWorkspaceSectionContentGapOffset>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={
|
||||
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
|
||||
}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceSectionListEditModeFallback
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={
|
||||
onActiveObjectMetadataItemClick
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceSectionListDndKit
|
||||
filteredItems={filteredItems}
|
||||
getEditModeProps={getEditModeProps}
|
||||
folderChildrenById={folderChildrenById}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={
|
||||
onActiveObjectMetadataItemClick
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceSectionListDndKit
|
||||
</Suspense>
|
||||
) : (
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
getEditModeProps={getEditModeProps}
|
||||
folderChildrenById={folderChildrenById}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledWorkspaceSectionContentGapOffset>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -3,7 +3,6 @@ import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/W
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -11,6 +10,7 @@ import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
|
||||
import { WorkspaceOrphanDropTarget } from '@/object-metadata/components/WorkspaceOrphanDropTarget';
|
||||
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
|
||||
@@ -20,6 +20,7 @@ const StyledList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
padding-top: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
const StyledListItemRow = styled.div`
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ const StyledList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.betweenSiblingsGap};
|
||||
padding-top: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick<
|
||||
|
||||
+1
-4
@@ -167,11 +167,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
... on FieldsConfiguration {
|
||||
configurationType
|
||||
viewId
|
||||
newFieldDefaultVisibility
|
||||
shouldAllowUserToSeeHiddenFields
|
||||
newFieldDefaultConfiguration {
|
||||
isVisible
|
||||
viewFieldGroupId
|
||||
}
|
||||
}
|
||||
... on FilesConfiguration {
|
||||
configurationType
|
||||
|
||||
+1
-41
@@ -11,18 +11,15 @@ import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fiel
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { FieldsConfigurationGroupEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor';
|
||||
import { FieldsConfigurationUngroupedEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor';
|
||||
import { NEW_FIELDS_INDICATOR_DRAGGABLE_ID } from '@/page-layout/widgets/fields/constants/NewFieldsIndicatorDraggableId';
|
||||
import { useCreateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup';
|
||||
import { useDeleteFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup';
|
||||
import { useFieldsWidgetEditorMode } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetEditorMode';
|
||||
import { useGetNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultConfiguration';
|
||||
import { useMoveFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveFieldInDraft';
|
||||
import { useMoveUngroupedFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveUngroupedFieldInDraft';
|
||||
import { useReorderFieldsWidgetEditorGroups } from '@/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups';
|
||||
import { useToggleFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft';
|
||||
import { useToggleUngroupedFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleUngroupedFieldVisibilityInDraft';
|
||||
import { useUpdateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup';
|
||||
import { useUpdateNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useUpdateNewFieldDefaultConfiguration';
|
||||
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
@@ -107,17 +104,6 @@ export const FieldsConfigurationEditor = ({
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { newFieldDefaultConfiguration } = useGetNewFieldDefaultConfiguration({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { updateNewFieldDefaultConfiguration } =
|
||||
useUpdateNewFieldDefaultConfiguration({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
const [renamingGroupValue, setRenamingGroupValue] = useState('');
|
||||
@@ -141,7 +127,7 @@ export const FieldsConfigurationEditor = ({
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
const { source, destination, type, draggableId } = result;
|
||||
const { source, destination, type } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
@@ -154,17 +140,6 @@ export const FieldsConfigurationEditor = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (draggableId === NEW_FIELDS_INDICATOR_DRAGGABLE_ID) {
|
||||
const cleanDestinationGroupId = destination.droppableId.replace(
|
||||
'group-',
|
||||
'',
|
||||
);
|
||||
updateNewFieldDefaultConfiguration({
|
||||
viewFieldGroupId: cleanDestinationGroupId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'GROUP') {
|
||||
handleGroupReorder(source.index, destination.index);
|
||||
} else if (type === 'FIELD') {
|
||||
@@ -227,12 +202,6 @@ export const FieldsConfigurationEditor = ({
|
||||
onMoveField={moveUngroupedField}
|
||||
onToggleFieldVisibility={toggleUngroupedFieldVisibility}
|
||||
onAddGroup={() => handleAddGroup({})}
|
||||
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
|
||||
onToggleNewFieldsVisibility={() =>
|
||||
updateNewFieldDefaultConfiguration({
|
||||
isVisible: !newFieldDefaultConfiguration.isVisible,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -276,15 +245,6 @@ export const FieldsConfigurationEditor = ({
|
||||
renamingGroupValue={renamingGroupValue}
|
||||
onRenamingGroupValueChange={setRenamingGroupValue}
|
||||
onStartRename={handleStartRename}
|
||||
showNewFieldsItem={
|
||||
group.id === newFieldDefaultConfiguration.viewFieldGroupId
|
||||
}
|
||||
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
|
||||
onToggleNewFieldsVisibility={() =>
|
||||
updateNewFieldDefaultConfiguration({
|
||||
isVisible: !newFieldDefaultConfiguration.isVisible,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
+2
-41
@@ -8,7 +8,6 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
|
||||
import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
|
||||
import { FieldsConfigurationGroupDropdown } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown';
|
||||
import { FieldsConfigurationGroupRenameInput } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupRenameInput';
|
||||
import { NEW_FIELDS_INDICATOR_DRAGGABLE_ID } from '@/page-layout/widgets/fields/constants/NewFieldsIndicatorDraggableId';
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@@ -16,13 +15,8 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import {
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconNewSection,
|
||||
IconPlaylistAdd,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
import { IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
import { FieldsConfigurationGroupDraggableHeader } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDraggableHeader';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -89,9 +83,6 @@ type FieldsConfigurationGroupEditorProps = {
|
||||
renamingGroupValue: string;
|
||||
onRenamingGroupValueChange: (value: string) => void;
|
||||
onStartRename: (params: { groupId: string; groupName: string }) => void;
|
||||
showNewFieldsItem: boolean;
|
||||
newFieldsIsVisible: boolean;
|
||||
onToggleNewFieldsVisibility: () => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationGroupEditor = ({
|
||||
@@ -105,9 +96,6 @@ export const FieldsConfigurationGroupEditor = ({
|
||||
renamingGroupValue,
|
||||
onRenamingGroupValueChange,
|
||||
onStartRename,
|
||||
showNewFieldsItem,
|
||||
newFieldsIsVisible,
|
||||
onToggleNewFieldsVisibility,
|
||||
}: FieldsConfigurationGroupEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -223,33 +211,6 @@ export const FieldsConfigurationGroupEditor = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showNewFieldsItem && (
|
||||
<DraggableItem
|
||||
key={NEW_FIELDS_INDICATOR_DRAGGABLE_ID}
|
||||
draggableId={NEW_FIELDS_INDICATOR_DRAGGABLE_ID}
|
||||
index={sortedFields.length}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconPlaylistAdd}
|
||||
text={t`New fields`}
|
||||
contextualText={t`Default position/visibility for fields created in the future`}
|
||||
gripMode="onHover"
|
||||
withIconContainer
|
||||
isIconDisplayedOnHoverOnly={false}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: newFieldsIsVisible ? IconEye : IconEyeOff,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onToggleNewFieldsVisibility();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{droppableProvided.placeholder}
|
||||
</StyledFieldsDroppable>
|
||||
)}
|
||||
|
||||
+2
-30
@@ -6,13 +6,8 @@ import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/com
|
||||
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconNewSection,
|
||||
IconPlaylistAdd,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
import { IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledFieldsDroppable = styled.div`
|
||||
display: flex;
|
||||
@@ -25,8 +20,6 @@ type FieldsConfigurationUngroupedEditorProps = {
|
||||
onMoveField: (sourceIndex: number, destinationIndex: number) => void;
|
||||
onToggleFieldVisibility: (fieldMetadataId: string) => void;
|
||||
onAddGroup: () => void;
|
||||
newFieldsIsVisible: boolean;
|
||||
onToggleNewFieldsVisibility: () => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationUngroupedEditor = ({
|
||||
@@ -34,8 +27,6 @@ export const FieldsConfigurationUngroupedEditor = ({
|
||||
onMoveField,
|
||||
onToggleFieldVisibility,
|
||||
onAddGroup,
|
||||
newFieldsIsVisible,
|
||||
onToggleNewFieldsVisibility,
|
||||
}: FieldsConfigurationUngroupedEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -89,25 +80,6 @@ export const FieldsConfigurationUngroupedEditor = ({
|
||||
))}
|
||||
{provided.placeholder}
|
||||
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconPlaylistAdd}
|
||||
text={t`New fields`}
|
||||
contextualText={t`Default position/visibility for fields created in the future`}
|
||||
gripMode="never"
|
||||
isDragDisabled
|
||||
withIconContainer
|
||||
isIconDisplayedOnHoverOnly={false}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: newFieldsIsVisible ? IconEye : IconEyeOff,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onToggleNewFieldsVisibility();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
LeftIcon={IconNewSection}
|
||||
withIconContainer
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const NEW_FIELDS_INDICATOR_DRAGGABLE_ID = 'new-fields-indicator';
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseGetNewFieldDefaultConfigurationParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useGetNewFieldDefaultConfiguration = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseGetNewFieldDefaultConfigurationParams) => {
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const draftGroups = fieldsWidgetGroupsDraft[widgetId] ?? [];
|
||||
|
||||
const widget = pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((w) => w.id === widgetId);
|
||||
|
||||
const fieldsConfiguration =
|
||||
isDefined(widget?.configuration) &&
|
||||
widget.configuration.configurationType === WidgetConfigurationType.FIELDS
|
||||
? (widget.configuration as FieldsConfiguration)
|
||||
: null;
|
||||
|
||||
const lastGroupId = getLastGroupId(draftGroups);
|
||||
|
||||
const persisted = fieldsConfiguration?.newFieldDefaultConfiguration;
|
||||
|
||||
const newFieldDefaultConfiguration = {
|
||||
isVisible: persisted?.isVisible ?? true,
|
||||
viewFieldGroupId: persisted?.viewFieldGroupId ?? lastGroupId,
|
||||
};
|
||||
|
||||
return { newFieldDefaultConfiguration, fieldsConfiguration };
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseGetNewFieldDefaultVisibilityParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useGetNewFieldDefaultVisibility = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseGetNewFieldDefaultVisibilityParams) => {
|
||||
const pageLayoutDraft = useAtomComponentStateValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const widget = pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((w) => w.id === widgetId);
|
||||
|
||||
const fieldsConfiguration =
|
||||
isDefined(widget?.configuration) &&
|
||||
widget.configuration.configurationType === WidgetConfigurationType.FIELDS
|
||||
? (widget.configuration as FieldsConfiguration)
|
||||
: null;
|
||||
|
||||
const newFieldDefaultVisibility =
|
||||
fieldsConfiguration?.newFieldDefaultVisibility ?? true;
|
||||
|
||||
return { newFieldDefaultVisibility, fieldsConfiguration };
|
||||
};
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useGetNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultConfiguration';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseUpdateNewFieldDefaultConfigurationParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useUpdateNewFieldDefaultConfiguration = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseUpdateNewFieldDefaultConfigurationParams) => {
|
||||
const { newFieldDefaultConfiguration, fieldsConfiguration } =
|
||||
useGetNewFieldDefaultConfiguration({ pageLayoutId, widgetId });
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const updateNewFieldDefaultConfiguration = useCallback(
|
||||
(updates: { isVisible?: boolean; viewFieldGroupId?: string | null }) => {
|
||||
if (!isDefined(fieldsConfiguration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageLayoutWidget(widgetId, {
|
||||
configuration: {
|
||||
...fieldsConfiguration,
|
||||
newFieldDefaultConfiguration: {
|
||||
...newFieldDefaultConfiguration,
|
||||
...updates,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
fieldsConfiguration,
|
||||
newFieldDefaultConfiguration,
|
||||
updatePageLayoutWidget,
|
||||
widgetId,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateNewFieldDefaultConfiguration };
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useGetNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultVisibility';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseUpdateNewFieldDefaultVisibilityParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const useUpdateNewFieldDefaultVisibility = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: UseUpdateNewFieldDefaultVisibilityParams) => {
|
||||
const { newFieldDefaultVisibility, fieldsConfiguration } =
|
||||
useGetNewFieldDefaultVisibility({ pageLayoutId, widgetId });
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
const updateNewFieldDefaultVisibility = useCallback(
|
||||
(isVisible: boolean) => {
|
||||
if (!isDefined(fieldsConfiguration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageLayoutWidget(widgetId, {
|
||||
configuration: {
|
||||
...fieldsConfiguration,
|
||||
newFieldDefaultVisibility: isVisible,
|
||||
},
|
||||
});
|
||||
},
|
||||
[fieldsConfiguration, updatePageLayoutWidget, widgetId],
|
||||
);
|
||||
|
||||
return { updateNewFieldDefaultVisibility, newFieldDefaultVisibility };
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
|
||||
|
||||
const makeGroup = (
|
||||
overrides: Partial<FieldsWidgetGroup> & { id: string },
|
||||
): FieldsWidgetGroup => ({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getLastGroupId', () => {
|
||||
it('should return null for empty array', () => {
|
||||
expect(getLastGroupId([])).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the id of a single group', () => {
|
||||
const groups = [makeGroup({ id: 'g1', position: 0 })];
|
||||
|
||||
expect(getLastGroupId(groups)).toBe('g1');
|
||||
});
|
||||
|
||||
it('should return the id of the group with the highest position', () => {
|
||||
const groups = [
|
||||
makeGroup({ id: 'g1', position: 0 }),
|
||||
makeGroup({ id: 'g2', position: 2 }),
|
||||
makeGroup({ id: 'g3', position: 1 }),
|
||||
];
|
||||
|
||||
expect(getLastGroupId(groups)).toBe('g2');
|
||||
});
|
||||
|
||||
it('should not mutate the original array', () => {
|
||||
const groups = [
|
||||
makeGroup({ id: 'g2', position: 2 }),
|
||||
makeGroup({ id: 'g1', position: 0 }),
|
||||
];
|
||||
|
||||
getLastGroupId(groups);
|
||||
|
||||
expect(groups[0].id).toBe('g2');
|
||||
expect(groups[1].id).toBe('g1');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
|
||||
export const getLastGroupId = (groups: FieldsWidgetGroup[]): string | null => {
|
||||
if (groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedGroups = [...groups].sort((a, b) => a.position - b.position);
|
||||
|
||||
return sortedGroups[sortedGroups.length - 1].id;
|
||||
};
|
||||
@@ -2,7 +2,11 @@ import { CommandMenuContextProvider } from '@/command-menu-item/contexts/Command
|
||||
import { SidePanelContainer } from '@/side-panel/components/SidePanelContainer';
|
||||
import { SidePanelTopBar } from '@/side-panel/components/SidePanelTopBar';
|
||||
import { SIDE_PANEL_PAGES_CONFIG } from '@/side-panel/constants/SidePanelPagesConfig';
|
||||
import { SIDE_PANEL_SUB_PAGES } from '@/side-panel/constants/SidePanelSubPages';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { SidePanelSubPageNavigationHeader } from '@/side-panel/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -20,6 +24,14 @@ const StyledSidePanelContent = styled.div`
|
||||
export const SidePanelRouter = () => {
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const sidePanelPageInfo = useAtomStateValue(sidePanelPageInfoState);
|
||||
const sidePanelNavigationStack = useAtomStateValue(
|
||||
sidePanelNavigationStackState,
|
||||
);
|
||||
const { goBackFromSidePanel } = useSidePanelHistory();
|
||||
|
||||
const isSubPage =
|
||||
sidePanelNavigationStack.length > 1 &&
|
||||
SIDE_PANEL_SUB_PAGES.has(sidePanelPage);
|
||||
|
||||
const rawPageComponent = isDefined(sidePanelPage)
|
||||
? SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage)
|
||||
@@ -50,6 +62,12 @@ export const SidePanelRouter = () => {
|
||||
>
|
||||
<SidePanelTopBar />
|
||||
</motion.div>
|
||||
{isSubPage && isDefined(sidePanelPageInfo.title) && (
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={sidePanelPageInfo.title}
|
||||
onBackClick={goBackFromSidePanel}
|
||||
/>
|
||||
)}
|
||||
<StyledSidePanelContent>
|
||||
<CommandMenuContextProvider
|
||||
isInSidePanel={true}
|
||||
|
||||
@@ -2,28 +2,29 @@ import { SidePanelBackButton } from '@/side-panel/components/SidePanelBackButton
|
||||
import { SidePanelPageInfo } from '@/side-panel/components/SidePanelPageInfo';
|
||||
import { SidePanelTopBarInputFocusEffect } from '@/side-panel/components/SidePanelTopBarInputFocusEffect';
|
||||
import { SidePanelTopBarRightCornerIcon } from '@/side-panel/components/SidePanelTopBarRightCornerIcon';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { SIDE_PANEL_SUB_PAGES } from '@/side-panel/constants/SidePanelSubPages';
|
||||
import { SIDE_PANEL_TOP_BAR_HEIGHT } from '@/side-panel/constants/SidePanelTopBarHeight';
|
||||
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useSidePanelContextChips } from '@/side-panel/hooks/useSidePanelContextChips';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useContext, useRef } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledInputContainer = styled.div<{ isMobile: boolean }>`
|
||||
align-items: center;
|
||||
@@ -93,10 +94,6 @@ export const SidePanelTopBar = () => {
|
||||
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
|
||||
const sidePanelNavigationStack = useAtomStateValue(
|
||||
sidePanelNavigationStackState,
|
||||
);
|
||||
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const { contextChips } = useSidePanelContextChips();
|
||||
@@ -124,45 +121,36 @@ export const SidePanelTopBar = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const sidePanelNavigationStack = useAtomStateValue(
|
||||
sidePanelNavigationStackState,
|
||||
);
|
||||
|
||||
const isOnSubPage = SIDE_PANEL_SUB_PAGES.has(sidePanelPage);
|
||||
const canGoBack = sidePanelNavigationStack.length > 1;
|
||||
|
||||
const shouldShowCloseButton =
|
||||
!isMobile && sidePanelNavigationStack.length === 1;
|
||||
|
||||
const shouldShowBackButton = canGoBack;
|
||||
const shouldShowBackButton = !isMobile && canGoBack && !isOnSubPage;
|
||||
const shouldShowCloseButton = !isMobile && !shouldShowBackButton;
|
||||
|
||||
const lastChip = contextChips.at(-1);
|
||||
|
||||
return (
|
||||
<StyledInputContainer isMobile={isMobile}>
|
||||
<StyledContentContainer>
|
||||
<AnimatePresence>
|
||||
{shouldShowBackButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<SidePanelBackButton />
|
||||
</motion.div>
|
||||
)}
|
||||
{shouldShowCloseButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={closeSidePanelMenu}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{shouldShowBackButton && <SidePanelBackButton />}
|
||||
{shouldShowCloseButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={closeSidePanelMenu}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{lastChip &&
|
||||
sidePanelPage !== SidePanelPages.Root &&
|
||||
sidePanelPage !== SidePanelPages.SearchRecords && (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
export const SIDE_PANEL_SUB_PAGES = new Set<SidePanelPages>([
|
||||
SidePanelPages.PageLayoutGraphFilter,
|
||||
SidePanelPages.PageLayoutFieldsLayout,
|
||||
]);
|
||||
+1
-9
@@ -1,5 +1,3 @@
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { SidePanelSubPageNavigationHeader } from '@/side-panel/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
import { ChartFiltersSettingsInitializeStateEffect } from '@/side-panel/pages/page-layout/components/ChartFiltersSettingsInitializeStateEffect';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
@@ -15,9 +13,9 @@ import { RecordFiltersComponentInstanceContext } from '@/object-record/record-fi
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledChartFiltersPageContainer = styled.div`
|
||||
@@ -38,8 +36,6 @@ export const ChartFiltersSettings = ({
|
||||
objectMetadataItem,
|
||||
widget,
|
||||
}: ChartFiltersSettingsProps) => {
|
||||
const { goBackFromSidePanel } = useSidePanelHistory();
|
||||
|
||||
const { instanceId } = getChartFiltersSettingsInstanceId({
|
||||
widgetId: widget.id,
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
@@ -80,10 +76,6 @@ export const ChartFiltersSettings = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={t`Filter`}
|
||||
onBackClick={goBackFromSidePanel}
|
||||
/>
|
||||
<StyledChartFiltersPageContainer>
|
||||
<div>
|
||||
<InputLabel>{t`Conditions`}</InputLabel>
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
|
||||
import { useGetNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultVisibility';
|
||||
import { useUpdateNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useUpdateNewFieldDefaultVisibility';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconEye } from 'twenty-ui/display';
|
||||
|
||||
export const NewFieldDefaultVisibilityToggle = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
}: {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { newFieldDefaultVisibility } = useGetNewFieldDefaultVisibility({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { updateNewFieldDefaultVisibility } =
|
||||
useUpdateNewFieldDefaultVisibility({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const handleToggle = () => {
|
||||
updateNewFieldDefaultVisibility(!newFieldDefaultVisibility);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectableListItem
|
||||
itemId="new-field-default-visibility"
|
||||
onEnter={handleToggle}
|
||||
>
|
||||
<CommandMenuItemToggle
|
||||
LeftIcon={IconEye}
|
||||
text={t`Set fields created in the future as "visible"`}
|
||||
id="new-field-default-visibility"
|
||||
toggled={newFieldDefaultVisibility}
|
||||
onToggleChange={handleToggle}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
+3
-12
@@ -1,18 +1,15 @@
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { SidePanelSubPageNavigationHeader } from '@/side-panel/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
|
||||
import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
|
||||
import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect';
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledOuterContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -30,8 +27,6 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const SidePanelPageLayoutFieldsLayout = () => {
|
||||
const { goBackFromSidePanel } = useSidePanelHistory();
|
||||
|
||||
const { pageLayoutId } =
|
||||
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();
|
||||
|
||||
@@ -52,10 +47,6 @@ export const SidePanelPageLayoutFieldsLayout = () => {
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={t`Layout`}
|
||||
onBackClick={goBackFromSidePanel}
|
||||
/>
|
||||
<StyledContainer>
|
||||
<FieldsWidgetGroupsDraftInitializationEffect
|
||||
viewId={fieldsConfiguration.viewId ?? null}
|
||||
|
||||
+6
@@ -3,6 +3,7 @@ import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItem
|
||||
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { NewFieldDefaultVisibilityToggle } from '@/side-panel/pages/page-layout/components/NewFieldDefaultVisibilityToggle';
|
||||
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
|
||||
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
|
||||
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
|
||||
@@ -78,6 +79,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
|
||||
const selectableItemIds = [
|
||||
'layout',
|
||||
'new-field-default-visibility',
|
||||
'display-more-fields-button',
|
||||
'action-button',
|
||||
'move-down',
|
||||
@@ -119,6 +121,10 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
onToggleChange={handleToggleShouldAllowUserToSeeHiddenFields}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<NewFieldDefaultVisibilityToggle
|
||||
pageLayoutId={pageLayoutId}
|
||||
widgetId={widgetInEditMode.id}
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
</SidePanelList>
|
||||
</StyledSidePanelContainer>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export const getPageLayoutPageTitle = (page: PageLayoutSidePanelPage) => {
|
||||
case SidePanelPages.PageLayoutIframeSettings:
|
||||
return t`iFrame Settings`;
|
||||
case SidePanelPages.PageLayoutGraphFilter:
|
||||
return t`Configure filters`;
|
||||
return t`Filter`;
|
||||
case SidePanelPages.PageLayoutTabSettings:
|
||||
return t`Tab Settings`;
|
||||
case SidePanelPages.PageLayoutFieldsSettings:
|
||||
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
|
||||
import { SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
|
||||
import { INSTALL_APPLICATION } from '~/modules/marketplace/graphql/mutations/installApplication';
|
||||
import { useUploadAppTarball } from '~/modules/marketplace/hooks/useUploadAppTarball';
|
||||
import {
|
||||
StyledAppModal,
|
||||
StyledAppModalButton,
|
||||
StyledAppModalSection,
|
||||
StyledAppModalTitle,
|
||||
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
|
||||
export const UPLOAD_TARBALL_MODAL_ID = 'upload-tarball-modal';
|
||||
|
||||
const StyledFileInput = styled.input`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
export const SettingsUploadTarballModal = () => {
|
||||
const { t: tFn } = useLingui();
|
||||
const { closeModal } = useModal();
|
||||
const { upload, isUploading } = useUploadAppTarball();
|
||||
const [installApplication] = useMutation(INSTALL_APPLICATION);
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const apolloClient = useApolloClient();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSelectFile = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (!isDefined(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadResult = await upload(file);
|
||||
|
||||
if (!uploadResult.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const registrationId = uploadResult.registrationId;
|
||||
|
||||
if (isDefined(registrationId)) {
|
||||
try {
|
||||
await installApplication({
|
||||
variables: { appRegistrationId: registrationId },
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application installed successfully.`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Tarball uploaded but installation failed.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await apolloClient.refetchQueries({
|
||||
include: [FIND_MANY_APPLICATION_REGISTRATIONS],
|
||||
});
|
||||
closeModal(UPLOAD_TARBALL_MODAL_ID);
|
||||
} finally {
|
||||
if (isDefined(fileInputRef.current)) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeModal(UPLOAD_TARBALL_MODAL_ID);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAppModal
|
||||
modalId={UPLOAD_TARBALL_MODAL_ID}
|
||||
isClosable={true}
|
||||
padding="large"
|
||||
dataGloballyPreventClickOutside
|
||||
>
|
||||
<StyledAppModalTitle>
|
||||
<H1Title
|
||||
title={tFn`Upload tarball`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
</StyledAppModalTitle>
|
||||
<StyledAppModalSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
{tFn`Select a .tar.gz application package to upload and install.`}
|
||||
</StyledAppModalSection>
|
||||
|
||||
<StyledFileInput
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".tar.gz,.tgz"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<StyledAppModalButton
|
||||
onClick={handleCancel}
|
||||
variant="secondary"
|
||||
title={tFn`Cancel`}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledAppModalButton
|
||||
onClick={handleSelectFile}
|
||||
variant="secondary"
|
||||
accent="blue"
|
||||
title={tFn`Choose file`}
|
||||
disabled={isUploading}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledAppModal>
|
||||
);
|
||||
};
|
||||
+25
-66
@@ -1,11 +1,7 @@
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { UPDATE_APPLICATION_REGISTRATION } from '@/settings/application-registrations/graphql/mutations/updateApplicationRegistration';
|
||||
import { FIND_APPLICATION_REGISTRATION_STATS } from '@/settings/application-registrations/graphql/queries/findApplicationRegistrationStats';
|
||||
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
|
||||
import { FIND_ONE_APPLICATION_REGISTRATION } from '@/settings/application-registrations/graphql/queries/findOneApplicationRegistration';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
@@ -35,7 +31,6 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
registration: ApplicationRegistrationData;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
@@ -47,32 +42,6 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
skip: !applicationRegistrationId,
|
||||
});
|
||||
|
||||
const [updateRegistration] = useMutation(UPDATE_APPLICATION_REGISTRATION, {
|
||||
refetchQueries: [
|
||||
FIND_ONE_APPLICATION_REGISTRATION,
|
||||
FIND_MANY_APPLICATION_REGISTRATIONS,
|
||||
],
|
||||
});
|
||||
|
||||
const handleToggleListed = async () => {
|
||||
try {
|
||||
await updateRegistration({
|
||||
variables: {
|
||||
input: {
|
||||
id: applicationRegistrationId,
|
||||
update: {
|
||||
isListed: !registration.isListed,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error updating marketplace listing`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const marketplacePageUrl = getSettingsPath(
|
||||
SettingsPath.AvailableApplicationDetail,
|
||||
{
|
||||
@@ -111,41 +80,31 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Marketplace Listing`}
|
||||
description={t`Control visibility on the marketplace. Unlisted apps are still accessible via direct link.`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
title={t`Listed on marketplace`}
|
||||
description={
|
||||
isNpmSource
|
||||
? t`Managed by the marketplace catalog sync for npm packages`
|
||||
: t`When enabled, this app appears in the marketplace browse page`
|
||||
}
|
||||
checked={registration.isListed}
|
||||
onChange={handleToggleListed}
|
||||
disabled={isNpmSource}
|
||||
divider
|
||||
{isNpmSource && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Marketplace Listing`}
|
||||
description={t`This app is listed on the marketplace because it is published to npm.`}
|
||||
/>
|
||||
<SettingsOptionCardContentToggle
|
||||
title={t`Featured`}
|
||||
description={t`Featured apps are curated. Open a PR to request featured status.`}
|
||||
checked={registration.isFeatured}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
/>
|
||||
</Card>
|
||||
<StyledButtonGroup>
|
||||
<Button
|
||||
Icon={IconExternalLink}
|
||||
title={t`View marketplace page`}
|
||||
variant="secondary"
|
||||
to={marketplacePageUrl}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
</Section>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
title={t`Featured`}
|
||||
description={t`Featured apps are curated. Open a PR to request featured status.`}
|
||||
checked={registration.isFeatured}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
/>
|
||||
</Card>
|
||||
<StyledButtonGroup>
|
||||
<Button
|
||||
Icon={IconExternalLink}
|
||||
title={t`View marketplace page`}
|
||||
variant="secondary"
|
||||
to={marketplacePageUrl}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{hasStats && (
|
||||
<Section>
|
||||
|
||||
+89
-52
@@ -2,12 +2,11 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { FIND_MANY_APPLICATION_REGISTRATIONS } from '@/settings/application-registrations/graphql/queries/findManyApplicationRegistrations';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useContext } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -17,73 +16,128 @@ import {
|
||||
IconChevronRight,
|
||||
IconCopy,
|
||||
IconFileInfo,
|
||||
IconUpload,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import {
|
||||
SettingsUploadTarballModal,
|
||||
UPLOAD_TARBALL_MODAL_ID,
|
||||
} from '~/pages/settings/applications/components/SettingsUploadTarballModal';
|
||||
type ApplicationRegistrationFragmentFragment,
|
||||
ApplicationRegistrationSourceType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
const StyledButtonGroupContainer = styled.div`
|
||||
const StyledRowRightContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type ApplicationRegistration = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
const SOURCE_TYPE_BADGE_CONFIG: Record<
|
||||
ApplicationRegistrationSourceType,
|
||||
{ label: string; color: 'gray' | 'blue' | 'green' }
|
||||
> = {
|
||||
[ApplicationRegistrationSourceType.LOCAL]: {
|
||||
label: 'Dev',
|
||||
color: 'gray',
|
||||
},
|
||||
[ApplicationRegistrationSourceType.NPM]: {
|
||||
label: 'Npm',
|
||||
color: 'blue',
|
||||
},
|
||||
[ApplicationRegistrationSourceType.TARBALL]: {
|
||||
label: 'Internal',
|
||||
color: 'green',
|
||||
},
|
||||
};
|
||||
|
||||
export const SettingsApplicationsDeveloperTab = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const { data, loading } = useQuery(FIND_MANY_APPLICATION_REGISTRATIONS);
|
||||
|
||||
const registrations: ApplicationRegistration[] =
|
||||
const registrations: ApplicationRegistrationFragmentFragment[] =
|
||||
data?.findManyApplicationRegistrations ?? [];
|
||||
|
||||
const commands = [
|
||||
const createCommands = [
|
||||
// oxlint-disable-next-line lingui/no-unlocalized-strings
|
||||
'npx create-twenty-app@latest my-twenty-app',
|
||||
// oxlint-disable-next-line lingui/no-unlocalized-strings
|
||||
'cd my-twenty-app',
|
||||
];
|
||||
|
||||
const copyButton = (
|
||||
const createCopyButton = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyToClipboard(commands.join('\n'), t`Commands copied to clipboard`);
|
||||
copyToClipboard(
|
||||
createCommands.join('\n'),
|
||||
t`Commands copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
ariaLabel={t`Copy commands`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
);
|
||||
|
||||
const getRegistrationLink = (
|
||||
registration: ApplicationRegistrationFragmentFragment,
|
||||
) =>
|
||||
getSettingsPath(SettingsPath.ApplicationRegistrationDetail, {
|
||||
applicationRegistrationId: registration.id,
|
||||
});
|
||||
|
||||
const syncCommands = [
|
||||
// oxlint-disable-next-line lingui/no-unlocalized-strings
|
||||
'yarn twenty app:dev',
|
||||
];
|
||||
|
||||
const syncCopyButton = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
syncCommands.join('\n'),
|
||||
t`Command copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
ariaLabel={t`Copy command`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
);
|
||||
|
||||
const RowRightWithBadge = ({
|
||||
item,
|
||||
}: {
|
||||
item: ApplicationRegistrationFragmentFragment;
|
||||
}) => {
|
||||
const badgeConfig = SOURCE_TYPE_BADGE_CONFIG[item.sourceType];
|
||||
|
||||
return (
|
||||
<StyledRowRightContainer>
|
||||
<Tag text={badgeConfig.label} color={badgeConfig.color} preventShrink />
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledRowRightContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Create an application`}
|
||||
description={t`You can either create a private app or share it to others`}
|
||||
title={t`Create & Develop`}
|
||||
description={t`Scaffold a new app, then use the CLI to develop, publish, and distribute`}
|
||||
/>
|
||||
<CommandBlock commands={commands} button={copyButton} />
|
||||
<CommandBlock commands={createCommands} button={createCopyButton} />
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconFileInfo}
|
||||
@@ -100,44 +154,27 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`My Apps`}
|
||||
description={t`Apps you've created, registered, or published`}
|
||||
title={t`Your Apps`}
|
||||
description={t`All applications registered on this workspace`}
|
||||
/>
|
||||
{registrations.length > 0 && (
|
||||
{registrations.length > 0 ? (
|
||||
<SettingsListCard
|
||||
items={registrations}
|
||||
getItemLabel={(registration) => registration.name}
|
||||
isLoading={loading}
|
||||
RowIcon={IconApps}
|
||||
onRowClick={(registration) => {
|
||||
navigate(
|
||||
getSettingsPath(SettingsPath.ApplicationRegistrationDetail, {
|
||||
applicationRegistrationId: registration.id,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
RowRightComponent={() => (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
to={getRegistrationLink}
|
||||
RowRightComponent={RowRightWithBadge}
|
||||
/>
|
||||
) : (
|
||||
!loading && (
|
||||
<CommandBlock commands={syncCommands} button={syncCopyButton} />
|
||||
)
|
||||
)}
|
||||
</Section>
|
||||
<StyledButtonGroupContainer>
|
||||
<Button
|
||||
Icon={IconUpload}
|
||||
title={t`Upload tarball`}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => openModal(UPLOAD_TARBALL_MODAL_ID)}
|
||||
/>
|
||||
</StyledButtonGroupContainer>
|
||||
|
||||
<SettingsUploadTarballModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -874,11 +874,6 @@ type RatioAggregateConfig {
|
||||
optionValue: String!
|
||||
}
|
||||
|
||||
type NewFieldDefaultConfiguration {
|
||||
isVisible: Boolean!
|
||||
viewFieldGroupId: String
|
||||
}
|
||||
|
||||
type RichTextV2Body {
|
||||
blocknote: String
|
||||
markdown: String
|
||||
@@ -1168,7 +1163,7 @@ type FieldRichTextConfiguration {
|
||||
type FieldsConfiguration {
|
||||
configurationType: WidgetConfigurationType!
|
||||
viewId: String
|
||||
newFieldDefaultConfiguration: NewFieldDefaultConfiguration
|
||||
newFieldDefaultVisibility: Boolean
|
||||
shouldAllowUserToSeeHiddenFields: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -644,12 +644,6 @@ export interface RatioAggregateConfig {
|
||||
__typename: 'RatioAggregateConfig'
|
||||
}
|
||||
|
||||
export interface NewFieldDefaultConfiguration {
|
||||
isVisible: Scalars['Boolean']
|
||||
viewFieldGroupId?: Scalars['String']
|
||||
__typename: 'NewFieldDefaultConfiguration'
|
||||
}
|
||||
|
||||
export interface RichTextV2Body {
|
||||
blocknote?: Scalars['String']
|
||||
markdown?: Scalars['String']
|
||||
@@ -887,7 +881,7 @@ export interface FieldRichTextConfiguration {
|
||||
export interface FieldsConfiguration {
|
||||
configurationType: WidgetConfigurationType
|
||||
viewId?: Scalars['String']
|
||||
newFieldDefaultConfiguration?: NewFieldDefaultConfiguration
|
||||
newFieldDefaultVisibility?: Scalars['Boolean']
|
||||
shouldAllowUserToSeeHiddenFields?: Scalars['Boolean']
|
||||
__typename: 'FieldsConfiguration'
|
||||
}
|
||||
@@ -3488,13 +3482,6 @@ export interface RatioAggregateConfigGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface NewFieldDefaultConfigurationGenqlSelection{
|
||||
isVisible?: boolean | number
|
||||
viewFieldGroupId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface RichTextV2BodyGenqlSelection{
|
||||
blocknote?: boolean | number
|
||||
markdown?: boolean | number
|
||||
@@ -3752,7 +3739,7 @@ export interface FieldRichTextConfigurationGenqlSelection{
|
||||
export interface FieldsConfigurationGenqlSelection{
|
||||
configurationType?: boolean | number
|
||||
viewId?: boolean | number
|
||||
newFieldDefaultConfiguration?: NewFieldDefaultConfigurationGenqlSelection
|
||||
newFieldDefaultVisibility?: boolean | number
|
||||
shouldAllowUserToSeeHiddenFields?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -6452,14 +6439,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const NewFieldDefaultConfiguration_possibleTypes: string[] = ['NewFieldDefaultConfiguration']
|
||||
export const isNewFieldDefaultConfiguration = (obj?: { __typename?: any } | null): obj is NewFieldDefaultConfiguration => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isNewFieldDefaultConfiguration"')
|
||||
return NewFieldDefaultConfiguration_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const RichTextV2Body_possibleTypes: string[] = ['RichTextV2Body']
|
||||
export const isRichTextV2Body = (obj?: { __typename?: any } | null): obj is RichTextV2Body => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRichTextV2Body"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-32
@@ -273,7 +273,7 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.FIELDS: {
|
||||
const { viewId, newFieldDefaultConfiguration, ...rest } = configuration;
|
||||
const { viewId, newFieldDefaultVisibility, ...rest } = configuration;
|
||||
|
||||
let viewUniversalIdentifier: string | null = null;
|
||||
|
||||
@@ -291,40 +291,10 @@ export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(newFieldDefaultConfiguration)) {
|
||||
return {
|
||||
...rest,
|
||||
newFieldDefaultConfiguration,
|
||||
viewId: viewUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
let viewFieldGroupUniversalIdentifier: string | null = null;
|
||||
|
||||
if (isDefined(newFieldDefaultConfiguration.viewFieldGroupId)) {
|
||||
viewFieldGroupUniversalIdentifier =
|
||||
viewFieldGroupUniversalIdentifierById[
|
||||
newFieldDefaultConfiguration.viewFieldGroupId
|
||||
] ?? null;
|
||||
|
||||
if (
|
||||
!isDefined(viewFieldGroupUniversalIdentifier) &&
|
||||
shouldThrowOnMissingIdentifier
|
||||
) {
|
||||
throw new FlatEntityMapsException(
|
||||
`View field group universal identifier not found for id: ${newFieldDefaultConfiguration.viewFieldGroupId}`,
|
||||
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
newFieldDefaultVisibility,
|
||||
viewId: viewUniversalIdentifier,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible: newFieldDefaultConfiguration.isVisible,
|
||||
viewFieldGroupId: viewFieldGroupUniversalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+24
-18
@@ -1,8 +1,8 @@
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { computeFlatViewFieldsFromFieldsWidgets } from 'src/engine/metadata-modules/flat-view-field/utils/compute-flat-view-fields-from-fields-widgets.util';
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
@@ -37,13 +37,24 @@ const buildFlatViewMaps = (
|
||||
}) as unknown as FlatViewMaps;
|
||||
|
||||
const buildFlatViewFieldGroupMaps = (
|
||||
entries: { id: string; universalIdentifier: string }[] = [],
|
||||
entries: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
viewId?: string;
|
||||
position?: number;
|
||||
}[] = [],
|
||||
): FlatViewFieldGroupMaps =>
|
||||
({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
entries.map((entry) => [
|
||||
entry.universalIdentifier,
|
||||
{ universalIdentifier: entry.universalIdentifier, id: entry.id },
|
||||
{
|
||||
universalIdentifier: entry.universalIdentifier,
|
||||
id: entry.id,
|
||||
viewId: entry.viewId ?? VIEW_ID,
|
||||
position: entry.position ?? 0,
|
||||
deletedAt: null,
|
||||
},
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
@@ -74,14 +85,12 @@ const buildFieldsWidget = ({
|
||||
objectMetadataUniversalIdentifier = OBJECT_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
viewId = VIEW_ID,
|
||||
isVisible = true,
|
||||
viewFieldGroupId = null as string | null,
|
||||
deletedAt = null as string | null,
|
||||
}: {
|
||||
widgetUniversalIdentifier?: string;
|
||||
objectMetadataUniversalIdentifier?: string;
|
||||
viewId?: string | null;
|
||||
isVisible?: boolean;
|
||||
viewFieldGroupId?: string | null;
|
||||
deletedAt?: string | null;
|
||||
} = {}) => ({
|
||||
universalIdentifier: widgetUniversalIdentifier,
|
||||
@@ -91,10 +100,7 @@ const buildFieldsWidget = ({
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible,
|
||||
viewFieldGroupId,
|
||||
},
|
||||
newFieldDefaultVisibility: isVisible,
|
||||
},
|
||||
universalConfiguration: null,
|
||||
overrides: null,
|
||||
@@ -478,7 +484,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
});
|
||||
|
||||
describe('view field group handling', () => {
|
||||
it('should resolve viewFieldGroupUniversalIdentifier when viewFieldGroupId is set', () => {
|
||||
it('should resolve to the last view field group when groups exist', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -488,7 +494,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
|
||||
buildFieldsWidget({ viewFieldGroupId: VIEW_FIELD_GROUP_ID }),
|
||||
buildFieldsWidget(),
|
||||
]),
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps(),
|
||||
flatViewMaps: buildFlatViewMaps([
|
||||
@@ -509,7 +515,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should set viewFieldGroupUniversalIdentifier to null when viewFieldGroupId is null', () => {
|
||||
it('should set viewFieldGroupUniversalIdentifier to null when no groups exist', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -519,7 +525,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
|
||||
buildFieldsWidget({ viewFieldGroupId: null }),
|
||||
buildFieldsWidget(),
|
||||
]),
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps(),
|
||||
flatViewMaps: buildFlatViewMaps([
|
||||
@@ -532,7 +538,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
expect(result[0].viewFieldGroupUniversalIdentifier).toBeNull();
|
||||
});
|
||||
|
||||
it('should compute position only from view fields in the same group', () => {
|
||||
it('should compute position only from view fields in the last group', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -542,7 +548,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
|
||||
buildFieldsWidget({ viewFieldGroupId: VIEW_FIELD_GROUP_ID }),
|
||||
buildFieldsWidget(),
|
||||
]),
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps([
|
||||
{
|
||||
@@ -582,7 +588,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
expect(result[0].position).toBe(3);
|
||||
});
|
||||
|
||||
it('should compute position only from ungrouped view fields when viewFieldGroupId is null', () => {
|
||||
it('should compute position only from ungrouped view fields when no groups exist', () => {
|
||||
const result = computeFlatViewFieldsFromFieldsWidgets({
|
||||
fieldsToCreate: [
|
||||
{
|
||||
@@ -592,7 +598,7 @@ describe('computeFlatViewFieldsFromFieldsWidgets', () => {
|
||||
},
|
||||
],
|
||||
flatPageLayoutWidgetMaps: buildFlatPageLayoutWidgetMaps([
|
||||
buildFieldsWidget({ viewFieldGroupId: null }),
|
||||
buildFieldsWidget(),
|
||||
]),
|
||||
flatViewFieldMaps: buildFlatViewFieldMaps([
|
||||
{
|
||||
|
||||
+32
-4
@@ -3,10 +3,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-field/constants/default-view-field-size.constant';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
import { type FieldsConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/fields-configuration.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
@@ -45,9 +45,33 @@ const getMatchingFieldsWidgets = ({
|
||||
objectMetadataUniversalIdentifier &&
|
||||
isFieldsWidgetConfiguration(widget.configuration) &&
|
||||
isDefined(widget.configuration.viewId) &&
|
||||
isDefined(widget.configuration.newFieldDefaultConfiguration),
|
||||
isDefined(widget.configuration.newFieldDefaultVisibility),
|
||||
);
|
||||
|
||||
const findLastViewFieldGroupId = ({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
viewId: string;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): string | null => {
|
||||
const groupsForView = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((group) => !isDefined(group.deletedAt) && group.viewId === viewId);
|
||||
|
||||
if (groupsForView.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastGroup = groupsForView.reduce((maxGroup, group) =>
|
||||
group.position > maxGroup.position ? group : maxGroup,
|
||||
);
|
||||
|
||||
return lastGroup.id;
|
||||
};
|
||||
|
||||
const computeNextPosition = ({
|
||||
viewId,
|
||||
viewFieldGroupId,
|
||||
@@ -127,8 +151,7 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
|
||||
const configuration = widget.configuration;
|
||||
|
||||
const viewId = configuration.viewId!;
|
||||
const { isVisible, viewFieldGroupId } =
|
||||
configuration.newFieldDefaultConfiguration!;
|
||||
const isVisible = configuration.newFieldDefaultVisibility!;
|
||||
|
||||
const viewUniversalIdentifier =
|
||||
flatViewMaps.universalIdentifierById[viewId] ?? null;
|
||||
@@ -137,6 +160,11 @@ export const computeFlatViewFieldsFromFieldsWidgets = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewFieldGroupId = findLastViewFieldGroupId({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
});
|
||||
|
||||
const viewFieldGroupUniversalIdentifier = isDefined(viewFieldGroupId)
|
||||
? (flatViewFieldGroupMaps.universalIdentifierById[viewFieldGroupId] ??
|
||||
null)
|
||||
|
||||
+3
-18
@@ -1,30 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { type FieldsConfiguration } from 'twenty-shared/types';
|
||||
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
@ObjectType('NewFieldDefaultConfiguration')
|
||||
export class NewFieldDefaultConfigurationDTO {
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
isVisible: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
viewFieldGroupId: string | null;
|
||||
}
|
||||
|
||||
@ObjectType('FieldsConfiguration')
|
||||
export class FieldsConfigurationDTO implements FieldsConfiguration {
|
||||
@Field(() => WidgetConfigurationType)
|
||||
@@ -37,11 +23,10 @@ export class FieldsConfigurationDTO implements FieldsConfiguration {
|
||||
@IsUUID()
|
||||
viewId: string | null;
|
||||
|
||||
@Field(() => NewFieldDefaultConfigurationDTO, { nullable: true })
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => NewFieldDefaultConfigurationDTO)
|
||||
newFieldDefaultConfiguration: NewFieldDefaultConfigurationDTO | null;
|
||||
@IsBoolean()
|
||||
newFieldDefaultVisibility: boolean | null;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
|
||||
+4
-26
@@ -128,18 +128,12 @@ const buildFieldsWidgetConfiguration = ({
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible: true,
|
||||
viewFieldGroupId: null,
|
||||
},
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: null,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible: true,
|
||||
viewFieldGroupId: null,
|
||||
},
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -167,32 +161,16 @@ const buildFieldsWidgetConfiguration = ({
|
||||
|
||||
const viewUniversalIdentifier = viewDefinition?.universalIdentifier ?? null;
|
||||
|
||||
const otherViewFieldGroupId =
|
||||
views[recordPageFieldsViewName]?.viewFieldGroups?.other?.id ?? null;
|
||||
|
||||
const otherViewFieldGroupUniversalIdentifier =
|
||||
viewDefinition?.viewFieldGroups?.other?.universalIdentifier ?? null;
|
||||
|
||||
const newFieldDefaultConfiguration = {
|
||||
isVisible: true,
|
||||
viewFieldGroupId: otherViewFieldGroupId,
|
||||
};
|
||||
|
||||
const universalNewFieldDefaultConfiguration = {
|
||||
isVisible: true,
|
||||
viewFieldGroupId: otherViewFieldGroupUniversalIdentifier,
|
||||
};
|
||||
|
||||
return {
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId,
|
||||
newFieldDefaultConfiguration,
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELDS,
|
||||
viewId: viewUniversalIdentifier,
|
||||
newFieldDefaultConfiguration: universalNewFieldDefaultConfiguration,
|
||||
newFieldDefaultVisibility: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+2
-32
@@ -257,7 +257,7 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({
|
||||
case WidgetConfigurationType.FIELDS: {
|
||||
const {
|
||||
viewId: viewUniversalIdentifier,
|
||||
newFieldDefaultConfiguration: universalNewFieldDefaultConfiguration,
|
||||
newFieldDefaultVisibility,
|
||||
...rest
|
||||
} = universalConfiguration;
|
||||
|
||||
@@ -279,37 +279,7 @@ export const fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration = ({
|
||||
viewId = flatView.id;
|
||||
}
|
||||
|
||||
let newFieldDefaultConfiguration:
|
||||
| { isVisible: boolean; viewFieldGroupId: string | null }
|
||||
| null
|
||||
| undefined = universalNewFieldDefaultConfiguration;
|
||||
|
||||
if (
|
||||
isDefined(universalNewFieldDefaultConfiguration) &&
|
||||
isDefined(universalNewFieldDefaultConfiguration.viewFieldGroupId)
|
||||
) {
|
||||
const viewFieldGroupUniversalIdentifier =
|
||||
universalNewFieldDefaultConfiguration.viewFieldGroupId;
|
||||
|
||||
const flatViewFieldGroup = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatViewFieldGroupMaps,
|
||||
universalIdentifier: viewFieldGroupUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(flatViewFieldGroup)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`View field group not found for universal identifier: ${viewFieldGroupUniversalIdentifier}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
newFieldDefaultConfiguration = {
|
||||
isVisible: universalNewFieldDefaultConfiguration.isVisible,
|
||||
viewFieldGroupId: flatViewFieldGroup.id,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...rest, viewId, newFieldDefaultConfiguration };
|
||||
return { ...rest, viewId, newFieldDefaultVisibility };
|
||||
}
|
||||
|
||||
case WidgetConfigurationType.FRONT_COMPONENT: {
|
||||
|
||||
+1
-6
@@ -95,15 +95,10 @@ export type FieldConfiguration = {
|
||||
configurationType: 'FIELD';
|
||||
};
|
||||
|
||||
type NewFieldDefaultConfiguration = {
|
||||
isVisible: boolean;
|
||||
viewFieldGroupId: string | null;
|
||||
};
|
||||
|
||||
export type FieldsConfiguration = {
|
||||
configurationType: 'FIELDS';
|
||||
viewId?: string | null;
|
||||
newFieldDefaultConfiguration?: NewFieldDefaultConfiguration | null;
|
||||
newFieldDefaultVisibility?: boolean | null;
|
||||
shouldAllowUserToSeeHiddenFields?: boolean;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user