148 cant access note without title from kanban board (#9817)
closes https://github.com/twentyhq/core-team-issues/issues/148 - fixes not openable kanban card when identifier empty - update card behavior (onClick open recordPage) - fixes right click actionDropdown position ## Before https://github.com/user-attachments/assets/696194b8-d7fa-4fc1-a6f9-b46241a262e5  ## After https://github.com/user-attachments/assets/41e296e5-ae16-47f8-b174-7dd21d74188d 
This commit is contained in:
@ -2,48 +2,29 @@ import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
|
|||||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
|
||||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
|
||||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||||
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 { 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 { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
|
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
|
||||||
import {
|
|
||||||
FieldContext,
|
|
||||||
RecordUpdateHook,
|
|
||||||
RecordUpdateHookParams,
|
|
||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
|
||||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
|
||||||
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
|
||||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
|
||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ReactNode, useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { InView, useInView } from 'react-intersection-observer';
|
import { InView, useInView } from 'react-intersection-observer';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import {
|
import { AnimatedEaseInOut } from 'twenty-ui';
|
||||||
AnimatedEaseInOut,
|
|
||||||
AvatarChipVariant,
|
|
||||||
Checkbox,
|
|
||||||
CheckboxVariant,
|
|
||||||
IconEye,
|
|
||||||
IconEyeOff,
|
|
||||||
LightIconButton,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useAddNewCard } from '../../record-board-column/hooks/useAddNewCard';
|
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
|
||||||
|
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
|
||||||
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
|
||||||
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||||
background-color: ${({ theme, selected }) =>
|
background-color: ${({ theme, selected }) =>
|
||||||
@ -81,76 +62,11 @@ const StyledBoardCard = styled.div<{ selected: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTextInput = styled(TextInput)`
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
width: ${({ theme }) => theme.spacing(53)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledBoardCardWrapper = styled.div`
|
const StyledBoardCardWrapper = styled.div`
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledBoardCardHeader = styled.div<{
|
|
||||||
showCompactView: boolean;
|
|
||||||
}>`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
height: 24px;
|
|
||||||
padding-bottom: ${({ theme, showCompactView }) =>
|
|
||||||
theme.spacing(showCompactView ? 2 : 1)};
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
transition: padding ease-in-out 160ms;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: ${({ theme }) => theme.icon.size.md}px;
|
|
||||||
object-fit: cover;
|
|
||||||
width: ${({ theme }) => theme.icon.size.md}px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledBoardCardBody = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
|
||||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(2.5)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
span {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
svg {
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCheckboxContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: end;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledFieldContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledCompactIconContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RecordBoardCard = ({
|
export const RecordBoardCard = ({
|
||||||
isCreating = false,
|
isCreating = false,
|
||||||
onCreateSuccess,
|
onCreateSuccess,
|
||||||
@ -160,15 +76,10 @@ export const RecordBoardCard = ({
|
|||||||
onCreateSuccess?: () => void;
|
onCreateSuccess?: () => void;
|
||||||
position?: 'first' | 'last';
|
position?: 'first' | 'last';
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigateApp();
|
||||||
|
|
||||||
const { recordId } = useContext(RecordBoardCardContext);
|
const { recordId } = useContext(RecordBoardCardContext);
|
||||||
|
|
||||||
const [newLabelValue, setNewLabelValue] = useState('');
|
|
||||||
|
|
||||||
const { handleBlur, handleInputEnter } = useAddNewCard();
|
|
||||||
|
|
||||||
const { updateOneRecord, objectMetadataItem } =
|
|
||||||
useContext(RecordBoardContext);
|
|
||||||
|
|
||||||
const visibleFieldDefinitions = useRecoilComponentValueV2(
|
const visibleFieldDefinitions = useRecoilComponentValueV2(
|
||||||
recordBoardVisibleFieldDefinitionsComponentSelector,
|
recordBoardVisibleFieldDefinitionsComponentSelector,
|
||||||
);
|
);
|
||||||
@ -185,16 +96,12 @@ export const RecordBoardCard = ({
|
|||||||
recordId,
|
recordId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const record = useRecoilValue(recordStoreFamilyState(recordId));
|
const { objectNameSingular } = useRecordIndexContextOrThrow();
|
||||||
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
|
|
||||||
|
|
||||||
const recordBoardId = useAvailableScopeIdOrThrow(
|
const recordBoardId = useAvailableScopeIdOrThrow(
|
||||||
RecordBoardScopeInternalContext,
|
RecordBoardScopeInternalContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { checkIfLastUnselectAndCloseDropdown } =
|
|
||||||
useRecordBoardSelection(recordBoardId);
|
|
||||||
|
|
||||||
const actionMenuId = getActionMenuIdFromRecordIndexId(recordBoardId);
|
const actionMenuId = getActionMenuIdFromRecordIndexId(recordBoardId);
|
||||||
|
|
||||||
const actionMenuDropdownId =
|
const actionMenuDropdownId =
|
||||||
@ -221,42 +128,19 @@ export const RecordBoardCard = ({
|
|||||||
|
|
||||||
const handleCardClick = () => {
|
const handleCardClick = () => {
|
||||||
if (!isCreating) {
|
if (!isCreating) {
|
||||||
setIsCurrentCardSelected(!isCurrentCardSelected);
|
navigate(AppPath.RecordShowPage, {
|
||||||
checkIfLastUnselectAndCloseDropdown();
|
objectNameSingular,
|
||||||
|
objectRecordId: recordId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const PreventSelectOnClickContainer = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}) => (
|
|
||||||
<StyledFieldContainer
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledFieldContainer>
|
|
||||||
);
|
|
||||||
|
|
||||||
const onMouseLeaveBoard = useDebouncedCallback(() => {
|
const onMouseLeaveBoard = useDebouncedCallback(() => {
|
||||||
if (isCompactModeActive && isCardExpanded) {
|
if (isCompactModeActive && isCardExpanded) {
|
||||||
setIsCardExpanded(false);
|
setIsCardExpanded(false);
|
||||||
}
|
}
|
||||||
}, 800);
|
}, 800);
|
||||||
|
|
||||||
const useUpdateOneRecordHook: RecordUpdateHook = () => {
|
|
||||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
|
||||||
updateOneRecord?.({
|
|
||||||
idToUpdate: variables.where.id as string,
|
|
||||||
updateOneRecordInput: variables.updateOneRecordInput,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return [updateEntity, { loading: false }];
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext);
|
const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext);
|
||||||
|
|
||||||
const { ref: cardRef } = useInView({
|
const { ref: cardRef } = useInView({
|
||||||
@ -285,110 +169,23 @@ export const RecordBoardCard = ({
|
|||||||
onMouseLeave={onMouseLeaveBoard}
|
onMouseLeave={onMouseLeaveBoard}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
|
{labelIdentifierField && (
|
||||||
{isCreating && position !== undefined ? (
|
<RecordBoardCardHeader
|
||||||
<RecordInlineCellEditMode>
|
identifierFieldDefinition={labelIdentifierField}
|
||||||
<StyledTextInput
|
isCreating={isCreating}
|
||||||
autoFocus
|
onCreateSuccess={onCreateSuccess}
|
||||||
value={newLabelValue}
|
position={position}
|
||||||
onInputEnter={() =>
|
isCardExpanded={isCardExpanded}
|
||||||
handleInputEnter(
|
setIsCardExpanded={setIsCardExpanded}
|
||||||
labelIdentifierField?.label ?? '',
|
/>
|
||||||
newLabelValue,
|
)}
|
||||||
position,
|
|
||||||
onCreateSuccess,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onBlur={() =>
|
|
||||||
handleBlur(
|
|
||||||
labelIdentifierField?.label ?? '',
|
|
||||||
newLabelValue,
|
|
||||||
position,
|
|
||||||
onCreateSuccess,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onChange={(text: string) => setNewLabelValue(text)}
|
|
||||||
placeholder={labelIdentifierField?.label}
|
|
||||||
/>
|
|
||||||
</RecordInlineCellEditMode>
|
|
||||||
) : (
|
|
||||||
<RecordIdentifierChip
|
|
||||||
objectNameSingular={objectMetadataItem.nameSingular}
|
|
||||||
record={record as ObjectRecord}
|
|
||||||
variant={AvatarChipVariant.Transparent}
|
|
||||||
maxWidth={150}
|
|
||||||
to={indexIdentifierUrl(recordId)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isCreating && (
|
|
||||||
<>
|
|
||||||
{isCompactModeActive && (
|
|
||||||
<StyledCompactIconContainer className="compact-icon-container">
|
|
||||||
<LightIconButton
|
|
||||||
Icon={isCardExpanded ? IconEyeOff : IconEye}
|
|
||||||
accent="tertiary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsCardExpanded((prev) => !prev);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledCompactIconContainer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<StyledCheckboxContainer className="checkbox-container">
|
|
||||||
<Checkbox
|
|
||||||
hoverable
|
|
||||||
checked={isCurrentCardSelected}
|
|
||||||
onChange={() =>
|
|
||||||
setIsCurrentCardSelected(!isCurrentCardSelected)
|
|
||||||
}
|
|
||||||
variant={CheckboxVariant.Secondary}
|
|
||||||
/>
|
|
||||||
</StyledCheckboxContainer>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</StyledBoardCardHeader>
|
|
||||||
|
|
||||||
<AnimatedEaseInOut
|
<AnimatedEaseInOut
|
||||||
isOpen={isCardExpanded || !isCompactModeActive}
|
isOpen={isCardExpanded || !isCompactModeActive}
|
||||||
initial={false}
|
initial={false}
|
||||||
>
|
>
|
||||||
<StyledBoardCardBody>
|
<RecordBoardCardBody
|
||||||
{visibleFieldDefinitionsFiltered.map((fieldDefinition) => (
|
fieldDefinitions={visibleFieldDefinitionsFiltered}
|
||||||
<PreventSelectOnClickContainer
|
/>
|
||||||
key={fieldDefinition.fieldMetadataId}
|
|
||||||
>
|
|
||||||
<FieldContext.Provider
|
|
||||||
value={{
|
|
||||||
recordId: isCreating ? '' : recordId,
|
|
||||||
maxWidth: 156,
|
|
||||||
recoilScopeId:
|
|
||||||
(isCreating ? 'new' : recordId) +
|
|
||||||
fieldDefinition.fieldMetadataId,
|
|
||||||
isLabelIdentifier: false,
|
|
||||||
fieldDefinition: {
|
|
||||||
disableTooltip: false,
|
|
||||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
|
||||||
label: fieldDefinition.label,
|
|
||||||
iconName: fieldDefinition.iconName,
|
|
||||||
type: fieldDefinition.type,
|
|
||||||
metadata: fieldDefinition.metadata,
|
|
||||||
defaultValue: fieldDefinition.defaultValue,
|
|
||||||
editButtonIcon: getFieldButtonIcon({
|
|
||||||
metadata: fieldDefinition.metadata,
|
|
||||||
type: fieldDefinition.type,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
useUpdateRecord: useUpdateOneRecordHook,
|
|
||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RecordInlineCell />
|
|
||||||
</FieldContext.Provider>
|
|
||||||
</PreventSelectOnClickContainer>
|
|
||||||
))}
|
|
||||||
</StyledBoardCardBody>
|
|
||||||
</AnimatedEaseInOut>
|
</AnimatedEaseInOut>
|
||||||
</StyledBoardCard>
|
</StyledBoardCard>
|
||||||
</InView>
|
</InView>
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import {
|
||||||
|
FieldContext,
|
||||||
|
RecordUpdateHook,
|
||||||
|
RecordUpdateHookParams,
|
||||||
|
} from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||||
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
|
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
|
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
|
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
|
||||||
|
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||||
|
|
||||||
|
export const RecordBoardCardBody = ({
|
||||||
|
fieldDefinitions,
|
||||||
|
}: {
|
||||||
|
fieldDefinitions: RecordBoardFieldDefinition<FieldMetadata>[];
|
||||||
|
}) => {
|
||||||
|
const { recordId } = useContext(RecordBoardCardContext);
|
||||||
|
|
||||||
|
const { updateOneRecord } = useContext(RecordBoardContext);
|
||||||
|
|
||||||
|
const useUpdateOneRecordHook: RecordUpdateHook = () => {
|
||||||
|
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||||
|
updateOneRecord?.({
|
||||||
|
idToUpdate: variables.where.id as string,
|
||||||
|
updateOneRecordInput: variables.updateOneRecordInput,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return [updateEntity, { loading: false }];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordBoardCardBodyContainer>
|
||||||
|
{fieldDefinitions.map((fieldDefinition) => (
|
||||||
|
<StopPropagationContainer key={fieldDefinition.fieldMetadataId}>
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
recordId,
|
||||||
|
maxWidth: 156,
|
||||||
|
recoilScopeId:
|
||||||
|
(recordId || 'new') + fieldDefinition.fieldMetadataId,
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
fieldDefinition: {
|
||||||
|
disableTooltip: false,
|
||||||
|
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||||
|
label: fieldDefinition.label,
|
||||||
|
iconName: fieldDefinition.iconName,
|
||||||
|
type: fieldDefinition.type,
|
||||||
|
metadata: fieldDefinition.metadata,
|
||||||
|
defaultValue: fieldDefinition.defaultValue,
|
||||||
|
editButtonIcon: getFieldButtonIcon({
|
||||||
|
metadata: fieldDefinition.metadata,
|
||||||
|
type: fieldDefinition.type,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
useUpdateRecord: useUpdateOneRecordHook,
|
||||||
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecordInlineCell />
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</StopPropagationContainer>
|
||||||
|
))}
|
||||||
|
</RecordBoardCardBodyContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledBoardCardBody = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2.5)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
span {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
svg {
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { StyledBoardCardBody as RecordBoardCardBodyContainer };
|
||||||
@ -0,0 +1,218 @@
|
|||||||
|
import {
|
||||||
|
AvatarChipVariant,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxVariant,
|
||||||
|
LightIconButton,
|
||||||
|
IconEye,
|
||||||
|
IconEyeOff,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
|
||||||
|
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { Dispatch, SetStateAction, useContext, useState } from 'react';
|
||||||
|
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||||
|
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import {
|
||||||
|
FieldContext,
|
||||||
|
RecordUpdateHook,
|
||||||
|
RecordUpdateHookParams,
|
||||||
|
} from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||||
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
|
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
|
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||||
|
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
||||||
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
|
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||||
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||||
|
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||||
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||||
|
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
|
||||||
|
|
||||||
|
const StyledTextInput = styled(TextInput)`
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
width: ${({ theme }) => theme.spacing(53)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCompactIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCheckboxContainer = styled.div`
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RecordBoardCardHeaderProps = {
|
||||||
|
isCreating?: boolean;
|
||||||
|
onCreateSuccess?: () => void;
|
||||||
|
position?: 'first' | 'last';
|
||||||
|
identifierFieldDefinition: RecordBoardFieldDefinition<FieldMetadata>;
|
||||||
|
isCardExpanded?: boolean;
|
||||||
|
setIsCardExpanded?: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordBoardCardHeader = ({
|
||||||
|
isCreating = false,
|
||||||
|
onCreateSuccess,
|
||||||
|
position,
|
||||||
|
identifierFieldDefinition,
|
||||||
|
isCardExpanded,
|
||||||
|
setIsCardExpanded,
|
||||||
|
}: RecordBoardCardHeaderProps) => {
|
||||||
|
const [newLabelValue, setNewLabelValue] = useState('');
|
||||||
|
|
||||||
|
const { handleBlur, handleInputEnter } = useAddNewCard();
|
||||||
|
|
||||||
|
const { recordId } = useContext(RecordBoardCardContext);
|
||||||
|
|
||||||
|
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
|
const record = useRecoilValue(recordStoreFamilyState(recordId));
|
||||||
|
|
||||||
|
const { updateOneRecord, objectMetadataItem } =
|
||||||
|
useContext(RecordBoardContext);
|
||||||
|
|
||||||
|
const recordBoardId = useAvailableScopeIdOrThrow(
|
||||||
|
RecordBoardScopeInternalContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const showCompactView = useRecoilComponentValueV2(
|
||||||
|
isRecordBoardCompactModeActiveComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isIdentifierEmpty =
|
||||||
|
(record?.[identifierFieldDefinition.metadata.fieldName] || '').trim() ===
|
||||||
|
'';
|
||||||
|
|
||||||
|
const { checkIfLastUnselectAndCloseDropdown } =
|
||||||
|
useRecordBoardSelection(recordBoardId);
|
||||||
|
|
||||||
|
const [isCurrentCardSelected, setIsCurrentCardSelected] =
|
||||||
|
useRecoilComponentFamilyStateV2(
|
||||||
|
isRecordBoardCardSelectedComponentFamilyState,
|
||||||
|
recordId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const useUpdateOneRecordHook: RecordUpdateHook = () => {
|
||||||
|
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||||
|
updateOneRecord?.({
|
||||||
|
idToUpdate: variables.where.id as string,
|
||||||
|
updateOneRecordInput: variables.updateOneRecordInput,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return [updateEntity, { loading: false }];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>
|
||||||
|
<StopPropagationContainer>
|
||||||
|
{isCreating && position !== undefined ? (
|
||||||
|
<RecordInlineCellEditMode>
|
||||||
|
<StyledTextInput
|
||||||
|
autoFocus
|
||||||
|
value={newLabelValue}
|
||||||
|
onInputEnter={() =>
|
||||||
|
handleInputEnter(
|
||||||
|
identifierFieldDefinition.label ?? '',
|
||||||
|
newLabelValue,
|
||||||
|
position,
|
||||||
|
onCreateSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
handleBlur(
|
||||||
|
identifierFieldDefinition.label ?? '',
|
||||||
|
newLabelValue,
|
||||||
|
position,
|
||||||
|
onCreateSuccess,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(text: string) => setNewLabelValue(text)}
|
||||||
|
placeholder={identifierFieldDefinition.label}
|
||||||
|
/>
|
||||||
|
</RecordInlineCellEditMode>
|
||||||
|
) : isIdentifierEmpty ? (
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
recordId: (record as ObjectRecord).id,
|
||||||
|
maxWidth: 156,
|
||||||
|
recoilScopeId:
|
||||||
|
(isCreating ? 'new' : recordId) +
|
||||||
|
identifierFieldDefinition.fieldMetadataId,
|
||||||
|
isLabelIdentifier: true,
|
||||||
|
fieldDefinition: {
|
||||||
|
disableTooltip: false,
|
||||||
|
fieldMetadataId: identifierFieldDefinition.fieldMetadataId,
|
||||||
|
label: `Set ${identifierFieldDefinition.label}`,
|
||||||
|
iconName: identifierFieldDefinition.iconName,
|
||||||
|
type: identifierFieldDefinition.type,
|
||||||
|
metadata: identifierFieldDefinition.metadata,
|
||||||
|
defaultValue: identifierFieldDefinition.defaultValue,
|
||||||
|
editButtonIcon: getFieldButtonIcon({
|
||||||
|
metadata: identifierFieldDefinition.metadata,
|
||||||
|
type: identifierFieldDefinition.type,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
useUpdateRecord: useUpdateOneRecordHook,
|
||||||
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecordInlineCell />
|
||||||
|
</FieldContext.Provider>
|
||||||
|
) : (
|
||||||
|
<RecordIdentifierChip
|
||||||
|
objectNameSingular={objectMetadataItem.nameSingular}
|
||||||
|
record={record as ObjectRecord}
|
||||||
|
variant={AvatarChipVariant.Transparent}
|
||||||
|
maxWidth={150}
|
||||||
|
to={indexIdentifierUrl(recordId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StopPropagationContainer>
|
||||||
|
|
||||||
|
{!isCreating && (
|
||||||
|
<>
|
||||||
|
{showCompactView && (
|
||||||
|
<StyledCompactIconContainer className="compact-icon-container">
|
||||||
|
<StopPropagationContainer>
|
||||||
|
<LightIconButton
|
||||||
|
Icon={isCardExpanded ? IconEyeOff : IconEye}
|
||||||
|
accent="tertiary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCardExpanded?.((prev) => !prev);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StopPropagationContainer>
|
||||||
|
</StyledCompactIconContainer>
|
||||||
|
)}
|
||||||
|
<StyledCheckboxContainer className="checkbox-container">
|
||||||
|
<StopPropagationContainer>
|
||||||
|
<Checkbox
|
||||||
|
hoverable
|
||||||
|
checked={isCurrentCardSelected}
|
||||||
|
onChange={() => {
|
||||||
|
setIsCurrentCardSelected(!isCurrentCardSelected);
|
||||||
|
checkIfLastUnselectAndCloseDropdown();
|
||||||
|
}}
|
||||||
|
variant={CheckboxVariant.Secondary}
|
||||||
|
/>
|
||||||
|
</StopPropagationContainer>
|
||||||
|
</StyledCheckboxContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</RecordBoardCardHeaderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledBoardCardHeader = styled.div<{
|
||||||
|
showCompactView: boolean;
|
||||||
|
}>`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
height: 24px;
|
||||||
|
padding-bottom: ${({ theme, showCompactView }) =>
|
||||||
|
theme.spacing(showCompactView ? 2 : 1)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
transition: padding ease-in-out 160ms;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: ${({ theme }) => theme.icon.size.md}px;
|
||||||
|
object-fit: cover;
|
||||||
|
width: ${({ theme }) => theme.icon.size.md}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { StyledBoardCardHeader as RecordBoardCardHeaderContainer };
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledFieldContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StopPropagationContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => (
|
||||||
|
<StyledFieldContainer
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledFieldContainer>
|
||||||
|
);
|
||||||
@ -3,11 +3,8 @@ import styled from '@emotion/styled';
|
|||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||||
import {
|
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
|
||||||
StyledBoardCardBody,
|
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
|
||||||
StyledBoardCardHeader,
|
|
||||||
} from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
|
|
||||||
|
|
||||||
const StyledSkeletonIconAndText = styled.div`
|
const StyledSkeletonIconAndText = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
@ -42,18 +39,18 @@ export const RecordBoardColumnCardContainerSkeletonLoader = ({
|
|||||||
highlightColor={theme.background.transparent.lighter}
|
highlightColor={theme.background.transparent.lighter}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
>
|
>
|
||||||
<StyledBoardCardHeader showCompactView={isCompactModeActive}>
|
<RecordBoardCardHeaderContainer showCompactView={isCompactModeActive}>
|
||||||
<StyledSkeletonTitle>
|
<StyledSkeletonTitle>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
width={titleSkeletonWidth}
|
width={titleSkeletonWidth}
|
||||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||||
/>
|
/>
|
||||||
</StyledSkeletonTitle>
|
</StyledSkeletonTitle>
|
||||||
</StyledBoardCardHeader>
|
</RecordBoardCardHeaderContainer>
|
||||||
<StyledSeparator />
|
<StyledSeparator />
|
||||||
{!isCompactModeActive &&
|
{!isCompactModeActive &&
|
||||||
skeletonItems.map(({ id }) => (
|
skeletonItems.map(({ id }) => (
|
||||||
<StyledBoardCardBody key={id}>
|
<RecordBoardCardBodyContainer key={id}>
|
||||||
<StyledSkeletonIconAndText>
|
<StyledSkeletonIconAndText>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
width={16}
|
width={16}
|
||||||
@ -64,7 +61,7 @@ export const RecordBoardColumnCardContainerSkeletonLoader = ({
|
|||||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||||
/>
|
/>
|
||||||
</StyledSkeletonIconAndText>
|
</StyledSkeletonIconAndText>
|
||||||
</StyledBoardCardBody>
|
</RecordBoardCardBodyContainer>
|
||||||
))}
|
))}
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { useDropdown } from '../hooks/useDropdown';
|
|||||||
|
|
||||||
const StyledDropdownFallbackAnchor = styled.div`
|
const StyledDropdownFallbackAnchor = styled.div`
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user