Refactor/new menu item (#1448)

* wip

* finished

* Added disabled

* Fixed disabled

* Finished cleaning

* Minor fixes from merge

* Added docs

* Added PascalCase

* Fix from review

* Fixes from merge

* Fix lint

* Fixed storybook tests
This commit is contained in:
Lucas Bordeau
2023-09-06 16:41:26 +02:00
committed by GitHub
parent 5c7660f588
commit 28ca9a9e49
96 changed files with 816 additions and 918 deletions

View File

@ -185,7 +185,7 @@ export function ActivityEditor({
<>
<DateEditableField
value={activity.dueAt}
icon={<IconCalendar />}
Icon={IconCalendar}
label="Due date"
onSubmit={(newDate) => {
updateActivityMutation({

View File

@ -23,7 +23,7 @@ export function ActivityAssigneeEditableField({ activity }: OwnProps) {
scope: RelationPickerHotkeyScope.RelationPicker,
}}
label="Assignee"
iconLabel={<IconUserCircle />}
IconLabel={IconUserCircle}
editModeContent={
<ActivityAssigneeEditableFieldEditMode activity={activity} />
}

View File

@ -28,7 +28,7 @@ export function ActivityRelationEditableField({ activity }: OwnProps) {
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={<IconArrowUpRight />}
IconLabel={IconArrowUpRight}
editModeContent={
<ActivityRelationEditableFieldEditMode activity={activity} />
}

View File

@ -167,7 +167,7 @@ export function CompanyBoardCard() {
value={{
key: viewField.key,
name: viewField.name,
icon: viewField.icon,
Icon: viewField.Icon,
type: viewField.metadata.type,
metadata: viewField.metadata,
}}

View File

@ -1,11 +1,9 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilState } from 'recoil';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
@ -13,6 +11,7 @@ import { IconChevronDown } from '@/ui/icon';
import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase';
import { useEntitySelectSearch } from '@/ui/input/relation-picker/hooks/useEntitySelectSearch';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery';
@ -47,8 +46,6 @@ export function CompanyProgressPicker({
string | null
>(null);
const theme = useTheme();
const [currentPipeline] = useRecoilState(currentPipelineState);
const currentPipelineStages = useMemo(
@ -89,22 +86,21 @@ export function CompanyProgressPicker({
{isProgressSelectionUnfolded ? (
<StyledDropdownMenuItemsContainer>
{currentPipelineStages.map((pipelineStage, index) => (
<DropdownMenuItem
<MenuItem
key={pipelineStage.id}
data-testid={`select-pipeline-stage-${index}`}
testId={`select-pipeline-stage-${index}`}
onClick={() => {
handlePipelineStageChange(pipelineStage.id);
}}
>
{pipelineStage.name}
</DropdownMenuItem>
text={pipelineStage.name}
/>
))}
</StyledDropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
data-testid="selected-pipeline-stage"
endIcon={<IconChevronDown size={theme.icon.size.md} />}
EndIcon={IconChevronDown}
onClick={() => setIsProgressSelectionUnfolded(true)}
>
{selectedPipelineStage?.name}

View File

@ -29,7 +29,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'name',
name: 'Name',
icon: <IconBuildingSkyscraper />,
Icon: IconBuildingSkyscraper,
size: 180,
index: 0,
metadata: {
@ -43,7 +43,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'domainName',
name: 'URL',
icon: <IconLink />,
Icon: IconLink,
size: 100,
index: 1,
metadata: {
@ -56,7 +56,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'accountOwner',
name: 'Account Owner',
icon: <IconUserCircle />,
Icon: IconUserCircle,
size: 150,
index: 2,
metadata: {
@ -69,7 +69,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'createdAt',
name: 'Creation',
icon: <IconCalendarEvent />,
Icon: IconCalendarEvent,
size: 150,
index: 3,
metadata: {
@ -81,7 +81,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'employees',
name: 'Employees',
icon: <IconUsers />,
Icon: IconUsers,
size: 150,
index: 4,
metadata: {
@ -94,7 +94,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'linkedin',
name: 'LinkedIn',
icon: <IconBrandLinkedin />,
Icon: IconBrandLinkedin,
size: 170,
index: 5,
metadata: {
@ -107,7 +107,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'address',
name: 'Address',
icon: <IconMap />,
Icon: IconMap,
size: 170,
index: 6,
metadata: {
@ -120,7 +120,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'idealCustomerProfile',
name: 'ICP',
icon: <IconTarget />,
Icon: IconTarget,
size: 150,
index: 7,
metadata: {
@ -132,7 +132,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'annualRecurringRevenue',
name: 'ARR',
icon: <IconMoneybag />,
Icon: IconMoneybag,
size: 150,
index: 8,
metadata: {
@ -143,7 +143,7 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
{
key: 'xUrl',
name: 'Twitter',
icon: <IconBrandX />,
Icon: IconBrandX,
size: 150,
index: 9,
metadata: {

View File

@ -26,19 +26,19 @@ export function useCompanyTableContextMenuEntries() {
setContextMenuEntries([
<ContextMenuEntry
label="Note"
icon={<IconNotes size={16} />}
Icon={IconNotes}
onClick={() => handleButtonClick(ActivityType.Note)}
key="note"
/>,
<ContextMenuEntry
label="Task"
icon={<IconCheckbox size={16} />}
Icon={IconCheckbox}
onClick={() => handleButtonClick(ActivityType.Task)}
key="task"
/>,
<ContextMenuEntry
label="Delete"
icon={<IconTrash size={16} />}
Icon={IconTrash}
accent="danger"
onClick={() => deleteSelectedCompanies()}
key="delete"

View File

@ -5,6 +5,7 @@ import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardId
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor';
import { Pipeline } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -97,12 +98,22 @@ export function useUpdateCompanyBoard() {
});
const newBoardColumns: BoardColumnDefinition[] =
orderedPipelineStages?.map((pipelineStage) => ({
id: pipelineStage.id,
title: pipelineStage.name,
colorCode: pipelineStage.color,
index: pipelineStage.index ?? 0,
}));
orderedPipelineStages?.map((pipelineStage) => {
if (!isThemeColor(pipelineStage.color)) {
console.warn(
`Color ${pipelineStage.color} is not recognized in useUpdateCompanyBoard.`,
);
}
return {
id: pipelineStage.id,
title: pipelineStage.name,
colorCode: isThemeColor(pipelineStage.color)
? pipelineStage.color
: undefined,
index: pipelineStage.index ?? 0,
};
});
if (!isDeeplyEqual(currentBoardColumns, newBoardColumns)) {
set(boardColumnsState, newBoardColumns);

View File

@ -11,7 +11,7 @@ import {
export const fieldsForCompany = [
{
icon: <IconBuildingSkyscraper />,
icon: IconBuildingSkyscraper,
label: 'Name',
key: 'name',
alternateMatches: ['name', 'company name', 'company'],
@ -21,7 +21,7 @@ export const fieldsForCompany = [
example: 'Tim',
},
{
icon: <IconMail />,
icon: IconMail,
label: 'Domain name',
key: 'domainName',
alternateMatches: ['domain', 'domain name'],
@ -31,7 +31,7 @@ export const fieldsForCompany = [
example: 'apple.dev',
},
{
icon: <IconBrandLinkedin />,
icon: IconBrandLinkedin,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
@ -41,7 +41,7 @@ export const fieldsForCompany = [
example: 'https://www.linkedin.com/in/apple',
},
{
icon: <IconMoneybag />,
icon: IconMoneybag,
label: 'ARR',
key: 'annualRecurringRevenue',
alternateMatches: [
@ -64,7 +64,7 @@ export const fieldsForCompany = [
example: '1000000',
},
{
icon: <IconTarget />,
icon: IconTarget,
label: 'ICP',
key: 'idealCustomerProfile',
alternateMatches: [
@ -86,7 +86,7 @@ export const fieldsForCompany = [
example: 'true/false',
},
{
icon: <IconBrandX />,
icon: IconBrandX,
label: 'x URL',
key: 'xUrl',
alternateMatches: ['x', 'twitter', 'twitter url', 'x url'],
@ -96,7 +96,7 @@ export const fieldsForCompany = [
example: 'https://x.com/tim_cook',
},
{
icon: <IconMap />,
icon: IconMap,
label: 'Address',
key: 'address',
fieldType: {
@ -105,7 +105,7 @@ export const fieldsForCompany = [
example: 'Maple street',
},
{
icon: <IconUsers />,
icon: IconUsers,
label: 'Employees',
key: 'employees',
alternateMatches: ['employees', 'total employees', 'number of employees'],

View File

@ -1,15 +1,14 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconDotsVertical, IconLinkOff, IconTrash } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar';
import {
@ -72,10 +71,6 @@ const StyledJobTitle = styled.div`
}
`;
const StyledRemoveOption = styled.div`
color: ${({ theme }) => theme.color.red};
`;
export function PeopleCard({
person,
hasBottomBorder = true,
@ -93,8 +88,6 @@ export function PeopleCard({
placement: 'right-start',
});
const theme = useTheme();
useListenClickOutside({
refs: [refs.floating],
callback: () => {
@ -175,14 +168,17 @@ export function PeopleCard({
<StyledDropdownMenuItemsContainer
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuSelectableItem onClick={handleDetachPerson}>
<IconLinkOff size={14} />
Detach relation
</DropdownMenuSelectableItem>
<DropdownMenuSelectableItem onClick={handleDeletePerson}>
<IconTrash size={14} color={theme.font.color.danger} />
<StyledRemoveOption>Delete person</StyledRemoveOption>
</DropdownMenuSelectableItem>
<MenuItem
onClick={handleDetachPerson}
LeftIcon={IconLinkOff}
text="Detach relation"
/>
<MenuItem
onClick={handleDeletePerson}
LeftIcon={IconTrash}
text="Delete person"
accent="danger"
/>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
)}

View File

@ -27,7 +27,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'displayName',
name: 'People',
icon: <IconUser />,
Icon: IconUser,
size: 210,
index: 0,
metadata: {
@ -43,7 +43,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'email',
name: 'Email',
icon: <IconMail />,
Icon: IconMail,
size: 150,
index: 1,
metadata: {
@ -55,7 +55,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'company',
name: 'Company',
icon: <IconBuildingSkyscraper />,
Icon: IconBuildingSkyscraper,
size: 150,
index: 2,
metadata: {
@ -67,7 +67,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'phone',
name: 'Phone',
icon: <IconPhone />,
Icon: IconPhone,
size: 150,
index: 3,
metadata: {
@ -79,7 +79,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'createdAt',
name: 'Creation',
icon: <IconCalendarEvent />,
Icon: IconCalendarEvent,
size: 150,
index: 4,
metadata: {
@ -90,7 +90,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'city',
name: 'City',
icon: <IconMap />,
Icon: IconMap,
size: 150,
index: 5,
metadata: {
@ -102,7 +102,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'jobTitle',
name: 'Job title',
icon: <IconBriefcase />,
Icon: IconBriefcase,
size: 150,
index: 6,
metadata: {
@ -114,7 +114,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'linkedin',
name: 'LinkedIn',
icon: <IconBrandLinkedin />,
Icon: IconBrandLinkedin,
size: 150,
index: 7,
metadata: {
@ -126,7 +126,7 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
{
key: 'x',
name: 'Twitter',
icon: <IconBrandX />,
Icon: IconBrandX,
size: 150,
index: 8,
metadata: {

View File

@ -60,19 +60,19 @@ export function usePersonTableContextMenuEntries() {
setContextMenuEntries([
<ContextMenuEntry
label="Note"
icon={<IconNotes size={16} />}
Icon={IconNotes}
onClick={() => handleActivityClick(ActivityType.Note)}
key="note"
/>,
<ContextMenuEntry
label="Task"
icon={<IconCheckbox size={16} />}
Icon={IconCheckbox}
onClick={() => handleActivityClick(ActivityType.Task)}
key="task"
/>,
<ContextMenuEntry
label="Delete"
icon={<IconTrash size={16} />}
Icon={IconTrash}
accent="danger"
onClick={handleDeleteClick}
key="delete"

View File

@ -1,5 +1,6 @@
import { isValidPhoneNumber } from 'libphonenumber-js';
import { Fields } from '@/spreadsheet-import/types';
import {
IconBrandLinkedin,
IconBrandX,
@ -11,7 +12,7 @@ import {
export const fieldsForPerson = [
{
icon: <IconUser />,
icon: IconUser,
label: 'Firstname',
key: 'firstName',
alternateMatches: ['first name', 'first', 'firstname'],
@ -21,7 +22,7 @@ export const fieldsForPerson = [
example: 'Tim',
},
{
icon: <IconUser />,
icon: IconUser,
label: 'Lastname',
key: 'lastName',
alternateMatches: ['last name', 'last', 'lastname'],
@ -31,7 +32,7 @@ export const fieldsForPerson = [
example: 'Cook',
},
{
icon: <IconMail />,
icon: IconMail,
label: 'Email',
key: 'email',
alternateMatches: ['email', 'mail'],
@ -41,7 +42,7 @@ export const fieldsForPerson = [
example: 'tim@apple.dev',
},
{
icon: <IconBrandLinkedin />,
icon: IconBrandLinkedin,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
@ -51,7 +52,7 @@ export const fieldsForPerson = [
example: 'https://www.linkedin.com/in/timcook',
},
{
icon: <IconBrandX />,
icon: IconBrandX,
label: 'X URL',
key: 'xUrl',
alternateMatches: ['x', 'x url'],
@ -61,7 +62,7 @@ export const fieldsForPerson = [
example: 'https://x.com/tim_cook',
},
{
icon: <IconBriefcase />,
icon: IconBriefcase,
label: 'Job title',
key: 'jobTitle',
alternateMatches: ['job', 'job title'],
@ -71,7 +72,7 @@ export const fieldsForPerson = [
example: 'CEO',
},
{
icon: <IconBriefcase />,
icon: IconBriefcase,
label: 'Phone',
key: 'phone',
fieldType: {
@ -88,7 +89,7 @@ export const fieldsForPerson = [
],
},
{
icon: <IconMap />,
icon: IconMap,
label: 'City',
key: 'city',
fieldType: {
@ -96,4 +97,4 @@ export const fieldsForPerson = [
},
example: 'Seattle',
},
] as const;
] as Fields<string>;

View File

@ -19,7 +19,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
{
key: 'closeDate',
name: 'Close Date',
icon: <IconCalendarEvent />,
Icon: IconCalendarEvent,
metadata: {
type: 'date',
fieldName: 'closeDate',
@ -29,7 +29,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
{
key: 'amount',
name: 'Amount',
icon: <IconCurrencyDollar />,
Icon: IconCurrencyDollar,
metadata: {
type: 'number',
fieldName: 'amount',
@ -39,7 +39,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
{
key: 'probability',
name: 'Probability',
icon: <IconProgressCheck />,
Icon: IconProgressCheck,
metadata: {
type: 'probability',
fieldName: 'probability',
@ -49,7 +49,7 @@ export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMet
{
key: 'pointOfContact',
name: 'Point of Contact',
icon: <IconUser />,
Icon: IconUser,
metadata: {
type: 'relation',
fieldName: 'pointOfContact',

View File

@ -22,7 +22,7 @@ export const usePipelineStages = () => {
return createPipelineStageMutation({
variables: {
data: {
color: boardColumn.colorCode,
color: boardColumn.colorCode ?? 'gray',
id: boardColumn.id,
index: boardColumn.index,
name: boardColumn.title,

View File

@ -14,44 +14,15 @@ import { ReadonlyDeep } from 'type-fest';
import type { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown, TablerIconsProps } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemSelect } from '@/ui/menu-item/components/MenuItemSelect';
import { AppTooltip } from '@/ui/tooltip/AppTooltip';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
const StyledDropdownItem = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: border-box;
display: flex;
flex-direction: row;
height: 32px;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
width: 100%;
&:hover {
background-color: ${({ theme }) => theme.background.quaternary};
}
`;
const StyledDropdownLabel = styled.span<{ isPlaceholder: boolean }>`
color: ${({ theme, isPlaceholder }) =>
isPlaceholder ? theme.font.color.tertiary : theme.font.color.primary};
display: flex;
flex: 1;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledFloatingDropdown = styled.div`
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
@ -69,11 +40,9 @@ export const MatchColumnSelect = ({
value,
options: initialOptions,
placeholder,
name,
}: Props) => {
const theme = useTheme();
const dropdownItemRef = useRef<HTMLDivElement>(null);
const dropdownContainerRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
@ -123,16 +92,6 @@ export const MatchColumnSelect = ({
setIsOpen(false);
}
function renderIcon(icon: ReadonlyDeep<React.ReactNode>) {
if (icon && React.isValidElement(icon)) {
return React.cloneElement<TablerIconsProps>(icon as any, {
size: 16,
color: theme.font.color.primary,
});
}
return null;
}
useListenClickOutside({
refs: [dropdownContainerRef],
callback: () => {
@ -146,28 +105,20 @@ export const MatchColumnSelect = ({
return (
<>
<StyledDropdownItem
id={name}
ref={(node) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
dropdownItemRef.current = node;
refs.setReference(node);
}}
onClick={handleDropdownItemClick}
>
{renderIcon(value?.icon)}
<StyledDropdownLabel isPlaceholder={!value?.label}>
{value?.label ?? placeholder}
</StyledDropdownLabel>
<IconChevronDown size={16} color={theme.font.color.tertiary} />
</StyledDropdownItem>
<div ref={refs.setReference}>
<MenuItem
LeftIcon={value?.icon}
onClick={handleDropdownItemClick}
text={value?.label ?? placeholder ?? ''}
accent={value?.label ? 'default' : 'placeholder'}
/>
</div>
{isOpen &&
createPortal(
<StyledFloatingDropdown ref={refs.setFloating} style={floatingStyles}>
<StyledDropdownMenu
ref={dropdownContainerRef}
width={dropdownItemRef.current?.clientWidth}
width={refs.domReference.current?.clientWidth}
>
<DropdownMenuInput
value={searchFilter}
@ -178,18 +129,16 @@ export const MatchColumnSelect = ({
<StyledDropdownMenuItemsContainer hasMaxHeight>
{options?.map((option) => (
<>
<DropdownMenuSelectableItem
id={option.value}
<MenuItemSelect
key={option.label}
selected={value?.label === option.label}
onClick={() => handleChange(option)}
disabled={
option.disabled && value?.value !== option.value
}
>
{renderIcon(option?.icon)}
{option.label}
</DropdownMenuSelectableItem>
LeftIcon={option?.icon}
text={option.label}
/>
{option.disabled &&
value?.value !== option.value &&
createPortal(
@ -204,9 +153,7 @@ export const MatchColumnSelect = ({
)}
</>
))}
{options?.length === 0 && (
<DropdownMenuItem>No result</DropdownMenuItem>
)}
{options?.length === 0 && <MenuItem text="No result" />}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
</StyledFloatingDropdown>,

View File

@ -109,7 +109,7 @@ export const TemplateColumn = <T extends string>({
});
const selectOptions = [
{
icon: <IconForbid />,
icon: IconForbid,
value: 'do-not-import',
label: 'Do not import',
},

View File

@ -134,7 +134,7 @@ export const generateColumns = <T extends string>(
value={
value
? ({
icon: null,
icon: undefined,
...value,
} as const)
: value

View File

@ -1,5 +1,5 @@
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
import type { Fields, SpreadsheetOptions } from '@/spreadsheet-import/types';
const fields = [
{
@ -85,7 +85,7 @@ const fields = [
},
example: 'true',
},
] as const;
] as Fields<string>;
const mockComponentBehaviourForTypes = <T extends string>(
props: SpreadsheetOptions<T>,

View File

@ -3,6 +3,7 @@ import { ReadonlyDeep } from 'type-fest';
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { StepState } from '@/spreadsheet-import/steps/components/UploadFlow';
import { Meta } from '@/spreadsheet-import/steps/components/ValidationStep/types';
import { IconComponent } from '@/ui/icon/types/IconComponent';
export type SpreadsheetOptions<Keys extends string> = {
// Is modal visible.
@ -65,7 +66,7 @@ export type Fields<T extends string> = ReadonlyDeep<Field<T>[]>;
export type Field<T extends string> = {
// Icon
icon: React.ReactNode;
icon: IconComponent | null | undefined;
// UI-facing field label
label: string;
// Field's unique identifier
@ -96,7 +97,7 @@ export type Select = {
export type SelectOption = {
// Icon
icon?: React.ReactNode;
icon?: IconComponent | null;
// UI-facing option label
label: string;
// Field entry matching criteria as well as select output

View File

@ -2,6 +2,7 @@ import React from 'react';
import styled from '@emotion/styled';
import { Tag } from '@/ui/tag/components/Tag';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
@ -52,7 +53,7 @@ const StyledNumChildren = styled.div`
`;
export type BoardColumnProps = {
color: string;
color?: ThemeColor;
title: string;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void;
@ -97,7 +98,7 @@ export function BoardColumn({
return (
<StyledColumn isFirstColumn={isFirstColumn}>
<StyledHeader>
<Tag onClick={handleTitleClick} color={color} text={title} />
<Tag onClick={handleTitleClick} color={color ?? 'gray'} text={title} />
{!!totalAmount && <StyledAmount>${totalAmount}</StyledAmount>}
<StyledNumChildren>{numChildren}</StyledNumChildren>
</StyledHeader>
@ -107,7 +108,7 @@ export function BoardColumn({
onDelete={onDelete}
onTitleEdit={onTitleEdit}
title={title}
color={color}
color={color ?? 'gray'}
stageId={stageId}
/>
)}

View File

@ -1,9 +1,10 @@
import { ChangeEvent, useState } from 'react';
import styled from '@emotion/styled';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { MenuItemSelectColor } from '@/ui/menu-item/components/MenuItemSelectColor';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { textInputStyle } from '@/ui/theme/constants/effects';
import { debounce } from '~/utils/debounce';
@ -32,21 +33,15 @@ export type BoardColumnEditTitleMenuProps = {
onClose: () => void;
title: string;
onTitleEdit: (title: string, color: string) => void;
color: string;
color: ThemeColor;
};
const StyledColorSample = styled.div<{ colorName: string }>`
background-color: ${({ theme, colorName }) =>
theme.tag.background[colorName]};
border: 1px solid
${({ theme, colorName }) =>
theme.color[colorName as keyof typeof theme.color]};
border-radius: ${({ theme }) => theme.border.radius.sm};
height: 12px;
width: 12px;
`;
type ColumnColorOption = {
name: string;
id: ThemeColor;
};
export const COLOR_OPTIONS = [
export const COLUMN_COLOR_OPTIONS: ColumnColorOption[] = [
{ name: 'Green', id: 'green' },
{ name: 'Turquoise', id: 'turquoise' },
{ name: 'Sky', id: 'sky' },
@ -85,18 +80,17 @@ export function BoardColumnEditTitleMenu({
/>
</StyledEditTitleContainer>
<StyledDropdownMenuSeparator />
{COLOR_OPTIONS.map((colorOption) => (
<DropdownMenuSelectableItem
{COLUMN_COLOR_OPTIONS.map((colorOption) => (
<MenuItemSelectColor
key={colorOption.name}
onClick={() => {
onTitleEdit(title, colorOption.id);
onClose();
}}
color={colorOption.id}
selected={colorOption.id === color}
>
<StyledColorSample colorName={colorOption.id} />
{colorOption.name}
</DropdownMenuSelectableItem>
text={colorOption.name}
/>
))}
</StyledDropdownMenuItemsContainer>
);

View File

@ -5,7 +5,6 @@ import { Key } from 'ts-key-enum';
import { useCreateCompanyProgress } from '@/companies/hooks/useCreateCompanyProgress';
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconPencil, IconPlus, IconTrash } from '@/ui/icon';
@ -13,8 +12,9 @@ import { SingleEntitySelect } from '@/ui/input/relation-picker/components/Single
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
import { icon } from '@/ui/theme/constants/icon';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -32,7 +32,7 @@ const StyledMenuContainer = styled.div`
`;
type OwnProps = {
color: string;
color: ThemeColor;
onClose: () => void;
onDelete?: (id: string) => void;
onTitleEdit: (title: string, color: string) => void;
@ -52,7 +52,7 @@ export function BoardColumnMenu({
}: OwnProps) {
const [currentMenu, setCurrentMenu] = useState('actions');
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
const [, setBoardColumns] = useRecoilState(boardColumnsState);
const boardColumnMenuRef = useRef(null);
@ -130,21 +130,21 @@ export function BoardColumnMenu({
<StyledDropdownMenu>
{currentMenu === 'actions' && (
<StyledDropdownMenuItemsContainer>
<DropdownMenuSelectableItem onClick={() => setMenu('title')}>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
Rename
</DropdownMenuSelectableItem>
<DropdownMenuSelectableItem
disabled={boardColumns.length <= 1}
<MenuItem
onClick={() => setMenu('title')}
LeftIcon={IconPencil}
text="Rename"
/>
<MenuItem
onClick={handleDelete}
>
<IconTrash size={icon.size.md} stroke={icon.stroke.sm} />
Delete
</DropdownMenuSelectableItem>
<DropdownMenuSelectableItem onClick={() => setMenu('add')}>
<IconPlus size={icon.size.md} stroke={icon.stroke.sm} />
New opportunity
</DropdownMenuSelectableItem>
LeftIcon={IconTrash}
text="Delete"
/>
<MenuItem
onClick={() => setMenu('add')}
LeftIcon={IconPlus}
text="New opportunity"
/>
</StyledDropdownMenuItemsContainer>
)}
{currentMenu === 'title' && (

View File

@ -7,18 +7,19 @@ import { v4 } from 'uuid';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import {
IconChevronLeft,
IconChevronRight,
IconLayoutKanban,
IconPlus,
IconSettings,
} from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemNavigate } from '@/ui/menu-item/components/MenuItemNavigate';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@ -35,16 +36,18 @@ const StyledIconSettings = styled(IconSettings)`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
margin-left: auto;
`;
enum BoardOptionsMenu {
StageCreation = 'StageCreation',
Stages = 'Stages',
}
type ColumnForCreate = {
id: string;
colorCode: ThemeColor;
index: number;
title: string;
};
export function BoardOptionsDropdownContent({
customHotkeyScope,
onStageAdd,
@ -68,7 +71,7 @@ export function BoardOptionsDropdownContent({
)
return;
const columnToCreate = {
const columnToCreate: ColumnForCreate = {
id: v4(),
colorCode: 'gray',
index: boardColumns.length,
@ -113,32 +116,26 @@ export function BoardOptionsDropdownContent({
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem
<MenuItemNavigate
onClick={() => setCurrentMenu(BoardOptionsMenu.Stages)}
>
<IconLayoutKanban size={theme.icon.size.md} />
Stages
<StyledIconChevronRight size={theme.icon.size.sm} />
</DropdownMenuItem>
LeftIcon={IconLayoutKanban}
text="Stages"
/>
</StyledDropdownMenuItemsContainer>
</>
)}
{currentMenu === BoardOptionsMenu.Stages && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={resetMenu}
>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
Stages
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem
<MenuItem
onClick={() => setCurrentMenu(BoardOptionsMenu.StageCreation)}
>
<IconPlus size={theme.icon.size.md} />
Add stage
</DropdownMenuItem>
LeftIcon={IconPlus}
text="Add stage"
/>
</StyledDropdownMenuItemsContainer>
</>
)}

View File

@ -4,7 +4,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import {
BoardColumnEditTitleMenu,
COLOR_OPTIONS,
COLUMN_COLOR_OPTIONS,
} from '../BoardColumnEditTitleMenu';
const meta: Meta<typeof BoardColumnEditTitleMenu> = {
@ -14,7 +14,7 @@ const meta: Meta<typeof BoardColumnEditTitleMenu> = {
argTypes: {
color: {
control: 'select',
options: COLOR_OPTIONS.map(({ id }) => id),
options: COLUMN_COLOR_OPTIONS.map(({ id }) => id),
},
},
args: { color: 'green', title: 'Column title' },

View File

@ -16,7 +16,7 @@ export function useBoardContextMenuEntries() {
setContextMenuEntries([
<ContextMenuEntry
label="Delete"
icon={<IconTrash size={16} />}
Icon={IconTrash}
accent="danger"
onClick={() => deleteSelectedBoardCards()}
key="delete"

View File

@ -1,6 +1,8 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
export type BoardColumnDefinition = {
id: string;
title: string;
index: number;
colorCode: string;
colorCode?: ThemeColor;
};

View File

@ -1,32 +1,22 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
type ContextMenuEntryAccent = 'regular' | 'danger';
type ContextMenuEntryAccent = 'default' | 'danger';
type OwnProps = {
icon: ReactNode;
Icon: IconComponent;
label: string;
accent?: ContextMenuEntryAccent;
onClick: () => void;
};
const StyledButtonLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(2)};
`;
export function ContextMenuEntry({
label,
icon,
accent = 'regular',
Icon,
accent = 'default',
onClick,
}: OwnProps) {
return (
<DropdownMenuItem onClick={onClick} accent={accent}>
{icon}
<StyledButtonLabel>{label}</StyledButtonLabel>
</DropdownMenuItem>
<MenuItem LeftIcon={Icon} onClick={onClick} accent={accent} text={label} />
);
}

View File

@ -1,52 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { Checkbox } from '@/ui/input/checkbox/components/Checkbox';
import { DropdownMenuItem } from './DropdownMenuItem';
type Props = {
checked: boolean;
onChange?: (newCheckedValue: boolean) => void;
id?: string;
};
const StyledDropdownMenuCheckableItemContainer = styled(DropdownMenuItem)`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledLeftContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledChildrenContainer = styled.div`
align-items: center;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
`;
export function DropdownMenuCheckableItem({
checked,
onChange,
children,
}: React.PropsWithChildren<Props>) {
function handleClick() {
onChange?.(!checked);
}
return (
<StyledDropdownMenuCheckableItemContainer onClick={handleClick}>
<StyledLeftContainer>
<Checkbox checked={checked} />
<StyledChildrenContainer>{children}</StyledChildrenContainer>
</StyledLeftContainer>
</StyledDropdownMenuCheckableItemContainer>
);
}

View File

@ -1,6 +1,9 @@
import { ComponentProps, ReactElement } from 'react';
import { ComponentProps } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
const StyledHeader = styled.li`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
@ -40,23 +43,31 @@ const StyledEndIconWrapper = styled(StyledStartIconWrapper)`
`;
type DropdownMenuHeaderProps = ComponentProps<'li'> & {
startIcon?: ReactElement;
endIcon?: ReactElement;
StartIcon?: IconComponent;
EndIcon?: IconComponent;
};
export function DropdownMenuHeader({
children,
startIcon,
endIcon,
StartIcon,
EndIcon,
...props
}: DropdownMenuHeaderProps) {
const theme = useTheme();
return (
<StyledHeader {...props}>
{startIcon && (
<StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>
{StartIcon && (
<StyledStartIconWrapper>
<StartIcon size={theme.icon.size.md} />
</StyledStartIconWrapper>
)}
{children}
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>}
{EndIcon && (
<StyledEndIconWrapper>
<EndIcon size={theme.icon.size.md} />
</StyledEndIconWrapper>
)}
</StyledHeader>
);
}

View File

@ -1,79 +0,0 @@
import { ComponentProps } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
import { hoverBackground } from '@/ui/theme/constants/effects';
export type DropdownMenuItemAccent = 'regular' | 'danger';
const StyledItem = styled.li<{ accent: DropdownMenuItemAccent }>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme, accent }) =>
accent === 'danger' ? theme.color.red : theme.font.color.secondary};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) var(--horizontal-padding);
${hoverBackground};
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
&:hover .actions-hover-container {
display: flex;
}
`;
const StyledActions = styled(motion.div)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
export type DropdownMenuItemProps = ComponentProps<'li'> & {
actions?: React.ReactNode[];
accent?: DropdownMenuItemAccent;
};
export function DropdownMenuItem({
actions,
children,
accent = 'regular',
...props
}: DropdownMenuItemProps) {
return (
<StyledItem {...props} accent={accent}>
{children}
{actions && (
<StyledActions
className="actions-hover-container"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<FloatingIconButtonGroup size="small">
{actions}
</FloatingIconButtonGroup>
</StyledActions>
)}
</StyledItem>
);
}

View File

@ -1,87 +0,0 @@
import React, { useEffect } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/icon/index';
import { hoverBackground } from '@/ui/theme/constants/effects';
import { DropdownMenuItem } from './DropdownMenuItem';
type Props = React.ComponentProps<'li'> & {
selected?: boolean;
hovered?: boolean;
disabled?: boolean;
};
const StyledDropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<
Pick<Props, 'hovered'>
>`
${hoverBackground};
align-items: center;
background: ${(props) =>
props.hovered ? props.theme.background.transparent.light : 'transparent'};
display: flex;
justify-content: space-between;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
`;
const StyledLeftContainer = styled.div<Pick<Props, 'disabled'>>`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
overflow: hidden;
`;
const StyledRightIcon = styled.div`
display: flex;
`;
export function DropdownMenuSelectableItem({
selected,
onClick,
children,
hovered,
disabled,
...restProps
}: React.PropsWithChildren<Props>) {
const theme = useTheme();
function handleClick(event: React.MouseEvent<HTMLLIElement>) {
if (disabled) {
return;
}
onClick?.(event);
}
useEffect(() => {
if (hovered) {
window.scrollTo({
behavior: 'smooth',
});
}
}, [hovered]);
return (
<StyledDropdownMenuSelectableItemContainer
{...restProps}
onClick={handleClick}
hovered={hovered}
data-testid="dropdown-menu-item"
>
<StyledLeftContainer disabled={disabled}>{children}</StyledLeftContainer>
<StyledRightIcon>
{selected && <IconCheck size={theme.icon.size.md} />}
</StyledRightIcon>
</StyledDropdownMenuSelectableItemContainer>
);
}

View File

@ -15,5 +15,6 @@ export const StyledDropdownMenuItemsContainer = styled.div<{
overflow-y: auto;
padding: var(--padding);
padding-right: var(--padding);
width: calc(100% - 2 * var(--padding));
`;

View File

@ -2,17 +2,16 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPlus, IconUser } from '@/ui/icon';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/menu-item/components/MenuItemMultiSelectAvatar';
import { MenuItemSelectAvatar } from '@/ui/menu-item/components/MenuItemSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput';
import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '../StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '../StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator';
@ -101,21 +100,22 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
return (
<>
{mockSelectArray.map((item) => (
<DropdownMenuSelectableItem
<MenuItemSelectAvatar
key={item.id}
selected={selectedItem === item.id}
onClick={() => setSelectedItem(item.id)}
>
{hasAvatar && (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
)}
{item.name}
</DropdownMenuSelectableItem>
avatar={
hasAvatar ? (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
) : undefined
}
text={item.name}
/>
))}
</>
);
@ -127,28 +127,28 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
return (
<>
{mockSelectArray.map((item) => (
<DropdownMenuCheckableItem
<MenuItemMultiSelectAvatar
key={item.id}
id={item.id}
checked={selectedItems.includes(item.id)}
onChange={(checked) => {
selected={selectedItems.includes(item.id)}
onSelectChange={(checked) => {
if (checked) {
setSelectedItems([...selectedItems, item.id]);
} else {
setSelectedItems(selectedItems.filter((id) => id !== item.id));
}
}}
>
{hasAvatar && (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
)}
{item.name}
</DropdownMenuCheckableItem>
avatar={
hasAvatar ? (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
) : undefined
}
text={item.name}
/>
))}
</>
);
@ -182,7 +182,7 @@ export const SimpleMenuItem: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -198,14 +198,14 @@ export const WithHeaders: Story = {
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(0, 3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -218,10 +218,7 @@ export const WithIcons: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>
<IconUser size={16} />
{name}
</DropdownMenuItem>
<MenuItem text={name} LeftIcon={IconUser} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -234,15 +231,11 @@ export const WithActions: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }, index) => (
<DropdownMenuItem
<MenuItem
className={index === 0 ? 'hover' : undefined}
actions={[
<IconButton icon={<IconUser />} />,
<IconButton icon={<IconPlus />} />,
]}
>
{name}
</DropdownMenuItem>
iconButtons={[{ Icon: IconUser }, { Icon: IconPlus }]}
text={name}
/>
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -273,7 +266,7 @@ export const Search: Story = {
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>

View File

@ -2,6 +2,7 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useEditableField } from '../hooks/useEditableField';
@ -71,7 +72,7 @@ const StyledEditableFieldBaseContainer = styled.div`
`;
type OwnProps = {
iconLabel?: React.ReactNode;
IconLabel?: IconComponent;
label?: string;
labelFixedWidth?: number;
useEditButton?: boolean;
@ -87,7 +88,7 @@ type OwnProps = {
};
export function EditableField({
iconLabel,
IconLabel,
label,
labelFixedWidth,
useEditButton,
@ -125,7 +126,11 @@ export function EditableField({
onMouseLeave={handleContainerMouseLeave}
>
<StyledLabelAndIconContainer>
{iconLabel && <StyledIconContainer>{iconLabel}</StyledIconContainer>}
{IconLabel && (
<StyledIconContainer>
<IconLabel />
</StyledIconContainer>
)}
{label && (
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)}

View File

@ -18,7 +18,7 @@ export function GenericEditableBooleanField() {
return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
displayModeContent={<GenericEditableBooleanFieldDisplayMode />}
displayModeContentOnly
/>

View File

@ -32,7 +32,7 @@ export function GenericEditableDateField() {
return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableDateFieldEditMode />}
displayModeContent={<GenericEditableDateFieldDisplayMode />}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -31,7 +31,7 @@ export function GenericEditableNumberField() {
return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableNumberFieldEditMode />}
displayModeContent={fieldValue}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -33,7 +33,7 @@ export function GenericEditablePhoneField() {
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
useEditButton
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditablePhoneFieldEditMode />}
displayModeContent={<PhoneInputDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -38,7 +38,7 @@ export function GenericEditableRelationField() {
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableRelationFieldEditMode />}
displayModeContent={<GenericEditableRelationFieldDisplayMode />}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -31,7 +31,7 @@ export function GenericEditableTextField() {
return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableTextFieldEditMode />}
displayModeContent={fieldValue}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -33,7 +33,7 @@ export function GenericEditableURLField() {
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
useEditButton
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableURLFieldEditMode />}
displayModeContent={<FieldDisplayURL URL={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}

View File

@ -18,7 +18,7 @@ export function ProbabilityEditableField() {
return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
IconLabel={currentEditableFieldDefinition.Icon}
displayModeContent={<ProbabilityEditableFieldEditMode />}
displayModeContentOnly
disableHoverEffect

View File

@ -1,9 +1,11 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { FieldMetadata, FieldType } from './FieldMetadata';
export type FieldDefinition<T extends FieldMetadata | unknown> = {
key: string;
name: string;
icon?: JSX.Element;
Icon?: IconComponent;
type: FieldType;
metadata: T;
};

View File

@ -1,3 +1,4 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
@ -118,7 +119,7 @@ export type ViewFieldMetadata = { type: ViewFieldType } & (
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
key: string;
name: string;
icon?: JSX.Element;
Icon?: IconComponent;
isVisible?: boolean;
metadata: T;
};

View File

@ -1,5 +1,6 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { parseDate } from '~/utils/date-utils';
@ -7,13 +8,13 @@ import { parseDate } from '~/utils/date-utils';
import { EditableFieldEditModeDate } from './EditableFieldEditModeDate';
type OwnProps = {
icon?: React.ReactNode;
Icon?: IconComponent;
label?: string;
value: string | null | undefined;
onSubmit?: (newValue: string) => void;
};
export function DateEditableField({ icon, value, label, onSubmit }: OwnProps) {
export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) {
async function handleChange(newValue: string) {
onSubmit?.(newValue);
}
@ -25,7 +26,7 @@ export function DateEditableField({ icon, value, label, onSubmit }: OwnProps) {
<EditableField
// onSubmit={handleSubmit}
// onCancel={handleCancel}
iconLabel={icon}
IconLabel={Icon}
label={label}
editModeContent={
<EditableFieldEditModeDate

View File

@ -2,19 +2,20 @@ import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { PhoneInputDisplay } from '@/ui/input/phone/components/PhoneInputDisplay';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
type OwnProps = {
icon?: React.ReactNode;
Icon?: IconComponent;
placeholder?: string;
value: string | null | undefined;
onSubmit?: (newValue: string) => void;
};
export function PhoneEditableField({
icon,
Icon,
placeholder,
value,
onSubmit,
@ -44,7 +45,7 @@ export function PhoneEditableField({
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
IconLabel={Icon}
editModeContent={
<TextInputEdit
placeholder={placeholder ?? ''}

View File

@ -10,10 +10,10 @@ const meta: Meta<typeof DateEditableField> = {
component: DateEditableField,
decorators: [ComponentDecorator],
argTypes: {
icon: {
Icon: {
type: 'boolean',
mapping: {
true: <IconCalendar />,
true: IconCalendar,
false: undefined,
},
},
@ -21,7 +21,7 @@ const meta: Meta<typeof DateEditableField> = {
},
args: {
value: new Date().toISOString(),
icon: true,
Icon: IconCalendar,
},
};

View File

@ -10,17 +10,17 @@ const meta: Meta<typeof PhoneEditableField> = {
component: PhoneEditableField,
decorators: [ComponentWithRouterDecorator],
argTypes: {
icon: {
Icon: {
type: 'boolean',
mapping: {
true: <IconPhone />,
true: IconPhone,
false: undefined,
},
},
},
args: {
value: '+33714446494',
icon: true,
Icon: IconPhone,
placeholder: 'Phone',
},
};

View File

@ -1,8 +1,8 @@
import { Context } from 'react';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
@ -43,8 +43,9 @@ export function FilterDropdownFilterSelect({
return (
<StyledDropdownMenuItemsContainer>
{availableFilters.map((availableFilter, index) => (
<DropdownMenuSelectableItem
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
setFilterDefinitionUsedInDropdown(availableFilter);
@ -58,10 +59,9 @@ export function FilterDropdownFilterSelect({
setFilterDropdownSearchInput('');
}}
>
{availableFilter.icon}
{availableFilter.label}
</DropdownMenuSelectableItem>
LeftIcon={availableFilter.Icon}
text={availableFilter.label}
/>
))}
</StyledDropdownMenuItemsContainer>
);

View File

@ -1,5 +1,4 @@
import { Context } from 'react';
import { useTheme } from '@emotion/react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { IconChevronDown } from '@/ui/icon';
@ -14,8 +13,6 @@ export function FilterDropdownOperandButton({
}: {
context: Context<string | null>;
}) {
const theme = useTheme();
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
@ -36,7 +33,7 @@ export function FilterDropdownOperandButton({
return (
<DropdownMenuHeader
key={'selected-filter-operand'}
endIcon={<IconChevronDown size={theme.icon.size.md} />}
EndIcon={IconChevronDown}
onClick={() => setIsFilterDropdownOperandSelectUnfolded(true)}
>
{getOperandLabel(selectedOperandInDropdown)}

View File

@ -1,7 +1,7 @@
import { Context } from 'react';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
@ -66,14 +66,13 @@ export function FilterDropdownOperandSelect({
return (
<StyledDropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => (
<DropdownMenuItem
<MenuItem
key={`select-filter-operand-${index}`}
onClick={() => {
handleOperangeChange(filterOperand);
}}
>
{getOperandLabel(filterOperand)}
</DropdownMenuItem>
text={getOperandLabel(filterOperand)}
/>
))}
</StyledDropdownMenuItemsContainer>
);

View File

@ -158,14 +158,12 @@ function SortAndFilterBar<SortField>({
return (
<SortOrFilterChip
key={sort.key}
testId={sort.key}
labelValue={sort.label}
id={sort.key}
icon={
sort.order === 'desc' ? (
<IconArrowNarrowDown size={theme.icon.size.md} />
) : (
<IconArrowNarrowUp size={theme.icon.size.md} />
)
Icon={
sort.order === 'desc'
? IconArrowNarrowDown
: IconArrowNarrowUp
}
isSort
onRemove={() => onRemoveSort(sort.key)}
@ -181,12 +179,12 @@ function SortAndFilterBar<SortField>({
return (
<SortOrFilterChip
key={filter.key}
testId={filter.key}
labelKey={filter.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
id={filter.key}
icon={filter.icon}
Icon={filter.Icon}
onRemove={() => {
removeFilter(filter.key);
}}

View File

@ -1,12 +1,10 @@
import { Context, useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '../types/interface';
@ -30,8 +28,6 @@ export function SortDropdownButton<SortField>({
onSortSelect,
HotkeyScope,
}: OwnProps<SortField>) {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
@ -74,21 +70,20 @@ export function SortDropdownButton<SortField>({
{isOptionUnfolded ? (
<StyledDropdownMenuItemsContainer>
{options.map((option, index) => (
<DropdownMenuSelectableItem
<MenuItem
key={index}
onClick={() => {
setSelectedSortDirection(option);
setIsOptionUnfolded(false);
}}
>
{option === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuSelectableItem>
text={option === 'asc' ? 'Ascending' : 'Descending'}
/>
))}
</StyledDropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
endIcon={<IconChevronDown size={theme.icon.size.md} />}
EndIcon={IconChevronDown}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
@ -97,13 +92,13 @@ export function SortDropdownButton<SortField>({
<StyledDropdownMenuItemsContainer>
{availableSorts.map((sort, index) => (
<DropdownMenuSelectableItem
<MenuItem
testId={`select-sort-${index}`}
key={index}
onClick={() => handleAddSort(sort)}
>
{sort.icon}
<OverflowingTextWithTooltip text={sort.label} />
</DropdownMenuSelectableItem>
LeftIcon={sort.Icon}
text={sort.label}
/>
))}
</StyledDropdownMenuItemsContainer>
</>

View File

@ -1,16 +1,16 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconX } from '@/ui/icon/index';
import { IconComponent } from '@/ui/icon/types/IconComponent';
type OwnProps = {
id: string;
labelKey?: string;
labelValue: string;
icon: ReactNode;
Icon?: IconComponent;
onRemove: () => void;
isSort?: boolean;
testId?: string;
};
type StyledChipProps = {
@ -55,20 +55,24 @@ const StyledLabelKey = styled.div`
`;
function SortOrFilterChip({
id,
labelKey,
labelValue,
icon,
Icon,
onRemove,
isSort,
testId,
}: OwnProps) {
const theme = useTheme();
return (
<StyledChip isSort={isSort}>
<StyledIcon>{icon}</StyledIcon>
{Icon && (
<StyledIcon>
<Icon />
</StyledIcon>
)}
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{labelValue}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + id}>
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + testId}>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
</StyledChip>

View File

@ -1,9 +1,11 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { FilterType } from './FilterType';
export type FilterDefinition = {
key: string;
label: string;
icon: JSX.Element;
Icon: IconComponent;
type: FilterType;
entitySelectComponent?: JSX.Element;
};

View File

@ -1,11 +1,10 @@
import { ReactNode } from 'react';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { SortOrder as Order_By } from '~/generated/graphql';
export type SortType<OrderByTemplate> = {
label: string;
key: string;
icon?: ReactNode;
Icon?: IconComponent;
orderByTemplate?: (order: Order_By) => OrderByTemplate[];
};

View File

@ -1,3 +1,3 @@
import { ComponentType } from 'react';
import { FunctionComponent } from 'react';
export type IconComponent = ComponentType<{ size: number }>;
export type IconComponent = FunctionComponent<{ size?: number }>;

View File

@ -1,12 +1,12 @@
import { useRef } from 'react';
import debounce from 'lodash.debounce';
import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/menu-item/components/MenuItemMultiSelectAvatar';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar';
import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -81,26 +81,25 @@ export function MultipleEntitySelect<
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer hasMaxHeight>
{entitiesInDropdown?.map((entity) => (
<DropdownMenuCheckableItem
<MenuItemMultiSelectAvatar
key={entity.id}
checked={value[entity.id]}
onChange={(newCheckedValue) =>
selected={value[entity.id]}
onSelectChange={(newCheckedValue) =>
onChange({ ...value, [entity.id]: newCheckedValue })
}
>
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
{entity.name}
</DropdownMenuCheckableItem>
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
text={entity.name}
/>
))}
{entitiesInDropdown?.length === 0 && (
<DropdownMenuItem>No result</DropdownMenuItem>
)}
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
);

View File

@ -1,12 +1,11 @@
import { useRef } from 'react';
import { useTheme } from '@emotion/react';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
@ -39,8 +38,6 @@ export function SingleEntitySelect<
}) {
const containerRef = useRef<HTMLDivElement>(null);
const theme = useTheme();
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
@ -76,10 +73,7 @@ export function SingleEntitySelect<
{showCreateButton && (
<>
<StyledDropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuItem onClick={onCreate}>
<IconPlus size={theme.icon.size.md} />
Add New
</DropdownMenuItem>
<MenuItem onClick={onCreate} LeftIcon={IconPlus} text="Add New" />
</StyledDropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
</>

View File

@ -1,12 +1,10 @@
import { useRef } from 'react';
import { useTheme } from '@emotion/react';
import { Key } from 'ts-key-enum';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconBuildingSkyscraper, IconUserCircle } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemSelectAvatar } from '@/ui/menu-item/components/MenuItemSelectAvatar';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Avatar } from '@/users/components/Avatar';
import { isDefined } from '~/utils/isDefined';
@ -76,43 +74,44 @@ export function SingleEntitySelectBase<
entitiesInDropdown = entitiesInDropdown.filter((entity) =>
isNonEmptyString(entity.name.trim()),
);
const theme = useTheme();
const NoUserIcon =
noUser?.entityType === Entity.User
? IconUserCircle
: IconBuildingSkyscraper;
return (
<StyledDropdownMenuItemsContainer ref={containerRef} hasMaxHeight>
{noUser && (
<DropdownMenuItem onClick={() => onEntitySelected(noUser)}>
{noUser.entityType === Entity.User ? (
<IconUserCircle size={theme.icon.size.md} />
) : (
<IconBuildingSkyscraper
size={theme.icon.size.md}
></IconBuildingSkyscraper>
)}
{noUser.name}
</DropdownMenuItem>
<MenuItem
onClick={() => onEntitySelected(noUser)}
LeftIcon={NoUserIcon}
text={noUser.name}
/>
)}
{entities.loading ? (
<DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 ? (
<DropdownMenuItem>No result</DropdownMenuItem>
<MenuItem text="No result" />
) : (
entitiesInDropdown?.map((entity, index) => (
<DropdownMenuSelectableItem
entitiesInDropdown?.map((entity) => (
<MenuItemSelectAvatar
key={entity.id}
testId="menu-item"
selected={entities.selectedEntity?.id === entity.id}
hovered={hoveredIndex === index}
onClick={() => onEntitySelected(entity)}
>
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
<OverflowingTextWithTooltip text={entity.name} />
</DropdownMenuSelectableItem>
text={entity.name}
hovered={hoveredIndex === entitiesInDropdown.indexOf(entity)}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
/>
))
)}
</StyledDropdownMenuItemsContainer>

View File

@ -4,12 +4,12 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/icon/index';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { ActivityType } from '~/generated/graphql';
const StyledContainer = styled.div`
@ -50,20 +50,18 @@ export function ShowPageAddButton({
<StyledDropdownMenuItemsContainer
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem
<MenuItem
onClick={() => handleSelect(ActivityType.Note)}
accent="regular"
>
<IconNotes size={16} />
Note
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleSelect(ActivityType.Task)}
accent="regular"
>
<IconCheckbox size={16} />
Task
</DropdownMenuItem>
accent="default"
LeftIcon={IconNotes}
text="Note"
/>
<MenuItem
onClick={() => handleSelect(ActivityType.Note)}
accent="default"
LeftIcon={IconCheckbox}
text="Task"
/>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
}

View File

@ -1,3 +1,4 @@
import { MouseEvent } from 'react';
import { useTheme } from '@emotion/react';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
@ -10,24 +11,26 @@ import { MenuItemAccent } from '../types/MenuItemAccent';
export type MenuItemIconButton = {
Icon: IconComponent;
onClick: () => void;
onClick?: (event: MouseEvent<any>) => void;
};
export type MenuItemProps = {
LeftIcon?: IconComponent;
accent: MenuItemAccent;
LeftIcon?: IconComponent | null;
accent?: MenuItemAccent;
text: string;
iconButtons?: MenuItemIconButton[];
className: string;
className?: string;
testId?: string;
onClick?: () => void;
};
export function MenuItem({
LeftIcon,
accent,
accent = 'default',
text,
iconButtons,
className,
testId,
onClick,
}: MenuItemProps) {
const theme = useTheme();
@ -35,8 +38,13 @@ export function MenuItem({
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
return (
<StyledMenuItemBase onClick={onClick} className={className} accent={accent}>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
<StyledMenuItemBase
data-testid={testId ?? undefined}
onClick={onClick}
className={className}
accent={accent}
>
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
{showIconButtons && (
<FloatingIconButtonGroup>
{iconButtons?.map(({ Icon, onClick }, index) => (

View File

@ -0,0 +1,51 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { Checkbox } from '@/ui/input/checkbox/components/Checkbox';
import {
StyledMenuItemBase,
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
const StyledLeftContentWithCheckboxContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
type OwnProps = {
avatar?: ReactNode;
selected: boolean;
text: string;
className?: string;
onSelectChange?: (selected: boolean) => void;
};
export function MenuItemMultiSelectAvatar({
avatar,
text,
selected,
className,
onSelectChange,
}: OwnProps) {
function handleOnClick() {
onSelectChange?.(!selected);
}
return (
<StyledMenuItemBase className={className} onClick={handleOnClick}>
<StyledLeftContentWithCheckboxContainer>
<Checkbox checked={selected} />
<StyledMenuItemLeftContent>
{avatar}
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
{text}
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
</StyledLeftContentWithCheckboxContainer>
</StyledMenuItemBase>
);
}

View File

@ -10,7 +10,7 @@ export type MenuItemProps = {
LeftIcon?: IconComponent;
text: string;
onClick?: () => void;
className: string;
className?: string;
};
export function MenuItemNavigate({

View File

@ -7,8 +7,12 @@ import { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase';
const StyledMenuItemSelect = styled(StyledMenuItemBase)<{ selected: boolean }>`
${({ theme, selected }) => {
export const StyledMenuItemSelect = styled(StyledMenuItemBase)<{
selected: boolean;
disabled?: boolean;
hovered?: boolean;
}>`
${({ theme, selected, disabled, hovered }) => {
if (selected) {
return css`
background: ${theme.background.transparent.light};
@ -16,16 +20,33 @@ const StyledMenuItemSelect = styled(StyledMenuItemBase)<{ selected: boolean }>`
background: ${theme.background.transparent.medium};
}
`;
} else if (disabled) {
return css`
background: inherit;
&:hover {
background: inherit;
}
color: ${theme.font.color.tertiary};
cursor: default;
`;
} else if (hovered) {
return css`
background: ${theme.background.transparent.light};
`;
}
}}
`;
type OwnProps = {
LeftIcon?: IconComponent;
LeftIcon: IconComponent | null | undefined;
selected: boolean;
text: string;
className: string;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
};
export function MenuItemSelect({
@ -34,6 +55,8 @@ export function MenuItemSelect({
selected,
className,
onClick,
disabled,
hovered,
}: OwnProps) {
const theme = useTheme();
@ -42,6 +65,8 @@ export function MenuItemSelect({
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
>
<MenuItemLeftContent LeftIcon={LeftIcon} text={text} />
{selected && <IconCheck size={theme.icon.size.sm} />}

View File

@ -0,0 +1,55 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import { IconCheck } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
type OwnProps = {
avatar: ReactNode;
selected: boolean;
text: string;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
testId?: string;
};
export function MenuItemSelectAvatar({
avatar,
text,
selected,
className,
onClick,
disabled,
hovered,
testId,
}: OwnProps) {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
data-testid={testId}
>
<StyledMenuItemLeftContent>
{avatar}
<StyledMenuItemLabel hasLeftIcon={!!avatar}>
<OverflowingTextWithTooltip text={text} />
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.sm} />}
</StyledMenuItemSelect>
);
}

View File

@ -0,0 +1,59 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/icon';
import { ThemeColor } from '@/ui/theme/constants/colors';
import {
StyledMenuItemLabel,
StyledMenuItemLeftContent,
} from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
const StyledColorSample = styled.div<{ colorName: ThemeColor }>`
background-color: ${({ theme, colorName }) =>
theme.tag.background[colorName]};
border: 1px solid ${({ theme, colorName }) => theme.color[colorName]};
border-radius: ${({ theme }) => theme.border.radius.sm};
height: 12px;
width: 12px;
`;
type OwnProps = {
selected: boolean;
text: string;
className?: string;
onClick?: () => void;
disabled?: boolean;
hovered?: boolean;
color: ThemeColor;
};
export function MenuItemSelectColor({
color,
text,
selected,
className,
onClick,
disabled,
hovered,
}: OwnProps) {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
disabled={disabled}
hovered={hovered}
>
<StyledMenuItemLeftContent>
<StyledColorSample colorName={color} />
<StyledMenuItemLabel hasLeftIcon={true}>{text}</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.sm} />}
</StyledMenuItemSelect>
);
}

View File

@ -1,6 +1,7 @@
import { useTheme } from '@emotion/react';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import {
StyledMenuItemLabel,
@ -8,7 +9,7 @@ import {
} from './StyledMenuItemBase';
type OwnProps = {
LeftIcon?: IconComponent;
LeftIcon: IconComponent | null | undefined;
text: string;
};
@ -18,7 +19,9 @@ export function MenuItemLeftContent({ LeftIcon, text }: OwnProps) {
return (
<StyledMenuItemLeftContent>
{LeftIcon && <LeftIcon size={theme.icon.size.md} />}
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>{text}</StyledMenuItemLabel>
<StyledMenuItemLabel hasLeftIcon={!!LeftIcon}>
<OverflowingTextWithTooltip text={text} />
</StyledMenuItemLabel>
</StyledMenuItemLeftContent>
);
}

View File

@ -16,9 +16,9 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
@ -29,8 +29,6 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
justify-content: space-between;
padding: var(--vertical-padding) var(--horizontal-padding);
${hoverBackground};
${({ theme, accent }) => {
@ -43,6 +41,11 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
}
`;
}
case 'placeholder': {
return css`
color: ${theme.font.color.tertiary};
`;
}
case 'default':
default: {
return css`
@ -52,7 +55,9 @@ export const StyledMenuItemBase = styled.li<MenuItemBaseProps>`
}
}}
padding: var(--vertical-padding) var(--horizontal-padding);
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));

View File

@ -1 +1 @@
export type MenuItemAccent = 'default' | 'danger';
export type MenuItemAccent = 'default' | 'danger' | 'placeholder';

View File

@ -1,9 +1,11 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
type OwnProps = {
viewName: string;
viewIcon?: ReactNode;
ViewIcon?: IconComponent;
};
const StyledTitle = styled.div`
@ -32,10 +34,13 @@ const StyledText = styled.span`
white-space: nowrap;
`;
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
export function ColumnHead({ viewName, ViewIcon }: OwnProps) {
const theme = useTheme();
return (
<StyledTitle>
<StyledIcon>{viewIcon}</StyledIcon>
<StyledIcon>
{ViewIcon && <ViewIcon size={theme.icon.size.md} />}
</StyledIcon>
<StyledText>{viewName}</StyledText>
</StyledTitle>
);

View File

@ -1,13 +1,11 @@
import { cloneElement, type ComponentProps, useCallback, useRef } from 'react';
import { useTheme } from '@emotion/react';
import { type ComponentProps, useCallback, useRef } from 'react';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
@ -31,7 +29,6 @@ export const EntityTableColumnMenu = ({
...props
}: EntityTableColumnMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
const hiddenTableColumns = useRecoilScopedValue(
hiddenTableColumnsScopedSelector,
@ -57,22 +54,17 @@ export const EntityTableColumnMenu = ({
<StyledColumnMenu {...props} ref={ref}>
<StyledDropdownMenuItemsContainer>
{hiddenTableColumns.map((column) => (
<DropdownMenuItem
<MenuItem
key={column.key}
actions={[
<IconButton
key={`add-${column.key}`}
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => handleAddColumn(column)}
/>,
iconButtons={[
{
Icon: IconPlus,
onClick: () => handleAddColumn(column),
},
]}
>
{column.icon &&
cloneElement(column.icon, {
size: theme.icon.size.md,
})}
{column.name}
</DropdownMenuItem>
LeftIcon={column.Icon}
text={column.name}
/>
))}
</StyledDropdownMenuItemsContainer>
</StyledColumnMenu>

View File

@ -173,7 +173,7 @@ export function EntityTableHeader() {
COLUMN_MIN_WIDTH,
)}
>
<ColumnHead viewName={column.name} viewIcon={column.icon} />
<ColumnHead viewName={column.name} ViewIcon={column.Icon} />
<StyledResizeHandler
className="cursor-col-resize"
role="separator"

View File

@ -1,35 +1,25 @@
import { type FormEvent, useCallback, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
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 { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import {
IconChevronLeft,
IconFileImport,
IconMinus,
IconPlus,
IconTag,
} from '@/ui/icon';
import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { tableColumnsScopedState } from '@/ui/table/states/tableColumnsScopedState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableColumns } from '../../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsScopedState } from '../../states/savedTableColumnsScopedState';
import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector';
@ -41,10 +31,9 @@ import {
tableViewsByIdState,
tableViewsState,
} from '../../states/tableViewsState';
import type { ColumnDefinition } from '../../types/ColumnDefinition';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
import { TableOptionsDropdownColumnVisibility } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = {
onViewsChange?: (views: TableView[]) => void;
@ -59,8 +48,6 @@ export function TableOptionsDropdownContent({
onViewsChange,
onImport,
}: TableOptionsDropdownButtonProps) {
const theme = useTheme();
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const { closeDropdownButton } = useDropdownButton({ key: 'options' });
@ -87,33 +74,6 @@ export function TableOptionsDropdownContent({
TableRecoilScopeContext,
);
const { handleColumnVisibilityChange } = useTableColumns();
const renderFieldActions = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!column.isVisible || visibleTableColumns.length > 1
? [
<IconButton
key={`action-${column.key}`}
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() => handleColumnVisibilityChange(column)}
/>,
]
: undefined,
[
handleColumnVisibilityChange,
theme.icon.size.sm,
visibleTableColumns.length,
],
);
const resetViewEditMode = useCallback(() => {
setTableViewEditMode({ mode: undefined, viewId: undefined });
@ -232,17 +192,17 @@ export function TableOptionsDropdownContent({
)}
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem
<MenuItem
onClick={() => handleSelectOption(Option.Properties)}
>
<IconTag size={theme.icon.size.md} />
Properties
</DropdownMenuItem>
LeftIcon={IconTag}
text="Properties"
/>
{onImport && (
<DropdownMenuItem onClick={onImport}>
<IconFileImport size={theme.icon.size.md} />
Import
</DropdownMenuItem>
<MenuItem
onClick={onImport}
LeftIcon={IconFileImport}
text="Import"
/>
)}
</StyledDropdownMenuItemsContainer>
</>
@ -250,22 +210,20 @@ export function TableOptionsDropdownContent({
{selectedOption === Option.Properties && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
StartIcon={IconChevronLeft}
onClick={resetSelectedOption}
>
Properties
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
<TableOptionsDropdownColumnVisibility
title="Visible"
columns={visibleTableColumns}
/>
{hiddenTableColumns.length > 0 && (
<>
<StyledDropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
<TableOptionsDropdownColumnVisibility
title="Hidden"
columns={hiddenTableColumns}
/>

View File

@ -1,43 +1,39 @@
import { cloneElement } from 'react';
import { useTheme } from '@emotion/react';
import {
DropdownMenuItem,
DropdownMenuItemProps,
} from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { IconMinus, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useTableColumns } from '../../hooks/useTableColumns';
import type { ColumnDefinition } from '../../types/ColumnDefinition';
type TableOptionsDropdownSectionProps = {
renderActions: (
column: ColumnDefinition<ViewFieldMetadata>,
) => DropdownMenuItemProps['actions'];
type OwnProps = {
title: string;
columns: ColumnDefinition<ViewFieldMetadata>[];
};
export function TableOptionsDropdownSection({
renderActions,
export function TableOptionsDropdownColumnVisibility({
title,
columns,
}: TableOptionsDropdownSectionProps) {
const theme = useTheme();
}: OwnProps) {
const { handleColumnVisibilityChange } = useTableColumns();
return (
<>
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{columns.map((column) => (
<DropdownMenuItem key={column.key} actions={renderActions(column)}>
{column.icon &&
cloneElement(column.icon, {
size: theme.icon.size.md,
})}
{column.name}
</DropdownMenuItem>
<MenuItem
key={column.key}
LeftIcon={column.Icon}
iconButtons={[
{
Icon: column.isVisible ? IconMinus : IconPlus,
onClick: () => handleColumnVisibilityChange(column),
},
]}
text={column.name}
/>
))}
</StyledDropdownMenuItemsContainer>
</>

View File

@ -1,12 +1,10 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Button } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer';
@ -17,6 +15,7 @@ import { canPersistFiltersScopedSelector } from '@/ui/filter-n-sort/states/selec
import { canPersistSortsScopedSelector } from '@/ui/filter-n-sort/states/selectors/canPersistSortsScopedSelector';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { IconChevronDown, IconPlus } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
@ -45,8 +44,6 @@ export const TableUpdateViewButtonGroup = ({
onViewSubmit,
HotkeyScope,
}: TableUpdateViewButtonGroupProps) => {
const theme = useTheme();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
@ -153,10 +150,11 @@ export const TableUpdateViewButtonGroup = ({
{isDropdownOpen && (
<DropdownMenuContainer onClose={handleDropdownClose}>
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleCreateViewButtonClick}>
<IconPlus size={theme.icon.size.md} />
Create view
</DropdownMenuItem>
<MenuItem
onClick={handleCreateViewButtonClick}
LeftIcon={IconPlus}
text="Create view"
/>
</StyledDropdownMenuItemsContainer>
</DropdownMenuContainer>
)}

View File

@ -3,8 +3,6 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
@ -20,6 +18,7 @@ import {
IconPlus,
IconTrash,
} from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import {
currentTableViewIdState,
currentTableViewState,
@ -204,37 +203,35 @@ export const TableViewsDropdownButton = ({
>
<StyledDropdownMenuItemsContainer>
{tableViews.map((view) => (
<DropdownMenuItem
<MenuItem
key={view.id}
actions={[
<FloatingIconButton
key="edit"
onClick={(event) => handleEditViewButtonClick(event, view.id)}
icon={<IconPencil size={theme.icon.size.sm} />}
/>,
tableViews.length > 1 ? (
<FloatingIconButton
key="delete"
onClick={(event) =>
handleDeleteViewButtonClick(event, view.id)
iconButtons={[
{
Icon: IconPencil,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleEditViewButtonClick(event, view.id),
},
tableViews.length > 1
? {
Icon: IconTrash,
onClick: (event: MouseEvent<HTMLButtonElement>) =>
handleDeleteViewButtonClick(event, view.id),
}
icon={<IconTrash size={theme.icon.size.sm} />}
/>
) : null,
: null,
].filter(assertNotNull)}
onClick={() => handleViewSelect(view.id)}
>
<IconList size={theme.icon.size.md} />
<StyledViewName>{view.name}</StyledViewName>
</DropdownMenuItem>
LeftIcon={IconList}
text={view.name}
/>
))}
</StyledDropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
<StyledBoldDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleAddViewButtonClick}>
<IconPlus size={theme.icon.size.md} />
Add view
</DropdownMenuItem>
<MenuItem
onClick={handleAddViewButtonClick}
LeftIcon={IconPlus}
text="Add view"
/>
</StyledBoldDropdownMenuItemsContainer>
</DropdownButton>
);

View File

@ -1,5 +1,7 @@
import styled from '@emotion/styled';
import { ThemeColor } from '@/ui/theme/constants/colors';
const tagColors = [
'green',
'turquoise',
@ -40,7 +42,7 @@ const StyledTag = styled.h3<{
`;
export type TagProps = {
color: string;
color: ThemeColor;
text: string;
onClick?: () => void;
};

View File

@ -22,8 +22,22 @@ export const grayScale = {
gray0: '#ffffff',
};
export const color = {
export const mainColors = {
yellow: '#ffd338',
green: '#55ef3c',
turquoise: '#15de8f',
sky: '#00e0ff',
blue: '#1961ed',
purple: '#915ffd',
pink: '#f54bd0',
red: '#f83e3e',
orange: '#ff7222',
gray: grayScale.gray30,
};
export type ThemeColor = keyof typeof mainColors;
export const secondaryColors = {
yellow80: '#2e2a1a',
yellow70: '#453d1e',
yellow60: '#746224',
@ -32,7 +46,7 @@ export const color = {
yellow30: '#ffedaf',
yellow20: '#fff6d7',
yellow10: '#fffbeb',
green: '#55ef3c',
green80: '#1d2d1b',
green70: '#23421e',
green60: '#2a5822',
@ -41,7 +55,7 @@ export const color = {
green30: '#ccfac5',
green20: '#ddfcd8',
green10: '#eefdec',
turquoise: '#15de8f',
turquoise80: '#172b23',
turquoise70: '#173f2f',
turquoise60: '#166747',
@ -50,7 +64,7 @@ export const color = {
turquoise30: '#a1f2d2',
turquoise20: '#d0f8e9',
turquoise10: '#e8fcf4',
sky: '#00e0ff',
sky80: '#152b2e',
sky70: '#123f45',
sky60: '#0e6874',
@ -59,7 +73,7 @@ export const color = {
sky30: '#99f3ff',
sky20: '#ccf9ff',
sky10: '#e5fcff',
blue: '#1961ed',
blue80: '#171e2c',
blue70: '#172642',
blue60: '#18356d',
@ -68,7 +82,7 @@ export const color = {
blue30: '#a3c0f8',
blue20: '#d1dffb',
blue10: '#e8effd',
purple: '#915ffd',
purple80: '#231e2e',
purple70: '#2f2545',
purple60: '#483473',
@ -77,7 +91,7 @@ export const color = {
purple30: '#d3bffe',
purple20: '#e9dfff',
purple10: '#f4efff',
pink: '#f54bd0',
pink80: '#2d1c29',
pink70: '#43213c',
pink60: '#702c61',
@ -86,7 +100,7 @@ export const color = {
pink30: '#fbb7ec',
pink20: '#fddbf6',
pink10: '#feedfa',
red: '#f83e3e',
red80: '#2d1b1b',
red70: '#441f1f',
red60: '#712727',
@ -95,7 +109,7 @@ export const color = {
red30: '#fcb2b2',
red20: '#fed8d8',
red10: '#feecec',
orange: '#ff7222',
orange80: '#2e2018',
orange70: '#452919',
orange60: '#743b1b',
@ -104,7 +118,7 @@ export const color = {
orange30: '#ffc7a7',
orange20: '#ffe3d3',
orange10: '#fff1e9',
gray: grayScale.gray30,
gray80: grayScale.gray70,
gray70: grayScale.gray65,
gray60: grayScale.gray55,
@ -127,6 +141,11 @@ export const color = {
blueAccent10: '#f5f9fd',
};
export const color = {
...mainColors,
...secondaryColors,
};
export function rgba(hex: string, alpha: number) {
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
return `rgba(${rgb},${alpha})`;

View File

@ -0,0 +1,7 @@
import { mainColors, ThemeColor } from '../constants/colors';
export const COLORS = Object.keys(mainColors);
export function isThemeColor(color: string): color is ThemeColor {
return COLORS.includes(color);
}