mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
Board improvements (#3694)
* New board improvements * Improve board * Fix
This commit is contained in:
parent
511627ccb8
commit
f68de1a299
@ -126,7 +126,7 @@
|
|||||||
"react-error-boundary": "^4.0.11",
|
"react-error-boundary": "^4.0.11",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-hook-form": "^7.45.1",
|
"react-hook-form": "^7.45.1",
|
||||||
"react-hotkeys-hook": "^4.4.3",
|
"react-hotkeys-hook": "^4.4.4",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-intersection-observer": "^9.5.2",
|
"react-intersection-observer": "^9.5.2",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.3.1",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
@ -124,7 +125,7 @@ export const CommandMenu = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
closeCommandMenu();
|
closeCommandMenu();
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -22,6 +23,7 @@ export const KeyboardShortcutMenu = () => {
|
|||||||
isKeyboardShortcutMenuOpenedState,
|
isKeyboardShortcutMenuOpenedState,
|
||||||
);
|
);
|
||||||
const { closeCommandMenu } = useCommandMenu();
|
const { closeCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'shift+?,meta+?',
|
'shift+?,meta+?',
|
||||||
() => {
|
() => {
|
||||||
@ -33,12 +35,12 @@ export const KeyboardShortcutMenu = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
closeKeyboardShortcutMenu();
|
closeKeyboardShortcutMenu();
|
||||||
},
|
},
|
||||||
AppHotkeyScope.KeyboardShortcutMenu,
|
AppHotkeyScope.KeyboardShortcutMenuOpen,
|
||||||
[toggleKeyboardShortcutMenu],
|
[closeKeyboardShortcutMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -46,8 +48,8 @@ export const KeyboardShortcutMenu = () => {
|
|||||||
{isKeyboardShortcutMenuOpened && (
|
{isKeyboardShortcutMenuOpened && (
|
||||||
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
|
<KeyboardMenuDialog onClose={toggleKeyboardShortcutMenu}>
|
||||||
<KeyboardMenuGroup heading="Table">
|
<KeyboardMenuGroup heading="Table">
|
||||||
{keyboardShortcutsTable.map((TableShortcut) => (
|
{keyboardShortcutsTable.map((TableShortcut, index) => (
|
||||||
<KeyboardMenuItem shortcut={TableShortcut} />
|
<KeyboardMenuItem shortcut={TableShortcut} key={index} />
|
||||||
))}
|
))}
|
||||||
</KeyboardMenuGroup>
|
</KeyboardMenuGroup>
|
||||||
<KeyboardMenuGroup heading="General">
|
<KeyboardMenuGroup heading="General">
|
||||||
|
@ -12,9 +12,7 @@ import { useRecordBoardDeprecatedScopedStates } from '@/object-record/record-boa
|
|||||||
import { useSetRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useSetRecordBoardDeprecatedCardSelectedInternal';
|
import { useSetRecordBoardDeprecatedCardSelectedInternal } from '@/object-record/record-board-deprecated/hooks/internal/useSetRecordBoardDeprecatedCardSelectedInternal';
|
||||||
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
|
import { RecordBoardDeprecatedScope } from '@/object-record/record-board-deprecated/scopes/RecordBoardDeprecatedScope';
|
||||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
@ -129,12 +127,6 @@ export const RecordBoardDeprecated = ({
|
|||||||
|
|
||||||
const boardRef = useRef<HTMLDivElement>(null);
|
const boardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
'escape',
|
|
||||||
unselectAllActiveCards,
|
|
||||||
PageHotkeyScope.OpportunitiesPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordBoardDeprecatedScope recordBoardScopeId={recordBoardId}>
|
<RecordBoardDeprecatedScope recordBoardScopeId={recordBoardId}>
|
||||||
<RecordBoardDeprecatedContextMenu />
|
<RecordBoardDeprecatedContextMenu />
|
||||||
|
@ -69,7 +69,9 @@ export const RecordBoardDeprecatedColumnDropdownMenu = ({
|
|||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape, Key.Enter],
|
[Key.Escape, Key.Enter],
|
||||||
closeMenu,
|
() => {
|
||||||
|
closeMenu();
|
||||||
|
},
|
||||||
BoardColumnHotkeyScope.BoardColumn,
|
BoardColumnHotkeyScope.BoardColumn,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
@ -123,7 +123,7 @@ export const RecordBoardDeprecatedOptionsDropdownContent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
setViewEditMode('none');
|
setViewEditMode('none');
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
@ -2,15 +2,19 @@ import { useContext, useRef } from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||||
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn';
|
||||||
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope';
|
||||||
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export type RecordBoardProps = {
|
export type RecordBoardProps = {
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
@ -40,17 +44,25 @@ const StyledBoardHeader = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
|
export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
|
||||||
const { updateOneRecord, objectMetadataItem } =
|
const { updateOneRecord, selectFieldMetadataItem } =
|
||||||
useContext(RecordBoardContext);
|
useContext(RecordBoardContext);
|
||||||
const boardRef = useRef<HTMLDivElement>(null);
|
const boardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { getColumnIdsState, columnsFamilySelector } =
|
const { getColumnIdsState, columnsFamilySelector } =
|
||||||
useRecordBoardStates(recordBoardId);
|
useRecordBoardStates(recordBoardId);
|
||||||
|
|
||||||
const columnIds = useRecoilValue(getColumnIdsState());
|
const columnIds = useRecoilValue(getColumnIdsState());
|
||||||
|
|
||||||
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
const { resetRecordSelection, setRecordAsSelected } =
|
||||||
(field) => field.type === FieldMetadataType.Select,
|
useRecordBoardSelection(recordBoardId);
|
||||||
);
|
|
||||||
|
useListenClickOutsideByClassName({
|
||||||
|
classNames: ['record-board-card'],
|
||||||
|
excludeClassNames: ['action-bar', 'context-menu'],
|
||||||
|
callback: resetRecordSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
useScopedHotkeys([Key.Escape], resetRecordSelection, TableHotkeyScope.Table);
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = useRecoilCallback(
|
const onDragEnd: OnDragEndResponder = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
@ -106,7 +118,8 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
|
|||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
<DragSelect
|
<DragSelect
|
||||||
dragSelectable={boardRef}
|
dragSelectable={boardRef}
|
||||||
onDragSelectionChange={() => {}}
|
onDragSelectionStart={resetRecordSelection}
|
||||||
|
onDragSelectionChange={setRecordAsSelected}
|
||||||
/>
|
/>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
</RecordBoardScope>
|
</RecordBoardScope>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
type RecordBoardContextProps = {
|
type RecordBoardContextProps = {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
selectFieldMetadataItem: FieldMetadataItem;
|
||||||
createOneRecord: (recordInput: Partial<ObjectRecord>) => void;
|
createOneRecord: (recordInput: Partial<ObjectRecord>) => void;
|
||||||
updateOneRecord: ({
|
updateOneRecord: ({
|
||||||
idToUpdate,
|
idToUpdate,
|
||||||
|
@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
|
|
||||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||||
|
|
||||||
export const useResetBoardRecordSelection = (recordBoardId?: string) => {
|
export const useRecordBoardSelection = (recordBoardId?: string) => {
|
||||||
const { getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } =
|
const { getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } =
|
||||||
useRecordBoardStates(recordBoardId);
|
useRecordBoardStates(recordBoardId);
|
||||||
|
|
||||||
@ -20,5 +20,21 @@ export const useResetBoardRecordSelection = (recordBoardId?: string) => {
|
|||||||
[getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState],
|
[getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { resetRecordSelection };
|
const setRecordAsSelected = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
(recordId: string, isSelected: boolean) => {
|
||||||
|
const isRecordCurrentlySelected = snapshot
|
||||||
|
.getLoadable(isRecordBoardCardSelectedFamilyState(recordId))
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (isRecordCurrentlySelected === isSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(isRecordBoardCardSelectedFamilyState(recordId), isSelected);
|
||||||
|
},
|
||||||
|
[isRecordBoardCardSelectedFamilyState],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { resetRecordSelection, setRecordAsSelected };
|
||||||
};
|
};
|
@ -18,7 +18,7 @@ export const RecordBoardCardDraggableContainer = ({
|
|||||||
{...draggableProvided?.dragHandleProps}
|
{...draggableProvided?.dragHandleProps}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...draggableProvided?.draggableProps}
|
{...draggableProvided?.draggableProps}
|
||||||
className="entity-board-card"
|
className="record-board-card"
|
||||||
data-selectable-id={recordId}
|
data-selectable-id={recordId}
|
||||||
data-select-disable
|
data-select-disable
|
||||||
>
|
>
|
||||||
|
@ -14,6 +14,7 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
|
|||||||
isFirstColumn ? 'none' : theme.border.color.light};
|
isFirstColumn ? 'none' : theme.border.color.light};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: fit-content;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ export const RecordBoardColumn = ({
|
|||||||
columnDefinition: columnDefinition,
|
columnDefinition: columnDefinition,
|
||||||
isFirstColumn: isFirstColumn,
|
isFirstColumn: isFirstColumn,
|
||||||
isLastColumn: isLastColumn,
|
isLastColumn: isLastColumn,
|
||||||
|
recordCount: recordIds.length,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Droppable droppableId={recordBoardColumnId}>
|
<Droppable droppableId={recordBoardColumnId}>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DroppableProvided } from '@hello-pangea/dnd';
|
import { Draggable, DroppableProvided } from '@hello-pangea/dnd';
|
||||||
|
|
||||||
import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
|
import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo';
|
||||||
|
import { RecordBoardColumnNewButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton';
|
||||||
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
|
|
||||||
const StyledPlaceholder = styled.div`
|
const StyledPlaceholder = styled.div`
|
||||||
min-height: 1px;
|
min-height: 1px;
|
||||||
@ -14,6 +16,10 @@ const StyledColumnCardsContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledNewButtonContainer = styled.div`
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
type RecordBoardColumnCardsContainerProps = {
|
type RecordBoardColumnCardsContainerProps = {
|
||||||
recordIds: string[];
|
recordIds: string[];
|
||||||
droppableProvided: DroppableProvided;
|
droppableProvided: DroppableProvided;
|
||||||
@ -23,6 +29,8 @@ export const RecordBoardColumnCardsContainer = ({
|
|||||||
recordIds,
|
recordIds,
|
||||||
droppableProvided,
|
droppableProvided,
|
||||||
}: RecordBoardColumnCardsContainerProps) => {
|
}: RecordBoardColumnCardsContainerProps) => {
|
||||||
|
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledColumnCardsContainer
|
<StyledColumnCardsContainer
|
||||||
ref={droppableProvided?.innerRef}
|
ref={droppableProvided?.innerRef}
|
||||||
@ -31,6 +39,23 @@ export const RecordBoardColumnCardsContainer = ({
|
|||||||
>
|
>
|
||||||
<RecordBoardColumnCardsMemo recordIds={recordIds} />
|
<RecordBoardColumnCardsMemo recordIds={recordIds} />
|
||||||
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
||||||
|
<Draggable
|
||||||
|
draggableId={`new-${columnDefinition.id}`}
|
||||||
|
index={recordIds.length}
|
||||||
|
isDragDisabled={true}
|
||||||
|
>
|
||||||
|
{(draggableProvided) => (
|
||||||
|
<div
|
||||||
|
ref={draggableProvided?.innerRef}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...draggableProvided?.draggableProps}
|
||||||
|
>
|
||||||
|
<StyledNewButtonContainer>
|
||||||
|
<RecordBoardColumnNewButton />
|
||||||
|
</StyledNewButtonContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
</StyledColumnCardsContainer>
|
</StyledColumnCardsContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -46,7 +46,9 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
|
||||||
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
|
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
|
||||||
|
|
||||||
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
const { columnDefinition, recordCount } = useContext(
|
||||||
|
RecordBoardColumnContext,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
@ -66,7 +68,6 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const boardColumnTotal = 0;
|
const boardColumnTotal = 0;
|
||||||
const cardIds = [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -81,7 +82,7 @@ export const RecordBoardColumnHeader = () => {
|
|||||||
/>
|
/>
|
||||||
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
|
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
|
||||||
{!isHeaderHovered && (
|
{!isHeaderHovered && (
|
||||||
<StyledNumChildren>{cardIds.length}</StyledNumChildren>
|
<StyledNumChildren>{recordCount}</StyledNumChildren>
|
||||||
)}
|
)}
|
||||||
{isHeaderHovered && (
|
{isHeaderHovered && (
|
||||||
<StyledHeaderActions>
|
<StyledHeaderActions>
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
|
import { IconPlus } from '@/ui/display/icon/index';
|
||||||
|
|
||||||
|
const StyledButton = styled.button`
|
||||||
|
align-items: center;
|
||||||
|
align-self: baseline;
|
||||||
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
|
border: none;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme }) => theme.background.tertiary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RecordBoardColumnNewButton = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { columnDefinition } = useContext(RecordBoardColumnContext);
|
||||||
|
const { createOneRecord, selectFieldMetadataItem } =
|
||||||
|
useContext(RecordBoardContext);
|
||||||
|
|
||||||
|
const onNewClick = () => {
|
||||||
|
createOneRecord({
|
||||||
|
[selectFieldMetadataItem.name]: columnDefinition.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledButton onClick={onNewClick}>
|
||||||
|
<IconPlus size={theme.icon.size.md} />
|
||||||
|
New
|
||||||
|
</StyledButton>
|
||||||
|
);
|
||||||
|
};
|
@ -6,6 +6,7 @@ type RecordBoardColumnContextProps = {
|
|||||||
columnDefinition: RecordBoardColumnDefinition;
|
columnDefinition: RecordBoardColumnDefinition;
|
||||||
isFirstColumn: boolean;
|
isFirstColumn: boolean;
|
||||||
isLastColumn: boolean;
|
isLastColumn: boolean;
|
||||||
|
recordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordBoardColumnContext =
|
export const RecordBoardColumnContext =
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -41,7 +43,7 @@ export const useRegisterInputEvents = <T>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
onEscape?.(inputValue);
|
onEscape?.(inputValue);
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,7 @@ import { RecordBoardActionBar } from '@/object-record/record-board/action-bar/co
|
|||||||
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
|
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
|
||||||
import { RecordBoardContextMenu } from '@/object-record/record-board/context-menu/components/RecordBoardContextMenu';
|
import { RecordBoardContextMenu } from '@/object-record/record-board/context-menu/components/RecordBoardContextMenu';
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
type RecordIndexBoardContainerProps = {
|
type RecordIndexBoardContainerProps = {
|
||||||
recordBoardId: string;
|
recordBoardId: string;
|
||||||
@ -20,14 +21,23 @@ export const RecordIndexBoardContainer = ({
|
|||||||
}: RecordIndexBoardContainerProps) => {
|
}: RecordIndexBoardContainerProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
|
const selectFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.type === FieldMetadataType.Select,
|
||||||
|
);
|
||||||
|
|
||||||
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });
|
const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular });
|
||||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||||
const { createOneRecord } = useCreateOneRecord({ objectNameSingular });
|
const { createOneRecord } = useCreateOneRecord({ objectNameSingular });
|
||||||
|
|
||||||
|
if (!selectFieldMetadataItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordBoardContext.Provider
|
<RecordBoardContext.Provider
|
||||||
value={{
|
value={{
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
selectFieldMetadataItem,
|
||||||
createOneRecord,
|
createOneRecord,
|
||||||
updateOneRecord,
|
updateOneRecord,
|
||||||
deleteOneRecord,
|
deleteOneRecord,
|
||||||
|
@ -5,8 +5,9 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||||
import { useResetBoardRecordSelection } from '@/object-record/record-board/hooks/useResetBoardRecordSelection';
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||||
import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard';
|
import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard';
|
||||||
|
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||||
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
|
||||||
|
|
||||||
type RecordIndexBoardContainerEffectProps = {
|
type RecordIndexBoardContainerEffectProps = {
|
||||||
@ -18,12 +19,13 @@ type RecordIndexBoardContainerEffectProps = {
|
|||||||
export const RecordIndexBoardContainerEffect = ({
|
export const RecordIndexBoardContainerEffect = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordBoardId,
|
recordBoardId,
|
||||||
|
viewBarId,
|
||||||
}: RecordIndexBoardContainerEffectProps) => {
|
}: RecordIndexBoardContainerEffectProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
useLoadRecordIndexBoard(objectNameSingular, recordBoardId);
|
useLoadRecordIndexBoard({ objectNameSingular, recordBoardId, viewBarId });
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -31,9 +33,13 @@ export const RecordIndexBoardContainerEffect = ({
|
|||||||
navigate(`/settings/objects/${objectMetadataItem.namePlural}`);
|
navigate(`/settings/objects/${objectMetadataItem.namePlural}`);
|
||||||
}, [navigate, objectMetadataItem.namePlural]);
|
}, [navigate, objectMetadataItem.namePlural]);
|
||||||
|
|
||||||
const { setColumns, setObjectSingularName, getSelectedRecordIdsSelector } =
|
const {
|
||||||
useRecordBoard(recordBoardId);
|
setColumns,
|
||||||
const { resetRecordSelection } = useResetBoardRecordSelection(recordBoardId);
|
setObjectSingularName,
|
||||||
|
getSelectedRecordIdsSelector,
|
||||||
|
setFieldDefinitions,
|
||||||
|
} = useRecordBoard(recordBoardId);
|
||||||
|
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setObjectSingularName(objectNameSingular);
|
setObjectSingularName(objectNameSingular);
|
||||||
@ -53,6 +59,14 @@ export const RecordIndexBoardContainerEffect = ({
|
|||||||
setColumns,
|
setColumns,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const recordIndexFieldDefinitions = useRecoilValue(
|
||||||
|
recordIndexFieldDefinitionsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFieldDefinitions(recordIndexFieldDefinitions);
|
||||||
|
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
|
||||||
|
|
||||||
const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector());
|
const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector());
|
||||||
|
|
||||||
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
||||||
|
@ -10,11 +10,19 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s
|
|||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||||
|
import { useViewBar } from '@/views/hooks/useViewBar';
|
||||||
|
|
||||||
export const useLoadRecordIndexBoard = (
|
type UseLoadRecordIndexBoardProps = {
|
||||||
objectNameSingular: string,
|
objectNameSingular: string;
|
||||||
recordBoardId: string,
|
viewBarId: string;
|
||||||
) => {
|
recordBoardId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLoadRecordIndexBoard = ({
|
||||||
|
objectNameSingular,
|
||||||
|
viewBarId,
|
||||||
|
recordBoardId,
|
||||||
|
}: UseLoadRecordIndexBoardProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
@ -47,6 +55,10 @@ export const useLoadRecordIndexBoard = (
|
|||||||
orderBy,
|
orderBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setEntityCountInCurrentView } = useViewBar({
|
||||||
|
viewBarId,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecordIdsInBoard(records);
|
setRecordIdsInBoard(records);
|
||||||
}, [records, setRecordIdsInBoard]);
|
}, [records, setRecordIdsInBoard]);
|
||||||
@ -55,6 +67,10 @@ export const useLoadRecordIndexBoard = (
|
|||||||
setRecordsInStore(records);
|
setRecordsInStore(records);
|
||||||
}, [records, setRecordsInStore]);
|
}, [records, setRecordsInStore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEntityCountInCurrentView(records.length);
|
||||||
|
}, [records.length, setEntityCountInCurrentView]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records,
|
records,
|
||||||
loading,
|
loading,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -28,7 +30,7 @@ export const RecordTableInternalEffect = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'escape',
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
},
|
},
|
||||||
|
@ -80,7 +80,7 @@ export const TableOptionsDropdownContent = ({
|
|||||||
const resetMenu = () => setCurrentMenu(undefined);
|
const resetMenu = () => setCurrentMenu(undefined);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,7 @@ export const SingleEntitySelectMenuItems = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
},
|
},
|
||||||
|
@ -94,7 +94,7 @@ export const DoubleTextInput = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
onEscape({
|
onEscape({
|
||||||
firstValue: firstInternalValue,
|
firstValue: firstInternalValue,
|
||||||
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
|
import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { IconArrowRight } from '@/ui/display/icon/index';
|
import { IconArrowRight } from '@/ui/display/icon/index';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
@ -151,7 +152,7 @@ export const AutosizeTextInput = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
Key.Escape,
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
if (!isFocused) {
|
if (!isFocused) {
|
||||||
return;
|
return;
|
||||||
|
@ -94,7 +94,7 @@ export const Dropdown = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Escape,
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
},
|
},
|
||||||
|
@ -66,7 +66,10 @@ export const RightDrawer = () => {
|
|||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape],
|
[Key.Escape],
|
||||||
() => closeRightDrawer(),
|
|
||||||
|
() => {
|
||||||
|
closeRightDrawer();
|
||||||
|
},
|
||||||
RightDrawerHotkeyScope.RightDrawer,
|
RightDrawerHotkeyScope.RightDrawer,
|
||||||
[setIsRightDrawerOpen],
|
[setIsRightDrawerOpen],
|
||||||
);
|
);
|
||||||
|
@ -4,4 +4,5 @@ export enum AppHotkeyScope {
|
|||||||
CommandMenu = 'command-menu',
|
CommandMenu = 'command-menu',
|
||||||
CommandMenuOpen = 'command-menu-open',
|
CommandMenuOpen = 'command-menu-open',
|
||||||
KeyboardShortcutMenu = 'keyboard-shortcut-menu',
|
KeyboardShortcutMenu = 'keyboard-shortcut-menu',
|
||||||
|
KeyboardShortcutMenuOpen = 'keyboard-shortcut-menu-open',
|
||||||
}
|
}
|
||||||
|
@ -127,8 +127,9 @@ export const useViewBar = (props?: UseViewProps) => {
|
|||||||
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
if (!isDeeplyEqual(savedViewFields, queriedViewFields)) {
|
||||||
set(currentViewFieldsState, queriedViewFields);
|
set(currentViewFieldsState, queriedViewFields);
|
||||||
set(savedViewFieldsState, queriedViewFields);
|
set(savedViewFieldsState, queriedViewFields);
|
||||||
onViewFieldsChange?.(queriedViewFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onViewFieldsChange?.(queriedViewFields);
|
||||||
},
|
},
|
||||||
[scopeId],
|
[scopeId],
|
||||||
);
|
);
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -38381,13 +38381,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-hotkeys-hook@npm:^4.4.3":
|
"react-hotkeys-hook@npm:^4.4.4":
|
||||||
version: 4.4.3
|
version: 4.4.4
|
||||||
resolution: "react-hotkeys-hook@npm:4.4.3"
|
resolution: "react-hotkeys-hook@npm:4.4.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">=16.8.1"
|
react: ">=16.8.1"
|
||||||
react-dom: ">=16.8.1"
|
react-dom: ">=16.8.1"
|
||||||
checksum: ef79e279129f6e55d81c8762b1da214d9c6ee4617b9597dbc3f93057cba8d166831508967b8e3f763a0c4ce0af3b59e6888c6fc94d152deac9335020b2ba80df
|
checksum: afe7418c8bd0ecd3a3f315648b84d9978d06a02e55f93df23aaa00e56613fca839f9ff4d10387e8336454702b4aa03cef97f3e67bfe3e121e42f16d84685f55a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -43453,7 +43453,7 @@ __metadata:
|
|||||||
react-error-boundary: "npm:^4.0.11"
|
react-error-boundary: "npm:^4.0.11"
|
||||||
react-helmet-async: "npm:^1.3.0"
|
react-helmet-async: "npm:^1.3.0"
|
||||||
react-hook-form: "npm:^7.45.1"
|
react-hook-form: "npm:^7.45.1"
|
||||||
react-hotkeys-hook: "npm:^4.4.3"
|
react-hotkeys-hook: "npm:^4.4.4"
|
||||||
react-icons: "npm:^4.12.0"
|
react-icons: "npm:^4.12.0"
|
||||||
react-intersection-observer: "npm:^9.5.2"
|
react-intersection-observer: "npm:^9.5.2"
|
||||||
react-loading-skeleton: "npm:^3.3.1"
|
react-loading-skeleton: "npm:^3.3.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user