Sync table from frontend (#4894)

This PR:
- separates the existing updateSyncStatus endpoint into 2 endpoints
- creates mutations and hooks that will call those endpoints
- trigger the hook on toggle
- removes form logic and add a separated component for toggling

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-04-11 11:51:49 +02:00
committed by GitHub
parent bea6d4173c
commit aecf8783a0
20 changed files with 372 additions and 149 deletions

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const REMOTE_TABLE_FRAGMENT = gql`
fragment RemoteTableFields on RemoteTable {
name
schema
status
}
`;

View File

@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const SYNC_REMOTE_TABLE = gql`
${REMOTE_TABLE_FRAGMENT}
mutation syncRemoteTable($input: RemoteTableInput!) {
syncRemoteTable(input: $input) {
...RemoteTableFields
}
}
`;

View File

@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const UNSYNC_REMOTE_TABLE = gql`
${REMOTE_TABLE_FRAGMENT}
mutation unsyncRemoteTable($input: RemoteTableInput!) {
unsyncRemoteTable(input: $input) {
...RemoteTableFields
}
}
`;

View File

@ -1,11 +0,0 @@
import { gql } from '@apollo/client';
export const GET_MANY_DATABASE_CONNECTION_TABLES = gql`
query GetManyDatabaseConnectionTables($input: RemoteServerIdInput!) {
findAvailableRemoteTablesByServerId(input: $input) {
name
schema
status
}
}
`;

View File

@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
import { REMOTE_TABLE_FRAGMENT } from '@/databases/graphql/fragments/remoteTableFragment';
export const GET_MANY_REMOTE_TABLES = gql`
${REMOTE_TABLE_FRAGMENT}
query GetManyRemoteTables($input: RemoteServerIdInput!) {
findAvailableRemoteTablesByServerId(input: $input) {
...RemoteTableFields
}
}
`;

View File

@ -1,10 +1,10 @@
import { useQuery } from '@apollo/client';
import { GET_MANY_DATABASE_CONNECTION_TABLES } from '@/databases/graphql/queries/findManyDatabaseConnectionTables';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import {
GetManyDatabaseConnectionTablesQuery,
GetManyDatabaseConnectionTablesQueryVariables,
GetManyRemoteTablesQuery,
GetManyRemoteTablesQueryVariables,
} from '~/generated-metadata/graphql';
type UseGetDatabaseConnectionTablesParams = {
@ -19,9 +19,9 @@ export const useGetDatabaseConnectionTables = ({
const apolloMetadataClient = useApolloMetadataClient();
const { data } = useQuery<
GetManyDatabaseConnectionTablesQuery,
GetManyDatabaseConnectionTablesQueryVariables
>(GET_MANY_DATABASE_CONNECTION_TABLES, {
GetManyRemoteTablesQuery,
GetManyRemoteTablesQueryVariables
>(GET_MANY_REMOTE_TABLES, {
client: apolloMetadataClient ?? undefined,
skip: skip || !apolloMetadataClient,
variables: {

View File

@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { SYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/syncRemoteTable';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import {
RemoteTableInput,
SyncRemoteTableMutation,
SyncRemoteTableMutationVariables,
} from '~/generated-metadata/graphql';
export const useSyncRemoteTable = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
SyncRemoteTableMutation,
SyncRemoteTableMutationVariables
>(SYNC_REMOTE_TABLE, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const syncRemoteTable = useCallback(
async (input: RemoteTableInput) => {
return await mutate({
variables: {
input,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(GET_MANY_REMOTE_TABLES) ?? ''],
});
},
[mutate],
);
return {
syncRemoteTable,
};
};

View File

@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { ApolloClient, useMutation } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable';
import { GET_MANY_REMOTE_TABLES } from '@/databases/graphql/queries/findManyRemoteTables';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import {
RemoteTableInput,
UnsyncRemoteTableMutation,
UnsyncRemoteTableMutationVariables,
} from '~/generated-metadata/graphql';
export const useUnsyncRemoteTable = () => {
const apolloMetadataClient = useApolloMetadataClient();
const [mutate] = useMutation<
UnsyncRemoteTableMutation,
UnsyncRemoteTableMutationVariables
>(UNSYNC_REMOTE_TABLE, {
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
});
const unsyncRemoteTable = useCallback(
async (input: RemoteTableInput) => {
return await mutate({
variables: {
input,
},
awaitRefetchQueries: true,
refetchQueries: [getOperationName(GET_MANY_REMOTE_TABLES) ?? ''],
});
},
[mutate],
);
return {
unsyncRemoteTable,
};
};

View File

@ -1,10 +1,13 @@
import { Controller, useFormContext } from 'react-hook-form';
import { useCallback, useState } from 'react';
import styled from '@emotion/styled';
import { z } from 'zod';
import { useSyncRemoteTable } from '@/databases/hooks/useSyncRemoteTable';
import { useUnsyncRemoteTable } from '@/databases/hooks/useUnsyncRemoteTable';
import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { Toggle } from '@/ui/input/components/Toggle';
import { SettingsIntegrationRemoteTableSyncStatusToggle } from '@/settings/integrations/components/SettingsIntegrationRemoteTableSyncStatusToggle';
import { RemoteTable, RemoteTableStatus } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const settingsIntegrationsDatabaseTablesSchema = z.object({
syncedTablesByName: z.record(z.boolean()),
@ -15,6 +18,7 @@ export type SettingsIntegrationsDatabaseTablesFormValues = z.infer<
>;
type SettingsIntegrationDatabaseTablesListCardProps = {
connectionId: string;
tables: RemoteTable[];
};
@ -25,27 +29,63 @@ const StyledRowRightContainer = styled.div`
`;
export const SettingsIntegrationDatabaseTablesListCard = ({
connectionId,
tables,
}: SettingsIntegrationDatabaseTablesListCardProps) => {
const { control } =
useFormContext<SettingsIntegrationsDatabaseTablesFormValues>();
const { syncRemoteTable } = useSyncRemoteTable();
const { unsyncRemoteTable } = useUnsyncRemoteTable();
// We need to use a state because the table status update re-render the whole list of toggles
const [items] = useState(
tables.map((table) => ({
id: table.name,
...table,
})),
);
const onSyncUpdate = useCallback(
async (isSynced: boolean, tableName: string) => {
const table = items.find((table) => table.name === tableName);
if (!isDefined(table)) return;
if (isSynced) {
await syncRemoteTable({
remoteServerId: connectionId,
name: tableName,
schema: table.schema,
});
} else {
await unsyncRemoteTable({
remoteServerId: connectionId,
name: tableName,
schema: table.schema,
});
}
},
[connectionId, syncRemoteTable, items, unsyncRemoteTable],
);
const rowRightComponent = useCallback(
({
item,
}: {
item: { id: string; name: string; status: RemoteTableStatus };
}) => (
<StyledRowRightContainer>
<SettingsIntegrationRemoteTableSyncStatusToggle
table={item}
onSyncUpdate={onSyncUpdate}
/>
</StyledRowRightContainer>
),
[onSyncUpdate],
);
return (
<SettingsListCard
items={tables.map((table) => ({ id: table.name, ...table }))}
RowRightComponent={({ item: table }) => (
<StyledRowRightContainer>
<Controller
name={`syncedTablesByName.${table.name}`}
control={control}
defaultValue={table.status === RemoteTableStatus.Synced}
render={({ field: { onChange, value } }) => (
<Toggle value={value} onChange={onChange} />
)}
/>
</StyledRowRightContainer>
)}
getItemLabel={(table) => table.name}
items={items}
RowRightComponent={rowRightComponent}
getItemLabel={(table) => table.id}
/>
);
};

View File

@ -0,0 +1,29 @@
import { useState } from 'react';
import { Toggle } from '@/ui/input/components/Toggle';
import { RemoteTableStatus } from '~/generated-metadata/graphql';
export const SettingsIntegrationRemoteTableSyncStatusToggle = ({
table,
onSyncUpdate,
}: {
table: { id: string; name: string; status: RemoteTableStatus };
onSyncUpdate: (value: boolean, tableName: string) => Promise<void>;
}) => {
const [isToggleLoading, setIsToggleLoading] = useState(false);
const onChange = async (newValue: boolean) => {
if (isToggleLoading) return;
setIsToggleLoading(true);
await onSyncUpdate(newValue, table.name);
setIsToggleLoading(false);
};
return (
<Toggle
value={table.status === RemoteTableStatus.Synced}
disabled={isToggleLoading}
onChange={onChange}
/>
);
};

View File

@ -10,6 +10,7 @@ type ContainerProps = {
isOn: boolean;
color?: string;
toggleSize: ToggleSize;
disabled?: boolean;
};
const StyledContainer = styled.div<ContainerProps>`
@ -22,6 +23,8 @@ const StyledContainer = styled.div<ContainerProps>`
height: ${({ toggleSize }) => (toggleSize === 'small' ? 16 : 20)}px;
transition: background-color 0.3s ease;
width: ${({ toggleSize }) => (toggleSize === 'small' ? 24 : 32)}px;
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
`;
const StyledCircle = styled(motion.div)<{
@ -39,6 +42,7 @@ export type ToggleProps = {
color?: string;
toggleSize?: ToggleSize;
className?: string;
disabled?: boolean;
};
export const Toggle = ({
@ -47,6 +51,7 @@ export const Toggle = ({
color,
toggleSize = 'medium',
className,
disabled,
}: ToggleProps) => {
const [isOn, setIsOn] = useState(value ?? false);
@ -77,6 +82,7 @@ export const Toggle = ({
color={color}
toggleSize={toggleSize}
className={className}
disabled={disabled}
>
<StyledCircle
animate={isOn ? 'on' : 'off'}