New view picker (#4610)

* Implement new view picker

* Complete feature

* Fixes according to review
This commit is contained in:
Charles Bochet 2024-03-22 15:04:17 +01:00 committed by GitHub
parent d876b40056
commit 4a493b6ecf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 1216 additions and 422 deletions

View File

@ -6,6 +6,7 @@ import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { GraphQLView } from '@/views/types/GraphQLView';
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();
@ -13,7 +14,9 @@ export const ObjectMetadataNavItems = () => {
const { getIcon } = useIcons();
const currentPath = useLocation().pathname;
const { records } = usePrefetchedData<GraphQLView>(PrefetchKey.AllViews);
const { records: views } = usePrefetchedData<GraphQLView>(
PrefetchKey.AllViews,
);
return (
<>
@ -45,9 +48,11 @@ export const ObjectMetadataNavItems = () => {
: -1;
}),
].map((objectMetadataItem) => {
const viewId = records?.find(
(view: any) => view?.objectMetadataId === objectMetadataItem.id,
)?.id;
const objectMetadataViews = getObjectMetadataItemViews(
objectMetadataItem.id,
views,
);
const viewId = objectMetadataViews[0]?.id;
const navigationPath = `/objects/${objectMetadataItem.namePlural}${
viewId ? `?view=${viewId}` : ''

View File

@ -8,6 +8,7 @@ import { onRecordBoardFetchMoreVisibilityChangeComponentState } from '@/object-r
import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState';
import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState';
import { recordBoardFiltersComponentState } from '@/object-record/record-board/states/recordBoardFiltersComponentState';
import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState';
import { recordBoardObjectSingularNameComponentState } from '@/object-record/record-board/states/recordBoardObjectSingularNameComponentState';
import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState';
import { recordBoardSortsComponentState } from '@/object-record/record-board/states/recordBoardSortsComponentState';
@ -32,6 +33,10 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
recordBoardObjectSingularNameComponentState,
scopeId,
),
kanbanFieldMetadataNameState: extractComponentState(
recordBoardKanbanFieldMetadataNameComponentState,
scopeId,
),
isFetchingRecordState: extractComponentState(
isRecordBoardFetchingRecordsComponentState,
scopeId,

View File

@ -10,6 +10,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
recordIdsByColumnIdFamilyState,
columnsFamilySelector,
columnIdsState,
kanbanFieldMetadataNameState,
} = useRecordBoardStates(recordBoardId);
const setRecordIds = useRecoilCallback(
@ -26,8 +27,18 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
.getLoadable(recordIdsByColumnIdFamilyState(columnId))
.getValue();
const kanbanFieldMetadataName = snapshot
.getLoadable(kanbanFieldMetadataNameState)
.getValue();
if (!kanbanFieldMetadataName) {
return;
}
const columnRecordIds = records
.filter((record) => record.stage === column?.value)
.filter(
(record) => record[kanbanFieldMetadataName] === column?.value,
)
.sort(sortRecordsByPosition)
.map((record) => record.id);
@ -36,7 +47,12 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
}
});
},
[columnsFamilySelector, columnIdsState, recordIdsByColumnIdFamilyState],
[
columnIdsState,
columnsFamilySelector,
recordIdsByColumnIdFamilyState,
kanbanFieldMetadataNameState,
],
);
return {

View File

@ -12,12 +12,16 @@ export const useRecordBoard = (recordBoardId?: string) => {
selectedRecordIdsSelector,
isCompactModeActiveState,
onFetchMoreVisibilityChangeState,
kanbanFieldMetadataNameState,
} = useRecordBoardStates(recordBoardId);
const { setColumns } = useSetRecordBoardColumns(recordBoardId);
const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId);
const setFieldDefinitions = useSetRecoilState(fieldDefinitionsState);
const setObjectSingularName = useSetRecoilState(objectSingularNameState);
const setKanbanFieldMetadataName = useSetRecoilState(
kanbanFieldMetadataNameState,
);
return {
scopeId,
@ -25,6 +29,7 @@ export const useRecordBoard = (recordBoardId?: string) => {
setRecordIds,
setFieldDefinitions,
setObjectSingularName,
setKanbanFieldMetadataName,
selectedRecordIdsSelector,
isCompactModeActiveState,
onFetchMoreVisibilityChangeState,

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const recordBoardKanbanFieldMetadataNameComponentState =
createComponentState<string | undefined>({
key: 'recordBoardKanbanFieldMetadataNameComponentState',
defaultValue: undefined,
});

View File

@ -8,7 +8,10 @@ import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoar
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
type RecordIndexBoardContainerEffectProps = {
objectNameSingular: string;
@ -31,6 +34,7 @@ export const RecordIndexBoardContainerEffect = ({
selectedRecordIdsSelector,
setFieldDefinitions,
onFetchMoreVisibilityChangeState,
setKanbanFieldMetadataName,
} = useRecordBoard(recordBoardId);
const { fetchMoreRecords, loading } = useLoadRecordIndexBoard({
@ -43,6 +47,10 @@ export const RecordIndexBoardContainerEffect = ({
onFetchMoreVisibilityChangeState,
);
const recordIndexKanbanFieldMetadataId = useRecoilValue(
recordIndexKanbanFieldMetadataIdState,
);
useEffect(() => {
setOnFetchMoreVisibilityChange(() => () => {
if (!loading) {
@ -67,6 +75,7 @@ export const RecordIndexBoardContainerEffect = ({
setColumns(
computeRecordBoardColumnDefinitionsFromObjectMetadata(
objectMetadataItem,
recordIndexKanbanFieldMetadataId ?? '',
navigateToSelectSettings,
),
);
@ -74,6 +83,7 @@ export const RecordIndexBoardContainerEffect = ({
navigateToSelectSettings,
objectMetadataItem,
objectNameSingular,
recordIndexKanbanFieldMetadataId,
setColumns,
]);
@ -85,6 +95,24 @@ export const RecordIndexBoardContainerEffect = ({
setFieldDefinitions(recordIndexFieldDefinitions);
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
useEffect(() => {
if (isDefined(recordIndexKanbanFieldMetadataId)) {
const kanbanFieldMetadataName = objectMetadataItem?.fields.find(
(field) =>
field.type === FieldMetadataType.Select &&
field.id === recordIndexKanbanFieldMetadataId,
)?.name;
if (isDefined(kanbanFieldMetadataName)) {
setKanbanFieldMetadataName(kanbanFieldMetadataName);
}
}
}, [
objectMetadataItem,
recordIndexKanbanFieldMetadataId,
setKanbanFieldMetadataName,
]);
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({

View File

@ -10,10 +10,10 @@ import { RecordIndexTableContainer } from '@/object-record/record-index/componen
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
@ -65,6 +65,9 @@ export const RecordIndexContainer = ({
const setRecordIndexIsCompactModeActive = useSetRecoilState(
recordIndexIsCompactModeActiveState,
);
const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState(
recordIndexKanbanFieldMetadataIdState,
);
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
recordTableId: recordIndexId,
@ -129,9 +132,11 @@ export const RecordIndexContainer = ({
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
setRecordIndexViewKanbanFieldMetadataIdState(
view.kanbanFieldMetadataId,
);
setRecordIndexIsCompactModeActive(view.isCompact);
}}
optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}

View File

@ -3,7 +3,6 @@ import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useViewBarEditMode } from '@/views/hooks/useViewBarEditMode';
import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsDropdownProps = {
@ -17,8 +16,6 @@ export const RecordIndexOptionsDropdown = ({
objectNameSingular,
viewType,
}: RecordIndexOptionsDropdownProps) => {
const { setViewEditMode } = useViewBarEditMode(recordIndexId);
return (
<Dropdown
dropdownId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
@ -32,7 +29,6 @@ export const RecordIndexOptionsDropdown = ({
recordIndexId={recordIndexId}
/>
}
onClickOutside={() => setViewEditMode('none')}
/>
);
};

View File

@ -1,6 +1,5 @@
import { useRef, useState } from 'react';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { v4 } from 'uuid';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
@ -14,7 +13,6 @@ import {
IconTag,
} from '@/ui/display/icon';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
@ -23,8 +21,6 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useHandleViews } from '@/views/hooks/useHandleViews';
import { useViewBarEditMode } from '@/views/hooks/useViewBarEditMode';
import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsMenu = 'fields';
@ -40,9 +36,6 @@ export const RecordIndexOptionsDropdownContent = ({
recordIndexId,
objectNameSingular,
}: RecordIndexOptionsDropdownContentProps) => {
const { updateCurrentView, createEmptyView, selectView } =
useHandleViews(recordIndexId);
const { viewEditMode, setViewEditMode } = useViewBarEditMode(recordIndexId);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
@ -53,8 +46,6 @@ export const RecordIndexOptionsDropdownContent = ({
const resetMenu = () => setCurrentMenu(undefined);
const viewEditInputRef = useRef<HTMLInputElement>(null);
const handleSelectMenu = (option: RecordIndexOptionsMenu) => {
setCurrentMenu(option);
};
@ -67,25 +58,6 @@ export const RecordIndexOptionsDropdownContent = ({
TableOptionsHotkeyScope.Dropdown,
);
useScopedHotkeys(
Key.Enter,
async () => {
const name = viewEditInputRef.current?.value;
if (viewEditMode === 'create') {
const id = v4();
await createEmptyView(id, name ?? '');
selectView(id);
} else {
updateCurrentView({ name });
}
resetMenu();
setViewEditMode('none');
closeDropdown();
},
TableOptionsHotkeyScope.Dropdown,
);
const {
handleColumnVisibilityChange,
handleReorderColumns,
@ -128,37 +100,18 @@ export const RecordIndexOptionsDropdownContent = ({
return (
<>
{!currentMenu && (
<>
<DropdownMenuInput
ref={viewEditInputRef}
autoFocus={viewEditMode !== 'none'}
placeholder={
viewEditMode === 'create'
? 'New view'
: viewEditMode === 'edit'
? 'View name'
: ''
}
defaultValue={
viewEditMode === 'create'
? ''
: currentViewWithCombinedFiltersAndSorts?.name
}
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => handleSelectMenu('fields')}
LeftIcon={IconTag}
text="Fields"
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<MenuItem
onClick={() => handleSelectMenu('fields')}
LeftIcon={IconTag}
text="Fields"
/>
<MenuItem
onClick={() => openRecordSpreadsheetImport()}
LeftIcon={IconFileImport}
text="Import"
/>
</DropdownMenuItemsContainer>
</>
<MenuItem
onClick={() => openRecordSpreadsheetImport()}
LeftIcon={IconFileImport}
text="Import"
/>
</DropdownMenuItemsContainer>
)}
{currentMenu === 'fields' && (
<>

View File

@ -0,0 +1,8 @@
import { createState } from '@/ui/utilities/state/utils/createState';
export const recordIndexKanbanFieldMetadataIdState = createState<string | null>(
{
key: 'recordIndexKanbanFieldMetadataIdState',
defaultValue: null,
},
);

View File

@ -5,10 +5,13 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
export const computeRecordBoardColumnDefinitionsFromObjectMetadata = (
objectMetadataItem: ObjectMetadataItem,
kanbanFieldMetadataId: string,
navigateToSelectSettings: () => void,
): RecordBoardColumnDefinition[] => {
const selectFieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.type === FieldMetadataType.Select,
(field) =>
field.id === kanbanFieldMetadataId &&
field.type === FieldMetadataType.Select,
);
if (!selectFieldMetadataItem) {

View File

@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar';
@ -32,7 +31,6 @@ export const SignInBackgroundMockContainer = () => {
viewType={ViewType.Table}
/>
}
optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}

View File

@ -20,6 +20,7 @@ export type ButtonProps = {
position?: ButtonPosition;
accent?: ButtonAccent;
soon?: boolean;
justify?: 'center' | 'flex-start' | 'flex-end';
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
@ -28,7 +29,13 @@ export type ButtonProps = {
const StyledButton = styled.button<
Pick<
ButtonProps,
'fullWidth' | 'variant' | 'size' | 'position' | 'accent' | 'focus'
| 'fullWidth'
| 'variant'
| 'size'
| 'position'
| 'accent'
| 'focus'
| 'justify'
>
>`
align-items: center;
@ -177,9 +184,7 @@ const StyledButton = styled.button<
`;
case 'danger':
return css`
background: ${!disabled
? theme.background.transparent.primary
: 'transparent'};
background: transparent;
border-color: ${variant === 'secondary'
? focus
? theme.color.red
@ -236,6 +241,7 @@ const StyledButton = styled.button<
font-weight: 500;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
justify-content: ${({ justify }) => justify};
padding: ${({ theme }) => {
return `0 ${theme.spacing(2)}`;
}};
@ -266,6 +272,7 @@ export const Button = ({
position = 'standalone',
soon = false,
disabled = false,
justify = 'flex-start',
focus = false,
onClick,
}: ButtonProps) => {
@ -279,6 +286,7 @@ export const Button = ({
position={position}
disabled={soon || disabled}
focus={focus}
justify={justify}
accent={accent}
className={className}
onClick={onClick}

View File

@ -31,6 +31,7 @@ export const LightIconButtonGroup = ({
<LightIconButton
key={`light-icon-button-${index}`}
Icon={Icon}
disabled={!onClick}
onClick={onClick}
size={size}
/>

View File

@ -30,6 +30,7 @@ type IconPickerProps = {
onOpen?: () => void;
variant?: IconButtonVariant;
className?: string;
disableBlur?: boolean;
};
const StyledMenuIconItemsContainer = styled.div`
@ -86,6 +87,7 @@ export const IconPicker = ({
onClose,
onOpen,
variant = 'secondary',
disableBlur = false,
className,
}: IconPickerProps) => {
const [searchString, setSearchString] = useState('');
@ -148,6 +150,7 @@ export const IconPicker = ({
/>
}
dropdownMenuWidth={176}
disableBlur={disableBlur}
dropdownComponents={
<SelectableList
selectableListId="icon-list"

View File

@ -22,6 +22,7 @@ export type SelectOption<Value extends string | number | null> = {
export type SelectProps<Value extends string | number | null> = {
className?: string;
disabled?: boolean;
disableBlur?: boolean;
dropdownId: string;
dropdownWidth?: `${string}px` | 'auto' | number;
emptyOption?: SelectOption<Value>;
@ -75,6 +76,7 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
export const Select = <Value extends string | number | null>({
className,
disabled: disabledFromProps,
disableBlur = false,
dropdownId,
dropdownWidth = 176,
emptyOption,
@ -141,6 +143,7 @@ export const Select = <Value extends string | number | null>({
dropdownMenuWidth={dropdownWidth}
dropdownPlacement="bottom-start"
clickableComponent={selectControl}
disableBlur={disableBlur}
dropdownComponents={
<>
{!!withSearchInput && (

View File

@ -35,6 +35,7 @@ type DropdownProps = {
dropdownPlacement?: Placement;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownOffset?: { x?: number; y?: number };
disableBlur?: boolean;
onClickOutside?: () => void;
onClose?: () => void;
onOpen?: () => void;
@ -50,6 +51,7 @@ export const Dropdown = ({
dropdownHotkeyScope,
dropdownPlacement = 'bottom-end',
dropdownOffset = { x: 0, y: 0 },
disableBlur = false,
onClickOutside,
onClose,
onOpen,
@ -109,7 +111,10 @@ export const Dropdown = ({
{clickableComponent && (
<div
ref={refs.setReference}
onClick={toggleDropdown}
onClick={() => {
toggleDropdown();
onClickOutside?.();
}}
className={className}
>
{clickableComponent}
@ -123,6 +128,7 @@ export const Dropdown = ({
)}
{isDropdownOpen && (
<DropdownMenu
disableBlur={disableBlur}
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
ref={refs.setFloating}

View File

@ -9,7 +9,10 @@ const StyledDropdownMenu = styled.div<{
? 'none'
: 'blur(12px) saturate(200%) contrast(50%) brightness(130%)'};
background: ${({ theme }) => theme.background.transparent.forBackdropFilter};
background: ${({ theme, disableBlur }) =>
disableBlur
? theme.background.primary
: theme.background.transparent.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};

View File

@ -11,14 +11,11 @@ const StyledHeader = styled.li`
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
padding: ${({ theme }) => theme.spacing(1)};
user-select: none;
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledChildrenWrapper = styled.span`
@ -46,9 +43,10 @@ export const DropdownMenuHeader = ({
testId,
}: DropdownMenuHeaderProps) => {
return (
<StyledHeader data-testid={testId} onClick={onClick}>
<StyledHeader data-testid={testId}>
{StartIcon && (
<LightIconButton
onClick={onClick}
testId="dropdown-menu-header-end-icon"
Icon={StartIcon}
accent="tertiary"
@ -58,6 +56,7 @@ export const DropdownMenuHeader = ({
<StyledChildrenWrapper>{children}</StyledChildrenWrapper>
{EndIcon && (
<StyledLightIconButton
onClick={onClick}
testId="dropdown-menu-header-end-icon"
Icon={EndIcon}
accent="tertiary"

View File

@ -30,12 +30,12 @@ const StyledInputContainer = styled.div`
export const DropdownMenuInput = forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
>(({ autoFocus, defaultValue, placeholder, onChange }, ref) => {
>(({ autoFocus, value, placeholder, onChange }, ref) => {
return (
<StyledInputContainer>
<StyledInput
autoFocus={autoFocus}
defaultValue={defaultValue}
value={value}
placeholder={placeholder}
onChange={onChange}
ref={ref}

View File

@ -274,7 +274,7 @@ export const WithInput: Story = {
args: {
dropdownComponents: (
<>
<DropdownMenuInput defaultValue="Lorem ipsum" autoFocus />
<DropdownMenuInput value="Lorem ipsum" autoFocus />
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{optionsMock.map(({ name }) => (

View File

@ -8,7 +8,7 @@ const meta: Meta<typeof DropdownMenuInput> = {
title: 'UI/Layout/Dropdown/DropdownMenuInput',
component: DropdownMenuInput,
decorators: [ComponentDecorator],
args: { defaultValue: 'Lorem ipsum' },
args: { value: 'Lorem ipsum' },
};
export default meta;

View File

@ -1,33 +1,24 @@
import { useEffect } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useSetRecoilState } from 'recoil';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
export const FilterQueryParamsEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams, viewIdQueryParam } =
export const QueryParamsFiltersEffect = () => {
const { hasFiltersQueryParams, getFiltersFromQueryParams } =
useViewFromQueryParams();
const { unsavedToUpsertViewFiltersState, currentViewIdState } =
useViewStates();
const { unsavedToUpsertViewFiltersState } = useViewStates();
const setUnsavedViewFilter = useSetRecoilState(
unsavedToUpsertViewFiltersState,
);
const setCurrentViewId = useSetRecoilState(currentViewIdState);
const { resetCurrentView } = useResetCurrentView();
useEffect(() => {
if (isUndefined(viewIdQueryParam) || !viewIdQueryParam) {
if (!hasFiltersQueryParams) {
return;
}
setCurrentViewId(viewIdQueryParam);
}, [getFiltersFromQueryParams, setCurrentViewId, viewIdQueryParam]);
useEffect(() => {
if (!hasFiltersQueryParams) return;
getFiltersFromQueryParams().then((filtersFromParams) => {
if (Array.isArray(filtersFromParams)) {
setUnsavedViewFilter(filtersFromParams);
@ -44,5 +35,5 @@ export const FilterQueryParamsEffect = () => {
setUnsavedViewFilter,
]);
return null;
return <></>;
};

View File

@ -0,0 +1,38 @@
import { useEffect } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useRecoilState } from 'recoil';
import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { isDefined } from '~/utils/isDefined';
export const QueryParamsViewIdEffect = () => {
const { getFiltersFromQueryParams, viewIdQueryParam } =
useViewFromQueryParams();
const { currentViewIdState } = useViewStates();
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
const { viewsOnCurrentObject } = useGetCurrentView();
useEffect(() => {
const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX');
if (isUndefined(viewIdQueryParam) && isDefined(indexView)) {
setCurrentViewId(indexView.id);
return;
}
if (isDefined(viewIdQueryParam)) {
setCurrentViewId(viewIdQueryParam);
}
}, [
currentViewId,
getFiltersFromQueryParams,
setCurrentViewId,
viewIdQueryParam,
viewsOnCurrentObject,
]);
return <></>;
};

View File

@ -7,14 +7,18 @@ import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { UPDATE_VIEW_DROPDOWN_ID } from '@/views/constants/UpdateViewDropdownId';
import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.color.blue};
border-radius: ${({ theme }) => theme.border.radius.md};
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
@ -23,25 +27,50 @@ const StyledContainer = styled.div`
export type UpdateViewButtonGroupProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
};
export const UpdateViewButtonGroup = ({
hotkeyScope,
onViewEditModeChange,
}: UpdateViewButtonGroupProps) => {
const { canPersistViewSelector, viewEditModeState } = useViewStates();
const { canPersistViewSelector, currentViewIdState } = useViewStates();
const { saveCurrentViewFilterAndSorts } = useSaveCurrentViewFiltersAndSorts();
const setViewEditMode = useSetRecoilState(viewEditModeState);
const { setViewPickerMode } = useViewPickerMode();
const { viewPickerReferenceViewIdState } = useViewPickerStates();
const canPersistView = useRecoilValue(canPersistViewSelector());
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode('create');
onViewEditModeChange?.();
}, [setViewEditMode, onViewEditModeChange]);
const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown(
UPDATE_VIEW_BUTTON_DROPDOWN_ID,
);
const { openDropdown: openViewPickerDropdown } = useDropdown(
VIEW_PICKER_DROPDOWN_ID,
);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const handleViewSubmit = async () => {
const currentViewId = useRecoilValue(currentViewIdState);
const setViewPickerReferenceViewId = useSetRecoilState(
viewPickerReferenceViewIdState,
);
const handleViewCreate = useCallback(() => {
if (!currentViewId) {
return;
}
openViewPickerDropdown();
setViewPickerReferenceViewId(currentViewId);
setViewPickerMode('create');
closeUpdateViewButtonDropdown();
}, [
closeUpdateViewButtonDropdown,
currentViewId,
openViewPickerDropdown,
setViewPickerMode,
setViewPickerReferenceViewId,
]);
const handleViewUpdate = async () => {
await saveCurrentViewFilterAndSorts();
};
@ -51,27 +80,42 @@ export const UpdateViewButtonGroup = ({
return (
<StyledContainer>
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewSubmit} />
<Dropdown
dropdownId={UPDATE_VIEW_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<Button size="small" accent="blue" Icon={IconChevronDown} />
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleCreateViewButtonClick}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
</>
}
{currentViewWithCombinedFiltersAndSorts?.key !== 'INDEX' ? (
<ButtonGroup size="small" accent="blue">
<Button title="Update view" onClick={handleViewUpdate} />
<Dropdown
dropdownId={UPDATE_VIEW_BUTTON_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<Button
size="small"
accent="blue"
Icon={IconChevronDown}
position="right"
/>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
<MenuItem
onClick={handleViewCreate}
LeftIcon={IconPlus}
text="Create view"
/>
</DropdownMenuItemsContainer>
</>
}
/>
</ButtonGroup>
) : (
<Button
title="Save as new view"
onClick={handleViewCreate}
accent="blue"
size="small"
variant="secondary"
/>
</ButtonGroup>
)}
</StyledContainer>
);
};

View File

@ -4,26 +4,25 @@ import { useParams } from 'react-router-dom';
import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
import { QueryParamsViewIdEffect } from '@/views/components/QueryParamsViewIdEffect';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { ViewScope } from '@/views/scopes/ViewScope';
import { GraphQLView } from '@/views/types/GraphQLView';
import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDropdown';
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails';
import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = {
viewBarId: string;
className?: string;
optionsDropdownButton: ReactNode;
optionsDropdownScopeId: string;
onCurrentViewChange: (view: GraphQLView | undefined) => void | Promise<void>;
};
@ -31,18 +30,17 @@ export const ViewBar = ({
viewBarId,
className,
optionsDropdownButton,
optionsDropdownScopeId,
onCurrentViewChange,
}: ViewBarProps) => {
const { openDropdown: openOptionsDropdownButton } = useDropdown(
optionsDropdownScopeId,
);
const { objectNamePlural } = useParams();
const filterDropdownId = 'view-filter';
const sortDropdownId = 'view-sort';
if (!objectNamePlural) {
return;
}
return (
<ViewScope
viewScopeId={viewBarId}
@ -51,16 +49,15 @@ export const ViewBar = ({
<ViewBarEffect viewBarId={viewBarId} />
<ViewBarFilterEffect filterDropdownId={filterDropdownId} />
<ViewBarSortEffect sortDropdownId={sortDropdownId} />
{!!objectNamePlural && <FilterQueryParamsEffect />}
<QueryParamsFiltersEffect />
<QueryParamsViewIdEffect />
<TopBar
className={className}
leftComponent={
<ViewsDropdownButton
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
optionsDropdownScopeId={optionsDropdownScopeId}
/>
<>
<ViewPickerDropdown />
</>
}
displayBottomBorder={false}
rightComponent={
@ -87,8 +84,9 @@ export const ViewBar = ({
viewBarId={viewBarId}
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}
hotkeyScope={{ scope: ViewsHotkeyScope.CreateDropdown }}
hotkeyScope={{
scope: ViewsHotkeyScope.UpdateViewButtonDropdown,
}}
/>
}
/>

View File

@ -1,12 +1,11 @@
import { useEffect, useState } from 'react';
import { isUndefined } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { GraphQLView } from '@/views/types/GraphQLView';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isDefined } from '~/utils/isDefined';
type ViewBarEffectProps = {
viewBarId: string;
@ -17,7 +16,6 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
useGetCurrentView(viewBarId);
const {
onCurrentViewChangeState,
currentViewIdState,
availableFilterDefinitionsState,
isPersistingViewFieldsState,
} = useViewStates(viewBarId);
@ -30,7 +28,6 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
availableFilterDefinitionsState,
);
const isPersistingViewFields = useRecoilValue(isPersistingViewFieldsState);
const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState);
useEffect(() => {
if (
@ -39,14 +36,14 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
currentViewSnapshot,
)
) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
if (isUndefined(currentViewWithCombinedFiltersAndSorts)) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(undefined);
return;
}
if (!isPersistingViewFields) {
setCurrentViewSnapshot(currentViewWithCombinedFiltersAndSorts);
onCurrentViewChange?.(currentViewWithCombinedFiltersAndSorts);
}
}
@ -58,14 +55,5 @@ export const ViewBarEffect = ({ viewBarId }: ViewBarEffectProps) => {
onCurrentViewChange,
]);
useEffect(() => {
if (
isDefined(currentViewWithCombinedFiltersAndSorts) &&
!isDefined(currentViewId)
) {
setCurrentViewId(currentViewWithCombinedFiltersAndSorts.id);
}
}, [currentViewWithCombinedFiltersAndSorts, currentViewId, setCurrentViewId]);
return <></>;
};

View File

@ -1,189 +0,0 @@
import { MouseEvent } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
IconChevronDown,
IconList,
IconPencil,
IconPlus,
IconTrash,
} from '@/ui/display/icon';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { VIEWS_DROPDOWN_ID } from '@/views/constants/ViewsDropdownId';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useHandleViews } from '@/views/hooks/useHandleViews';
import { isDefined } from '~/utils/isDefined';
import { useViewStates } from '../hooks/internal/useViewStates';
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledDropdownLabelAdornments = styled.span`
align-items: center;
color: ${({ theme }) => theme.grayScale.gray35};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewName = styled.span`
margin-left: ${({ theme }) => theme.spacing(1)};
display: inline-block;
max-width: 130px;
@media (max-width: 375px) {
max-width: 90px;
}
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
max-width: 110px;
}
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
`;
export type ViewsDropdownButtonProps = {
hotkeyScope: HotkeyScope;
onViewEditModeChange?: () => void;
optionsDropdownScopeId: string;
};
export const ViewsDropdownButton = ({
hotkeyScope,
onViewEditModeChange,
optionsDropdownScopeId,
}: ViewsDropdownButtonProps) => {
const theme = useTheme();
const { removeView, selectView } = useHandleViews();
const { entityCountInCurrentViewState, viewEditModeState } = useViewStates();
const { currentViewWithCombinedFiltersAndSorts, viewsOnCurrentObject } =
useGetCurrentView();
const entityCountInCurrentView = useRecoilValue(
entityCountInCurrentViewState,
);
const setViewEditMode = useSetRecoilState(viewEditModeState);
const {
isDropdownOpen: isViewsDropdownOpen,
closeDropdown: closeViewsDropdown,
} = useDropdown(VIEWS_DROPDOWN_ID);
const { openDropdown: openOptionsDropdown } = useDropdown(
optionsDropdownScopeId,
);
const handleViewSelect = (viewId: string) => {
selectView(viewId);
closeViewsDropdown();
};
const handleAddViewButtonClick = () => {
setViewEditMode('create');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleEditViewButtonClick = (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
selectView(viewId);
setViewEditMode('edit');
onViewEditModeChange?.();
closeViewsDropdown();
openOptionsDropdown();
};
const handleDeleteViewButtonClick = async (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
await removeView(viewId);
selectView(viewsOnCurrentObject.filter((view) => view.id !== viewId)[0].id);
closeViewsDropdown();
};
const { getIcon } = useIcons();
const CurrentViewIcon = getIcon(currentViewWithCombinedFiltersAndSorts?.icon);
return (
<Dropdown
dropdownId={VIEWS_DROPDOWN_ID}
dropdownHotkeyScope={hotkeyScope}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsDropdownOpen}>
{currentViewWithCombinedFiltersAndSorts && CurrentViewIcon ? (
<CurrentViewIcon size={theme.icon.size.md} />
) : (
<IconList size={theme.icon.size.md} />
)}
<StyledViewName>
{currentViewWithCombinedFiltersAndSorts?.name ?? 'All'}
</StyledViewName>
<StyledDropdownLabelAdornments>
· {entityCountInCurrentView}{' '}
<IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments>
</StyledDropdownButtonContainer>
}
dropdownComponents={
<>
<DropdownMenuItemsContainer>
{viewsOnCurrentObject.map((view) => (
<MenuItem
key={view.id}
iconButtons={[
currentViewWithCombinedFiltersAndSorts?.id === view.id
? {
Icon: IconPencil,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleEditViewButtonClick(event, view.id),
}
: null,
viewsOnCurrentObject.length > 1
? {
Icon: IconTrash,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleDeleteViewButtonClick(event, view.id),
}
: null,
].filter(isDefined)}
onClick={() => handleViewSelect(view.id)}
LeftIcon={getIcon(view.icon)}
text={view.name}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<StyledBoldDropdownMenuItemsContainer>
<MenuItem
onClick={handleAddViewButtonClick}
LeftIcon={IconPlus}
text="Add view"
/>
</StyledBoldDropdownMenuItemsContainer>
</>
}
/>
);
};

View File

@ -0,0 +1 @@
export const UPDATE_VIEW_BUTTON_DROPDOWN_ID = 'update-view-button';

View File

@ -1 +0,0 @@
export const UPDATE_VIEW_DROPDOWN_ID = 'update-view';

View File

@ -1 +0,0 @@
export const VIEWS_DROPDOWN_ID = 'views';

View File

@ -52,7 +52,10 @@ export const useViewFromQueryParams = () => {
[queryParamsValidation],
);
const viewIdQueryParam = useMemo(
() => queryParamsValidation.success && queryParamsValidation.data.view,
() =>
queryParamsValidation.success
? queryParamsValidation.data.view
: undefined,
[queryParamsValidation],
);

View File

@ -15,7 +15,6 @@ import { unsavedToDeleteViewFilterIdsComponentState } from '@/views/states/unsav
import { unsavedToDeleteViewSortIdsComponentState } from '@/views/states/unsavedToDeleteViewSortIdsComponentState';
import { unsavedToUpsertViewFiltersComponentState } from '@/views/states/unsavedToUpsertViewFiltersComponentState';
import { unsavedToUpsertViewSortsComponentState } from '@/views/states/unsavedToUpsertViewSortsComponentState';
import { viewEditModeComponentState } from '@/views/states/viewEditModeComponentState';
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
import { ViewScopeInternalContext } from '../../scopes/scope-internal-context/ViewScopeInternalContext';
@ -60,10 +59,6 @@ export const useViewStates = (viewComponentId?: string) => {
entityCountInCurrentViewComponentState,
componentId,
),
viewEditModeState: extractComponentState(
viewEditModeComponentState,
componentId,
),
viewObjectMetadataIdState: extractComponentState(
viewObjectMetadataIdComponentState,
componentId,

View File

@ -9,6 +9,7 @@ import { ViewScopeInternalContext } from '@/views/scopes/scope-internal-context/
import { GraphQLView } from '@/views/types/GraphQLView';
import { combinedViewFilters } from '@/views/utils/combinedViewFilters';
import { combinedViewSorts } from '@/views/utils/combinedViewSorts';
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
import { isDefined } from '~/utils/isDefined';
export const useGetCurrentView = (viewBarComponentId?: string) => {
@ -51,16 +52,10 @@ export const useGetCurrentView = (viewBarComponentId?: string) => {
setIsCurrentViewKeyIndex(currentView?.key === 'INDEX');
}, [currentView, setIsCurrentViewKeyIndex]);
const viewsOnCurrentObject = views
.filter((view) => view.objectMetadataId === viewObjectMetadataId)
.map((view) => ({
id: view.id,
name: view.name,
type: view.type,
key: view.key,
objectMetadataId: view.objectMetadataId,
icon: view.icon,
}));
const viewsOnCurrentObject = getObjectMetadataItemViews(
viewObjectMetadataId ?? '',
views,
);
const unsavedToUpsertViewFilters = useRecoilValue(
unsavedToUpsertViewFiltersState,

View File

@ -12,6 +12,7 @@ import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistVie
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { GraphQLView } from '@/views/types/GraphQLView';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -19,7 +20,8 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useHandleViews = (viewBarComponentId?: string) => {
const { resetCurrentView } = useResetCurrentView(viewBarComponentId);
const { currentViewIdState } = useViewStates(viewBarComponentId);
const { currentViewIdState, isPersistingViewFieldsState } =
useViewStates(viewBarComponentId);
const { getViewFromCache } = useGetViewFromCache();
@ -36,8 +38,8 @@ export const useHandleViews = (viewBarComponentId?: string) => {
});
const { createViewFieldRecords } = usePersistViewFieldRecords();
const createViewFromCurrent = useRecoilCallback(() => () => {}, []);
const { saveCurrentViewFilterAndSorts } =
useSaveCurrentViewFiltersAndSorts(viewBarComponentId);
const [_, setSearchParams] = useSearchParams();
@ -48,9 +50,20 @@ export const useHandleViews = (viewBarComponentId?: string) => {
[deleteOneRecord],
);
const createEmptyView = useRecoilCallback(
({ snapshot }) =>
async (id: string, name: string) => {
const createView = useRecoilCallback(
({ snapshot, set }) =>
async ({
id,
name,
icon,
kanbanFieldMetadataId,
type,
}: Partial<
Pick<
GraphQLView,
'id' | 'name' | 'icon' | 'kanbanFieldMetadataId' | 'type'
>
>) => {
const currentViewId = getSnapshotValue(snapshot, currentViewIdState);
if (!isDefined(currentViewId)) {
@ -63,11 +76,17 @@ export const useHandleViews = (viewBarComponentId?: string) => {
return;
}
set(isPersistingViewFieldsState, true);
const newView = await createOneRecord({
id: id ?? v4(),
name: name,
name: name ?? view.name,
icon: icon ?? view.icon,
key: null,
kanbanFieldMetadataId:
kanbanFieldMetadataId ?? view.kanbanFieldMetadataId,
type: type ?? view.type,
objectMetadataId: view.objectMetadataId,
type: view.type,
});
if (isUndefinedOrNull(newView)) {
@ -75,33 +94,36 @@ export const useHandleViews = (viewBarComponentId?: string) => {
}
await createViewFieldRecords(view.viewFields, newView);
await saveCurrentViewFilterAndSorts(newView.id);
set(isPersistingViewFieldsState, false);
},
[
createOneRecord,
createViewFieldRecords,
currentViewIdState,
getViewFromCache,
isPersistingViewFieldsState,
saveCurrentViewFilterAndSorts,
],
);
const changeViewInUrl = useCallback(
(viewId: string) => {
setSearchParams((previousSearchParams) => {
previousSearchParams.set('view', viewId);
return previousSearchParams;
setSearchParams(() => {
const searchParams = new URLSearchParams();
searchParams.set('view', viewId);
return searchParams;
});
},
[setSearchParams],
);
const selectView = useRecoilCallback(
({ set }) =>
async (viewId: string) => {
set(currentViewIdState, viewId);
changeViewInUrl(viewId);
resetCurrentView();
},
[changeViewInUrl, currentViewIdState, resetCurrentView],
() => async (viewId: string) => {
changeViewInUrl(viewId);
resetCurrentView();
},
[changeViewInUrl, resetCurrentView],
);
const updateCurrentView = useRecoilCallback(
@ -120,11 +142,23 @@ export const useHandleViews = (viewBarComponentId?: string) => {
[currentViewIdState, updateOneRecord],
);
const updateView = useRecoilCallback(
() => async (view: Partial<GraphQLView>) => {
if (isDefined(view.id)) {
await updateOneRecord({
idToUpdate: view.id,
updateOneRecordInput: view,
});
}
},
[updateOneRecord],
);
return {
selectView,
updateCurrentView,
updateView,
removeView,
createEmptyView,
createViewFromCurrent,
createView,
};
};

View File

@ -125,7 +125,7 @@ export const useSaveCurrentViewFiltersAndSorts = (
const saveCurrentViewFilterAndSorts = useRecoilCallback(
({ snapshot }) =>
async () => {
async (viewId?: string) => {
const currentViewId = snapshot
.getLoadable(currentViewIdState)
.getValue();
@ -134,8 +134,8 @@ export const useSaveCurrentViewFiltersAndSorts = (
return;
}
await saveViewFilters(currentViewId);
await saveViewSorts(currentViewId);
await saveViewFilters(viewId ?? currentViewId);
await saveViewSorts(viewId ?? currentViewId);
resetCurrentView();
},
[currentViewIdState, resetCurrentView, saveViewFilters, saveViewSorts],

View File

@ -1,14 +0,0 @@
import { useRecoilState } from 'recoil';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
export const useViewBarEditMode = (viewBarComponentId?: string) => {
const { viewEditModeState } = useViewStates(viewBarComponentId);
const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState);
return {
viewEditMode,
setViewEditMode,
};
};

View File

@ -1,4 +1,3 @@
import { Position } from '@/object-metadata/types/Position';
import { ViewField } from '@/views/types/ViewField';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewKey } from '@/views/types/ViewKey';
@ -10,11 +9,12 @@ export type GraphQLView = {
name: string;
type: ViewType;
key: ViewKey | null;
kanbanFieldMetadataId: string;
objectMetadataId: string;
isCompact: boolean;
viewFields: ViewField[];
viewFilters: ViewFilter[];
viewSorts: ViewSort[];
position: Position;
position: number;
icon: string;
};

View File

@ -1,4 +1,4 @@
export enum ViewsHotkeyScope {
ListDropdown = 'views-list-dropdown',
CreateDropdown = 'views-create-dropdown',
UpdateViewButtonDropdown = 'update-view-button-dropdown',
}

View File

@ -0,0 +1,29 @@
import { GraphQLView } from '@/views/types/GraphQLView';
export const getObjectMetadataItemViews = (
viewObjectMetadataId: string,
views: GraphQLView[],
) => {
const indexView = views.find(
(view) =>
view.key === 'INDEX' && view.objectMetadataId === viewObjectMetadataId,
);
return [
...views
.filter((view) => view.objectMetadataId === viewObjectMetadataId)
.filter((view) => view.key !== 'INDEX'),
]
.sort((a, b) => a.position - b.position)
.concat(indexView ? [indexView] : [])
.map((view) => ({
id: view.id,
name: view.name,
type: view.type,
key: view.key,
position: view.position,
objectMetadataId: view.objectMetadataId,
kanbanFieldMetadataId: view.kanbanFieldMetadataId,
icon: view.icon,
}));
};

View File

@ -0,0 +1,81 @@
import { useRecoilValue } from 'recoil';
import { Button } from '@/ui/input/button/components/Button';
import { ViewType } from '@/views/types/ViewType';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
export const ViewPickerCreateOrEditButton = () => {
const { availableFieldsForKanban, navigateToSelectSettings } =
useGetAvailableFieldsForKanban();
const {
viewPickerIsPersistingState,
viewPickerKanbanFieldMetadataIdState,
viewPickerTypeState,
} = useViewPickerStates();
const { viewPickerMode } = useViewPickerMode();
const viewPickerType = useRecoilValue(viewPickerTypeState);
const viewPickerIsPersisting = useRecoilValue(viewPickerIsPersistingState);
const viewPickerKanbanFieldMetadataId = useRecoilValue(
viewPickerKanbanFieldMetadataIdState,
);
const { handleCreate, handleDelete } = useViewPickerPersistView();
if (viewPickerMode === 'edit') {
return (
<Button
title="Delete"
onClick={handleDelete}
accent="danger"
fullWidth
size="small"
justify="center"
focus={false}
variant="secondary"
disabled={viewPickerIsPersisting}
/>
);
}
if (
viewPickerType === ViewType.Kanban &&
availableFieldsForKanban.length === 0
) {
return (
<Button
title="Go to Settings"
onClick={navigateToSelectSettings}
size="small"
accent="blue"
fullWidth
justify="center"
/>
);
}
if (
viewPickerType === ViewType.Table ||
viewPickerKanbanFieldMetadataId !== ''
) {
return (
<Button
title="Create"
onClick={handleCreate}
accent="blue"
fullWidth
size="small"
justify="center"
disabled={
viewPickerIsPersisting ||
(viewPickerType === ViewType.Kanban &&
viewPickerKanbanFieldMetadataId === '')
}
/>
);
}
};

View File

@ -0,0 +1,180 @@
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconChevronLeft, IconX } from '@/ui/display/icon';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope';
import { ViewType } from '@/views/types/ViewType';
import { ViewPickerCreateOrEditButton } from '@/views/view-picker/components/ViewPickerCreateOrEditButton';
import { VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerKanbanFieldDropdownId';
import { VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerViewTypeDropdownId';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerPersistView } from '@/views/view-picker/hooks/useViewPickerPersistView';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
const StyledIconAndNameContainer = styled.div`
align-items: center;
display: flex;
margin-left: ${({ theme }) => theme.spacing(1)};
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledSelectContainer = styled.div`
display: flex;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
margin: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.light};
user-select: none;
`;
const StyledNoKanbanFieldAvailableContainer = styled.div`
color: ${({ theme }) => theme.font.color.light};
display: flex;
margin: ${({ theme }) => theme.spacing(1, 2)};
user-select: none;
width: calc(100% - ${({ theme }) => theme.spacing(4)});
`;
const StyledSaveButtonContainer = styled.div`
display: flex;
padding: ${({ theme }) => theme.spacing(1)};
width: calc(100% - ${({ theme }) => theme.spacing(2)});
`;
export const ViewPickerCreateOrEditContent = () => {
const { viewPickerMode, setViewPickerMode } = useViewPickerMode();
const {
viewPickerInputNameState,
viewPickerSelectedIconState,
viewPickerIsPersistingState,
viewPickerKanbanFieldMetadataIdState,
viewPickerTypeState,
} = useViewPickerStates();
const [viewPickerInputName, setViewPickerInputName] = useRecoilState(
viewPickerInputNameState,
);
const [viewPickerSelectedIcon, setViewPickerSelectedIcon] = useRecoilState(
viewPickerSelectedIconState,
);
const viewPickerIsPersisting = useRecoilValue(viewPickerIsPersistingState);
const [viewPickerKanbanFieldMetadataId, setViewPickerKanbanFieldMetadataId] =
useRecoilState(viewPickerKanbanFieldMetadataIdState);
const [viewPickerType, setViewPickerType] =
useRecoilState(viewPickerTypeState);
const setHotkeyScope = useSetHotkeyScope();
const { handleCreate, handleUpdate } = useViewPickerPersistView();
useScopedHotkeys(
Key.Enter,
async () => {
if (viewPickerMode === 'create') {
if (viewPickerIsPersisting) {
return;
}
await handleCreate();
}
if (viewPickerMode === 'edit') {
if (viewPickerIsPersisting) {
return;
}
await handleUpdate();
}
},
ViewsHotkeyScope.ListDropdown,
);
const onIconChange = ({ iconKey }: { iconKey: string }) => {
setViewPickerSelectedIcon(iconKey);
};
const { availableFieldsForKanban } = useGetAvailableFieldsForKanban();
return (
<>
<DropdownMenuHeader
StartIcon={viewPickerMode === 'create' ? IconX : IconChevronLeft}
onClick={() => setViewPickerMode('list')}
>
{viewPickerMode === 'create' ? 'Create view' : 'Edit view'}
</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<StyledIconAndNameContainer>
<IconPicker
onChange={onIconChange}
selectedIconKey={viewPickerSelectedIcon}
disableBlur
onClose={() => setHotkeyScope(ViewsHotkeyScope.ListDropdown)}
/>
<DropdownMenuInput
value={viewPickerInputName}
onChange={(event) => setViewPickerInputName(event.target.value)}
autoFocus
/>
</StyledIconAndNameContainer>
{viewPickerMode === 'create' && (
<StyledSelectContainer>
<Select
disableBlur
label="View type"
fullWidth
value={viewPickerType}
onChange={(value) => setViewPickerType(value)}
options={[
{ value: ViewType.Table, label: 'Table' },
{ value: ViewType.Kanban, label: 'Kanban' },
]}
dropdownId={VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID}
/>
</StyledSelectContainer>
)}
{viewPickerType === ViewType.Kanban && viewPickerMode === 'create' && (
<>
<StyledSelectContainer>
<Select
disableBlur
label="Stages"
fullWidth
value={viewPickerKanbanFieldMetadataId}
onChange={(value) => setViewPickerKanbanFieldMetadataId(value)}
options={
availableFieldsForKanban.length > 0
? availableFieldsForKanban.map((field) => ({
value: field.id,
label: field.label,
}))
: [{ value: '', label: 'No Select field' }]
}
dropdownId={VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID}
/>
</StyledSelectContainer>
{availableFieldsForKanban.length === 0 && (
<StyledNoKanbanFieldAvailableContainer>
Set up a Select field on Companies to create a Kanban
</StyledNoKanbanFieldAvailableContainer>
)}
</>
)}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<StyledSaveButtonContainer>
<ViewPickerCreateOrEditButton />
</StyledSaveButtonContainer>
</DropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1,65 @@
import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
import { isDefined } from '~/utils/isDefined';
export const ViewPickerCreateOrEditContentEffect = () => {
const {
viewPickerSelectedIconState,
viewPickerInputNameState,
viewPickerReferenceViewIdState,
viewPickerIsPersistingState,
viewPickerKanbanFieldMetadataIdState,
viewPickerTypeState,
} = useViewPickerStates();
const setViewPickerSelectedIcon = useSetRecoilState(
viewPickerSelectedIconState,
);
const setViewPickerInputName = useSetRecoilState(viewPickerInputNameState);
const setViewPickerKanbanFieldMetadataId = useSetRecoilState(
viewPickerKanbanFieldMetadataIdState,
);
const setViewPickerType = useSetRecoilState(viewPickerTypeState);
const viewPickerReferenceViewId = useRecoilValue(
viewPickerReferenceViewIdState,
);
const viewPickerIsPersisting = useRecoilValue(viewPickerIsPersistingState);
const { viewsOnCurrentObject } = useGetCurrentView();
const referenceView = viewsOnCurrentObject.find(
(view) => view.id === viewPickerReferenceViewId,
);
const { availableFieldsForKanban } = useGetAvailableFieldsForKanban();
useEffect(() => {
if (isDefined(referenceView) && !viewPickerIsPersisting) {
setViewPickerSelectedIcon(referenceView.icon);
setViewPickerInputName(referenceView.name);
setViewPickerKanbanFieldMetadataId(referenceView.kanbanFieldMetadataId);
setViewPickerType(referenceView.type);
}
}, [
referenceView,
setViewPickerInputName,
setViewPickerKanbanFieldMetadataId,
setViewPickerSelectedIcon,
setViewPickerType,
viewPickerIsPersisting,
]);
useEffect(() => {
if (availableFieldsForKanban.length > 0) {
setViewPickerKanbanFieldMetadataId(availableFieldsForKanban[0].id);
}
}, [availableFieldsForKanban, setViewPickerKanbanFieldMetadataId]);
return <></>;
};

View File

@ -0,0 +1,100 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconChevronDown, IconList } from '@/ui/display/icon';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/MobileViewport';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope';
import { ViewPickerCreateOrEditContent } from '@/views/view-picker/components/ViewPickerCreateOrEditContent';
import { ViewPickerCreateOrEditContentEffect } from '@/views/view-picker/components/ViewPickerCreateOrEditContentEffect';
import { ViewPickerListContent } from '@/views/view-picker/components/ViewPickerListContent';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewStates } from '../../hooks/internal/useViewStates';
const StyledDropdownLabelAdornments = styled.span`
align-items: center;
color: ${({ theme }) => theme.grayScale.gray35};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewName = styled.span`
margin-left: ${({ theme }) => theme.spacing(1)};
display: inline-block;
max-width: 130px;
@media (max-width: 375px) {
max-width: 90px;
}
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
max-width: 110px;
}
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
`;
export const ViewPickerDropdown = () => {
const theme = useTheme();
const { entityCountInCurrentViewState } = useViewStates();
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const entityCountInCurrentView = useRecoilValue(
entityCountInCurrentViewState,
);
const { isDropdownOpen: isViewsListDropdownOpen } = useDropdown(
VIEW_PICKER_DROPDOWN_ID,
);
const { viewPickerMode, setViewPickerMode } = useViewPickerMode();
const { getIcon } = useIcons();
const CurrentViewIcon = getIcon(currentViewWithCombinedFiltersAndSorts?.icon);
return (
<Dropdown
dropdownId={VIEW_PICKER_DROPDOWN_ID}
dropdownHotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
dropdownOffset={{ x: 0, y: 8 }}
dropdownMenuWidth={200}
onClickOutside={() => setViewPickerMode('list')}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isViewsListDropdownOpen}>
{currentViewWithCombinedFiltersAndSorts && CurrentViewIcon ? (
<CurrentViewIcon size={theme.icon.size.md} />
) : (
<IconList size={theme.icon.size.md} />
)}
<StyledViewName>
{currentViewWithCombinedFiltersAndSorts?.name ?? 'All'}
</StyledViewName>
<StyledDropdownLabelAdornments>
· {entityCountInCurrentView}{' '}
<IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments>
</StyledDropdownButtonContainer>
}
dropdownComponents={
viewPickerMode === 'list' ? (
<ViewPickerListContent />
) : (
<>
<ViewPickerCreateOrEditContent />
<ViewPickerCreateOrEditContentEffect />
</>
)
}
/>
);
};

View File

@ -0,0 +1,93 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { IconLock, IconPencil, IconPlus } from '@/ui/display/icon';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useHandleViews } from '@/views/hooks/useHandleViews';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
import { isDefined } from '~/utils/isDefined';
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
export const ViewPickerListContent = () => {
const { selectView } = useHandleViews();
const { currentViewWithCombinedFiltersAndSorts, viewsOnCurrentObject } =
useGetCurrentView();
const { viewPickerReferenceViewIdState } = useViewPickerStates();
const setViewPickerReferenceViewId = useSetRecoilState(
viewPickerReferenceViewIdState,
);
const { setViewPickerMode } = useViewPickerMode();
const { closeDropdown } = useDropdown(VIEW_PICKER_DROPDOWN_ID);
const handleViewSelect = (viewId: string) => {
selectView(viewId);
closeDropdown();
};
const handleAddViewButtonClick = () => {
if (isDefined(currentViewWithCombinedFiltersAndSorts?.id)) {
setViewPickerReferenceViewId(currentViewWithCombinedFiltersAndSorts.id);
setViewPickerMode('create');
}
};
const handleEditViewButtonClick = (
event: MouseEvent<HTMLButtonElement>,
viewId: string,
) => {
event.stopPropagation();
setViewPickerReferenceViewId(viewId);
setViewPickerMode('edit');
};
const { getIcon } = useIcons();
return (
<>
<DropdownMenuItemsContainer>
{viewsOnCurrentObject.map((view) => (
<MenuItem
key={view.id}
iconButtons={[
view.key !== 'INDEX'
? {
Icon: IconPencil,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleEditViewButtonClick(event, view.id),
}
: {
Icon: IconLock,
},
].filter(isDefined)}
onClick={() => handleViewSelect(view.id)}
LeftIcon={getIcon(view.icon)}
text={view.name}
/>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<StyledBoldDropdownMenuItemsContainer>
<MenuItem
onClick={handleAddViewButtonClick}
LeftIcon={IconPlus}
text="Add view"
/>
</StyledBoldDropdownMenuItemsContainer>
</>
);
};

View File

@ -0,0 +1 @@
export const VIEW_PICKER_DROPDOWN_ID = 'view-picker';

View File

@ -0,0 +1 @@
export const VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID = 'view-picker-kanban-field';

View File

@ -0,0 +1 @@
export const VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID = 'view-picker-view-type';

View File

@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerKanbanFieldDropdownId';
import { VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerViewTypeDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
export const useCloseAndResetViewPicker = () => {
const { setViewPickerMode } = useViewPickerMode();
const { viewPickerIsPersistingState } = useViewPickerStates();
const setViewPickerIsPersisting = useSetRecoilState(
viewPickerIsPersistingState,
);
const { closeDropdown: closeViewPickerDropdown } = useDropdown(
VIEW_PICKER_DROPDOWN_ID,
);
const { closeDropdown: closeKanbanFieldDropdown } = useDropdown(
VIEW_PICKER_KANBAN_FIELD_DROPDOWN_ID,
);
const { closeDropdown: closeTypeDropdown } = useDropdown(
VIEW_PICKER_VIEW_TYPE_DROPDOWN_ID,
);
const closeAndResetViewPicker = useCallback(() => {
setViewPickerIsPersisting(false);
setViewPickerMode('list');
closeKanbanFieldDropdown();
closeTypeDropdown();
closeViewPickerDropdown();
}, [
closeKanbanFieldDropdown,
closeTypeDropdown,
closeViewPickerDropdown,
setViewPickerIsPersisting,
setViewPickerMode,
]);
return { closeAndResetViewPicker };
};

View File

@ -0,0 +1,34 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useViewStates } from '@/views/hooks/internal/useViewStates';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useGetAvailableFieldsForKanban = () => {
const { viewObjectMetadataIdState } = useViewStates();
const viewObjectMetadataId = useRecoilValue(viewObjectMetadataIdState);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadata) => objectMetadata.id === viewObjectMetadataId,
);
const availableFieldsForKanban =
objectMetadataItem?.fields.filter(
(field) => field.type === FieldMetadataType.Select,
) ?? [];
const navigate = useNavigate();
const navigateToSelectSettings = useCallback(() => {
navigate(`/settings/objects/${objectMetadataItem?.namePlural}`);
}, [navigate, objectMetadataItem?.namePlural]);
return {
availableFieldsForKanban,
navigateToSelectSettings,
};
};

View File

@ -0,0 +1,15 @@
import { useRecoilState } from 'recoil';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
export const useViewPickerMode = (viewBarComponentId?: string) => {
const { viewPickerModeState } = useViewPickerStates(viewBarComponentId);
const [viewPickerMode, setViewPickerMode] =
useRecoilState(viewPickerModeState);
return {
viewPickerMode,
setViewPickerMode,
};
};

View File

@ -0,0 +1,125 @@
import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useHandleViews } from '@/views/hooks/useHandleViews';
import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker';
import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates';
export const useViewPickerPersistView = () => {
const {
viewPickerInputNameState,
viewPickerSelectedIconState,
viewPickerIsPersistingState,
viewPickerReferenceViewIdState,
viewPickerKanbanFieldMetadataIdState,
viewPickerTypeState,
} = useViewPickerStates();
const { createView, selectView, removeView, updateView } = useHandleViews();
const { viewsOnCurrentObject } = useGetCurrentView();
const { closeAndResetViewPicker } = useCloseAndResetViewPicker();
const handleCreate = useRecoilCallback(
({ snapshot, set }) =>
async () => {
const name = getSnapshotValue(snapshot, viewPickerInputNameState);
const iconKey = getSnapshotValue(snapshot, viewPickerSelectedIconState);
const type = getSnapshotValue(snapshot, viewPickerTypeState);
const kanbanFieldMetadataId = getSnapshotValue(
snapshot,
viewPickerKanbanFieldMetadataIdState,
);
const id = v4();
set(viewPickerIsPersistingState, true);
await createView({
id,
name,
icon: iconKey,
type,
kanbanFieldMetadataId,
});
closeAndResetViewPicker();
selectView(id);
},
[
closeAndResetViewPicker,
createView,
selectView,
viewPickerInputNameState,
viewPickerIsPersistingState,
viewPickerKanbanFieldMetadataIdState,
viewPickerSelectedIconState,
viewPickerTypeState,
],
);
const handleDelete = useRecoilCallback(
({ set, snapshot }) =>
async () => {
set(viewPickerIsPersistingState, true);
const viewPickerReferenceViewId = getSnapshotValue(
snapshot,
viewPickerReferenceViewIdState,
);
selectView(
viewsOnCurrentObject.filter(
(view) => view.id !== viewPickerReferenceViewId,
)[0].id,
);
await removeView(viewPickerReferenceViewId);
closeAndResetViewPicker();
},
[
closeAndResetViewPicker,
removeView,
selectView,
viewPickerIsPersistingState,
viewPickerReferenceViewIdState,
viewsOnCurrentObject,
],
);
const handleUpdate = useRecoilCallback(
({ set, snapshot }) =>
async () => {
set(viewPickerIsPersistingState, true);
const viewPickerReferenceViewId = getSnapshotValue(
snapshot,
viewPickerReferenceViewIdState,
);
const viewPickerInputName = getSnapshotValue(
snapshot,
viewPickerInputNameState,
);
const viewPickerSelectedIcon = getSnapshotValue(
snapshot,
viewPickerSelectedIconState,
);
await updateView({
id: viewPickerReferenceViewId,
name: viewPickerInputName,
icon: viewPickerSelectedIcon,
});
selectView(viewPickerReferenceViewId);
closeAndResetViewPicker();
},
[
viewPickerIsPersistingState,
viewPickerReferenceViewIdState,
viewPickerInputNameState,
viewPickerSelectedIconState,
updateView,
selectView,
closeAndResetViewPicker,
],
);
return { handleCreate, handleDelete, handleUpdate };
};

View File

@ -0,0 +1,50 @@
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState';
import { viewPickerModeComponentState } from '@/views/view-picker/states/viewPickerModeComponentState';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState';
import { ViewScopeInternalContext } from '../../scopes/scope-internal-context/ViewScopeInternalContext';
export const useViewPickerStates = (viewComponentId?: string) => {
const componentId = useAvailableScopeIdOrThrow(
ViewScopeInternalContext,
viewComponentId,
);
return {
componentId,
viewPickerModeState: extractComponentState(
viewPickerModeComponentState,
componentId,
),
viewPickerInputNameState: extractComponentState(
viewPickerInputNameComponentState,
componentId,
),
viewPickerSelectedIconState: extractComponentState(
viewPickerSelectedIconComponentState,
componentId,
),
viewPickerKanbanFieldMetadataIdState: extractComponentState(
viewPickerKanbanFieldMetadataIdComponentState,
componentId,
),
viewPickerReferenceViewIdState: extractComponentState(
viewPickerReferenceViewIdComponentState,
componentId,
),
viewPickerIsPersistingState: extractComponentState(
viewPickerIsPersistingComponentState,
componentId,
),
viewPickerTypeState: extractComponentState(
viewPickerTypeComponentState,
componentId,
),
};
};

View File

@ -0,0 +1,6 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewPickerInputNameComponentState = createComponentState<string>({
key: 'viewPickerInputNameComponentState',
defaultValue: '',
});

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewPickerIsPersistingComponentState =
createComponentState<boolean>({
key: 'viewPickerIsPersistingComponentState',
defaultValue: false,
});

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewPickerKanbanFieldMetadataIdComponentState =
createComponentState<string>({
key: 'viewPickerKanbanFieldMetadataIdComponentState',
defaultValue: '',
});

View File

@ -1,8 +1,8 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewEditModeComponentState = createComponentState<
'none' | 'edit' | 'create'
export const viewPickerModeComponentState = createComponentState<
'list' | 'edit' | 'create'
>({
key: 'viewEditModeComponentState',
defaultValue: 'none',
defaultValue: 'list',
});

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewPickerReferenceViewIdComponentState =
createComponentState<string>({
key: 'viewPickerReferenceViewIdComponentState',
defaultValue: '',
});

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const viewPickerSelectedIconComponentState =
createComponentState<string>({
key: 'viewPickerSelectedIconComponentState',
defaultValue: '',
});

View File

@ -0,0 +1,7 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { ViewType } from '@/views/types/ViewType';
export const viewPickerTypeComponentState = createComponentState<ViewType>({
key: 'viewPickerTypeComponentState',
defaultValue: ViewType.Table,
});

View File

@ -85,14 +85,12 @@ export const viewPrefillData = async (
'size',
])
.values([
...viewCompanyFields(viewIdMap['Index Companies'], objectMetadataMap),
...viewPersonFields(viewIdMap['Index People'], objectMetadataMap),
...viewOpportunityFields(
viewIdMap['Index Opportunities'],
objectMetadataMap,
),
...viewCompanyFields(viewIdMap['All Companies'], objectMetadataMap),
...viewPersonFields(viewIdMap['All People'], objectMetadataMap),
...viewOpportunityFields(
viewIdMap['All Opportunities'],
objectMetadataMap,
),
...viewOpportunityFields(viewIdMap['By Stage'], objectMetadataMap),
])
.execute();