Skip to content

Commit 4eb8592

Browse files
feat(settings): add loading state to save buttons (#11639)
Introduce a loading state to SaveButton and SaveAndCancelButtons components to enhance user feedback during save operations. Update SettingsNewObject to manage the loading state while submitting the form. Fix twentyhq/core-team-issues#572 --------- Co-authored-by: Charles Bochet <[email protected]>
1 parent d248e53 commit 4eb8592

File tree

6 files changed

+45
-27
lines changed

6 files changed

+45
-27
lines changed

packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveAndCancelButtons.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,27 @@ const StyledContainer = styled.div`
1111

1212
type SaveAndCancelButtonsProps = {
1313
onSave?: () => void;
14+
isLoading?: boolean;
1415
onCancel?: () => void;
1516
isSaveDisabled?: boolean;
1617
isCancelDisabled?: boolean;
1718
};
1819

1920
export const SaveAndCancelButtons = ({
2021
onSave,
22+
isLoading,
2123
onCancel,
2224
isSaveDisabled,
2325
isCancelDisabled,
2426
}: SaveAndCancelButtonsProps) => {
2527
return (
2628
<StyledContainer>
2729
<CancelButton onCancel={onCancel} disabled={isCancelDisabled} />
28-
<SaveButton onSave={onSave} disabled={isSaveDisabled} />
30+
<SaveButton
31+
onSave={onSave}
32+
disabled={isSaveDisabled}
33+
isLoading={isLoading}
34+
/>
2935
</StyledContainer>
3036
);
3137
};

packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveButton.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { t } from '@lingui/core/macro';
2-
import { Button } from 'twenty-ui/input';
32
import { IconDeviceFloppy } from 'twenty-ui/display';
3+
import { Button } from 'twenty-ui/input';
44

55
type SaveButtonProps = {
66
onSave?: () => void;
77
disabled?: boolean;
8+
isLoading?: boolean;
89
};
910

10-
export const SaveButton = ({ onSave, disabled }: SaveButtonProps) => {
11+
export const SaveButton = ({
12+
onSave,
13+
disabled,
14+
isLoading,
15+
}: SaveButtonProps) => {
1116
return (
1217
<Button
1318
title={t`Save`}
@@ -18,6 +23,7 @@ export const SaveButton = ({ onSave, disabled }: SaveButtonProps) => {
1823
onClick={onSave}
1924
type="submit"
2025
Icon={IconDeviceFloppy}
26+
isLoading={isLoading}
2127
/>
2228
);
2329
};

packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings';
1919
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
2020
import { H2Title } from 'twenty-ui/display';
2121
import { Section } from 'twenty-ui/layout';
22+
import { useState } from 'react';
2223

2324
export const SettingsNewObject = () => {
2425
const { t } = useLingui();
2526
const navigate = useNavigateSettings();
2627
const { enqueueSnackBar } = useSnackBar();
27-
28+
const [isLoading, setIsLoading] = useState(false);
2829
const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem();
2930

3031
const formConfig = useForm<SettingsDataModelObjectAboutFormValues>({
@@ -43,6 +44,7 @@ export const SettingsNewObject = () => {
4344
formValues: SettingsDataModelObjectAboutFormValues,
4445
) => {
4546
try {
47+
setIsLoading(true);
4648
const { data: response } = await createOneObjectMetadataItem(formValues);
4749

4850
navigate(
@@ -57,6 +59,8 @@ export const SettingsNewObject = () => {
5759
enqueueSnackBar((error as Error).message, {
5860
variant: SnackBarVariant.Error,
5961
});
62+
} finally {
63+
setIsLoading(false);
6064
}
6165
};
6266

@@ -79,6 +83,7 @@ export const SettingsNewObject = () => {
7983
actionButton={
8084
<SaveAndCancelButtons
8185
isSaveDisabled={!canSave}
86+
isLoading={isLoading}
8287
isCancelDisabled={isSubmitting}
8388
onCancel={() => navigate(SettingsPath.Objects)}
8489
onSave={formConfig.handleSubmit(handleSave)}

packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { zodResolver } from '@hookform/resolvers/zod';
22
import omit from 'lodash.omit';
33
import pick from 'lodash.pick';
4-
import { useEffect } from 'react';
4+
import { useEffect, useState } from 'react';
55
import { FormProvider, useForm } from 'react-hook-form';
66
import { useParams } from 'react-router-dom';
77
import { z } from 'zod';
@@ -59,8 +59,14 @@ export const SettingsObjectFieldEdit = () => {
5959
const { deactivateMetadataField, activateMetadataField } =
6060
useFieldMetadataItem();
6161

62+
const [newNameDuringSave, setNewNameDuringSave] = useState<string | null>(
63+
null,
64+
);
65+
6266
const fieldMetadataItem = objectMetadataItem?.fields.find(
63-
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
67+
(fieldMetadataItem) =>
68+
fieldMetadataItem.name === fieldName ||
69+
fieldMetadataItem.name === newNameDuringSave,
6470
);
6571

6672
const getRelationMetadata = useGetRelationMetadata();
@@ -100,6 +106,7 @@ export const SettingsObjectFieldEdit = () => {
100106
formValues: SettingsDataModelFieldEditFormValues,
101107
) => {
102108
const { dirtyFields } = formConfig.formState;
109+
setNewNameDuringSave(formValues.name);
103110

104111
try {
105112
if (
@@ -129,15 +136,15 @@ export const SettingsObjectFieldEdit = () => {
129136
Object.keys(otherDirtyFields),
130137
);
131138

132-
navigateSettings(SettingsPath.ObjectDetail, {
133-
objectNamePlural,
134-
});
135-
136139
await updateOneFieldMetadataItem({
137140
objectMetadataId: objectMetadataItem.id,
138141
fieldMetadataIdToUpdate: fieldMetadataItem.id,
139142
updatePayload: formattedInput,
140143
});
144+
145+
navigateSettings(SettingsPath.ObjectDetail, {
146+
objectNamePlural,
147+
});
141148
}
142149
} catch (error) {
143150
enqueueSnackBar((error as Error).message, {
@@ -187,6 +194,7 @@ export const SettingsObjectFieldEdit = () => {
187194
]}
188195
actionButton={
189196
<SaveAndCancelButtons
197+
isLoading={isSubmitting}
190198
isSaveDisabled={!canSave}
191199
isCancelDisabled={isSubmitting}
192200
onCancel={() =>

packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
3131
import { useNavigateApp } from '~/hooks/useNavigateApp';
3232
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
3333
import { DEFAULT_ICONS_BY_FIELD_TYPE } from '~/pages/settings/data-model/constants/DefaultIconsByFieldType';
34-
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
3534
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
3635

3736
type SettingsDataModelNewFieldFormValues = z.infer<
@@ -85,19 +84,14 @@ export const SettingsObjectNewFieldConfigure = () => {
8584
);
8685
}, [fieldType, formConfig]);
8786

88-
const [, setObjectViews] = useState<View[]>([]);
89-
const [, setRelationObjectViews] = useState<View[]>([]);
87+
const [isSaving, setIsSaving] = useState(false);
9088

9189
useFindManyRecords<View>({
9290
objectNameSingular: CoreObjectNameSingular.View,
9391
filter: {
9492
type: { eq: ViewType.Table },
9593
objectMetadataId: { eq: activeObjectMetadataItem?.id },
9694
},
97-
onCompleted: async (views) => {
98-
if (isUndefinedOrNull(views)) return;
99-
setObjectViews(views);
100-
},
10195
});
10296

10397
const relationObjectMetadataId = formConfig.watch(
@@ -111,10 +105,6 @@ export const SettingsObjectNewFieldConfigure = () => {
111105
type: { eq: ViewType.Table },
112106
objectMetadataId: { eq: relationObjectMetadataId },
113107
},
114-
onCompleted: async (views) => {
115-
if (isUndefinedOrNull(views)) return;
116-
setRelationObjectViews(views);
117-
},
118108
});
119109

120110
useEffect(() => {
@@ -132,10 +122,7 @@ export const SettingsObjectNewFieldConfigure = () => {
132122
formValues: SettingsDataModelNewFieldFormValues,
133123
) => {
134124
try {
135-
navigate(SettingsPath.ObjectDetail, {
136-
objectNamePlural,
137-
});
138-
125+
setIsSaving(true);
139126
if (
140127
formValues.type === FieldMetadataType.RELATION &&
141128
'relation' in formValues
@@ -163,7 +150,12 @@ export const SettingsObjectNewFieldConfigure = () => {
163150
await apolloClient.refetchQueries({
164151
include: ['FindManyViews', 'CombinedFindManyRecords'],
165152
});
153+
navigate(SettingsPath.ObjectDetail, {
154+
objectNamePlural,
155+
});
156+
setIsSaving(false);
166157
} catch (error) {
158+
setIsSaving(false);
167159
const isDuplicateFieldNameInObject = (error as Error).message.includes(
168160
'duplicate key value violates unique constraint "IndexOnNameObjectMetadataIdAndWorkspaceIdUnique"',
169161
);
@@ -206,6 +198,7 @@ export const SettingsObjectNewFieldConfigure = () => {
206198
]}
207199
actionButton={
208200
<SaveAndCancelButtons
201+
isLoading={isSaving}
209202
isSaveDisabled={!canSave}
210203
isCancelDisabled={isSubmitting}
211204
onCancel={() =>

packages/twenty-ui/src/feedback/loader/components/Loader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ const StyledLoaderContainer = styled.div<{
1515
border-radius: ${({ theme }) => theme.border.radius.pill};
1616
border: 1px solid
1717
${({ color, theme }) =>
18-
color ? theme.tag.text[color] : theme.font.color.tertiary};
18+
color ? theme.tag.text[color] : `var(--tw-button-color)`};
1919
overflow: hidden;
2020
`;
2121

2222
const StyledLoader = styled(motion.div)<{
2323
color?: ThemeColor;
2424
}>`
2525
background-color: ${({ color, theme }) =>
26-
color ? theme.tag.text[color] : theme.font.color.tertiary};
26+
color ? theme.tag.text[color] : `var(--tw-button-color)`};
2727
border-radius: ${({ theme }) => theme.border.radius.pill};
2828
height: 8px;
2929
width: 8px;

0 commit comments

Comments
 (0)