Feat/show page metadata (#2234)

* Fix view fetch bug

* Finished types

* Removed console.log

* Fixed todo

* Working Object Show Page

* Minor fixes

* Fix custom object requests pending (#2240)

* Fix custom object requests pending

* fix typo

* Fix various bugs

* Typo

* Fix

* Fix

* Fix

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-10-27 11:06:07 +02:00
committed by GitHub
parent 5ba68e997d
commit 3f2e1b622e
28 changed files with 335 additions and 33 deletions

View File

@ -4,6 +4,7 @@ module.exports = {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
ecmaVersion: "2023"
},
plugins: [
'@typescript-eslint/eslint-plugin',

View File

@ -22,6 +22,20 @@ module.exports = {
'@': path.resolve(__dirname, 'src/modules'),
'@testing': path.resolve(__dirname, 'src/testing'),
},
mode: 'extends',
// TODO: remove this workaround by resolving source map errors with @sniptt/guards
configure: {
module: {
rules: [
{
test: /\.js$/,
enforce: "pre",
use: ["source-map-loader"],
},
],
},
ignoreWarnings: [/Failed to parse source map/],
},
},
jest: {
configure: {

View File

@ -180,7 +180,7 @@
"storybook-addon-cookie": "^3.0.1",
"storybook-addon-pseudo-states": "^2.1.0",
"ts-jest": "^29.1.0",
"typescript": "^4.9.3",
"typescript": "^5.2.2",
"webpack": "^5.75.0"
},
"msw": {

View File

@ -1,5 +1,6 @@
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { ObjectShowPage } from '@/metadata/components/ObjectShowPage';
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
@ -63,6 +64,7 @@ export const App = () => {
<Route path={AppPath.OpportunitiesPage} element={<Opportunities />} />
<Route path={AppPath.ObjectTablePage} element={<ObjectTablePage />} />
<Route path={AppPath.ObjectShowPage} element={<ObjectShowPage />} />
<Route
path={AppPath.SettingsCatchAll}

View File

@ -10,7 +10,10 @@ import { useFindManyObjects } from '../hooks/useFindManyObjects';
import { useSetObjectDataTableData } from '../hooks/useSetDataTableData';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
export type ObjectDataTableEffectProps = MetadataObjectIdentifier;
export type ObjectDataTableEffectProps = Pick<
MetadataObjectIdentifier,
'objectNamePlural'
>;
export const ObjectDataTableEffect = ({
objectNamePlural,
@ -33,10 +36,11 @@ export const ObjectDataTableEffect = ({
const tableRecoilScopeId = useRecoilScopeId(TableRecoilScopeContext);
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId: string) => {
const currentView = await snapshot.getPromise(
(viewId: string) => {
const currentView = snapshot.getLoadable(
currentViewIdScopedState({ scopeId: tableRecoilScopeId }),
);
).getValue()
if (currentView === viewId) {
return;
}

View File

@ -0,0 +1,159 @@
import { useParams } from 'react-router-dom';
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
import { FieldContext } from '@/ui/data/field/contexts/FieldContext';
import { entityFieldsFamilyState } from '@/ui/data/field/states/entityFieldsFamilyState';
import { InlineCell } from '@/ui/data/inline-cell/components/InlineCell';
import { PropertyBox } from '@/ui/data/inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/ui/data/inline-cell/types/InlineCellHotkeyScope';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { PageBody } from '@/ui/layout/page/PageBody';
import { PageContainer } from '@/ui/layout/page/PageContainer';
import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton';
import { PageHeader } from '@/ui/layout/page/PageHeader';
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useFindOneMetadataObject } from '../hooks/useFindOneMetadataObject';
import { useFindOneObject } from '../hooks/useFindOneObject';
import { useUpdateOneObject } from '../hooks/useUpdateOneObject';
import { formatMetadataFieldAsColumnDefinition } from '../utils/formatMetadataFieldAsColumnDefinition';
export const ObjectShowPage = () => {
const { objectNameSingular, objectId } = useParams<{
objectNameSingular: string;
objectId: string;
}>();
const { foundMetadataObject } = useFindOneMetadataObject({
objectNameSingular,
});
const [, setEntityFields] = useRecoilState(
entityFieldsFamilyState(objectId ?? ''),
);
const { object } = useFindOneObject({
objectId: objectId,
objectNameSingular,
onCompleted: (data) => {
setEntityFields(data);
},
});
const useUpdateOneObjectMutation: () => [(params: any) => any, any] = () => {
const { updateOneObject } = useUpdateOneObject({
objectNameSingular,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
updateOneObject?.({
idToUpdate: variables.where.id,
input: variables.data,
});
};
return [updateEntity, { loading: false }];
};
const handleFavoriteButtonClick = async () => {
//
};
if (!object) return <></>;
return (
<PageContainer>
<PageTitle title={object.name || 'No Name'} />
<PageHeader
title={object.name ?? ''}
hasBackButton
Icon={IconBuildingSkyscraper}
>
<PageFavoriteButton
isFavorite={false}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton
key="add"
entity={{
id: object.id,
type: ActivityTargetableEntityType.Company,
}}
/>
</PageHeader>
<PageBody>
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
<ShowPageContainer>
<ShowPageLeftContainer>
<ShowPageSummaryCard
id={object.id}
logoOrAvatar={''}
title={object.name ?? 'No name'}
date={object.createdAt ?? ''}
renderTitleEditComponent={() => <></>}
avatarType="squared"
/>
<PropertyBox extraPadding={true}>
{foundMetadataObject?.fields
.toSorted((a, b) =>
DateTime.fromISO(a.createdAt)
.diff(DateTime.fromISO(b.createdAt))
.toMillis(),
)
.map((metadataField, index) => {
return (
<FieldContext.Provider
key={object.id + metadataField.id}
value={{
entityId: object.id,
recoilScopeId: object.id + metadataField.id,
fieldDefinition:
formatMetadataFieldAsColumnDefinition({
field: metadataField,
index: index,
metadataObject: foundMetadataObject,
}),
useUpdateEntityMutation: useUpdateOneObjectMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<InlineCell />
</FieldContext.Provider>
);
})}
</PropertyBox>
</ShowPageLeftContainer>
<ShowPageRightContainer
entity={{
id: object.id,
type: ActivityTargetableEntityType.Company,
}}
timeline
tasks
notes
emails
/>
</ShowPageContainer>
</RecoilScope>
</PageBody>
</PageContainer>
);
};

View File

@ -6,7 +6,10 @@ import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { ObjectDataTableEffect } from './ObjectDataTableEffect';
export type ObjectTableProps = MetadataObjectIdentifier;
export type ObjectTableProps = Pick<
MetadataObjectIdentifier,
'objectNamePlural'
>;
export const ObjectTable = ({ objectNamePlural }: ObjectTableProps) => {
const { updateOneObject } = useUpdateOneObject({

View File

@ -24,7 +24,10 @@ const StyledTableContainer = styled.div`
width: 100%;
`;
export type ObjectTablePageProps = MetadataObjectIdentifier;
export type ObjectTablePageProps = Pick<
MetadataObjectIdentifier,
'objectNamePlural'
>;
export const ObjectTablePage = () => {
const objectNamePlural = useParams().objectNamePlural ?? '';

View File

@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
export const FIND_MANY_METADATA_OBJECTS = gql`
query MetadataObjects {
objects {
objects(paging: { first: 100 }) {
edges {
node {
id
@ -17,7 +17,7 @@ export const FIND_MANY_METADATA_OBJECTS = gql`
isActive
createdAt
updatedAt
fields {
fields(paging: { first: 100 }) {
edges {
node {
id

View File

@ -169,8 +169,8 @@ export const useCreateNewTempsCustomObject = () => {
});
const createdFields = [
emailFieldData,
nameFieldData,
emailFieldData,
cityFieldData,
phoneFieldData,
twitterFieldData,

View File

@ -7,7 +7,7 @@ import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useCreateOneObject = ({
objectNamePlural,
}: MetadataObjectIdentifier) => {
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => {
const {
foundMetadataObject,
objectNotFoundInMetadata,

View File

@ -7,7 +7,7 @@ import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useDeleteOneObject = ({
objectNamePlural,
}: MetadataObjectIdentifier) => {
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => {
const {
foundMetadataObject,
objectNotFoundInMetadata,

View File

@ -13,7 +13,7 @@ export const useFindManyObjects = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNamePlural,
}: MetadataObjectIdentifier) => {
}: Pick<MetadataObjectIdentifier, 'objectNamePlural'>) => {
const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } =
useFindOneMetadataObject({
objectNamePlural,
@ -28,10 +28,12 @@ export const useFindManyObjects = <
const objects = useMemo(
() =>
formatPagedObjectsToObjects({
pagedObjects: data,
objectNamePlural,
}),
objectNamePlural
? formatPagedObjectsToObjects({
pagedObjects: data,
objectNamePlural,
})
: [],
[data, objectNamePlural],
);

View File

@ -7,16 +7,20 @@ import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { formatMetadataFieldAsColumnDefinition } from '../utils/formatMetadataFieldAsColumnDefinition';
import { generateCreateOneObjectMutation } from '../utils/generateCreateOneObjectMutation';
import { generateFindManyCustomObjectsQuery } from '../utils/generateFindManyCustomObjectsQuery';
import { generateFindOneCustomObjectQuery } from '../utils/generateFindOneCustomObjectQuery';
import { useFindManyMetadataObjects } from './useFindManyMetadataObjects';
export const useFindOneMetadataObject = ({
objectNamePlural,
objectNameSingular,
}: MetadataObjectIdentifier) => {
const { metadataObjects, loading } = useFindManyMetadataObjects();
const foundMetadataObject = metadataObjects.find(
(object) => object.namePlural === objectNamePlural,
(object) =>
object.namePlural === objectNamePlural ||
object.nameSingular === objectNameSingular,
);
const objectNotFoundInMetadata =
@ -28,6 +32,7 @@ export const useFindOneMetadataObject = ({
formatMetadataFieldAsColumnDefinition({
index,
field,
metadataObject: foundMetadataObject,
}),
) ?? [];
@ -41,6 +46,16 @@ export const useFindOneMetadataObject = ({
}
`;
const findOneQuery = foundMetadataObject
? generateFindOneCustomObjectQuery({
metadataObject: foundMetadataObject,
})
: gql`
query EmptyQuery {
empty
}
`;
const createOneMutation = foundMetadataObject
? generateCreateOneObjectMutation({
metadataObject: foundMetadataObject,
@ -67,6 +82,7 @@ export const useFindOneMetadataObject = ({
objectNotFoundInMetadata,
columnDefinitions,
findManyQuery,
findOneQuery,
createOneMutation,
deleteOneMutation,
loading,

View File

@ -0,0 +1,46 @@
import { useQuery } from '@apollo/client';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useFindOneObject = <
ObjectType extends { id: string } & Record<string, any>,
>({
objectNameSingular,
objectId,
onCompleted,
}: Pick<MetadataObjectIdentifier, 'objectNameSingular'> & {
objectId: string | undefined;
onCompleted?: (data: ObjectType) => void;
}) => {
const { foundMetadataObject, objectNotFoundInMetadata, findOneQuery } =
useFindOneMetadataObject({
objectNameSingular,
});
const { data, loading, error } = useQuery<
{ [nameSingular: string]: ObjectType },
{ objectId: string }
>(findOneQuery, {
skip: !foundMetadataObject || !objectId,
variables: {
objectId: objectId ?? '',
},
onCompleted: (data) => {
if (onCompleted && objectNameSingular) {
onCompleted(data[objectNameSingular]);
}
},
});
const object =
objectNameSingular && data ? data[objectNameSingular] : undefined;
return {
object,
loading,
error,
objectNotFoundInMetadata,
};
};

View File

@ -1,4 +1,5 @@
import { gql, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { generateUpdateOneObjectMutation } from '../utils/generateUpdateOneObjectMutation';
@ -7,10 +8,12 @@ import { useFindOneMetadataObject } from './useFindOneMetadataObject';
export const useUpdateOneObject = ({
objectNamePlural,
objectNameSingular,
}: MetadataObjectIdentifier) => {
const { foundMetadataObject, objectNotFoundInMetadata } =
const { foundMetadataObject, objectNotFoundInMetadata, findManyQuery } =
useFindOneMetadataObject({
objectNamePlural,
objectNameSingular,
});
const generatedMutation = foundMetadataObject
@ -24,7 +27,9 @@ export const useUpdateOneObject = ({
`;
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(generatedMutation);
const [mutate] = useMutation(generatedMutation, {
refetchQueries: [getOperationName(findManyQuery) ?? ''],
});
const updateOneObject = foundMetadataObject
? ({

View File

@ -1,3 +1,4 @@
export type MetadataObjectIdentifier = {
objectNamePlural: string;
objectNamePlural?: string;
objectNameSingular?: string;
};

View File

@ -20,9 +20,11 @@ const parseFieldType = (fieldType: string): FieldType => {
export const formatMetadataFieldAsColumnDefinition = ({
index,
field,
metadataObject,
}: {
index: number;
field: MetadataObject['fields'][0];
metadataObject: Omit<MetadataObject, 'fields'>;
}): ColumnDefinition<FieldMetadata> => ({
index,
key: field.name,
@ -35,4 +37,5 @@ export const formatMetadataFieldAsColumnDefinition = ({
},
Icon: IconBrandLinkedin,
isVisible: true,
basePathToShowPage: `/object/${metadataObject.nameSingular}/`,
});

View File

@ -0,0 +1,24 @@
import { gql } from '@apollo/client';
import { MetadataObject } from '../types/MetadataObject';
import { mapFieldMetadataToGraphQLQuery } from './mapFieldMetadataToGraphQLQuery';
export const generateFindOneCustomObjectQuery = ({
metadataObject,
}: {
metadataObject: MetadataObject;
}) => {
return gql`
query FindOne${metadataObject.nameSingular}($objectId: UUID!) {
${metadataObject.nameSingular}(filter: {
id: {
eq: $objectId
}
}){
id
${metadataObject.fields.map(mapFieldMetadataToGraphQLQuery).join('\n')}
}
}
`;
};

View File

@ -31,7 +31,8 @@ export const SettingsObjectFieldDataType = ({
value,
}: SettingsObjectFieldDataTypeProps) => {
const theme = useTheme();
const { label, Icon } = dataTypes[value];
const { label, Icon } = dataTypes?.[value];
return (
<StyledDataType value={value}>

View File

@ -37,6 +37,18 @@ export const SettingsObjectFieldItemTableRow = ({
const theme = useTheme();
const { Icon } = useLazyLoadIcon(fieldItem.icon ?? '');
// TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example)
const fieldDataTypeIsSupported = [
'text',
'number',
'boolean',
'url',
].includes(fieldItem.type);
if (!fieldDataTypeIsSupported) {
return <></>;
}
return (
<StyledObjectFieldTableRow>
<StyledNameTableCell>

View File

@ -19,6 +19,8 @@ export enum AppPath {
OpportunitiesPage = '/opportunities',
ObjectTablePage = '/objects/:objectNamePlural',
ObjectShowPage = '/object/:objectNameSingular/:objectId',
SettingsCatchAll = `/settings/*`,
DevelopersCatchAll = `/developers/*`,

View File

@ -6,9 +6,9 @@ import { isTableCellInEditModeFamilyState } from '../states/isTableCellInEditMod
export const useCloseCurrentTableCellInEditMode = () =>
useRecoilCallback(({ set, snapshot }) => {
return async () => {
const currentTableCellInEditModePosition = await snapshot.getPromise(
currentTableCellInEditModePositionState,
);
const currentTableCellInEditModePosition = snapshot
.getLoadable(currentTableCellInEditModePositionState)
.valueOrThrow();
set(
isTableCellInEditModeFamilyState(currentTableCellInEditModePosition),

View File

@ -110,6 +110,7 @@ export const BoardOptionsDropdownContent = ({
const viewEditMode = snapshot
.getLoadable(viewEditModeScopedState({ scopeId: boardRecoilScopeId }))
.getValue();
if (!viewEditMode) {
return;
}

View File

@ -23,9 +23,10 @@ export const useSetHotkeyScope = () =>
useRecoilCallback(
({ snapshot, set }) =>
async (hotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = await snapshot.getPromise(
currentHotkeyScopeState,
);
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
if (!isDefined(customScopes)) {
if (

View File

@ -13,6 +13,7 @@ import {
} from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '../../graphql/queries/getViewFields';
import { GET_VIEWS } from '@/views/graphql/queries/getViews';
export const toViewFieldInput = (
objectId: string,
@ -92,6 +93,7 @@ export const useViewFields = (viewScopeId: string) => {
viewId_key: { key: viewField.key, viewId: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
}),
),
);

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": ["dom", "dom.iterable", "esnext", "ES2023", "ES2023.Array"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,

View File

@ -18729,10 +18729,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript@^4.9.3:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
ua-parser-js@^1.0.35:
version "1.0.35"