mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 20:13:21 +03:00
Implement object fields and settings new layout (#7979)
### Description - This PR has as the base branch the TWNTY-5491 branch, but we also had to include updates from the main branch, and currently, there are conflicts in the TWNTY-5491, that cause errors on typescript in this PR, so, we can update once the conflicts are resolved on the base branch, but the functionality can be reviewed anyway - We Implemented a new layout of object details settings and new, the data is auto-saved in `Settings `tab of object detail - There is no indication to the user that data are saved automatically in the design, currently we are disabling the form ### Demo\ <https://www.loom.com/share/4198c0aa54b5450780a570ceee574838?sid=b4ef0a42-2d41-435f-9f5f-1b16816939f7> ### Refs #TWNTY-5491 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
parent
3be30651b7
commit
7bab65b569
@ -38,6 +38,10 @@ const StyledYear = styled.span`
|
|||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledTitleContainer = styled.div`
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
export const Calendar = ({
|
export const Calendar = ({
|
||||||
targetableObject,
|
targetableObject,
|
||||||
}: {
|
}: {
|
||||||
@ -131,6 +135,7 @@ export const Calendar = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section key={monthTime}>
|
<Section key={monthTime}>
|
||||||
|
<StyledTitleContainer>
|
||||||
<H3Title
|
<H3Title
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
@ -139,6 +144,7 @@ export const Calendar = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</StyledTitleContainer>
|
||||||
<CalendarMonthCard dayTimes={monthDayTimes} />
|
<CalendarMonthCard dayTimes={monthDayTimes} />
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
@ -143,12 +143,6 @@ const SettingsDevelopers = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const SettingsObjectEdit = lazy(() =>
|
|
||||||
import('~/pages/settings/data-model/SettingsObjectEdit').then((module) => ({
|
|
||||||
default: module.SettingsObjectEdit,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const SettingsIntegrations = lazy(() =>
|
const SettingsIntegrations = lazy(() =>
|
||||||
import('~/pages/settings/integrations/SettingsIntegrations').then(
|
import('~/pages/settings/integrations/SettingsIntegrations').then(
|
||||||
(module) => ({
|
(module) => ({
|
||||||
@ -292,7 +286,6 @@ export const SettingsRoutes = ({
|
|||||||
path={SettingsPath.ObjectDetail}
|
path={SettingsPath.ObjectDetail}
|
||||||
element={<SettingsObjectDetailPage />}
|
element={<SettingsObjectDetailPage />}
|
||||||
/>
|
/>
|
||||||
<Route path={SettingsPath.ObjectEdit} element={<SettingsObjectEdit />} />
|
|
||||||
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
|
<Route path={SettingsPath.NewObject} element={<SettingsNewObject />} />
|
||||||
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
|
<Route path={SettingsPath.Developers} element={<SettingsDevelopers />} />
|
||||||
{isCRMMigrationEnabled && (
|
{isCRMMigrationEnabled && (
|
||||||
|
@ -16,7 +16,7 @@ import { useApolloMetadataClient } from './useApolloMetadataClient';
|
|||||||
export const useUpdateOneObjectMetadataItem = () => {
|
export const useUpdateOneObjectMetadataItem = () => {
|
||||||
const apolloClientMetadata = useApolloMetadataClient();
|
const apolloClientMetadata = useApolloMetadataClient();
|
||||||
|
|
||||||
const [mutate] = useMutation<
|
const [mutate, { loading }] = useMutation<
|
||||||
UpdateOneObjectMetadataItemMutation,
|
UpdateOneObjectMetadataItemMutation,
|
||||||
UpdateOneObjectMetadataItemMutationVariables
|
UpdateOneObjectMetadataItemMutationVariables
|
||||||
>(UPDATE_ONE_OBJECT_METADATA_ITEM, {
|
>(UPDATE_ONE_OBJECT_METADATA_ITEM, {
|
||||||
@ -42,5 +42,6 @@ export const useUpdateOneObjectMetadataItem = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
updateOneObjectMetadataItem,
|
updateOneObjectMetadataItem,
|
||||||
|
loading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,9 @@ import styled from '@emotion/styled';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const StyledSettingsPageContainer = styled.div<{ width?: number }>`
|
const StyledSettingsPageContainer = styled.div<{
|
||||||
|
width?: number;
|
||||||
|
}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(8)};
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||||
|
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Button, H2Title, IconPlus, Section, UndecoratedLink } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledDiv = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ObjectFieldsProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
|
||||||
|
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Fields"
|
||||||
|
description={`Customise the fields available in the ${objectMetadataItem.labelSingular} views and their display order in the ${objectMetadataItem.labelSingular} detail view and menus.`}
|
||||||
|
/>
|
||||||
|
<SettingsObjectFieldTable
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
mode="view"
|
||||||
|
/>
|
||||||
|
{shouldDisplayAddFieldButton && (
|
||||||
|
<StyledDiv>
|
||||||
|
<UndecoratedLink to={'./new-field/select'}>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Add Field"
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
</StyledDiv>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
import { H2Title, Section } from 'twenty-ui';
|
||||||
|
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
||||||
|
|
||||||
|
type ObjectIndexesProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectIndexes = ({ objectMetadataItem }: ObjectIndexesProps) => {
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Indexes"
|
||||||
|
description={`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
||||||
|
/>
|
||||||
|
<SettingsObjectIndexTable objectMetadataItem={objectMetadataItem} />
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
@ -1,20 +1,16 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import pick from 'lodash.pick';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
import { Button, H2Title, IconArchive, Section } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z, ZodError } from 'zod';
|
||||||
|
|
||||||
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem';
|
||||||
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import {
|
import {
|
||||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||||
SettingsDataModelObjectAboutForm,
|
SettingsDataModelObjectAboutForm,
|
||||||
@ -24,13 +20,15 @@ import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-mo
|
|||||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||||
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
|
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import pick from 'lodash.pick';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
const objectEditFormSchema = z
|
const objectEditFormSchema = z
|
||||||
@ -42,21 +40,30 @@ type SettingsDataModelObjectEditFormValues = z.infer<
|
|||||||
typeof objectEditFormSchema
|
typeof objectEditFormSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const SettingsObjectEdit = () => {
|
type ObjectSettingsProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContentContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFormSection = styled(Section)`
|
||||||
|
padding-left: 0 !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const setUpdatedObjectSlugState = useSetRecoilState(updatedObjectSlugState);
|
||||||
|
|
||||||
const { objectSlug = '' } = useParams();
|
|
||||||
const { findActiveObjectMetadataItemBySlug } =
|
|
||||||
useFilteredObjectMetadataItems();
|
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
const { lastVisitedObjectMetadataItemId } =
|
const { lastVisitedObjectMetadataItemId } =
|
||||||
useLastVisitedObjectMetadataItem();
|
useLastVisitedObjectMetadataItem();
|
||||||
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
|
||||||
|
|
||||||
const settingsObjectsPagePath = getSettingsPagePath(SettingsPath.Objects);
|
const settingsObjectsPagePath = getSettingsPagePath(SettingsPath.Objects);
|
||||||
|
|
||||||
const formConfig = useForm<SettingsDataModelObjectEditFormValues>({
|
const formConfig = useForm<SettingsDataModelObjectEditFormValues>({
|
||||||
@ -68,15 +75,6 @@ export const SettingsObjectEdit = () => {
|
|||||||
navigationMemorizedUrlState,
|
navigationMemorizedUrlState,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
|
||||||
}, [activeObjectMetadataItem, navigate]);
|
|
||||||
|
|
||||||
if (!activeObjectMetadataItem) return null;
|
|
||||||
|
|
||||||
const { isDirty, isValid, isSubmitting } = formConfig.formState;
|
|
||||||
const canSave = isDirty && isValid && !isSubmitting;
|
|
||||||
|
|
||||||
const getUpdatePayload = (
|
const getUpdatePayload = (
|
||||||
formValues: SettingsDataModelObjectEditFormValues,
|
formValues: SettingsDataModelObjectEditFormValues,
|
||||||
) => {
|
) => {
|
||||||
@ -88,19 +86,19 @@ export const SettingsObjectEdit = () => {
|
|||||||
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
IS_LABEL_SYNCED_WITH_NAME_LABEL,
|
||||||
)
|
)
|
||||||
? (formValues.isLabelSyncedWithName as boolean)
|
? (formValues.isLabelSyncedWithName as boolean)
|
||||||
: activeObjectMetadataItem.isLabelSyncedWithName;
|
: objectMetadataItem.isLabelSyncedWithName;
|
||||||
|
|
||||||
if (shouldComputeNamesFromLabels) {
|
if (shouldComputeNamesFromLabels) {
|
||||||
values = {
|
values = {
|
||||||
...values,
|
...values,
|
||||||
...(values.labelSingular
|
...(values.labelSingular && dirtyFieldKeys.includes('labelSingular')
|
||||||
? {
|
? {
|
||||||
nameSingular: computeMetadataNameFromLabelOrThrow(
|
nameSingular: computeMetadataNameFromLabelOrThrow(
|
||||||
formValues.labelSingular,
|
formValues.labelSingular,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(values.labelPlural
|
...(values.labelPlural && dirtyFieldKeys.includes('labelPlural')
|
||||||
? {
|
? {
|
||||||
namePlural: computeMetadataNameFromLabelOrThrow(
|
namePlural: computeMetadataNameFromLabelOrThrow(
|
||||||
formValues.labelPlural,
|
formValues.labelPlural,
|
||||||
@ -113,8 +111,14 @@ export const SettingsObjectEdit = () => {
|
|||||||
return settingsUpdateObjectInputSchema.parse(
|
return settingsUpdateObjectInputSchema.parse(
|
||||||
pick(values, [
|
pick(values, [
|
||||||
...dirtyFieldKeys,
|
...dirtyFieldKeys,
|
||||||
...(values.namePlural ? ['namePlural'] : []),
|
...(shouldComputeNamesFromLabels &&
|
||||||
...(values.nameSingular ? ['nameSingular'] : []),
|
dirtyFieldKeys.includes('labelPlural')
|
||||||
|
? ['namePlural']
|
||||||
|
: []),
|
||||||
|
...(shouldComputeNamesFromLabels &&
|
||||||
|
dirtyFieldKeys.includes('labelSingular')
|
||||||
|
? ['nameSingular']
|
||||||
|
: []),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -122,41 +126,53 @@ export const SettingsObjectEdit = () => {
|
|||||||
const handleSave = async (
|
const handleSave = async (
|
||||||
formValues: SettingsDataModelObjectEditFormValues,
|
formValues: SettingsDataModelObjectEditFormValues,
|
||||||
) => {
|
) => {
|
||||||
|
if (isEmpty(formConfig.formState.dirtyFields) === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const updatePayload = getUpdatePayload(formValues);
|
const updatePayload = getUpdatePayload(formValues);
|
||||||
|
const objectNamePluralForRedirection =
|
||||||
|
updatePayload.namePlural ?? objectMetadataItem.namePlural;
|
||||||
|
const objectSlug = getObjectSlug({
|
||||||
|
...updatePayload,
|
||||||
|
namePlural: objectNamePluralForRedirection,
|
||||||
|
});
|
||||||
|
|
||||||
|
setUpdatedObjectSlugState(objectSlug);
|
||||||
|
|
||||||
await updateOneObjectMetadataItem({
|
await updateOneObjectMetadataItem({
|
||||||
idToUpdate: activeObjectMetadataItem.id,
|
idToUpdate: objectMetadataItem.id,
|
||||||
updatePayload,
|
updatePayload,
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectNamePluralForRedirection =
|
formConfig.reset(undefined, { keepValues: true });
|
||||||
updatePayload.namePlural ?? activeObjectMetadataItem.namePlural;
|
|
||||||
|
|
||||||
if (lastVisitedObjectMetadataItemId === activeObjectMetadataItem.id) {
|
if (lastVisitedObjectMetadataItemId === objectMetadataItem.id) {
|
||||||
const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId(
|
const lastVisitedView = getLastVisitedViewIdFromObjectMetadataItemId(
|
||||||
activeObjectMetadataItem.id,
|
objectMetadataItem.id,
|
||||||
);
|
);
|
||||||
setNavigationMemorizedUrl(
|
setNavigationMemorizedUrl(
|
||||||
`/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`,
|
`/objects/${objectNamePluralForRedirection}?view=${lastVisitedView}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(
|
navigate(`${settingsObjectsPagePath}/${objectSlug}`);
|
||||||
`${settingsObjectsPagePath}/${getObjectSlug({
|
|
||||||
...updatePayload,
|
|
||||||
namePlural: objectNamePluralForRedirection,
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
enqueueSnackBar(error.issues[0].message, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
enqueueSnackBar((error as Error).message, {
|
enqueueSnackBar((error as Error).message, {
|
||||||
variant: SnackBarVariant.Error,
|
variant: SnackBarVariant.Error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisable = async () => {
|
const handleDisable = async () => {
|
||||||
await updateOneObjectMetadataItem({
|
await updateOneObjectMetadataItem({
|
||||||
idToUpdate: activeObjectMetadataItem.id,
|
idToUpdate: objectMetadataItem.id,
|
||||||
updatePayload: { isActive: false },
|
updatePayload: { isActive: false },
|
||||||
});
|
});
|
||||||
navigate(settingsObjectsPagePath);
|
navigate(settingsObjectsPagePath);
|
||||||
@ -165,57 +181,33 @@ export const SettingsObjectEdit = () => {
|
|||||||
return (
|
return (
|
||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<FormProvider {...formConfig}>
|
<FormProvider {...formConfig}>
|
||||||
<SubMenuTopBarContainer
|
<StyledContentContainer>
|
||||||
title="Edit"
|
<StyledFormSection>
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: 'Workspace',
|
|
||||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: 'Objects',
|
|
||||||
href: settingsObjectsPagePath,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
|
||||||
href: `${settingsObjectsPagePath}/${objectSlug}`,
|
|
||||||
},
|
|
||||||
{ children: 'Edit Object' },
|
|
||||||
]}
|
|
||||||
actionButton={
|
|
||||||
activeObjectMetadataItem.isCustom && (
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
isCancelDisabled={isSubmitting}
|
|
||||||
onCancel={() =>
|
|
||||||
navigate(`${settingsObjectsPagePath}/${objectSlug}`)
|
|
||||||
}
|
|
||||||
onSave={formConfig.handleSubmit(handleSave)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SettingsPageContainer>
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
<H2Title
|
||||||
title="About"
|
title="About"
|
||||||
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
description="Name in both singular (e.g., 'Invoice') and plural (e.g., 'Invoices') forms."
|
||||||
/>
|
/>
|
||||||
<SettingsDataModelObjectAboutForm
|
<SettingsDataModelObjectAboutForm
|
||||||
disabled={!activeObjectMetadataItem.isCustom}
|
disabled={!objectMetadataItem.isCustom}
|
||||||
disableNameEdit={!activeObjectMetadataItem.isCustom}
|
disableNameEdit={!objectMetadataItem.isCustom}
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
onBlur={() => {
|
||||||
|
formConfig.handleSubmit(handleSave)();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</StyledFormSection>
|
||||||
|
<StyledFormSection>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Settings"
|
title="Options"
|
||||||
description="Choose the fields that will identify your records"
|
description="Choose the fields that will identify your records"
|
||||||
/>
|
/>
|
||||||
<SettingsDataModelObjectSettingsFormCard
|
<SettingsDataModelObjectSettingsFormCard
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
</StyledFormSection>
|
||||||
|
<StyledFormSection>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Danger zone" description="Deactivate object" />
|
<H2Title title="Danger zone" description="Deactivate object" />
|
||||||
<Button
|
<Button
|
||||||
@ -225,8 +217,8 @@ export const SettingsObjectEdit = () => {
|
|||||||
onClick={handleDisable}
|
onClick={handleDisable}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</SettingsPageContainer>
|
</StyledFormSection>
|
||||||
</SubMenuTopBarContainer>
|
</StyledContentContainer>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
);
|
);
|
@ -49,6 +49,7 @@ type SettingsDataModelObjectAboutFormProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
disableNameEdit?: boolean;
|
disableNameEdit?: boolean;
|
||||||
objectMetadataItem?: ObjectMetadataItem;
|
objectMetadataItem?: ObjectMetadataItem;
|
||||||
|
onBlur?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledInputsContainer = styled.div`
|
const StyledInputsContainer = styled.div`
|
||||||
@ -68,12 +69,16 @@ const StyledAdvancedSettingsSectionInputWrapper = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAdvancedSettingsOuterContainer = styled.div`
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledAdvancedSettingsContainer = styled.div`
|
const StyledAdvancedSettingsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
padding-top: ${({ theme }) => theme.spacing(4)};
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
@ -81,7 +86,7 @@ const StyledAdvancedSettingsContainer = styled.div`
|
|||||||
const StyledIconToolContainer = styled.div`
|
const StyledIconToolContainer = styled.div`
|
||||||
border-right: 1px solid ${MAIN_COLORS.yellow};
|
border-right: 1px solid ${MAIN_COLORS.yellow};
|
||||||
display: flex;
|
display: flex;
|
||||||
left: ${({ theme }) => theme.spacing(-5)};
|
left: ${({ theme }) => theme.spacing(-6)};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
@ -105,6 +110,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
disabled,
|
disabled,
|
||||||
disableNameEdit,
|
disableNameEdit,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
onBlur,
|
||||||
}: SettingsDataModelObjectAboutFormProps) => {
|
}: SettingsDataModelObjectAboutFormProps) => {
|
||||||
const { control, watch, setValue } =
|
const { control, watch, setValue } =
|
||||||
useFormContext<SettingsDataModelObjectAboutFormValues>();
|
useFormContext<SettingsDataModelObjectAboutFormValues>();
|
||||||
@ -117,6 +123,9 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
const isLabelSyncedWithName = watch(IS_LABEL_SYNCED_WITH_NAME_LABEL);
|
const isLabelSyncedWithName = watch(IS_LABEL_SYNCED_WITH_NAME_LABEL);
|
||||||
const labelSingular = watch('labelSingular');
|
const labelSingular = watch('labelSingular');
|
||||||
const labelPlural = watch('labelPlural');
|
const labelPlural = watch('labelPlural');
|
||||||
|
watch('nameSingular');
|
||||||
|
watch('namePlural');
|
||||||
|
watch('description');
|
||||||
const apiNameTooltipText = isLabelSyncedWithName
|
const apiNameTooltipText = isLabelSyncedWithName
|
||||||
? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name'
|
? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name'
|
||||||
: 'Input must be in camel case and cannot start with a number';
|
: 'Input must be in camel case and cannot start with a number';
|
||||||
@ -138,14 +147,14 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
setValue(
|
setValue(
|
||||||
'nameSingular',
|
'nameSingular',
|
||||||
computeMetadataNameFromLabelOrThrow(labelSingular),
|
computeMetadataNameFromLabelOrThrow(labelSingular),
|
||||||
{ shouldDirty: false },
|
{ shouldDirty: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
||||||
isDefined(labelPlural) &&
|
isDefined(labelPlural) &&
|
||||||
setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), {
|
setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), {
|
||||||
shouldDirty: false,
|
shouldDirty: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,6 +193,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
fillNameSingularFromLabelSingular(value);
|
fillNameSingularFromLabelSingular(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onBlur={onBlur}
|
||||||
disabled={disabled || disableNameEdit}
|
disabled={disabled || disableNameEdit}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
@ -236,6 +246,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
exit="exit"
|
exit="exit"
|
||||||
variants={motionAnimationVariants}
|
variants={motionAnimationVariants}
|
||||||
>
|
>
|
||||||
|
<StyledAdvancedSettingsOuterContainer>
|
||||||
<StyledAdvancedSettingsContainer>
|
<StyledAdvancedSettingsContainer>
|
||||||
<StyledIconToolContainer>
|
<StyledIconToolContainer>
|
||||||
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
<StyledIconTool size={12} color={MAIN_COLORS.yellow} />
|
||||||
@ -286,6 +297,7 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
maxLength={OBJECT_NAME_MAXIMUM_LENGTH}
|
||||||
|
onBlur={onBlur}
|
||||||
RightIcon={() =>
|
RightIcon={() =>
|
||||||
tooltip && (
|
tooltip && (
|
||||||
<>
|
<>
|
||||||
@ -293,15 +305,15 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
id={infoCircleElementId + fieldName}
|
id={infoCircleElementId + fieldName}
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
color={theme.font.color.tertiary}
|
color={theme.font.color.tertiary}
|
||||||
|
style={{ outline: 'none' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
anchorSelect={`#${infoCircleElementId}${fieldName}`}
|
anchorSelect={`#${infoCircleElementId}${fieldName}`}
|
||||||
content={tooltip}
|
content={tooltip}
|
||||||
offset={5}
|
offset={5}
|
||||||
noArrow
|
noArrow
|
||||||
place="bottom"
|
place="bottom"
|
||||||
positionStrategy="absolute"
|
positionStrategy="fixed"
|
||||||
delay={TooltipDelay.shortDelay}
|
delay={TooltipDelay.shortDelay}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -323,18 +335,21 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<SyncObjectLabelAndNameToggle
|
<SyncObjectLabelAndNameToggle
|
||||||
value={value ?? true}
|
value={value ?? true}
|
||||||
|
disabled={!objectMetadataItem?.isCustom}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onChange(value);
|
onChange(value);
|
||||||
if (value === true) {
|
if (value === true) {
|
||||||
fillNamePluralFromLabelPlural(labelPlural);
|
fillNamePluralFromLabelPlural(labelPlural);
|
||||||
fillNameSingularFromLabelSingular(labelSingular);
|
fillNameSingularFromLabelSingular(labelSingular);
|
||||||
}
|
}
|
||||||
|
onBlur?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</StyledAdvancedSettingsSectionInputWrapper>
|
</StyledAdvancedSettingsSectionInputWrapper>
|
||||||
</StyledAdvancedSettingsContainer>
|
</StyledAdvancedSettingsContainer>
|
||||||
|
</StyledAdvancedSettingsOuterContainer>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
@ -5,10 +5,11 @@ import { IconRefresh, MAIN_COLORS, Toggle } from 'twenty-ui';
|
|||||||
const StyledToggleContainer = styled.div`
|
const StyledToggleContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: ${({ theme }) => theme.spacing(4)};
|
padding: ${({ theme }) => theme.spacing(4)};
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledIconRefreshContainer = styled.div`
|
const StyledIconRefreshContainer = styled.div`
|
||||||
@ -40,17 +41,19 @@ const StyledDescription = styled.h3`
|
|||||||
font-size: ${({ theme }) => theme.font.size.md};
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type SyncObjectLabelAndNameToggleProps = {
|
type SyncObjectLabelAndNameToggleProps = {
|
||||||
value: boolean;
|
value: boolean;
|
||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SyncObjectLabelAndNameToggle = ({
|
export const SyncObjectLabelAndNameToggle = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled,
|
||||||
}: SyncObjectLabelAndNameToggleProps) => {
|
}: SyncObjectLabelAndNameToggleProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
@ -66,7 +69,12 @@ export const SyncObjectLabelAndNameToggle = ({
|
|||||||
</StyledDescription>
|
</StyledDescription>
|
||||||
</div>
|
</div>
|
||||||
</StyledTitleContainer>
|
</StyledTitleContainer>
|
||||||
<Toggle onChange={onChange} color={MAIN_COLORS.yellow} value={value} />
|
<Toggle
|
||||||
|
onChange={onChange}
|
||||||
|
color={MAIN_COLORS.yellow}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
</StyledToggleContainer>
|
</StyledToggleContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,6 @@ export enum SettingsPath {
|
|||||||
Objects = 'objects',
|
Objects = 'objects',
|
||||||
ObjectOverview = 'objects/overview',
|
ObjectOverview = 'objects/overview',
|
||||||
ObjectDetail = 'objects/:objectSlug',
|
ObjectDetail = 'objects/:objectSlug',
|
||||||
ObjectEdit = 'objects/:objectSlug/edit',
|
|
||||||
ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select',
|
ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select',
|
||||||
ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure',
|
ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure',
|
||||||
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
||||||
|
@ -17,6 +17,7 @@ export type TextAreaProps = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onBlur?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -70,6 +71,7 @@ export const TextArea = ({
|
|||||||
value = '',
|
value = '',
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
|
onBlur,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
const computedMinRows = Math.min(minRows, MAX_ROWS);
|
||||||
|
|
||||||
@ -86,6 +88,7 @@ export const TextArea = ({
|
|||||||
|
|
||||||
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
|
onBlur?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -99,7 +99,7 @@ const StyledTrailingIconContainer = styled.div<
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -10,7 +10,7 @@ import { PageHeader } from './PageHeader';
|
|||||||
|
|
||||||
type SubMenuTopBarContainerProps = {
|
type SubMenuTopBarContainerProps = {
|
||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[];
|
||||||
title?: string;
|
title?: string | JSX.Element;
|
||||||
actionButton?: ReactNode;
|
actionButton?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
links: BreadcrumbProps['links'];
|
links: BreadcrumbProps['links'];
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
import { IconComponent, Pill } from 'twenty-ui';
|
import { IconComponent, Pill } from 'twenty-ui';
|
||||||
|
|
||||||
type TabProps = {
|
type TabProps = {
|
||||||
@ -10,7 +11,7 @@ type TabProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
pill?: string;
|
pill?: string | ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
|
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
|
||||||
@ -73,7 +74,7 @@ export const Tab = ({
|
|||||||
<StyledHover>
|
<StyledHover>
|
||||||
{Icon && <Icon size={theme.icon.size.md} />}
|
{Icon && <Icon size={theme.icon.size.md} />}
|
||||||
{title}
|
{title}
|
||||||
{pill && <Pill label={pill} />}
|
{pill && typeof pill === 'string' ? <Pill label={pill} /> : pill}
|
||||||
</StyledHover>
|
</StyledHover>
|
||||||
</StyledTab>
|
</StyledTab>
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,7 @@ export type SingleTabProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
pill?: string;
|
pill?: string | React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabListProps = {
|
type TabListProps = {
|
||||||
|
@ -2,9 +2,71 @@ import { useEffect } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { ObjectFields } from '@/settings/data-model/object-details/components/tabs/ObjectFields';
|
||||||
|
import { ObjectIndexes } from '@/settings/data-model/object-details/components/tabs/ObjectIndexes';
|
||||||
|
import { ObjectSettings } from '@/settings/data-model/object-details/components/tabs/ObjectSettings';
|
||||||
|
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
|
||||||
|
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SettingsObjectDetailPageContent } from '~/pages/settings/data-model/SettingsObjectDetailPageContent';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
H3Title,
|
||||||
|
IconCodeCircle,
|
||||||
|
IconListDetails,
|
||||||
|
IconPlus,
|
||||||
|
IconSettings,
|
||||||
|
IconTool,
|
||||||
|
isDefined,
|
||||||
|
MAIN_COLORS,
|
||||||
|
UndecoratedLink,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
||||||
|
|
||||||
|
const StyledTabListContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
height: ${({ theme }) => theme.spacing(10)};
|
||||||
|
.tab-list {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
.tab-list > div {
|
||||||
|
padding: ${({ theme }) => theme.spacing(3) + ' 0'};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledContentContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledObjectTypeTag = styled(SettingsDataModelObjectTypeTag)`
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: ${({ theme }) => theme.spacing(5)};
|
||||||
|
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TAB_LIST_COMPONENT_ID = 'object-details-tab-list';
|
||||||
|
const FIELDS_TAB_ID = 'fields';
|
||||||
|
const SETTINGS_TAB_ID = 'settings';
|
||||||
|
const INDEXES_TAB_ID = 'indexes';
|
||||||
|
|
||||||
export const SettingsObjectDetailPage = () => {
|
export const SettingsObjectDetailPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -13,18 +75,115 @@ export const SettingsObjectDetailPage = () => {
|
|||||||
const { findActiveObjectMetadataItemBySlug } =
|
const { findActiveObjectMetadataItemBySlug } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
const [updatedObjectSlug, setUpdatedObjectSlug] = useRecoilState(
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
updatedObjectSlugState,
|
||||||
|
);
|
||||||
|
const objectMetadataItem =
|
||||||
|
findActiveObjectMetadataItemBySlug(objectSlug) ??
|
||||||
|
findActiveObjectMetadataItemBySlug(updatedObjectSlug);
|
||||||
|
|
||||||
|
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||||
|
const activeTabId = useRecoilValue(activeTabIdState);
|
||||||
|
|
||||||
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
||||||
|
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_UNIQUE_INDEXES_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
if (objectSlug === updatedObjectSlug) setUpdatedObjectSlug('');
|
||||||
}, [activeObjectMetadataItem, navigate]);
|
if (!isDefined(objectMetadataItem)) navigate(AppPath.NotFound);
|
||||||
|
}, [
|
||||||
|
objectMetadataItem,
|
||||||
|
navigate,
|
||||||
|
objectSlug,
|
||||||
|
updatedObjectSlug,
|
||||||
|
setUpdatedObjectSlug,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!isDefined(activeObjectMetadataItem)) return <></>;
|
if (!isDefined(objectMetadataItem)) return <></>;
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: FIELDS_TAB_ID,
|
||||||
|
title: 'Fields',
|
||||||
|
Icon: IconListDetails,
|
||||||
|
hide: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: SETTINGS_TAB_ID,
|
||||||
|
title: 'Settings',
|
||||||
|
Icon: IconSettings,
|
||||||
|
hide: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: INDEXES_TAB_ID,
|
||||||
|
title: 'Indexes',
|
||||||
|
Icon: IconCodeCircle,
|
||||||
|
hide: !isAdvancedModeEnabled || !isUniqueIndexesEnabled,
|
||||||
|
pill: <IconTool size={12} color={MAIN_COLORS.yellow} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderActiveTabContent = () => {
|
||||||
|
switch (activeTabId) {
|
||||||
|
case FIELDS_TAB_ID:
|
||||||
|
return <ObjectFields objectMetadataItem={objectMetadataItem} />;
|
||||||
|
case SETTINGS_TAB_ID:
|
||||||
|
return <ObjectSettings objectMetadataItem={objectMetadataItem} />;
|
||||||
|
case INDEXES_TAB_ID:
|
||||||
|
return <ObjectIndexes objectMetadataItem={objectMetadataItem} />;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsObjectDetailPageContent
|
<SubMenuTopBarContainer
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
title={
|
||||||
|
<StyledTitleContainer>
|
||||||
|
<H3Title title={objectMetadataItem.labelPlural} />
|
||||||
|
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
||||||
|
</StyledTitleContainer>
|
||||||
|
}
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Workspace',
|
||||||
|
href: getSettingsPagePath(SettingsPath.Workspace),
|
||||||
|
},
|
||||||
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
|
{
|
||||||
|
children: objectMetadataItem.labelPlural,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
actionButton={
|
||||||
|
activeTabId === FIELDS_TAB_ID && (
|
||||||
|
<UndecoratedLink to={'./new-field/select'}>
|
||||||
|
<Button
|
||||||
|
title="New Field"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
accent="blue"
|
||||||
|
Icon={IconPlus}
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<StyledTabListContainer>
|
||||||
|
<TabList
|
||||||
|
tabListId={TAB_LIST_COMPONENT_ID}
|
||||||
|
tabs={tabs}
|
||||||
|
className="tab-list"
|
||||||
|
/>
|
||||||
|
</StyledTabListContainer>
|
||||||
|
<StyledContentContainer>
|
||||||
|
{renderActiveTabContent()}
|
||||||
|
</StyledContentContainer>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
||||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { Button, H2Title, IconPlus, Section, UndecoratedLink } from 'twenty-ui';
|
|
||||||
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
|
||||||
import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable';
|
|
||||||
|
|
||||||
const StyledDiv = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type SettingsObjectDetailPageContentProps = {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsObjectDetailPageContent = ({
|
|
||||||
objectMetadataItem,
|
|
||||||
}: SettingsObjectDetailPageContentProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
|
||||||
|
|
||||||
const handleDisableObject = async () => {
|
|
||||||
await updateOneObjectMetadataItem({
|
|
||||||
idToUpdate: objectMetadataItem.id,
|
|
||||||
updatePayload: { isActive: false },
|
|
||||||
});
|
|
||||||
navigate(getSettingsPagePath(SettingsPath.Objects));
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
|
||||||
|
|
||||||
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
|
||||||
|
|
||||||
const isUniqueIndexesEnabled = useIsFeatureEnabled(
|
|
||||||
'IS_UNIQUE_INDEXES_ENABLED',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SubMenuTopBarContainer
|
|
||||||
title={objectMetadataItem.labelPlural}
|
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: 'Workspace',
|
|
||||||
href: getSettingsPagePath(SettingsPath.Workspace),
|
|
||||||
},
|
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
|
||||||
{ children: objectMetadataItem.labelPlural },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SettingsPageContainer>
|
|
||||||
<Section>
|
|
||||||
<H2Title title="About" description="Manage your object" />
|
|
||||||
<SettingsObjectSummaryCard
|
|
||||||
iconKey={objectMetadataItem.icon ?? undefined}
|
|
||||||
name={objectMetadataItem.labelPlural || ''}
|
|
||||||
objectMetadataItem={objectMetadataItem}
|
|
||||||
onDeactivate={handleDisableObject}
|
|
||||||
onEdit={() => navigate('./edit')}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Fields"
|
|
||||||
description={`Customise the fields available in the ${objectMetadataItem.labelSingular} views and their display order in the ${objectMetadataItem.labelSingular} detail view and menus.`}
|
|
||||||
/>
|
|
||||||
<SettingsObjectFieldTable
|
|
||||||
objectMetadataItem={objectMetadataItem}
|
|
||||||
mode="view"
|
|
||||||
/>
|
|
||||||
{shouldDisplayAddFieldButton && (
|
|
||||||
<StyledDiv>
|
|
||||||
<UndecoratedLink to={'./new-field/select'}>
|
|
||||||
<Button
|
|
||||||
Icon={IconPlus}
|
|
||||||
title="Add Field"
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</UndecoratedLink>
|
|
||||||
</StyledDiv>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
{isAdvancedModeEnabled && isUniqueIndexesEnabled && (
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Indexes"
|
|
||||||
description={`Advanced feature to improve the performance of queries and to enforce unicity constraints.`}
|
|
||||||
/>
|
|
||||||
<SettingsObjectIndexTable objectMetadataItem={objectMetadataItem} />
|
|
||||||
</Section>
|
|
||||||
)}
|
|
||||||
</SettingsPageContainer>
|
|
||||||
</SubMenuTopBarContainer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/test';
|
import { userEvent, within } from '@storybook/test';
|
||||||
|
|
||||||
@ -39,20 +40,15 @@ export const CustomObject: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ObjectDropdownMenu: Story = {
|
export const ObjectTabs: Story = {
|
||||||
play: async () => {
|
play: async () => {
|
||||||
const canvas = within(document.body);
|
const canvas = within(document.body);
|
||||||
const objectSummaryVerticalDotsIconButton = await canvas.findByRole(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
name: 'Object Options',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(objectSummaryVerticalDotsIconButton);
|
const fieldsTab = await canvas.findByTestId('tab-fields');
|
||||||
|
const settingsTab = await canvas.findByTestId('tab-settings');
|
||||||
|
|
||||||
await canvas.findByText('Edit');
|
await expect(fieldsTab).toBeVisible();
|
||||||
await canvas.findByText('Deactivate');
|
await expect(settingsTab).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
PageDecorator,
|
|
||||||
PageDecoratorArgs,
|
|
||||||
} from '~/testing/decorators/PageDecorator';
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
|
||||||
import { sleep } from '~/utils/sleep';
|
|
||||||
|
|
||||||
import { SettingsObjectEdit } from '../SettingsObjectEdit';
|
|
||||||
|
|
||||||
const meta: Meta<PageDecoratorArgs> = {
|
|
||||||
title: 'Pages/Settings/DataModel/SettingsObjectEdit',
|
|
||||||
component: SettingsObjectEdit,
|
|
||||||
decorators: [PageDecorator],
|
|
||||||
args: {
|
|
||||||
routePath: '/settings/objects/:objectSlug/edit',
|
|
||||||
routeParams: { ':objectSlug': 'companies' },
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
msw: graphqlMocks,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
export type Story = StoryObj<typeof SettingsObjectEdit>;
|
|
||||||
|
|
||||||
export const StandardObject: Story = {
|
|
||||||
play: async () => {
|
|
||||||
await sleep(100);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomObject: Story = {
|
|
||||||
args: {
|
|
||||||
routeParams: { ':objectSlug': 'my-custom-objects' },
|
|
||||||
},
|
|
||||||
};
|
|
@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const updatedObjectSlugState = createState<string>({
|
||||||
|
key: 'updatedObjectSlugState',
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
@ -428,11 +428,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
await this.objectMetadataMigrationService.createRenameTableMigration(
|
await this.objectMetadataMigrationService.createRenameTableMigration(
|
||||||
existingObjectMetadata,
|
existingObjectMetadata,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
|
objectMetadataForUpdate.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.createRelationsUpdatesMigrations(
|
await this.objectMetadataMigrationService.createRelationsUpdatesMigrations(
|
||||||
existingObjectMetadata,
|
existingObjectMetadata,
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
|
objectMetadataForUpdate.workspaceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
public async createRenameTableMigration(
|
public async createRenameTableMigration(
|
||||||
existingObjectMetadata: ObjectMetadataEntity,
|
existingObjectMetadata: ObjectMetadataEntity,
|
||||||
objectMetadataForUpdate: ObjectMetadataEntity,
|
objectMetadataForUpdate: ObjectMetadataEntity,
|
||||||
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const newTargetTableName = computeObjectTargetTable(
|
const newTargetTableName = computeObjectTargetTable(
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
@ -103,7 +104,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
|
|
||||||
this.workspaceMigrationService.createCustomMigration(
|
this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`),
|
||||||
objectMetadataForUpdate.workspaceId,
|
workspaceId,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: existingTargetTableName,
|
name: existingTargetTableName,
|
||||||
@ -117,6 +118,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
public async createRelationsUpdatesMigrations(
|
public async createRelationsUpdatesMigrations(
|
||||||
existingObjectMetadata: ObjectMetadataEntity,
|
existingObjectMetadata: ObjectMetadataEntity,
|
||||||
updatedObjectMetadata: ObjectMetadataEntity,
|
updatedObjectMetadata: ObjectMetadataEntity,
|
||||||
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const existingTableName = computeObjectTargetTable(existingObjectMetadata);
|
const existingTableName = computeObjectTargetTable(existingObjectMetadata);
|
||||||
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
|
const newTableName = computeObjectTargetTable(updatedObjectMetadata);
|
||||||
@ -128,6 +130,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
isForeignKey: true,
|
isForeignKey: true,
|
||||||
},
|
},
|
||||||
name: `${existingObjectMetadata.nameSingular}Id`,
|
name: `${existingObjectMetadata.nameSingular}Id`,
|
||||||
|
workspaceId: workspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldsWihStandardRelation = await this.fieldMetadataRepository.find(
|
const fieldsWihStandardRelation = await this.fieldMetadataRepository.find(
|
||||||
@ -150,7 +153,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
fieldsWihStandardRelation.map(async (fieldWihStandardRelation) => {
|
fieldsWihStandardRelation.map(async (fieldWihStandardRelation) => {
|
||||||
const relatedObject = await this.objectMetadataRepository.findOneBy({
|
const relatedObject = await this.objectMetadataRepository.findOneBy({
|
||||||
id: fieldWihStandardRelation.objectMetadataId,
|
id: fieldWihStandardRelation.objectMetadataId,
|
||||||
workspaceId: updatedObjectMetadata.workspaceId,
|
workspaceId: workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (relatedObject) {
|
if (relatedObject) {
|
||||||
@ -175,7 +178,7 @@ export class ObjectMetadataMigrationService {
|
|||||||
generateMigrationName(
|
generateMigrationName(
|
||||||
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
|
`rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`,
|
||||||
),
|
),
|
||||||
updatedObjectMetadata.workspaceId,
|
workspaceId,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: relationTableName,
|
name: relationTableName,
|
||||||
|
@ -50,6 +50,7 @@ export {
|
|||||||
IconClockHour8,
|
IconClockHour8,
|
||||||
IconClockShare,
|
IconClockShare,
|
||||||
IconCode,
|
IconCode,
|
||||||
|
IconCodeCircle,
|
||||||
IconCoins,
|
IconCoins,
|
||||||
IconColorSwatch,
|
IconColorSwatch,
|
||||||
IconMessageCircle as IconComment,
|
IconMessageCircle as IconComment,
|
||||||
@ -159,6 +160,7 @@ export {
|
|||||||
IconLinkOff,
|
IconLinkOff,
|
||||||
IconList,
|
IconList,
|
||||||
IconListCheck,
|
IconListCheck,
|
||||||
|
IconListDetails,
|
||||||
IconListNumbers,
|
IconListNumbers,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconLockOpen,
|
IconLockOpen,
|
||||||
|
@ -31,7 +31,7 @@ const StyledDescription = styled.h3`
|
|||||||
font-size: ${({ theme }) => theme.font.size.md};
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const H2Title = ({
|
export const H2Title = ({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
type H3TitleProps = {
|
type H3TitleProps = {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
@ -11,7 +11,6 @@ const StyledH3Title = styled.h3`
|
|||||||
font-size: ${({ theme }) => theme.font.size.lg};
|
font-size: ${({ theme }) => theme.font.size.lg};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const H3Title = ({ title, className }: H3TitleProps) => {
|
export const H3Title = ({ title, className }: H3TitleProps) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user