Feat/company card fields (#686)
* wip * Ok * asd * Fixed cancel submit * Renamed * Fixed
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
import { PersonChip } from '@/people/components/PersonChip';
|
import { PersonChip } from '@/people/components/PersonChip';
|
||||||
import { RelationPickerHotkeyScope } from '@/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
|
import { EditableCell } from '@/ui/components/editable-cell/EditableCell';
|
||||||
|
import { useEditableCell } from '@/ui/components/editable-cell/hooks/useEditableCell';
|
||||||
import { Company, User } from '~/generated/graphql';
|
import { Company, User } from '~/generated/graphql';
|
||||||
|
|
||||||
import { CompanyAccountOwnerPicker } from './CompanyAccountOwnerPicker';
|
import { CompanyAccountOwnerPicker } from './CompanyAccountOwnerPicker';
|
||||||
@ -14,10 +15,26 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function CompanyAccountOwnerCell({ company }: OwnProps) {
|
export function CompanyAccountOwnerCell({ company }: OwnProps) {
|
||||||
|
const { closeEditableCell } = useEditableCell();
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
closeEditableCell();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
closeEditableCell();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableCell
|
<EditableCell
|
||||||
editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }}
|
editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }}
|
||||||
editModeContent={<CompanyAccountOwnerPicker company={company} />}
|
editModeContent={
|
||||||
|
<CompanyAccountOwnerPicker
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
company={company}
|
||||||
|
/>
|
||||||
|
}
|
||||||
nonEditModeContent={
|
nonEditModeContent={
|
||||||
company.accountOwner?.displayName ? (
|
company.accountOwner?.displayName ? (
|
||||||
<PersonChip
|
<PersonChip
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/
|
|||||||
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
|
||||||
import { Entity } from '@/relation-picker/types/EntityTypeForSelect';
|
import { Entity } from '@/relation-picker/types/EntityTypeForSelect';
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
import { useEditableCell } from '@/ui/components/editable-cell/hooks/useEditableCell';
|
|
||||||
import {
|
import {
|
||||||
Company,
|
Company,
|
||||||
User,
|
User,
|
||||||
@ -16,20 +15,24 @@ export type OwnProps = {
|
|||||||
company: Pick<Company, 'id'> & {
|
company: Pick<Company, 'id'> & {
|
||||||
accountOwner?: Pick<User, 'id' | 'displayName'> | null;
|
accountOwner?: Pick<User, 'id' | 'displayName'> | null;
|
||||||
};
|
};
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserForSelect = EntityForSelect & {
|
type UserForSelect = EntityForSelect & {
|
||||||
entityType: Entity.User;
|
entityType: Entity.User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CompanyAccountOwnerPicker({ company }: OwnProps) {
|
export function CompanyAccountOwnerPicker({
|
||||||
|
company,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: OwnProps) {
|
||||||
const [searchFilter] = useRecoilScopedState(
|
const [searchFilter] = useRecoilScopedState(
|
||||||
relationPickerSearchFilterScopedState,
|
relationPickerSearchFilterScopedState,
|
||||||
);
|
);
|
||||||
const [updateCompany] = useUpdateCompanyMutation();
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
const { closeEditableCell } = useEditableCell();
|
|
||||||
|
|
||||||
const companies = useFilteredSearchEntityQuery({
|
const companies = useFilteredSearchEntityQuery({
|
||||||
queryHook: useSearchUserQuery,
|
queryHook: useSearchUserQuery,
|
||||||
selectedIds: [company?.accountOwner?.id ?? ''],
|
selectedIds: [company?.accountOwner?.id ?? ''],
|
||||||
@ -52,12 +55,13 @@ export function CompanyAccountOwnerPicker({ company }: OwnProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
closeEditableCell();
|
onSubmit?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleEntitySelect
|
<SingleEntitySelect
|
||||||
onEntitySelected={handleEntitySelected}
|
onEntitySelected={handleEntitySelected}
|
||||||
|
onCancel={onCancel}
|
||||||
entities={{
|
entities={{
|
||||||
loading: companies.loading,
|
loading: companies.loading,
|
||||||
entitiesToSelect: companies.entitiesToSelect,
|
entitiesToSelect: companies.entitiesToSelect,
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { IconUserCircle } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { PersonChip } from '@/people/components/PersonChip';
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { RelationPickerHotkeyScope } from '@/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { Company, User } from '~/generated/graphql';
|
||||||
|
import { PageHotkeyScope } from '~/sync-hooks/types/PageHotkeyScope';
|
||||||
|
|
||||||
|
import { CompanyAccountOwnerPickerFieldEditMode } from './CompanyAccountOwnerPickerFieldEditMode';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'accountOwnerId'> & {
|
||||||
|
accountOwner?: Pick<User, 'id' | 'displayName'> | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyAccountOwnerEditableField({ company }: OwnProps) {
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<RecoilScope>
|
||||||
|
<EditableField
|
||||||
|
customEditHotkeyScope={{
|
||||||
|
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||||
|
}}
|
||||||
|
parentHotkeyScope={{
|
||||||
|
scope: PageHotkeyScope.CompanyShowPage,
|
||||||
|
}}
|
||||||
|
iconLabel={<IconUserCircle />}
|
||||||
|
editModeContent={
|
||||||
|
<CompanyAccountOwnerPickerFieldEditMode
|
||||||
|
parentHotkeyScope={{
|
||||||
|
scope: PageHotkeyScope.CompanyShowPage,
|
||||||
|
}}
|
||||||
|
company={company}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={
|
||||||
|
company.accountOwner?.displayName ? (
|
||||||
|
<PersonChip
|
||||||
|
id={company.accountOwner.id}
|
||||||
|
name={company.accountOwner?.displayName ?? ''}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { CompanyAccountOwnerPicker } from '@/companies/components/CompanyAccountOwnerPicker';
|
||||||
|
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
|
||||||
|
import { useEditableField } from '@/ui/editable-fields/hooks/useEditableField';
|
||||||
|
import { Company, User } from '~/generated/graphql';
|
||||||
|
|
||||||
|
const CompanyAccountOwnerPickerContainer = styled.div`
|
||||||
|
left: 24px;
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
company: Pick<Company, 'id'> & {
|
||||||
|
accountOwner?: Pick<User, 'id' | 'displayName'> | null;
|
||||||
|
};
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
parentHotkeyScope?: HotkeyScope;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyAccountOwnerPickerFieldEditMode({
|
||||||
|
company,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
parentHotkeyScope,
|
||||||
|
}: OwnProps) {
|
||||||
|
const { closeEditableField } = useEditableField(parentHotkeyScope);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
closeEditableField();
|
||||||
|
onSubmit?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
closeEditableField();
|
||||||
|
onCancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CompanyAccountOwnerPickerContainer>
|
||||||
|
<CompanyAccountOwnerPicker
|
||||||
|
company={company}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</CompanyAccountOwnerPickerContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IconMap } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { InplaceInputText } from '@/ui/inplace-inputs/components/InplaceInputText';
|
||||||
|
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'address'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyAddressEditableField({ company }: OwnProps) {
|
||||||
|
const [internalValue, setInternalValue] = useState(company.address);
|
||||||
|
|
||||||
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(company.address);
|
||||||
|
}, [company.address]);
|
||||||
|
|
||||||
|
async function handleChange(newValue: string) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
id: company.id,
|
||||||
|
address: internalValue ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
setInternalValue(company.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<EditableField
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
iconLabel={<IconMap />}
|
||||||
|
editModeContent={
|
||||||
|
<InplaceInputText
|
||||||
|
placeholder={'Address'}
|
||||||
|
autoFocus
|
||||||
|
value={internalValue}
|
||||||
|
onChange={(newValue: string) => {
|
||||||
|
handleChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={internalValue ?? ''}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IconCalendar } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { EditableFieldEditModeDate } from '@/ui/editable-fields/variants/components/EditableFieldEditModeDate';
|
||||||
|
import { parseDate } from '@/utils/datetime/date-utils';
|
||||||
|
import { formatToHumanReadableDate } from '@/utils/utils';
|
||||||
|
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'createdAt'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyCreatedAtEditableField({ company }: OwnProps) {
|
||||||
|
const [internalValue, setInternalValue] = useState(company.createdAt);
|
||||||
|
|
||||||
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(company.createdAt);
|
||||||
|
}, [company.createdAt]);
|
||||||
|
|
||||||
|
async function handleChange(newValue: string) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
id: company.id,
|
||||||
|
createdAt: internalValue ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
setInternalValue(company.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<EditableField
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
iconLabel={<IconCalendar />}
|
||||||
|
editModeContent={
|
||||||
|
<EditableFieldEditModeDate
|
||||||
|
value={internalValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={
|
||||||
|
internalValue !== ''
|
||||||
|
? formatToHumanReadableDate(parseDate(internalValue).toJSDate())
|
||||||
|
: 'No date'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IconLink } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldDisplayURL } from '@/ui/editable-fields/components/FieldDisplayURL';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { InplaceInputText } from '@/ui/inplace-inputs/components/InplaceInputText';
|
||||||
|
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'domainName'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyDomainNameEditableField({ company }: OwnProps) {
|
||||||
|
const [internalValue, setInternalValue] = useState(company.domainName);
|
||||||
|
|
||||||
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(company.domainName);
|
||||||
|
}, [company.domainName]);
|
||||||
|
|
||||||
|
async function handleChange(newValue: string) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
id: company.id,
|
||||||
|
domainName: internalValue ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
setInternalValue(company.domainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<EditableField
|
||||||
|
iconLabel={<IconLink />}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
editModeContent={
|
||||||
|
<InplaceInputText
|
||||||
|
placeholder={'URL'}
|
||||||
|
autoFocus
|
||||||
|
value={internalValue}
|
||||||
|
onChange={(newValue: string) => {
|
||||||
|
handleChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={<FieldDisplayURL URL={internalValue} />}
|
||||||
|
useEditButton
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IconUsers } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { InplaceInputText } from '@/ui/inplace-inputs/components/InplaceInputText';
|
||||||
|
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'employees'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyEmployeesEditableField({ company }: OwnProps) {
|
||||||
|
const [internalValue, setInternalValue] = useState(
|
||||||
|
company.employees?.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(company.employees?.toString());
|
||||||
|
}, [company.employees]);
|
||||||
|
|
||||||
|
async function handleChange(newValue: string) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!internalValue) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const numberValue = parseInt(internalValue);
|
||||||
|
|
||||||
|
if (isNaN(numberValue)) {
|
||||||
|
throw new Error('Not a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
id: company.id,
|
||||||
|
employees: numberValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setInternalValue(numberValue.toString());
|
||||||
|
} catch {
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
setInternalValue(company.employees?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<EditableField
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
iconLabel={<IconUsers />}
|
||||||
|
editModeContent={
|
||||||
|
<InplaceInputText
|
||||||
|
placeholder={'Employees'}
|
||||||
|
autoFocus
|
||||||
|
value={internalValue ?? ''}
|
||||||
|
onChange={(newValue: string) => {
|
||||||
|
handleChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={internalValue}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
type SelectStringKeys<T> = NonNullable<
|
export type SelectStringKeys<T> = NonNullable<
|
||||||
{
|
{
|
||||||
[K in keyof T]: K extends '__typename'
|
[K in keyof T]: K extends '__typename'
|
||||||
? never
|
? never
|
||||||
@ -20,7 +20,7 @@ type SelectStringKeys<T> = NonNullable<
|
|||||||
}[keyof T]
|
}[keyof T]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type ExtractEntityTypeFromQueryResponse<T> = T extends {
|
export type ExtractEntityTypeFromQueryResponse<T> = T extends {
|
||||||
searchResults: Array<infer U>;
|
searchResults: Array<infer U>;
|
||||||
}
|
}
|
||||||
? U
|
? U
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { InplaceInputDateEditMode } from '@/ui/inplace-inputs/components/InplaceInputDateEditMode';
|
import { InplaceInputDate } from '@/ui/inplace-inputs/components/InplaceInputDate';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
value: Date;
|
value: Date;
|
||||||
@ -13,5 +13,5 @@ export function BoardCardEditableFieldDateEditMode({
|
|||||||
onChange(newDate);
|
onChange(newDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <InplaceInputDateEditMode value={value} onChange={handleDateChange} />;
|
return <InplaceInputDate value={value} onChange={handleDateChange} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,11 +13,11 @@ export type ButtonProps = {
|
|||||||
|
|
||||||
const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${({ theme, variant, disabled }) => {
|
background: ${({ theme, variant }) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'shadow':
|
case 'shadow':
|
||||||
case 'white':
|
case 'white':
|
||||||
return theme.background.transparent.lighter;
|
return theme.background.transparent.primary;
|
||||||
case 'transparent':
|
case 'transparent':
|
||||||
case 'border':
|
case 'border':
|
||||||
default:
|
default:
|
||||||
@ -35,10 +35,10 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
|||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
transition: background 0.1s ease;
|
|
||||||
border-radius: ${({ theme }) => {
|
border-radius: ${({ theme }) => {
|
||||||
return theme.border.radius.sm;
|
return theme.border.radius.sm;
|
||||||
}};
|
}};
|
||||||
|
border-style: solid;
|
||||||
border-width: ${({ variant }) => {
|
border-width: ${({ variant }) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'border':
|
case 'border':
|
||||||
@ -50,6 +50,17 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
|
box-shadow: ${({ theme, variant }) => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'shadow':
|
||||||
|
return theme.boxShadow.light;
|
||||||
|
case 'border':
|
||||||
|
case 'white':
|
||||||
|
case 'transparent':
|
||||||
|
default:
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
}};
|
||||||
color: ${({ theme, disabled }) => {
|
color: ${({ theme, disabled }) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return theme.font.color.extraLight;
|
return theme.font.color.extraLight;
|
||||||
@ -57,8 +68,9 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
|||||||
|
|
||||||
return theme.font.color.tertiary;
|
return theme.font.color.tertiary;
|
||||||
}};
|
}};
|
||||||
border-style: solid;
|
|
||||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
height: ${({ size }) => {
|
height: ${({ size }) => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'large':
|
case 'large':
|
||||||
@ -70,9 +82,15 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
|||||||
return '20px';
|
return '20px';
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
transition: background 0.1s ease;
|
||||||
|
user-select: none;
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme, disabled }) => {
|
||||||
|
return disabled ? 'auto' : theme.background.transparent.light;
|
||||||
|
}};
|
||||||
|
}
|
||||||
width: ${({ size }) => {
|
width: ${({ size }) => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'large':
|
case 'large':
|
||||||
@ -84,17 +102,11 @@ const StyledIconButton = styled.button<Pick<ButtonProps, 'variant' | 'size'>>`
|
|||||||
return '20px';
|
return '20px';
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
flex-shrink: 0;
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme, disabled }) => {
|
|
||||||
return disabled ? 'auto' : theme.background.transparent.light;
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
user-select: none;
|
|
||||||
&:active {
|
&:active {
|
||||||
background: ${({ theme, disabled }) => {
|
background: ${({ theme, disabled }) => {
|
||||||
return disabled ? 'auto' : theme.background.transparent.medium;
|
return disabled ? 'auto' : theme.background.transparent.medium;
|
||||||
}};
|
}};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function IconButton({
|
export function IconButton({
|
||||||
|
|||||||
@ -12,14 +12,16 @@ export const EditableCellEditModeContainer = styled.div<OwnProps>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
left: ${(props) =>
|
left: ${(props) =>
|
||||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
||||||
margin-left: -2px;
|
margin-left: -1px;
|
||||||
min-height: 100%;
|
margin-top: -1px;
|
||||||
min-width: calc(100% + 20px);
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
position: absolute;
|
||||||
right: ${(props) =>
|
right: ${(props) =>
|
||||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||||
|
|
||||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||||
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
${overlayBackground}
|
${overlayBackground}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||||
import { InplaceInputDateEditMode } from '@/ui/inplace-inputs/components/InplaceInputDateEditMode';
|
import { InplaceInputDate } from '@/ui/inplace-inputs/components/InplaceInputDate';
|
||||||
import { TableHotkeyScope } from '@/ui/tables/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/ui/tables/types/TableHotkeyScope';
|
||||||
|
|
||||||
import { useEditableCell } from '../hooks/useEditableCell';
|
import { useEditableCell } from '../hooks/useEditableCell';
|
||||||
|
|
||||||
|
const EditableCellDateEditModeContainer = styled.div`
|
||||||
|
margin-top: -1px;
|
||||||
|
width: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
export type EditableDateProps = {
|
export type EditableDateProps = {
|
||||||
value: Date;
|
value: Date;
|
||||||
onChange: (date: Date) => void;
|
onChange: (date: Date) => void;
|
||||||
@ -31,5 +37,9 @@ export function EditableCellDateEditMode({
|
|||||||
[closeEditableCell],
|
[closeEditableCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <InplaceInputDateEditMode onChange={handleDateChange} value={value} />;
|
return (
|
||||||
|
<EditableCellDateEditModeContainer>
|
||||||
|
<InplaceInputDate onChange={handleDateChange} value={value} />
|
||||||
|
</EditableCellDateEditModeContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const StyledClickable = styled.div`
|
|||||||
a {
|
a {
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ const StyledClickable = styled.div`
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,12 @@ const StyledPropertyBoxContainer = styled.div`
|
|||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface PropertyBoxProps {
|
interface PropertyBoxProps {
|
||||||
children: JSX.Element;
|
children: React.ReactNode;
|
||||||
extraPadding?: boolean;
|
extraPadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,133 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
|
||||||
|
|
||||||
|
import { useEditableField } from '../hooks/useEditableField';
|
||||||
|
|
||||||
|
import { EditableFieldDisplayMode } from './EditableFieldDisplayMode';
|
||||||
|
import { EditableFieldEditButton } from './EditableFieldEditButton';
|
||||||
|
import { EditableFieldEditMode } from './EditableFieldEditMode';
|
||||||
|
|
||||||
|
const StyledIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabelAndIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: ${({ labelFixedWidth }) =>
|
||||||
|
labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EditableFieldBaseContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
height: 24px;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
iconLabel?: React.ReactNode;
|
||||||
|
label?: string;
|
||||||
|
labelFixedWidth?: number;
|
||||||
|
useEditButton?: boolean;
|
||||||
|
editModeContent: React.ReactNode;
|
||||||
|
displayModeContent: React.ReactNode;
|
||||||
|
parentHotkeyScope?: HotkeyScope;
|
||||||
|
customEditHotkeyScope?: HotkeyScope;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditableField({
|
||||||
|
iconLabel,
|
||||||
|
label,
|
||||||
|
labelFixedWidth,
|
||||||
|
useEditButton,
|
||||||
|
editModeContent,
|
||||||
|
displayModeContent,
|
||||||
|
parentHotkeyScope,
|
||||||
|
customEditHotkeyScope,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
}: OwnProps) {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
function handleContainerMouseEnter() {
|
||||||
|
setIsHovered(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContainerMouseLeave() {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isFieldInEditMode, openEditableField } =
|
||||||
|
useEditableField(parentHotkeyScope);
|
||||||
|
|
||||||
|
function handleDisplayModeClick() {
|
||||||
|
openEditableField(customEditHotkeyScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showEditButton = !isFieldInEditMode && isHovered && useEditButton;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditableFieldBaseContainer
|
||||||
|
onMouseEnter={handleContainerMouseEnter}
|
||||||
|
onMouseLeave={handleContainerMouseLeave}
|
||||||
|
>
|
||||||
|
<StyledLabelAndIconContainer>
|
||||||
|
{iconLabel && <StyledIconContainer>{iconLabel}</StyledIconContainer>}
|
||||||
|
{label && (
|
||||||
|
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
|
||||||
|
)}
|
||||||
|
</StyledLabelAndIconContainer>
|
||||||
|
{isFieldInEditMode ? (
|
||||||
|
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
|
||||||
|
{editModeContent}
|
||||||
|
</EditableFieldEditMode>
|
||||||
|
) : (
|
||||||
|
<EditableFieldDisplayMode
|
||||||
|
disableClick={useEditButton}
|
||||||
|
onClick={handleDisplayModeClick}
|
||||||
|
>
|
||||||
|
{displayModeContent}
|
||||||
|
</EditableFieldDisplayMode>
|
||||||
|
)}
|
||||||
|
{showEditButton && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
whileHover={{ scale: 1.04 }}
|
||||||
|
>
|
||||||
|
<EditableFieldEditButton customHotkeyScope={customEditHotkeyScope} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</EditableFieldBaseContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const EditableFieldNormalModeOuterContainer = styled.div<
|
||||||
|
Pick<OwnProps, 'disableClick'>
|
||||||
|
>`
|
||||||
|
align-items: center;
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
${(props) => {
|
||||||
|
if (props.disableClick) {
|
||||||
|
return css`
|
||||||
|
cursor: default;
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return css`
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${props.theme.background.transparent.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EditableFieldNormalModeInnerContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
disableClick?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditableFieldDisplayMode({
|
||||||
|
children,
|
||||||
|
disableClick,
|
||||||
|
onClick,
|
||||||
|
}: React.PropsWithChildren<OwnProps>) {
|
||||||
|
return (
|
||||||
|
<EditableFieldNormalModeOuterContainer
|
||||||
|
onClick={disableClick ? undefined : onClick}
|
||||||
|
disableClick={disableClick}
|
||||||
|
>
|
||||||
|
<EditableFieldNormalModeInnerContainer>
|
||||||
|
{children}
|
||||||
|
</EditableFieldNormalModeInnerContainer>
|
||||||
|
</EditableFieldNormalModeOuterContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconPencil } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
|
||||||
|
import { IconButton } from '@/ui/components/buttons/IconButton';
|
||||||
|
import { overlayBackground } from '@/ui/themes/effects';
|
||||||
|
|
||||||
|
import { useEditableField } from '../hooks/useEditableField';
|
||||||
|
|
||||||
|
export const StyledEditableFieldEditButton = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
height: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-left: -2px;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
${overlayBackground}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
customHotkeyScope?: HotkeyScope;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditableFieldEditButton({ customHotkeyScope }: OwnProps) {
|
||||||
|
const { openEditableField } = useEditableField();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
openEditableField(customHotkeyScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
variant="shadow"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClick}
|
||||||
|
icon={<IconPencil size={14} />}
|
||||||
|
data-testid="editable-field-edit-mode-container"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
|
||||||
|
|
||||||
|
export const EditableFieldEditModeContainer = styled.div<OwnProps>`
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
margin-left: -${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
width: inherit;
|
||||||
|
z-index: 10;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onOutsideClick?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditableFieldEditMode({
|
||||||
|
children,
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
}: OwnProps) {
|
||||||
|
const wrapperRef = useRef(null);
|
||||||
|
|
||||||
|
useRegisterCloseFieldHandlers(wrapperRef, onSubmit, onCancel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditableFieldEditModeContainer
|
||||||
|
data-testid="editable-field-edit-mode-container"
|
||||||
|
ref={wrapperRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</EditableFieldEditModeContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IconMap } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||||
|
import { EditableField } from '@/ui/editable-fields/components/EditableField';
|
||||||
|
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
|
||||||
|
import { InplaceInputText } from '@/ui/inplace-inputs/components/InplaceInputText';
|
||||||
|
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
company: Pick<Company, 'id' | 'address'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CompanyEditableFieldAddress({ company }: OwnProps) {
|
||||||
|
const [internalValue, setInternalValue] = useState(company.address);
|
||||||
|
|
||||||
|
const [updateCompany] = useUpdateCompanyMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(company.address);
|
||||||
|
}, [company.address]);
|
||||||
|
|
||||||
|
async function handleChange(newValue: string) {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await updateCompany({
|
||||||
|
variables: {
|
||||||
|
id: company.id,
|
||||||
|
address: internalValue ?? '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
setInternalValue(company.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RecoilScope SpecificContext={FieldContext}>
|
||||||
|
<EditableField
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
iconLabel={<IconMap />}
|
||||||
|
editModeContent={
|
||||||
|
<InplaceInputText
|
||||||
|
placeholder={'Address'}
|
||||||
|
autoFocus
|
||||||
|
value={internalValue}
|
||||||
|
onChange={(newValue: string) => {
|
||||||
|
handleChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
displayModeContent={internalValue !== '' ? internalValue : 'No address'}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { RawLink } from '@/ui/components/links/RawLink';
|
||||||
|
|
||||||
|
export function FieldDisplayURL({ URL }: { URL: string | undefined }) {
|
||||||
|
return <RawLink href={URL ? 'https://' + URL : ''}>{URL}</RawLink>;
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
|
||||||
|
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
|
||||||
|
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
|
import { FieldContext } from '../states/FieldContext';
|
||||||
|
import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState';
|
||||||
|
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||||
|
|
||||||
|
// TODO: use atoms for hotkey scopes
|
||||||
|
export function useEditableField(parentHotkeyScope?: HotkeyScope) {
|
||||||
|
const [isFieldInEditMode, setIsFieldInEditMode] = useRecoilScopedState(
|
||||||
|
isFieldInEditModeScopedState,
|
||||||
|
FieldContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
function closeEditableField() {
|
||||||
|
setIsFieldInEditMode(false);
|
||||||
|
|
||||||
|
if (parentHotkeyScope) {
|
||||||
|
setHotkeyScope(parentHotkeyScope.scope, parentHotkeyScope.customScopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditableField(customHotkeyScope?: HotkeyScope) {
|
||||||
|
setIsFieldInEditMode(true);
|
||||||
|
|
||||||
|
if (customHotkeyScope) {
|
||||||
|
setHotkeyScope(customHotkeyScope.scope, customHotkeyScope.customScopes);
|
||||||
|
} else {
|
||||||
|
setHotkeyScope(EditableFieldHotkeyScope.EditableField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFieldInEditMode,
|
||||||
|
closeEditableField,
|
||||||
|
openEditableField,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
|
||||||
|
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||||
|
|
||||||
|
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||||
|
|
||||||
|
import { useEditableField } from './useEditableField';
|
||||||
|
|
||||||
|
export function useRegisterCloseFieldHandlers(
|
||||||
|
wrapperRef: React.RefObject<HTMLDivElement>,
|
||||||
|
onSubmit?: () => void,
|
||||||
|
onCancel?: () => void,
|
||||||
|
) {
|
||||||
|
const { closeEditableField, isFieldInEditMode } = useEditableField();
|
||||||
|
|
||||||
|
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||||
|
if (isFieldInEditMode) {
|
||||||
|
onSubmit?.();
|
||||||
|
closeEditableField();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'enter',
|
||||||
|
() => {
|
||||||
|
onSubmit?.();
|
||||||
|
closeEditableField();
|
||||||
|
},
|
||||||
|
EditableFieldHotkeyScope.EditableField,
|
||||||
|
[closeEditableField, onSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
'esc',
|
||||||
|
() => {
|
||||||
|
closeEditableField();
|
||||||
|
onCancel?.();
|
||||||
|
},
|
||||||
|
EditableFieldHotkeyScope.EditableField,
|
||||||
|
[closeEditableField, onCancel],
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const FieldContext = createContext<string | null>(null);
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const isFieldInEditModeScopedState = atomFamily<boolean, string>({
|
||||||
|
key: 'isFieldInEditModeScopedState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export enum EditableFieldHotkeyScope {
|
||||||
|
EditableField = 'editable-field',
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
|
||||||
|
import { InplaceInputDate } from '@/ui/inplace-inputs/components/InplaceInputDate';
|
||||||
|
import { parseDate } from '@/utils/datetime/date-utils';
|
||||||
|
|
||||||
|
import { useEditableField } from '../../hooks/useEditableField';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
value: string;
|
||||||
|
onChange?: (newValue: string) => void;
|
||||||
|
parentHotkeyScope?: HotkeyScope;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EditableFieldEditModeDate({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
parentHotkeyScope,
|
||||||
|
}: OwnProps) {
|
||||||
|
const { closeEditableField } = useEditableField(parentHotkeyScope);
|
||||||
|
|
||||||
|
function handleChange(newValue: string) {
|
||||||
|
onChange?.(newValue);
|
||||||
|
closeEditableField();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InplaceInputDate
|
||||||
|
value={parseDate(value).toJSDate()}
|
||||||
|
onChange={(newDate: Date) => {
|
||||||
|
handleChange(newDate.toISOString());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { overlayBackground } from '@/ui/themes/effects';
|
||||||
|
|
||||||
|
export const InplaceInputContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
margin-left: -1px;
|
||||||
|
min-height: 32px;
|
||||||
|
width: inherit;
|
||||||
|
|
||||||
|
${overlayBackground}
|
||||||
|
|
||||||
|
z-index: 10;
|
||||||
|
`;
|
||||||
@ -2,36 +2,40 @@ import { forwardRef } from 'react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import DatePicker from '@/ui/components/form/DatePicker';
|
import DatePicker from '@/ui/components/form/DatePicker';
|
||||||
import { humanReadableDate } from '@/utils/utils';
|
import { formatToHumanReadableDate } from '@/utils/utils';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
import { InplaceInputContainer } from './InplaceInputContainer';
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
margin: 0px ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type StyledCalendarContainerProps = {
|
export type StyledCalendarContainerProps = {
|
||||||
editModeHorizontalAlign?: 'left' | 'right';
|
editModeHorizontalAlign?: 'left' | 'right';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledInputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
|
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||||
left: -10px;
|
|
||||||
|
margin-top: 1px;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type DivProps = React.HTMLProps<HTMLDivElement>;
|
type DivProps = React.HTMLProps<HTMLDivElement>;
|
||||||
|
|
||||||
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
export const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
||||||
({ value, onClick }, ref) => (
|
({ value, onClick }, ref) => (
|
||||||
<div onClick={onClick} ref={ref}>
|
<StyledInputContainer onClick={onClick} ref={ref}>
|
||||||
{value && humanReadableDate(new Date(value as string))}
|
{value && formatToHumanReadableDate(new Date(value as string))}
|
||||||
</div>
|
</StyledInputContainer>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,7 +43,7 @@ type DatePickerContainerProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
|
export const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
|
||||||
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
|
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,15 +52,15 @@ type OwnProps = {
|
|||||||
onChange: (newDate: Date) => void;
|
onChange: (newDate: Date) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InplaceInputDateEditMode({ onChange, value }: OwnProps) {
|
export function InplaceInputDate({ onChange, value }: OwnProps) {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<InplaceInputContainer>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
date={value}
|
date={value}
|
||||||
onChangeHandler={onChange}
|
onChangeHandler={onChange}
|
||||||
customInput={<DateDisplay />}
|
customInput={<DateDisplay />}
|
||||||
customCalendarContainer={DatePickerContainer}
|
customCalendarContainer={DatePickerContainer}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</InplaceInputContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { humanReadableDate } from '@/utils/utils';
|
import { formatToHumanReadableDate } from '@/utils/utils';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
value: Date;
|
value: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InplaceInputDateDisplayMode({ value }: OwnProps) {
|
export function InplaceInputDateDisplayMode({ value }: OwnProps) {
|
||||||
return <div>{value && humanReadableDate(value)}</div>;
|
return <div>{value && formatToHumanReadableDate(value)}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { textInputStyle } from '@/ui/themes/effects';
|
||||||
|
|
||||||
|
import { InplaceInputContainer } from './InplaceInputContainer';
|
||||||
|
|
||||||
|
export const InplaceInputTextInput = styled.input`
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
${textInputStyle}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
placeholder?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (newValue: string) => void;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InplaceInputText({
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
autoFocus,
|
||||||
|
}: OwnProps) {
|
||||||
|
return (
|
||||||
|
<InplaceInputContainer>
|
||||||
|
<InplaceInputTextInput
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InplaceInputContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -9,11 +9,15 @@ export const overlayBackground = (props: { theme: ThemeType }) =>
|
|||||||
box-shadow: ${props.theme.boxShadow.strong};
|
box-shadow: ${props.theme.boxShadow.strong};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const textInputStyle = (props: any) =>
|
export const textInputStyle = (props: { theme: ThemeType }) =>
|
||||||
css`
|
css`
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: ${props.theme.font.color.primary};
|
color: ${props.theme.font.color.primary};
|
||||||
|
font-family: ${props.theme.font.family};
|
||||||
|
font-size: ${props.theme.font.size.md};
|
||||||
|
|
||||||
|
font-weight: ${props.theme.font.weight.regular};
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
|
padding: ${props.theme.spacing(0)} ${props.theme.spacing(2)};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
export const humanReadableDate = (date: Date) => {
|
import { parseDate } from './datetime/date-utils';
|
||||||
|
|
||||||
|
export function formatToHumanReadableDate(date: Date | string) {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}).format(date);
|
}).format(parsedJSDate);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getLogoUrlFromDomainName = (domainName?: string): string => {
|
export const getLogoUrlFromDomainName = (domainName?: string): string => {
|
||||||
return `https://api.faviconkit.com/${domainName}/144`;
|
return `https://api.faviconkit.com/${domainName}/144`;
|
||||||
|
|||||||
@ -2,11 +2,14 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
import { Timeline } from '@/comments/components/timeline/Timeline';
|
import { Timeline } from '@/comments/components/timeline/Timeline';
|
||||||
|
import { CompanyAccountOwnerEditableField } from '@/companies/fields/components/CompanyAccountOwnerEditableField';
|
||||||
|
import { CompanyAddressEditableField } from '@/companies/fields/components/CompanyAddressEditableField';
|
||||||
|
import { CompanyCreatedAtEditableField } from '@/companies/fields/components/CompanyCreatedAtEditableField';
|
||||||
|
import { CompanyDomainNameEditableField } from '@/companies/fields/components/CompanyDomainNameEditableField';
|
||||||
|
import { CompanyEmployeesEditableField } from '@/companies/fields/components/CompanyEmployeesEditableField';
|
||||||
import { useCompanyQuery } from '@/companies/services';
|
import { useCompanyQuery } from '@/companies/services';
|
||||||
import { RawLink } from '@/ui/components/links/RawLink';
|
|
||||||
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
|
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
|
||||||
import { PropertyBoxItem } from '@/ui/components/property-box/PropertyBoxItem';
|
import { IconBuildingSkyscraper } from '@/ui/icons/index';
|
||||||
import { IconBuildingSkyscraper, IconLink, IconMap } from '@/ui/icons/index';
|
|
||||||
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
||||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/containers/ShowPageLeftContainer';
|
import { ShowPageLeftContainer } from '@/ui/layout/show-page/containers/ShowPageLeftContainer';
|
||||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/containers/ShowPageRightContainer';
|
import { ShowPageRightContainer } from '@/ui/layout/show-page/containers/ShowPageRightContainer';
|
||||||
@ -22,48 +25,33 @@ export function CompanyShow() {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (!company) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTopBarContainer
|
<WithTopBarContainer
|
||||||
title={company?.name ?? ''}
|
title={company?.name ?? ''}
|
||||||
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
||||||
>
|
>
|
||||||
<>
|
<ShowPageLeftContainer>
|
||||||
<ShowPageLeftContainer>
|
<ShowPageSummaryCard
|
||||||
<ShowPageSummaryCard
|
id={company?.id}
|
||||||
id={company?.id}
|
logoOrAvatar={getLogoUrlFromDomainName(company?.domainName ?? '')}
|
||||||
logoOrAvatar={getLogoUrlFromDomainName(company?.domainName ?? '')}
|
title={company?.name ?? 'No name'}
|
||||||
title={company?.name ?? 'No name'}
|
date={company?.createdAt ?? ''}
|
||||||
date={company?.createdAt ?? ''}
|
/>
|
||||||
/>
|
<PropertyBox extraPadding={true}>
|
||||||
<PropertyBox extraPadding={true}>
|
<CompanyDomainNameEditableField company={company} />
|
||||||
<>
|
<CompanyAccountOwnerEditableField company={company} />
|
||||||
<PropertyBoxItem
|
<CompanyEmployeesEditableField company={company} />
|
||||||
icon={<IconLink />}
|
<CompanyAddressEditableField company={company} />
|
||||||
value={
|
<CompanyCreatedAtEditableField company={company} />
|
||||||
<RawLink
|
</PropertyBox>
|
||||||
href={
|
</ShowPageLeftContainer>
|
||||||
company?.domainName
|
<ShowPageRightContainer>
|
||||||
? 'https://' + company?.domainName
|
<Timeline
|
||||||
: ''
|
entity={{ id: company?.id ?? '', type: CommentableType.Company }}
|
||||||
}
|
/>
|
||||||
>
|
</ShowPageRightContainer>
|
||||||
{company?.domainName}
|
|
||||||
</RawLink>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PropertyBoxItem
|
|
||||||
icon={<IconMap />}
|
|
||||||
value={company?.address ? company?.address : 'No address'}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</PropertyBox>
|
|
||||||
</ShowPageLeftContainer>
|
|
||||||
<ShowPageRightContainer>
|
|
||||||
<Timeline
|
|
||||||
entity={{ id: company?.id ?? '', type: CommentableType.Company }}
|
|
||||||
/>
|
|
||||||
</ShowPageRightContainer>
|
|
||||||
</>
|
|
||||||
</WithTopBarContainer>
|
</WithTopBarContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user