Feat/generic editable cell all types (#987)
* Added generic relation cell * Deactivated debug * Added default warning * Put back display component * Removed unused types * wip * Renamed to view field * Use new view field structure to have chip working * Finished * Added a temp feature flag * Added double text chip cell * Ok * Finished tables * Fixed icon size * Fixed bug on date field * Use icon index * Fix * Fixed naming * Fix * removed file from merge * Fixed tests * Coverage
This commit is contained in:
@ -167,8 +167,8 @@
|
||||
"workerDirectory": "public"
|
||||
},
|
||||
"nyc": {
|
||||
"statements": 70,
|
||||
"lines": 70,
|
||||
"statements": 65,
|
||||
"lines": 65,
|
||||
"functions": 60,
|
||||
"exclude": [
|
||||
"src/generated/**/*"
|
||||
|
||||
@ -20,7 +20,7 @@ import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
|
||||
import { SignInUp } from './pages/auth/SignInUp';
|
||||
|
||||
// TEMP FEATURE FLAG FOR VIEW FIELDS
|
||||
export const ACTIVATE_VIEW_FIELDS = false;
|
||||
export const ACTIVATE_VIEW_FIELDS = true;
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import { IconBuildingSkyscraper } from '@tabler/icons-react';
|
||||
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import {
|
||||
ViewFieldChipMetadata,
|
||||
ViewFieldDefinition,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
export const companyViewFields: ViewFieldDefinition<unknown>[] = [
|
||||
{
|
||||
columnLabel: 'Name',
|
||||
columnIcon: <IconBuildingSkyscraper size={16} />,
|
||||
columnSize: 150,
|
||||
type: 'chip',
|
||||
columnOrder: 1,
|
||||
metadata: {
|
||||
urlFieldName: 'domainName',
|
||||
contentFieldName: 'name',
|
||||
relationType: Entity.Company,
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldChipMetadata>,
|
||||
];
|
||||
105
front/src/modules/companies/constants/companyViewFields.tsx
Normal file
105
front/src/modules/companies/constants/companyViewFields.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import {
|
||||
IconBuildingSkyscraper,
|
||||
IconCalendarEvent,
|
||||
IconLink,
|
||||
IconMap,
|
||||
IconUser,
|
||||
IconUsers,
|
||||
} from '@/ui/icon/index';
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import {
|
||||
ViewFieldChipMetadata,
|
||||
ViewFieldDateMetadata,
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldNumberMetadata,
|
||||
ViewFieldRelationMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
ViewFieldURLMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
export const companyViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
|
||||
{
|
||||
id: 'name',
|
||||
columnLabel: 'Name',
|
||||
columnIcon: <IconBuildingSkyscraper />,
|
||||
columnSize: 180,
|
||||
columnOrder: 1,
|
||||
metadata: {
|
||||
type: 'chip',
|
||||
urlFieldName: 'domainName',
|
||||
contentFieldName: 'name',
|
||||
relationType: Entity.Company,
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldChipMetadata>,
|
||||
{
|
||||
id: 'domainName',
|
||||
columnLabel: 'URL',
|
||||
columnIcon: <IconLink />,
|
||||
columnSize: 100,
|
||||
columnOrder: 2,
|
||||
metadata: {
|
||||
type: 'url',
|
||||
fieldName: 'domainName',
|
||||
placeHolder: 'example.com',
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldURLMetadata>,
|
||||
{
|
||||
id: 'accountOwner',
|
||||
columnLabel: 'Account Owner',
|
||||
columnIcon: <IconUser />,
|
||||
columnSize: 150,
|
||||
columnOrder: 3,
|
||||
metadata: {
|
||||
type: 'relation',
|
||||
fieldName: 'accountOwner',
|
||||
relationType: Entity.User,
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
|
||||
{
|
||||
id: 'createdAt',
|
||||
columnLabel: 'Creation',
|
||||
columnIcon: <IconCalendarEvent />,
|
||||
columnSize: 150,
|
||||
columnOrder: 4,
|
||||
metadata: {
|
||||
type: 'date',
|
||||
fieldName: 'createdAt',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
|
||||
{
|
||||
id: 'employees',
|
||||
columnLabel: 'Employees',
|
||||
columnIcon: <IconUsers />,
|
||||
columnSize: 150,
|
||||
columnOrder: 5,
|
||||
metadata: {
|
||||
type: 'number',
|
||||
fieldName: 'employees',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldNumberMetadata>,
|
||||
{
|
||||
id: 'linkedin',
|
||||
columnLabel: 'LinkedIn',
|
||||
columnIcon: <IconMap />,
|
||||
columnSize: 170,
|
||||
columnOrder: 6,
|
||||
metadata: {
|
||||
type: 'url',
|
||||
fieldName: 'linkedinUrl',
|
||||
placeHolder: 'LinkedIn URL',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
|
||||
{
|
||||
id: 'address',
|
||||
columnLabel: 'Address',
|
||||
columnIcon: <IconMap />,
|
||||
columnSize: 170,
|
||||
columnOrder: 7,
|
||||
metadata: {
|
||||
type: 'text',
|
||||
fieldName: 'address',
|
||||
placeHolder: 'Address',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
];
|
||||
@ -22,8 +22,20 @@ export function CompanyCreatedAtEditableField({ company }: OwnProps) {
|
||||
setInternalValue(company.createdAt);
|
||||
}, [company.createdAt]);
|
||||
|
||||
// TODO: refactor change and submit
|
||||
async function handleChange(newValue: string) {
|
||||
setInternalValue(newValue);
|
||||
|
||||
await updateCompany({
|
||||
variables: {
|
||||
where: {
|
||||
id: company.id,
|
||||
},
|
||||
data: {
|
||||
createdAt: newValue ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { companyViewFields } from '@/companies/constants/companyFieldMetadataArray';
|
||||
import { companyViewFields } from '@/companies/constants/companyViewFields';
|
||||
import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries';
|
||||
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
|
||||
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { IconList } from '@/ui/icon';
|
||||
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTableV2';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { TableContext } from '@/ui/table/states/TableContext';
|
||||
import {
|
||||
CompanyOrderByWithRelationInput,
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import {
|
||||
IconBriefcase,
|
||||
IconBuildingSkyscraper,
|
||||
IconMap,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldRelationMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
export const peopleViewFields: ViewFieldDefinition<unknown>[] = [
|
||||
{
|
||||
id: 'city',
|
||||
columnLabel: 'City',
|
||||
columnIcon: <IconMap size={16} />,
|
||||
columnSize: 150,
|
||||
type: 'text',
|
||||
columnOrder: 1,
|
||||
metadata: {
|
||||
fieldName: 'city',
|
||||
placeHolder: 'City',
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
{
|
||||
id: 'jobTitle',
|
||||
columnLabel: 'Job title',
|
||||
columnIcon: <IconBriefcase size={16} />,
|
||||
columnSize: 150,
|
||||
type: 'text',
|
||||
columnOrder: 2,
|
||||
metadata: {
|
||||
fieldName: 'jobTitle',
|
||||
placeHolder: 'Job title',
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
{
|
||||
id: 'company',
|
||||
columnLabel: 'Company',
|
||||
columnIcon: <IconBuildingSkyscraper size={16} />,
|
||||
columnSize: 150,
|
||||
type: 'relation',
|
||||
relationType: Entity.Company,
|
||||
columnOrder: 3,
|
||||
metadata: {
|
||||
fieldName: 'company',
|
||||
relationType: Entity.Company,
|
||||
},
|
||||
} as ViewFieldDefinition<ViewFieldRelationMetadata>,
|
||||
];
|
||||
122
front/src/modules/people/constants/peopleViewFields.tsx
Normal file
122
front/src/modules/people/constants/peopleViewFields.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import {
|
||||
IconBrandLinkedin,
|
||||
IconBriefcase,
|
||||
IconBuildingSkyscraper,
|
||||
IconCalendarEvent,
|
||||
IconMail,
|
||||
IconMap,
|
||||
IconPhone,
|
||||
IconUser,
|
||||
} from '@/ui/icon/index';
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import {
|
||||
ViewFieldDateMetadata,
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldPhoneMetadata,
|
||||
ViewFieldRelationMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
ViewFieldURLMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
|
||||
{
|
||||
id: 'displayName',
|
||||
columnLabel: 'People',
|
||||
columnIcon: <IconUser />,
|
||||
columnSize: 210,
|
||||
columnOrder: 1,
|
||||
metadata: {
|
||||
type: 'double-text-chip',
|
||||
firstValueFieldName: 'firstName',
|
||||
secondValueFieldName: 'lastName',
|
||||
firstValuePlaceholder: 'First name',
|
||||
secondValuePlaceholder: 'Last name',
|
||||
entityType: Entity.Person,
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>,
|
||||
{
|
||||
id: 'email',
|
||||
columnLabel: 'Email',
|
||||
columnIcon: <IconMail />,
|
||||
columnSize: 150,
|
||||
columnOrder: 2,
|
||||
metadata: {
|
||||
type: 'text',
|
||||
fieldName: 'email',
|
||||
placeHolder: 'Email',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
{
|
||||
id: 'company',
|
||||
columnLabel: 'Company',
|
||||
columnIcon: <IconBuildingSkyscraper />,
|
||||
columnSize: 150,
|
||||
columnOrder: 3,
|
||||
metadata: {
|
||||
type: 'relation',
|
||||
fieldName: 'company',
|
||||
relationType: Entity.Company,
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
|
||||
{
|
||||
id: 'phone',
|
||||
columnLabel: 'Phone',
|
||||
columnIcon: <IconPhone />,
|
||||
columnSize: 150,
|
||||
columnOrder: 4,
|
||||
metadata: {
|
||||
type: 'phone',
|
||||
fieldName: 'phone',
|
||||
placeHolder: 'Phone',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldPhoneMetadata>,
|
||||
{
|
||||
id: 'createdAt',
|
||||
columnLabel: 'Creation',
|
||||
columnIcon: <IconCalendarEvent />,
|
||||
columnSize: 150,
|
||||
columnOrder: 5,
|
||||
metadata: {
|
||||
type: 'date',
|
||||
fieldName: 'createdAt',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
|
||||
{
|
||||
id: 'city',
|
||||
columnLabel: 'City',
|
||||
columnIcon: <IconMap />,
|
||||
columnSize: 150,
|
||||
columnOrder: 6,
|
||||
metadata: {
|
||||
type: 'text',
|
||||
fieldName: 'city',
|
||||
placeHolder: 'City',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
{
|
||||
id: 'jobTitle',
|
||||
columnLabel: 'Job title',
|
||||
columnIcon: <IconBriefcase />,
|
||||
columnSize: 150,
|
||||
columnOrder: 7,
|
||||
metadata: {
|
||||
type: 'text',
|
||||
fieldName: 'jobTitle',
|
||||
placeHolder: 'Job title',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
|
||||
{
|
||||
id: 'linkedin',
|
||||
columnLabel: 'LinkedIn',
|
||||
columnIcon: <IconBrandLinkedin />,
|
||||
columnSize: 150,
|
||||
columnOrder: 8,
|
||||
metadata: {
|
||||
type: 'url',
|
||||
fieldName: 'linkedinUrl',
|
||||
placeHolder: 'LinkedIn',
|
||||
},
|
||||
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
|
||||
];
|
||||
@ -1,80 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
|
||||
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
|
||||
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
|
||||
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
|
||||
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
|
||||
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
|
||||
|
||||
export function useUpdateEntityField() {
|
||||
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
|
||||
|
||||
const [updateEntity] = useUpdateEntityMutation();
|
||||
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
|
||||
return function updatePeopleField(
|
||||
currentEntityId: string,
|
||||
viewFieldId: string,
|
||||
newFieldValue: unknown,
|
||||
) {
|
||||
const viewField = viewFields.find(
|
||||
(metadata) => metadata.id === viewFieldId,
|
||||
);
|
||||
|
||||
if (!viewField) {
|
||||
throw new Error(`View field not found for id ${viewFieldId}`);
|
||||
}
|
||||
|
||||
// TODO: improve type narrowing here with validation maybe ? Also validate the newFieldValue with linked type guards
|
||||
if (isViewFieldRelation(viewField)) {
|
||||
const newSelectedEntity = newFieldValue as EntityForSelect | null;
|
||||
|
||||
const fieldName = viewField.metadata.fieldName;
|
||||
|
||||
if (!newSelectedEntity) {
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[fieldName]: {
|
||||
disconnect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[fieldName]: {
|
||||
connect: { id: newSelectedEntity.id },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (isViewFieldChip(viewField)) {
|
||||
const newContent = newFieldValue as string;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.contentFieldName]: newContent },
|
||||
},
|
||||
});
|
||||
} else if (isViewFieldText(viewField)) {
|
||||
const newContent = newFieldValue as string;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { defaultOrderBy } from '@/companies/queries';
|
||||
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
|
||||
import { peopleViewFields } from '@/people/constants/peopleFieldMetadataArray';
|
||||
import { peopleViewFields } from '@/people/constants/peopleViewFields';
|
||||
import { PeopleSelectedSortType } from '@/people/queries';
|
||||
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
@ -10,6 +9,7 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn
|
||||
import { IconList } from '@/ui/icon';
|
||||
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTableV2';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { TableContext } from '@/ui/table/states/TableContext';
|
||||
import {
|
||||
PersonOrderByWithRelationInput,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
|
||||
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
|
||||
import { BoardCardEditableField } from './BoardCardEditableField';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
|
||||
type OwnProps = {
|
||||
value: Date | null;
|
||||
value: Date | string | null;
|
||||
};
|
||||
|
||||
export function InplaceInputDateDisplayMode({ value }: OwnProps) {
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { MouseEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RawLink } from '@/ui/link/components/RawLink';
|
||||
|
||||
const StyledRawLink = styled(RawLink)`
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function InplaceInputURLDisplayMode({ value }: OwnProps) {
|
||||
function handleClick(event: MouseEvent<HTMLElement>) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const absoluteUrl = value
|
||||
? value.startsWith('http')
|
||||
? value
|
||||
: 'https://' + value
|
||||
: '';
|
||||
|
||||
return (
|
||||
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
|
||||
{value}
|
||||
</StyledRawLink>
|
||||
);
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
|
||||
|
||||
const DEBUG_HOTKEY_SCOPE = false;
|
||||
const DEBUG_HOTKEY_SCOPE = true;
|
||||
|
||||
export function useScopedHotkeyCallback() {
|
||||
return useRecoilCallback(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
@ -11,7 +11,7 @@ type OwnProps = {
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
export const StyledDoubleTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -30,7 +30,7 @@ export function InplaceInputDoubleText({
|
||||
onChange,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledDoubleTextContainer>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
placeholder={firstValuePlaceholder}
|
||||
@ -47,6 +47,6 @@ export function InplaceInputDoubleText({
|
||||
onChange(firstValue, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</StyledDoubleTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/themes/effects';
|
||||
|
||||
import { useRegisterCloseCellHandlers } from '../../table/editable-cell/hooks/useRegisterCloseCellHandlers';
|
||||
|
||||
import { StyledDoubleTextContainer } from './InplaceInputDoubleText';
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
firstValuePlaceholder?: string;
|
||||
secondValuePlaceholder?: string;
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
onSubmit: (newFirstValue: string, newSecondValue: string) => void;
|
||||
};
|
||||
|
||||
export function InplaceInputDoubleTextCellEditMode({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const [internalFirstValue, setInternalFirstValue] = useState(firstValue);
|
||||
const [internalSecondValue, setInternalSecondValue] = useState(secondValue);
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
function handleSubmit() {
|
||||
onSubmit(internalFirstValue, internalSecondValue);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setInternalFirstValue(firstValue);
|
||||
setInternalSecondValue(secondValue);
|
||||
}
|
||||
|
||||
function handleFirstValueChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
setInternalFirstValue(event.target.value);
|
||||
}
|
||||
|
||||
function handleSecondValueChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
setInternalSecondValue(event.target.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setInternalFirstValue(firstValue);
|
||||
}, [firstValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalSecondValue(secondValue);
|
||||
}, [secondValue]);
|
||||
|
||||
useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel);
|
||||
|
||||
return (
|
||||
<StyledDoubleTextContainer ref={wrapperRef}>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
placeholder={firstValuePlaceholder}
|
||||
value={internalFirstValue}
|
||||
onChange={handleFirstValueChange}
|
||||
/>
|
||||
<StyledInput
|
||||
autoComplete="off"
|
||||
placeholder={secondValuePlaceholder}
|
||||
value={internalSecondValue}
|
||||
onChange={handleSecondValueChange}
|
||||
/>
|
||||
</StyledDoubleTextContainer>
|
||||
);
|
||||
}
|
||||
@ -18,7 +18,7 @@ type OwnProps = {
|
||||
onSubmit: (newText: string) => void;
|
||||
};
|
||||
|
||||
export function InplaceInputTextEditMode({
|
||||
export function InplaceInputTextCellEditMode({
|
||||
placeholder,
|
||||
autoFocus,
|
||||
value,
|
||||
@ -11,13 +11,18 @@ const StyledTitle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
display: flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
& > svg {
|
||||
height: ${({ theme }) => theme.icon.size.md}px;
|
||||
width: ${({ theme }) => theme.icon.size.md}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function ColumnHead({ viewName, viewIcon }: OwnProps) {
|
||||
|
||||
@ -25,9 +25,9 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
|
||||
});
|
||||
}
|
||||
|
||||
const entityFieldMetadata = useContext(ViewFieldContext);
|
||||
const viewField = useContext(ViewFieldContext);
|
||||
|
||||
if (!entityFieldMetadata) {
|
||||
if (!viewField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -37,12 +37,12 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
|
||||
<td
|
||||
onContextMenu={(event) => handleContextMenu(event)}
|
||||
style={{
|
||||
width: entityFieldMetadata.columnSize,
|
||||
minWidth: entityFieldMetadata.columnSize,
|
||||
maxWidth: entityFieldMetadata.columnSize,
|
||||
width: viewField.columnSize,
|
||||
minWidth: viewField.columnSize,
|
||||
maxWidth: viewField.columnSize,
|
||||
}}
|
||||
>
|
||||
<GenericEditableCell fieldDefinition={entityFieldMetadata} />
|
||||
<GenericEditableCell viewField={viewField} />
|
||||
</td>
|
||||
</ColumnIndexContext.Provider>
|
||||
</RecoilScope>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
|
||||
export function EntityTableHeader() {
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
|
||||
return (
|
||||
<thead>
|
||||
|
||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ViewFieldContext } from '../states/ViewFieldContext';
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
import { viewFieldsFamilyState } from '../states/viewFieldsState';
|
||||
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import { EntityTableCell } from './EntityTableCellV2';
|
||||
@ -13,18 +13,18 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||
const entityFieldMetadataArray = useRecoilValue(viewFieldsState);
|
||||
const viewFields = useRecoilValue(viewFieldsFamilyState);
|
||||
|
||||
return (
|
||||
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{entityFieldMetadataArray.map((entityFieldMetadata, columnIndex) => {
|
||||
{viewFields.map((viewField, columnIndex) => {
|
||||
return (
|
||||
<ViewFieldContext.Provider
|
||||
value={entityFieldMetadata}
|
||||
key={entityFieldMetadata.columnOrder}
|
||||
value={viewField}
|
||||
key={viewField.columnOrder}
|
||||
>
|
||||
<EntityTableCell cellIndex={columnIndex} />
|
||||
</ViewFieldContext.Provider>
|
||||
|
||||
@ -1,37 +1,54 @@
|
||||
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
import { isViewFieldChip } from '../types/guards/isViewFieldChip';
|
||||
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
|
||||
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
|
||||
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
|
||||
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
|
||||
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
|
||||
import { isViewFieldRelation } from '../types/guards/isViewFieldRelation';
|
||||
import { isViewFieldText } from '../types/guards/isViewFieldText';
|
||||
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
|
||||
|
||||
import { GenericEditableChipCell } from './GenericEditableChipCell';
|
||||
import { GenericEditableDateCell } from './GenericEditableDateCell';
|
||||
import { GenericEditableDoubleTextCell } from './GenericEditableDoubleTextCell';
|
||||
import { GenericEditableDoubleTextChipCell } from './GenericEditableDoubleTextChipCell';
|
||||
import { GenericEditableNumberCell } from './GenericEditableNumberCell';
|
||||
import { GenericEditablePhoneCell } from './GenericEditablePhoneCell';
|
||||
import { GenericEditableRelationCell } from './GenericEditableRelationCell';
|
||||
import { GenericEditableTextCell } from './GenericEditableTextCell';
|
||||
import { GenericEditableURLCell } from './GenericEditableURLCell';
|
||||
|
||||
type OwnProps = {
|
||||
fieldDefinition: ViewFieldDefinition<unknown>;
|
||||
viewField: ViewFieldDefinition<ViewFieldMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableCell({ fieldDefinition }: OwnProps) {
|
||||
export function GenericEditableCell({ viewField: fieldDefinition }: OwnProps) {
|
||||
if (isViewFieldText(fieldDefinition)) {
|
||||
return (
|
||||
<GenericEditableTextCell
|
||||
viewField={fieldDefinition}
|
||||
editModeHorizontalAlign="left"
|
||||
/>
|
||||
);
|
||||
return <GenericEditableTextCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldRelation(fieldDefinition)) {
|
||||
return <GenericEditableRelationCell fieldDefinition={fieldDefinition} />;
|
||||
} else if (isViewFieldDoubleTextChip(fieldDefinition)) {
|
||||
return <GenericEditableDoubleTextChipCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldDoubleText(fieldDefinition)) {
|
||||
return <GenericEditableDoubleTextCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldPhone(fieldDefinition)) {
|
||||
return <GenericEditablePhoneCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldURL(fieldDefinition)) {
|
||||
return <GenericEditableURLCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldDate(fieldDefinition)) {
|
||||
return <GenericEditableDateCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldNumber(fieldDefinition)) {
|
||||
return <GenericEditableNumberCell viewField={fieldDefinition} />;
|
||||
} else if (isViewFieldChip(fieldDefinition)) {
|
||||
return (
|
||||
<GenericEditableChipCell
|
||||
viewField={fieldDefinition}
|
||||
editModeHorizontalAlign="left"
|
||||
/>
|
||||
);
|
||||
return <GenericEditableChipCell viewField={fieldDefinition} />;
|
||||
} else {
|
||||
console.warn(
|
||||
`Unknown field type: ${fieldDefinition.type} in GenericEditableCell`,
|
||||
`Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`,
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField';
|
||||
|
||||
import { GenericEditableChipCellDisplayMode } from './GenericEditableChipCellDisplayMode';
|
||||
import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMode';
|
||||
import { GenericEditableChipCellEditMode } from './GenericEditableChipCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldChipMetadata>;
|
||||
@ -14,17 +14,12 @@ type OwnProps = {
|
||||
export function GenericEditableChipCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
placeholder,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<GenericEditableTextCellEditMode
|
||||
fieldName={viewField.metadata.contentFieldName}
|
||||
viewFieldId={viewField.id}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<GenericEditableChipCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<GenericEditableChipCellDisplayMode fieldDefinition={viewField} />
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldChipMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableChipCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.contentFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newText: string) {
|
||||
if (newText === fieldValue) return;
|
||||
|
||||
setFieldValue(newText);
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewField, newText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField';
|
||||
|
||||
import { GenericEditableDateCellEditMode } from './GenericEditableDateCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function GenericEditableDateCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const fieldValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<GenericEditableDateCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={<InplaceInputDateDisplayMode value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { EditableCellDateEditMode } from '../editable-cell/types/EditableCellDateEditMode';
|
||||
import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDateCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newDate: Date) {
|
||||
const fieldValueDate = fieldValue
|
||||
? DateTime.fromISO(fieldValue).toJSDate()
|
||||
: null;
|
||||
|
||||
const newDateISO = DateTime.fromJSDate(newDate).toISO();
|
||||
|
||||
if (newDate === fieldValueDate || !newDateISO) return;
|
||||
|
||||
setFieldValue(newDateISO);
|
||||
|
||||
if (currentRowEntityId && updateField && newDateISO) {
|
||||
updateField(currentRowEntityId, viewField, newDateISO);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellDateEditMode
|
||||
value={DateTime.fromISO(fieldValue).toJSDate()}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
import { GenericEditableDoubleTextCellEditMode } from './GenericEditableDoubleTextCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDoubleTextMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDoubleTextCell({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const firstValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const secondValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.secondValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const displayName = `${firstValue ?? ''} ${secondValue ?? ''}`;
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeContent={
|
||||
<GenericEditableDoubleTextCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<InplaceInputTextDisplayMode>{displayName}</InplaceInputTextDisplayMode>
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { InplaceInputDoubleTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDoubleTextMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDoubleTextCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [firstValue, setFirstValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [secondValue, setSecondValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newFirstValue: string, newSecondValue: string) {
|
||||
if (newFirstValue === firstValue && newSecondValue === secondValue) return;
|
||||
|
||||
setFirstValue(newFirstValue);
|
||||
setSecondValue(newSecondValue);
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewField, {
|
||||
firstValue: newFirstValue,
|
||||
secondValue: newSecondValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputDoubleTextCellEditMode
|
||||
firstValuePlaceholder={viewField.metadata.firstValuePlaceholder}
|
||||
secondValuePlaceholder={viewField.metadata.secondValuePlaceholder}
|
||||
firstValue={firstValue ?? ''}
|
||||
secondValue={secondValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
|
||||
import { TableHotkeyScope } from '../types/TableHotkeyScope';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
import { GenericEditableDoubleTextChipCellDisplayMode } from './GenericEditableDoubleTextChipCellDisplayMode';
|
||||
import { GenericEditableDoubleTextChipCellEditMode } from './GenericEditableDoubleTextChipCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDoubleTextChipCell({ viewField }: OwnProps) {
|
||||
return (
|
||||
<EditableCell
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }}
|
||||
editModeContent={
|
||||
<GenericEditableDoubleTextChipCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<GenericEditableDoubleTextChipCellDisplayMode viewField={viewField} />
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { CompanyChip } from '@/companies/components/CompanyChip';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDoubleTextChipCellDisplayMode({
|
||||
viewField,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const [firstValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [secondValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.secondValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const displayName = `${firstValue} ${secondValue}`;
|
||||
|
||||
switch (viewField.metadata.entityType) {
|
||||
case Entity.Company: {
|
||||
return <CompanyChip id={currentRowEntityId ?? ''} name={displayName} />;
|
||||
}
|
||||
case Entity.Person: {
|
||||
return <PersonChip id={currentRowEntityId ?? ''} name={displayName} />;
|
||||
}
|
||||
default:
|
||||
console.warn(
|
||||
`Unknown relation type: "${viewField.metadata.entityType}" in GenericEditableDoubleTextChipCellDisplayMode`,
|
||||
);
|
||||
return <> </>;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { EditableCellDoubleTextEditMode } from '../editable-cell/types/EditableCellDoubleTextEditMode';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableDoubleTextChipCellEditMode({
|
||||
viewField,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [firstValue, setFirstValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.firstValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const [secondValue, setSecondValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.secondValueFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newFirstValue: string, newSecondValue: string) {
|
||||
const firstValueChanged = newFirstValue !== firstValue;
|
||||
const secondValueChanged = newSecondValue !== secondValue;
|
||||
|
||||
if (firstValueChanged) {
|
||||
setFirstValue(newFirstValue);
|
||||
}
|
||||
|
||||
if (secondValueChanged) {
|
||||
setSecondValue(newSecondValue);
|
||||
}
|
||||
|
||||
if (
|
||||
currentRowEntityId &&
|
||||
updateField &&
|
||||
(firstValueChanged || secondValueChanged)
|
||||
) {
|
||||
updateField(currentRowEntityId, viewField, {
|
||||
firstValue: firstValueChanged ? newFirstValue : firstValue,
|
||||
secondValue: secondValueChanged ? newSecondValue : secondValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCellDoubleTextEditMode
|
||||
firstValuePlaceholder={viewField.metadata.firstValuePlaceholder}
|
||||
secondValuePlaceholder={viewField.metadata.secondValuePlaceholder}
|
||||
firstValue={firstValue ?? ''}
|
||||
secondValue={secondValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldNumberMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
import { GenericEditableNumberCellEditMode } from './GenericEditableNumberCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function GenericEditableNumberCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const fieldValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<GenericEditableNumberCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={<>{fieldValue}</>}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldNumberMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableNumberCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newText: string) {
|
||||
if (newText === fieldValue) return;
|
||||
|
||||
try {
|
||||
const numberValue = parseInt(newText);
|
||||
|
||||
if (isNaN(numberValue)) {
|
||||
throw new Error('Not a number');
|
||||
}
|
||||
|
||||
// TODO: find a way to store this better in DB
|
||||
if (numberValue > 2000000000) {
|
||||
throw new Error('Number too big');
|
||||
}
|
||||
|
||||
console.log({ numberValue });
|
||||
|
||||
setFieldValue(numberValue.toString());
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewField, numberValue);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`In GenericEditableNumberCellEditMode, Invalid number: ${newText}, ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputTextCellEditMode
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldPhoneMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
import { GenericEditablePhoneCellEditMode } from './GenericEditablePhoneCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function GenericEditablePhoneCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const fieldValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<GenericEditablePhoneCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={<InplaceInputPhoneDisplayMode value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldPhoneMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditablePhoneCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newText: string) {
|
||||
if (newText === fieldValue) return;
|
||||
|
||||
setFieldValue(newText);
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewField, newText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -24,9 +24,7 @@ export function GenericEditableRelationCell({
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }}
|
||||
editModeContent={
|
||||
<GenericEditableRelationCellEditMode
|
||||
viewFieldDefinition={fieldDefinition}
|
||||
/>
|
||||
<GenericEditableRelationCellEditMode viewField={fieldDefinition} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<GenericEditableRelationCellDisplayMode
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldRelationMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
import { UserChip } from '@/users/components/UserChip';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
type OwnProps = {
|
||||
@ -21,6 +22,7 @@ export function GenericEditableRelationCellDisplayMode({
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: type value with generic getter
|
||||
const fieldValue = useRecoilValue<any | null>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
@ -38,6 +40,14 @@ export function GenericEditableRelationCellDisplayMode({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case Entity.User: {
|
||||
return (
|
||||
<UserChip
|
||||
id={fieldValue?.id ?? ''}
|
||||
name={fieldValue?.displayName ?? ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
console.warn(
|
||||
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`,
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { CompanyPickerCell } from '@/companies/components/CompanyPickerCell';
|
||||
import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
|
||||
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldRelationMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
import { UserPicker } from '@/users/components/UserPicker';
|
||||
|
||||
type OwnProps = {
|
||||
viewFieldDefinition: ViewFieldDefinition<ViewFieldRelationMetadata>;
|
||||
viewField: ViewFieldDefinition<ViewFieldRelationMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableRelationCellEditMode({
|
||||
viewFieldDefinition,
|
||||
}: OwnProps) {
|
||||
export function GenericEditableRelationCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
@ -26,7 +25,7 @@ export function GenericEditableRelationCellEditMode({
|
||||
const [fieldValueEntity] = useRecoilState<any | null>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewFieldDefinition.metadata.fieldName,
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -38,11 +37,7 @@ export function GenericEditableRelationCellEditMode({
|
||||
currentRowEntityId &&
|
||||
updateEntityField
|
||||
) {
|
||||
updateEntityField(
|
||||
currentRowEntityId,
|
||||
viewFieldDefinition.id,
|
||||
newFieldEntity,
|
||||
);
|
||||
updateEntityField(currentRowEntityId, viewField, newFieldEntity);
|
||||
}
|
||||
|
||||
closeEditableCell();
|
||||
@ -52,7 +47,7 @@ export function GenericEditableRelationCellEditMode({
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
switch (viewFieldDefinition.metadata.relationType) {
|
||||
switch (viewField.metadata.relationType) {
|
||||
case Entity.Company: {
|
||||
return (
|
||||
<CompanyPickerCell
|
||||
@ -62,9 +57,18 @@ export function GenericEditableRelationCellEditMode({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case Entity.User: {
|
||||
return (
|
||||
<UserPicker
|
||||
userId={fieldValueEntity?.id ?? null}
|
||||
onSubmit={handleEntitySubmit}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
console.warn(
|
||||
`Unknown relation type: "${viewFieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`,
|
||||
`Unknown relation type: "${viewField.metadata.relationType}" in GenericEditableRelationCellEditMode`,
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -12,13 +12,11 @@ import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMo
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldTextMetadata>;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export function GenericEditableTextCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
placeholder,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
@ -33,11 +31,7 @@ export function GenericEditableTextCell({
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<GenericEditableTextCellEditMode
|
||||
fieldName={viewField.metadata.fieldName}
|
||||
viewFieldId={viewField.id}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<GenericEditableTextCellEditMode viewField={viewField} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<InplaceInputTextDisplayMode>{fieldValue}</InplaceInputTextDisplayMode>
|
||||
|
||||
@ -1,28 +1,24 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { ViewFieldDefinition, ViewFieldTextMetadata } from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
fieldName: string;
|
||||
viewFieldId: string;
|
||||
placeholder?: string;
|
||||
viewField: ViewFieldDefinition<ViewFieldTextMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableTextCellEditMode({
|
||||
fieldName,
|
||||
viewFieldId,
|
||||
placeholder,
|
||||
}: OwnProps) {
|
||||
export function GenericEditableTextCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName,
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -34,13 +30,13 @@ export function GenericEditableTextCellEditMode({
|
||||
setFieldValue(newText);
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewFieldId, newText);
|
||||
updateField(currentRowEntityId, viewField, newText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputTextEditMode
|
||||
placeholder={placeholder ?? ''}
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { InplaceInputURLDisplayMode } from '@/ui/display/component/InplaceInputURLDisplayMode';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { ViewFieldDefinition, ViewFieldURLMetadata } from '../types/ViewField';
|
||||
|
||||
import { GenericEditableURLCellEditMode } from './GenericEditableURLCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldURLMetadata>;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export function GenericEditableURLCell({
|
||||
viewField,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
const fieldValue = useRecoilValue<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={<GenericEditableURLCellEditMode viewField={viewField} />}
|
||||
nonEditModeContent={<InplaceInputURLDisplayMode value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
|
||||
|
||||
import { ViewFieldDefinition, ViewFieldURLMetadata } from '../types/ViewField';
|
||||
|
||||
type OwnProps = {
|
||||
viewField: ViewFieldDefinition<ViewFieldURLMetadata>;
|
||||
};
|
||||
|
||||
export function GenericEditableURLCellEditMode({ viewField }: OwnProps) {
|
||||
const currentRowEntityId = useCurrentRowEntityId();
|
||||
|
||||
// TODO: we could use a hook that would return the field value with the right type
|
||||
const [fieldValue, setFieldValue] = useRecoilState<string>(
|
||||
tableEntityFieldFamilySelector({
|
||||
entityId: currentRowEntityId ?? '',
|
||||
fieldName: viewField.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newText: string) {
|
||||
if (newText === fieldValue) return;
|
||||
|
||||
setFieldValue(newText);
|
||||
|
||||
if (currentRowEntityId && updateField) {
|
||||
updateField(currentRowEntityId, viewField, newText);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||
autoFocus
|
||||
value={fieldValue ?? ''}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
|
||||
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
import { useSetEntityTableData } from '../hooks/useSetEntityTableData';
|
||||
import { defaultOrderBy } from '../queries';
|
||||
import { defaultOrderBy } from '../../../people/queries';
|
||||
|
||||
export function GenericEntityTableData({
|
||||
useGetRequest,
|
||||
@ -16,7 +19,7 @@ export function GenericEntityTableData({
|
||||
getRequestResultKey: string;
|
||||
orderBy?: any;
|
||||
whereFilters?: any;
|
||||
viewFields: ViewFieldDefinition<unknown>[];
|
||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
filterDefinitionArray: FilterDefinition[];
|
||||
}) {
|
||||
const setEntityTableData = useSetEntityTableData();
|
||||
@ -20,7 +20,7 @@ export function EditableCellDate({
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<EditableCellDateEditMode onChange={onChange} value={value} />
|
||||
<EditableCellDateEditMode onSubmit={onChange} value={value} />
|
||||
}
|
||||
nonEditModeContent={<InplaceInputDateDisplayMode value={value} />}
|
||||
editHotkeyScope={{ scope: TableHotkeyScope.CellDateEditMode }}
|
||||
|
||||
@ -16,17 +16,17 @@ const EditableCellDateEditModeContainer = styled.div`
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
onChange: (date: Date) => void;
|
||||
onSubmit: (date: Date) => void;
|
||||
};
|
||||
|
||||
export function EditableCellDateEditMode({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
}: EditableDateProps) {
|
||||
const { closeEditableCell } = useEditableCell();
|
||||
|
||||
function handleDateChange(newDate: Date) {
|
||||
onChange(newDate);
|
||||
onSubmit(newDate);
|
||||
|
||||
closeEditableCell();
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
|
||||
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
@ -13,7 +13,7 @@ export function EditableCellPhone({ value, placeholder, onSubmit }: OwnProps) {
|
||||
return (
|
||||
<EditableCell
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
<InplaceInputTextCellEditMode
|
||||
autoFocus
|
||||
placeholder={placeholder || ''}
|
||||
value={value}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
@ -23,7 +23,7 @@ export function EditableCellText({
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
value={value}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
|
||||
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
|
||||
|
||||
import { RawLink } from '../../../link/components/RawLink';
|
||||
import { CellSkeleton } from '../components/CellSkeleton';
|
||||
@ -25,7 +25,7 @@ export function EditableCellURL({
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
value={url}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { InplaceInputTextEditMode } from '../../../inplace-input/components/InplaceInputTextEditMode';
|
||||
import { InplaceInputTextCellEditMode } from '../../../inplace-input/components/InplaceInputTextCellEditMode';
|
||||
import { EditableCell } from '../components/EditableCell';
|
||||
|
||||
export type EditableChipProps = {
|
||||
@ -52,7 +52,7 @@ export function EditableCellChip({
|
||||
<EditableCell
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<InplaceInputTextEditMode
|
||||
<InplaceInputTextCellEditMode
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
value={inputValue}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { useContextScopeId } from '@/ui/recoil-scope/hooks/useContextScopeId';
|
||||
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
|
||||
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
|
||||
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
|
||||
import { TableContext } from '@/ui/table/states/TableContext';
|
||||
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
|
||||
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
|
||||
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
|
||||
|
||||
import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState';
|
||||
import { useContextScopeId } from '../../ui/recoil-scope/hooks/useContextScopeId';
|
||||
import { useResetTableRowSelection } from '../../ui/table/hooks/useResetTableRowSelection';
|
||||
import { entityTableDimensionsState } from '../../ui/table/states/entityTableDimensionsState';
|
||||
import { isFetchingEntityTableDataState } from '../../ui/table/states/isFetchingEntityTableDataState';
|
||||
import { TableContext } from '../../ui/table/states/TableContext';
|
||||
import { tableRowIdsState } from '../../ui/table/states/tableRowIdsState';
|
||||
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
|
||||
import { viewFieldsFamilyState } from '@/ui/table/states/viewFieldsState';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/table/types/ViewField';
|
||||
|
||||
export function useSetEntityTableData() {
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
@ -22,7 +24,7 @@ export function useSetEntityTableData() {
|
||||
({ set, snapshot }) =>
|
||||
<T extends { id: string }>(
|
||||
newEntityArray: T[],
|
||||
viewFields: ViewFieldDefinition<unknown>[],
|
||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[],
|
||||
filters: FilterDefinition[],
|
||||
) => {
|
||||
for (const entity of newEntityArray) {
|
||||
@ -54,7 +56,7 @@ export function useSetEntityTableData() {
|
||||
|
||||
set(availableFiltersScopedState(tableContextScopeId), filters);
|
||||
|
||||
set(viewFieldsState, viewFields);
|
||||
set(viewFieldsFamilyState, viewFields);
|
||||
|
||||
set(isFetchingEntityTableDataState, false);
|
||||
},
|
||||
228
front/src/modules/ui/table/hooks/useUpdateEntityField.ts
Normal file
228
front/src/modules/ui/table/hooks/useUpdateEntityField.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
|
||||
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
|
||||
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
|
||||
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
|
||||
|
||||
import { isViewFieldChipValue } from '../types/guards/isViewFieldChipValue';
|
||||
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
|
||||
import { isViewFieldDateValue } from '../types/guards/isViewFieldDateValue';
|
||||
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
|
||||
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
|
||||
import { isViewFieldDoubleTextChipValue } from '../types/guards/isViewFieldDoubleTextChipValue';
|
||||
import { isViewFieldDoubleTextValue } from '../types/guards/isViewFieldDoubleTextValue';
|
||||
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
|
||||
import { isViewFieldNumberValue } from '../types/guards/isViewFieldNumberValue';
|
||||
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
|
||||
import { isViewFieldPhoneValue } from '../types/guards/isViewFieldPhoneValue';
|
||||
import { isViewFieldRelationValue } from '../types/guards/isViewFieldRelationValue';
|
||||
import { isViewFieldTextValue } from '../types/guards/isViewFieldTextValue';
|
||||
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
|
||||
import { isViewFieldURLValue } from '../types/guards/isViewFieldURLValue';
|
||||
import {
|
||||
ViewFieldChipMetadata,
|
||||
ViewFieldChipValue,
|
||||
ViewFieldDateMetadata,
|
||||
ViewFieldDateValue,
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
ViewFieldDoubleTextChipValue,
|
||||
ViewFieldDoubleTextMetadata,
|
||||
ViewFieldDoubleTextValue,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldNumberMetadata,
|
||||
ViewFieldNumberValue,
|
||||
ViewFieldPhoneMetadata,
|
||||
ViewFieldPhoneValue,
|
||||
ViewFieldRelationMetadata,
|
||||
ViewFieldRelationValue,
|
||||
ViewFieldTextMetadata,
|
||||
ViewFieldTextValue,
|
||||
ViewFieldURLMetadata,
|
||||
ViewFieldURLValue,
|
||||
} from '../types/ViewField';
|
||||
|
||||
export function useUpdateEntityField() {
|
||||
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
|
||||
|
||||
const [updateEntity] = useUpdateEntityMutation();
|
||||
|
||||
return function updatePeopleField<
|
||||
MetadataType extends ViewFieldMetadata,
|
||||
ValueType extends MetadataType extends ViewFieldDoubleTextMetadata
|
||||
? ViewFieldDoubleTextValue
|
||||
: MetadataType extends ViewFieldTextMetadata
|
||||
? ViewFieldTextValue
|
||||
: MetadataType extends ViewFieldPhoneMetadata
|
||||
? ViewFieldPhoneValue
|
||||
: MetadataType extends ViewFieldURLMetadata
|
||||
? ViewFieldURLValue
|
||||
: MetadataType extends ViewFieldNumberMetadata
|
||||
? ViewFieldNumberValue
|
||||
: MetadataType extends ViewFieldDateMetadata
|
||||
? ViewFieldDateValue
|
||||
: MetadataType extends ViewFieldChipMetadata
|
||||
? ViewFieldChipValue
|
||||
: MetadataType extends ViewFieldDoubleTextChipMetadata
|
||||
? ViewFieldDoubleTextChipValue
|
||||
: MetadataType extends ViewFieldRelationMetadata
|
||||
? ViewFieldRelationValue
|
||||
: unknown,
|
||||
>(
|
||||
currentEntityId: string,
|
||||
viewField: ViewFieldDefinition<MetadataType>,
|
||||
newFieldValue: ValueType,
|
||||
) {
|
||||
const newFieldValueUnknown = newFieldValue as unknown;
|
||||
// TODO: improve type guards organization, maybe with a common typeguard for all view fields
|
||||
// taking an object of options as parameter ?
|
||||
//
|
||||
// The goal would be to check that the view field value not only is valid,
|
||||
// but also that it is validated against the corresponding view field type
|
||||
|
||||
// Relation
|
||||
if (
|
||||
isViewFieldRelation(viewField) &&
|
||||
isViewFieldRelationValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newSelectedEntity = newFieldValueUnknown;
|
||||
|
||||
const fieldName = viewField.metadata.fieldName;
|
||||
|
||||
if (!newSelectedEntity) {
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[fieldName]: {
|
||||
disconnect: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[fieldName]: {
|
||||
connect: { id: newSelectedEntity.id },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// Chip
|
||||
} else if (
|
||||
isViewFieldChip(viewField) &&
|
||||
isViewFieldChipValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.contentFieldName]: newContent },
|
||||
},
|
||||
});
|
||||
// Text
|
||||
} else if (
|
||||
isViewFieldText(viewField) &&
|
||||
isViewFieldTextValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
// Double text
|
||||
} else if (
|
||||
isViewFieldDoubleText(viewField) &&
|
||||
isViewFieldDoubleTextValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
|
||||
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Double Text Chip
|
||||
} else if (
|
||||
isViewFieldDoubleTextChip(viewField) &&
|
||||
isViewFieldDoubleTextChipValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: {
|
||||
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
|
||||
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Phone
|
||||
} else if (
|
||||
isViewFieldPhone(viewField) &&
|
||||
isViewFieldPhoneValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
// URL
|
||||
} else if (
|
||||
isViewFieldURL(viewField) &&
|
||||
isViewFieldURLValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
// Number
|
||||
} else if (
|
||||
isViewFieldNumber(viewField) &&
|
||||
isViewFieldNumberValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
// Date
|
||||
} else if (
|
||||
isViewFieldDate(viewField) &&
|
||||
isViewFieldDateValue(newFieldValueUnknown)
|
||||
) {
|
||||
const newContent = newFieldValueUnknown;
|
||||
|
||||
updateEntity({
|
||||
variables: {
|
||||
where: { id: currentEntityId },
|
||||
data: { [viewField.metadata.fieldName]: newContent },
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { ViewFieldDefinition } from '../types/ViewField';
|
||||
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
||||
|
||||
export const ViewFieldContext =
|
||||
createContext<ViewFieldDefinition<unknown> | null>(null);
|
||||
createContext<ViewFieldDefinition<ViewFieldMetadata> | null>(null);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { ViewFieldDefinition } from '../types/ViewField';
|
||||
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
|
||||
|
||||
export const viewFieldsState = atom<ViewFieldDefinition<unknown>[]>({
|
||||
key: 'viewFieldsState',
|
||||
export const viewFieldsFamilyState = atom<
|
||||
ViewFieldDefinition<ViewFieldMetadata>[]
|
||||
>({
|
||||
key: 'viewFieldsFamilyState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
@ -1,36 +1,114 @@
|
||||
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
|
||||
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
|
||||
|
||||
export type ViewFieldType = 'text' | 'relation' | 'chip';
|
||||
export type ViewFieldType =
|
||||
| 'text'
|
||||
| 'relation'
|
||||
| 'chip'
|
||||
| 'double-text-chip'
|
||||
| 'double-text'
|
||||
| 'number'
|
||||
| 'date'
|
||||
| 'phone'
|
||||
| 'url';
|
||||
|
||||
export type ViewFieldTextMetadata = {
|
||||
type: 'text';
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldPhoneMetadata = {
|
||||
type: 'phone';
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldURLMetadata = {
|
||||
type: 'url';
|
||||
placeHolder: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldDateMetadata = {
|
||||
type: 'date';
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldNumberMetadata = {
|
||||
type: 'number';
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldRelationMetadata = {
|
||||
type: 'relation';
|
||||
relationType: Entity;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type ViewFieldChipMetadata = {
|
||||
type: 'chip';
|
||||
relationType: Entity;
|
||||
contentFieldName: string;
|
||||
urlFieldName: string;
|
||||
placeHolder: string;
|
||||
};
|
||||
|
||||
export type ViewFieldDefinition<
|
||||
T extends
|
||||
| ViewFieldTextMetadata
|
||||
| ViewFieldRelationMetadata
|
||||
| ViewFieldChipMetadata
|
||||
| unknown,
|
||||
> = {
|
||||
export type ViewFieldDoubleTextMetadata = {
|
||||
type: 'double-text';
|
||||
firstValueFieldName: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValueFieldName: string;
|
||||
secondValuePlaceholder: string;
|
||||
};
|
||||
|
||||
export type ViewFieldDoubleTextChipMetadata = {
|
||||
type: 'double-text-chip';
|
||||
firstValueFieldName: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValueFieldName: string;
|
||||
secondValuePlaceholder: string;
|
||||
entityType: Entity;
|
||||
};
|
||||
|
||||
export type ViewFieldMetadata = { type: ViewFieldType } & (
|
||||
| ViewFieldTextMetadata
|
||||
| ViewFieldRelationMetadata
|
||||
| ViewFieldChipMetadata
|
||||
| ViewFieldDoubleTextChipMetadata
|
||||
| ViewFieldDoubleTextMetadata
|
||||
| ViewFieldPhoneMetadata
|
||||
| ViewFieldURLMetadata
|
||||
| ViewFieldNumberMetadata
|
||||
| ViewFieldDateMetadata
|
||||
);
|
||||
|
||||
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
|
||||
id: string;
|
||||
columnLabel: string;
|
||||
columnSize: number;
|
||||
columnOrder: number;
|
||||
columnIcon?: JSX.Element;
|
||||
filterIcon?: JSX.Element;
|
||||
type: ViewFieldType;
|
||||
metadata: T;
|
||||
};
|
||||
|
||||
export type ViewFieldTextValue = string;
|
||||
|
||||
export type ViewFieldChipValue = string;
|
||||
export type ViewFieldDateValue = string;
|
||||
export type ViewFieldPhoneValue = string;
|
||||
export type ViewFieldURLValue = string;
|
||||
export type ViewFieldNumberValue = number;
|
||||
|
||||
export type ViewFieldDoubleTextValue = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
};
|
||||
|
||||
export type ViewFieldDoubleTextChipValue = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
};
|
||||
|
||||
export type ViewFieldRelationValue = EntityForSelect | null;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../ViewField';
|
||||
import {
|
||||
ViewFieldChipMetadata,
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldChip(
|
||||
field: ViewFieldDefinition<unknown>,
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldChipMetadata> {
|
||||
return field.type === 'chip';
|
||||
return field.metadata.type === 'chip';
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldChipValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldChipValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldChipValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string'
|
||||
);
|
||||
}
|
||||
11
front/src/modules/ui/table/types/guards/isViewFieldDate.ts
Normal file
11
front/src/modules/ui/table/types/guards/isViewFieldDate.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDateMetadata,
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldDate(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldDateMetadata> {
|
||||
return field.metadata.type === 'date';
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldDateValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldDateValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldDateValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string'
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextMetadata,
|
||||
ViewFieldMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldDoubleText(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldDoubleTextMetadata> {
|
||||
return field.metadata.type === 'double-text';
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldDoubleTextChipMetadata,
|
||||
ViewFieldMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldDoubleTextChip(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldDoubleTextChipMetadata> {
|
||||
return field.metadata.type === 'double-text-chip';
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldDoubleTextChipValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldDoubleTextChipValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldDoubleTextChipValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'object'
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldDoubleTextValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldDoubleTextValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldDoubleTextValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'object'
|
||||
);
|
||||
}
|
||||
11
front/src/modules/ui/table/types/guards/isViewFieldNumber.ts
Normal file
11
front/src/modules/ui/table/types/guards/isViewFieldNumber.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldNumberMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldNumber(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldNumberMetadata> {
|
||||
return field.metadata.type === 'number';
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldNumberValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldNumberValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldNumberValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'number'
|
||||
);
|
||||
}
|
||||
11
front/src/modules/ui/table/types/guards/isViewFieldPhone.ts
Normal file
11
front/src/modules/ui/table/types/guards/isViewFieldPhone.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldPhoneMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldPhone(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldPhoneMetadata> {
|
||||
return field.metadata.type === 'phone';
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldPhoneValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldPhoneValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldPhoneValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string'
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
import { ViewFieldDefinition, ViewFieldRelationMetadata } from '../ViewField';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldRelationMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldRelation(
|
||||
field: ViewFieldDefinition<unknown>,
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldRelationMetadata> {
|
||||
return field.type === 'relation';
|
||||
return field.metadata.type === 'relation';
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldRelationValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldRelationValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldRelationValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'object'
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
import { ViewFieldDefinition, ViewFieldTextMetadata } from '../ViewField';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldText(
|
||||
field: ViewFieldDefinition<unknown>,
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldTextMetadata> {
|
||||
return field.type === 'text';
|
||||
return field.metadata.type === 'text';
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldTextValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldTextValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldTextValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string'
|
||||
);
|
||||
}
|
||||
11
front/src/modules/ui/table/types/guards/isViewFieldURL.ts
Normal file
11
front/src/modules/ui/table/types/guards/isViewFieldURL.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldURLMetadata,
|
||||
} from '../ViewField';
|
||||
|
||||
export function isViewFieldURL(
|
||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
): field is ViewFieldDefinition<ViewFieldURLMetadata> {
|
||||
return field.metadata.type === 'url';
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { ViewFieldURLValue } from '../ViewField';
|
||||
|
||||
// TODO: add yup
|
||||
export function isViewFieldURLValue(
|
||||
fieldValue: unknown,
|
||||
): fieldValue is ViewFieldURLValue {
|
||||
return (
|
||||
fieldValue !== null &&
|
||||
fieldValue !== undefined &&
|
||||
typeof fieldValue === 'string'
|
||||
);
|
||||
}
|
||||
@ -8,7 +8,7 @@ import { useSearchUserQuery } from '~/generated/graphql';
|
||||
|
||||
export type OwnProps = {
|
||||
userId: string;
|
||||
onSubmit: (newUserId: string) => void;
|
||||
onSubmit: (newUser: EntityForSelect | null) => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
@ -23,7 +23,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) {
|
||||
|
||||
const users = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchUserQuery,
|
||||
selectedIds: [userId],
|
||||
selectedIds: userId ? [userId] : [],
|
||||
searchFilter: searchFilter,
|
||||
mappingFunction: (user) => ({
|
||||
entityType: Entity.User,
|
||||
@ -39,7 +39,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) {
|
||||
async function handleEntitySelected(
|
||||
selectedUser: UserForSelect | null | undefined,
|
||||
) {
|
||||
onSubmit(selectedUser?.id ?? '');
|
||||
onSubmit(selectedUser ?? null);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -34,11 +34,11 @@ export const FilterByName: Story = {
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const nameFilterButton = canvas
|
||||
.queryAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
return item.textContent === 'Name';
|
||||
});
|
||||
const nameFilterButton = (
|
||||
await canvas.findAllByTestId('dropdown-menu-item')
|
||||
).find((item) => {
|
||||
return item.textContent === 'Name';
|
||||
});
|
||||
|
||||
assert(nameFilterButton);
|
||||
|
||||
@ -49,7 +49,7 @@ export const FilterByName: Story = {
|
||||
delay: 200,
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
await sleep(50);
|
||||
|
||||
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Aircall')).toBeInTheDocument();
|
||||
@ -88,11 +88,11 @@ export const FilterByAccountOwner: Story = {
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
const charlesChip = canvas
|
||||
.getAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
return item.textContent?.includes('Charles Test');
|
||||
});
|
||||
const charlesChip = (
|
||||
await canvas.findAllByTestId('dropdown-menu-item')
|
||||
).find((item) => {
|
||||
return item.textContent?.includes('Charles Test');
|
||||
});
|
||||
|
||||
assert(charlesChip);
|
||||
|
||||
|
||||
@ -34,22 +34,23 @@ export const Email: Story = {
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const emailFilterButton = canvas
|
||||
.getAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
return item.textContent?.includes('Email');
|
||||
});
|
||||
const emailFilterButton = (
|
||||
await canvas.findAllByTestId('dropdown-menu-item')
|
||||
).find((item) => {
|
||||
return item.textContent?.includes('Email');
|
||||
});
|
||||
|
||||
assert(emailFilterButton);
|
||||
|
||||
await userEvent.click(emailFilterButton);
|
||||
|
||||
const emailInput = canvas.getByPlaceholderText('Email');
|
||||
|
||||
await userEvent.type(emailInput, 'al', {
|
||||
delay: 200,
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
await sleep(50);
|
||||
|
||||
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
|
||||
await expect(canvas.queryAllByText('John Doe')).toStrictEqual([]);
|
||||
@ -68,11 +69,11 @@ export const CompanyName: Story = {
|
||||
const filterButton = await canvas.findByText('Filter');
|
||||
await userEvent.click(filterButton);
|
||||
|
||||
const companyFilterButton = canvas
|
||||
.getAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
return item.textContent?.includes('Company');
|
||||
});
|
||||
const companyFilterButton = (
|
||||
await canvas.findAllByTestId('dropdown-menu-item')
|
||||
).find((item) => {
|
||||
return item.textContent?.includes('Company');
|
||||
});
|
||||
|
||||
assert(companyFilterButton);
|
||||
|
||||
@ -85,11 +86,11 @@ export const CompanyName: Story = {
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const qontoChip = canvas
|
||||
.getAllByTestId('dropdown-menu-item')
|
||||
.find((item) => {
|
||||
const qontoChip = (await canvas.findAllByTestId('dropdown-menu-item')).find(
|
||||
(item) => {
|
||||
return item.textContent?.includes('Qonto');
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(qontoChip).toBeInTheDocument();
|
||||
|
||||
|
||||
@ -191,7 +191,7 @@ export const EditRelation: Story = {
|
||||
|
||||
await step('Click on second row company cell', async () => {
|
||||
const secondRowCompanyCell = await canvas.findByText(
|
||||
mockedPeopleData[1].company.name,
|
||||
mockedPeopleData[2].company.name,
|
||||
);
|
||||
|
||||
await userEvent.click(
|
||||
@ -262,11 +262,24 @@ export const SelectRelationWithKeys: Story = {
|
||||
});
|
||||
|
||||
await userEvent.type(relationInput, '{arrowdown}');
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await userEvent.type(relationInput, '{arrowup}');
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await userEvent.type(relationInput, '{arrowdown}');
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await userEvent.type(relationInput, '{arrowdown}');
|
||||
|
||||
await sleep(50);
|
||||
|
||||
await userEvent.type(relationInput, '{enter}');
|
||||
sleep(25);
|
||||
|
||||
await sleep(50);
|
||||
|
||||
const allAirbns = await canvas.findAllByText('Aircall');
|
||||
expect(allAirbns.length).toBe(1);
|
||||
|
||||
Reference in New Issue
Block a user