@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { companyViewFields } from '@/companies/constants/companyViewFields';
|
||||
import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries';
|
||||
@ -7,15 +6,15 @@ import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyT
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { IconList } from '@/ui/icon';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTable';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
|
||||
import { useTableViews } from '@/views/hooks/useTableViews';
|
||||
import { useViewSorts } from '@/views/hooks/useViewSorts';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import {
|
||||
SortOrder,
|
||||
UpdateOneCompanyMutationVariables,
|
||||
@ -26,7 +25,10 @@ import { companiesFilters } from '~/pages/companies/companies-filters';
|
||||
import { availableSorts } from '~/pages/companies/companies-sorts';
|
||||
|
||||
export function CompanyTable() {
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const orderBy = useRecoilScopedValue(
|
||||
sortsOrderByScopedState,
|
||||
TableRecoilScopeContext,
|
||||
@ -34,14 +36,13 @@ export function CompanyTable() {
|
||||
const [updateEntityMutation] = useUpdateOneCompanyMutation();
|
||||
const upsertEntityTableItem = useUpsertEntityTableItem();
|
||||
|
||||
const objectId = 'company';
|
||||
const { handleViewsChange } = useTableViews({ objectId });
|
||||
const { handleColumnsChange } = useTableViewFields({
|
||||
objectName: 'company',
|
||||
objectName: objectId,
|
||||
viewFieldDefinitions: companyViewFields,
|
||||
});
|
||||
const { updateSorts } = useViewSorts({
|
||||
availableSorts,
|
||||
Context: TableRecoilScopeContext,
|
||||
});
|
||||
const { updateSorts } = useViewSorts({ availableSorts });
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
@ -76,10 +77,10 @@ export function CompanyTable() {
|
||||
/>
|
||||
<EntityTable
|
||||
viewName="All Companies"
|
||||
viewIcon={<IconList size={16} />}
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={handleColumnsChange}
|
||||
onSortsUpdate={currentViewId ? updateSorts : undefined}
|
||||
onViewsChange={handleViewsChange}
|
||||
updateEntityMutation={({
|
||||
variables,
|
||||
}: {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { IconList } from '@/ui/icon';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTable';
|
||||
import { useUpdateOneCompanyMutation } from '~/generated/graphql';
|
||||
import { availableSorts } from '~/pages/companies/companies-sorts';
|
||||
@ -11,7 +10,6 @@ export function CompanyTableMockMode() {
|
||||
<CompanyTableMockData />
|
||||
<EntityTable
|
||||
viewName="All Companies"
|
||||
viewIcon={<IconList size={16} />}
|
||||
availableSorts={availableSorts}
|
||||
updateEntityMutation={[useUpdateOneCompanyMutation()]}
|
||||
/>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { peopleViewFields } from '@/people/constants/peopleViewFields';
|
||||
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
|
||||
@ -7,15 +6,15 @@ import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableAct
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { IconList } from '@/ui/icon';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTable';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
|
||||
import { useTableViews } from '@/views/hooks/useTableViews';
|
||||
import { useViewSorts } from '@/views/hooks/useViewSorts';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import {
|
||||
SortOrder,
|
||||
UpdateOnePersonMutationVariables,
|
||||
@ -26,7 +25,10 @@ import { peopleFilters } from '~/pages/people/people-filters';
|
||||
import { availableSorts } from '~/pages/people/people-sorts';
|
||||
|
||||
export function PeopleTable() {
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const orderBy = useRecoilScopedValue(
|
||||
sortsOrderByScopedState,
|
||||
TableRecoilScopeContext,
|
||||
@ -34,14 +36,13 @@ export function PeopleTable() {
|
||||
const [updateEntityMutation] = useUpdateOnePersonMutation();
|
||||
const upsertEntityTableItem = useUpsertEntityTableItem();
|
||||
|
||||
const objectId = 'person';
|
||||
const { handleViewsChange } = useTableViews({ objectId });
|
||||
const { handleColumnsChange } = useTableViewFields({
|
||||
objectName: 'person',
|
||||
objectName: objectId,
|
||||
viewFieldDefinitions: peopleViewFields,
|
||||
});
|
||||
const { updateSorts } = useViewSorts({
|
||||
availableSorts,
|
||||
Context: TableRecoilScopeContext,
|
||||
});
|
||||
const { updateSorts } = useViewSorts({ availableSorts });
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
@ -76,10 +77,10 @@ export function PeopleTable() {
|
||||
/>
|
||||
<EntityTable
|
||||
viewName="All People"
|
||||
viewIcon={<IconList size={16} />}
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={handleColumnsChange}
|
||||
onSortsUpdate={currentViewId ? updateSorts : undefined}
|
||||
onViewsChange={handleViewsChange}
|
||||
updateEntityMutation={({
|
||||
variables,
|
||||
}: {
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
|
||||
export const DropdownMenuInputContainer = styled.div`
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(36px - 2 * var(--vertical-padding));
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
width: calc(100%);
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
|
||||
${textInputStyle}
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const DropdownMenuInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>((props, ref) => (
|
||||
<DropdownMenuInputContainer>
|
||||
<StyledInput autoComplete="off" placeholder="Search" {...props} ref={ref} />
|
||||
</DropdownMenuInputContainer>
|
||||
));
|
||||
@ -1,39 +0,0 @@
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
|
||||
export const DropdownMenuSearchContainer = styled.div`
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(36px - 2 * var(--vertical-padding));
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
width: calc(100%);
|
||||
`;
|
||||
|
||||
const StyledEditModeSearchInput = styled.input`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
|
||||
${textInputStyle}
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function DropdownMenuSearch(
|
||||
props: InputHTMLAttributes<HTMLInputElement>,
|
||||
) {
|
||||
return (
|
||||
<DropdownMenuSearchContainer>
|
||||
<StyledEditModeSearchInput
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
placeholder={props.placeholder ?? 'Search'}
|
||||
/>
|
||||
</DropdownMenuSearchContainer>
|
||||
);
|
||||
}
|
||||
@ -11,9 +11,9 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { DropdownMenu } from '../DropdownMenu';
|
||||
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
|
||||
import { DropdownMenuHeader } from '../DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '../DropdownMenuInput';
|
||||
import { DropdownMenuItem } from '../DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearch } from '../DropdownMenuSearch';
|
||||
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
|
||||
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||
import { DropdownMenuSubheader } from '../DropdownMenuSubheader';
|
||||
@ -256,7 +256,7 @@ export const LoadingMenu: Story = {
|
||||
...WithContentBelow,
|
||||
render: () => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuSearch value={'query'} autoFocus />
|
||||
<DropdownMenuInput value={'query'} autoFocus />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSkeletonItem />
|
||||
@ -269,7 +269,7 @@ export const Search: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuSearch />
|
||||
<DropdownMenuInput />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
|
||||
@ -4,19 +4,18 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||
|
||||
import { DropdownMenuContainer } from './DropdownMenuContainer';
|
||||
|
||||
type OwnProps = {
|
||||
label: string;
|
||||
anchor?: 'left' | 'right';
|
||||
label: ReactNode;
|
||||
isActive: boolean;
|
||||
children?: ReactNode;
|
||||
isUnfolded?: boolean;
|
||||
icon?: ReactNode;
|
||||
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
|
||||
resetState?: () => void;
|
||||
HotkeyScope: FiltersHotkeyScope;
|
||||
HotkeyScope: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
@ -59,7 +58,12 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDropdownMenuContainer = styled(DropdownMenuContainer)`
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
function DropdownButton({
|
||||
anchor,
|
||||
label,
|
||||
isActive,
|
||||
children,
|
||||
@ -99,9 +103,9 @@ function DropdownButton({
|
||||
{label}
|
||||
</StyledDropdownButton>
|
||||
{isUnfolded && (
|
||||
<DropdownMenuContainer onClose={onOutsideClick}>
|
||||
<StyledDropdownMenuContainer anchor={anchor} onClose={onOutsideClick}>
|
||||
{children}
|
||||
</DropdownMenuContainer>
|
||||
</StyledDropdownMenuContainer>
|
||||
)}
|
||||
</StyledDropdownButtonContainer>
|
||||
);
|
||||
|
||||
@ -1,23 +1,32 @@
|
||||
import { useRef } from 'react';
|
||||
import { type HTMLAttributes, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||
|
||||
export const StyledDropdownMenuContainer = styled.ul`
|
||||
export const StyledDropdownMenuContainer = styled.ul<{
|
||||
anchor: 'left' | 'right';
|
||||
}>`
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
${({ anchor }) => {
|
||||
if (anchor === 'right') return 'right: 0';
|
||||
}};
|
||||
top: 14px;
|
||||
`;
|
||||
|
||||
export function DropdownMenuContainer({
|
||||
children,
|
||||
onClose,
|
||||
}: {
|
||||
type DropdownMenuContainerProps = {
|
||||
anchor?: 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
} & HTMLAttributes<HTMLUListElement>;
|
||||
|
||||
export function DropdownMenuContainer({
|
||||
anchor = 'right',
|
||||
children,
|
||||
onClose,
|
||||
...props
|
||||
}: DropdownMenuContainerProps) {
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useListenClickOutside({
|
||||
@ -28,7 +37,7 @@ export function DropdownMenuContainer({
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDropdownMenuContainer data-select-disable>
|
||||
<StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}>
|
||||
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
|
||||
</StyledDropdownMenuContainer>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ChangeEvent, Context } from 'react';
|
||||
|
||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
|
||||
@ -27,7 +27,7 @@ export function FilterDropdownEntitySearchInput({
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearch
|
||||
<DropdownMenuInput
|
||||
type="text"
|
||||
value={filterDropdownSearchInput}
|
||||
placeholder={filterDefinitionUsedInDropdown.label}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ChangeEvent, Context } from 'react';
|
||||
|
||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useRemoveFilter } from '../hooks/useRemoveFilter';
|
||||
@ -29,7 +29,7 @@ export function FilterDropdownNumberSearchInput({
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearch
|
||||
<DropdownMenuInput
|
||||
type="number"
|
||||
placeholder={filterDefinitionUsedInDropdown.label}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ChangeEvent, Context } from 'react';
|
||||
|
||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
|
||||
@ -36,7 +36,7 @@ export function FilterDropdownTextSearchInput({
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearch
|
||||
<DropdownMenuInput
|
||||
type="text"
|
||||
placeholder={filterDefinitionUsedInDropdown.label}
|
||||
value={filterCurrentlyEdited?.value ?? filterDropdownSearchInput}
|
||||
|
||||
@ -1,58 +1,60 @@
|
||||
export { IconAddressBook } from './components/IconAddressBook';
|
||||
export { IconBuildingSkyscraper } from '@tabler/icons-react';
|
||||
export { IconMessageCircle as IconComment } from '@tabler/icons-react';
|
||||
export { IconCheck } from '@tabler/icons-react';
|
||||
export { IconTrash } from '@tabler/icons-react';
|
||||
export { IconLayoutSidebarRightCollapse } from '@tabler/icons-react';
|
||||
export { IconLayoutSidebarLeftCollapse } from '@tabler/icons-react';
|
||||
export { IconUser } from '@tabler/icons-react';
|
||||
export { IconBell } from '@tabler/icons-react';
|
||||
export { IconList } from '@tabler/icons-react';
|
||||
export { IconInbox } from '@tabler/icons-react';
|
||||
export { IconSearch } from '@tabler/icons-react';
|
||||
export { IconArchive } from '@tabler/icons-react';
|
||||
export { IconSettings } from '@tabler/icons-react';
|
||||
export { IconLogout } from '@tabler/icons-react';
|
||||
export { IconColorSwatch } from '@tabler/icons-react';
|
||||
export { IconProgressCheck } from '@tabler/icons-react';
|
||||
export { IconX } from '@tabler/icons-react';
|
||||
export { IconChevronLeft } from '@tabler/icons-react';
|
||||
export { IconBriefcase } from '@tabler/icons-react';
|
||||
export { IconPlus } from '@tabler/icons-react';
|
||||
export { IconMinus } from '@tabler/icons-react';
|
||||
export { IconLink } from '@tabler/icons-react';
|
||||
export { IconBrandLinkedin } from '@tabler/icons-react';
|
||||
export { IconUsers } from '@tabler/icons-react';
|
||||
export { IconCalendarEvent } from '@tabler/icons-react';
|
||||
export { IconMap } from '@tabler/icons-react';
|
||||
export { IconMail } from '@tabler/icons-react';
|
||||
export { IconPhone } from '@tabler/icons-react';
|
||||
export { IconTargetArrow } from '@tabler/icons-react';
|
||||
export { IconChevronDown } from '@tabler/icons-react';
|
||||
export { IconArrowNarrowDown } from '@tabler/icons-react';
|
||||
export { IconArrowNarrowUp } from '@tabler/icons-react';
|
||||
export { IconArrowRight } from '@tabler/icons-react';
|
||||
export { IconArrowUpRight } from '@tabler/icons-react';
|
||||
export { IconBrandGoogle } from '@tabler/icons-react';
|
||||
export { IconUpload } from '@tabler/icons-react';
|
||||
export { IconFileUpload } from '@tabler/icons-react';
|
||||
export { IconChevronsRight } from '@tabler/icons-react';
|
||||
export { IconNotes } from '@tabler/icons-react';
|
||||
export { IconCirclePlus } from '@tabler/icons-react';
|
||||
export { IconCheckbox } from '@tabler/icons-react';
|
||||
export { IconTimelineEvent } from '@tabler/icons-react';
|
||||
export { IconAlertCircle } from '@tabler/icons-react';
|
||||
export { IconEye } from '@tabler/icons-react';
|
||||
export { IconEyeOff } from '@tabler/icons-react';
|
||||
export { IconAlertTriangle } from '@tabler/icons-react';
|
||||
export { IconCopy } from '@tabler/icons-react';
|
||||
export { IconCurrencyDollar } from '@tabler/icons-react';
|
||||
export { IconUserCircle } from '@tabler/icons-react';
|
||||
export { IconCalendar } from '@tabler/icons-react';
|
||||
export { IconPencil } from '@tabler/icons-react';
|
||||
export { IconCircleDot } from '@tabler/icons-react';
|
||||
export { IconHeart } from '@tabler/icons-react';
|
||||
export { IconBrandX } from '@tabler/icons-react';
|
||||
export { IconTag } from '@tabler/icons-react';
|
||||
export { IconHelpCircle } from '@tabler/icons-react';
|
||||
export { IconBrandGithub } from '@tabler/icons-react';
|
||||
export {
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconArchive,
|
||||
IconArrowNarrowDown,
|
||||
IconArrowNarrowUp,
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
IconBell,
|
||||
IconBrandGithub,
|
||||
IconBrandGoogle,
|
||||
IconBrandLinkedin,
|
||||
IconBrandX,
|
||||
IconBriefcase,
|
||||
IconBuildingSkyscraper,
|
||||
IconCalendar,
|
||||
IconCalendarEvent,
|
||||
IconCheck,
|
||||
IconCheckbox,
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronsRight,
|
||||
IconCircleDot,
|
||||
IconCirclePlus,
|
||||
IconColorSwatch,
|
||||
IconMessageCircle as IconComment,
|
||||
IconCopy,
|
||||
IconCurrencyDollar,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconFileUpload,
|
||||
IconHeart,
|
||||
IconHelpCircle,
|
||||
IconInbox,
|
||||
IconLayoutSidebarLeftCollapse,
|
||||
IconLayoutSidebarRightCollapse,
|
||||
IconLink,
|
||||
IconList,
|
||||
IconLogout,
|
||||
IconMail,
|
||||
IconMap,
|
||||
IconMinus,
|
||||
IconNotes,
|
||||
IconPencil,
|
||||
IconPhone,
|
||||
IconPlus,
|
||||
IconProgressCheck,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconTag,
|
||||
IconTargetArrow,
|
||||
IconTimelineEvent,
|
||||
IconTrash,
|
||||
IconUpload,
|
||||
IconUser,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
@ -3,9 +3,9 @@ import debounce from 'lodash.debounce';
|
||||
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
@ -73,7 +73,7 @@ export function MultipleEntitySelect<
|
||||
|
||||
return (
|
||||
<DropdownMenu ref={containerRef}>
|
||||
<DropdownMenuSearch
|
||||
<DropdownMenuInput
|
||||
value={searchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
|
||||
@ -2,9 +2,9 @@ import { useRef } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
@ -65,7 +65,7 @@ export function SingleEntitySelect<
|
||||
ref={containerRef}
|
||||
width={width}
|
||||
>
|
||||
<DropdownMenuSearch
|
||||
<DropdownMenuInput
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
autoFocus
|
||||
|
||||
@ -14,6 +14,7 @@ import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
|
||||
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
|
||||
import type { TableView } from '../states/tableViewsState';
|
||||
import { TableHeader } from '../table-header/components/TableHeader';
|
||||
|
||||
import { EntityTableBody } from './EntityTableBody';
|
||||
@ -97,16 +98,16 @@ type OwnProps<SortField> = {
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onRowSelectionChange?: (rowSelection: string[]) => void;
|
||||
onViewsChange?: (views: TableView[]) => void;
|
||||
updateEntityMutation: any;
|
||||
};
|
||||
|
||||
export function EntityTable<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onColumnsChange,
|
||||
onSortsUpdate,
|
||||
onViewsChange,
|
||||
updateEntityMutation,
|
||||
}: OwnProps<SortField>) {
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
@ -131,10 +132,10 @@ export function EntityTable<SortField>({
|
||||
<StyledTableContainer ref={tableBodyRef}>
|
||||
<TableHeader
|
||||
viewName={viewName}
|
||||
viewIcon={viewIcon}
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={onColumnsChange}
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
onViewsChange={onViewsChange}
|
||||
/>
|
||||
<StyledTableWrapper>
|
||||
<StyledTable>
|
||||
|
||||
@ -1,30 +1,50 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
type FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import {
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
|
||||
import {
|
||||
hiddenTableColumnsState,
|
||||
tableColumnsState,
|
||||
visibleTableColumnsState,
|
||||
} from '@/ui/table/states/tableColumnsState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import {
|
||||
type TableView,
|
||||
tableViewEditModeState,
|
||||
tableViewsByIdState,
|
||||
tableViewsState,
|
||||
} from '../../states/tableViewsState';
|
||||
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
|
||||
|
||||
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
|
||||
|
||||
type TableOptionsDropdownButtonProps = {
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
HotkeyScope: FiltersHotkeyScope;
|
||||
onViewsChange?: (views: TableView[]) => void;
|
||||
HotkeyScope: TableOptionsHotkeyScope;
|
||||
};
|
||||
|
||||
enum Option {
|
||||
@ -33,17 +53,37 @@ enum Option {
|
||||
|
||||
export const TableOptionsDropdownButton = ({
|
||||
onColumnsChange,
|
||||
onViewsChange,
|
||||
HotkeyScope,
|
||||
}: TableOptionsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const viewEditInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [columns, setColumns] = useRecoilState(tableColumnsState);
|
||||
const [viewEditMode, setViewEditMode] = useRecoilState(
|
||||
tableViewEditModeState,
|
||||
);
|
||||
const [views, setViews] = useRecoilScopedState(
|
||||
tableViewsState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const visibleColumns = useRecoilValue(visibleTableColumnsState);
|
||||
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
|
||||
const viewsById = useRecoilScopedValue(
|
||||
tableViewsByIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(columnId: string, nextIsVisible: boolean) => {
|
||||
@ -79,25 +119,109 @@ export const TableOptionsDropdownButton = ({
|
||||
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
|
||||
);
|
||||
|
||||
const resetViewEditMode = useCallback(() => {
|
||||
setViewEditMode({ mode: undefined, viewId: undefined });
|
||||
|
||||
if (viewEditInputRef.current) {
|
||||
viewEditInputRef.current.value = '';
|
||||
}
|
||||
}, [setViewEditMode]);
|
||||
|
||||
const handleViewNameSubmit = useCallback(
|
||||
(event?: FormEvent) => {
|
||||
event?.preventDefault();
|
||||
|
||||
if (viewEditMode.mode && viewEditInputRef.current?.value) {
|
||||
const name = viewEditInputRef.current.value;
|
||||
const nextViews =
|
||||
viewEditMode.mode === 'create'
|
||||
? [...views, { id: v4(), name }]
|
||||
: views.map((view) =>
|
||||
view.id === viewEditMode.viewId ? { ...view, name } : view,
|
||||
);
|
||||
|
||||
(onViewsChange ?? setViews)(nextViews);
|
||||
}
|
||||
|
||||
resetViewEditMode();
|
||||
},
|
||||
[
|
||||
onViewsChange,
|
||||
resetViewEditMode,
|
||||
setViews,
|
||||
viewEditMode.mode,
|
||||
viewEditMode.viewId,
|
||||
views,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSelectOption = useCallback(
|
||||
(option: Option) => {
|
||||
handleViewNameSubmit();
|
||||
setIsUnfolded(true);
|
||||
setSelectedOption(option);
|
||||
},
|
||||
[handleViewNameSubmit],
|
||||
);
|
||||
|
||||
const resetSelectedOption = useCallback(() => {
|
||||
setSelectedOption(undefined);
|
||||
}, []);
|
||||
|
||||
const handleUnfoldedChange = useCallback(
|
||||
(nextIsUnfolded: boolean) => {
|
||||
setIsUnfolded(nextIsUnfolded);
|
||||
|
||||
if (!nextIsUnfolded) {
|
||||
handleViewNameSubmit();
|
||||
resetSelectedOption();
|
||||
}
|
||||
},
|
||||
[handleViewNameSubmit, resetSelectedOption],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
isUnfolded || viewEditMode.mode
|
||||
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
|
||||
: goBackToPreviousHotkeyScope();
|
||||
}, [
|
||||
HotkeyScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
isUnfolded,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
viewEditMode.mode,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
label="Options"
|
||||
isActive={false}
|
||||
isUnfolded={isUnfolded}
|
||||
onIsUnfoldedChange={setIsUnfolded}
|
||||
isUnfolded={isUnfolded || !!viewEditMode.mode}
|
||||
onIsUnfoldedChange={handleUnfoldedChange}
|
||||
HotkeyScope={HotkeyScope}
|
||||
>
|
||||
{!selectedOption && (
|
||||
<>
|
||||
<DropdownMenuHeader>View settings</DropdownMenuHeader>
|
||||
{!!viewEditMode.mode ? (
|
||||
<DropdownMenuInput
|
||||
ref={viewEditInputRef}
|
||||
autoFocus
|
||||
placeholder={
|
||||
viewEditMode.mode === 'create' ? 'New view' : 'View name'
|
||||
}
|
||||
defaultValue={
|
||||
viewEditMode.viewId
|
||||
? viewsById[viewEditMode.viewId]?.name
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuHeader>View settings</DropdownMenuHeader>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setSelectedOption(Option.Properties)}
|
||||
onClick={() => handleSelectOption(Option.Properties)}
|
||||
>
|
||||
<IconTag size={theme.icon.size.md} />
|
||||
Properties
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
import { type MouseEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||
import { IconChevronDown, IconList, IconPencil, IconPlus } from '@/ui/icon';
|
||||
import {
|
||||
currentTableViewIdState,
|
||||
currentTableViewState,
|
||||
tableViewEditModeState,
|
||||
tableViewsState,
|
||||
} from '@/ui/table/states/tableViewsState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope';
|
||||
|
||||
const StyledDropdownMenuItemsContainer = 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 StyledViewIcon = styled(IconList)`
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type TableViewsDropdownButtonProps = {
|
||||
defaultViewName: string;
|
||||
HotkeyScope: TableViewsHotkeyScope;
|
||||
};
|
||||
|
||||
export const TableViewsDropdownButton = ({
|
||||
defaultViewName,
|
||||
HotkeyScope,
|
||||
}: TableViewsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
|
||||
const currentView = useRecoilScopedValue(
|
||||
currentTableViewState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const views = useRecoilScopedValue(tableViewsState, TableRecoilScopeContext);
|
||||
const [, setCurrentViewId] = useRecoilScopedState(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const setViewEditMode = useSetRecoilState(tableViewEditModeState);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const handleViewSelect = useCallback(
|
||||
(viewId?: string) => {
|
||||
setCurrentViewId(viewId);
|
||||
setIsUnfolded(false);
|
||||
},
|
||||
[setCurrentViewId],
|
||||
);
|
||||
|
||||
const handleAddViewButtonClick = useCallback(() => {
|
||||
setViewEditMode({ mode: 'create', viewId: undefined });
|
||||
setIsUnfolded(false);
|
||||
}, [setViewEditMode]);
|
||||
|
||||
const handleEditViewButtonClick = useCallback(
|
||||
(event: MouseEvent<HTMLButtonElement>, viewId: string) => {
|
||||
event.stopPropagation();
|
||||
setViewEditMode({ mode: 'edit', viewId });
|
||||
setIsUnfolded(false);
|
||||
},
|
||||
[setViewEditMode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
isUnfolded
|
||||
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
|
||||
: goBackToPreviousHotkeyScope();
|
||||
}, [
|
||||
HotkeyScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
isUnfolded,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
label={
|
||||
<>
|
||||
<StyledViewIcon size={theme.icon.size.md} />
|
||||
{currentView?.name || defaultViewName}{' '}
|
||||
<StyledDropdownLabelAdornments>
|
||||
· {views.length + 1} <IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledDropdownLabelAdornments>
|
||||
</>
|
||||
}
|
||||
isActive={false}
|
||||
isUnfolded={isUnfolded}
|
||||
onIsUnfoldedChange={setIsUnfolded}
|
||||
anchor="left"
|
||||
HotkeyScope={HotkeyScope}
|
||||
>
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
<DropdownMenuItem onClick={() => handleViewSelect(undefined)}>
|
||||
<IconList size={theme.icon.size.md} />
|
||||
{defaultViewName}
|
||||
</DropdownMenuItem>
|
||||
{views.map((view) => (
|
||||
<DropdownMenuItem
|
||||
key={view.id}
|
||||
actions={
|
||||
<IconButton
|
||||
onClick={(event) => handleEditViewButtonClick(event, view.id)}
|
||||
icon={<IconPencil size={theme.icon.size.sm} />}
|
||||
/>
|
||||
}
|
||||
onClick={() => handleViewSelect(view.id)}
|
||||
>
|
||||
<IconList size={theme.icon.size.md} />
|
||||
{view.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<StyledDropdownMenuItemsContainer>
|
||||
<DropdownMenuItem onClick={handleAddViewButtonClick}>
|
||||
<IconPlus size={theme.icon.size.md} />
|
||||
Add view
|
||||
</DropdownMenuItem>
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</DropdownButton>
|
||||
);
|
||||
};
|
||||
50
front/src/modules/ui/table/states/tableViewsState.ts
Normal file
50
front/src/modules/ui/table/states/tableViewsState.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { atom, atomFamily, selectorFamily } from 'recoil';
|
||||
|
||||
export type TableView = { id: string; name: string };
|
||||
|
||||
export const tableViewsState = atomFamily<TableView[], string>({
|
||||
key: 'tableViewsState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const tableViewsByIdState = selectorFamily<
|
||||
Record<string, TableView>,
|
||||
string
|
||||
>({
|
||||
key: 'tableViewsByIdState',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) =>
|
||||
get(tableViewsState(scopeId)).reduce<Record<string, TableView>>(
|
||||
(result, view) => ({ ...result, [view.id]: view }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
|
||||
export const currentTableViewIdState = atomFamily<string | undefined, string>({
|
||||
key: 'currentTableViewIdState',
|
||||
default: undefined,
|
||||
});
|
||||
|
||||
export const currentTableViewState = selectorFamily<
|
||||
TableView | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'currentTableViewState',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) => {
|
||||
const currentViewId = get(currentTableViewIdState(scopeId));
|
||||
return currentViewId
|
||||
? get(tableViewsByIdState(scopeId))[currentViewId]
|
||||
: undefined;
|
||||
},
|
||||
});
|
||||
|
||||
export const tableViewEditModeState = atom<{
|
||||
mode: 'create' | 'edit' | undefined;
|
||||
viewId: string | undefined;
|
||||
}>({
|
||||
key: 'tableViewEditModeState',
|
||||
default: { mode: undefined, viewId: undefined },
|
||||
});
|
||||
@ -1,5 +1,4 @@
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
@ -15,32 +14,26 @@ import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableO
|
||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import type { TableView } from '../../states/tableViewsState';
|
||||
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
|
||||
import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope';
|
||||
|
||||
type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
viewIcon?: ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onViewsChange?: (views: TableView[]) => void;
|
||||
};
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
& > svg {
|
||||
font-size: ${({ theme }) => theme.icon.size.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
export function TableHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onColumnsChange,
|
||||
onSortsUpdate,
|
||||
onViewsChange,
|
||||
}: OwnProps<SortField>) {
|
||||
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
|
||||
sortScopedState,
|
||||
@ -67,10 +60,10 @@ export function TableHeader<SortField>({
|
||||
return (
|
||||
<TopBar
|
||||
leftComponent={
|
||||
<>
|
||||
<StyledIcon>{viewIcon}</StyledIcon>
|
||||
{viewName}
|
||||
</>
|
||||
<TableViewsDropdownButton
|
||||
defaultViewName={viewName}
|
||||
HotkeyScope={TableViewsHotkeyScope.Dropdown}
|
||||
/>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
rightComponent={
|
||||
@ -90,7 +83,8 @@ export function TableHeader<SortField>({
|
||||
/>
|
||||
<TableOptionsDropdownButton
|
||||
onColumnsChange={onColumnsChange}
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
onViewsChange={onViewsChange}
|
||||
HotkeyScope={TableOptionsHotkeyScope.Dropdown}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export enum TableOptionsHotkeyScope {
|
||||
Dropdown = 'table-options-dropdown',
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export enum TableViewsHotkeyScope {
|
||||
Dropdown = 'table-views-dropdown',
|
||||
}
|
||||
@ -4,13 +4,15 @@ import { v4 } from 'uuid';
|
||||
import { RecoilScopeContext } from '../states/RecoilScopeContext';
|
||||
|
||||
export function RecoilScope({
|
||||
SpecificContext,
|
||||
children,
|
||||
scopeId,
|
||||
SpecificContext,
|
||||
}: {
|
||||
SpecificContext?: Context<string | null>;
|
||||
children: React.ReactNode;
|
||||
scopeId?: string;
|
||||
SpecificContext?: Context<string | null>;
|
||||
}) {
|
||||
const currentScopeId = useRef(v4());
|
||||
const currentScopeId = useRef(scopeId || v4());
|
||||
|
||||
return SpecificContext ? (
|
||||
<SpecificContext.Provider value={currentScopeId.current}>
|
||||
|
||||
9
front/src/modules/views/graphql/mutations/createViews.ts
Normal file
9
front/src/modules/views/graphql/mutations/createViews.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_VIEWS = gql`
|
||||
mutation CreateViews($data: [ViewCreateManyInput!]!) {
|
||||
createManyView(data: $data) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
10
front/src/modules/views/graphql/mutations/updateView.ts
Normal file
10
front/src/modules/views/graphql/mutations/updateView.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_VIEW = gql`
|
||||
mutation UpdateView($data: ViewUpdateInput!, $where: ViewWhereUniqueInput!) {
|
||||
updateOneView(data: $data, where: $where) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
10
front/src/modules/views/graphql/queries/getViews.ts
Normal file
10
front/src/modules/views/graphql/queries/getViews.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_VIEWS = gql`
|
||||
query GetViews($where: ViewWhereInput) {
|
||||
views: findManyView(where: $where) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -7,11 +7,13 @@ import {
|
||||
ViewFieldMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import {
|
||||
tableColumnsByIdState,
|
||||
tableColumnsState,
|
||||
} from '@/ui/table/states/tableColumnsState';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
SortOrder,
|
||||
useCreateViewFieldsMutation,
|
||||
@ -45,7 +47,10 @@ export const useTableViewFields = ({
|
||||
objectName: 'company' | 'person';
|
||||
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
}) => {
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const setColumns = useSetRecoilState(tableColumnsState);
|
||||
const columnsById = useRecoilValue(tableColumnsByIdState);
|
||||
|
||||
|
||||
109
front/src/modules/views/hooks/useTableViews.ts
Normal file
109
front/src/modules/views/hooks/useTableViews.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import {
|
||||
type TableView,
|
||||
tableViewsByIdState,
|
||||
tableViewsState,
|
||||
} from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import {
|
||||
useCreateViewsMutation,
|
||||
useGetViewsQuery,
|
||||
useUpdateViewMutation,
|
||||
ViewType,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { GET_VIEWS } from '../graphql/queries/getViews';
|
||||
|
||||
export const useTableViews = ({
|
||||
objectId,
|
||||
}: {
|
||||
objectId: 'company' | 'person';
|
||||
}) => {
|
||||
const [, setViews] = useRecoilScopedState(
|
||||
tableViewsState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const viewsById = useRecoilScopedValue(
|
||||
tableViewsByIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [createViewsMutation] = useCreateViewsMutation();
|
||||
const [updateViewMutation] = useUpdateViewMutation();
|
||||
|
||||
const createViews = useCallback(
|
||||
(views: TableView[]) => {
|
||||
if (!views.length) return;
|
||||
|
||||
return createViewsMutation({
|
||||
variables: {
|
||||
data: views.map((view) => ({
|
||||
...view,
|
||||
objectId,
|
||||
type: ViewType.Table,
|
||||
})),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
});
|
||||
},
|
||||
[createViewsMutation, objectId],
|
||||
);
|
||||
|
||||
const updateViewFields = useCallback(
|
||||
(views: TableView[]) => {
|
||||
if (!views.length) return;
|
||||
|
||||
return Promise.all(
|
||||
views.map((view) =>
|
||||
updateViewMutation({
|
||||
variables: {
|
||||
data: { name: view.name },
|
||||
where: { id: view.id },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[updateViewMutation],
|
||||
);
|
||||
|
||||
useGetViewsQuery({
|
||||
variables: {
|
||||
where: {
|
||||
objectId: { equals: objectId },
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setViews(
|
||||
data.views.map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
})),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const handleViewsChange = useCallback(
|
||||
async (nextViews: TableView[]) => {
|
||||
const viewsToCreate = nextViews.filter(
|
||||
(nextView) => !viewsById[nextView.id],
|
||||
);
|
||||
await createViews(viewsToCreate);
|
||||
|
||||
const viewsToUpdate = nextViews.filter(
|
||||
(nextView) =>
|
||||
viewsById[nextView.id] &&
|
||||
viewsById[nextView.id].name !== nextView.name,
|
||||
);
|
||||
await updateViewFields(viewsToUpdate);
|
||||
},
|
||||
[createViews, updateViewFields, viewsById],
|
||||
);
|
||||
|
||||
return { handleViewsChange };
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import { Context, useCallback } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import {
|
||||
sortsByKeyScopedState,
|
||||
@ -10,9 +9,10 @@ import type {
|
||||
SelectedSortType,
|
||||
SortType,
|
||||
} from '@/ui/filter-n-sort/types/interface';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import {
|
||||
useCreateViewSortsMutation,
|
||||
useDeleteViewSortsMutation,
|
||||
@ -25,14 +25,25 @@ import { GET_VIEW_SORTS } from '../graphql/queries/getViewSorts';
|
||||
|
||||
export const useViewSorts = <SortField>({
|
||||
availableSorts,
|
||||
Context,
|
||||
}: {
|
||||
availableSorts: SortType<SortField>[];
|
||||
Context?: Context<string | null>;
|
||||
}) => {
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const [, setSorts] = useRecoilScopedState(sortScopedState, Context);
|
||||
const sortsByKey = useRecoilScopedValue(sortsByKeyScopedState, Context);
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const [, setSorts] = useRecoilScopedState(
|
||||
sortScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const sortsByKey = useRecoilScopedValue(
|
||||
sortsByKeyScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentViewId) setSorts([]);
|
||||
}, [currentViewId, setSorts]);
|
||||
|
||||
useGetViewSortsQuery({
|
||||
skip: !currentViewId,
|
||||
@ -44,11 +55,19 @@ export const useViewSorts = <SortField>({
|
||||
onCompleted: (data) => {
|
||||
setSorts(
|
||||
data.viewSorts
|
||||
.map((viewSort) => ({
|
||||
...availableSorts.find((sort) => sort.key === viewSort.key),
|
||||
label: viewSort.name,
|
||||
order: viewSort.direction.toLowerCase(),
|
||||
}))
|
||||
.map((viewSort) => {
|
||||
const availableSort = availableSorts.find(
|
||||
(sort) => sort.key === viewSort.key,
|
||||
);
|
||||
|
||||
return availableSort
|
||||
? {
|
||||
...availableSort,
|
||||
label: viewSort.name,
|
||||
order: viewSort.direction.toLowerCase(),
|
||||
}
|
||||
: undefined;
|
||||
})
|
||||
.filter((sort): sort is SelectedSortType<SortField> => !!sort),
|
||||
);
|
||||
},
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const currentViewIdState = atom<string | undefined>({
|
||||
key: 'currentViewIdState',
|
||||
default: undefined,
|
||||
});
|
||||
Reference in New Issue
Block a user