Optimize table loading (#866)

* wip

* wip

* Ok

* Deleted unused code

* Fixed lint

* Minor fixes

* Minor fixes

* Minor Fixes

* Minor merge fixes

* Ok

* Fix storybook tests

* Removed console.log

* Fix login

* asd

* Fixed storybook

* Added await

* Fixed await

* Added sleep for failing test

* Fix sleep

* Fix test

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-25 20:00:15 +02:00
committed by GitHub
parent c2d6abde65
commit a2ccb643ff
85 changed files with 846 additions and 904 deletions

View File

@ -6,6 +6,7 @@ import { lightTheme, darkTheme } from '../src/modules/ui/themes/themes';
import { RootDecorator } from '../src/testing/decorators/RootDecorator'; import { RootDecorator } from '../src/testing/decorators/RootDecorator';
import 'react-loading-skeleton/dist/skeleton.css'; import 'react-loading-skeleton/dist/skeleton.css';
import { mockedUserJWT } from '../src/testing/mock-data/jwt'; import { mockedUserJWT } from '../src/testing/mock-data/jwt';
initialize(); initialize();
const preview: Preview = { const preview: Preview = {

View File

@ -17,6 +17,7 @@
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/react-modal": "^3.16.0", "@types/react-modal": "^3.16.0",
"afterframe": "^1.0.2",
"apollo-link-rest": "^0.9.0", "apollo-link-rest": "^0.9.0",
"apollo-upload-client": "^17.0.0", "apollo-upload-client": "^17.0.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",

View File

@ -1,11 +0,0 @@
import { useEffect, useRef } from 'react';
export default function usePrevious<T>(state: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = state;
});
return ref.current;
}

View File

@ -1,12 +1,10 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { HotkeysProvider } from 'react-hotkeys-hook';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { INITIAL_HOTKEYS_SCOPES } from '@/ui/hotkey/constants';
import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider'; import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider';
import { AppThemeProvider } from '@/ui/themes/components/AppThemeProvider'; import { AppThemeProvider } from '@/ui/themes/components/AppThemeProvider';
import { ThemeType } from '@/ui/themes/themes'; import { ThemeType } from '@/ui/themes/themes';
@ -14,6 +12,7 @@ import { UserProvider } from '@/users/components/UserProvider';
import '@emotion/react'; import '@emotion/react';
import { AuthAutoRouter } from './sync-hooks/AuthAutoRouter';
import { App } from './App'; import { App } from './App';
import './index.css'; import './index.css';
@ -26,23 +25,20 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<RecoilRoot> <RecoilRoot>
<ApolloProvider> <ApolloProvider>
<AppThemeProvider> <UserProvider>
<StrictMode> <ClientConfigProvider>
<BrowserRouter> <AppThemeProvider>
<UserProvider> <SnackBarProvider>
<SnackBarProvider> <BrowserRouter>
<ClientConfigProvider> <AuthAutoRouter />
<HotkeysProvider <StrictMode>
initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES} <App />
> </StrictMode>
<App /> </BrowserRouter>
</HotkeysProvider> </SnackBarProvider>
</ClientConfigProvider> </AppThemeProvider>
</SnackBarProvider> </ClientConfigProvider>
</UserProvider> </UserProvider>
</BrowserRouter>
</StrictMode>
</AppThemeProvider>
</ApolloProvider> </ApolloProvider>
</RecoilRoot>, </RecoilRoot>,
); );

View File

@ -1,26 +0,0 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import usePrevious from '~/hooks/usePrevious';
import { useEventTracker } from './useEventTracker';
export function useTrackPageView() {
const location = useLocation();
const previousLocation = usePrevious(location);
const eventTracker = useEventTracker();
useEffect(() => {
// Avoid lot of pageview events enven if the location is the same
if (
!previousLocation?.pathname ||
previousLocation?.pathname !== location.pathname
) {
eventTracker('pageview', {
location: {
pathname: location.pathname,
},
});
}
}, [location, eventTracker, previousLocation?.pathname]);
}

View File

