feat: view groups (#7176)
Fix #4244 and #4356 This pull request introduces the new "view groups" capability, enabling the reordering, hiding, and showing of columns in Kanban mode. The core enhancement includes the addition of a new entity named `ViewGroup`, which manages column behaviors and interactions. #### Key Changes: 1. **ViewGroup Entity**: The newly added `ViewGroup` entity is responsible for handling the organization and state of columns. This includes: - The ability to reorder columns. - The option to hide or show specific columns based on user preferences. #### Conclusion: This PR adds a significant new feature that enhances the flexibility of Kanban views through the `ViewGroup` entity. We'll later add the view group logic to table view too. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -57,5 +57,6 @@ const config: StorybookConfig = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
logLevel: 'error',
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@ -29,6 +29,7 @@ initialize({
|
|||||||
with payload ${JSON.stringify(requestBody)}\n
|
with payload ${JSON.stringify(requestBody)}\n
|
||||||
This request should be mocked with MSW`);
|
This request should be mocked with MSW`);
|
||||||
},
|
},
|
||||||
|
quiet: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const jestConfig: JestConfigWithTsJest = {
|
|||||||
global: {
|
global: {
|
||||||
statements: 59,
|
statements: 59,
|
||||||
lines: 55,
|
lines: 55,
|
||||||
functions: 49,
|
functions: 48,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export enum CoreObjectNameSingular {
|
|||||||
ViewField = 'viewField',
|
ViewField = 'viewField',
|
||||||
ViewFilter = 'viewFilter',
|
ViewFilter = 'viewFilter',
|
||||||
ViewSort = 'viewSort',
|
ViewSort = 'viewSort',
|
||||||
|
ViewGroup = 'viewGroup',
|
||||||
Webhook = 'webhook',
|
Webhook = 'webhook',
|
||||||
WorkspaceMember = 'workspaceMember',
|
WorkspaceMember = 'workspaceMember',
|
||||||
MessageThreadSubscriber = 'messageThreadSubscriber',
|
MessageThreadSubscriber = 'messageThreadSubscriber',
|
||||||
|
|||||||
@ -31,16 +31,21 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
const StyledColumnContainer = styled.div`
|
const StyledColumnContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
& > *:not(:first-child) {
|
||||||
|
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledContainerContainer = styled.div`
|
const StyledContainerContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledBoardContentContainer = styled.div`
|
const StyledBoardContentContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: calc(100% - 48px);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RecordBoardScrollRestoreEffect = () => {
|
const RecordBoardScrollRestoreEffect = () => {
|
||||||
@ -137,6 +142,12 @@ export const RecordBoard = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// FixMe: Check if we really need this as it depends on the times it takes to update the view groups
|
||||||
|
// if (isPersistingViewGroups) {
|
||||||
|
// // TODO: Add skeleton state
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordBoardScope
|
<RecordBoardScope
|
||||||
recordBoardScopeId={getScopeIdFromComponentId(recordBoardId)}
|
recordBoardScopeId={getScopeIdFromComponentId(recordBoardId)}
|
||||||
|
|||||||
@ -17,6 +17,10 @@ const StyledHeaderContainer = styled.div`
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > *:not(:first-child) {
|
||||||
|
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordBoardHeader = () => {
|
export const RecordBoardHeader = () => {
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||||
import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState';
|
|
||||||
import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState';
|
|
||||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||||
@ -51,14 +49,6 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
|
|||||||
recordBoardColumnIdsComponentState,
|
recordBoardColumnIdsComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
isFirstColumnFamilyState: extractComponentFamilyState(
|
|
||||||
isFirstRecordBoardColumnComponentFamilyState,
|
|
||||||
scopeId,
|
|
||||||
),
|
|
||||||
isLastColumnFamilyState: extractComponentFamilyState(
|
|
||||||
isLastRecordBoardColumnComponentFamilyState,
|
|
||||||
scopeId,
|
|
||||||
),
|
|
||||||
columnsFamilySelector: extractComponentFamilyState(
|
columnsFamilySelector: extractComponentFamilyState(
|
||||||
recordBoardColumnsComponentFamilySelector,
|
recordBoardColumnsComponentFamilySelector,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
|
||||||
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
||||||
const { scopeId, columnIdsState, columnsFamilySelector } =
|
const { scopeId, columnIdsState, columnsFamilySelector } =
|
||||||
@ -10,21 +10,20 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => {
|
|||||||
|
|
||||||
const setColumns = useRecoilCallback(
|
const setColumns = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(columns: RecordBoardColumnDefinition[]) => {
|
(columns: RecordGroupDefinition[]) => {
|
||||||
const currentColumnsIds = snapshot
|
const currentColumnsIds = snapshot
|
||||||
.getLoadable(columnIdsState)
|
.getLoadable(columnIdsState)
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
const columnIds = columns.map(({ id }) => id);
|
const columnIds = columns
|
||||||
|
.filter(({ isVisible }) => isVisible)
|
||||||
|
.map(({ id }) => id);
|
||||||
|
|
||||||
if (isDeeplyEqual(currentColumnsIds, columnIds)) {
|
if (isDeeplyEqual(currentColumnsIds, columnIds)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(
|
set(columnIdsState, columnIds);
|
||||||
columnIdsState,
|
|
||||||
columns.map((column) => column.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
columns.forEach((column) => {
|
columns.forEach((column) => {
|
||||||
const currentColumn = snapshot
|
const currentColumn = snapshot
|
||||||
|
|||||||
@ -6,11 +6,8 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna
|
|||||||
import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer';
|
import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer';
|
||||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
|
|
||||||
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
const StyledColumn = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border-left: 1px solid
|
|
||||||
${({ theme, isFirstColumn }) =>
|
|
||||||
isFirstColumn ? 'none' : theme.border.color.light};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
@ -32,24 +29,12 @@ type RecordBoardColumnProps = {
|
|||||||
export const RecordBoardColumn = ({
|
export const RecordBoardColumn = ({
|
||||||
recordBoardColumnId,
|
recordBoardColumnId,
|
||||||
}: RecordBoardColumnProps) => {
|
}: RecordBoardColumnProps) => {
|
||||||
const {
|
const { columnsFamilySelector, recordIdsByColumnIdFamilyState } =
|
||||||
isFirstColumnFamilyState,
|
useRecordBoardStates();
|
||||||
isLastColumnFamilyState,
|
|
||||||
columnsFamilySelector,
|
|
||||||
recordIdsByColumnIdFamilyState,
|
|
||||||
} = useRecordBoardStates();
|
|
||||||
const columnDefinition = useRecoilValue(
|
const columnDefinition = useRecoilValue(
|
||||||
columnsFamilySelector(recordBoardColumnId),
|
columnsFamilySelector(recordBoardColumnId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFirstColumn = useRecoilValue(
|
|
||||||
isFirstColumnFamilyState(recordBoardColumnId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLastColumn = useRecoilValue(
|
|
||||||
isLastColumnFamilyState(recordBoardColumnId),
|
|
||||||
);
|
|
||||||
|
|
||||||
const recordIds = useRecoilValue(
|
const recordIds = useRecoilValue(
|
||||||
recordIdsByColumnIdFamilyState(recordBoardColumnId),
|
recordIdsByColumnIdFamilyState(recordBoardColumnId),
|
||||||
);
|
);
|
||||||
@ -62,8 +47,6 @@ export const RecordBoardColumn = ({
|
|||||||
<RecordBoardColumnContext.Provider
|
<RecordBoardColumnContext.Provider
|
||||||
value={{
|
value={{
|
||||||
columnDefinition: columnDefinition,
|
columnDefinition: columnDefinition,
|
||||||
isFirstColumn: isFirstColumn,
|
|
||||||
isLastColumn: isLastColumn,
|
|
||||||
recordCount: recordIds.length,
|
recordCount: recordIds.length,
|
||||||
columnId: recordBoardColumnId,
|
columnId: recordBoardColumnId,
|
||||||
recordIds,
|
recordIds,
|
||||||
@ -71,7 +54,7 @@ export const RecordBoardColumn = ({
|
|||||||
>
|
>
|
||||||
<Droppable droppableId={recordBoardColumnId}>
|
<Droppable droppableId={recordBoardColumnId}>
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<StyledColumn isFirstColumn={isFirstColumn}>
|
<StyledColumn>
|
||||||
<RecordBoardColumnCardsContainer
|
<RecordBoardColumnCardsContainer
|
||||||
droppableProvided={droppableProvided}
|
droppableProvided={droppableProvided}
|
||||||
recordIds={recordIds}
|
recordIds={recordIds}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback, useContext, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
@ -25,6 +25,8 @@ export const RecordBoardColumnDropdownMenu = ({
|
|||||||
}: RecordBoardColumnDropdownMenuProps) => {
|
}: RecordBoardColumnDropdownMenuProps) => {
|
||||||
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
const boardColumnMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const recordGroupActions = useRecordGroupActions();
|
||||||
|
|
||||||
const closeMenu = useCallback(() => {
|
const closeMenu = useCallback(() => {
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
@ -34,13 +36,11 @@ export const RecordBoardColumnDropdownMenu = ({
|
|||||||
callback: closeMenu,
|
callback: closeMenu,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledMenuContainer ref={boardColumnMenuRef}>
|
<StyledMenuContainer ref={boardColumnMenuRef}>
|
||||||
<DropdownMenu data-select-disable>
|
<DropdownMenu data-select-disable>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
{columnDefinition.actions.map((action) => (
|
{recordGroupActions.map((action) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={action.id}
|
key={action.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo
|
|||||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
|
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
|
||||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
||||||
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
|
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
|
||||||
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
|
||||||
@ -59,11 +59,8 @@ const StyledRightContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
const StyledColumn = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border-left: 1px solid
|
|
||||||
${({ theme, isFirstColumn }) =>
|
|
||||||
isFirstColumn ? 'none' : theme.border.color.light};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
@ -75,7 +72,7 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordBoardColumnHeader = () => {
|
export const RecordBoardColumnHeader = () => {
|
||||||
const { columnDefinition, isFirstColumn, recordCount } = useContext(
|
const { columnDefinition, recordCount } = useContext(
|
||||||
RecordBoardColumnContext,
|
RecordBoardColumnContext,
|
||||||
);
|
);
|
||||||
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
||||||
@ -120,7 +117,7 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
!isOpportunitiesCompanyFieldDisabled;
|
!isOpportunitiesCompanyFieldDisabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledColumn isFirstColumn={isFirstColumn}>
|
<StyledColumn>
|
||||||
<StyledHeader
|
<StyledHeader
|
||||||
onMouseEnter={() => setIsHeaderHovered(true)}
|
onMouseEnter={() => setIsHeaderHovered(true)}
|
||||||
onMouseLeave={() => setIsHeaderHovered(false)}
|
onMouseLeave={() => setIsHeaderHovered(false)}
|
||||||
@ -130,18 +127,18 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
<Tag
|
<Tag
|
||||||
onClick={handleBoardColumnMenuOpen}
|
onClick={handleBoardColumnMenuOpen}
|
||||||
variant={
|
variant={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? 'solid'
|
? 'solid'
|
||||||
: 'outline'
|
: 'outline'
|
||||||
}
|
}
|
||||||
color={
|
color={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? columnDefinition.color
|
? columnDefinition.color
|
||||||
: 'transparent'
|
: 'transparent'
|
||||||
}
|
}
|
||||||
text={columnDefinition.title}
|
text={columnDefinition.title}
|
||||||
weight={
|
weight={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? 'regular'
|
? 'regular'
|
||||||
: 'medium'
|
: 'medium'
|
||||||
}
|
}
|
||||||
@ -154,13 +151,11 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
<StyledRightContainer>
|
<StyledRightContainer>
|
||||||
{isHeaderHovered && (
|
{isHeaderHovered && (
|
||||||
<StyledHeaderActions>
|
<StyledHeaderActions>
|
||||||
{columnDefinition.actions.length > 0 && (
|
<LightIconButton
|
||||||
<LightIconButton
|
accent="tertiary"
|
||||||
accent="tertiary"
|
Icon={IconDotsVertical}
|
||||||
Icon={IconDotsVertical}
|
onClick={handleBoardColumnMenuOpen}
|
||||||
onClick={handleBoardColumnMenuOpen}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<LightIconButton
|
<LightIconButton
|
||||||
accent="tertiary"
|
accent="tertiary"
|
||||||
@ -172,7 +167,7 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
</StyledRightContainer>
|
</StyledRightContainer>
|
||||||
</StyledHeaderContainer>
|
</StyledHeaderContainer>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
{isBoardColumnMenuOpen && columnDefinition.actions.length > 0 && (
|
{isBoardColumnMenuOpen && (
|
||||||
<RecordBoardColumnDropdownMenu
|
<RecordBoardColumnDropdownMenu
|
||||||
onClose={handleBoardColumnMenuClose}
|
onClose={handleBoardColumnMenuClose}
|
||||||
stageId={columnDefinition.id}
|
stageId={columnDefinition.id}
|
||||||
|
|||||||
@ -12,19 +12,11 @@ type RecordBoardColumnHeaderWrapperProps = {
|
|||||||
export const RecordBoardColumnHeaderWrapper = ({
|
export const RecordBoardColumnHeaderWrapper = ({
|
||||||
columnId,
|
columnId,
|
||||||
}: RecordBoardColumnHeaderWrapperProps) => {
|
}: RecordBoardColumnHeaderWrapperProps) => {
|
||||||
const {
|
const { columnsFamilySelector, recordIdsByColumnIdFamilyState } =
|
||||||
isFirstColumnFamilyState,
|
useRecordBoardStates();
|
||||||
isLastColumnFamilyState,
|
|
||||||
columnsFamilySelector,
|
|
||||||
recordIdsByColumnIdFamilyState,
|
|
||||||
} = useRecordBoardStates();
|
|
||||||
|
|
||||||
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||||
|
|
||||||
const isFirstColumn = useRecoilValue(isFirstColumnFamilyState(columnId));
|
|
||||||
|
|
||||||
const isLastColumn = useRecoilValue(isLastColumnFamilyState(columnId));
|
|
||||||
|
|
||||||
const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId));
|
const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId));
|
||||||
|
|
||||||
if (!isDefined(columnDefinition)) {
|
if (!isDefined(columnDefinition)) {
|
||||||
@ -36,8 +28,6 @@ export const RecordBoardColumnHeaderWrapper = ({
|
|||||||
value={{
|
value={{
|
||||||
columnId,
|
columnId,
|
||||||
columnDefinition: columnDefinition,
|
columnDefinition: columnDefinition,
|
||||||
isFirstColumn: isFirstColumn,
|
|
||||||
isLastColumn: isLastColumn,
|
|
||||||
recordCount: recordIds.length,
|
recordCount: recordIds.length,
|
||||||
recordIds,
|
recordIds,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
|
||||||
type RecordBoardColumnContextProps = {
|
type RecordBoardColumnContextProps = {
|
||||||
columnDefinition: RecordBoardColumnDefinition;
|
columnDefinition: RecordGroupDefinition;
|
||||||
isFirstColumn: boolean;
|
|
||||||
isLastColumn: boolean;
|
|
||||||
recordCount: number;
|
recordCount: number;
|
||||||
columnId: string;
|
columnId: string;
|
||||||
recordIds: string[];
|
recordIds: string[];
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
|
||||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
|
||||||
type RecordBoardScopeProps = {
|
type RecordBoardScopeProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
recordBoardScopeId: string;
|
recordBoardScopeId: string;
|
||||||
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
||||||
onColumnsChange: (column: RecordBoardColumnDefinition[]) => void;
|
onColumnsChange: (column: RecordGroupDefinition[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
|
||||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
|
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
|
||||||
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
|
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
|
||||||
|
|
||||||
type RecordBoardScopeInternalContextProps = RecoilComponentStateKey & {
|
type RecordBoardScopeInternalContextProps = RecoilComponentStateKey & {
|
||||||
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
onFieldsChange: (fields: FieldDefinition<FieldMetadata>[]) => void;
|
||||||
onColumnsChange: (column: RecordBoardColumnDefinition[]) => void;
|
onColumnsChange: (column: RecordGroupDefinition[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardScopeInternalContext =
|
export const RecordBoardScopeInternalContext =
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
|
||||||
|
|
||||||
export const isFirstRecordBoardColumnComponentFamilyState =
|
|
||||||
createComponentFamilyState<boolean, string>({
|
|
||||||
key: 'isFirstRecordBoardColumnComponentFamilyState',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
|
||||||
|
|
||||||
export const isLastRecordBoardColumnComponentFamilyState =
|
|
||||||
createComponentFamilyState<boolean, string>({
|
|
||||||
key: 'isLastRecordBoardColumnComponentFamilyState',
|
|
||||||
defaultValue: false,
|
|
||||||
});
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||||
|
|
||||||
export const recordBoardColumnsComponentFamilyState =
|
export const recordBoardColumnsComponentFamilyState =
|
||||||
createComponentFamilyState<RecordBoardColumnDefinition | undefined, string>({
|
createComponentFamilyState<RecordGroupDefinition | undefined, string>({
|
||||||
key: 'recordBoardColumnsComponentFamilyState',
|
key: 'recordBoardColumnsComponentFamilyState',
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import { isUndefined } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { isFirstRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isFirstRecordBoardColumnComponentFamilyState';
|
|
||||||
import { isLastRecordBoardColumnComponentFamilyState } from '@/object-record/record-board/states/isLastRecordBoardColumnComponentFamilyState';
|
|
||||||
import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState';
|
|
||||||
import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState';
|
import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState';
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
|
||||||
import { guardRecoilDefaultValue } from '@/ui/utilities/recoil-scope/utils/guardRecoilDefaultValue';
|
|
||||||
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
|
import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
|
||||||
export const recordBoardColumnsComponentFamilySelector =
|
export const recordBoardColumnsComponentFamilySelector =
|
||||||
createComponentFamilySelector<
|
createComponentFamilySelector<RecordGroupDefinition | undefined, string>({
|
||||||
RecordBoardColumnDefinition | undefined,
|
|
||||||
string
|
|
||||||
>({
|
|
||||||
key: 'recordBoardColumnsComponentFamilySelector',
|
key: 'recordBoardColumnsComponentFamilySelector',
|
||||||
get:
|
get:
|
||||||
({
|
({
|
||||||
@ -39,7 +29,7 @@ export const recordBoardColumnsComponentFamilySelector =
|
|||||||
scopeId: string;
|
scopeId: string;
|
||||||
familyKey: string;
|
familyKey: string;
|
||||||
}) =>
|
}) =>
|
||||||
({ set, get }, newColumn) => {
|
({ set }, newColumn) => {
|
||||||
set(
|
set(
|
||||||
recordBoardColumnsComponentFamilyState({
|
recordBoardColumnsComponentFamilyState({
|
||||||
scopeId,
|
scopeId,
|
||||||
@ -47,72 +37,5 @@ export const recordBoardColumnsComponentFamilySelector =
|
|||||||
}),
|
}),
|
||||||
newColumn,
|
newColumn,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (guardRecoilDefaultValue(newColumn)) return;
|
|
||||||
|
|
||||||
const columnIds = get(recordBoardColumnIdsComponentState({ scopeId }));
|
|
||||||
|
|
||||||
const columns = columnIds
|
|
||||||
.map((columnId) => {
|
|
||||||
return get(
|
|
||||||
recordBoardColumnsComponentFamilyState({
|
|
||||||
scopeId,
|
|
||||||
familyKey: columnId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter(isDefined);
|
|
||||||
|
|
||||||
const lastColumn = [...columns].sort(
|
|
||||||
(a, b) => b.position - a.position,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
const firstColumn = [...columns].sort(
|
|
||||||
(a, b) => a.position - b.position,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (!newColumn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastColumn || newColumn.position > lastColumn.position) {
|
|
||||||
set(
|
|
||||||
isLastRecordBoardColumnComponentFamilyState({
|
|
||||||
scopeId,
|
|
||||||
familyKey: columnId,
|
|
||||||
}),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUndefined(lastColumn)) {
|
|
||||||
set(
|
|
||||||
isLastRecordBoardColumnComponentFamilyState({
|
|
||||||
scopeId,
|
|
||||||
familyKey: lastColumn.id,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstColumn || newColumn.position < firstColumn.position) {
|
|
||||||
set(
|
|
||||||
isFirstRecordBoardColumnComponentFamilyState({
|
|
||||||
scopeId,
|
|
||||||
familyKey: columnId,
|
|
||||||
}),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUndefined(firstColumn)) {
|
|
||||||
set(
|
|
||||||
isFirstRecordBoardColumnComponentFamilyState({
|
|
||||||
scopeId,
|
|
||||||
familyKey: firstColumn.id,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
import { ThemeColor } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { RecordBoardColumnAction } from '@/object-record/record-board/types/RecordBoardColumnAction';
|
|
||||||
|
|
||||||
export const enum RecordBoardColumnDefinitionType {
|
|
||||||
Value = 'value',
|
|
||||||
NoValue = 'no-value',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RecordBoardColumnDefinitionNoValue = {
|
|
||||||
id: 'no-value';
|
|
||||||
type: RecordBoardColumnDefinitionType.NoValue;
|
|
||||||
title: 'No Value';
|
|
||||||
position: number;
|
|
||||||
value: null;
|
|
||||||
actions: RecordBoardColumnAction[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordBoardColumnDefinitionValue = {
|
|
||||||
id: string;
|
|
||||||
type: RecordBoardColumnDefinitionType.Value;
|
|
||||||
title: string;
|
|
||||||
value: string;
|
|
||||||
color: ThemeColor;
|
|
||||||
position: number;
|
|
||||||
actions: RecordBoardColumnAction[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecordBoardColumnDefinition =
|
|
||||||
| RecordBoardColumnDefinitionValue
|
|
||||||
| RecordBoardColumnDefinitionNoValue;
|
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
|
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||||
|
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||||
|
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||||
|
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useRecordGroupActions = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { objectNameSingular, recordIndexId } = useContext(
|
||||||
|
RecordIndexRootPropsContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { columnDefinition: recordGroupDefinition } = useContext(
|
||||||
|
RecordBoardColumnContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { viewGroupFieldMetadataItem } = useRecordGroups({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||||
|
useRecordGroupVisibility({
|
||||||
|
viewBarId: recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||||
|
navigationMemorizedUrlState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const navigateToSelectSettings = useCallback(() => {
|
||||||
|
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||||
|
|
||||||
|
if (!isDefined(viewGroupFieldMetadataItem)) {
|
||||||
|
throw new Error('viewGroupFieldMetadataItem is not a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(viewGroupFieldMetadataItem)}`;
|
||||||
|
|
||||||
|
navigate(settingsPath);
|
||||||
|
}, [
|
||||||
|
setNavigationMemorizedUrl,
|
||||||
|
location.pathname,
|
||||||
|
location.search,
|
||||||
|
navigate,
|
||||||
|
objectMetadataItem,
|
||||||
|
viewGroupFieldMetadataItem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const recordGroupActions: RecordGroupAction[] = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
label: 'Edit',
|
||||||
|
icon: IconSettings,
|
||||||
|
position: 0,
|
||||||
|
callback: () => {
|
||||||
|
navigateToSelectSettings();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordGroupDefinition.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? {
|
||||||
|
id: 'hide',
|
||||||
|
label: 'Hide',
|
||||||
|
icon: IconEyeOff,
|
||||||
|
position: 1,
|
||||||
|
callback: () => {
|
||||||
|
handleRecordGroupVisibilityChange(recordGroupDefinition);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
].filter(isDefined),
|
||||||
|
[
|
||||||
|
handleRecordGroupVisibilityChange,
|
||||||
|
navigateToSelectSettings,
|
||||||
|
recordGroupDefinition,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return recordGroupActions;
|
||||||
|
};
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
|
||||||
|
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||||
|
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
type UseRecordGroupHandlersParams = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
viewBarId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecordGroupReorder = ({
|
||||||
|
objectNameSingular,
|
||||||
|
viewBarId,
|
||||||
|
}: UseRecordGroupHandlersParams) => {
|
||||||
|
const setRecordGroupDefinitions = useSetRecoilComponentStateV2(
|
||||||
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { visibleRecordGroups } = useRecordGroups({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
||||||
|
|
||||||
|
const handleOrderChange: OnDragEndResponder = useCallback(
|
||||||
|
(result) => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reorderedVisibleBoardGroups = moveArrayItem(visibleRecordGroups, {
|
||||||
|
fromIndex: result.source.index - 1,
|
||||||
|
toIndex: result.destination.index - 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDeeplyEqual(visibleRecordGroups, reorderedVisibleBoardGroups))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const updatedGroups = [...reorderedVisibleBoardGroups].map(
|
||||||
|
(group, index) => ({ ...group, position: index }),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecordGroupDefinitions(updatedGroups);
|
||||||
|
saveViewGroups(mapRecordGroupDefinitionsToViewGroups(updatedGroups));
|
||||||
|
},
|
||||||
|
[saveViewGroups, setRecordGroupDefinitions, visibleRecordGroups],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibleRecordGroups,
|
||||||
|
handleOrderChange,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
|
import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups';
|
||||||
|
import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups';
|
||||||
|
|
||||||
|
type UseRecordGroupVisibilityParams = {
|
||||||
|
viewBarId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecordGroupVisibility = ({
|
||||||
|
viewBarId,
|
||||||
|
}: UseRecordGroupVisibilityParams) => {
|
||||||
|
const [recordGroupDefinitions, setRecordGroupDefinitions] =
|
||||||
|
useRecoilComponentStateV2(recordGroupDefinitionsComponentState);
|
||||||
|
|
||||||
|
const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId);
|
||||||
|
|
||||||
|
const handleVisibilityChange = useCallback(
|
||||||
|
async (updatedRecordGroupDefinition: RecordGroupDefinition) => {
|
||||||
|
const updatedRecordGroupDefinitions = recordGroupDefinitions.map(
|
||||||
|
(groupDefinition) =>
|
||||||
|
groupDefinition.id === updatedRecordGroupDefinition.id
|
||||||
|
? {
|
||||||
|
...groupDefinition,
|
||||||
|
isVisible: !groupDefinition.isVisible,
|
||||||
|
}
|
||||||
|
: groupDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
|
setRecordGroupDefinitions(updatedRecordGroupDefinitions);
|
||||||
|
|
||||||
|
saveViewGroups(
|
||||||
|
mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleVisibilityChange,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
|
type UseRecordGroupsParams = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecordGroups = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: UseRecordGroupsParams) => {
|
||||||
|
const recordGroupDefinitions = useRecoilComponentValueV2(
|
||||||
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewGroupFieldMetadataItem = useMemo(() => {
|
||||||
|
if (recordGroupDefinitions.length === 0) return null;
|
||||||
|
// We're assuming that all groups have the same fieldMetadataId for now
|
||||||
|
const fieldMetadataId =
|
||||||
|
'fieldMetadataId' in recordGroupDefinitions[0]
|
||||||
|
? recordGroupDefinitions[0].fieldMetadataId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!fieldMetadataId) return null;
|
||||||
|
|
||||||
|
return objectMetadataItem.fields.find(
|
||||||
|
(field) => field.id === fieldMetadataId,
|
||||||
|
);
|
||||||
|
}, [objectMetadataItem, recordGroupDefinitions]);
|
||||||
|
|
||||||
|
const visibleRecordGroups = useMemo(
|
||||||
|
() =>
|
||||||
|
recordGroupDefinitions
|
||||||
|
.filter((boardGroup) => boardGroup.isVisible)
|
||||||
|
.sort(
|
||||||
|
(boardGroupA, boardGroupB) =>
|
||||||
|
boardGroupA.position - boardGroupB.position,
|
||||||
|
),
|
||||||
|
[recordGroupDefinitions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hiddenRecordGroups = useMemo(
|
||||||
|
() => recordGroupDefinitions.filter((boardGroup) => !boardGroup.isVisible),
|
||||||
|
[recordGroupDefinitions],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenRecordGroups,
|
||||||
|
visibleRecordGroups,
|
||||||
|
viewGroupFieldMetadataItem,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
|
|
||||||
|
export const recordGroupDefinitionsComponentState = createComponentStateV2<
|
||||||
|
RecordGroupDefinition[]
|
||||||
|
>({
|
||||||
|
key: 'recordGroupDefinitionsComponentState',
|
||||||
|
defaultValue: [],
|
||||||
|
componentInstanceContext: ViewComponentInstanceContext,
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
export type RecordBoardColumnAction = {
|
export type RecordGroupAction = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: IconComponent;
|
icon: IconComponent;
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { ThemeColor } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const enum RecordGroupDefinitionType {
|
||||||
|
Value = 'value',
|
||||||
|
NoValue = 'no-value',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecordGroupDefinition = {
|
||||||
|
id: string;
|
||||||
|
fieldMetadataId: string;
|
||||||
|
type: RecordGroupDefinitionType;
|
||||||
|
title: string;
|
||||||
|
value: string | null;
|
||||||
|
color: ThemeColor | 'transparent';
|
||||||
|
position: number;
|
||||||
|
isVisible: boolean;
|
||||||
|
};
|
||||||
@ -9,14 +9,12 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get
|
|||||||
|
|
||||||
export const RecordIndexBoardColumnLoaderEffect = ({
|
export const RecordIndexBoardColumnLoaderEffect = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
boardFieldSelectValue,
|
|
||||||
boardFieldMetadataId,
|
boardFieldMetadataId,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
columnId,
|
columnId,
|
||||||
}: {
|
}: {
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
boardFieldSelectValue: string | null;
|
|
||||||
boardFieldMetadataId: string | null;
|
boardFieldMetadataId: string | null;
|
||||||
columnId: string;
|
columnId: string;
|
||||||
}) => {
|
}) => {
|
||||||
@ -40,7 +38,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
boardFieldMetadataId,
|
boardFieldMetadataId,
|
||||||
columnFieldSelectValue: boardFieldSelectValue,
|
|
||||||
columnId,
|
columnId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,7 +67,6 @@ export const RecordIndexBoardColumnLoaderEffect = ({
|
|||||||
fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
loading,
|
loading,
|
||||||
shouldFetchMore,
|
shouldFetchMore,
|
||||||
boardFieldSelectValue,
|
|
||||||
setLoadingRecordsForThisColumn,
|
setLoadingRecordsForThisColumn,
|
||||||
loadingRecordsForThisColumn,
|
loadingRecordsForThisColumn,
|
||||||
|
|
||||||
|
|||||||
@ -26,23 +26,18 @@ export const RecordIndexBoardDataLoader = ({
|
|||||||
(field) => field.id === recordIndexKanbanFieldMetadataId,
|
(field) => field.id === recordIndexKanbanFieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const possibleKanbanSelectFieldValues =
|
|
||||||
recordIndexKanbanFieldMetadataItem?.options ?? [];
|
|
||||||
|
|
||||||
const { columnIdsState } = useRecordBoardStates(recordBoardId);
|
const { columnIdsState } = useRecordBoardStates(recordBoardId);
|
||||||
|
|
||||||
// TODO: we should make sure there's no way to have a mismatch between columnIds and possibleKanbanSelectFieldValues order
|
|
||||||
const columnIds = useRecoilValue(columnIdsState);
|
const columnIds = useRecoilValue(columnIdsState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{possibleKanbanSelectFieldValues.map((option, index) => (
|
{columnIds.map((columnId, index) => (
|
||||||
<RecordIndexBoardColumnLoaderEffect
|
<RecordIndexBoardColumnLoaderEffect
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
||||||
boardFieldSelectValue={option.value}
|
|
||||||
recordBoardId={recordBoardId}
|
recordBoardId={recordBoardId}
|
||||||
columnId={columnIds[index]}
|
columnId={columnId}
|
||||||
key={index}
|
key={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -50,7 +45,6 @@ export const RecordIndexBoardDataLoader = ({
|
|||||||
<RecordIndexBoardColumnLoaderEffect
|
<RecordIndexBoardColumnLoaderEffect
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
boardFieldMetadataId={recordIndexKanbanFieldMetadataId}
|
||||||
boardFieldSelectValue={null}
|
|
||||||
recordBoardId={recordBoardId}
|
recordBoardId={recordBoardId}
|
||||||
columnId={'no-value'}
|
columnId={'no-value'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
|
||||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -32,6 +30,10 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
|||||||
recordIndexFieldDefinitionsState,
|
recordIndexFieldDefinitionsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||||
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const recordIndexKanbanFieldMetadataId = useRecoilValue(
|
const recordIndexKanbanFieldMetadataId = useRecoilValue(
|
||||||
recordIndexKanbanFieldMetadataIdState,
|
recordIndexKanbanFieldMetadataIdState,
|
||||||
);
|
);
|
||||||
@ -60,43 +62,17 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
|||||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
|
||||||
navigationMemorizedUrlState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateToSelectSettings = useCallback(() => {
|
|
||||||
setNavigationMemorizedUrl(location.pathname + location.search);
|
|
||||||
navigate(`/settings/objects/${getObjectSlug(objectMetadataItem)}`);
|
|
||||||
}, [
|
|
||||||
navigate,
|
|
||||||
objectMetadataItem,
|
|
||||||
location.pathname,
|
|
||||||
location.search,
|
|
||||||
setNavigationMemorizedUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setObjectSingularName(objectNameSingular);
|
setObjectSingularName(objectNameSingular);
|
||||||
}, [objectNameSingular, setObjectSingularName]);
|
}, [objectNameSingular, setObjectSingularName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns(
|
setColumns(recordIndexGroupDefinitions);
|
||||||
computeRecordBoardColumnDefinitionsFromObjectMetadata(
|
}, [recordIndexGroupDefinitions, setColumns]);
|
||||||
objectMetadataItem,
|
|
||||||
recordIndexKanbanFieldMetadataId ?? '',
|
|
||||||
navigateToSelectSettings,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
navigateToSelectSettings,
|
|
||||||
objectMetadataItem,
|
|
||||||
objectNameSingular,
|
|
||||||
recordIndexKanbanFieldMetadataId,
|
|
||||||
setColumns,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// TODO: Remove this duplicate useEffect by ensuring it's not here because
|
||||||
|
// We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern
|
||||||
|
// As it is an unnecessary dependency
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||||
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
|
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
|
||||||
|
|||||||
@ -24,13 +24,17 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone
|
|||||||
|
|
||||||
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
|
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
|
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { ViewBar } from '@/views/components/ViewBar';
|
import { ViewBar } from '@/views/components/ViewBar';
|
||||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
|
||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
|
||||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
|
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
@ -61,6 +65,10 @@ export const RecordIndexContainer = () => {
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
} = useContext(RecordIndexRootPropsContext);
|
} = useContext(RecordIndexRootPropsContext);
|
||||||
|
|
||||||
|
const recordGroupDefinitionsCallbackState = useRecoilComponentCallbackStateV2(
|
||||||
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
const { columnDefinitions, filterDefinitions, sortDefinitions } =
|
||||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||||
|
|
||||||
@ -77,6 +85,8 @@ export const RecordIndexContainer = () => {
|
|||||||
recordTableId: recordIndexId,
|
recordTableId: recordIndexId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setColumns } = useRecordBoard(recordIndexId);
|
||||||
|
|
||||||
const onViewFieldsChange = useRecoilCallback(
|
const onViewFieldsChange = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(viewFields: ViewField[]) => {
|
(viewFields: ViewField[]) => {
|
||||||
@ -103,6 +113,32 @@ export const RecordIndexContainer = () => {
|
|||||||
[columnDefinitions, setTableColumns],
|
[columnDefinitions, setTableColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onViewGroupsChange = useRecoilCallback(
|
||||||
|
({ set, snapshot }) =>
|
||||||
|
(viewGroups: ViewGroup[]) => {
|
||||||
|
const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({
|
||||||
|
objectMetadataItem,
|
||||||
|
viewGroups,
|
||||||
|
});
|
||||||
|
|
||||||
|
setColumns(newGroupDefinitions);
|
||||||
|
|
||||||
|
const existingRecordIndexGroupDefinitions = snapshot
|
||||||
|
.getLoadable(recordGroupDefinitionsCallbackState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDeeplyEqual(
|
||||||
|
existingRecordIndexGroupDefinitions,
|
||||||
|
newGroupDefinitions,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
set(recordGroupDefinitionsCallbackState, newGroupDefinitions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[objectMetadataItem, recordGroupDefinitionsCallbackState, setColumns],
|
||||||
|
);
|
||||||
|
|
||||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
@ -110,86 +146,83 @@ export const RecordIndexContainer = () => {
|
|||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<InformationBannerWrapper />
|
<InformationBannerWrapper />
|
||||||
<ViewComponentInstanceContext.Provider
|
<RecordFieldValueSelectorContextProvider>
|
||||||
value={{ instanceId: recordIndexId }}
|
<SpreadsheetImportProvider>
|
||||||
>
|
<ViewBar
|
||||||
<RecordFieldValueSelectorContextProvider>
|
viewBarId={recordIndexId}
|
||||||
<SpreadsheetImportProvider>
|
optionsDropdownButton={
|
||||||
<ViewBar
|
<RecordIndexOptionsDropdown
|
||||||
viewBarId={recordIndexId}
|
recordIndexId={recordIndexId}
|
||||||
optionsDropdownButton={
|
objectMetadataItem={objectMetadataItem}
|
||||||
<RecordIndexOptionsDropdown
|
viewType={recordIndexViewType ?? ViewType.Table}
|
||||||
recordIndexId={recordIndexId}
|
/>
|
||||||
objectMetadataItem={objectMetadataItem}
|
}
|
||||||
viewType={recordIndexViewType ?? ViewType.Table}
|
onCurrentViewChange={(view) => {
|
||||||
/>
|
if (!view) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
onCurrentViewChange={(view) => {
|
|
||||||
if (!view) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewFieldsChange(view.viewFields);
|
onViewFieldsChange(view.viewFields);
|
||||||
setTableFilters(
|
onViewGroupsChange(view.viewGroups);
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
setTableFilters(
|
||||||
);
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
setRecordIndexFilters(
|
);
|
||||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
setRecordIndexFilters(
|
||||||
);
|
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||||
setContextStoreTargetedRecordsRule((prev) => ({
|
);
|
||||||
...prev,
|
setContextStoreTargetedRecordsRule((prev) => ({
|
||||||
filters: mapViewFiltersToFilters(
|
...prev,
|
||||||
view.viewFilters,
|
filters: mapViewFiltersToFilters(
|
||||||
filterDefinitions,
|
view.viewFilters,
|
||||||
),
|
filterDefinitions,
|
||||||
}));
|
),
|
||||||
setTableSorts(
|
}));
|
||||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
setTableSorts(
|
||||||
);
|
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||||
setRecordIndexSorts(
|
);
|
||||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
setRecordIndexSorts(
|
||||||
);
|
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||||
setRecordIndexViewType(view.type);
|
);
|
||||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
setRecordIndexViewType(view.type);
|
||||||
view.kanbanFieldMetadataId,
|
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||||
);
|
view.kanbanFieldMetadataId,
|
||||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
);
|
||||||
}}
|
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||||
/>
|
}}
|
||||||
<RecordIndexViewBarEffect
|
/>
|
||||||
objectNamePlural={objectNamePlural}
|
<RecordIndexViewBarEffect
|
||||||
|
objectNamePlural={objectNamePlural}
|
||||||
|
viewBarId={recordIndexId}
|
||||||
|
/>
|
||||||
|
</SpreadsheetImportProvider>
|
||||||
|
{recordIndexViewType === ViewType.Table && (
|
||||||
|
<>
|
||||||
|
<RecordIndexTableContainer
|
||||||
|
recordTableId={recordIndexId}
|
||||||
viewBarId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
/>
|
/>
|
||||||
</SpreadsheetImportProvider>
|
<RecordIndexTableContainerEffect />
|
||||||
{recordIndexViewType === ViewType.Table && (
|
</>
|
||||||
<>
|
)}
|
||||||
<RecordIndexTableContainer
|
{recordIndexViewType === ViewType.Kanban && (
|
||||||
recordTableId={recordIndexId}
|
<StyledContainerWithPadding>
|
||||||
viewBarId={recordIndexId}
|
<RecordIndexBoardContainer
|
||||||
/>
|
recordBoardId={recordIndexId}
|
||||||
<RecordIndexTableContainerEffect />
|
viewBarId={recordIndexId}
|
||||||
</>
|
objectNameSingular={objectNameSingular}
|
||||||
)}
|
/>
|
||||||
{recordIndexViewType === ViewType.Kanban && (
|
<RecordIndexBoardDataLoader
|
||||||
<StyledContainerWithPadding>
|
objectNameSingular={objectNameSingular}
|
||||||
<RecordIndexBoardContainer
|
recordBoardId={recordIndexId}
|
||||||
recordBoardId={recordIndexId}
|
/>
|
||||||
viewBarId={recordIndexId}
|
<RecordIndexBoardDataLoaderEffect
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
/>
|
recordBoardId={recordIndexId}
|
||||||
<RecordIndexBoardDataLoader
|
/>
|
||||||
objectNameSingular={objectNameSingular}
|
</StyledContainerWithPadding>
|
||||||
recordBoardId={recordIndexId}
|
)}
|
||||||
/>
|
<RecordIndexActionMenu actionMenuId={recordIndexId} />
|
||||||
<RecordIndexBoardDataLoaderEffect
|
</RecordFieldValueSelectorContextProvider>
|
||||||
objectNameSingular={objectNameSingular}
|
|
||||||
recordBoardId={recordIndexId}
|
|
||||||
/>
|
|
||||||
</StyledContainerWithPadding>
|
|
||||||
)}
|
|
||||||
<RecordIndexActionMenu actionMenuId={recordIndexId} />
|
|
||||||
</RecordFieldValueSelectorContextProvider>
|
|
||||||
</ViewComponentInstanceContext.Provider>
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
||||||
import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
|
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
|
||||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||||
@ -58,7 +58,7 @@ export const RecordIndexPageKanbanAddButton = () => {
|
|||||||
const { handleAddNewCardClick } = useAddNewCard();
|
const { handleAddNewCardClick } = useAddNewCard();
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(columnDefinition: RecordBoardColumnDefinition) => {
|
(columnDefinition: RecordGroupDefinition) => {
|
||||||
const isOpportunityEnabled =
|
const isOpportunityEnabled =
|
||||||
isOpportunity && !isOpportunitiesCompanyFieldDisabled;
|
isOpportunity && !isOpportunitiesCompanyFieldDisabled;
|
||||||
handleAddNewCardClick(
|
handleAddNewCardClick(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem';
|
import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -32,18 +32,18 @@ export const RecordIndexPageKanbanAddMenuItem = ({
|
|||||||
text={
|
text={
|
||||||
<Tag
|
<Tag
|
||||||
variant={
|
variant={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? 'solid'
|
? 'solid'
|
||||||
: 'outline'
|
: 'outline'
|
||||||
}
|
}
|
||||||
color={
|
color={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? columnDefinition.color
|
? columnDefinition.color
|
||||||
: 'transparent'
|
: 'transparent'
|
||||||
}
|
}
|
||||||
text={columnDefinition.title}
|
text={columnDefinition.title}
|
||||||
weight={
|
weight={
|
||||||
columnDefinition.type === RecordBoardColumnDefinitionType.Value
|
columnDefinition.type === RecordGroupDefinitionType.Value
|
||||||
? 'regular'
|
? 'regular'
|
||||||
: 'medium'
|
: 'medium'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
|
||||||
|
import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState';
|
||||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||||
|
|
||||||
type UseLoadRecordIndexBoardProps = {
|
type UseLoadRecordIndexBoardProps = {
|
||||||
@ -31,6 +33,7 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
const {
|
const {
|
||||||
setRecordIds: setRecordIdsInBoard,
|
setRecordIds: setRecordIdsInBoard,
|
||||||
setFieldDefinitions,
|
setFieldDefinitions,
|
||||||
|
setColumns,
|
||||||
isCompactModeActiveState,
|
isCompactModeActiveState,
|
||||||
} = useRecordBoard(recordBoardId);
|
} = useRecordBoard(recordBoardId);
|
||||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
@ -42,6 +45,13 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
setFieldDefinitions(recordIndexFieldDefinitions);
|
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||||
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
}, [recordIndexFieldDefinitions, setFieldDefinitions]);
|
||||||
|
|
||||||
|
const recordIndexGroupDefinitions = useRecoilComponentValueV2(
|
||||||
|
recordGroupDefinitionsComponentState,
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
setColumns(recordIndexGroupDefinitions);
|
||||||
|
}, [recordIndexGroupDefinitions, setColumns]);
|
||||||
|
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
const requestFilters = turnFiltersIntoQueryFilter(
|
const requestFilters = turnFiltersIntoQueryFilter(
|
||||||
|
|||||||
@ -11,12 +11,12 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec
|
|||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
|
|
||||||
type UseLoadRecordIndexBoardProps = {
|
type UseLoadRecordIndexBoardProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
boardFieldMetadataId: string | null;
|
boardFieldMetadataId: string | null;
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
columnFieldSelectValue: string | null;
|
|
||||||
columnId: string;
|
columnId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,17 +24,18 @@ export const useLoadRecordIndexBoardColumn = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
boardFieldMetadataId,
|
boardFieldMetadataId,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
columnFieldSelectValue,
|
|
||||||
columnId,
|
columnId,
|
||||||
}: UseLoadRecordIndexBoardProps) => {
|
}: UseLoadRecordIndexBoardProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
||||||
|
const { columnsFamilySelector } = useRecordBoardStates(recordBoardId);
|
||||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
|
const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
|
||||||
const requestFilters = turnFiltersIntoQueryFilter(
|
const requestFilters = turnFiltersIntoQueryFilter(
|
||||||
recordIndexFilters,
|
recordIndexFilters,
|
||||||
objectMetadataItem?.fields ?? [],
|
objectMetadataItem?.fields ?? [],
|
||||||
@ -53,9 +54,9 @@ export const useLoadRecordIndexBoardColumn = ({
|
|||||||
const filter = {
|
const filter = {
|
||||||
...requestFilters,
|
...requestFilters,
|
||||||
[recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined(
|
[recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined(
|
||||||
columnFieldSelectValue,
|
columnDefinition?.value,
|
||||||
)
|
)
|
||||||
? { in: [columnFieldSelectValue] }
|
? { in: [columnDefinition?.value] }
|
||||||
: { is: 'NULL' },
|
: { is: 'NULL' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import {
|
import {
|
||||||
IconBaselineDensitySmall,
|
IconBaselineDensitySmall,
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
IconTag,
|
IconTag,
|
||||||
UndecoratedLink,
|
UndecoratedLink,
|
||||||
|
useIcons,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||||
@ -21,6 +22,9 @@ import {
|
|||||||
useExportRecordData,
|
useExportRecordData,
|
||||||
} from '@/action-menu/hooks/useExportRecordData';
|
} from '@/action-menu/hooks/useExportRecordData';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder';
|
||||||
|
import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups';
|
||||||
|
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
|
||||||
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
|
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
|
||||||
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
|
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
|
||||||
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
|
||||||
@ -37,12 +41,17 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog
|
|||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
|
||||||
|
import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection';
|
||||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
|
type RecordIndexOptionsMenu =
|
||||||
|
| 'viewGroups'
|
||||||
|
| 'hiddenViewGroups'
|
||||||
|
| 'fields'
|
||||||
|
| 'hiddenFields';
|
||||||
|
|
||||||
type RecordIndexOptionsDropdownContentProps = {
|
type RecordIndexOptionsDropdownContentProps = {
|
||||||
recordIndexId: string;
|
recordIndexId: string;
|
||||||
@ -50,6 +59,7 @@ type RecordIndexOptionsDropdownContentProps = {
|
|||||||
viewType: ViewType;
|
viewType: ViewType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Break this component down
|
||||||
export const RecordIndexOptionsDropdownContent = ({
|
export const RecordIndexOptionsDropdownContent = ({
|
||||||
viewType,
|
viewType,
|
||||||
recordIndexId,
|
recordIndexId,
|
||||||
@ -57,6 +67,8 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
}: RecordIndexOptionsDropdownContentProps) => {
|
}: RecordIndexOptionsDropdownContentProps) => {
|
||||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||||
|
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
|
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
|
||||||
|
|
||||||
const [currentMenu, setCurrentMenu] = useState<
|
const [currentMenu, setCurrentMenu] = useState<
|
||||||
@ -111,6 +123,28 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
viewBarId: recordIndexId,
|
viewBarId: recordIndexId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
hiddenRecordGroups,
|
||||||
|
visibleRecordGroups,
|
||||||
|
viewGroupFieldMetadataItem,
|
||||||
|
} = useRecordGroups({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
});
|
||||||
|
const { handleVisibilityChange: handleRecordGroupVisibilityChange } =
|
||||||
|
useRecordGroupVisibility({
|
||||||
|
viewBarId: recordIndexId,
|
||||||
|
});
|
||||||
|
const { handleOrderChange: handleRecordGroupOrderChange } =
|
||||||
|
useRecordGroupReorder({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
viewBarId: recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewGroupSettingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
|
||||||
|
id: viewGroupFieldMetadataItem?.name,
|
||||||
|
objectSlug: objectNamePlural,
|
||||||
|
});
|
||||||
|
|
||||||
const visibleRecordFields =
|
const visibleRecordFields =
|
||||||
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
|
viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns;
|
||||||
|
|
||||||
@ -143,10 +177,28 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
navigationMemorizedUrlState,
|
navigationMemorizedUrlState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isViewGroupMenuItemVisible =
|
||||||
|
viewGroupFieldMetadataItem &&
|
||||||
|
(visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) {
|
||||||
|
setCurrentMenu('viewGroups');
|
||||||
|
}
|
||||||
|
}, [hiddenRecordGroups, currentMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!currentMenu && (
|
{!currentMenu && (
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
|
{isViewGroupMenuItemVisible && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => handleSelectMenu('viewGroups')}
|
||||||
|
LeftIcon={getIcon(currentViewWithCombinedFiltersAndSorts?.icon)}
|
||||||
|
text={viewGroupFieldMetadataItem.label}
|
||||||
|
hasSubMenu
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => handleSelectMenu('fields')}
|
onClick={() => handleSelectMenu('fields')}
|
||||||
LeftIcon={IconTag}
|
LeftIcon={IconTag}
|
||||||
@ -174,6 +226,34 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
/>
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
)}
|
)}
|
||||||
|
{currentMenu === 'viewGroups' && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||||
|
{viewGroupFieldMetadataItem?.label}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<ViewGroupsVisibilityDropdownSection
|
||||||
|
title={viewGroupFieldMetadataItem?.label ?? ''}
|
||||||
|
viewGroups={visibleRecordGroups}
|
||||||
|
onDragEnd={handleRecordGroupOrderChange}
|
||||||
|
onVisibilityChange={handleRecordGroupVisibilityChange}
|
||||||
|
isDraggable
|
||||||
|
showSubheader={false}
|
||||||
|
showDragGrip={true}
|
||||||
|
/>
|
||||||
|
{hiddenRecordGroups.length > 0 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItemNavigate
|
||||||
|
onClick={() => handleSelectMenu('hiddenViewGroups')}
|
||||||
|
LeftIcon={IconEyeOff}
|
||||||
|
text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`}
|
||||||
|
/>
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{currentMenu === 'fields' && (
|
{currentMenu === 'fields' && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||||
@ -198,6 +278,36 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{currentMenu === 'hiddenViewGroups' && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuHeader
|
||||||
|
StartIcon={IconChevronLeft}
|
||||||
|
onClick={() => setCurrentMenu('viewGroups')}
|
||||||
|
>
|
||||||
|
Hidden {viewGroupFieldMetadataItem?.label}
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<ViewGroupsVisibilityDropdownSection
|
||||||
|
title={`Hidden ${viewGroupFieldMetadataItem?.label}`}
|
||||||
|
viewGroups={hiddenRecordGroups}
|
||||||
|
onVisibilityChange={handleRecordGroupVisibilityChange}
|
||||||
|
isDraggable={false}
|
||||||
|
showSubheader={false}
|
||||||
|
showDragGrip={false}
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<UndecoratedLink
|
||||||
|
to={viewGroupSettingsUrl}
|
||||||
|
onClick={() => {
|
||||||
|
setNavigationMemorizedUrl(location.pathname + location.search);
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
<MenuItem LeftIcon={IconSettings} text="Edit field values" />
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</UndecoratedLink>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{currentMenu === 'hiddenFields' && (
|
{currentMenu === 'hiddenFields' && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader
|
<DropdownMenuHeader
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import { expect } from '@storybook/test';
|
|
||||||
|
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
|
||||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '../computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
|
||||||
|
|
||||||
describe('computeRecordBoardColumnDefinitionsFromObjectMetadata', () => {
|
|
||||||
it('should correctly compute', () => {
|
|
||||||
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
|
||||||
(item) => item.nameSingular === 'opportunity',
|
|
||||||
);
|
|
||||||
|
|
||||||
const stageField = objectMetadataItem?.fields.find(
|
|
||||||
(field) => field.name === 'stage',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!objectMetadataItem) {
|
|
||||||
throw new Error('Object metadata item not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = computeRecordBoardColumnDefinitionsFromObjectMetadata(
|
|
||||||
objectMetadataItem,
|
|
||||||
stageField?.id,
|
|
||||||
() => null,
|
|
||||||
);
|
|
||||||
expect(res.length).toEqual(stageField?.options?.length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import {
|
|
||||||
RecordBoardColumnDefinition,
|
|
||||||
RecordBoardColumnDefinitionNoValue,
|
|
||||||
RecordBoardColumnDefinitionType,
|
|
||||||
RecordBoardColumnDefinitionValue,
|
|
||||||
} from '@/object-record/record-board/types/RecordBoardColumnDefinition';
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
|
|
||||||
objectMetadataItem: ObjectMetadataItem,
|
|
||||||
kanbanFieldMetadataId: string,
|
|
||||||
navigateToSelectSettings: () => void,
|
|
||||||
): RecordBoardColumnDefinition[] => {
|
|
||||||
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
|
||||||
(field) =>
|
|
||||||
field.id === kanbanFieldMetadataId &&
|
|
||||||
field.type === FieldMetadataType.Select,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectFieldMetadataItem) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectFieldMetadataItem.options) {
|
|
||||||
throw new Error(
|
|
||||||
`Select Field ${objectMetadataItem.nameSingular} has no options`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueColumns = selectFieldMetadataItem.options.map(
|
|
||||||
(selectOption) =>
|
|
||||||
({
|
|
||||||
id: selectOption.id,
|
|
||||||
type: RecordBoardColumnDefinitionType.Value,
|
|
||||||
title: selectOption.label,
|
|
||||||
value: selectOption.value,
|
|
||||||
color: selectOption.color,
|
|
||||||
position: selectOption.position,
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
id: 'edit',
|
|
||||||
label: 'Edit from Settings',
|
|
||||||
icon: IconSettings,
|
|
||||||
position: 0,
|
|
||||||
callback: navigateToSelectSettings,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}) satisfies RecordBoardColumnDefinitionValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
const noValueColumn = {
|
|
||||||
id: 'no-value',
|
|
||||||
title: 'No Value',
|
|
||||||
type: RecordBoardColumnDefinitionType.NoValue,
|
|
||||||
value: null,
|
|
||||||
actions: [],
|
|
||||||
position:
|
|
||||||
selectFieldMetadataItem.options
|
|
||||||
.map((option) => option.position)
|
|
||||||
.reduce((a, b) => Math.max(a, b), 0) + 1,
|
|
||||||
} satisfies RecordBoardColumnDefinitionNoValue;
|
|
||||||
|
|
||||||
if (selectFieldMetadataItem.isNullable === true) {
|
|
||||||
return [...valueColumns, noValueColumn];
|
|
||||||
}
|
|
||||||
|
|
||||||
return valueColumns;
|
|
||||||
};
|
|
||||||
@ -20,5 +20,6 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF
|
|||||||
viewFilters: true,
|
viewFilters: true,
|
||||||
viewSorts: true,
|
viewSorts: true,
|
||||||
viewFields: true,
|
viewFields: true,
|
||||||
|
viewGroups: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,14 +5,15 @@ import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuI
|
|||||||
import { MenuItemAccent } from '../types/MenuItemAccent';
|
import { MenuItemAccent } from '../types/MenuItemAccent';
|
||||||
|
|
||||||
import { MenuItemIconButton } from './MenuItem';
|
import { MenuItemIconButton } from './MenuItem';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export type MenuItemDraggableProps = {
|
export type MenuItemDraggableProps = {
|
||||||
LeftIcon: IconComponent | undefined;
|
LeftIcon?: IconComponent | undefined;
|
||||||
accent?: MenuItemAccent;
|
accent?: MenuItemAccent;
|
||||||
iconButtons?: MenuItemIconButton[];
|
iconButtons?: MenuItemIconButton[];
|
||||||
isTooltipOpen?: boolean;
|
isTooltipOpen?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
text: string;
|
text: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
isIconDisplayedOnHoverOnly?: boolean;
|
isIconDisplayedOnHoverOnly?: boolean;
|
||||||
showGrip?: boolean;
|
showGrip?: boolean;
|
||||||
|
|||||||
@ -0,0 +1,192 @@
|
|||||||
|
import {
|
||||||
|
DropResult,
|
||||||
|
OnDragEndResponder,
|
||||||
|
ResponderProvided,
|
||||||
|
} from '@hello-pangea/dnd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { IconEye, IconEyeOff, Tag } from 'twenty-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RecordGroupDefinition,
|
||||||
|
RecordGroupDefinitionType,
|
||||||
|
} from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||||
|
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||||
|
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type ViewGroupsVisibilityDropdownSectionProps = {
|
||||||
|
viewGroups: RecordGroupDefinition[];
|
||||||
|
isDraggable: boolean;
|
||||||
|
onDragEnd?: OnDragEndResponder;
|
||||||
|
onVisibilityChange: (viewGroup: RecordGroupDefinition) => void;
|
||||||
|
title: string;
|
||||||
|
showSubheader: boolean;
|
||||||
|
showDragGrip: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewGroupsVisibilityDropdownSection = ({
|
||||||
|
viewGroups,
|
||||||
|
isDraggable,
|
||||||
|
onDragEnd,
|
||||||
|
onVisibilityChange,
|
||||||
|
title,
|
||||||
|
showSubheader = true,
|
||||||
|
showDragGrip,
|
||||||
|
}: ViewGroupsVisibilityDropdownSectionProps) => {
|
||||||
|
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||||
|
onDragEnd?.(result, provided);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIconButtons = (index: number, viewGroup: RecordGroupDefinition) => {
|
||||||
|
const iconButtons = [
|
||||||
|
{
|
||||||
|
Icon: viewGroup.isVisible ? IconEyeOff : IconEye,
|
||||||
|
onClick: () => onVisibilityChange(viewGroup),
|
||||||
|
},
|
||||||
|
].filter(isDefined);
|
||||||
|
|
||||||
|
return iconButtons.length ? iconButtons : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const noValueViewGroups =
|
||||||
|
viewGroups.filter(
|
||||||
|
(viewGroup) => viewGroup.type === RecordGroupDefinitionType.NoValue,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const viewGroupsWithoutNoValueGroups = viewGroups.filter(
|
||||||
|
(viewGroup) => viewGroup.type !== RecordGroupDefinitionType.NoValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
{showSubheader && (
|
||||||
|
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{!!viewGroups.length && (
|
||||||
|
<>
|
||||||
|
{!isDraggable ? (
|
||||||
|
viewGroupsWithoutNoValueGroups.map(
|
||||||
|
(viewGroup, viewGroupIndex) => (
|
||||||
|
<MenuItemDraggable
|
||||||
|
key={viewGroup.id}
|
||||||
|
text={
|
||||||
|
<Tag
|
||||||
|
variant={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? 'solid'
|
||||||
|
: 'outline'
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? viewGroup.color
|
||||||
|
: 'transparent'
|
||||||
|
}
|
||||||
|
text={viewGroup.title}
|
||||||
|
weight={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? 'regular'
|
||||||
|
: 'medium'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
iconButtons={getIconButtons(viewGroupIndex, viewGroup)}
|
||||||
|
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||||
|
showGrip={showDragGrip}
|
||||||
|
isDragDisabled={!isDraggable}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<DraggableList
|
||||||
|
onDragEnd={handleOnDrag}
|
||||||
|
draggableItems={
|
||||||
|
<>
|
||||||
|
{viewGroupsWithoutNoValueGroups.map(
|
||||||
|
(viewGroup, viewGroupIndex) => (
|
||||||
|
<DraggableItem
|
||||||
|
key={viewGroup.id}
|
||||||
|
draggableId={viewGroup.id}
|
||||||
|
index={viewGroupIndex + 1}
|
||||||
|
itemComponent={
|
||||||
|
<MenuItemDraggable
|
||||||
|
key={viewGroup.id}
|
||||||
|
text={
|
||||||
|
<Tag
|
||||||
|
variant={
|
||||||
|
viewGroup.type !==
|
||||||
|
RecordGroupDefinitionType.NoValue
|
||||||
|
? 'solid'
|
||||||
|
: 'outline'
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
viewGroup.type !==
|
||||||
|
RecordGroupDefinitionType.NoValue
|
||||||
|
? viewGroup.color
|
||||||
|
: 'transparent'
|
||||||
|
}
|
||||||
|
text={viewGroup.title}
|
||||||
|
weight={
|
||||||
|
viewGroup.type !==
|
||||||
|
RecordGroupDefinitionType.NoValue
|
||||||
|
? 'regular'
|
||||||
|
: 'medium'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
iconButtons={getIconButtons(
|
||||||
|
viewGroupIndex,
|
||||||
|
viewGroup,
|
||||||
|
)}
|
||||||
|
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||||
|
showGrip={showDragGrip}
|
||||||
|
isDragDisabled={!isDraggable}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{noValueViewGroups.map((viewGroup) => (
|
||||||
|
<MenuItemDraggable
|
||||||
|
key={viewGroup.id}
|
||||||
|
text={
|
||||||
|
<Tag
|
||||||
|
variant={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? 'solid'
|
||||||
|
: 'outline'
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? viewGroup.color
|
||||||
|
: 'transparent'
|
||||||
|
}
|
||||||
|
text={viewGroup.title}
|
||||||
|
weight={
|
||||||
|
viewGroup.type !== RecordGroupDefinitionType.NoValue
|
||||||
|
? 'regular'
|
||||||
|
: 'medium'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
accent={showDragGrip ? 'placeholder' : 'default'}
|
||||||
|
showGrip={true}
|
||||||
|
isDragDisabled={true}
|
||||||
|
isHoverDisabled
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
|
||||||
|
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
|
||||||
|
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
|
|
||||||
|
export const usePersistViewGroupRecords = () => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { createOneRecordMutation } = useCreateOneRecordMutation({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { updateOneRecordMutation } = useUpdateOneRecordMutation({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.ViewGroup,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const createViewGroupRecords = useCallback(
|
||||||
|
(viewGroupsToCreate: ViewGroup[], view: GraphQLView) => {
|
||||||
|
if (!viewGroupsToCreate.length) return;
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
viewGroupsToCreate.map((viewGroup) =>
|
||||||
|
apolloClient.mutate({
|
||||||
|
mutation: createOneRecordMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
fieldMetadataId: viewGroup.fieldMetadataId,
|
||||||
|
viewId: view.id,
|
||||||
|
isVisible: viewGroup.isVisible,
|
||||||
|
position: viewGroup.position,
|
||||||
|
id: v4(),
|
||||||
|
fieldValue: viewGroup.fieldValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: (cache, { data }) => {
|
||||||
|
const record = data?.['createViewGroup'];
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
triggerCreateRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordsToCreate: [record],
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
apolloClient,
|
||||||
|
createOneRecordMutation,
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataItems,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateViewGroupRecords = useCallback(
|
||||||
|
async (viewGroupsToUpdate: ViewGroup[]) => {
|
||||||
|
if (!viewGroupsToUpdate.length) return;
|
||||||
|
|
||||||
|
const mutationPromises = viewGroupsToUpdate.map((viewGroup) =>
|
||||||
|
apolloClient.mutate<{ updateViewGroup: ViewGroup }>({
|
||||||
|
mutation: updateOneRecordMutation,
|
||||||
|
variables: {
|
||||||
|
idToUpdate: viewGroup.id,
|
||||||
|
input: {
|
||||||
|
isVisible: viewGroup.isVisible,
|
||||||
|
position: viewGroup.position,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Avoid cache being updated with stale data
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutationResults = await Promise.all(mutationPromises);
|
||||||
|
|
||||||
|
// FixMe: Using triggerCreateRecordsOptimisticEffect is actaully causing multiple records to be created
|
||||||
|
mutationResults.forEach(({ data }) => {
|
||||||
|
const record = data?.['updateViewGroup'];
|
||||||
|
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
apolloClient.cache.modify({
|
||||||
|
id: apolloClient.cache.identify({
|
||||||
|
__typename: 'ViewGroup',
|
||||||
|
id: record.id,
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
isVisible: () => record.isVisible,
|
||||||
|
position: () => record.position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[apolloClient, updateOneRecordMutation],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
createViewGroupRecords,
|
||||||
|
updateViewGroupRecords,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
|
||||||
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
|
||||||
|
|
||||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
|
||||||
import { ViewSort } from '@/views/types/ViewSort';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const useCreateViewFiltersAndSorts = () => {
|
|
||||||
const { getViewFromCache } = useGetViewFromCache();
|
|
||||||
|
|
||||||
const { createViewSortRecords } = usePersistViewSortRecords();
|
|
||||||
|
|
||||||
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
|
||||||
|
|
||||||
const createViewFiltersAndSorts = async (
|
|
||||||
viewIdToCreateOn: string,
|
|
||||||
filtersToCreate: ViewFilter[],
|
|
||||||
sortsToCreate: ViewSort[],
|
|
||||||
) => {
|
|
||||||
const view = await getViewFromCache(viewIdToCreateOn);
|
|
||||||
|
|
||||||
if (!isDefined(view)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await createViewSortRecords(sortsToCreate, view);
|
|
||||||
await createViewFilterRecords(filtersToCreate, view);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
createViewFiltersAndSorts,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
|
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords';
|
||||||
import { useCreateViewFiltersAndSorts } from '@/views/hooks/useCreateViewFiltersAndSorts';
|
import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords';
|
||||||
|
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
||||||
|
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
|
||||||
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
|
import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters';
|
||||||
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
|
import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts';
|
||||||
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||||
@ -11,6 +14,10 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone
|
|||||||
import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState';
|
import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState';
|
||||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||||
import { View } from '@/views/types/View';
|
import { View } from '@/views/types/View';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
@ -35,12 +42,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
|
|
||||||
const { createViewFieldRecords } = usePersistViewFieldRecords();
|
const { createViewFieldRecords } = usePersistViewFieldRecords();
|
||||||
|
|
||||||
const { createViewFiltersAndSorts } = useCreateViewFiltersAndSorts();
|
|
||||||
|
|
||||||
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
|
const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId);
|
||||||
const { getViewFiltersCombined } =
|
const { getViewFiltersCombined } =
|
||||||
useGetViewFiltersCombined(viewBarComponentId);
|
useGetViewFiltersCombined(viewBarComponentId);
|
||||||
|
|
||||||
|
const { createViewSortRecords } = usePersistViewSortRecords();
|
||||||
|
|
||||||
|
const { createViewGroupRecords } = usePersistViewGroupRecords();
|
||||||
|
|
||||||
|
const { createViewFilterRecords } = usePersistViewFilterRecords();
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
|
||||||
|
|
||||||
const createViewFromCurrentView = useRecoilCallback(
|
const createViewFromCurrentView = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
async (
|
async (
|
||||||
@ -93,20 +106,56 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
|
|
||||||
await createViewFieldRecords(view.viewFields, newView);
|
await createViewFieldRecords(view.viewFields, newView);
|
||||||
|
|
||||||
|
if (type === ViewType.Kanban) {
|
||||||
|
if (!isNonEmptyArray(view.viewGroups)) {
|
||||||
|
if (!isDefined(kanbanFieldMetadataId)) {
|
||||||
|
throw new Error('Kanban view must have a kanban field');
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewGroupsToCreate =
|
||||||
|
objectMetadataItem?.fields
|
||||||
|
?.find((field) => field.id === kanbanFieldMetadataId)
|
||||||
|
?.options?.map(
|
||||||
|
(option, index) =>
|
||||||
|
({
|
||||||
|
id: v4(),
|
||||||
|
__typename: 'ViewGroup',
|
||||||
|
fieldMetadataId: kanbanFieldMetadataId,
|
||||||
|
fieldValue: option.value,
|
||||||
|
isVisible: true,
|
||||||
|
position: index,
|
||||||
|
}) satisfies ViewGroup,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
viewGroupsToCreate.push({
|
||||||
|
__typename: 'ViewGroup',
|
||||||
|
id: v4(),
|
||||||
|
fieldValue: '',
|
||||||
|
position: viewGroupsToCreate.length,
|
||||||
|
isVisible: true,
|
||||||
|
fieldMetadataId: kanbanFieldMetadataId,
|
||||||
|
} satisfies ViewGroup);
|
||||||
|
|
||||||
|
await createViewGroupRecords(viewGroupsToCreate, newView);
|
||||||
|
} else {
|
||||||
|
await createViewGroupRecords(view.viewGroups, newView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldCopyFiltersAndSorts === true) {
|
if (shouldCopyFiltersAndSorts === true) {
|
||||||
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
|
const sourceViewCombinedFilters = getViewFiltersCombined(view.id);
|
||||||
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
|
const sourceViewCombinedSorts = getViewSortsCombined(view.id);
|
||||||
|
|
||||||
await createViewFiltersAndSorts(
|
await createViewSortRecords(sourceViewCombinedSorts, view);
|
||||||
newView.id,
|
await createViewFilterRecords(sourceViewCombinedFilters, view);
|
||||||
sourceViewCombinedFilters,
|
|
||||||
sourceViewCombinedSorts,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set(isPersistingViewFieldsCallbackState, false);
|
set(isPersistingViewFieldsCallbackState, false);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
objectMetadataItem,
|
||||||
|
createViewSortRecords,
|
||||||
|
createViewFilterRecords,
|
||||||
createOneRecord,
|
createOneRecord,
|
||||||
createViewFieldRecords,
|
createViewFieldRecords,
|
||||||
getViewSortsCombined,
|
getViewSortsCombined,
|
||||||
@ -114,7 +163,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
currentViewIdCallbackState,
|
currentViewIdCallbackState,
|
||||||
getViewFromCache,
|
getViewFromCache,
|
||||||
isPersistingViewFieldsCallbackState,
|
isPersistingViewFieldsCallbackState,
|
||||||
createViewFiltersAndSorts,
|
createViewGroupRecords,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
|
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
|
||||||
|
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
|
||||||
|
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => {
|
||||||
|
const { createViewGroupRecords, updateViewGroupRecords } =
|
||||||
|
usePersistViewGroupRecords();
|
||||||
|
|
||||||
|
const { getViewFromCache } = useGetViewFromCache();
|
||||||
|
|
||||||
|
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
|
||||||
|
currentViewIdComponentState,
|
||||||
|
viewBarComponentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveViewGroups = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
async (viewGroupsToSave: ViewGroup[]) => {
|
||||||
|
const currentViewId = snapshot
|
||||||
|
.getLoadable(currentViewIdCallbackState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (!currentViewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = await getViewFromCache(currentViewId);
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(view)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentViewGroups = view.viewGroups;
|
||||||
|
|
||||||
|
const viewGroupsToUpdate = viewGroupsToSave
|
||||||
|
.map((viewGroupToSave) => {
|
||||||
|
const existingField = currentViewGroups.find(
|
||||||
|
(currentViewGroup) =>
|
||||||
|
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(existingField)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDeeplyEqual(
|
||||||
|
{
|
||||||
|
position: existingField.position,
|
||||||
|
isVisible: existingField.isVisible,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: viewGroupToSave.position,
|
||||||
|
isVisible: viewGroupToSave.isVisible,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...viewGroupToSave, id: existingField.id };
|
||||||
|
})
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
|
const viewGroupsToCreate = viewGroupsToSave.filter(
|
||||||
|
(viewFieldToSave) =>
|
||||||
|
!currentViewGroups.some(
|
||||||
|
(currentViewGroup) =>
|
||||||
|
currentViewGroup.fieldValue === viewFieldToSave.fieldValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
createViewGroupRecords(viewGroupsToCreate, view),
|
||||||
|
updateViewGroupRecords(viewGroupsToUpdate),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createViewGroupRecords,
|
||||||
|
currentViewIdCallbackState,
|
||||||
|
getViewFromCache,
|
||||||
|
updateViewGroupRecords,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveViewGroups,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
import { ViewKey } from '@/views/types/ViewKey';
|
import { ViewKey } from '@/views/types/ViewKey';
|
||||||
import { ViewSort } from '@/views/types/ViewSort';
|
import { ViewSort } from '@/views/types/ViewSort';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
@ -15,6 +16,7 @@ export type GraphQLView = {
|
|||||||
viewFields: ViewField[];
|
viewFields: ViewField[];
|
||||||
viewFilters: ViewFilter[];
|
viewFilters: ViewFilter[];
|
||||||
viewSorts: ViewSort[];
|
viewSorts: ViewSort[];
|
||||||
|
viewGroups: ViewGroup[];
|
||||||
position: number;
|
position: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ViewField } from '@/views/types/ViewField';
|
import { ViewField } from '@/views/types/ViewField';
|
||||||
import { ViewFilter } from '@/views/types/ViewFilter';
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
import { ViewKey } from '@/views/types/ViewKey';
|
import { ViewKey } from '@/views/types/ViewKey';
|
||||||
import { ViewSort } from '@/views/types/ViewSort';
|
import { ViewSort } from '@/views/types/ViewSort';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
@ -12,6 +13,7 @@ export type View = {
|
|||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
isCompact: boolean;
|
isCompact: boolean;
|
||||||
viewFields: ViewField[];
|
viewFields: ViewField[];
|
||||||
|
viewGroups: ViewGroup[];
|
||||||
viewFilters: ViewFilter[];
|
viewFilters: ViewFilter[];
|
||||||
viewSorts: ViewSort[];
|
viewSorts: ViewSort[];
|
||||||
kanbanFieldMetadataId: string;
|
kanbanFieldMetadataId: string;
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
export type ViewGroup = {
|
||||||
|
__typename: 'ViewGroup';
|
||||||
|
id: string;
|
||||||
|
fieldMetadataId: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
fieldValue: string;
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
|
|
||||||
|
export const mapRecordGroupDefinitionsToViewGroups = (
|
||||||
|
groupDefinitions: RecordGroupDefinition[],
|
||||||
|
): ViewGroup[] => {
|
||||||
|
return groupDefinitions.map(
|
||||||
|
(groupDefinition): ViewGroup => ({
|
||||||
|
__typename: 'ViewGroup',
|
||||||
|
id: groupDefinition.id,
|
||||||
|
fieldMetadataId: groupDefinition.fieldMetadataId,
|
||||||
|
position: groupDefinition.position,
|
||||||
|
isVisible: groupDefinition.isVisible ?? true,
|
||||||
|
fieldValue: groupDefinition.value ?? '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import {
|
||||||
|
RecordGroupDefinition,
|
||||||
|
RecordGroupDefinitionType,
|
||||||
|
} from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
|
import { ViewGroup } from '@/views/types/ViewGroup';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const mapViewGroupsToRecordGroupDefinitions = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
viewGroups,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
viewGroups: ViewGroup[];
|
||||||
|
}): RecordGroupDefinition[] => {
|
||||||
|
if (viewGroups?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMetadataId = viewGroups?.[0]?.fieldMetadataId;
|
||||||
|
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
(field) =>
|
||||||
|
field.id === fieldMetadataId && field.type === FieldMetadataType.Select,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectFieldMetadataItem) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectFieldMetadataItem.options) {
|
||||||
|
throw new Error(
|
||||||
|
`Select Field ${objectMetadataItem.nameSingular} has no options`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordGroupDefinitionsFromViewGroups = viewGroups
|
||||||
|
.map((viewGroup) => {
|
||||||
|
const selectedOption = selectFieldMetadataItem.options?.find(
|
||||||
|
(option) => option.value === viewGroup.fieldValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedOption) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: viewGroup.id,
|
||||||
|
fieldMetadataId: viewGroup.fieldMetadataId,
|
||||||
|
type: RecordGroupDefinitionType.Value,
|
||||||
|
title: selectedOption.label,
|
||||||
|
value: selectedOption.value,
|
||||||
|
color: selectedOption.color,
|
||||||
|
position: viewGroup.position,
|
||||||
|
isVisible: viewGroup.isVisible,
|
||||||
|
} as RecordGroupDefinition;
|
||||||
|
})
|
||||||
|
.filter(isDefined)
|
||||||
|
.sort((a, b) => a.position - b.position);
|
||||||
|
|
||||||
|
if (selectFieldMetadataItem.isNullable === true) {
|
||||||
|
const noValueColumn = {
|
||||||
|
id: 'no-value',
|
||||||
|
title: 'No Value',
|
||||||
|
type: RecordGroupDefinitionType.NoValue,
|
||||||
|
value: null,
|
||||||
|
position:
|
||||||
|
recordGroupDefinitionsFromViewGroups
|
||||||
|
.map((option) => option.position)
|
||||||
|
.reduce((a, b) => Math.max(a, b), 0) + 1,
|
||||||
|
isVisible: true,
|
||||||
|
fieldMetadataId: selectFieldMetadataItem.id,
|
||||||
|
color: 'transparent',
|
||||||
|
} satisfies RecordGroupDefinition;
|
||||||
|
|
||||||
|
return [...recordGroupDefinitionsFromViewGroups, noValueColumn];
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordGroupDefinitionsFromViewGroups;
|
||||||
|
};
|
||||||
@ -16,6 +16,7 @@ import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useC
|
|||||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||||
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||||
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -71,22 +72,26 @@ export const RecordIndexPage = () => {
|
|||||||
onCreateRecord: handleCreateRecord,
|
onCreateRecord: handleCreateRecord,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
<ViewComponentInstanceContext.Provider
|
||||||
<RecordIndexPageHeader />
|
value={{ instanceId: recordIndexId }}
|
||||||
<PageBody>
|
>
|
||||||
<StyledIndexContainer>
|
<PageTitle title={`${capitalize(objectNamePlural)}`} />
|
||||||
<ContextStoreComponentInstanceContext.Provider
|
<RecordIndexPageHeader />
|
||||||
value={{
|
<PageBody>
|
||||||
instanceId: 'record-index',
|
<StyledIndexContainer>
|
||||||
}}
|
<ContextStoreComponentInstanceContext.Provider
|
||||||
>
|
value={{
|
||||||
<RecordIndexContainerContextStoreObjectMetadataEffect />
|
instanceId: 'record-index',
|
||||||
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
|
}}
|
||||||
<SetMainContextStoreComponentInstanceIdEffect />
|
>
|
||||||
<RecordIndexContainer />
|
<RecordIndexContainerContextStoreObjectMetadataEffect />
|
||||||
</ContextStoreComponentInstanceContext.Provider>
|
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
|
||||||
</StyledIndexContainer>
|
<SetMainContextStoreComponentInstanceIdEffect />
|
||||||
</PageBody>
|
<RecordIndexContainer />
|
||||||
|
</ContextStoreComponentInstanceContext.Provider>
|
||||||
|
</StyledIndexContainer>
|
||||||
|
</PageBody>
|
||||||
|
</ViewComponentInstanceContext.Provider>
|
||||||
</RecordIndexRootPropsContext.Provider>
|
</RecordIndexRootPropsContext.Provider>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const Default: Story = {
|
|||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
await canvas.findByText('People', undefined, { timeout: 3000 });
|
await canvas.findByText('People', undefined, { timeout: 10000 });
|
||||||
await canvas.findByText('Linkedin');
|
await canvas.findByText('Linkedin');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const WithStandardSelected: Story = {
|
|||||||
play: async () => {
|
play: async () => {
|
||||||
const canvas = within(document.body);
|
const canvas = within(document.body);
|
||||||
|
|
||||||
await canvas.findByText('New Object');
|
await canvas.findByText('New Object', undefined, { timeout: 2000 });
|
||||||
|
|
||||||
const listingInput = await canvas.findByPlaceholderText('Listing');
|
const listingInput = await canvas.findByPlaceholderText('Listing');
|
||||||
const pluralInput = await canvas.findByPlaceholderText('Listings');
|
const pluralInput = await canvas.findByPlaceholderText('Listings');
|
||||||
|
|||||||
@ -126,6 +126,33 @@ export const viewPrefillData = async (
|
|||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
'groups' in viewDefinition &&
|
||||||
|
viewDefinition.groups &&
|
||||||
|
viewDefinition.groups.length > 0
|
||||||
|
) {
|
||||||
|
await entityManager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(`${schemaName}.viewGroup`, [
|
||||||
|
'fieldMetadataId',
|
||||||
|
'isVisible',
|
||||||
|
'fieldValue',
|
||||||
|
'position',
|
||||||
|
'viewId',
|
||||||
|
])
|
||||||
|
.values(
|
||||||
|
viewDefinition.groups.map((group: any) => ({
|
||||||
|
fieldMetadataId: group.fieldMetadataId,
|
||||||
|
isVisible: group.isVisible,
|
||||||
|
fieldValue: group.fieldValue,
|
||||||
|
position: group.position,
|
||||||
|
viewId: viewDefinition.id,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewDefinitionsWithId;
|
return viewDefinitionsWithId;
|
||||||
|
|||||||
@ -73,5 +73,52 @@ export const opportunitiesByStageView = (
|
|||||||
size: 150,
|
size: 150,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||||
|
OPPORTUNITY_STANDARD_FIELD_IDS.stage
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'NEW',
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||||
|
OPPORTUNITY_STANDARD_FIELD_IDS.stage
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'SCREENING',
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||||
|
OPPORTUNITY_STANDARD_FIELD_IDS.stage
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'MEETING',
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||||
|
OPPORTUNITY_STANDARD_FIELD_IDS.stage
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'PROPOSAL',
|
||||||
|
position: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||||
|
OPPORTUNITY_STANDARD_FIELD_IDS.stage
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'CUSTOMER',
|
||||||
|
position: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -89,5 +89,34 @@ export const tasksByStatusView = (
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
],
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||||
|
TASK_STANDARD_FIELD_IDS.status
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'TODO',
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||||
|
TASK_STANDARD_FIELD_IDS.status
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'IN_PROGESS',
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldMetadataId:
|
||||||
|
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||||
|
TASK_STANDARD_FIELD_IDS.status
|
||||||
|
],
|
||||||
|
isVisible: true,
|
||||||
|
fieldValue: 'DONE',
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -368,6 +368,14 @@ export const VIEW_FIELD_STANDARD_FIELD_IDS = {
|
|||||||
view: '20202020-e8da-4521-afab-d6d231f9fa18',
|
view: '20202020-e8da-4521-afab-d6d231f9fa18',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VIEW_GROUP_STANDARD_FIELD_IDS = {
|
||||||
|
fieldMetadataId: '20202020-8f26-46ae-afed-fdacd7778682',
|
||||||
|
fieldValue: '20202020-175e-4596-b7a4-1cd9d14e5a30',
|
||||||
|
isVisible: '20202020-0fed-4b44-88fd-a064c4fcfce4',
|
||||||
|
position: '20202020-748e-4645-8f32-84aae7726c04',
|
||||||
|
view: '20202020-5bc7-4110-b23f-fb851fb133b4',
|
||||||
|
};
|
||||||
|
|
||||||
export const VIEW_FILTER_STANDARD_FIELD_IDS = {
|
export const VIEW_FILTER_STANDARD_FIELD_IDS = {
|
||||||
fieldMetadataId: '20202020-c9aa-4c94-8d0e-9592f5008fb0',
|
fieldMetadataId: '20202020-c9aa-4c94-8d0e-9592f5008fb0',
|
||||||
operand: '20202020-bd23-48c4-9fab-29d1ffb80310',
|
operand: '20202020-bd23-48c4-9fab-29d1ffb80310',
|
||||||
@ -392,6 +400,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
|
|||||||
position: '20202020-e9db-4303-b271-e8250c450172',
|
position: '20202020-e9db-4303-b271-e8250c450172',
|
||||||
isCompact: '20202020-674e-4314-994d-05754ea7b22b',
|
isCompact: '20202020-674e-4314-994d-05754ea7b22b',
|
||||||
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',
|
viewFields: '20202020-542b-4bdc-b177-b63175d48edf',
|
||||||
|
viewGroups: '20202020-e1a1-419f-ac81-1986a5ea59a8',
|
||||||
viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967',
|
viewFilters: '20202020-ff23-4154-b63c-21fb36cd0967',
|
||||||
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
||||||
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export const STANDARD_OBJECT_IDS = {
|
|||||||
taskTarget: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
taskTarget: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||||
timelineActivity: '20202020-6736-4337-b5c4-8b39fae325a5',
|
timelineActivity: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||||
viewField: '20202020-4d19-4655-95bf-b2a04cf206d4',
|
viewField: '20202020-4d19-4655-95bf-b2a04cf206d4',
|
||||||
|
viewGroup: '20202020-725f-47a4-8008-4255f9519f70',
|
||||||
viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8',
|
viewFilter: '20202020-6fb6-4631-aded-b7d67e952ec8',
|
||||||
viewSort: '20202020-e46a-47a8-939a-e5d911f83531',
|
viewSort: '20202020-e46a-47a8-939a-e5d911f83531',
|
||||||
view: '20202020-722e-4739-8e2c-0c372d661f49',
|
view: '20202020-722e-4739-8e2c-0c372d661f49',
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-ob
|
|||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
||||||
@ -56,6 +57,7 @@ export const standardObjectMetadataDefinitions = [
|
|||||||
FavoriteWorkspaceEntity,
|
FavoriteWorkspaceEntity,
|
||||||
TimelineActivityWorkspaceEntity,
|
TimelineActivityWorkspaceEntity,
|
||||||
ViewFieldWorkspaceEntity,
|
ViewFieldWorkspaceEntity,
|
||||||
|
ViewGroupWorkspaceEntity,
|
||||||
ViewFilterWorkspaceEntity,
|
ViewFilterWorkspaceEntity,
|
||||||
ViewSortWorkspaceEntity,
|
ViewSortWorkspaceEntity,
|
||||||
ViewWorkspaceEntity,
|
ViewWorkspaceEntity,
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||||
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
|
import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
|
|
||||||
|
@WorkspaceEntity({
|
||||||
|
standardId: STANDARD_OBJECT_IDS.viewGroup,
|
||||||
|
namePlural: 'viewGroups',
|
||||||
|
labelSingular: 'View Group',
|
||||||
|
labelPlural: 'View Groups',
|
||||||
|
description: '(System) View Groups',
|
||||||
|
icon: 'IconTag',
|
||||||
|
})
|
||||||
|
@WorkspaceIsNotAuditLogged()
|
||||||
|
@WorkspaceIsSystem()
|
||||||
|
export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldMetadataId,
|
||||||
|
type: FieldMetadataType.UUID,
|
||||||
|
label: 'Field Metadata Id',
|
||||||
|
description: 'View Group target field',
|
||||||
|
icon: 'IconTag',
|
||||||
|
})
|
||||||
|
fieldMetadataId: string;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.isVisible,
|
||||||
|
type: FieldMetadataType.BOOLEAN,
|
||||||
|
label: 'Visible',
|
||||||
|
description: 'View Group visibility',
|
||||||
|
icon: 'IconEye',
|
||||||
|
defaultValue: true,
|
||||||
|
})
|
||||||
|
isVisible: boolean;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.fieldValue,
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
label: 'Field Value',
|
||||||
|
description: 'Group by this field value',
|
||||||
|
})
|
||||||
|
fieldValue: string;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.position,
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
label: 'Position',
|
||||||
|
description: 'View Field position',
|
||||||
|
icon: 'IconList',
|
||||||
|
defaultValue: 0,
|
||||||
|
})
|
||||||
|
position: number;
|
||||||
|
|
||||||
|
@WorkspaceRelation({
|
||||||
|
standardId: VIEW_GROUP_STANDARD_FIELD_IDS.view,
|
||||||
|
type: RelationMetadataType.MANY_TO_ONE,
|
||||||
|
label: 'View',
|
||||||
|
description: 'View Group related view',
|
||||||
|
icon: 'IconLayoutCollage',
|
||||||
|
inverseSideTarget: () => ViewWorkspaceEntity,
|
||||||
|
inverseSideFieldKey: 'viewGroups',
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
view?: ViewWorkspaceEntity | null;
|
||||||
|
|
||||||
|
@WorkspaceJoinColumn('view')
|
||||||
|
viewId: string | null;
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f
|
|||||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||||
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.view,
|
standardId: STANDARD_OBJECT_IDS.view,
|
||||||
@ -113,6 +114,18 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
viewFields: Relation<ViewFieldWorkspaceEntity[]>;
|
viewFields: Relation<ViewFieldWorkspaceEntity[]>;
|
||||||
|
|
||||||
|
@WorkspaceRelation({
|
||||||
|
standardId: VIEW_STANDARD_FIELD_IDS.viewGroups,
|
||||||
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
|
label: 'View Groups',
|
||||||
|
description: 'View Groups',
|
||||||
|
icon: 'IconTag',
|
||||||
|
inverseSideTarget: () => ViewGroupWorkspaceEntity,
|
||||||
|
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
viewGroups: Relation<ViewGroupWorkspaceEntity[]>;
|
||||||
|
|
||||||
@WorkspaceRelation({
|
@WorkspaceRelation({
|
||||||
standardId: VIEW_STANDARD_FIELD_IDS.viewFilters,
|
standardId: VIEW_STANDARD_FIELD_IDS.viewFilters,
|
||||||
type: RelationMetadataType.ONE_TO_MANY,
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
|
|||||||
Reference in New Issue
Block a user