Feat/phone email link enhancements (#1172)
* feat: Add type guards for ViewField email values and definitions, update ViewFieldTypes & peopleViewFields * feat: use ContactLink for EditablePhoneCell & create EditableEmailCell & EmailInputDisplay comp * fix: set second value for field * enhance: add edit btn for phone cell * feat: install dependencies intl-tel-input * feat: add phone cell input & connect intl-tel-input * fix: resolve rebase errors * fix: remove placeholder * feat(storybook): create stories for EmailInputDisplay, PhoneInputDisplay, and PhoneEditableField components --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
@ -30,6 +30,7 @@
|
|||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"hex-rgb": "^5.0.0",
|
"hex-rgb": "^5.0.0",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
|
"intl-tel-input": "^18.2.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-levenshtein": "^1.1.6",
|
"js-levenshtein": "^1.1.6",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/apollo-upload-client": "^17.0.2",
|
"@types/apollo-upload-client": "^17.0.2",
|
||||||
"@types/deep-equal": "^1.0.1",
|
"@types/deep-equal": "^1.0.1",
|
||||||
|
"@types/intl-tel-input": "^18.1.1",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/js-cookie": "^3.0.3",
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { PhoneCellEdit } from '@/ui/table/editable-cell/type/components/PhoneCellEdit';
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof PhoneCellEdit> = {
|
||||||
|
title: 'Modules/People/EditableFields/PhoneEditableField',
|
||||||
|
component: PhoneCellEdit,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Story />
|
||||||
|
</BrowserRouter>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
value: '+33714446494',
|
||||||
|
autoFocus: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof PhoneCellEdit>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@ -2,6 +2,7 @@ import {
|
|||||||
ViewFieldDateMetadata,
|
ViewFieldDateMetadata,
|
||||||
ViewFieldDefinition,
|
ViewFieldDefinition,
|
||||||
ViewFieldDoubleTextChipMetadata,
|
ViewFieldDoubleTextChipMetadata,
|
||||||
|
ViewFieldEmailMetadata,
|
||||||
ViewFieldMetadata,
|
ViewFieldMetadata,
|
||||||
ViewFieldPhoneMetadata,
|
ViewFieldPhoneMetadata,
|
||||||
ViewFieldRelationMetadata,
|
ViewFieldRelationMetadata,
|
||||||
@ -45,11 +46,11 @@ export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
|
|||||||
columnSize: 150,
|
columnSize: 150,
|
||||||
columnOrder: 2,
|
columnOrder: 2,
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'text',
|
type: 'email',
|
||||||
fieldName: 'email',
|
fieldName: 'email',
|
||||||
placeHolder: 'Email', // Hack: Fake character to prevent password-manager from filling the field
|
placeHolder: 'Email', // Hack: Fake character to prevent password-manager from filling the field
|
||||||
},
|
},
|
||||||
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
|
} satisfies ViewFieldDefinition<ViewFieldEmailMetadata>,
|
||||||
{
|
{
|
||||||
id: 'company',
|
id: 'company',
|
||||||
columnLabel: 'Company',
|
columnLabel: 'Company',
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type FieldType =
|
|||||||
| 'double-text-chip'
|
| 'double-text-chip'
|
||||||
| 'double-text'
|
| 'double-text'
|
||||||
| 'number'
|
| 'number'
|
||||||
|
| 'email'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
| 'date'
|
| 'date'
|
||||||
| 'phone'
|
| 'phone'
|
||||||
@ -40,6 +41,11 @@ export type FieldNumberMetadata = {
|
|||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldEmailMetadata = {
|
||||||
|
fieldName: string;
|
||||||
|
placeHolder: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldRelationMetadata = {
|
export type FieldRelationMetadata = {
|
||||||
relationType: Entity;
|
relationType: Entity;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -82,6 +88,7 @@ export type FieldMetadata =
|
|||||||
| FieldPhoneMetadata
|
| FieldPhoneMetadata
|
||||||
| FieldURLMetadata
|
| FieldURLMetadata
|
||||||
| FieldNumberMetadata
|
| FieldNumberMetadata
|
||||||
|
| FieldEmailMetadata
|
||||||
| FieldDateMetadata
|
| FieldDateMetadata
|
||||||
| FieldProbabilityMetadata;
|
| FieldProbabilityMetadata;
|
||||||
|
|
||||||
@ -92,6 +99,7 @@ export type FieldDateValue = string;
|
|||||||
export type FieldPhoneValue = string;
|
export type FieldPhoneValue = string;
|
||||||
export type FieldURLValue = string;
|
export type FieldURLValue = string;
|
||||||
export type FieldNumberValue = number | null;
|
export type FieldNumberValue = number | null;
|
||||||
|
export type FieldEmailValue = string;
|
||||||
export type FieldProbabilityValue = number;
|
export type FieldProbabilityValue = number;
|
||||||
|
|
||||||
export type FieldDoubleTextValue = {
|
export type FieldDoubleTextValue = {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type ViewFieldType =
|
|||||||
| 'number'
|
| 'number'
|
||||||
| 'date'
|
| 'date'
|
||||||
| 'phone'
|
| 'phone'
|
||||||
|
| 'email'
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'probability'
|
| 'probability'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
@ -27,6 +28,12 @@ export type ViewFieldPhoneMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ViewFieldEmailMetadata = {
|
||||||
|
type: 'email';
|
||||||
|
placeHolder: string;
|
||||||
|
fieldName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ViewFieldURLMetadata = {
|
export type ViewFieldURLMetadata = {
|
||||||
type: 'url';
|
type: 'url';
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
@ -99,6 +106,7 @@ export type ViewFieldMetadata = { type: ViewFieldType } & (
|
|||||||
| ViewFieldDoubleTextChipMetadata
|
| ViewFieldDoubleTextChipMetadata
|
||||||
| ViewFieldDoubleTextMetadata
|
| ViewFieldDoubleTextMetadata
|
||||||
| ViewFieldPhoneMetadata
|
| ViewFieldPhoneMetadata
|
||||||
|
| ViewFieldEmailMetadata
|
||||||
| ViewFieldURLMetadata
|
| ViewFieldURLMetadata
|
||||||
| ViewFieldNumberMetadata
|
| ViewFieldNumberMetadata
|
||||||
| ViewFieldBooleanMetadata
|
| ViewFieldBooleanMetadata
|
||||||
@ -123,6 +131,7 @@ export type ViewFieldTextValue = string;
|
|||||||
export type ViewFieldChipValue = string;
|
export type ViewFieldChipValue = string;
|
||||||
export type ViewFieldDateValue = string;
|
export type ViewFieldDateValue = string;
|
||||||
export type ViewFieldPhoneValue = string;
|
export type ViewFieldPhoneValue = string;
|
||||||
|
export type ViewFieldEmailValue = string;
|
||||||
export type ViewFieldBooleanValue = boolean;
|
export type ViewFieldBooleanValue = boolean;
|
||||||
export type ViewFieldMoneyValue = number;
|
export type ViewFieldMoneyValue = number;
|
||||||
export type ViewFieldURLValue = string;
|
export type ViewFieldURLValue = string;
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
ViewFieldDefinition,
|
||||||
|
ViewFieldEmailMetadata,
|
||||||
|
ViewFieldMetadata,
|
||||||
|
} from '../ViewField';
|
||||||
|
|
||||||
|
export function isViewFieldEmail(
|
||||||
|
field: ViewFieldDefinition<ViewFieldMetadata>,
|
||||||
|
): field is ViewFieldDefinition<ViewFieldEmailMetadata> {
|
||||||
|
return field.metadata.type === 'email';
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { ViewFieldEmailValue } from '../ViewField';
|
||||||
|
|
||||||
|
export function isViewFieldEmailValue(
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is ViewFieldEmailValue {
|
||||||
|
return (
|
||||||
|
fieldValue !== null &&
|
||||||
|
fieldValue !== undefined &&
|
||||||
|
typeof fieldValue === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
import { ContactLink } from '@/ui/link/components/ContactLink';
|
||||||
|
|
||||||
|
function validateEmail(email: string) {
|
||||||
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailPattern.test(email.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
value: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EmailInputDisplay({ value }: OwnProps) {
|
||||||
|
return value && validateEmail(value) ? (
|
||||||
|
<ContactLink
|
||||||
|
href={`mailto:${value}`}
|
||||||
|
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</ContactLink>
|
||||||
|
) : (
|
||||||
|
<ContactLink href="#">{value}</ContactLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
|
import { EmailInputDisplay } from '../EmailInputDisplay';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Modules/People/EmailInputDisplay',
|
||||||
|
component: EmailInputDisplay,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StyledTestEmailContainer>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Story />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StyledTestEmailContainer>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
value: 'mustajab.ikram@google.com',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof EmailInputDisplay>;
|
||||||
|
|
||||||
|
const StyledTestEmailContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
export const Default: Story = {};
|
||||||
@ -1,18 +1,7 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
||||||
|
|
||||||
import { RawLink } from '@/ui/link/components/RawLink';
|
import { ContactLink } from '@/ui/link/components/ContactLink';
|
||||||
|
|
||||||
const StyledRawLink = styled(RawLink)`
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
a {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@ -20,15 +9,15 @@ type OwnProps = {
|
|||||||
|
|
||||||
export function PhoneInputDisplay({ value }: OwnProps) {
|
export function PhoneInputDisplay({ value }: OwnProps) {
|
||||||
return value && isValidPhoneNumber(value) ? (
|
return value && isValidPhoneNumber(value) ? (
|
||||||
<StyledRawLink
|
<ContactLink
|
||||||
href={parsePhoneNumber(value, 'FR')?.getURI()}
|
href={parsePhoneNumber(value, 'FR')?.getURI()}
|
||||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{parsePhoneNumber(value, 'FR')?.formatInternational() || value}
|
{parsePhoneNumber(value, 'FR')?.formatInternational() || value}
|
||||||
</StyledRawLink>
|
</ContactLink>
|
||||||
) : (
|
) : (
|
||||||
<StyledRawLink href="#">{value}</StyledRawLink>
|
<ContactLink href="#">{value}</ContactLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
|
import { PhoneInputDisplay } from '../PhoneInputDisplay'; // Adjust the import path as needed
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Modules/People/PhoneInputDisplay',
|
||||||
|
component: PhoneInputDisplay,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<StyledTestPhoneContainer>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Story />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StyledTestPhoneContainer>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
value: '+33788901234',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof PhoneInputDisplay>;
|
||||||
|
|
||||||
|
const StyledTestPhoneContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
38
front/src/modules/ui/link/components/ContactLink.tsx
Normal file
38
front/src/modules/ui/link/components/ContactLink.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Link as ReactLink } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
className?: string;
|
||||||
|
href: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledClickable = styled.div`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: ${({ theme }) => theme.border.color.strong};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration-color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function ContactLink({ className, href, children, onClick }: OwnProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StyledClickable className={className}>
|
||||||
|
<ReactLink target="_blank" onClick={onClick} to={href}>
|
||||||
|
{children}
|
||||||
|
</ReactLink>
|
||||||
|
</StyledClickable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
front/src/modules/ui/table/constants/countries.json
Normal file
52
front/src/modules/ui/table/constants/countries.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"ch": "Switzerland",
|
||||||
|
"de": "Germany",
|
||||||
|
"ca": "Canada",
|
||||||
|
"us": "US",
|
||||||
|
"se": "Sweden",
|
||||||
|
"jp": "Japan",
|
||||||
|
"au": "Australia",
|
||||||
|
"gb": "UK",
|
||||||
|
"fr": "France",
|
||||||
|
"dk": "Denmark",
|
||||||
|
"nz": "New Zealand",
|
||||||
|
"nl": "Netherlands",
|
||||||
|
"no": "Norway",
|
||||||
|
"it": "Italy",
|
||||||
|
"fi": "Finland",
|
||||||
|
"es": "Spain",
|
||||||
|
"cn": "China",
|
||||||
|
"be": "Belgium",
|
||||||
|
"sg": "Singapore",
|
||||||
|
"kr": "South Korea",
|
||||||
|
"ae": "UAE",
|
||||||
|
"at": "Austria",
|
||||||
|
"ie": "Ireland",
|
||||||
|
"lu": "Luxembourg",
|
||||||
|
"gr": "Greece",
|
||||||
|
"pt": "Portugal",
|
||||||
|
"br": "Brazil",
|
||||||
|
"th": "Thailand",
|
||||||
|
"qa": "Qatar",
|
||||||
|
"tr": "Turkey",
|
||||||
|
"in": "India",
|
||||||
|
"pl": "Poland",
|
||||||
|
"mx": "Mexico",
|
||||||
|
"sa": "Saudi Arabia",
|
||||||
|
"eg": "Egypt",
|
||||||
|
"ru": "Russia",
|
||||||
|
"il": "Israel",
|
||||||
|
"ar": "Argentina",
|
||||||
|
"my": "Malaysia",
|
||||||
|
"cr": "Costa Rica",
|
||||||
|
"id": "Indonesia",
|
||||||
|
"za": "South Africa",
|
||||||
|
"ma": "Morocco",
|
||||||
|
"cz": "Czechia",
|
||||||
|
"hr": "Croatia",
|
||||||
|
"ph": "Philippines",
|
||||||
|
"vn": "Vietnam",
|
||||||
|
"hu": "Hungary",
|
||||||
|
"cl": "Chile",
|
||||||
|
"pe": "Peru"
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { isViewFieldBoolean } from '@/ui/editable-field/types/guards/isViewField
|
|||||||
import { isViewFieldDate } from '@/ui/editable-field/types/guards/isViewFieldDate';
|
import { isViewFieldDate } from '@/ui/editable-field/types/guards/isViewFieldDate';
|
||||||
import { isViewFieldDoubleText } from '@/ui/editable-field/types/guards/isViewFieldDoubleText';
|
import { isViewFieldDoubleText } from '@/ui/editable-field/types/guards/isViewFieldDoubleText';
|
||||||
import { isViewFieldDoubleTextChip } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChip';
|
import { isViewFieldDoubleTextChip } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChip';
|
||||||
|
import { isViewFieldEmail } from '@/ui/editable-field/types/guards/isViewFieldEmail';
|
||||||
import { isViewFieldMoney } from '@/ui/editable-field/types/guards/isViewFieldMoney';
|
import { isViewFieldMoney } from '@/ui/editable-field/types/guards/isViewFieldMoney';
|
||||||
import { isViewFieldNumber } from '@/ui/editable-field/types/guards/isViewFieldNumber';
|
import { isViewFieldNumber } from '@/ui/editable-field/types/guards/isViewFieldNumber';
|
||||||
import { isViewFieldPhone } from '@/ui/editable-field/types/guards/isViewFieldPhone';
|
import { isViewFieldPhone } from '@/ui/editable-field/types/guards/isViewFieldPhone';
|
||||||
@ -19,6 +20,7 @@ import { GenericEditableChipCell } from '../type/components/GenericEditableChipC
|
|||||||
import { GenericEditableDateCell } from '../type/components/GenericEditableDateCell';
|
import { GenericEditableDateCell } from '../type/components/GenericEditableDateCell';
|
||||||
import { GenericEditableDoubleTextCell } from '../type/components/GenericEditableDoubleTextCell';
|
import { GenericEditableDoubleTextCell } from '../type/components/GenericEditableDoubleTextCell';
|
||||||
import { GenericEditableDoubleTextChipCell } from '../type/components/GenericEditableDoubleTextChipCell';
|
import { GenericEditableDoubleTextChipCell } from '../type/components/GenericEditableDoubleTextChipCell';
|
||||||
|
import { GenericEditableEmailCell } from '../type/components/GenericEditableEmailCell';
|
||||||
import { GenericEditableMoneyCell } from '../type/components/GenericEditableMoneyCell';
|
import { GenericEditableMoneyCell } from '../type/components/GenericEditableMoneyCell';
|
||||||
import { GenericEditableNumberCell } from '../type/components/GenericEditableNumberCell';
|
import { GenericEditableNumberCell } from '../type/components/GenericEditableNumberCell';
|
||||||
import { GenericEditablePhoneCell } from '../type/components/GenericEditablePhoneCell';
|
import { GenericEditablePhoneCell } from '../type/components/GenericEditablePhoneCell';
|
||||||
@ -31,7 +33,9 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function GenericEditableCell({ viewField: fieldDefinition }: OwnProps) {
|
export function GenericEditableCell({ viewField: fieldDefinition }: OwnProps) {
|
||||||
if (isViewFieldText(fieldDefinition)) {
|
if (isViewFieldEmail(fieldDefinition)) {
|
||||||
|
return <GenericEditableEmailCell viewField={fieldDefinition} />;
|
||||||
|
} else if (isViewFieldText(fieldDefinition)) {
|
||||||
return <GenericEditableTextCell viewField={fieldDefinition} />;
|
return <GenericEditableTextCell viewField={fieldDefinition} />;
|
||||||
} else if (isViewFieldRelation(fieldDefinition)) {
|
} else if (isViewFieldRelation(fieldDefinition)) {
|
||||||
return <GenericEditableRelationCell fieldDefinition={fieldDefinition} />;
|
return <GenericEditableRelationCell fieldDefinition={fieldDefinition} />;
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function GenericEditableDoubleTextCellEditMode({ viewField }: OwnProps) {
|
|||||||
const [secondValue, setSecondValue] = useRecoilState<string>(
|
const [secondValue, setSecondValue] = useRecoilState<string>(
|
||||||
tableEntityFieldFamilySelector({
|
tableEntityFieldFamilySelector({
|
||||||
entityId: currentRowEntityId ?? '',
|
entityId: currentRowEntityId ?? '',
|
||||||
fieldName: viewField.metadata.firstValueFieldName,
|
fieldName: viewField.metadata.secondValueFieldName,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ViewFieldDefinition,
|
||||||
|
ViewFieldEmailMetadata,
|
||||||
|
} from '@/ui/editable-field/types/ViewField';
|
||||||
|
import { EmailInputDisplay } from '@/ui/input/email/components/EmailInputDisplay';
|
||||||
|
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||||
|
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||||
|
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
||||||
|
|
||||||
|
import { GenericEditableEmailCellEditMode } from './GenericEditableEmailCellEditMode';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
viewField: ViewFieldDefinition<ViewFieldEmailMetadata>;
|
||||||
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GenericEditableEmailCell({
|
||||||
|
viewField,
|
||||||
|
editModeHorizontalAlign,
|
||||||
|
}: OwnProps) {
|
||||||
|
const currentRowEntityId = useCurrentRowEntityId();
|
||||||
|
|
||||||
|
const fieldValue = useRecoilValue<string>(
|
||||||
|
tableEntityFieldFamilySelector({
|
||||||
|
entityId: currentRowEntityId ?? '',
|
||||||
|
fieldName: viewField.metadata.fieldName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
|
editModeContent={
|
||||||
|
<GenericEditableEmailCellEditMode viewField={viewField} />
|
||||||
|
}
|
||||||
|
nonEditModeContent={<EmailInputDisplay value={fieldValue} />}
|
||||||
|
></EditableCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ViewFieldDefinition,
|
||||||
|
ViewFieldEmailMetadata,
|
||||||
|
} from '@/ui/editable-field/types/ViewField';
|
||||||
|
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||||
|
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||||
|
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
||||||
|
|
||||||
|
import { TextCellEdit } from './TextCellEdit';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
viewField: ViewFieldDefinition<ViewFieldEmailMetadata>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GenericEditableEmailCellEditMode({ 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(newEmail: string) {
|
||||||
|
if (newEmail === fieldValue) return;
|
||||||
|
|
||||||
|
setFieldValue(newEmail);
|
||||||
|
|
||||||
|
if (currentRowEntityId && updateField) {
|
||||||
|
updateField(currentRowEntityId, viewField, newEmail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextCellEdit
|
||||||
|
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||||
|
autoFocus
|
||||||
|
value={fieldValue ?? ''}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ export function GenericEditablePhoneCell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCell
|
<EditableCell
|
||||||
|
useEditButton
|
||||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||||
editModeContent={
|
editModeContent={
|
||||||
<GenericEditablePhoneCellEditMode viewField={viewField} />
|
<GenericEditablePhoneCellEditMode viewField={viewField} />
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
|||||||
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
||||||
|
|
||||||
import { TextCellEdit } from './TextCellEdit';
|
import { PhoneCellEdit } from './PhoneCellEdit';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
|
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
|
||||||
@ -38,7 +38,7 @@ export function GenericEditablePhoneCellEditMode({ viewField }: OwnProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextCellEdit
|
<PhoneCellEdit
|
||||||
placeholder={viewField.metadata.placeHolder ?? ''}
|
placeholder={viewField.metadata.placeHolder ?? ''}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={fieldValue ?? ''}
|
value={fieldValue ?? ''}
|
||||||
|
|||||||
@ -0,0 +1,130 @@
|
|||||||
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import intlTelInput from 'intl-tel-input';
|
||||||
|
|
||||||
|
import { hoverBackground } from '@/ui/theme/constants/effects';
|
||||||
|
|
||||||
|
import countries from '../../../constants/countries.json';
|
||||||
|
import { useRegisterCloseCellHandlers } from '../../hooks/useRegisterCloseCellHandlers';
|
||||||
|
|
||||||
|
import 'intl-tel-input/build/css/intlTelInput.css';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.iti__country-list {
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||||
|
|
||||||
|
.iti__country {
|
||||||
|
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
--vertical-padding: ${({ theme }) => theme.spacing(3)};
|
||||||
|
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
height: calc(32px - 2 * var(--vertical-padding));
|
||||||
|
|
||||||
|
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||||
|
|
||||||
|
${hoverBackground};
|
||||||
|
|
||||||
|
width: calc(100% - 2 * var(--horizontal-padding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iti__flag {
|
||||||
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
|
}
|
||||||
|
|
||||||
|
.iti__arrow {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInput = styled.input`
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
border: none;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
width: ${({ theme }) => theme.spacing(48)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
placeholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
value: string;
|
||||||
|
onSubmit: (newText: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PhoneCellEdit({ autoFocus, value, onSubmit }: OwnProps) {
|
||||||
|
const [internalText, setInternalText] = useState(value);
|
||||||
|
const phoneInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
onSubmit(internalText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
setInternalText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
setInternalText(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalText(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phoneInputRef.current) {
|
||||||
|
intlTelInput(phoneInputRef.current, {
|
||||||
|
utilsScript:
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/8.4.6/js/utils.js',
|
||||||
|
initialCountry: 'auto',
|
||||||
|
formatOnDisplay: true,
|
||||||
|
localizedCountries: countries,
|
||||||
|
onlyCountries: Object.keys(countries),
|
||||||
|
preferredCountries: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer ref={wrapperRef}>
|
||||||
|
<StyledInput
|
||||||
|
type="tel"
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
ref={phoneInputRef}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={internalText}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5378,6 +5378,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/intl-tel-input@^18.1.1":
|
||||||
|
version "18.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/intl-tel-input/-/intl-tel-input-18.1.1.tgz#33a1f4a3a97b02f34d16c227d9083e3d69e6b322"
|
||||||
|
integrity sha512-lxxF5QhR57Q/KEaA+52x1mb8f4tVgFXpKWC6VCod1EpL9w61D6kzW5srLcMQrv0/fCZ1UsSJATqFowLeUhxQXQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/jquery" "*"
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.4":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
||||||
@ -5413,6 +5420,13 @@
|
|||||||
jest-matcher-utils "^27.0.0"
|
jest-matcher-utils "^27.0.0"
|
||||||
pretty-format "^27.0.0"
|
pretty-format "^27.0.0"
|
||||||
|
|
||||||
|
"@types/jquery@*":
|
||||||
|
version "3.5.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.16.tgz#632131baf30951915b0317d48c98e9890bdf051d"
|
||||||
|
integrity sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==
|
||||||
|
dependencies:
|
||||||
|
"@types/sizzle" "*"
|
||||||
|
|
||||||
"@types/js-cookie@^3.0.3":
|
"@types/js-cookie@^3.0.3":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.3.tgz#d6bfbbdd0c187354ca555213d1962f6d0691ff4e"
|
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.3.tgz#d6bfbbdd0c187354ca555213d1962f6d0691ff4e"
|
||||||
@ -5663,6 +5677,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/sizzle@*":
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
|
||||||
|
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
|
||||||
|
|
||||||
"@types/sockjs@^0.3.33":
|
"@types/sockjs@^0.3.33":
|
||||||
version "0.3.33"
|
version "0.3.33"
|
||||||
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f"
|
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f"
|
||||||
@ -11453,6 +11472,16 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
|
interpret@^1.0.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
||||||
|
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
||||||
|
|
||||||
|
intl-tel-input@^18.2.1:
|
||||||
|
version "18.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/intl-tel-input/-/intl-tel-input-18.2.1.tgz#17de678f5ccfd156e4d125750cab4da8bd57fcbd"
|
||||||
|
integrity sha512-wOm0/61kTtpYjOOW1bhHzC4G8Om+atTxHmg31FS0KD0LQ8k8BpgO925npyi4jlT/EK4+joABABZzz0/XeSgupQ==
|
||||||
|
|
||||||
invariant@^2.2.2, invariant@^2.2.4:
|
invariant@^2.2.2, invariant@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||||
|
|||||||
Reference in New Issue
Block a user