@ -48,15 +48,19 @@ export function CompanyAccountOwnerPicker({
searchOnFields: ['firstName', 'lastName'], searchOnFields: ['firstName', 'lastName'],
}); });
async function handleEntitySelected(selectedUser: UserForSelect) { async function handleEntitySelected(
await updateCompany({ selectedUser: UserForSelect | null | undefined,
variables: { ) {
where: { id: company.id }, if (selectedUser) {
data: { await updateCompany({
accountOwner: { connect: { id: selectedUser.id } }, variables: {
where: { id: company.id },
data: {
accountOwner: { connect: { id: selectedUser.id } },
},
}, },
}, });
}); }
onSubmit?.(); onSubmit?.();
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { EditableCellChip } from '@/ui/table/editable-cell/types/EditableChip'; import { EditableCellChip } from '@/ui/table/editable-cell/types/EditableChip';
@ -22,17 +21,10 @@ type OwnProps = {
export function CompanyEditableNameChipCell({ company }: OwnProps) { export function CompanyEditableNameChipCell({ company }: OwnProps) {
const [updateCompany] = useUpdateOneCompanyMutation(); const [updateCompany] = useUpdateOneCompanyMutation();
const [internalValue, setInternalValue] = useState(company.name ?? '');
useEffect(() => {
setInternalValue(company.name ?? '');
}, [company.name]);
return ( return (
<EditableCellChip <EditableCellChip
value={internalValue} value={company.name}
placeholder="Name" placeholder="Name"
changeHandler={setInternalValue}
ChipComponent={ ChipComponent={
<CompanyChip <CompanyChip
id={company.id} id={company.id}
@ -40,18 +32,17 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
pictureUrl={getLogoUrlFromDomainName(company.domainName)} pictureUrl={getLogoUrlFromDomainName(company.domainName)}
/> />
} }
onSubmit={() => onSubmit={(newName) =>
updateCompany({ updateCompany({
variables: { variables: {
where: { id: company.id }, where: { id: company.id },
data: { data: {
name: internalValue, name: newName,
}, },
}, },
refetchQueries: [getOperationName(GET_COMPANY) ?? ''], refetchQueries: [getOperationName(GET_COMPANY) ?? ''],
}) })
} }
onCancel={() => setInternalValue(company.name ?? '')}
/> />
); );
} }

View File

@ -1,8 +1,4 @@
import { useRecoilState } from 'recoil';
import { defaultOrderBy } from '@/companies/queries'; import { defaultOrderBy } from '@/companies/queries';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { import {
PersonOrderByWithRelationInput, PersonOrderByWithRelationInput,
useGetCompaniesQuery, useGetCompaniesQuery,
@ -17,12 +13,6 @@ export function CompanyEntityTableData({
orderBy?: PersonOrderByWithRelationInput[]; orderBy?: PersonOrderByWithRelationInput[];
whereFilters?: any; whereFilters?: any;
}) { }) {
const [, setTableRowIds] = useRecoilState(tableRowIdsState);
const [, setIsFetchingEntityTableData] = useRecoilState(
isFetchingEntityTableDataState,
);
const setCompanyEntityTable = useSetCompanyEntityTable(); const setCompanyEntityTable = useSetCompanyEntityTable();
useGetCompaniesQuery({ useGetCompaniesQuery({
@ -30,19 +20,7 @@ export function CompanyEntityTableData({
onCompleted: (data) => { onCompleted: (data) => {
const companies = data.companies ?? []; const companies = data.companies ?? [];
const companyIds = companies.map((company) => company.id);
setTableRowIds((currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(companyIds)) {
return companyIds;
}
return currentRowIds;
});
setCompanyEntityTable(companies); setCompanyEntityTable(companies);
setIsFetchingEntityTableData(false);
}, },
}); });

View File

@ -1,37 +1,15 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useSetCompanyEntityTable } from '../hooks/useSetCompanyEntityTable'; import { useSetCompanyEntityTable } from '../hooks/useSetCompanyEntityTable';
import { mockedCompaniesData } from './companies-mock-data'; import { mockedCompaniesData } from './companies-mock-data';
export function CompanyEntityTableDataMocked() { export function CompanyEntityTableDataMocked() {
const [, setTableRowIds] = useRecoilState(tableRowIdsState);
const [, setIsFetchingEntityTableData] = useRecoilState(
isFetchingEntityTableDataState,
);
const setCompanyEntityTable = useSetCompanyEntityTable(); const setCompanyEntityTable = useSetCompanyEntityTable();
useEffect(() => { useEffect(() => {
const companyIds = mockedCompaniesData.map((company) => company.id);
setTableRowIds((currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(companyIds)) {
return companyIds;
}
return currentRowIds;
});
setCompanyEntityTable(mockedCompaniesData); setCompanyEntityTable(mockedCompaniesData);
}, [setCompanyEntityTable]);
setIsFetchingEntityTableData(false);
}, [setCompanyEntityTable, setIsFetchingEntityTableData, setTableRowIds]);
return <></>; return <></>;
} }

View File

@ -9,10 +9,8 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn
import { IconList } from '@/ui/icon'; import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { HooksEntityTable } from '@/ui/table/components/HooksEntityTable';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { CompanyOrderByWithRelationInput } from '~/generated/graphql'; import { CompanyOrderByWithRelationInput } from '~/generated/graphql';
import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts'; import { availableSorts } from '~/pages/companies/companies-sorts';
export function CompanyTable() { export function CompanyTable() {
@ -32,10 +30,6 @@ export function CompanyTable() {
return ( return (
<> <>
<CompanyEntityTableData orderBy={orderBy} whereFilters={whereFilters} /> <CompanyEntityTableData orderBy={orderBy} whereFilters={whereFilters} />
<HooksEntityTable
numberOfColumns={companyColumns.length}
availableFilters={companiesFilters}
/>
<EntityTable <EntityTable
columns={companyColumns} columns={companyColumns}
viewName="All Companies" viewName="All Companies"

View File

@ -2,18 +2,12 @@ import { companyColumns } from '@/companies/table/components/companyColumns';
import { CompanyEntityTableDataMocked } from '@/companies/table/components/CompanyEntityTableDataMocked'; import { CompanyEntityTableDataMocked } from '@/companies/table/components/CompanyEntityTableDataMocked';
import { IconList } from '@/ui/icon'; import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { HooksEntityTable } from '@/ui/table/components/HooksEntityTable';
import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts'; import { availableSorts } from '~/pages/companies/companies-sorts';
export function CompanyTableMockMode() { export function CompanyTableMockMode() {
return ( return (
<> <>
<CompanyEntityTableDataMocked /> <CompanyEntityTableDataMocked />
<HooksEntityTable
numberOfColumns={companyColumns.length}
availableFilters={companiesFilters}
/>
<EntityTable <EntityTable
columns={companyColumns} columns={companyColumns}
viewName="All Companies" viewName="All Companies"

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { companyDomainNameFamilyState } from '@/companies/states/companyDomainNameFamilyState'; import { companyDomainNameFamilyState } from '@/companies/states/companyDomainNameFamilyState';
@ -12,31 +11,25 @@ export function EditableCompanyDomainNameCell() {
const [updateCompany] = useUpdateOneCompanyMutation(); const [updateCompany] = useUpdateOneCompanyMutation();
const name = useRecoilValue( const domainName = useRecoilValue(
companyDomainNameFamilyState(currentRowEntityId ?? ''), companyDomainNameFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(name ?? '');
useEffect(() => {
setInternalValue(name ?? '');
}, [name]);
return ( return (
<EditableCellURL <EditableCellURL
url={internalValue} url={domainName ?? ''}
onChange={setInternalValue} onSubmit={(newURL) =>
onSubmit={() =>
updateCompany({ updateCompany({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
domainName: internalValue, domainName: newURL,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(name ?? '')}
/> />
); );
} }

View File

@ -1,3 +1,4 @@
import { useLocation } from 'react-router-dom';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { companyAccountOwnerFamilyState } from '@/companies/states/companyAccountOwnerFamilyState'; import { companyAccountOwnerFamilyState } from '@/companies/states/companyAccountOwnerFamilyState';
@ -10,7 +11,24 @@ import { companyLinkedinUrlFamilyState } from '@/companies/states/companyLinkedi
import { companyNameFamilyState } from '@/companies/states/companyNameFamilyState'; import { companyNameFamilyState } from '@/companies/states/companyNameFamilyState';
import { GetCompaniesQuery } from '~/generated/graphql'; import { GetCompaniesQuery } from '~/generated/graphql';
import { companiesFilters } from '../../../../pages/companies/companies-filters';
import { availableFiltersScopedState } from '../../../ui/filter-n-sort/states/availableFiltersScopedState';
import { useContextScopeId } from '../../../ui/recoil-scope/hooks/useContextScopeId';
import { currentPageLocationState } from '../../../ui/states/currentPageLocationState';
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 { companyColumns } from '../components/companyColumns';
export function useSetCompanyEntityTable() { export function useSetCompanyEntityTable() {
const resetTableRowSelection = useResetTableRowSelection();
const tableContextScopeId = useContextScopeId(TableContext);
const currentLocation = useLocation().pathname;
return useRecoilCallback( return useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
(newCompanyArray: GetCompaniesQuery['companies']) => { (newCompanyArray: GetCompaniesQuery['companies']) => {
@ -94,7 +112,30 @@ export function useSetCompanyEntityTable() {
set(companyCreatedAtFamilyState(company.id), company.createdAt); set(companyCreatedAtFamilyState(company.id), company.createdAt);
} }
} }
const companyIds = newCompanyArray.map((company) => company.id);
set(tableRowIdsState, (currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(companyIds)) {
return companyIds;
}
return currentRowIds;
});
resetTableRowSelection();
set(entityTableDimensionsState, {
numberOfColumns: companyColumns.length,
numberOfRows: companyIds.length,
});
set(availableFiltersScopedState(tableContextScopeId), companiesFilters);
set(currentPageLocationState, currentLocation);
set(isFetchingEntityTableDataState, false);
}, },
[], [resetTableRowSelection, tableContextScopeId, currentLocation],
); );
} }

View File

@ -19,8 +19,8 @@ type OwnProps = {
> >
| null | null
| undefined; | undefined;
onChange: (firstName: string, lastName: string) => void; onChange?: (firstName: string, lastName: string) => void;
onSubmit?: () => void; onSubmit?: (firstName: string, lastName: string) => void;
onCancel?: () => void; onCancel?: () => void;
}; };
@ -37,20 +37,12 @@ export function EditablePeopleFullName({
onSubmit, onSubmit,
onCancel, onCancel,
}: OwnProps) { }: OwnProps) {
function handleDoubleTextChange(
firstValue: string,
secondValue: string,
): void {
onChange(firstValue, secondValue);
}
return ( return (
<EditableCellDoubleText <EditableCellDoubleText
firstValue={person?.firstName ?? ''} firstValue={person?.firstName ?? ''}
secondValue={person?.lastName ?? ''} secondValue={person?.lastName ?? ''}
firstValuePlaceholder="First name" firstValuePlaceholder="First name"
secondValuePlaceholder="Last name" secondValuePlaceholder="Last name"
onChange={handleDoubleTextChange}
onSubmit={onSubmit} onSubmit={onSubmit}
onCancel={onCancel} onCancel={onCancel}
nonEditModeContent={ nonEditModeContent={

View File

@ -16,6 +16,8 @@ import {
useUpdateOnePersonMutation, useUpdateOnePersonMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { EntityForSelect } from '../../ui/relation-picker/types/EntityForSelect';
export type OwnProps = { export type OwnProps = {
people: Pick<Person, 'id'> & { company?: Pick<Company, 'id'> | null }; people: Pick<Person, 'id'> & { company?: Pick<Company, 'id'> | null };
}; };
@ -37,17 +39,21 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
selectedIds: people.company?.id ? [people.company.id] : [], selectedIds: people.company?.id ? [people.company.id] : [],
}); });
async function handleEntitySelected(entity: any) { async function handleEntitySelected(
await updatePerson({ entity: EntityForSelect | null | undefined,
variables: { ) {
where: { if (entity) {
id: people.id, await updatePerson({
variables: {
where: {
id: people.id,
},
data: {
company: { connect: { id: entity.id } },
},
}, },
data: { });
company: { connect: { id: entity.id } }, }
},
},
});
closeEditableCell(); closeEditableCell();
} }
@ -67,6 +73,7 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
return ( return (
<SingleEntitySelect <SingleEntitySelect
onCreate={handleCreate} onCreate={handleCreate}
onCancel={() => closeEditableCell()}
onEntitySelected={handleEntitySelected} onEntitySelected={handleEntitySelected}
entities={{ entities={{
entitiesToSelect: companies.entitiesToSelect, entitiesToSelect: companies.entitiesToSelect,

View File

@ -27,17 +27,22 @@ export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) {
selectedIds: people.company?.id ? [people.company.id] : [], selectedIds: people.company?.id ? [people.company.id] : [],
}); });
async function handleEntitySelected(entity: EntityForSelect) { async function handleEntitySelected(
await updatePerson({ entity: EntityForSelect | null | undefined,
variables: { ) {
where: { if (entity) {
id: people.id, await updatePerson({
variables: {
where: {
id: people.id,
},
data: {
company: { connect: { id: entity.id } },
},
}, },
data: { });
company: { connect: { id: entity.id } }, }
},
},
});
closeEditableField(); closeEditableField();
} }

View File

@ -1,7 +1,17 @@
import { useLocation } from 'react-router-dom';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { GetPeopleQuery } from '~/generated/graphql'; import { GetPeopleQuery } from '~/generated/graphql';
import { peopleFilters } from '../../../pages/people/people-filters';
import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState';
import { useContextScopeId } from '../../ui/recoil-scope/hooks/useContextScopeId';
import { currentPageLocationState } from '../../ui/states/currentPageLocationState';
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 { peopleCityFamilyState } from '../states/peopleCityFamilyState'; import { peopleCityFamilyState } from '../states/peopleCityFamilyState';
import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState'; import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState';
import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState'; import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState';
@ -10,8 +20,15 @@ import { peopleJobTitleFamilyState } from '../states/peopleJobTitleFamilyState';
import { peopleLinkedinUrlFamilyState } from '../states/peopleLinkedinUrlFamilyState'; import { peopleLinkedinUrlFamilyState } from '../states/peopleLinkedinUrlFamilyState';
import { peopleNameCellFamilyState } from '../states/peopleNamesFamilyState'; import { peopleNameCellFamilyState } from '../states/peopleNamesFamilyState';
import { peoplePhoneFamilyState } from '../states/peoplePhoneFamilyState'; import { peoplePhoneFamilyState } from '../states/peoplePhoneFamilyState';
import { peopleColumns } from '../table/components/peopleColumns';
export function useSetPeopleEntityTable() { export function useSetPeopleEntityTable() {
const resetTableRowSelection = useResetTableRowSelection();
const tableContextScopeId = useContextScopeId(TableContext);
const currentLocation = useLocation().pathname;
return useRecoilCallback( return useRecoilCallback(
({ set, snapshot }) => ({ set, snapshot }) =>
(newPeopleArray: GetPeopleQuery['people']) => { (newPeopleArray: GetPeopleQuery['people']) => {
@ -94,6 +111,29 @@ export function useSetPeopleEntityTable() {
}); });
} }
} }
const peopleIds = newPeopleArray.map((people) => people.id);
set(tableRowIdsState, (currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(peopleIds)) {
return peopleIds;
}
return currentRowIds;
});
resetTableRowSelection();
set(entityTableDimensionsState, {
numberOfColumns: peopleColumns.length,
numberOfRows: peopleIds.length,
});
set(availableFiltersScopedState(tableContextScopeId), peopleFilters);
set(currentPageLocationState, currentLocation);
set(isFetchingEntityTableDataState, false);
}, },
[], [],
); );

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { peopleCityFamilyState } from '@/people/states/peopleCityFamilyState'; import { peopleCityFamilyState } from '@/people/states/peopleCityFamilyState';
@ -13,29 +12,21 @@ export function EditablePeopleCityCell() {
const city = useRecoilValue(peopleCityFamilyState(currentRowEntityId ?? '')); const city = useRecoilValue(peopleCityFamilyState(currentRowEntityId ?? ''));
const [internalValue, setInternalValue] = useState(city ?? '');
useEffect(() => {
setInternalValue(city ?? '');
}, [city]);
return ( return (
<EditableCellText <EditableCellText
value={internalValue} value={city ?? ''}
onChange={setInternalValue} onSubmit={(newText) =>
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
city: internalValue, city: newText,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(city ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -18,45 +17,29 @@ export function EditablePeopleFullNameCell() {
peopleNameCellFamilyState(currentRowEntityId ?? ''), peopleNameCellFamilyState(currentRowEntityId ?? ''),
); );
const [internalFirstName, setInternalFirstName] = useState(firstName ?? '');
const [internalLastName, setInternalLastName] = useState(lastName ?? '');
useEffect(() => {
setInternalFirstName(firstName ?? '');
setInternalLastName(lastName ?? '');
}, [firstName, lastName]);
return ( return (
<EditablePeopleFullName <EditablePeopleFullName
person={{ person={{
id: currentRowEntityId ?? undefined, id: currentRowEntityId ?? undefined,
_commentThreadCount: commentCount ?? undefined, _commentThreadCount: commentCount ?? undefined,
firstName: internalFirstName, firstName,
lastName: internalLastName, lastName,
displayName: displayName ?? undefined, displayName: displayName ?? undefined,
}} }}
onChange={(firstName, lastName) => { onSubmit={(newFirstValue, newSecondValue) =>
setInternalFirstName(firstName);
setInternalLastName(lastName);
}}
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
firstName: internalFirstName, firstName: newFirstValue,
lastName: internalLastName, lastName: newSecondValue,
}, },
}, },
refetchQueries: [getOperationName(GET_PERSON) ?? ''], refetchQueries: [getOperationName(GET_PERSON) ?? ''],
}) })
} }
onCancel={() => {
setInternalFirstName(firstName ?? '');
setInternalLastName(lastName ?? '');
}}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { peopleJobTitleFamilyState } from '@/people/states/peopleJobTitleFamilyState'; import { peopleJobTitleFamilyState } from '@/people/states/peopleJobTitleFamilyState';
@ -15,29 +14,21 @@ export function EditablePeopleJobTitleCell() {
peopleJobTitleFamilyState(currentRowEntityId ?? ''), peopleJobTitleFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(jobTitle ?? '');
useEffect(() => {
setInternalValue(jobTitle ?? '');
}, [jobTitle]);
return ( return (
<EditableCellText <EditableCellText
value={internalValue} value={jobTitle ?? ''}
onChange={setInternalValue} onSubmit={(newText) =>
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
jobTitle: internalValue, jobTitle: newText,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(jobTitle ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { peopleLinkedinUrlFamilyState } from '@/people/states/peopleLinkedinUrlFamilyState'; import { peopleLinkedinUrlFamilyState } from '@/people/states/peopleLinkedinUrlFamilyState';
@ -16,29 +15,21 @@ export function EditablePeopleLinkedinUrlCell() {
peopleLinkedinUrlFamilyState(currentRowEntityId ?? ''), peopleLinkedinUrlFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(linkedinUrl ?? '');
useEffect(() => {
setInternalValue(linkedinUrl ?? '');
}, [linkedinUrl]);
return ( return (
<EditableCellURL <EditableCellURL
url={internalValue} url={linkedinUrl ?? ''}
onChange={setInternalValue} onSubmit={(newURL) =>
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
linkedinUrl: internalValue, linkedinUrl: newURL,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(linkedinUrl ?? '')}
/> />
); );
} }

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { peoplePhoneFamilyState } from '@/people/states/peoplePhoneFamilyState'; import { peoplePhoneFamilyState } from '@/people/states/peoplePhoneFamilyState';
@ -15,29 +14,21 @@ export function EditablePeoplePhoneCell() {
peoplePhoneFamilyState(currentRowEntityId ?? ''), peoplePhoneFamilyState(currentRowEntityId ?? ''),
); );
const [internalValue, setInternalValue] = useState(phone ?? '');
useEffect(() => {
setInternalValue(phone ?? '');
}, [phone]);
return ( return (
<EditableCellPhone <EditableCellPhone
value={internalValue} value={phone?.toString() ?? ''}
onChange={setInternalValue} onSubmit={(newPhone) =>
onSubmit={() =>
updatePerson({ updatePerson({
variables: { variables: {
where: { where: {
id: currentRowEntityId, id: currentRowEntityId,
}, },
data: { data: {
phone: internalValue, phone: newPhone,
}, },
}, },
}) })
} }
onCancel={() => setInternalValue(phone ?? '')}
/> />
); );
} }

View File

@ -10,10 +10,8 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn
import { IconList } from '@/ui/icon'; import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { HooksEntityTable } from '@/ui/table/components/HooksEntityTable';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { PersonOrderByWithRelationInput } from '~/generated/graphql'; import { PersonOrderByWithRelationInput } from '~/generated/graphql';
import { peopleFilters } from '~/pages/people/people-filters';
import { availableSorts } from '~/pages/people/people-sorts'; import { availableSorts } from '~/pages/people/people-sorts';
export function PeopleTable() { export function PeopleTable() {
@ -33,10 +31,6 @@ export function PeopleTable() {
return ( return (
<> <>
<PeopleEntityTableData orderBy={orderBy} whereFilters={whereFilters} /> <PeopleEntityTableData orderBy={orderBy} whereFilters={whereFilters} />
<HooksEntityTable
numberOfColumns={peopleColumns.length}
availableFilters={peopleFilters}
/>
<EntityTable <EntityTable
columns={peopleColumns} columns={peopleColumns}
viewName="All People" viewName="All People"

View File

@ -44,7 +44,13 @@ export function PipelineProgressPointOfContactPicker({
: [], : [],
}); });
async function handleEntitySelected(entity: EntityForSelect) { async function handleEntitySelected(
entity: EntityForSelect | null | undefined,
) {
if (!entity) {
return;
}
await updatePipelineProgress({ await updatePipelineProgress({
variables: { variables: {
...pipelineProgress, ...pipelineProgress,

View File

@ -1,8 +1,9 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useMatch, useResolvedPath } from 'react-router-dom'; import { useMatch, useNavigate, useResolvedPath } from 'react-router-dom';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useAuth } from '@/auth/hooks/useAuth'; import { useAuth } from '@/auth/hooks/useAuth';
import { AppPath } from '@/types/AppPath';
import { import {
IconColorSwatch, IconColorSwatch,
IconLogout, IconLogout,
@ -16,12 +17,14 @@ import SubMenuNavbar from '@/ui/navbar/components/SubMenuNavbar';
export function SettingsNavbar() { export function SettingsNavbar() {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate();
const { signOut } = useAuth(); const { signOut } = useAuth();
const handleLogout = useCallback(() => { const handleLogout = useCallback(() => {
signOut(); signOut();
}, [signOut]); navigate(AppPath.SignIn);
}, [signOut, navigate]);
return ( return (
<SubMenuNavbar backButtonTitle="Settings"> <SubMenuNavbar backButtonTitle="Settings">

View File

@ -1,7 +1,7 @@
import { ChangeEvent, useMemo, useState } from 'react'; import { ChangeEvent, useMemo, useState } from 'react';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode'; import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { debounce } from '~/utils/debounce'; import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField'; import { BoardCardEditableField } from './BoardCardEditableField';
@ -29,7 +29,7 @@ export function BoardCardEditableFieldText({
<BoardCardEditableField <BoardCardEditableField
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={ editModeContent={
<InplaceInputTextEditMode <StyledInput
placeholder={placeholder || ''} placeholder={placeholder || ''}
autoFocus autoFocus
value={internalValue} value={internalValue}

View File

@ -0,0 +1,42 @@
import { Profiler } from 'react';
import { Interaction } from 'scheduler/tracing';
type OwnProps = {
id: string;
children: React.ReactNode;
};
export function TimingProfiler({ id, children }: OwnProps) {
function handleRender(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
interactions: Set<Interaction>,
) {
console.debug(
'TimingProfiler',
JSON.stringify(
{
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
},
null,
2,
),
);
}
return (
<Profiler id={id} onRender={handleRender}>
{children}
</Profiler>
);
}

View File

@ -1,5 +1,4 @@
import { Context, useCallback, useState } from 'react'; import { Context, useCallback, useState } from 'react';
import { Key } from 'ts-key-enum';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState'; import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
@ -7,10 +6,8 @@ import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState'; import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState'; import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
@ -83,15 +80,6 @@ export function FilterDropdownButton({
} }
} }
useScopedHotkeys(
[Key.Escape],
() => {
handleIsUnfoldedChange(false);
},
RelationPickerHotkeyScope.RelationPicker,
[handleIsUnfoldedChange],
);
return ( return (
<DropdownButton <DropdownButton
label="Filter" label="Filter"

View File

@ -36,8 +36,14 @@ export function FilterDropdownEntitySearchSelect({
const filterCurrentlyEdited = useFilterCurrentlyEdited(context); const filterCurrentlyEdited = useFilterCurrentlyEdited(context);
function handleUserSelected(selectedEntity: EntityForSelect) { function handleUserSelected(
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) { selectedEntity: EntityForSelect | null | undefined,
) {
if (
!filterDefinitionUsedInDropdown ||
!selectedOperandInDropdown ||
!selectedEntity
) {
return; return;
} }

View File

@ -0,0 +1,12 @@
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { currentPageLocationState } from '../states/currentPageLocationState';
export function useIsPageLoading() {
const currentLocation = useLocation().pathname;
const currentPageLocation = useRecoilValue(currentPageLocationState);
return currentLocation !== currentPageLocation;
}

View File

@ -1,30 +0,0 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
import { AppHotkeyScope } from '../../types/AppHotkeyScope';
import { useHotkeyScopes } from './useHotkeyScopes';
export function useHotkeyScopeAutoSync() {
const { setHotkeyScopes } = useHotkeyScopes();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
useEffect(() => {
const scopesToSet: string[] = [];
if (currentHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (currentHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(currentHotkeyScope.scope);
setHotkeyScopes(scopesToSet);
}, [setHotkeyScopes, currentHotkeyScope]);
}

View File

@ -0,0 +1,40 @@
import { Hotkey } from 'react-hotkeys-hook/dist/types';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
export function useScopedHotkeyCallback() {
return useRecoilCallback(
({ snapshot }) =>
({
callback,
hotkeysEvent,
keyboardEvent,
scope,
preventDefault = true,
}: {
keyboardEvent: KeyboardEvent;
hotkeysEvent: Hotkey;
callback: (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey) => void;
scope: string;
preventDefault?: boolean;
}) => {
const currentHotkeyScopes = snapshot
.getLoadable(internalHotkeysEnabledScopesState)
.valueOrThrow();
if (!currentHotkeyScopes.includes(scope)) {
return;
}
if (preventDefault) {
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
keyboardEvent.stopImmediatePropagation();
}
return callback(keyboardEvent, hotkeysEvent);
},
[],
);
}

View File

@ -1,6 +1,5 @@
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { import {
Hotkey,
HotkeyCallback, HotkeyCallback,
Keys, Keys,
Options, Options,
@ -10,6 +9,8 @@ import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useScopedHotkeys( export function useScopedHotkeys(
keys: Keys, keys: Keys,
callback: HotkeyCallback, callback: HotkeyCallback,
@ -23,21 +24,29 @@ export function useScopedHotkeys(
) { ) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
function callbackIfDirectKey( const callScopedHotkeyCallback = useScopedHotkeyCallback();
keyboardEvent: KeyboardEvent,
hotkeysEvent: Hotkey,
) {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
}
return useHotkeys( return useHotkeys(
keys, keys,
callbackIfDirectKey, (keyboardEvent, hotkeysEvent) => {
{ ...options, scopes: [scope] }, callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
},
scope,
preventDefault: !!options.preventDefault,
});
},
{
enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
dependencies, dependencies,
); );
} }

View File

@ -4,10 +4,12 @@ import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
export function useSequenceHotkeys( export function useSequenceHotkeys(
firstKey: Keys, firstKey: Keys,
secondKey: Keys, secondKey: Keys,
callback: () => void, sequenceCallback: () => void,
scope: string, scope: string,
options: Options = { options: Options = {
enableOnContentEditable: true, enableOnContentEditable: true,
@ -18,25 +20,57 @@ export function useSequenceHotkeys(
) { ) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
const callScopedHotkeyCallback = useScopedHotkeyCallback();
useHotkeys( useHotkeys(
firstKey, firstKey,
() => { (keyboardEvent, hotkeysEvent) => {
setPendingHotkey(firstKey); callScopedHotkeyCallback({
keyboardEvent,
hotkeysEvent,
callback: () => {
setPendingHotkey(firstKey);
},
scope,
preventDefault: !!options.preventDefault,
});
}, },
{ ...options, scopes: [scope] }, {
[setPendingHotkey], enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[setPendingHotkey, scope],
); );
useHotkeys( useHotkeys(
secondKey, secondKey,
() => { (keyboardEvent, hotkeysEvent) => {
if (pendingHotkey !== firstKey) { callScopedHotkeyCallback({
return; keyboardEvent,
} hotkeysEvent,
setPendingHotkey(null); callback: () => {
callback(); if (pendingHotkey !== firstKey) {
return;
}
setPendingHotkey(null);
if (!!options.preventDefault) {
keyboardEvent.stopImmediatePropagation();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
sequenceCallback();
},
scope,
preventDefault: false,
});
}, },
{ ...options, scopes: [scope] }, {
[pendingHotkey, setPendingHotkey, ...deps], enableOnContentEditable: options.enableOnContentEditable,
enableOnFormTags: options.enableOnFormTags,
},
[pendingHotkey, setPendingHotkey, scope, ...deps],
); );
} }

View File

@ -4,7 +4,10 @@ import { isDefined } from '~/utils/isDefined';
import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants'; import { DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES } from '../constants';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState'; import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope'; import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
function isCustomScopesEqual( function isCustomScopesEqual(
customScopesA: CustomHotkeyScopes | undefined, customScopesA: CustomHotkeyScopes | undefined,
@ -46,13 +49,27 @@ export function useSetHotkeyScope() {
} }
} }
set(currentHotkeyScopeState, { const newHotkeyScope: HotkeyScope = {
scope: hotkeyScopeToSet, scope: hotkeyScopeToSet,
customScopes: { customScopes: {
commandMenu: customScopes?.commandMenu ?? true, commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false, goto: customScopes?.goto ?? false,
}, },
}); };
const scopesToSet: string[] = [];
if (newHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (newHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(newHotkeyScope.scope);
set(internalHotkeysEnabledScopesState, scopesToSet);
}, },
[], [],
); );

View File

@ -1,7 +1,7 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
type OwnProps = { type OwnProps = {
firstValue: string; firstValue: string;
@ -31,7 +31,7 @@ export function InplaceInputDoubleText({
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<InplaceInputTextEditMode <StyledInput
autoFocus autoFocus
placeholder={firstValuePlaceholder} placeholder={firstValuePlaceholder}
value={firstValue} value={firstValue}
@ -39,7 +39,7 @@ export function InplaceInputDoubleText({
onChange(event.target.value, secondValue); onChange(event.target.value, secondValue);
}} }}
/> />
<InplaceInputTextEditMode <StyledInput
placeholder={secondValuePlaceholder} placeholder={secondValuePlaceholder}
value={secondValue} value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {

View File

@ -1,9 +1,58 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects'; import { textInputStyle } from '@/ui/themes/effects';
export const InplaceInputTextEditMode = styled.input` import { useRegisterCloseCellHandlers } from '../../table/editable-cell/hooks/useRegisterCloseCellHandlers';
export const StyledInput = styled.input`
margin: 0; margin: 0;
width: 100%; width: 100%;
${textInputStyle} ${textInputStyle}
`; `;
type OwnProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
onSubmit: (newText: string) => void;
};
export function InplaceInputTextEditMode({
placeholder,
autoFocus,
value,
onSubmit,
}: OwnProps) {
const [internalText, setInternalText] = useState(value);
const wrapperRef = useRef(null);
function handleSubmit() {
onSubmit(internalText);
}
function handleCancel() {
setInternalText(value);
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setInternalText(event.target.value);
}
useEffect(() => {
setInternalText(value);
}, [value]);
useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel);
return (
<StyledInput
ref={wrapperRef}
placeholder={placeholder}
onChange={handleChange}
autoFocus={autoFocus}
value={internalText}
/>
);
}

View File

@ -11,7 +11,6 @@ import { MOBILE_VIEWPORT } from '@/ui/themes/themes';
import { AppNavbar } from '~/AppNavbar'; import { AppNavbar } from '~/AppNavbar';
import { CompaniesMockMode } from '~/pages/companies/CompaniesMockMode'; import { CompaniesMockMode } from '~/pages/companies/CompaniesMockMode';
import { useAutoNavigateOnboarding } from '../hooks/useAutoNavigateOnboarding';
import { isNavbarOpenedState } from '../states/isNavbarOpenedState'; import { isNavbarOpenedState } from '../states/isNavbarOpenedState';
const StyledLayout = styled.div` const StyledLayout = styled.div`
@ -39,12 +38,10 @@ const MainContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
children: JSX.Element; children: React.ReactNode;
}; };
export function DefaultLayout({ children }: OwnProps) { export function DefaultLayout({ children }: OwnProps) {
useAutoNavigateOnboarding();
const onboardingStatus = useOnboardingStatus(); const onboardingStatus = useOnboardingStatus();
return ( return (

View File

@ -1,52 +0,0 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useIsMatchingLocation } from '../../../../hooks/useIsMatchingLocation';
import { useOnboardingStatus } from '../../../auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '../../../auth/utils/getOnboardingStatus';
import { AppPath } from '../../../types/AppPath';
export function useAutoNavigateOnboarding() {
const navigate = useNavigate();
const isMatchingLocation = useIsMatchingLocation();
const onboardingStatus = useOnboardingStatus();
useEffect(() => {
const isMachinOngoingUserCreationRoute =
isMatchingLocation(AppPath.SignUp) ||
isMatchingLocation(AppPath.SignIn) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.Verify);
const isMatchingOnboardingRoute =
isMatchingLocation(AppPath.SignUp) ||
isMatchingLocation(AppPath.SignIn) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.CreateWorkspace) ||
isMatchingLocation(AppPath.CreateProfile);
if (
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
!isMachinOngoingUserCreationRoute
) {
navigate(AppPath.SignIn);
} else if (
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
!isMatchingLocation(AppPath.CreateWorkspace)
) {
navigate(AppPath.CreateWorkspace);
} else if (
onboardingStatus === OnboardingStatus.OngoingProfileCreation &&
!isMatchingLocation(AppPath.CreateProfile)
) {
navigate(AppPath.CreateProfile);
} else if (
onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
) {
navigate('/');
}
}, [onboardingStatus, navigate, isMatchingLocation]);
}

View File

@ -35,7 +35,7 @@ export function SingleEntitySelect<
onCancel?: () => void; onCancel?: () => void;
onCreate?: () => void; onCreate?: () => void;
entities: EntitiesForSingleEntitySelect<CustomEntityForSelect>; entities: EntitiesForSingleEntitySelect<CustomEntityForSelect>;
onEntitySelected: (entity: CustomEntityForSelect) => void; onEntitySelected: (entity: CustomEntityForSelect | null | undefined) => void;
disableBackgroundBlur?: boolean; disableBackgroundBlur?: boolean;
}) { }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -48,7 +48,11 @@ export function SingleEntitySelect<
useListenClickOutside({ useListenClickOutside({
refs: [containerRef], refs: [containerRef],
callback: () => { callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onCancel?.(); onCancel?.();
}, },
}); });

View File

@ -32,7 +32,7 @@ export function SingleEntitySelectBase<
onCancel, onCancel,
}: { }: {
entities: EntitiesForSingleEntitySelect<CustomEntityForSelect>; entities: EntitiesForSingleEntitySelect<CustomEntityForSelect>;
onEntitySelected: (entity: CustomEntityForSelect) => void; onEntitySelected: (entity: CustomEntityForSelect | null | undefined) => void;
onCancel?: () => void; onCancel?: () => void;
}) { }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const currentPageLocationState = atom<string>({
key: 'currentPageLocationState',
default: '',
});

View File

@ -5,7 +5,9 @@ import { TableColumn } from '@/people/table/components/peopleColumns';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside';
import { useIsPageLoading } from '../../hooks/useIsPageLoading';
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus'; import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { TableHeader } from '../table-header/components/TableHeader'; import { TableHeader } from '../table-header/components/TableHeader';
import { EntityTableBody } from './EntityTableBody'; import { EntityTableBody } from './EntityTableBody';
@ -88,6 +90,8 @@ export function EntityTable<SortField>({
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const tableBodyRef = React.useRef<HTMLDivElement>(null); const tableBodyRef = React.useRef<HTMLDivElement>(null);
useMapKeyboardToSoftFocus();
const leaveTableFocus = useLeaveTableFocus(); const leaveTableFocus = useLeaveTableFocus();
useListenClickOutside({ useListenClickOutside({
@ -97,6 +101,12 @@ export function EntityTable<SortField>({
}, },
}); });
const isPageLoading = useIsPageLoading();
if (isPageLoading) {
return null;
}
return ( return (
<StyledTableWithHeader> <StyledTableWithHeader>
<TableHeader <TableHeader

View File

@ -2,10 +2,10 @@ import { useRecoilValue } from 'recoil';
import { TableColumn } from '@/people/table/components/peopleColumns'; import { TableColumn } from '@/people/table/components/peopleColumns';
import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState'; import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState'; import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState';
import { RowContext } from '../states/RowContext'; import { RowIdContext } from '../states/RowIdContext';
import { RowIndexContext } from '../states/RowIndexContext';
import { tableRowIdsState } from '../states/tableRowIdsState'; import { tableRowIdsState } from '../states/tableRowIdsState';
import { EntityTableRow } from './EntityTableRow'; import { EntityTableRow } from './EntityTableRow';
@ -19,15 +19,19 @@ export function EntityTableBody({ columns }: { columns: Array<TableColumn> }) {
isFetchingEntityTableDataState, isFetchingEntityTableDataState,
); );
if (isFetchingEntityTableData || isNavbarSwitchingSize) {
return null;
}
return ( return (
<tbody> <tbody>
{!isFetchingEntityTableData && !isNavbarSwitchingSize {rowIds.map((rowId, index) => (
? rowIds.map((rowId, index) => ( <RowIdContext.Provider value={rowId} key={rowId}>
<RecoilScope SpecificContext={RowContext} key={rowId}> <RowIndexContext.Provider value={index}>
<EntityTableRow columns={columns} rowId={rowId} index={index} /> <EntityTableRow columns={columns} rowId={rowId} />
</RecoilScope> </RowIndexContext.Provider>
)) </RowIdContext.Provider>
: null} ))}
</tbody> </tbody>
); );
} }

View File

@ -1,33 +1,19 @@
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState'; import { RecoilScope } from '../../recoil-scope/components/RecoilScope';
import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected'; import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
import { CellContext } from '../states/CellContext'; import { ColumnIndexContext } from '../states/ColumnIndexContext';
import { contextMenuPositionState } from '../states/contextMenuPositionState'; import { contextMenuPositionState } from '../states/contextMenuPositionState';
import { currentColumnNumberScopedState } from '../states/currentColumnNumberScopedState';
export function EntityTableCell({ export function EntityTableCell({
rowId,
cellIndex, cellIndex,
children, children,
size, size,
}: { }: {
size: number; size: number;
rowId: string;
cellIndex: number; cellIndex: number;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [, setCurrentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
useEffect(() => {
setCurrentColumnNumber(cellIndex);
}, [cellIndex, setCurrentColumnNumber]);
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
const { setCurrentRowSelected } = useCurrentRowSelected(); const { setCurrentRowSelected } = useCurrentRowSelected();
@ -44,15 +30,19 @@ export function EntityTableCell({
} }
return ( return (
<td <RecoilScope>
onContextMenu={(event) => handleContextMenu(event)} <ColumnIndexContext.Provider value={cellIndex}>
style={{ <td
width: size, onContextMenu={(event) => handleContextMenu(event)}
minWidth: size, style={{
maxWidth: size, width: size,
}} minWidth: size,
> maxWidth: size,
{children} }}
</td> >
{children}
</td>
</ColumnIndexContext.Provider>
</RecoilScope>
); );
} }

View File

@ -1,16 +1,6 @@
import { useEffect } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { TableColumn } from '@/people/table/components/peopleColumns'; import { TableColumn } from '@/people/table/components/peopleColumns';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '../states/CellContext';
import { currentRowEntityIdScopedState } from '../states/currentRowEntityIdScopedState';
import { currentRowNumberScopedState } from '../states/currentRowNumberScopedState';
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
import { RowContext } from '../states/RowContext';
import { CheckboxCell } from './CheckboxCell'; import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCell'; import { EntityTableCell } from './EntityTableCell';
@ -23,56 +13,24 @@ const StyledRow = styled.tr<{ selected: boolean }>`
export function EntityTableRow({ export function EntityTableRow({
columns, columns,
rowId, rowId,
index,
}: { }: {
columns: TableColumn[]; columns: TableColumn[];
rowId: string; rowId: string;
index: number;
}) { }) {
const [currentRowEntityId, setCurrentRowEntityId] = useRecoilScopedState(
currentRowEntityIdScopedState,
RowContext,
);
const isCurrentRowSelected = useRecoilValue(isRowSelectedFamilyState(rowId));
const [, setCurrentRowNumber] = useRecoilScopedState(
currentRowNumberScopedState,
RowContext,
);
useEffect(() => {
if (currentRowEntityId !== rowId) {
setCurrentRowEntityId(rowId);
}
}, [rowId, setCurrentRowEntityId, currentRowEntityId]);
useEffect(() => {
setCurrentRowNumber(index);
}, [index, setCurrentRowNumber]);
return ( return (
<StyledRow <StyledRow data-testid={`row-id-${rowId}`} selected={false}>
key={rowId}
data-testid={`row-id-${rowId}`}
selected={isCurrentRowSelected}
>
<td> <td>
<CheckboxCell /> <CheckboxCell />
</td> </td>
{columns.map((column, columnIndex) => { {columns.map((column, columnIndex) => {
return ( return (
<RecoilScope SpecificContext={CellContext} key={column.id.toString()}> <EntityTableCell
<RecoilScope> key={column.id}
<EntityTableCell size={column.size}
rowId={rowId} cellIndex={columnIndex}
size={column.size} >
cellIndex={columnIndex} {column.cellComponent}
> </EntityTableCell>
{column.cellComponent}
</EntityTableCell>
</RecoilScope>
</RecoilScope>
); );
})} })}
<td></td> <td></td>

View File

@ -1,25 +0,0 @@
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useInitializeEntityTable } from '../hooks/useInitializeEntityTable';
import { useInitializeEntityTableFilters } from '../hooks/useInitializeEntityTableFilters';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
export function HooksEntityTable({
numberOfColumns,
availableFilters,
}: {
numberOfColumns: number;
availableFilters: FilterDefinition[];
}) {
useMapKeyboardToSoftFocus();
useInitializeEntityTable({
numberOfColumns,
});
useInitializeEntityTableFilters({
availableFilters,
});
return <></>;
}

View File

@ -3,9 +3,10 @@ import styled from '@emotion/styled';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { CellHotkeyScopeContext } from '../../states/CellHotkeyScopeContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentCellEditMode } from '../hooks/useCurrentCellEditMode'; import { useCurrentCellEditMode } from '../hooks/useCurrentCellEditMode';
import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell'; import { useIsSoftFocusOnCurrentCell } from '../hooks/useIsSoftFocusOnCurrentCell';
import { useRegisterEditableCell } from '../hooks/useRegisterEditableCell';
import { EditableCellDisplayMode } from './EditableCellDisplayMode'; import { EditableCellDisplayMode } from './EditableCellDisplayMode';
import { EditableCellEditMode } from './EditableCellEditMode'; import { EditableCellEditMode } from './EditableCellEditMode';
@ -34,6 +35,10 @@ type OwnProps = {
onCancel?: () => void; onCancel?: () => void;
}; };
const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export function EditableCell({ export function EditableCell({
editModeHorizontalAlign = 'left', editModeHorizontalAlign = 'left',
editModeVerticalPosition = 'over', editModeVerticalPosition = 'over',
@ -42,35 +47,35 @@ export function EditableCell({
editHotkeyScope, editHotkeyScope,
transparent = false, transparent = false,
maxContentWidth, maxContentWidth,
onSubmit,
onCancel,
}: OwnProps) { }: OwnProps) {
const { isCurrentCellInEditMode } = useCurrentCellEditMode(); const { isCurrentCellInEditMode } = useCurrentCellEditMode();
const hasSoftFocus = useIsSoftFocusOnCurrentCell(); const hasSoftFocus = useIsSoftFocusOnCurrentCell();
useRegisterEditableCell(editHotkeyScope);
return ( return (
<CellBaseContainer> <CellHotkeyScopeContext.Provider
{isCurrentCellInEditMode ? ( value={editHotkeyScope ?? DEFAULT_CELL_SCOPE}
<EditableCellEditMode >
maxContentWidth={maxContentWidth} <CellBaseContainer>
transparent={transparent} {isCurrentCellInEditMode ? (
editModeHorizontalAlign={editModeHorizontalAlign} <EditableCellEditMode
editModeVerticalPosition={editModeVerticalPosition} maxContentWidth={maxContentWidth}
onSubmit={onSubmit} transparent={transparent}
onCancel={onCancel} editModeHorizontalAlign={editModeHorizontalAlign}
> editModeVerticalPosition={editModeVerticalPosition}
{editModeContent} >
</EditableCellEditMode> {editModeContent}
) : hasSoftFocus ? ( </EditableCellEditMode>
<EditableCellSoftFocusMode> ) : hasSoftFocus ? (
{nonEditModeContent} <EditableCellSoftFocusMode>
</EditableCellSoftFocusMode> {nonEditModeContent}
) : ( </EditableCellSoftFocusMode>
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode> ) : (
)} <EditableCellDisplayMode>
</CellBaseContainer> {nonEditModeContent}
</EditableCellDisplayMode>
)}
</CellBaseContainer>
</CellHotkeyScopeContext.Provider>
); );
} }

View File

@ -1,10 +1,8 @@
import { ReactElement, useRef } from 'react'; import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { overlayBackground } from '@/ui/themes/effects'; import { overlayBackground } from '@/ui/themes/effects';
import { useRegisterCloseCellHandlers } from '../hooks/useRegisterCloseCellHandlers';
export const EditableCellEditModeContainer = styled.div<OwnProps>` export const EditableCellEditModeContainer = styled.div<OwnProps>`
align-items: center; align-items: center;
border: ${({ transparent, theme }) => border: ${({ transparent, theme }) =>
@ -36,30 +34,21 @@ type OwnProps = {
maxContentWidth?: number; maxContentWidth?: number;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
editModeVerticalPosition?: 'over' | 'below'; editModeVerticalPosition?: 'over' | 'below';
onOutsideClick?: () => void; initialValue?: string;
onCancel?: () => void;
onSubmit?: () => void;
}; };
export function EditableCellEditMode({ export function EditableCellEditMode({
editModeHorizontalAlign, editModeHorizontalAlign,
editModeVerticalPosition, editModeVerticalPosition,
children, children,
onCancel,
onSubmit,
transparent = false, transparent = false,
maxContentWidth, maxContentWidth,
}: OwnProps) { }: OwnProps) {
const wrapperRef = useRef(null);
useRegisterCloseCellHandlers(wrapperRef, onSubmit, onCancel);
return ( return (
<EditableCellEditModeContainer <EditableCellEditModeContainer
maxContentWidth={maxContentWidth} maxContentWidth={maxContentWidth}
transparent={transparent} transparent={transparent}
data-testid="editable-cell-edit-mode-container" data-testid="editable-cell-edit-mode-container"
ref={wrapperRef}
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
editModeVerticalPosition={editModeVerticalPosition} editModeVerticalPosition={editModeVerticalPosition}
> >

View File

@ -4,6 +4,7 @@ import { userEvent, within } from '@storybook/testing-library';
import { CellPositionDecorator } from '~/testing/decorators/CellPositionDecorator'; import { CellPositionDecorator } from '~/testing/decorators/CellPositionDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { sleep } from '~/testing/sleep';
import { EditableCellText } from '../../types/EditableCellText'; import { EditableCellText } from '../../types/EditableCellText';
@ -28,13 +29,12 @@ export const SoftFocusMode: Story = {
play: async ({ canvasElement, step }) => { play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await step('Click once', () => const content = await canvas.findByText('Content');
userEvent.click(canvas.getByText('Content')),
);
await step('Escape', () => { await userEvent.click(content);
userEvent.keyboard('{esc}'); await userEvent.keyboard('{esc}');
});
await sleep(10);
await step('Has soft focus mode', () => { await step('Has soft focus mode', () => {
expect(canvas.getByTestId('editable-cell-soft-focus-mode')).toBeDefined(); expect(canvas.getByTestId('editable-cell-soft-focus-mode')).toBeDefined();
@ -47,7 +47,7 @@ export const EditMode: Story = {
play: async ({ canvasElement, step }) => { play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
const click = async () => userEvent.click(canvas.getByText('Content')); const click = () => userEvent.click(canvas.getByText('Content'));
await step('Click once', click); await step('Click once', click);

View File

@ -1,23 +1,12 @@
import { useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState'; import { ColumnIndexContext } from '../../states/ColumnIndexContext';
import { RowIndexContext } from '../../states/RowIndexContext';
import { CellContext } from '../../states/CellContext';
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
import { RowContext } from '../../states/RowContext';
import { CellPosition } from '../../types/CellPosition'; import { CellPosition } from '../../types/CellPosition';
export function useCurrentCellPosition() { export function useCurrentCellPosition() {
const [currentRowNumber] = useRecoilScopedState( const currentRowNumber = useContext(RowIndexContext);
currentRowNumberScopedState, const currentColumnNumber = useContext(ColumnIndexContext);
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentCellPosition: CellPosition = useMemo( const currentCellPosition: CellPosition = useMemo(
() => ({ () => ({

View File

@ -1,15 +1,13 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useContextScopeId } from '../../../recoil-scope/hooks/useContextScopeId';
import { getSnapshotScopedState } from '../../../recoil-scope/utils/getSnapshotScopedState';
import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode'; import { useCloseCurrentCellInEditMode } from '../../hooks/useClearCellInEditMode';
import { CellContext } from '../../states/CellContext'; import { CellHotkeyScopeContext } from '../../states/CellHotkeyScopeContext';
import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState'; import { isSomeInputInEditModeState } from '../../states/isSomeInputInEditModeState';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { customCellHotkeyScopeScopedState } from '../states/customCellHotkeyScopeScopedState';
import { useCurrentCellEditMode } from './useCurrentCellEditMode'; import { useCurrentCellEditMode } from './useCurrentCellEditMode';
@ -24,7 +22,7 @@ export function useEditableCell() {
const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode(); const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode();
const cellContextId = useContextScopeId(CellContext); const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
function closeEditableCell() { function closeEditableCell() {
closeCurrentCellInEditMode(); closeCurrentCellInEditMode();
@ -38,12 +36,6 @@ export function useEditableCell() {
.getLoadable(isSomeInputInEditModeState) .getLoadable(isSomeInputInEditModeState)
.valueOrThrow(); .valueOrThrow();
const customCellHotkeyScope = getSnapshotScopedState({
snapshot,
state: customCellHotkeyScopeScopedState,
contextScopeId: cellContextId,
});
if (!isSomeInputInEditMode) { if (!isSomeInputInEditMode) {
set(isSomeInputInEditModeState, true); set(isSomeInputInEditModeState, true);
@ -62,7 +54,7 @@ export function useEditableCell() {
} }
} }
}, },
[setCurrentCellInEditMode, setHotkeyScope, cellContextId], [setCurrentCellInEditMode, setHotkeyScope, customCellHotkeyScope],
); );
return { return {

View File

@ -14,6 +14,7 @@ export function useRegisterCloseCellHandlers(
) { ) {
const { closeEditableCell } = useEditableCell(); const { closeEditableCell } = useEditableCell();
const { isCurrentCellInEditMode } = useCurrentCellEditMode(); const { isCurrentCellInEditMode } = useCurrentCellEditMode();
useListenClickOutside({ useListenClickOutside({
refs: [wrapperRef], refs: [wrapperRef],
callback: (event) => { callback: (event) => {
@ -26,6 +27,7 @@ export function useRegisterCloseCellHandlers(
} }
}, },
}); });
const { moveRight, moveLeft, moveDown } = useMoveSoftFocus(); const { moveRight, moveLeft, moveDown } = useMoveSoftFocus();
useScopedHotkeys( useScopedHotkeys(

View File

@ -1,23 +0,0 @@
import { useEffect } from 'react';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '../../../recoil-scope/hooks/useRecoilScopedState';
import { CellContext } from '../../states/CellContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { customCellHotkeyScopeScopedState } from '../states/customCellHotkeyScopeScopedState';
const DEFAULT_CELL_SCOPE: HotkeyScope = {
scope: TableHotkeyScope.CellEditMode,
};
export function useRegisterEditableCell(cellHotkeyScope?: HotkeyScope) {
const [, setCustomCellHotkeyScope] = useRecoilScopedState(
customCellHotkeyScopeScopedState,
CellContext,
);
useEffect(() => {
setCustomCellHotkeyScope(cellHotkeyScope ?? DEFAULT_CELL_SCOPE);
}, [cellHotkeyScope, setCustomCellHotkeyScope]);
}

View File

@ -1,50 +1,29 @@
import { useMemo } from 'react';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { useSetSoftFocusPosition } from '../../hooks/useSetSoftFocusPosition'; import { useSetSoftFocusPosition } from '../../hooks/useSetSoftFocusPosition';
import { CellContext } from '../../states/CellContext';
import { currentColumnNumberScopedState } from '../../states/currentColumnNumberScopedState';
import { currentRowNumberScopedState } from '../../states/currentRowNumberScopedState';
import { isSoftFocusActiveState } from '../../states/isSoftFocusActiveState'; import { isSoftFocusActiveState } from '../../states/isSoftFocusActiveState';
import { RowContext } from '../../states/RowContext';
import { CellPosition } from '../../types/CellPosition';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useCurrentCellPosition } from './useCurrentCellPosition';
export function useSetSoftFocusOnCurrentCell() { export function useSetSoftFocusOnCurrentCell() {
const setSoftFocusPosition = useSetSoftFocusPosition(); const setSoftFocusPosition = useSetSoftFocusPosition();
const [currentRowNumber] = useRecoilScopedState( const currentCellPosition = useCurrentCellPosition();
currentRowNumberScopedState,
RowContext,
);
const [currentColumnNumber] = useRecoilScopedState(
currentColumnNumberScopedState,
CellContext,
);
const currentTablePosition: CellPosition = useMemo(
() => ({
column: currentColumnNumber,
row: currentRowNumber,
}),
[currentColumnNumber, currentRowNumber],
);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
return useRecoilCallback( return useRecoilCallback(
({ set }) => ({ set }) =>
() => { () => {
setSoftFocusPosition(currentTablePosition); setSoftFocusPosition(currentCellPosition);
set(isSoftFocusActiveState, true); set(isSoftFocusActiveState, true);
setHotkeyScope(TableHotkeyScope.TableSoftFocus); setHotkeyScope(TableHotkeyScope.TableSoftFocus);
}, },
[setHotkeyScope, currentTablePosition, setSoftFocusPosition], [setHotkeyScope, currentCellPosition, setSoftFocusPosition],
); );
} }

View File

@ -1,11 +0,0 @@
import { atomFamily } from 'recoil';
import { HotkeyScope } from '../../../hotkey/types/HotkeyScope';
export const customCellHotkeyScopeScopedState = atomFamily<
HotkeyScope | null,
string
>({
key: 'customCellHotkeyScopeScopedState',
default: null,
});

View File

@ -1,9 +1,11 @@
import { useRef } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate'; import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
import { useListenClickOutside } from '../../../hooks/useListenClickOutside';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useEditableCell } from '../hooks/useEditableCell'; import { useEditableCell } from '../hooks/useEditableCell';
@ -38,8 +40,21 @@ export function EditableCellDateEditMode({
[closeEditableCell], [closeEditableCell],
); );
const containerRef = useRef(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
closeEditableCell();
},
});
return ( return (
<EditableCellDateEditModeContainer> <EditableCellDateEditModeContainer ref={containerRef}>
<InplaceInputDate onChange={handleDateChange} value={value} /> <InplaceInputDate onChange={handleDateChange} value={value} />
</EditableCellDateEditModeContainer> </EditableCellDateEditModeContainer>
); );

View File

@ -1,4 +1,4 @@
import { ReactElement, useEffect, useState } from 'react'; import { ReactElement } from 'react';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { CellSkeleton } from '../components/CellSkeleton'; import { CellSkeleton } from '../components/CellSkeleton';
@ -12,8 +12,7 @@ type OwnProps = {
firstValuePlaceholder: string; firstValuePlaceholder: string;
secondValuePlaceholder: string; secondValuePlaceholder: string;
nonEditModeContent: ReactElement; nonEditModeContent: ReactElement;
onChange: (firstValue: string, secondValue: string) => void; onSubmit?: (firstValue: string, secondValue: string) => void;
onSubmit?: () => void;
onCancel?: () => void; onCancel?: () => void;
loading?: boolean; loading?: boolean;
}; };
@ -23,36 +22,21 @@ export function EditableCellDoubleText({
secondValue, secondValue,
firstValuePlaceholder, firstValuePlaceholder,
secondValuePlaceholder, secondValuePlaceholder,
onChange,
onSubmit, onSubmit,
onCancel, onCancel,
nonEditModeContent, nonEditModeContent,
loading, loading,
}: OwnProps) { }: OwnProps) {
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
useEffect(() => {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
}, [firstValue, secondValue]);
function handleOnChange(firstValue: string, secondValue: string): void {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
onChange(firstValue, secondValue);
}
return ( return (
<EditableCell <EditableCell
editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }} editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }}
editModeContent={ editModeContent={
<EditableCellDoubleTextEditMode <EditableCellDoubleTextEditMode
firstValue={firstInternalValue} firstValue={firstValue}
secondValue={secondInternalValue} secondValue={secondValue}
firstValuePlaceholder={firstValuePlaceholder} firstValuePlaceholder={firstValuePlaceholder}
secondValuePlaceholder={secondValuePlaceholder} secondValuePlaceholder={secondValuePlaceholder}
onChange={handleOnChange}
onSubmit={onSubmit} onSubmit={onSubmit}
onCancel={onCancel} onCancel={onCancel}
/> />

View File

@ -1,21 +1,22 @@
import { ChangeEvent, useRef, useState } from 'react'; import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus'; import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useEditableCell } from '../hooks/useEditableCell'; import { useEditableCell } from '../hooks/useEditableCell';
import { useRegisterCloseCellHandlers } from '../hooks/useRegisterCloseCellHandlers';
type OwnProps = { type OwnProps = {
firstValue: string; firstValue: string;
secondValue: string; secondValue: string;
firstValuePlaceholder: string; firstValuePlaceholder: string;
secondValuePlaceholder: string; secondValuePlaceholder: string;
onChange: (firstValue: string, secondValue: string) => void; onChange?: (firstValue: string, secondValue: string) => void;
onSubmit?: () => void; onSubmit?: (firstValue: string, secondValue: string) => void;
onCancel?: () => void; onCancel?: () => void;
}; };
@ -39,6 +40,19 @@ export function EditableCellDoubleTextEditMode({
onSubmit, onSubmit,
onCancel, onCancel,
}: OwnProps) { }: OwnProps) {
const [firstInternalValue, setFirstInternalValue] = useState(firstValue);
const [secondInternalValue, setSecondInternalValue] = useState(secondValue);
useEffect(() => {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
}, [firstValue, secondValue]);
function handleOnChange(firstValue: string, secondValue: string): void {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
}
const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left'); const [focusPosition, setFocusPosition] = useState<'left' | 'right'>('left');
const firstValueInputRef = useRef<HTMLInputElement>(null); const firstValueInputRef = useRef<HTMLInputElement>(null);
@ -52,12 +66,23 @@ export function EditableCellDoubleTextEditMode({
closeEditableCell(); closeEditableCell();
} }
function handleCancel() {
setFirstInternalValue(firstValue);
setSecondInternalValue(secondValue);
onCancel?.();
}
function handleSubmit() {
onSubmit?.(firstInternalValue, secondInternalValue);
}
useScopedHotkeys( useScopedHotkeys(
Key.Enter, Key.Enter,
() => { () => {
closeCell(); closeCell();
moveDown(); moveDown();
onSubmit?.(); handleSubmit();
}, },
TableHotkeyScope.CellDoubleTextInput, TableHotkeyScope.CellDoubleTextInput,
[closeCell], [closeCell],
@ -66,7 +91,7 @@ export function EditableCellDoubleTextEditMode({
useScopedHotkeys( useScopedHotkeys(
Key.Escape, Key.Escape,
() => { () => {
onCancel?.(); handleCancel();
closeCell(); closeCell();
}, },
TableHotkeyScope.CellDoubleTextInput, TableHotkeyScope.CellDoubleTextInput,
@ -80,7 +105,8 @@ export function EditableCellDoubleTextEditMode({
setFocusPosition('right'); setFocusPosition('right');
secondValueInputRef.current?.focus(); secondValueInputRef.current?.focus();
} else { } else {
onSubmit?.(); handleSubmit();
closeCell(); closeCell();
moveRight(); moveRight();
} }
@ -96,7 +122,7 @@ export function EditableCellDoubleTextEditMode({
setFocusPosition('left'); setFocusPosition('left');
firstValueInputRef.current?.focus(); firstValueInputRef.current?.focus();
} else { } else {
onSubmit?.(); handleSubmit();
closeCell(); closeCell();
moveLeft(); moveLeft();
} }
@ -105,23 +131,27 @@ export function EditableCellDoubleTextEditMode({
[closeCell, moveRight, focusPosition], [closeCell, moveRight, focusPosition],
); );
const wrapperRef = useRef(null);
useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel);
return ( return (
<StyledContainer> <StyledContainer ref={wrapperRef}>
<InplaceInputTextEditMode <StyledInput
autoFocus autoFocus
placeholder={firstValuePlaceholder} placeholder={firstValuePlaceholder}
ref={firstValueInputRef} ref={firstValueInputRef}
value={firstValue} value={firstValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value, secondValue); handleOnChange(event.target.value, secondValue);
}} }}
/> />
<InplaceInputTextEditMode <StyledInput
placeholder={secondValuePlaceholder} placeholder={secondValuePlaceholder}
ref={secondValueInputRef} ref={secondValueInputRef}
value={secondValue} value={secondValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
onChange(firstValue, event.target.value); handleOnChange(firstValue, event.target.value);
}} }}
/> />
</StyledContainer> </StyledContainer>

View File

@ -1,5 +1,3 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode'; import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
@ -8,42 +6,21 @@ import { EditableCell } from '../components/EditableCell';
type OwnProps = { type OwnProps = {
placeholder?: string; placeholder?: string;
value: string; value: string;
onChange: (updated: string) => void; onSubmit?: (newText: string) => void;
onSubmit?: () => void;
onCancel?: () => void;
}; };
export function EditableCellPhone({ export function EditableCellPhone({ value, placeholder, onSubmit }: OwnProps) {
value,
placeholder,
onChange,
onSubmit,
onCancel,
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
useEffect(() => {
setInputValue(value);
}, [value]);
return ( return (
<EditableCell <EditableCell
editModeContent={ editModeContent={
<InplaceInputTextEditMode <InplaceInputTextEditMode
autoFocus autoFocus
placeholder={placeholder || ''} placeholder={placeholder || ''}
ref={inputRef} value={value}
value={inputValue} onSubmit={(newText) => onSubmit?.(newText)}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
onChange(event.target.value);
}}
/> />
} }
nonEditModeContent={<InplaceInputPhoneDisplayMode value={inputValue} />} nonEditModeContent={<InplaceInputPhoneDisplayMode value={value} />}
onSubmit={onSubmit}
onCancel={onCancel}
/> />
); );
} }

View File

@ -1,5 +1,3 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode'; import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
@ -9,10 +7,10 @@ import { EditableCell } from '../components/EditableCell';
type OwnProps = { type OwnProps = {
placeholder?: string; placeholder?: string;
value: string; value: string;
onChange: (newValue: string) => void; onChange?: (newValue: string) => void;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
loading?: boolean; loading?: boolean;
onSubmit?: () => void; onSubmit?: (newText: string) => void;
onCancel?: () => void; onCancel?: () => void;
}; };
@ -25,12 +23,6 @@ export function EditableCellText({
onCancel, onCancel,
onSubmit, onSubmit,
}: OwnProps) { }: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
return ( return (
<EditableCell <EditableCell
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
@ -38,22 +30,15 @@ export function EditableCellText({
<InplaceInputTextEditMode <InplaceInputTextEditMode
placeholder={placeholder || ''} placeholder={placeholder || ''}
autoFocus autoFocus
value={internalValue} value={value}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onSubmit={(newText) => onSubmit?.(newText)}
setInternalValue(event.target.value);
onChange(event.target.value);
}}
/> />
} }
onSubmit={onSubmit}
onCancel={onCancel}
nonEditModeContent={ nonEditModeContent={
loading ? ( loading ? (
<CellSkeleton /> <CellSkeleton />
) : ( ) : (
<InplaceInputTextDisplayMode> <InplaceInputTextDisplayMode>{value}</InplaceInputTextDisplayMode>
{internalValue}
</InplaceInputTextDisplayMode>
) )
} }
></EditableCell> ></EditableCell>

View File

@ -1,5 +1,3 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode'; import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { RawLink } from '../../../link/components/RawLink'; import { RawLink } from '../../../link/components/RawLink';
@ -9,53 +7,40 @@ import { EditableCell } from '../components/EditableCell';
type OwnProps = { type OwnProps = {
placeholder?: string; placeholder?: string;
url: string; url: string;
onChange: (newURL: string) => void; onChange?: (newURL: string) => void;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
loading?: boolean; loading?: boolean;
onSubmit?: () => void; onSubmit?: (newURL: string) => void;
onCancel?: () => void; onCancel?: () => void;
}; };
export function EditableCellURL({ export function EditableCellURL({
url, url,
placeholder, placeholder,
onChange,
editModeHorizontalAlign, editModeHorizontalAlign,
loading, loading,
onCancel,
onSubmit, onSubmit,
}: OwnProps) { }: OwnProps) {
const [internalValue, setInternalValue] = useState(url);
useEffect(() => {
setInternalValue(url);
}, [url]);
return ( return (
<EditableCell <EditableCell
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={ editModeContent={
<InplaceInputTextEditMode <InplaceInputTextEditMode
placeholder={placeholder || ''} placeholder={placeholder}
autoFocus autoFocus
value={internalValue} value={url}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onSubmit={(newURL) => onSubmit?.(newURL)}
setInternalValue(event.target.value);
onChange(event.target.value);
}}
/> />
} }
onSubmit={onSubmit}
onCancel={onCancel}
nonEditModeContent={ nonEditModeContent={
loading ? ( loading ? (
<CellSkeleton /> <CellSkeleton />
) : ( ) : (
<RawLink <RawLink
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
href={internalValue ? 'https://' + internalValue : ''} href={url ? 'https://' + url : ''}
> >
{internalValue} {url}
</RawLink> </RawLink>
) )
} }

View File

@ -1,30 +1,21 @@
import { ChangeEvent, ReactNode, useEffect, useRef, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects'; import { InplaceInputTextEditMode } from '../../../inplace-input/components/InplaceInputTextEditMode';
import { EditableCell } from '../components/EditableCell'; import { EditableCell } from '../components/EditableCell';
export type EditableChipProps = { export type EditableChipProps = {
placeholder?: string; placeholder?: string;
value: string; value: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: React.ReactNode; ChipComponent: React.ReactNode;
commentThreadCount?: number; commentThreadCount?: number;
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void; onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
rightEndContents?: ReactNode[]; rightEndContents?: ReactNode[];
onSubmit?: () => void; onSubmit?: (newValue: string) => void;
onCancel?: () => void; onCancel?: () => void;
}; };
// TODO: refactor
const StyledInplaceInput = styled.input`
width: 100%;
${textInputStyle}
`;
const NoEditModeContainer = styled.div` const NoEditModeContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
@ -40,14 +31,11 @@ const RightContainer = styled.div`
export function EditableCellChip({ export function EditableCellChip({
value, value,
placeholder, placeholder,
changeHandler,
editModeHorizontalAlign, editModeHorizontalAlign,
ChipComponent, ChipComponent,
rightEndContents, rightEndContents,
onSubmit, onSubmit,
onCancel,
}: EditableChipProps) { }: EditableChipProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
useEffect(() => { useEffect(() => {
@ -64,19 +52,13 @@ export function EditableCellChip({
<EditableCell <EditableCell
editModeHorizontalAlign={editModeHorizontalAlign} editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={ editModeContent={
<StyledInplaceInput <InplaceInputTextEditMode
placeholder={placeholder || ''} placeholder={placeholder || ''}
autoFocus autoFocus
ref={inputRef}
value={inputValue} value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onSubmit={(newValue) => onSubmit?.(newValue)}
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/> />
} }
onSubmit={onSubmit}
onCancel={onCancel}
nonEditModeContent={ nonEditModeContent={
<NoEditModeContainer> <NoEditModeContainer>
{ChipComponent} {ChipComponent}

View File

@ -1,7 +1,6 @@
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue'; import { useContext } from 'react';
import { currentRowEntityIdScopedState } from '../states/currentRowEntityIdScopedState'; import { RowIdContext } from '../states/RowIdContext';
import { RowContext } from '../states/RowContext';
export type TableDimensions = { export type TableDimensions = {
numberOfColumns: number; numberOfColumns: number;
@ -9,10 +8,7 @@ export type TableDimensions = {
}; };
export function useCurrentRowEntityId() { export function useCurrentRowEntityId() {
const currentRowEntityIdScoped = useRecoilScopedValue( const currentEntityId = useContext(RowIdContext);
currentRowEntityIdScopedState,
RowContext,
);
return currentRowEntityIdScoped; return currentEntityId;
} }

View File

@ -1,11 +1,11 @@
import { useContext } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState } from 'recoil';
import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState'; import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState';
import { RowIdContext } from '../states/RowIdContext';
import { useCurrentRowEntityId } from './useCurrentEntityId';
export function useCurrentRowSelected() { export function useCurrentRowSelected() {
const currentRowId = useCurrentRowEntityId(); const currentRowId = useContext(RowIdContext);
const [isRowSelected] = useRecoilState( const [isRowSelected] = useRecoilState(
isRowSelectedFamilyState(currentRowId ?? ''), isRowSelectedFamilyState(currentRowId ?? ''),

View File

@ -45,8 +45,6 @@ export function useLeaveTableFocus() {
closeCurrentCellInEditMode(); closeCurrentCellInEditMode();
disableSoftFocus(); disableSoftFocus();
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
}, },
[setHotkeyScope, closeCurrentCellInEditMode, disableSoftFocus], [setHotkeyScope, closeCurrentCellInEditMode, disableSoftFocus],
); );

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const CellContext = createContext<string | null>(null);

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
import { HotkeyScope } from '../../hotkey/types/HotkeyScope';
export const CellHotkeyScopeContext = createContext<HotkeyScope | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ColumnIndexContext = createContext<number>(0);

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const RowContext = createContext<string | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const RowIdContext = createContext<string | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const RowIndexContext = createContext<number>(0);

View File

@ -3,11 +3,10 @@ import { userEvent, within } from '@storybook/testing-library';
import { IconList } from '@/ui/icon/index'; import { IconList } from '@/ui/icon/index';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts'; import { availableSorts } from '~/pages/companies/companies-sorts';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { HooksEntityTable } from '../../../components/HooksEntityTable'; import { ComponentWithRouterDecorator } from '../../../../../../testing/decorators/ComponentWithRouterDecorator';
import { CompanyEntityTableDataMocked } from '../../../../../companies/table/components/CompanyEntityTableDataMocked';
import { TableContext } from '../../../states/TableContext'; import { TableContext } from '../../../states/TableContext';
import { TableHeader } from '../TableHeader'; import { TableHeader } from '../TableHeader';
@ -17,15 +16,11 @@ const meta: Meta<typeof TableHeader> = {
decorators: [ decorators: [
(Story) => ( (Story) => (
<RecoilScope SpecificContext={TableContext}> <RecoilScope SpecificContext={TableContext}>
{/* TODO: add company mocked loader <CompanyEntityTableData */} <CompanyEntityTableDataMocked />
<HooksEntityTable
availableFilters={companiesFilters}
numberOfColumns={5}
/>
<Story /> <Story />
</RecoilScope> </RecoilScope>
), ),
ComponentDecorator, ComponentWithRouterDecorator,
], ],
argTypes: { viewIcon: { control: false } }, argTypes: { viewIcon: { control: false } },
args: { args: {

View File

@ -21,6 +21,7 @@ export function Verify() {
navigate(AppPath.SignIn); navigate(AppPath.SignIn);
} else { } else {
await verify(loginToken); await verify(loginToken);
navigate(AppPath.CompaniesPage);
} }
} }

View File

@ -1,7 +0,0 @@
import { useTrackPageView } from '@/analytics/hooks/useTrackPageView';
export function AnalyticsHook() {
useTrackPageView();
return <></>;
}

View File

@ -1,15 +1,9 @@
import { AnalyticsHook } from './AnalyticsHook';
import { GotoHotkeysHooks } from './GotoHotkeysHooks'; import { GotoHotkeysHooks } from './GotoHotkeysHooks';
import { HotkeyScopeAutoSyncHook } from './HotkeyScopeAutoSyncHook';
import { HotkeyScopeBrowserRouterSync } from './HotkeyScopeBrowserRouterSync';
export function AppInternalHooks() { export function AppInternalHooks() {
return ( return (
<> <>
<AnalyticsHook />
<GotoHotkeysHooks /> <GotoHotkeysHooks />
<HotkeyScopeAutoSyncHook />
<HotkeyScopeBrowserRouterSync />
</> </>
); );
} }

View File

@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation';
import { useEventTracker } from '../modules/analytics/hooks/useEventTracker';
import { useOnboardingStatus } from '../modules/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '../modules/auth/utils/getOnboardingStatus';
import { AppBasePath } from '../modules/types/AppBasePath';
import { AppPath } from '../modules/types/AppPath';
import { PageHotkeyScope } from '../modules/types/PageHotkeyScope';
import { SettingsPath } from '../modules/types/SettingsPath';
import { useSetHotkeyScope } from '../modules/ui/hotkey/hooks/useSetHotkeyScope';
import { TableHotkeyScope } from '../modules/ui/table/types/TableHotkeyScope';
export function AuthAutoRouter() {
const navigate = useNavigate();
const isMatchingLocation = useIsMatchingLocation();
const [previousLocation, setPreviousLocation] = useState('');
const onboardingStatus = useOnboardingStatus();
const setHotkeyScope = useSetHotkeyScope();
const location = useLocation();
const eventTracker = useEventTracker();
useEffect(() => {
if (!previousLocation || previousLocation !== location.pathname) {
setPreviousLocation(location.pathname);
} else {
return;
}
const isMachinOngoingUserCreationRoute =
isMatchingLocation(AppPath.SignUp) ||
isMatchingLocation(AppPath.SignIn) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.Verify);
const isMatchingOnboardingRoute =
isMatchingLocation(AppPath.SignUp) ||
isMatchingLocation(AppPath.SignIn) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.Verify) ||
isMatchingLocation(AppPath.CreateWorkspace) ||
isMatchingLocation(AppPath.CreateProfile);
if (
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
!isMachinOngoingUserCreationRoute
) {
navigate(AppPath.SignIn);
} else if (
onboardingStatus === OnboardingStatus.OngoingWorkspaceCreation &&
!isMatchingLocation(AppPath.CreateWorkspace)
) {
navigate(AppPath.CreateWorkspace);
} else if (
onboardingStatus === OnboardingStatus.OngoingProfileCreation &&
!isMatchingLocation(AppPath.CreateProfile)
) {
navigate(AppPath.CreateProfile);
} else if (
onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
) {
navigate('/');
}
switch (true) {
case isMatchingLocation(AppPath.CompaniesPage): {
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
break;
}
case isMatchingLocation(AppPath.PeoplePage): {
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
break;
}
case isMatchingLocation(AppPath.CompanyShowPage): {
setHotkeyScope(PageHotkeyScope.CompanyShowPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.PersonShowPage): {
setHotkeyScope(PageHotkeyScope.PersonShowPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.OpportunitiesPage): {
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.SignIn): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.SignUp): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.Invite): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.CreateProfile): {
setHotkeyScope(PageHotkeyScope.CreateProfile);
break;
}
case isMatchingLocation(AppPath.CreateWorkspace): {
setHotkeyScope(PageHotkeyScope.CreateWokspace);
break;
}
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
setHotkeyScope(PageHotkeyScope.ProfilePage, { goto: true });
break;
}
case isMatchingLocation(
SettingsPath.WorkspaceMembersPage,
AppBasePath.Settings,
): {
setHotkeyScope(PageHotkeyScope.WorkspaceMemberPage, { goto: true });
break;
}
}
eventTracker('pageview', {
location: {
pathname: location.pathname,
},
});
}, [
onboardingStatus,
navigate,
isMatchingLocation,
setHotkeyScope,
location,
previousLocation,
eventTracker,
]);
return <></>;
}

View File

@ -1,7 +0,0 @@
import { useHotkeyScopeAutoSync } from '@/ui/hotkey/hooks/internal/useHotkeyScopeAutoSync';
export function HotkeyScopeAutoSyncHook() {
useHotkeyScopeAutoSync();
return <></>;
}

View File

@ -1,73 +0,0 @@
import { useEffect } from 'react';
import { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
export function HotkeyScopeBrowserRouterSync() {
const isMatchingLocation = useIsMatchingLocation();
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {
switch (true) {
case isMatchingLocation(AppPath.CompaniesPage): {
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
break;
}
case isMatchingLocation(AppPath.PeoplePage): {
setHotkeyScope(TableHotkeyScope.Table, { goto: true });
break;
}
case isMatchingLocation(AppPath.CompanyShowPage): {
setHotkeyScope(PageHotkeyScope.CompanyShowPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.PersonShowPage): {
setHotkeyScope(PageHotkeyScope.PersonShowPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.OpportunitiesPage): {
setHotkeyScope(PageHotkeyScope.OpportunitiesPage, { goto: true });
break;
}
case isMatchingLocation(AppPath.SignIn): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.SignUp): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.Invite): {
setHotkeyScope(PageHotkeyScope.SignInUp);
break;
}
case isMatchingLocation(AppPath.CreateProfile): {
setHotkeyScope(PageHotkeyScope.CreateProfile);
break;
}
case isMatchingLocation(AppPath.CreateWorkspace): {
setHotkeyScope(PageHotkeyScope.CreateWokspace);
break;
}
case isMatchingLocation(SettingsPath.ProfilePage, AppBasePath.Settings): {
setHotkeyScope(PageHotkeyScope.ProfilePage, { goto: true });
break;
}
case isMatchingLocation(
SettingsPath.WorkspaceMembersPage,
AppBasePath.Settings,
): {
setHotkeyScope(PageHotkeyScope.WorkspaceMemberPage, { goto: true });
break;
}
}
}, [isMatchingLocation, setHotkeyScope]);
return <></>;
}

View File

@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { useSetHotkeyScope } from '../modules/ui/hotkey/hooks/useSetHotkeyScope';
import { AppHotkeyScope } from '../modules/ui/hotkey/types/AppHotkeyScope';
export function InitializeHotkeyStorybookHook() {
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {
setHotkeyScope(AppHotkeyScope.App, { commandMenu: true, goto: false });
}, [setHotkeyScope]);
return <></>;
}

View File

@ -1,13 +1,12 @@
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { RecoilScope } from '../../modules/ui/recoil-scope/components/RecoilScope'; import { ColumnIndexContext } from '../../modules/ui/table/states/ColumnIndexContext';
import { CellContext } from '../../modules/ui/table/states/CellContext'; import { RowIndexContext } from '../../modules/ui/table/states/RowIndexContext';
import { RowContext } from '../../modules/ui/table/states/RowContext';
export const CellPositionDecorator: Decorator = (Story) => ( export const CellPositionDecorator: Decorator = (Story) => (
<RecoilScope SpecificContext={RowContext}> <RowIndexContext.Provider value={1}>
<RecoilScope SpecificContext={CellContext}> <ColumnIndexContext.Provider value={1}>
<Story /> <Story />
</RecoilScope> </ColumnIndexContext.Provider>
</RecoilScope> </RowIndexContext.Provider>
); );

View File

@ -1,11 +1,10 @@
import { HotkeysProvider } from 'react-hotkeys-hook';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { ClientConfigProvider } from '../../modules/client-config/components/ClientConfigProvider'; import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
import { INITIAL_HOTKEYS_SCOPES } from '../../modules/ui/hotkey/constants'; import { DefaultLayout } from '~/modules/ui/layout/components/DefaultLayout';
import { DefaultLayout } from '../../modules/ui/layout/components/DefaultLayout'; import { UserProvider } from '~/modules/users/components/UserProvider';
import { UserProvider } from '../../modules/users/components/UserProvider';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout'; import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
export type PageDecoratorArgs = { currentPath: string }; export type PageDecoratorArgs = { currentPath: string };
@ -16,15 +15,13 @@ export const PageDecorator: Decorator<{ currentPath: string }> = (
) => ( ) => (
<UserProvider> <UserProvider>
<ClientConfigProvider> <ClientConfigProvider>
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}> <MemoryRouter initialEntries={[args.currentPath]}>
<MemoryRouter initialEntries={[args.currentPath]}> <FullHeightStorybookLayout>
<FullHeightStorybookLayout> <DefaultLayout>
<DefaultLayout> <Story />
<Story /> </DefaultLayout>
</DefaultLayout> </FullHeightStorybookLayout>
</FullHeightStorybookLayout> </MemoryRouter>
</MemoryRouter>
</HotkeysProvider>
</ClientConfigProvider> </ClientConfigProvider>
</UserProvider> </UserProvider>
); );

View File

@ -2,11 +2,13 @@ import { ApolloProvider } from '@apollo/client';
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { InitializeHotkeyStorybookHook } from '../InitializeHotkeyStorybookHook';
import { mockedClient } from '../mockedClient'; import { mockedClient } from '../mockedClient';
export const RootDecorator: Decorator = (Story) => ( export const RootDecorator: Decorator = (Story) => (
<RecoilRoot> <RecoilRoot>
<ApolloProvider client={mockedClient}> <ApolloProvider client={mockedClient}>
<InitializeHotkeyStorybookHook />
<Story /> <Story />
</ApolloProvider> </ApolloProvider>
</RecoilRoot> </RecoilRoot>

View File

@ -0,0 +1,11 @@
import afterFrame from 'afterframe';
export function measureTotalFrameLoad(id: string) {
const timerId = `Total loading time for : ${id}`;
console.time(timerId);
afterFrame(() => {
console.timeEnd(timerId);
});
}

View File

@ -5877,6 +5877,11 @@ adjust-sourcemap-loader@^4.0.0:
loader-utils "^2.0.0" loader-utils "^2.0.0"
regex-parser "^2.2.11" regex-parser "^2.2.11"
afterframe@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/afterframe/-/afterframe-1.0.2.tgz#c63e17cdb29e4e60be2e618a315caf5ab5ade0c0"
integrity sha512-0JeMZI7dIfVs5guqLgidQNV7c6jBC2HO0QNSekAUB82Hr7PdU9QXNAF3kpFkvATvHYDDTGto7FPsRu1ey+aKJQ==
agent-base@5: agent-base@5:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"