Allow users to set where new fields must be created in a record page layout (#18420)
## Demo https://github.com/user-attachments/assets/eaf89d0c-96e0-4e49-ac58-290c8e7403ff
This commit is contained in:
committed by
GitHub
parent
d37ed7e07c
commit
a79b816117
@@ -0,0 +1,38 @@
|
||||
import { isValidReturnToPath } from '@/auth/utils/isValidReturnToPath';
|
||||
|
||||
describe('isValidReturnToPath', () => {
|
||||
it('should return false for empty string', () => {
|
||||
expect(isValidReturnToPath('')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for root path', () => {
|
||||
expect(isValidReturnToPath('/')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for paths not starting with slash', () => {
|
||||
expect(isValidReturnToPath('objects/people')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for double-slash paths', () => {
|
||||
expect(isValidReturnToPath('//evil.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for onboarding paths', () => {
|
||||
expect(isValidReturnToPath('/create/workspace')).toBe(false);
|
||||
expect(isValidReturnToPath('/create/profile')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for sign-in paths', () => {
|
||||
expect(isValidReturnToPath('/welcome')).toBe(false);
|
||||
expect(isValidReturnToPath('/verify')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for reset-password paths', () => {
|
||||
expect(isValidReturnToPath('/reset-password')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for valid application paths', () => {
|
||||
expect(isValidReturnToPath('/objects/people')).toBe(true);
|
||||
expect(isValidReturnToPath('/settings/accounts')).toBe(true);
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { resolveDateFormat } from '@/localization/utils/resolveDateFormat';
|
||||
|
||||
jest.mock('@/localization/utils/detection/detectDateFormat', () => ({
|
||||
detectDateFormat: jest.fn(() => 'DAY_FIRST'),
|
||||
}));
|
||||
|
||||
describe('resolveDateFormat', () => {
|
||||
it('should detect system format when SYSTEM is passed', () => {
|
||||
const result = resolveDateFormat(DateFormat.SYSTEM);
|
||||
|
||||
expect(result).toBe(DateFormat.DAY_FIRST);
|
||||
});
|
||||
|
||||
it('should return MONTH_FIRST as-is', () => {
|
||||
expect(resolveDateFormat(DateFormat.MONTH_FIRST)).toBe(
|
||||
DateFormat.MONTH_FIRST,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return DAY_FIRST as-is', () => {
|
||||
expect(resolveDateFormat(DateFormat.DAY_FIRST)).toBe(DateFormat.DAY_FIRST);
|
||||
});
|
||||
|
||||
it('should return YEAR_FIRST as-is', () => {
|
||||
expect(resolveDateFormat(DateFormat.YEAR_FIRST)).toBe(
|
||||
DateFormat.YEAR_FIRST,
|
||||
);
|
||||
});
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { resolveTimeFormat } from '@/localization/utils/resolveTimeFormat';
|
||||
|
||||
jest.mock('@/localization/utils/detection/detectTimeFormat', () => ({
|
||||
detectTimeFormat: jest.fn(() => 'HOUR_24'),
|
||||
}));
|
||||
|
||||
describe('resolveTimeFormat', () => {
|
||||
it('should detect system format when SYSTEM is passed', () => {
|
||||
const result = resolveTimeFormat(TimeFormat.SYSTEM);
|
||||
|
||||
expect(result).toBe(TimeFormat.HOUR_24);
|
||||
});
|
||||
|
||||
it('should return HOUR_24 as-is', () => {
|
||||
expect(resolveTimeFormat(TimeFormat.HOUR_24)).toBe(TimeFormat.HOUR_24);
|
||||
});
|
||||
|
||||
it('should return HOUR_12 as-is', () => {
|
||||
expect(resolveTimeFormat(TimeFormat.HOUR_12)).toBe(TimeFormat.HOUR_12);
|
||||
});
|
||||
});
|
||||
+4
@@ -166,6 +166,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
... on FieldsConfiguration {
|
||||
configurationType
|
||||
viewId
|
||||
newFieldDefaultConfiguration {
|
||||
isVisible
|
||||
viewFieldGroupId
|
||||
}
|
||||
}
|
||||
... on FilesConfiguration {
|
||||
configurationType
|
||||
|
||||
+42
-2
@@ -1,25 +1,28 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
type DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
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';
|
||||
@@ -104,6 +107,17 @@ export const FieldsConfigurationEditor = ({
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { newFieldDefaultConfiguration } = useGetNewFieldDefaultConfiguration({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { updateNewFieldDefaultConfiguration } =
|
||||
useUpdateNewFieldDefaultConfiguration({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
const [renamingGroupValue, setRenamingGroupValue] = useState('');
|
||||
@@ -127,7 +141,7 @@ export const FieldsConfigurationEditor = ({
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: DropResult) => {
|
||||
const { source, destination, type } = result;
|
||||
const { source, destination, type, draggableId } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
@@ -140,6 +154,17 @@ 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') {
|
||||
@@ -202,6 +227,12 @@ export const FieldsConfigurationEditor = ({
|
||||
onMoveField={moveUngroupedField}
|
||||
onToggleFieldVisibility={toggleUngroupedFieldVisibility}
|
||||
onAddGroup={() => handleAddGroup({})}
|
||||
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
|
||||
onToggleNewFieldsVisibility={() =>
|
||||
updateNewFieldDefaultConfiguration({
|
||||
isVisible: !newFieldDefaultConfiguration.isVisible,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -245,6 +276,15 @@ export const FieldsConfigurationEditor = ({
|
||||
renamingGroupValue={renamingGroupValue}
|
||||
onRenamingGroupValueChange={setRenamingGroupValue}
|
||||
onStartRename={handleStartRename}
|
||||
showNewFieldsItem={
|
||||
group.id === newFieldDefaultConfiguration.viewFieldGroupId
|
||||
}
|
||||
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
|
||||
onToggleNewFieldsVisibility={() =>
|
||||
updateNewFieldDefaultConfiguration({
|
||||
isVisible: !newFieldDefaultConfiguration.isVisible,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
+41
-2
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -15,8 +16,13 @@ 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 { IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import {
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconNewSection,
|
||||
IconPlaylistAdd,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
|
||||
import { FieldsConfigurationGroupDraggableHeader } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDraggableHeader';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -83,6 +89,9 @@ type FieldsConfigurationGroupEditorProps = {
|
||||
renamingGroupValue: string;
|
||||
onRenamingGroupValueChange: (value: string) => void;
|
||||
onStartRename: (params: { groupId: string; groupName: string }) => void;
|
||||
showNewFieldsItem: boolean;
|
||||
newFieldsIsVisible: boolean;
|
||||
onToggleNewFieldsVisibility: () => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationGroupEditor = ({
|
||||
@@ -96,6 +105,9 @@ export const FieldsConfigurationGroupEditor = ({
|
||||
renamingGroupValue,
|
||||
onRenamingGroupValueChange,
|
||||
onStartRename,
|
||||
showNewFieldsItem,
|
||||
newFieldsIsVisible,
|
||||
onToggleNewFieldsVisibility,
|
||||
}: FieldsConfigurationGroupEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -211,6 +223,33 @@ 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>
|
||||
)}
|
||||
|
||||
+30
-2
@@ -6,8 +6,13 @@ 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 { IconNewSection } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import {
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconNewSection,
|
||||
IconPlaylistAdd,
|
||||
} from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledFieldsDroppable = styled.div`
|
||||
display: flex;
|
||||
@@ -20,6 +25,8 @@ type FieldsConfigurationUngroupedEditorProps = {
|
||||
onMoveField: (sourceIndex: number, destinationIndex: number) => void;
|
||||
onToggleFieldVisibility: (fieldMetadataId: string) => void;
|
||||
onAddGroup: () => void;
|
||||
newFieldsIsVisible: boolean;
|
||||
onToggleNewFieldsVisibility: () => void;
|
||||
};
|
||||
|
||||
export const FieldsConfigurationUngroupedEditor = ({
|
||||
@@ -27,6 +34,8 @@ export const FieldsConfigurationUngroupedEditor = ({
|
||||
onMoveField,
|
||||
onToggleFieldVisibility,
|
||||
onAddGroup,
|
||||
newFieldsIsVisible,
|
||||
onToggleNewFieldsVisibility,
|
||||
}: FieldsConfigurationUngroupedEditorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -80,6 +89,25 @@ 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
@@ -0,0 +1 @@
|
||||
export const NEW_FIELDS_INDICATOR_DRAGGABLE_ID = 'new-fields-indicator';
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
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 };
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
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 };
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { getShortNestedFieldLabel } from '@/spreadsheet-import/utils/getShortNestedFieldLabel';
|
||||
|
||||
describe('getShortNestedFieldLabel', () => {
|
||||
it('should return everything after the first separator', () => {
|
||||
expect(getShortNestedFieldLabel('Address / City')).toBe('City');
|
||||
});
|
||||
|
||||
it('should return empty string when there is no separator', () => {
|
||||
expect(getShortNestedFieldLabel('Name')).toBe('');
|
||||
});
|
||||
|
||||
it('should preserve nested separators after the first one', () => {
|
||||
expect(getShortNestedFieldLabel('Address / City / Zip')).toBe('City / Zip');
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { type CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
|
||||
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
|
||||
|
||||
const makeWorkspace = (
|
||||
featureFlags?: Array<{ key: string; value: boolean; id: string }>,
|
||||
): CurrentWorkspace =>
|
||||
({
|
||||
id: 'workspace-1',
|
||||
featureFlags,
|
||||
}) as unknown as CurrentWorkspace;
|
||||
|
||||
describe('checkIfFeatureFlagIsEnabledOnWorkspace', () => {
|
||||
it('should return false when featureKey is null', () => {
|
||||
const workspace = makeWorkspace([]);
|
||||
|
||||
expect(checkIfFeatureFlagIsEnabledOnWorkspace(null, workspace)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when featureKey is undefined', () => {
|
||||
const workspace = makeWorkspace([]);
|
||||
|
||||
expect(checkIfFeatureFlagIsEnabledOnWorkspace(undefined, workspace)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when workspace is null', () => {
|
||||
expect(
|
||||
checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
'IS_AIRTABLE_INTEGRATION_ENABLED' as any,
|
||||
null,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when workspace has no featureFlags', () => {
|
||||
const workspace = makeWorkspace(undefined);
|
||||
|
||||
expect(
|
||||
checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
'IS_AIRTABLE_INTEGRATION_ENABLED' as any,
|
||||
workspace,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when feature flag is not found', () => {
|
||||
const workspace = makeWorkspace([
|
||||
{ key: 'OTHER_FLAG', value: true, id: 'flag-1' },
|
||||
]);
|
||||
|
||||
expect(
|
||||
checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
'IS_AIRTABLE_INTEGRATION_ENABLED' as any,
|
||||
workspace,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when feature flag exists but is disabled', () => {
|
||||
const workspace = makeWorkspace([
|
||||
{ key: 'IS_AIRTABLE_INTEGRATION_ENABLED', value: false, id: 'flag-1' },
|
||||
]);
|
||||
|
||||
expect(
|
||||
checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
'IS_AIRTABLE_INTEGRATION_ENABLED' as any,
|
||||
workspace,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when feature flag exists and is enabled', () => {
|
||||
const workspace = makeWorkspace([
|
||||
{ key: 'IS_AIRTABLE_INTEGRATION_ENABLED', value: true, id: 'flag-1' },
|
||||
]);
|
||||
|
||||
expect(
|
||||
checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
'IS_AIRTABLE_INTEGRATION_ENABLED' as any,
|
||||
workspace,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user