feat: add resizable kanban column width (#21828)
## What & why Lets users resize the columns of a Kanban (record board) view. Requested by a user; the design avoids the "ragged board" problem by making the width a **single shared value**. ## Behaviour - A drag handle appears on the right edge of every column header. - Because all columns read **one** width value, dragging any handle resizes **every** column together — they can never end up mismatched. - Width is clamped between **150px** and **400px** (default **200px**). - The width is **persisted per view** and restored on reload. ## Approach **Backend** — a new nullable `View.kanbanColumnWidth` field, threaded through the existing view-level setting pattern (the same one `kanbanAggregateOperation` / `shouldHideEmptyGroups` use), so it gets create/update/manifest/override support for free: - entity column + `ViewOverrides` + `@WasIntroducedInUpgrade` - `CreateViewInput` / `UpdateViewInput` (`Int`, `@Min(150)`/`@Max(400)`) + `ViewDTO` - flat-view editable properties, entity-properties config, compare-type, standard-view + manifest converters - a fast instance command adding the `core.view` column **Frontend** — the value hydrates into a view-scoped atom and drives a single CSS variable set on the board container, which both column headers and bodies read. Live dragging only writes that CSS variable (no per-move React re-render); the final width is committed to the atom and persisted via `updateView` on pointer-up. ## Nullability / defaults `kanbanColumnWidth` is nullable — `null` means "never resized" and the UI falls back to the 200px default, so existing rows need no backfill. ## Validation - `nx typecheck twenty-server` ✅ and `nx typecheck twenty-front` ✅ - `nx lint:diff-with-main twenty-server` ✅; frontend lint fixes applied (split constants to one-per-file, removed `useRef`-for-state in favour of `useState`). - Draft pending a final green CI run (the dev container reclaimed `node_modules` mid-session; re-running locally). ## Test plan - [ ] Drag a kanban column edge → all columns resize together, clamped 150–400px - [ ] Reload → width persists for that view; other views unaffected - [ ] A view that was never resized still renders at 200px https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE --- _Generated by [Claude Code](https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21828?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -869,6 +869,7 @@ type View {
|
||||
kanbanAggregateOperationFieldMetadataId: UUID
|
||||
mainGroupByFieldMetadataId: UUID
|
||||
shouldHideEmptyGroups: Boolean!
|
||||
kanbanColumnWidth: Int
|
||||
calendarFieldMetadataId: UUID
|
||||
workspaceId: UUID!
|
||||
anyFieldFilterValue: String
|
||||
@@ -3575,6 +3576,7 @@ input CreateViewInput {
|
||||
position: Float = 0
|
||||
isCompact: Boolean = false
|
||||
shouldHideEmptyGroups: Boolean = false
|
||||
kanbanColumnWidth: Int
|
||||
openRecordIn: ViewOpenRecordIn = SIDE_PANEL
|
||||
kanbanAggregateOperation: AggregateOperations
|
||||
kanbanAggregateOperationFieldMetadataId: UUID
|
||||
@@ -3601,6 +3603,7 @@ input UpdateViewInput {
|
||||
visibility: ViewVisibility
|
||||
mainGroupByFieldMetadataId: UUID
|
||||
shouldHideEmptyGroups: Boolean
|
||||
kanbanColumnWidth: Int
|
||||
}
|
||||
|
||||
input UpsertViewWidgetInput {
|
||||
|
||||
@@ -602,6 +602,7 @@ export interface View {
|
||||
kanbanAggregateOperationFieldMetadataId?: Scalars['UUID']
|
||||
mainGroupByFieldMetadataId?: Scalars['UUID']
|
||||
shouldHideEmptyGroups: Scalars['Boolean']
|
||||
kanbanColumnWidth?: Scalars['Int']
|
||||
calendarFieldMetadataId?: Scalars['UUID']
|
||||
workspaceId: Scalars['UUID']
|
||||
anyFieldFilterValue?: Scalars['String']
|
||||
@@ -3631,6 +3632,7 @@ export interface ViewGenqlSelection{
|
||||
kanbanAggregateOperationFieldMetadataId?: boolean | number
|
||||
mainGroupByFieldMetadataId?: boolean | number
|
||||
shouldHideEmptyGroups?: boolean | number
|
||||
kanbanColumnWidth?: boolean | number
|
||||
calendarFieldMetadataId?: boolean | number
|
||||
workspaceId?: boolean | number
|
||||
anyFieldFilterValue?: boolean | number
|
||||
@@ -6202,9 +6204,9 @@ export interface DestroyViewFilterInput {
|
||||
/** The id of the view filter to destroy. */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
|
||||
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
|
||||
|
||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
|
||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetInput {
|
||||
/** The id of the view widget (page layout widget). */
|
||||
|
||||
@@ -1712,6 +1712,9 @@ export default {
|
||||
"shouldHideEmptyGroups": [
|
||||
6
|
||||
],
|
||||
"kanbanColumnWidth": [
|
||||
21
|
||||
],
|
||||
"calendarFieldMetadataId": [
|
||||
3
|
||||
],
|
||||
@@ -9352,6 +9355,9 @@ export default {
|
||||
"shouldHideEmptyGroups": [
|
||||
6
|
||||
],
|
||||
"kanbanColumnWidth": [
|
||||
21
|
||||
],
|
||||
"openRecordIn": [
|
||||
73
|
||||
],
|
||||
@@ -9426,6 +9432,9 @@ export default {
|
||||
"shouldHideEmptyGroups": [
|
||||
6
|
||||
],
|
||||
"kanbanColumnWidth": [
|
||||
21
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
-1
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
||||
import { useContext, useRef } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { RecordBoardColumnWidthEffect } from '@/object-record/record-board/components/RecordBoardColumnWidthEffect';
|
||||
import { RecordBoardColumns } from '@/object-record/record-board/components/RecordBoardColumns';
|
||||
import { RecordBoardDragDropContext } from '@/object-record/record-board/components/RecordBoardDragDropContext';
|
||||
import { RecordBoardDragSelect } from '@/object-record/record-board/components/RecordBoardDragSelect';
|
||||
@@ -12,6 +13,8 @@ import { RecordBoardHeader } from '@/object-record/record-board/components/Recor
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { getRecordBoardHtmlId } from '@/object-record/record-board/utils/getRecordBoardHtmlId';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -43,7 +46,8 @@ export const RecordBoard = () => {
|
||||
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
|
||||
>
|
||||
<RecordBoardEffects />
|
||||
<StyledContainerContainer>
|
||||
<RecordBoardColumnWidthEffect />
|
||||
<StyledContainerContainer id={getRecordBoardHtmlId(recordBoardId)}>
|
||||
<RecordBoardHeader />
|
||||
<StyledBoardContentContainer>
|
||||
<StyledContainer ref={boardRef}>
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { useContext, useLayoutEffect } from 'react';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { setRecordBoardColumnWidthCssVariable } from '@/object-record/record-board/utils/setRecordBoardColumnWidthCssVariable';
|
||||
import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
|
||||
export const RecordBoardColumnWidthEffect = () => {
|
||||
const { recordBoardId } = useContext(RecordBoardContext);
|
||||
|
||||
const recordIndexKanbanColumnWidth = useAtomComponentStateValue(
|
||||
recordIndexKanbanColumnWidthComponentState,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setRecordBoardColumnWidthCssVariable(
|
||||
recordBoardId,
|
||||
recordIndexKanbanColumnWidth,
|
||||
);
|
||||
}, [recordBoardId, recordIndexKanbanColumnWidth]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+6
-2
@@ -5,7 +5,7 @@ import { useInView } from 'react-intersection-observer';
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
|
||||
import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize';
|
||||
import { recordBoardShouldFetchMoreComponentState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreComponentState';
|
||||
import { isDraggingRecordComponentState } from '@/object-record/record-drag/states/isDraggingRecordComponentState';
|
||||
@@ -69,8 +69,12 @@ export const RecordBoardFetchMoreInViewTriggerComponent = () => {
|
||||
ViewType.KANBAN,
|
||||
);
|
||||
|
||||
const recordIndexKanbanColumnWidth = useAtomComponentStateValue(
|
||||
recordIndexKanbanColumnWidthComponentState,
|
||||
);
|
||||
|
||||
const componentWidth =
|
||||
visibleRecordGroupIds.length * RECORD_BOARD_COLUMN_WIDTH +
|
||||
visibleRecordGroupIds.length * recordIndexKanbanColumnWidth +
|
||||
visibleRecordGroupIds.length *
|
||||
RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH -
|
||||
1;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_COLUMN_MAX_WIDTH = 400;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_BOARD_COLUMN_MIN_WIDTH = 150;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME =
|
||||
'--record-board-column-width';
|
||||
+10
-2
@@ -2,6 +2,8 @@ import { styled } from '@linaria/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME } from '@/object-record/record-board/constants/RecordBoardColumnWidthCssVariableName';
|
||||
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 { useShouldHideRecordGroup } from '@/object-record/record-group/hooks/useShouldHideRecordGroup';
|
||||
@@ -16,8 +18,14 @@ const StyledColumn = styled.div`
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: var(
|
||||
${RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME},
|
||||
${RECORD_BOARD_COLUMN_WIDTH}px
|
||||
);
|
||||
min-width: var(
|
||||
${RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME},
|
||||
${RECORD_BOARD_COLUMN_WIDTH}px
|
||||
);
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
padding-top: 0px;
|
||||
position: relative;
|
||||
|
||||
+11
-2
@@ -8,6 +8,8 @@ import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/reco
|
||||
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
|
||||
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME } from '@/object-record/record-board/constants/RecordBoardColumnWidthCssVariableName';
|
||||
import { RecordBoardColumnResizeHandler } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnResizeHandler';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
@@ -61,8 +63,14 @@ const StyledColumn = styled.div`
|
||||
background-color: ${themeCssVariables.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: ${RECORD_BOARD_COLUMN_WIDTH}px;
|
||||
min-width: ${RECORD_BOARD_COLUMN_WIDTH}px;
|
||||
max-width: var(
|
||||
${RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME},
|
||||
${RECORD_BOARD_COLUMN_WIDTH}px
|
||||
);
|
||||
min-width: var(
|
||||
${RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME},
|
||||
${RECORD_BOARD_COLUMN_WIDTH}px
|
||||
);
|
||||
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
|
||||
@@ -202,6 +210,7 @@ export const RecordBoardColumnHeader = () => {
|
||||
</StyledRightContainer>
|
||||
</StyledHeaderContainer>
|
||||
</StyledHeader>
|
||||
<RecordBoardColumnResizeHandler />
|
||||
</StyledColumn>
|
||||
);
|
||||
};
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { useResizeBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useResizeBoardColumn';
|
||||
import { RecordColumnResizeHandle } from '@/object-record/record-index/components/RecordColumnResizeHandle';
|
||||
|
||||
export const RecordBoardColumnResizeHandler = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { isResizing, handleResizeStart } = useResizeBoardColumn();
|
||||
|
||||
if (isMobile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordColumnResizeHandle
|
||||
isResizing={isResizing}
|
||||
position="right"
|
||||
onPointerDown={handleResizeStart}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { clampRecordBoardColumnWidth } from '@/object-record/record-board/utils/clampRecordBoardColumnWidth';
|
||||
import { setRecordBoardColumnWidthCssVariable } from '@/object-record/record-board/utils/setRecordBoardColumnWidthCssVariable';
|
||||
import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
|
||||
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
|
||||
import { type PointerEventListener } from '@/ui/utilities/pointer-event/types/PointerEventListener';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useUpdateViewKanbanColumnWidth } from '@/views/hooks/useUpdateViewKanbanColumnWidth';
|
||||
|
||||
export const useResizeBoardColumn = () => {
|
||||
const { recordBoardId } = useContext(RecordBoardContext);
|
||||
|
||||
const [recordIndexKanbanColumnWidth, setRecordIndexKanbanColumnWidth] =
|
||||
useAtomComponentState(recordIndexKanbanColumnWidthComponentState);
|
||||
|
||||
const { updateViewKanbanColumnWidth } = useUpdateViewKanbanColumnWidth();
|
||||
|
||||
const { setDragSelectionStartEnabled } = useDragSelect();
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const isResizing = isDefined(initialPointerPositionX);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
|
||||
setDragSelectionStartEnabled(false);
|
||||
setInitialPointerPositionX(event.clientX);
|
||||
},
|
||||
[setDragSelectionStartEnabled],
|
||||
);
|
||||
|
||||
const handleResizeMove = useCallback<PointerEventListener>(
|
||||
({ x, event }) => {
|
||||
if (!isDefined(initialPointerPositionX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('buttons' in event && event.buttons === 0) {
|
||||
setInitialPointerPositionX(null);
|
||||
setDragSelectionStartEnabled(true);
|
||||
setRecordBoardColumnWidthCssVariable(
|
||||
recordBoardId,
|
||||
recordIndexKanbanColumnWidth,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setRecordBoardColumnWidthCssVariable(
|
||||
recordBoardId,
|
||||
clampRecordBoardColumnWidth(
|
||||
recordIndexKanbanColumnWidth + (x - initialPointerPositionX),
|
||||
),
|
||||
);
|
||||
},
|
||||
[
|
||||
initialPointerPositionX,
|
||||
recordIndexKanbanColumnWidth,
|
||||
recordBoardId,
|
||||
setDragSelectionStartEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback<PointerEventListener>(
|
||||
({ x }) => {
|
||||
if (!isDefined(initialPointerPositionX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialPointerPositionX(null);
|
||||
setDragSelectionStartEnabled(true);
|
||||
|
||||
const nextWidth = Math.round(
|
||||
clampRecordBoardColumnWidth(
|
||||
recordIndexKanbanColumnWidth + (x - initialPointerPositionX),
|
||||
),
|
||||
);
|
||||
|
||||
if (nextWidth !== recordIndexKanbanColumnWidth) {
|
||||
setRecordIndexKanbanColumnWidth(nextWidth);
|
||||
updateViewKanbanColumnWidth(nextWidth);
|
||||
}
|
||||
},
|
||||
[
|
||||
initialPointerPositionX,
|
||||
recordIndexKanbanColumnWidth,
|
||||
setRecordIndexKanbanColumnWidth,
|
||||
updateViewKanbanColumnWidth,
|
||||
setDragSelectionStartEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
shouldTrackPointer: isResizing,
|
||||
onMouseMove: handleResizeMove,
|
||||
onMouseUp: handleResizeEnd,
|
||||
});
|
||||
|
||||
return { isResizing, handleResizeStart };
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { RECORD_BOARD_COLUMN_MAX_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnMaxWidth';
|
||||
import { RECORD_BOARD_COLUMN_MIN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnMinWidth';
|
||||
|
||||
export const clampRecordBoardColumnWidth = (width: number): number =>
|
||||
Math.min(
|
||||
RECORD_BOARD_COLUMN_MAX_WIDTH,
|
||||
Math.max(RECORD_BOARD_COLUMN_MIN_WIDTH, width),
|
||||
);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const getRecordBoardHtmlId = (recordBoardId: string): string =>
|
||||
`record-board-${recordBoardId}`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME } from '@/object-record/record-board/constants/RecordBoardColumnWidthCssVariableName';
|
||||
import { getRecordBoardHtmlId } from '@/object-record/record-board/utils/getRecordBoardHtmlId';
|
||||
|
||||
export const setRecordBoardColumnWidthCssVariable = (
|
||||
recordBoardId: string,
|
||||
widthInPixels: number,
|
||||
) => {
|
||||
document
|
||||
.getElementById(getRecordBoardHtmlId(recordBoardId))
|
||||
?.style.setProperty(
|
||||
RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME,
|
||||
`${widthInPixels}px`,
|
||||
);
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledRecordColumnResizeHandle = styled.div<{
|
||||
isResizing: boolean;
|
||||
position: 'left' | 'right';
|
||||
}>`
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
left: ${({ position }) => (position === 'left' ? '-1px' : 'auto')};
|
||||
position: absolute;
|
||||
right: ${({ position }) => (position === 'right' ? '-1px' : 'auto')};
|
||||
top: 0;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
|
||||
&:after {
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: ${({ isResizing }) => (isResizing ? 'block' : 'none')};
|
||||
left: ${({ position }) => (position === 'left' ? '-1px' : 'auto')};
|
||||
position: absolute;
|
||||
right: ${({ position }) => (position === 'right' ? '-1px' : 'auto')};
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
type RecordColumnResizeHandleProps = {
|
||||
isResizing: boolean;
|
||||
position: 'left' | 'right';
|
||||
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
export const RecordColumnResizeHandle = ({
|
||||
isResizing,
|
||||
position,
|
||||
onPointerDown,
|
||||
}: RecordColumnResizeHandleProps) => (
|
||||
<StyledRecordColumnResizeHandle
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
isResizing={isResizing}
|
||||
position={position}
|
||||
onPointerDown={onPointerDown}
|
||||
/>
|
||||
);
|
||||
+15
@@ -15,10 +15,13 @@ import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
|
||||
import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState';
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { clampRecordBoardColumnWidth } from '@/object-record/record-board/utils/clampRecordBoardColumnWidth';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
|
||||
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { recordIndexKanbanColumnWidthComponentState } from '@/object-record/record-index/states/recordIndexKanbanColumnWidthComponentState';
|
||||
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
@@ -68,6 +71,10 @@ export const useLoadRecordIndexStates = () => {
|
||||
recordIndexShouldHideEmptyRecordGroupsComponentState,
|
||||
);
|
||||
|
||||
const recordIndexKanbanColumnWidthAtom = useAtomComponentStateCallbackState(
|
||||
recordIndexKanbanColumnWidthComponentState,
|
||||
);
|
||||
|
||||
const { getFieldMetadataItemByIdOrThrow } =
|
||||
useGetFieldMetadataItemByIdOrThrow();
|
||||
|
||||
@@ -318,6 +325,13 @@ export const useLoadRecordIndexStates = () => {
|
||||
view.shouldHideEmptyGroups,
|
||||
);
|
||||
|
||||
batchSet(
|
||||
recordIndexKanbanColumnWidthAtom,
|
||||
clampRecordBoardColumnWidth(
|
||||
view.kanbanColumnWidth ?? RECORD_BOARD_COLUMN_WIDTH,
|
||||
),
|
||||
);
|
||||
|
||||
if (isDefined(recordIndexGroupFieldMetadataItemValue)) {
|
||||
batchSet(
|
||||
recordIndexGroupFieldMetadataItemAtom,
|
||||
@@ -355,6 +369,7 @@ export const useLoadRecordIndexStates = () => {
|
||||
recordIndexGroupAggregateOperationAtom,
|
||||
recordIndexGroupAggregateFieldMetadataItemAtom,
|
||||
recordIndexShouldHideEmptyRecordGroupsAtom,
|
||||
recordIndexKanbanColumnWidthAtom,
|
||||
getFieldMetadataItemByIdOrThrow,
|
||||
setRecordGroupsFromViewGroups,
|
||||
syncRecordIndexViewFields,
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const recordIndexKanbanColumnWidthComponentState =
|
||||
createAtomComponentState<number>({
|
||||
key: 'recordIndexKanbanColumnWidthComponentState',
|
||||
defaultValue: RECORD_BOARD_COLUMN_WIDTH,
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
+3
-32
@@ -1,38 +1,11 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { RecordColumnResizeHandle } from '@/object-record/record-index/components/RecordColumnResizeHandle';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
|
||||
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledResizeHandler = styled.div<{
|
||||
isResizing: boolean;
|
||||
position: 'left' | 'right';
|
||||
}>`
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
left: ${({ position }) => (position === 'left' ? '-1px' : 'auto')};
|
||||
position: absolute;
|
||||
right: ${({ position }) => (position === 'right' ? '-1px' : 'auto')};
|
||||
top: 0;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
|
||||
&:after {
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: ${({ isResizing }) => (isResizing ? 'block' : 'none')};
|
||||
left: ${({ position }) => (position === 'left' ? '-1px' : 'auto')};
|
||||
position: absolute;
|
||||
right: ${({ position }) => (position === 'right' ? '-1px' : 'auto')};
|
||||
top: 0;
|
||||
width: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordTableHeaderResizeHandler = ({
|
||||
recordFieldIndex,
|
||||
position,
|
||||
@@ -66,12 +39,10 @@ export const RecordTableHeaderResizeHandler = ({
|
||||
|
||||
return (
|
||||
!columnResizeDisabled && (
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={handlePointerDown}
|
||||
<RecordColumnResizeHandle
|
||||
isResizing={isResizing}
|
||||
position={position}
|
||||
onPointerDown={handlePointerDown}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ export const VIEW_FRAGMENT = gql`
|
||||
kanbanAggregateOperationFieldMetadataId
|
||||
mainGroupByFieldMetadataId
|
||||
shouldHideEmptyGroups
|
||||
kanbanColumnWidth
|
||||
anyFieldFilterValue
|
||||
calendarFieldMetadataId
|
||||
calendarLayout
|
||||
|
||||
@@ -122,6 +122,9 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
||||
shouldCopyFiltersAndSortsAndAggregate
|
||||
? sourceView.kanbanAggregateOperationFieldMetadataId
|
||||
: undefined,
|
||||
kanbanColumnWidth: shouldCopyFiltersAndSortsAndAggregate
|
||||
? sourceView.kanbanColumnWidth
|
||||
: undefined,
|
||||
mainGroupByFieldMetadataId: shouldCopyFiltersAndSortsAndAggregate
|
||||
? sourceView.mainGroupByFieldMetadataId
|
||||
: mainGroupByFieldMetadataId,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
|
||||
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useUpdateViewKanbanColumnWidth = () => {
|
||||
const { canPersistChanges } = useCanPersistViewChanges();
|
||||
const contextStoreCurrentViewId = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
);
|
||||
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
|
||||
|
||||
const updateViewKanbanColumnWidth = useCallback(
|
||||
async (kanbanColumnWidth: number) => {
|
||||
if (!canPersistChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(contextStoreCurrentViewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await performViewAPIUpdate({
|
||||
id: contextStoreCurrentViewId,
|
||||
input: {
|
||||
kanbanColumnWidth,
|
||||
},
|
||||
});
|
||||
},
|
||||
[canPersistChanges, contextStoreCurrentViewId, performViewAPIUpdate],
|
||||
);
|
||||
|
||||
return {
|
||||
updateViewKanbanColumnWidth,
|
||||
};
|
||||
};
|
||||
@@ -19,6 +19,7 @@ export type GraphQLView = {
|
||||
key?: ViewKey | null;
|
||||
mainGroupByFieldMetadataId?: string | null;
|
||||
shouldHideEmptyGroups: boolean;
|
||||
kanbanColumnWidth?: number | null;
|
||||
kanbanAggregateOperation?: AggregateOperations | null;
|
||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||
objectMetadataId: string;
|
||||
|
||||
@@ -30,6 +30,7 @@ export type View = {
|
||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||
mainGroupByFieldMetadataId?: string | null;
|
||||
shouldHideEmptyGroups: boolean;
|
||||
kanbanColumnWidth?: number | null;
|
||||
calendarFieldMetadataId?: string | null;
|
||||
calendarLayout?: ViewCalendarLayout | null;
|
||||
position: number;
|
||||
|
||||
@@ -36,6 +36,9 @@ export const convertUpdateViewInputToGql = (
|
||||
...(isDefined(view.shouldHideEmptyGroups) && {
|
||||
shouldHideEmptyGroups: view.shouldHideEmptyGroups,
|
||||
}),
|
||||
...(view.kanbanColumnWidth !== undefined && {
|
||||
kanbanColumnWidth: view.kanbanColumnWidth,
|
||||
}),
|
||||
...(view.mainGroupByFieldMetadataId !== undefined && {
|
||||
mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId,
|
||||
}),
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.15.0', 1781900000000)
|
||||
export class AddViewKanbanColumnWidthFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" ADD COLUMN IF NOT EXISTS "kanbanColumnWidth" integer`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."view" DROP COLUMN IF EXISTS "kanbanColumnWidth"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ADD_VIEW_KANBAN_COLUMN_WIDTH_UPGRADE_COMMAND_NAME =
|
||||
'2.15.0_AddViewKanbanColumnWidthFastInstanceCommand_1781900000000';
|
||||
+2
@@ -74,6 +74,7 @@ import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/data
|
||||
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
|
||||
import { AddHasPaymentMethodToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781280240009-add-has-payment-method-to-billing-customer';
|
||||
import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action';
|
||||
import { AddViewKanbanColumnWidthFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781900000000-add-view-kanban-column-width';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -150,4 +151,5 @@ export const INSTANCE_COMMANDS = [
|
||||
AddIsSystemSideEffectFastInstanceCommand,
|
||||
BackfillConnectionSecuritySlowInstanceCommand,
|
||||
AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand,
|
||||
AddViewKanbanColumnWidthFastInstanceCommand,
|
||||
];
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ export const fromViewManifestToUniversalFlatView = ({
|
||||
mainGroupByFieldMetadataUniversalIdentifier:
|
||||
viewManifest.mainGroupByFieldMetadataUniversalIdentifier ?? null,
|
||||
shouldHideEmptyGroups: viewManifest.shouldHideEmptyGroups ?? false,
|
||||
kanbanColumnWidth: viewManifest.kanbanColumnWidth ?? null,
|
||||
anyFieldFilterValue: null,
|
||||
createdByUserWorkspaceId: null,
|
||||
isActive: true,
|
||||
|
||||
+1
@@ -333,6 +333,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"visibility",
|
||||
"mainGroupByFieldMetadataUniversalIdentifier",
|
||||
"shouldHideEmptyGroups",
|
||||
"kanbanColumnWidth",
|
||||
"isActive",
|
||||
"universalOverrides",
|
||||
],
|
||||
|
||||
+6
@@ -376,6 +376,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
kanbanColumnWidth: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
isOverridable: true,
|
||||
},
|
||||
isActive: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
|
||||
+1
@@ -15,4 +15,5 @@ export const FLAT_VIEW_EDITABLE_PROPERTIES = [
|
||||
'visibility',
|
||||
'mainGroupByFieldMetadataId',
|
||||
'shouldHideEmptyGroups',
|
||||
'kanbanColumnWidth',
|
||||
] as const satisfies MetadataEntityPropertyName<'view'>[];
|
||||
|
||||
+1
@@ -76,6 +76,7 @@ export const fromCreateViewInputToFlatViewToCreate = ({
|
||||
icon: createViewInput.icon,
|
||||
isCompact: createViewInput.isCompact ?? false,
|
||||
shouldHideEmptyGroups: createViewInput.shouldHideEmptyGroups ?? false,
|
||||
kanbanColumnWidth: createViewInput.kanbanColumnWidth ?? null,
|
||||
kanbanAggregateOperation: createViewInput.kanbanAggregateOperation ?? null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier,
|
||||
mainGroupByFieldMetadataUniversalIdentifier,
|
||||
|
||||
+1
@@ -692,6 +692,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
calendarLayout: null,
|
||||
isCompact: false,
|
||||
shouldHideEmptyGroups: false,
|
||||
kanbanColumnWidth: null,
|
||||
kanbanAggregateOperation: null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+1
@@ -34,6 +34,7 @@ export const computeFlatRecordPageFieldsViewToCreate = ({
|
||||
calendarLayout: null,
|
||||
isCompact: false,
|
||||
shouldHideEmptyGroups: false,
|
||||
kanbanColumnWidth: null,
|
||||
kanbanAggregateOperation: null,
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier: null,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: null,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const KANBAN_COLUMN_MAX_WIDTH = 400;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const KANBAN_COLUMN_MIN_WIDTH = 150;
|
||||
+13
-1
@@ -1,13 +1,16 @@
|
||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
||||
import { Field, HideField, InputType, Int } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
AggregateOperations,
|
||||
@@ -20,6 +23,8 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { KANBAN_COLUMN_MAX_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-max-width.constant';
|
||||
import { KANBAN_COLUMN_MIN_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-min-width.constant';
|
||||
|
||||
@InputType()
|
||||
export class CreateViewInput {
|
||||
@@ -66,6 +71,13 @@ export class CreateViewInput {
|
||||
@Field({ nullable: true, defaultValue: false })
|
||||
shouldHideEmptyGroups?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(KANBAN_COLUMN_MIN_WIDTH)
|
||||
@Max(KANBAN_COLUMN_MAX_WIDTH)
|
||||
@Field(() => Int, { nullable: true })
|
||||
kanbanColumnWidth?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ViewOpenRecordIn)
|
||||
@Field(() => ViewOpenRecordIn, {
|
||||
|
||||
+13
-1
@@ -1,12 +1,15 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Max,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
AggregateOperations,
|
||||
@@ -18,6 +21,8 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
||||
import { KANBAN_COLUMN_MAX_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-max-width.constant';
|
||||
import { KANBAN_COLUMN_MIN_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-min-width.constant';
|
||||
|
||||
// TODO: this should be refactored like for view-field.input.ts
|
||||
// This is a temporary fix as we were extending the CreateViewInput class which was adding default values for the non filled fields
|
||||
@@ -99,4 +104,11 @@ export class UpdateViewInput {
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
shouldHideEmptyGroups?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(KANBAN_COLUMN_MIN_WIDTH)
|
||||
@Max(KANBAN_COLUMN_MAX_WIDTH)
|
||||
@Field(() => Int, { nullable: true })
|
||||
kanbanColumnWidth?: number | null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Field,
|
||||
HideField,
|
||||
Int,
|
||||
ObjectType,
|
||||
registerEnumType,
|
||||
} from '@nestjs/graphql';
|
||||
@@ -77,6 +78,9 @@ export class ViewDTO {
|
||||
@Field({ nullable: false, defaultValue: false })
|
||||
shouldHideEmptyGroups: boolean;
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
kanbanColumnWidth?: number | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
calendarFieldMetadataId?: string | null;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
|
||||
import { ADD_VIEW_KANBAN_COLUMN_WIDTH_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/add-view-kanban-column-width-upgrade-command-name.constant';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
|
||||
@@ -50,6 +51,7 @@ export type ViewOverrides = {
|
||||
visibility?: ViewVisibility;
|
||||
mainGroupByFieldMetadataId?: SerializedRelation | null;
|
||||
shouldHideEmptyGroups?: boolean;
|
||||
kanbanColumnWidth?: number | null;
|
||||
};
|
||||
|
||||
// We could refactor this type to be dynamic to view type
|
||||
@@ -185,6 +187,12 @@ export class ViewEntity
|
||||
@Column({ nullable: false, default: false, type: 'boolean' })
|
||||
shouldHideEmptyGroups: boolean;
|
||||
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName: ADD_VIEW_KANBAN_COLUMN_WIDTH_UPGRADE_COMMAND_NAME,
|
||||
})
|
||||
@Column({ nullable: true, type: 'int', default: null })
|
||||
kanbanColumnWidth: number | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+1
@@ -137,6 +137,7 @@ export const createStandardViewFlatMetadata = <
|
||||
kanbanAggregateOperationFieldMetadataId,
|
||||
mainGroupByFieldMetadataId,
|
||||
shouldHideEmptyGroups: false,
|
||||
kanbanColumnWidth: null,
|
||||
calendarLayout: null,
|
||||
calendarFieldMetadataId,
|
||||
anyFieldFilterValue: null,
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ type Assertions = [
|
||||
| 'calendarFieldMetadataUniversalIdentifier'
|
||||
| 'mainGroupByFieldMetadataUniversalIdentifier'
|
||||
| 'shouldHideEmptyGroups'
|
||||
| 'kanbanColumnWidth'
|
||||
| 'anyFieldFilterValue'
|
||||
| 'visibility'
|
||||
| 'createdByUserWorkspaceId'
|
||||
|
||||
+5
@@ -1,3 +1,4 @@
|
||||
import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
@@ -92,11 +93,14 @@ describe('Update core view', () => {
|
||||
type: ViewType.KANBAN,
|
||||
mainGroupByFieldMetadataId: testSelectFieldMetadataId,
|
||||
isCompact: true,
|
||||
kanbanColumnWidth: 250,
|
||||
};
|
||||
|
||||
const { data, errors } = await updateOneView({
|
||||
viewId: view.id,
|
||||
input: updateInput,
|
||||
gqlFields: `${VIEW_GQL_FIELDS}
|
||||
kanbanColumnWidth`,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
@@ -106,6 +110,7 @@ describe('Update core view', () => {
|
||||
name: 'Updated View',
|
||||
type: ViewType.KANBAN,
|
||||
isCompact: true,
|
||||
kanbanColumnWidth: 250,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export type ViewManifest = SyncableEntityOptions & {
|
||||
openRecordIn?: ViewOpenRecordIn;
|
||||
mainGroupByFieldMetadataUniversalIdentifier?: string;
|
||||
shouldHideEmptyGroups?: boolean;
|
||||
kanbanColumnWidth?: number | null;
|
||||
kanbanAggregateOperation?: AggregateOperations;
|
||||
kanbanAggregateOperationFieldMetadataUniversalIdentifier?: string;
|
||||
calendarLayout?: ViewCalendarLayout;
|
||||
|
||||
Reference in New Issue
Block a user