diff --git a/front/package.json b/front/package.json index 06ac7d7a05..8218d1e805 100644 --- a/front/package.json +++ b/front/package.json @@ -37,7 +37,7 @@ "lodash.debounce": "^4.0.8", "luxon": "^3.3.0", "react": "^18.2.0", - "react-data-grid": "7.0.0-beta.13", + "react-data-grid": "^7.0.0-beta.36", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index b82a3daf4a..846da6d9c3 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -647,6 +647,18 @@ export type CompanyCreateInput = { updatedAt?: InputMaybe; }; +export type CompanyCreateManyInput = { + accountOwnerId?: InputMaybe; + address: Scalars['String']; + createdAt?: InputMaybe; + domainName: Scalars['String']; + employees?: InputMaybe; + id?: InputMaybe; + linkedinUrl?: InputMaybe; + name: Scalars['String']; + updatedAt?: InputMaybe; +}; + export type CompanyCreateNestedOneWithoutActivityTargetInput = { connect?: InputMaybe; }; @@ -953,6 +965,8 @@ export type Mutation = { createEvent: Analytics; createFavoriteForCompany: Favorite; createFavoriteForPerson: Favorite; + createManyCompany: AffectedRows; + createManyPerson: AffectedRows; createManyView: AffectedRows; createManyViewField: AffectedRows; createManyViewSort: AffectedRows; @@ -1021,6 +1035,18 @@ export type MutationCreateFavoriteForPersonArgs = { }; +export type MutationCreateManyCompanyArgs = { + data: Array; + skipDuplicates?: InputMaybe; +}; + + +export type MutationCreateManyPersonArgs = { + data: Array; + skipDuplicates?: InputMaybe; +}; + + export type MutationCreateManyViewArgs = { data: Array; skipDuplicates?: InputMaybe; @@ -1383,6 +1409,22 @@ export type PersonCreateInput = { xUrl?: InputMaybe; }; +export type PersonCreateManyInput = { + avatarUrl?: InputMaybe; + city?: InputMaybe; + companyId?: InputMaybe; + createdAt?: InputMaybe; + email?: InputMaybe; + firstName?: InputMaybe; + id?: InputMaybe; + jobTitle?: InputMaybe; + lastName?: InputMaybe; + linkedinUrl?: InputMaybe; + phone?: InputMaybe; + updatedAt?: InputMaybe; + xUrl?: InputMaybe; +}; + export type PersonCreateNestedManyWithoutCompanyInput = { connect?: InputMaybe>; }; @@ -2851,6 +2893,13 @@ export type DeleteManyCompaniesMutationVariables = Exact<{ export type DeleteManyCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } }; +export type InsertManyCompanyMutationVariables = Exact<{ + data: Array | CompanyCreateManyInput; +}>; + + +export type InsertManyCompanyMutation = { __typename?: 'Mutation', createManyCompany: { __typename?: 'AffectedRows', count: number } }; + export type InsertOneCompanyMutationVariables = Exact<{ data: CompanyCreateInput; }>; @@ -2916,6 +2965,13 @@ export type DeleteManyPersonMutationVariables = Exact<{ export type DeleteManyPersonMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } }; +export type InsertManyPersonMutationVariables = Exact<{ + data: Array | PersonCreateManyInput; +}>; + + +export type InsertManyPersonMutation = { __typename?: 'Mutation', createManyPerson: { __typename?: 'AffectedRows', count: number } }; + export type InsertOnePersonMutationVariables = Exact<{ data: PersonCreateInput; }>; @@ -3147,13 +3203,6 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>; export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null }> }; -export type CreateViewsMutationVariables = Exact<{ - data: Array | ViewCreateManyInput; -}>; - - -export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } }; - export type CreateViewFieldsMutationVariables = Exact<{ data: Array | ViewFieldCreateManyInput; }>; @@ -3168,6 +3217,13 @@ export type CreateViewSortsMutationVariables = Exact<{ export type CreateViewSortsMutation = { __typename?: 'Mutation', createManyViewSort: { __typename?: 'AffectedRows', count: number } }; +export type CreateViewsMutationVariables = Exact<{ + data: Array | ViewCreateManyInput; +}>; + + +export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } }; + export type DeleteViewSortsMutationVariables = Exact<{ where: ViewSortWhereInput; }>; @@ -3199,13 +3255,6 @@ export type UpdateViewSortMutationVariables = Exact<{ export type UpdateViewSortMutation = { __typename?: 'Mutation', viewSort: { __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string } }; -export type GetViewsQueryVariables = Exact<{ - where?: InputMaybe; -}>; - - -export type GetViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'View', id: string, name: string }> }; - export type GetViewFieldsQueryVariables = Exact<{ where?: InputMaybe; orderBy?: InputMaybe | ViewFieldOrderByWithRelationInput>; @@ -3221,6 +3270,13 @@ export type GetViewSortsQueryVariables = Exact<{ export type GetViewSortsQuery = { __typename?: 'Query', viewSorts: Array<{ __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string }> }; +export type GetViewsQueryVariables = Exact<{ + where?: InputMaybe; +}>; + + +export type GetViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'View', id: string, name: string }> }; + export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -4149,6 +4205,39 @@ export function useDeleteManyCompaniesMutation(baseOptions?: Apollo.MutationHook export type DeleteManyCompaniesMutationHookResult = ReturnType; export type DeleteManyCompaniesMutationResult = Apollo.MutationResult; export type DeleteManyCompaniesMutationOptions = Apollo.BaseMutationOptions; +export const InsertManyCompanyDocument = gql` + mutation InsertManyCompany($data: [CompanyCreateManyInput!]!) { + createManyCompany(data: $data) { + count + } +} + `; +export type InsertManyCompanyMutationFn = Apollo.MutationFunction; + +/** + * __useInsertManyCompanyMutation__ + * + * To run a mutation, you first call `useInsertManyCompanyMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInsertManyCompanyMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [insertManyCompanyMutation, { data, loading, error }] = useInsertManyCompanyMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useInsertManyCompanyMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InsertManyCompanyDocument, options); + } +export type InsertManyCompanyMutationHookResult = ReturnType; +export type InsertManyCompanyMutationResult = Apollo.MutationResult; +export type InsertManyCompanyMutationOptions = Apollo.BaseMutationOptions; export const InsertOneCompanyDocument = gql` mutation InsertOneCompany($data: CompanyCreateInput!) { createOneCompany(data: $data) { @@ -4517,6 +4606,39 @@ export function useDeleteManyPersonMutation(baseOptions?: Apollo.MutationHookOpt export type DeleteManyPersonMutationHookResult = ReturnType; export type DeleteManyPersonMutationResult = Apollo.MutationResult; export type DeleteManyPersonMutationOptions = Apollo.BaseMutationOptions; +export const InsertManyPersonDocument = gql` + mutation InsertManyPerson($data: [PersonCreateManyInput!]!) { + createManyPerson(data: $data) { + count + } +} + `; +export type InsertManyPersonMutationFn = Apollo.MutationFunction; + +/** + * __useInsertManyPersonMutation__ + * + * To run a mutation, you first call `useInsertManyPersonMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useInsertManyPersonMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [insertManyPersonMutation, { data, loading, error }] = useInsertManyPersonMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useInsertManyPersonMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(InsertManyPersonDocument, options); + } +export type InsertManyPersonMutationHookResult = ReturnType; +export type InsertManyPersonMutationResult = Apollo.MutationResult; +export type InsertManyPersonMutationOptions = Apollo.BaseMutationOptions; export const InsertOnePersonDocument = gql` mutation InsertOnePerson($data: PersonCreateInput!) { createOnePerson(data: $data) { @@ -5763,39 +5885,6 @@ export function useGetUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type GetUsersLazyQueryHookResult = ReturnType; export type GetUsersQueryResult = Apollo.QueryResult; -export const CreateViewsDocument = gql` - mutation CreateViews($data: [ViewCreateManyInput!]!) { - createManyView(data: $data) { - count - } -} - `; -export type CreateViewsMutationFn = Apollo.MutationFunction; - -/** - * __useCreateViewsMutation__ - * - * To run a mutation, you first call `useCreateViewsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateViewsMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [createViewsMutation, { data, loading, error }] = useCreateViewsMutation({ - * variables: { - * data: // value for 'data' - * }, - * }); - */ -export function useCreateViewsMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(CreateViewsDocument, options); - } -export type CreateViewsMutationHookResult = ReturnType; -export type CreateViewsMutationResult = Apollo.MutationResult; -export type CreateViewsMutationOptions = Apollo.BaseMutationOptions; export const CreateViewFieldsDocument = gql` mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { createManyViewField(data: $data) { @@ -5862,6 +5951,39 @@ export function useCreateViewSortsMutation(baseOptions?: Apollo.MutationHookOpti export type CreateViewSortsMutationHookResult = ReturnType; export type CreateViewSortsMutationResult = Apollo.MutationResult; export type CreateViewSortsMutationOptions = Apollo.BaseMutationOptions; +export const CreateViewsDocument = gql` + mutation CreateViews($data: [ViewCreateManyInput!]!) { + createManyView(data: $data) { + count + } +} + `; +export type CreateViewsMutationFn = Apollo.MutationFunction; + +/** + * __useCreateViewsMutation__ + * + * To run a mutation, you first call `useCreateViewsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateViewsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createViewsMutation, { data, loading, error }] = useCreateViewsMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useCreateViewsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateViewsDocument, options); + } +export type CreateViewsMutationHookResult = ReturnType; +export type CreateViewsMutationResult = Apollo.MutationResult; +export type CreateViewsMutationOptions = Apollo.BaseMutationOptions; export const DeleteViewSortsDocument = gql` mutation DeleteViewSorts($where: ViewSortWhereInput!) { deleteManyViewSort(where: $where) { @@ -6004,42 +6126,6 @@ export function useUpdateViewSortMutation(baseOptions?: Apollo.MutationHookOptio export type UpdateViewSortMutationHookResult = ReturnType; export type UpdateViewSortMutationResult = Apollo.MutationResult; export type UpdateViewSortMutationOptions = Apollo.BaseMutationOptions; -export const GetViewsDocument = gql` - query GetViews($where: ViewWhereInput) { - views: findManyView(where: $where) { - id - name - } -} - `; - -/** - * __useGetViewsQuery__ - * - * To run a query within a React component, call `useGetViewsQuery` and pass it any options that fit your needs. - * When your component renders, `useGetViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetViewsQuery({ - * variables: { - * where: // value for 'where' - * }, - * }); - */ -export function useGetViewsQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetViewsDocument, options); - } -export function useGetViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetViewsDocument, options); - } -export type GetViewsQueryHookResult = ReturnType; -export type GetViewsLazyQueryHookResult = ReturnType; -export type GetViewsQueryResult = Apollo.QueryResult; export const GetViewFieldsDocument = gql` query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) { viewFields: findManyViewField(where: $where, orderBy: $orderBy) { @@ -6117,6 +6203,42 @@ export function useGetViewSortsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio export type GetViewSortsQueryHookResult = ReturnType; export type GetViewSortsLazyQueryHookResult = ReturnType; export type GetViewSortsQueryResult = Apollo.QueryResult; +export const GetViewsDocument = gql` + query GetViews($where: ViewWhereInput) { + views: findManyView(where: $where) { + id + name + } +} + `; + +/** + * __useGetViewsQuery__ + * + * To run a query within a React component, call `useGetViewsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetViewsQuery({ + * variables: { + * where: // value for 'where' + * }, + * }); + */ +export function useGetViewsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetViewsDocument, options); + } +export function useGetViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetViewsDocument, options); + } +export type GetViewsQueryHookResult = ReturnType; +export type GetViewsLazyQueryHookResult = ReturnType; +export type GetViewsQueryResult = Apollo.QueryResult; export const DeleteCurrentWorkspaceDocument = gql` mutation DeleteCurrentWorkspace { deleteCurrentWorkspace { diff --git a/front/src/index.tsx b/front/src/index.tsx index e3390a20bf..ccbbce2e69 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -5,7 +5,6 @@ import { RecoilRoot } from 'recoil'; import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; -import { SpreadsheetImportProvider } from '@/spreadsheet-import/components/SpreadsheetImportProvider'; import { DialogProvider } from '@/ui/dialog/components/DialogProvider'; import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; @@ -34,11 +33,9 @@ root.render( - - - - - + + + diff --git a/front/src/modules/companies/graphql/mutations/insertManyCompany.ts b/front/src/modules/companies/graphql/mutations/insertManyCompany.ts new file mode 100644 index 0000000000..096720737b --- /dev/null +++ b/front/src/modules/companies/graphql/mutations/insertManyCompany.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const INSERT_MANY_COMPANY = gql` + mutation InsertManyCompany($data: [CompanyCreateManyInput!]!) { + createManyCompany(data: $data) { + count + } + } +`; diff --git a/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts new file mode 100644 index 0000000000..a16feeec2f --- /dev/null +++ b/front/src/modules/companies/hooks/useSpreadsheetCompanyImport.ts @@ -0,0 +1,69 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; +import { SpreadsheetOptions } from '@/spreadsheet-import/types'; +import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; +import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds'; +import { + GetPeopleDocument, + useInsertManyCompanyMutation, +} from '~/generated/graphql'; + +import { fieldsForCompany } from '../utils/fieldsForCompany'; + +export type FieldCompanyMapping = (typeof fieldsForCompany)[number]['key']; + +export function useSpreadsheetCompanyImport() { + const { openSpreadsheetImport } = useSpreadsheetImport(); + const upsertEntityTableItems = useUpsertEntityTableItems(); + const upsertTableRowIds = useUpsertTableRowIds(); + const { enqueueSnackBar } = useSnackBar(); + + const [createManyCompany] = useInsertManyCompanyMutation(); + + const openCompanySpreadsheetImport = ( + options?: Omit< + SpreadsheetOptions, + 'fields' | 'isOpen' | 'onClose' + >, + ) => { + openSpreadsheetImport({ + ...options, + async onSubmit(data) { + // TODO: Add better type checking in spreadsheet import later + const createInputs = data.validData.map((company) => ({ + id: uuidv4(), + name: company.name as string, + domainName: company.domainName as string, + address: company.address as string, + employees: parseInt(company.employees as string, 10), + linkedinUrl: company.linkedinUrl as string | undefined, + })); + + try { + const result = await createManyCompany({ + variables: { + data: createInputs, + }, + refetchQueries: [GetPeopleDocument], + }); + + if (result.errors) { + throw result.errors; + } + + upsertTableRowIds(createInputs.map((company) => company.id)); + upsertEntityTableItems(createInputs); + } catch (error: any) { + enqueueSnackBar(error?.message || 'Something went wrong', { + variant: 'error', + }); + } + }, + fields: fieldsForCompany, + }); + }; + + return { openCompanySpreadsheetImport }; +} diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index 5015aeffe8..6803740848 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { companyViewFields } from '@/companies/constants/companyViewFields'; import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries'; import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries'; +import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; @@ -43,6 +44,7 @@ export function CompanyTable() { viewFieldDefinitions: companyViewFields, }); const { updateSorts } = useViewSorts({ availableSorts }); + const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport(); const filters = useRecoilScopedValue( filtersScopedState, @@ -56,6 +58,10 @@ export function CompanyTable() { const { setContextMenuEntries } = useCompanyTableContextMenuEntries(); const { setActionBarEntries } = useCompanyTableActionBarEntries(); + function handleImport() { + openCompanySpreadsheetImport(); + } + return ( <> , + label: 'Name', + key: 'name', + alternateMatches: ['name', 'company name', 'company'], + fieldType: { + type: 'input', + }, + example: 'Tim', + validations: [ + { + rule: 'required', + errorMessage: 'Name is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Domain name', + key: 'domainName', + alternateMatches: ['domain', 'domain name'], + fieldType: { + type: 'input', + }, + example: 'apple.dev', + validations: [ + { + rule: 'required', + errorMessage: 'Domain name is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Linkedin URL', + key: 'linkedinUrl', + alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'], + fieldType: { + type: 'input', + }, + example: 'https://www.linkedin.com/in/apple', + }, + { + icon: , + label: 'Address', + key: 'address', + fieldType: { + type: 'input', + }, + example: 'Maple street', + validations: [ + { + rule: 'required', + errorMessage: 'Address is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Employees', + key: 'employees', + alternateMatches: ['employees', 'total employees', 'number of employees'], + fieldType: { + type: 'input', + }, + example: '150', + }, +] as const; diff --git a/front/src/modules/people/graphql/mutations/insertManyPerson.ts b/front/src/modules/people/graphql/mutations/insertManyPerson.ts new file mode 100644 index 0000000000..a75a32363e --- /dev/null +++ b/front/src/modules/people/graphql/mutations/insertManyPerson.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const INSERT_MANY_PERSON = gql` + mutation InsertManyPerson($data: [PersonCreateManyInput!]!) { + createManyPerson(data: $data) { + count + } + } +`; diff --git a/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts new file mode 100644 index 0000000000..1894c09a3d --- /dev/null +++ b/front/src/modules/people/hooks/useSpreadsheetPersonImport.ts @@ -0,0 +1,72 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; +import { SpreadsheetOptions } from '@/spreadsheet-import/types'; +import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; +import { useUpsertEntityTableItems } from '@/ui/table/hooks/useUpsertEntityTableItems'; +import { useUpsertTableRowIds } from '@/ui/table/hooks/useUpsertTableRowIds'; +import { + GetPeopleDocument, + useInsertManyPersonMutation, +} from '~/generated/graphql'; + +import { fieldsForPerson } from '../utils/fieldsForPerson'; + +export type FieldPersonMapping = (typeof fieldsForPerson)[number]['key']; + +export function useSpreadsheetPersonImport() { + const { openSpreadsheetImport } = useSpreadsheetImport(); + const upsertEntityTableItems = useUpsertEntityTableItems(); + const upsertTableRowIds = useUpsertTableRowIds(); + const { enqueueSnackBar } = useSnackBar(); + + const [createManyPerson] = useInsertManyPersonMutation(); + + const openPersonSpreadsheetImport = ( + options?: Omit< + SpreadsheetOptions, + 'fields' | 'isOpen' | 'onClose' + >, + ) => { + openSpreadsheetImport({ + ...options, + async onSubmit(data) { + // TODO: Add better type checking in spreadsheet import later + const createInputs = data.validData.map((person) => ({ + id: uuidv4(), + firstName: person.firstName as string | undefined, + lastName: person.lastName as string | undefined, + email: person.email as string | undefined, + linkedinUrl: person.linkedinUrl as string | undefined, + xUrl: person.xUrl as string | undefined, + jobTitle: person.jobTitle as string | undefined, + phone: person.phone as string | undefined, + city: person.city as string | undefined, + })); + + try { + const result = await createManyPerson({ + variables: { + data: createInputs, + }, + refetchQueries: [GetPeopleDocument], + }); + + if (result.errors) { + throw result.errors; + } + + upsertTableRowIds(createInputs.map((person) => person.id)); + upsertEntityTableItems(createInputs); + } catch (error: any) { + enqueueSnackBar(error?.message || 'Something went wrong', { + variant: 'error', + }); + } + }, + fields: fieldsForPerson, + }); + }; + + return { openPersonSpreadsheetImport }; +} diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index d8af74c29e..a54ad0c1ff 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { peopleViewFields } from '@/people/constants/peopleViewFields'; import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries'; import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries'; +import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; @@ -35,6 +36,7 @@ export function PeopleTable() { ); const [updateEntityMutation] = useUpdateOnePersonMutation(); const upsertEntityTableItem = useUpsertEntityTableItem(); + const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport(); const objectId = 'person'; const { handleViewsChange } = useTableViews({ objectId }); @@ -56,6 +58,10 @@ export function PeopleTable() { const { setContextMenuEntries } = usePersonTableContextMenuEntries(); const { setActionBarEntries } = usePersonTableActionBarEntries(); + function handleImport() { + openPersonSpreadsheetImport(); + } + return ( <> , + label: 'Firstname', + key: 'firstName', + alternateMatches: ['first name', 'first', 'firstname'], + fieldType: { + type: 'input', + }, + example: 'Tim', + validations: [ + { + rule: 'required', + errorMessage: 'Firstname is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Lastname', + key: 'lastName', + alternateMatches: ['last name', 'last', 'lastname'], + fieldType: { + type: 'input', + }, + example: 'Cook', + validations: [ + { + rule: 'required', + errorMessage: 'Lastname is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Email', + key: 'email', + alternateMatches: ['email', 'mail'], + fieldType: { + type: 'input', + }, + example: 'tim@apple.dev', + validations: [ + { + rule: 'required', + errorMessage: 'email is required', + level: 'error', + }, + ], + }, + { + icon: , + label: 'Linkedin URL', + key: 'linkedinUrl', + alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'], + fieldType: { + type: 'input', + }, + example: 'https://www.linkedin.com/in/timcook', + }, + { + icon: , + label: 'X URL', + key: 'xUrl', + alternateMatches: ['x', 'x url'], + fieldType: { + type: 'input', + }, + example: 'https://x.com/tim_cook', + }, + { + icon: , + label: 'Job title', + key: 'jobTitle', + alternateMatches: ['job', 'job title'], + fieldType: { + type: 'input', + }, + example: 'CEO', + }, + { + icon: , + label: 'Phone', + key: 'phone', + fieldType: { + type: 'input', + }, + example: '+1234567890', + validations: [ + { + rule: 'function', + isValid: (value: string) => isValidPhoneNumber(value), + errorMessage: 'phone is not valid', + level: 'error', + }, + ], + }, + { + icon: , + label: 'City', + key: 'city', + fieldType: { + type: 'input', + }, + example: 'Seattle', + }, +] as const; diff --git a/front/src/modules/spreadsheet-import/components/core/ContinueButton.tsx b/front/src/modules/spreadsheet-import/components/ContinueButton.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/core/ContinueButton.tsx rename to front/src/modules/spreadsheet-import/components/ContinueButton.tsx diff --git a/front/src/modules/spreadsheet-import/components/core/Heading.tsx b/front/src/modules/spreadsheet-import/components/Heading.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/core/Heading.tsx rename to front/src/modules/spreadsheet-import/components/Heading.tsx diff --git a/front/src/modules/spreadsheet-import/components/core/MatchColumnSelect.tsx b/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx similarity index 99% rename from front/src/modules/spreadsheet-import/components/core/MatchColumnSelect.tsx rename to front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx index ffbf1088ce..21ca3284c8 100644 --- a/front/src/modules/spreadsheet-import/components/core/MatchColumnSelect.tsx +++ b/front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx @@ -195,6 +195,7 @@ export const MatchColumnSelect = ({ value?.value !== option.value && createPortal( { - const { rtl } = useRsi(); + const { rtl } = useSpreadsheetImportInternal(); return ( diff --git a/front/src/modules/spreadsheet-import/components/core/Providers.tsx b/front/src/modules/spreadsheet-import/components/Providers.tsx similarity index 55% rename from front/src/modules/spreadsheet-import/components/core/Providers.tsx rename to front/src/modules/spreadsheet-import/components/Providers.tsx index c15a32e84f..a09b774f17 100644 --- a/front/src/modules/spreadsheet-import/components/core/Providers.tsx +++ b/front/src/modules/spreadsheet-import/components/Providers.tsx @@ -1,25 +1,21 @@ import { createContext } from 'react'; -import type { RsiProps } from '@/spreadsheet-import/types'; +import type { SpreadsheetOptions } from '@/spreadsheet-import/types'; export const RsiContext = createContext({} as any); type ProvidersProps = { children: React.ReactNode; - rsiValues: RsiProps; + values: SpreadsheetOptions; }; -export const rootId = 'chakra-modal-rsi'; - export const Providers = ({ children, - rsiValues, + values, }: ProvidersProps) => { - if (!rsiValues.fields) { + if (!values.fields) { throw new Error('Fields must be provided to spreadsheet-import'); } - return ( - {children} - ); + return {children}; }; diff --git a/front/src/modules/spreadsheet-import/components/SpreadsheetImport.tsx b/front/src/modules/spreadsheet-import/components/SpreadsheetImport.tsx deleted file mode 100644 index 345d0c99cf..0000000000 --- a/front/src/modules/spreadsheet-import/components/SpreadsheetImport.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { RsiProps } from '../types'; - -import { ModalWrapper } from './core/ModalWrapper'; -import { Providers } from './core/Providers'; -import { Steps } from './steps/Steps'; - -export const defaultRSIProps: Partial> = { - autoMapHeaders: true, - allowInvalidSubmit: true, - autoMapDistance: 2, - uploadStepHook: async (value) => value, - selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }), - matchColumnsStepHook: async (table) => table, - dateFormat: 'yyyy-mm-dd', // ISO 8601, - parseRaw: true, -} as const; - -export const SpreadsheetImport = (props: RsiProps) => { - return ( - - - - - - ); -}; - -SpreadsheetImport.defaultProps = defaultRSIProps; diff --git a/front/src/modules/spreadsheet-import/components/core/Table.tsx b/front/src/modules/spreadsheet-import/components/Table.tsx similarity index 95% rename from front/src/modules/spreadsheet-import/components/core/Table.tsx rename to front/src/modules/spreadsheet-import/components/Table.tsx index ac5530e5e9..9f840f5079 100644 --- a/front/src/modules/spreadsheet-import/components/core/Table.tsx +++ b/front/src/modules/spreadsheet-import/components/Table.tsx @@ -1,7 +1,7 @@ import DataGrid, { DataGridProps } from 'react-data-grid'; import styled from '@emotion/styled'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { rgba } from '@/ui/theme/constants/colors'; const StyledDataGrid = styled(DataGrid)` @@ -112,7 +112,7 @@ type Props = DataGridProps & { }; export const Table = (props: Props) => { - const { rtl } = useRsi(); + const { rtl } = useSpreadsheetImportInternal(); return ( diff --git a/front/src/modules/spreadsheet-import/hooks/useRsi.ts b/front/src/modules/spreadsheet-import/hooks/useRsi.ts deleted file mode 100644 index 4c2fbe35df..0000000000 --- a/front/src/modules/spreadsheet-import/hooks/useRsi.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; -import { SetRequired } from 'type-fest'; - -import { RsiContext } from '@/spreadsheet-import/components/core/Providers'; -import { defaultRSIProps } from '@/spreadsheet-import/components/SpreadsheetImport'; -import { RsiProps } from '@/spreadsheet-import/types'; - -export const useRsi = () => - useContext, keyof typeof defaultRSIProps>>( - RsiContext, - ); diff --git a/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.tsx similarity index 66% rename from front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts rename to front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.tsx index 41ac2187c6..a9628f574b 100644 --- a/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts +++ b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.tsx @@ -1,13 +1,13 @@ import { useSetRecoilState } from 'recoil'; import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; -import { RsiProps } from '@/spreadsheet-import/types'; +import { SpreadsheetOptions } from '@/spreadsheet-import/types'; -export function useSpreadsheetImport() { +export function useSpreadsheetImport() { const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState); const openSpreadsheetImport = ( - options: Omit, 'isOpen' | 'onClose'>, + options: Omit, 'isOpen' | 'onClose'>, ) => { setSpreadSheetImport({ isOpen: true, diff --git a/front/src/modules/spreadsheet-import/hooks/useRsiInitialStep.ts b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInitialStep.ts similarity index 80% rename from front/src/modules/spreadsheet-import/hooks/useRsiInitialStep.ts rename to front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInitialStep.ts index 706e34bd08..f0b52b8b1e 100644 --- a/front/src/modules/spreadsheet-import/hooks/useRsiInitialStep.ts +++ b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInitialStep.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react'; -import { StepType } from '@/spreadsheet-import/components/steps/UploadFlow'; +import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow'; -export const useRsiInitialStep = (initialStep?: StepType) => { +export const useSpreadsheetImportInitialStep = (initialStep?: StepType) => { const steps = ['uploadStep', 'matchColumnsStep', 'validationStep'] as const; const initialStepNumber = useMemo(() => { diff --git a/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts new file mode 100644 index 0000000000..b9dccb62e2 --- /dev/null +++ b/front/src/modules/spreadsheet-import/hooks/useSpreadsheetImportInternal.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; +import { SetRequired } from 'type-fest'; + +import { RsiContext } from '@/spreadsheet-import/components/Providers'; +import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport'; +import { SpreadsheetOptions } from '@/spreadsheet-import/types'; + +export const useSpreadsheetImportInternal = () => + useContext< + SetRequired< + SpreadsheetOptions, + keyof typeof defaultSpreadsheetImportProps + > + >(RsiContext); diff --git a/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx new file mode 100644 index 0000000000..d4fb470dc5 --- /dev/null +++ b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx @@ -0,0 +1,29 @@ +import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; +import { Providers } from '@/spreadsheet-import/components/Providers'; +import { Steps } from '@/spreadsheet-import/steps/components/Steps'; +import type { SpreadsheetOptions } from '@/spreadsheet-import/types'; + +export const defaultSpreadsheetImportProps: Partial> = { + autoMapHeaders: true, + allowInvalidSubmit: true, + autoMapDistance: 2, + uploadStepHook: async (value) => value, + selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }), + matchColumnsStepHook: async (table) => table, + dateFormat: 'yyyy-mm-dd', // ISO 8601, + parseRaw: true, +} as const; + +export const SpreadsheetImport = ( + props: SpreadsheetOptions, +) => { + return ( + + + + + + ); +}; + +SpreadsheetImport.defaultProps = defaultSpreadsheetImportProps; diff --git a/front/src/modules/spreadsheet-import/components/SpreadsheetImportProvider.tsx b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx similarity index 90% rename from front/src/modules/spreadsheet-import/components/SpreadsheetImportProvider.tsx rename to front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx index 11ca40879b..58dad0954f 100644 --- a/front/src/modules/spreadsheet-import/components/SpreadsheetImportProvider.tsx +++ b/front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useRecoilState } from 'recoil'; -import { spreadsheetImportState } from '../states/spreadsheetImportState'; +import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; import { SpreadsheetImport } from './SpreadsheetImport'; diff --git a/front/src/modules/spreadsheet-import/states/spreadsheetImportState.ts b/front/src/modules/spreadsheet-import/states/spreadsheetImportState.ts index c0c5fde548..d1f0da412b 100644 --- a/front/src/modules/spreadsheet-import/states/spreadsheetImportState.ts +++ b/front/src/modules/spreadsheet-import/states/spreadsheetImportState.ts @@ -1,13 +1,13 @@ import { atom } from 'recoil'; -import { RsiProps } from '../types'; +import { SpreadsheetOptions } from '../types'; export type SpreadsheetImportState = { isOpen: boolean; - options: Omit, 'isOpen' | 'onClose'> | null; + options: Omit, 'isOpen' | 'onClose'> | null; }; -export const spreadsheetImportState = atom>({ +export const spreadsheetImportState = atom>({ key: 'spreadsheetImportState', default: { isOpen: false, diff --git a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx similarity index 96% rename from front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep.tsx rename to front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx index e1183b8a14..0abf440b8d 100644 --- a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import styled from '@emotion/styled'; -import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; -import { Heading } from '@/spreadsheet-import/components/core/Heading'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; +import { Heading } from '@/spreadsheet-import/components/Heading'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import type { Field, RawData } from '@/spreadsheet-import/types'; import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields'; import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; @@ -114,7 +114,8 @@ export const MatchColumnsStep = ({ const { enqueueDialog } = useDialog(); const { enqueueSnackBar } = useSnackBar(); const dataExample = data.slice(0, 2); - const { fields, autoMapHeaders, autoMapDistance } = useRsi(); + const { fields, autoMapHeaders, autoMapDistance } = + useSpreadsheetImportInternal(); const [isLoading, setIsLoading] = useState(false); const [columns, setColumns] = useState>( // Do not remove spread, it indexes empty array elements, otherwise map() skips over them diff --git a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/ColumnGrid.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/ColumnGrid.tsx rename to front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid.tsx diff --git a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/SubMatchingSelect.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx similarity index 89% rename from front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/SubMatchingSelect.tsx rename to front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx index af45862c32..01f7a848ee 100644 --- a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/SubMatchingSelect.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { SelectOption } from '@/spreadsheet-import/types'; import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions'; @@ -35,7 +35,7 @@ export const SubMatchingSelect = ({ column, onSubChange, }: Props) => { - const { fields } = useRsi(); + const { fields } = useSpreadsheetImportInternal(); const options = getFieldOptions(fields, column.value) as SelectOption[]; const value = options.find((opt) => opt.value === option.value); diff --git a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/TemplateColumn.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx similarity index 96% rename from front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/TemplateColumn.tsx rename to front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx index d6d6bd0e66..760f4a47f0 100644 --- a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/TemplateColumn.tsx @@ -8,8 +8,8 @@ import { } from '@chakra-ui/accordion'; import styled from '@emotion/styled'; -import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import type { Fields } from '@/spreadsheet-import/types'; import { IconChevronDown, IconForbid } from '@/ui/icon'; @@ -86,7 +86,7 @@ export const TemplateColumn = ({ onChange, onSubChange, }: TemplateColumnProps) => { - const { fields } = useRsi(); + const { fields } = useSpreadsheetImportInternal(); const column = columns[columnIndex]; const isIgnored = column.type === ColumnType.ignored; const isSelect = 'matchedOptions' in column; diff --git a/front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/UserTableColumn.tsx b/front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/MatchColumnsStep/components/UserTableColumn.tsx rename to front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/components/UserTableColumn.tsx diff --git a/front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/SelectHeaderStep.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx similarity index 94% rename from front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/SelectHeaderStep.tsx rename to front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx index 6a89ab621a..784d25af41 100644 --- a/front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/SelectHeaderStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep.tsx @@ -1,8 +1,8 @@ import { useCallback, useState } from 'react'; import styled from '@emotion/styled'; -import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; -import { Heading } from '@/spreadsheet-import/components/core/Heading'; +import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; +import { Heading } from '@/spreadsheet-import/components/Heading'; import type { RawData } from '@/spreadsheet-import/types'; import { Modal } from '@/ui/modal/components/Modal'; diff --git a/front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/components/SelectColumn.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/components/SelectColumn.tsx rename to front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx diff --git a/front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/components/SelectHeaderTable.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectHeaderTable.tsx similarity index 93% rename from front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/components/SelectHeaderTable.tsx rename to front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectHeaderTable.tsx index 124c48f9c4..b2222d2f33 100644 --- a/front/src/modules/spreadsheet-import/components/steps/SelectHeaderStep/components/SelectHeaderTable.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectHeaderTable.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { Table } from '@/spreadsheet-import/components/core/Table'; +import { Table } from '@/spreadsheet-import/components/Table'; import type { RawData } from '@/spreadsheet-import/types'; import { generateSelectionColumns } from './SelectColumn'; diff --git a/front/src/modules/spreadsheet-import/components/steps/SelectSheetStep/SelectSheetStep.tsx b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx similarity index 91% rename from front/src/modules/spreadsheet-import/components/steps/SelectSheetStep/SelectSheetStep.tsx rename to front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx index 1feee3e37c..aedf609f61 100644 --- a/front/src/modules/spreadsheet-import/components/steps/SelectSheetStep/SelectSheetStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx @@ -1,13 +1,12 @@ import { useCallback, useState } from 'react'; import styled from '@emotion/styled'; -import { Heading } from '@/spreadsheet-import/components/core/Heading'; +import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; +import { Heading } from '@/spreadsheet-import/components/Heading'; import { Radio } from '@/ui/input/radio/components/Radio'; import { RadioGroup } from '@/ui/input/radio/components/RadioGroup'; import { Modal } from '@/ui/modal/components/Modal'; -import { ContinueButton } from '../../core/ContinueButton'; - const Content = styled(Modal.Content)` align-items: center; `; diff --git a/front/src/modules/spreadsheet-import/components/steps/Steps.tsx b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx similarity index 74% rename from front/src/modules/spreadsheet-import/components/steps/Steps.tsx rename to front/src/modules/spreadsheet-import/steps/components/Steps.tsx index b034a8b245..6e53f1fb41 100644 --- a/front/src/modules/spreadsheet-import/components/steps/Steps.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/Steps.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; -import { useRsiInitialStep } from '@/spreadsheet-import/hooks/useRsiInitialStep'; +import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { Modal } from '@/ui/modal/components/Modal'; import { StepBar } from '@/ui/step-bar/components/StepBar'; import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; @@ -24,9 +24,11 @@ const stepTitles = { } as const; export const Steps = () => { - const { initialStepState } = useRsi(); + const { initialStepState } = useSpreadsheetImportInternal(); - const { steps, initialStep } = useRsiInitialStep(initialStepState?.type); + const { steps, initialStep } = useSpreadsheetImportInitialStep( + initialStepState?.type, + ); const { nextStep, activeStep } = useStepBar({ initialStep, diff --git a/front/src/modules/spreadsheet-import/components/steps/UploadFlow.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx similarity index 91% rename from front/src/modules/spreadsheet-import/components/steps/UploadFlow.tsx rename to front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx index fa0f49d87a..ce2db5bb47 100644 --- a/front/src/modules/spreadsheet-import/components/steps/UploadFlow.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadFlow.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import type XLSX from 'xlsx-ugnis'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import type { RawData } from '@/spreadsheet-import/types'; import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'; import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; @@ -29,6 +29,7 @@ export enum StepType { selectHeader = 'selectHeader', matchColumns = 'matchColumns', validateData = 'validateData', + loading = 'loading', } export type StepState = | { @@ -50,6 +51,9 @@ export type StepState = | { type: StepType.validateData; data: any[]; + } + | { + type: StepType.loading; }; interface Props { @@ -58,7 +62,7 @@ interface Props { export const UploadFlow = ({ nextStep }: Props) => { const theme = useTheme(); - const { initialStepState } = useRsi(); + const { initialStepState } = useSpreadsheetImportInternal(); const [state, setState] = useState( initialStepState || { type: StepType.upload }, ); @@ -68,7 +72,7 @@ export const UploadFlow = ({ nextStep }: Props) => { uploadStepHook, selectHeaderStepHook, matchColumnsStepHook, - } = useRsi(); + } = useSpreadsheetImportInternal(); const { enqueueSnackBar } = useSnackBar(); const errorToast = useCallback( @@ -191,7 +195,18 @@ export const UploadFlow = ({ nextStep }: Props) => { if (!uploadedFile) { throw new Error('File not found'); } - return ; + return ( + + setState({ + type: StepType.loading, + }) + } + /> + ); + case StepType.loading: default: return ( diff --git a/front/src/modules/spreadsheet-import/components/steps/UploadStep/UploadStep.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/UploadStep/UploadStep.tsx rename to front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx diff --git a/front/src/modules/spreadsheet-import/components/steps/UploadStep/components/DropZone.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx similarity index 95% rename from front/src/modules/spreadsheet-import/components/steps/UploadStep/components/DropZone.tsx rename to front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index c44f4a6ee3..e35aacf0d5 100644 --- a/front/src/modules/spreadsheet-import/components/steps/UploadStep/components/DropZone.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -3,7 +3,7 @@ import { useDropzone } from 'react-dropzone'; import styled from '@emotion/styled'; import * as XLSX from 'xlsx-ugnis'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; import { MainButton } from '@/ui/button/components/MainButton'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; @@ -83,7 +83,7 @@ type DropZoneProps = { }; export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { - const { maxFileSize, dateFormat, parseRaw } = useRsi(); + const { maxFileSize, dateFormat, parseRaw } = useSpreadsheetImportInternal(); const [loading, setLoading] = useState(false); diff --git a/front/src/modules/spreadsheet-import/components/steps/UploadStep/components/ExampleTable.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx similarity index 89% rename from front/src/modules/spreadsheet-import/components/steps/UploadStep/components/ExampleTable.tsx rename to front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx index 407d438656..79e474bed1 100644 --- a/front/src/modules/spreadsheet-import/components/steps/UploadStep/components/ExampleTable.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/ExampleTable.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { Table } from '@/spreadsheet-import/components/core/Table'; +import { Table } from '@/spreadsheet-import/components/Table'; import type { Fields } from '@/spreadsheet-import/types'; import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow'; diff --git a/front/src/modules/spreadsheet-import/components/steps/UploadStep/components/columns.tsx b/front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/UploadStep/components/columns.tsx rename to front/src/modules/spreadsheet-import/steps/components/UploadStep/components/columns.tsx diff --git a/front/src/modules/spreadsheet-import/components/steps/ValidationStep/ValidationStep.tsx b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx similarity index 93% rename from front/src/modules/spreadsheet-import/components/steps/ValidationStep/ValidationStep.tsx rename to front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index 07d2e14b60..e24f43f395 100644 --- a/front/src/modules/spreadsheet-import/components/steps/ValidationStep/ValidationStep.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -2,10 +2,10 @@ import { useCallback, useMemo, useState } from 'react'; import type { RowsChangeData } from 'react-data-grid'; import styled from '@emotion/styled'; -import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; -import { Heading } from '@/spreadsheet-import/components/core/Heading'; -import { Table } from '@/spreadsheet-import/components/core/Table'; -import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; +import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton'; +import { Heading } from '@/spreadsheet-import/components/Heading'; +import { Table } from '@/spreadsheet-import/components/Table'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import type { Data } from '@/spreadsheet-import/types'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { Button, ButtonVariant } from '@/ui/button/components/Button'; @@ -56,14 +56,17 @@ const NoRowsContainer = styled.div` type Props = { initialData: Data[]; file: File; + onSubmitStart?: () => void; }; export const ValidationStep = ({ initialData, file, + onSubmitStart, }: Props) => { const { enqueueDialog } = useDialog(); - const { fields, onClose, onSubmit, rowHook, tableHook } = useRsi(); + const { fields, onClose, onSubmit, rowHook, tableHook } = + useSpreadsheetImportInternal(); const [data, setData] = useState<(Data & Meta)[]>( useMemo( @@ -146,7 +149,8 @@ export const ValidationStep = ({ }, { validData: [] as Data[], invalidData: [] as Data[], all: data }, ); - onSubmit(calculatedData, file); + onSubmitStart?.(); + await onSubmit(calculatedData, file); onClose(); }; const onContinue = () => { diff --git a/front/src/modules/spreadsheet-import/components/steps/ValidationStep/components/columns.tsx b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx similarity index 85% rename from front/src/modules/spreadsheet-import/components/steps/ValidationStep/components/columns.tsx rename to front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index edd978f183..471a2d7919 100644 --- a/front/src/modules/spreadsheet-import/components/steps/ValidationStep/components/columns.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -2,7 +2,7 @@ import { Column, useRowSelection } from 'react-data-grid'; import { createPortal } from 'react-dom'; import styled from '@emotion/styled'; -import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect'; +import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import type { Data, Fields } from '@/spreadsheet-import/types'; import { Checkbox, @@ -118,14 +118,13 @@ export const generateColumns = ( ), editable: column.fieldType.type !== 'checkbox', editor: ({ row, onRowChange, onClose }) => { + const columnKey = column.key as keyof (Data & Meta); let component; switch (column.fieldType.type) { case 'select': { const value = column.fieldType.options.find( - (option) => - option.value === - (row[column.key as keyof (Data & Meta)] as string), + (option) => option.value === (row[columnKey] as string), ); component = ( @@ -139,7 +138,7 @@ export const generateColumns = ( : value } onChange={(value) => { - onRowChange({ ...row, [column.key]: value?.value }, true); + onRowChange({ ...row, [columnKey]: value?.value }, true); }} options={column.fieldType.options} /> @@ -149,9 +148,9 @@ export const generateColumns = ( default: component = ( { - onRowChange({ ...row, [column.key]: value }); + onRowChange({ ...row, [columnKey]: value }); }} autoFocus={true} onBlur={() => onClose(true)} @@ -165,23 +164,24 @@ export const generateColumns = ( editOnClick: true, }, formatter: ({ row, onRowChange }) => { + const columnKey = column.key as keyof (Data & Meta); let component; switch (column.fieldType.type) { case 'checkbox': component = ( { event.stopPropagation(); }} > { onRowChange({ ...row, - [column.key]: !row[column.key as T], + [columnKey]: !row[columnKey], }); }} /> @@ -190,30 +190,30 @@ export const generateColumns = ( break; case 'select': component = ( - + {column.fieldType.options.find( - (option) => option.value === row[column.key as T], + (option) => option.value === row[columnKey as T], )?.label || null} ); break; default: component = ( - - {row[column.key as T]} + + {row[columnKey]} ); } - if (row.__errors?.[column.key]) { + if (row.__errors?.[columnKey]) { return ( <> {component} {createPortal( , document.body, )} diff --git a/front/src/modules/spreadsheet-import/components/steps/ValidationStep/types.ts b/front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts similarity index 100% rename from front/src/modules/spreadsheet-import/components/steps/ValidationStep/types.ts rename to front/src/modules/spreadsheet-import/steps/components/ValidationStep/types.ts diff --git a/front/src/modules/spreadsheet-import/components/__stories__/MatchColumns.stories.tsx b/front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx similarity index 83% rename from front/src/modules/spreadsheet-import/components/__stories__/MatchColumns.stories.tsx rename to front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx index 1358c06720..5af288cf05 100644 --- a/front/src/modules/spreadsheet-import/components/__stories__/MatchColumns.stories.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/react'; -import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; -import { Providers } from '@/spreadsheet-import/components/core/Providers'; -import { MatchColumnsStep } from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; +import { Providers } from '@/spreadsheet-import/components/Providers'; +import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; const meta: Meta = { @@ -58,7 +58,7 @@ const mockData = [ export function Default() { return ( - + null}> + null}> = { @@ -19,7 +19,7 @@ const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3']; export function Default() { return ( - + null}> = { @@ -17,7 +17,7 @@ export default meta; export function Default() { return ( - + null}> Promise.resolve()} /> diff --git a/front/src/modules/spreadsheet-import/components/__stories__/Validation.stories.tsx b/front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx similarity index 66% rename from front/src/modules/spreadsheet-import/components/__stories__/Validation.stories.tsx rename to front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx index 9277cc41fa..748c3105e3 100644 --- a/front/src/modules/spreadsheet-import/components/__stories__/Validation.stories.tsx +++ b/front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx @@ -1,8 +1,8 @@ import { Meta } from '@storybook/react'; -import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; -import { Providers } from '@/spreadsheet-import/components/core/Providers'; -import { ValidationStep } from '@/spreadsheet-import/components/steps/ValidationStep/ValidationStep'; +import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; +import { Providers } from '@/spreadsheet-import/components/Providers'; +import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep'; import { editableTableInitialData, mockRsiValues, @@ -22,7 +22,7 @@ const file = new File([''], 'file.csv'); export function Default() { return ( - + null}> diff --git a/front/src/modules/spreadsheet-import/tests/mockRsiValues.ts b/front/src/modules/spreadsheet-import/tests/mockRsiValues.ts index 17dbe206eb..6121e0bde1 100644 --- a/front/src/modules/spreadsheet-import/tests/mockRsiValues.ts +++ b/front/src/modules/spreadsheet-import/tests/mockRsiValues.ts @@ -1,5 +1,5 @@ -import { defaultRSIProps } from '@/spreadsheet-import/components/SpreadsheetImport'; -import type { RsiProps } from '@/spreadsheet-import/types'; +import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport'; +import type { SpreadsheetOptions } from '@/spreadsheet-import/types'; const fields = [ { @@ -87,13 +87,14 @@ const fields = [ }, ] as const; -const mockComponentBehaviourForTypes = (props: RsiProps) => - props; +const mockComponentBehaviourForTypes = ( + props: SpreadsheetOptions, +) => props; export const mockRsiValues = mockComponentBehaviourForTypes({ - ...defaultRSIProps, + ...defaultSpreadsheetImportProps, fields: fields, - onSubmit: (data) => { + onSubmit: async (data) => { console.log(data.all.map((value) => value)); }, isOpen: true, diff --git a/front/src/modules/spreadsheet-import/types/index.ts b/front/src/modules/spreadsheet-import/types/index.ts index 363015547b..2346e1679a 100644 --- a/front/src/modules/spreadsheet-import/types/index.ts +++ b/front/src/modules/spreadsheet-import/types/index.ts @@ -1,16 +1,16 @@ import { ReadonlyDeep } from 'type-fest'; -import { Columns } from '../components/steps/MatchColumnsStep/MatchColumnsStep'; -import { StepState } from '../components/steps/UploadFlow'; -import { Meta } from '../components/steps/ValidationStep/types'; +import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { StepState } from '@/spreadsheet-import/steps/components/UploadFlow'; +import { Meta } from '@/spreadsheet-import/steps/components/ValidationStep/types'; -export type RsiProps = { +export type SpreadsheetOptions = { // Is modal visible. isOpen: boolean; // callback when RSI is closed before final submit onClose: () => void; // Field description for requested data - fields: Fields; + fields: Fields; // Runs after file upload step, receives and returns raw sheet data uploadStepHook?: (data: RawData[]) => Promise; // Runs after header selection step, receives and returns raw sheet data @@ -20,16 +20,16 @@ export type RsiProps = { ) => Promise<{ headerValues: RawData; data: RawData[] }>; // Runs once before validation step, used for data mutations and if you want to change how columns were matched matchColumnsStepHook?: ( - table: Data[], + table: Data[], rawData: RawData[], - columns: Columns, - ) => Promise[]>; + columns: Columns, + ) => Promise[]>; // Runs after column matching and on entry change - rowHook?: RowHook; + rowHook?: RowHook; // Runs after column matching and on entry change - tableHook?: TableHook; + tableHook?: TableHook; // Function called after user finishes the flow - onSubmit: (data: Result, file: File) => void; + onSubmit: (data: Result, file: File) => Promise; // Allows submitting with errors. Default: true allowInvalidSubmit?: boolean; // Theme configuration passed to underlying Chakra-UI @@ -110,7 +110,8 @@ export type Input = { export type Validation = | RequiredValidation | UniqueValidation - | RegexValidation; + | RegexValidation + | FunctionValidation; export type RequiredValidation = { rule: 'required'; @@ -133,6 +134,13 @@ export type RegexValidation = { level?: ErrorLevel; }; +export type FunctionValidation = { + rule: 'function'; + isValid: (value: string) => boolean; + errorMessage: string; + level?: ErrorLevel; +}; + export type RowHook = ( row: Data, addError: (fieldKey: T, error: Info) => void, diff --git a/front/src/modules/spreadsheet-import/utils/dataMutations.ts b/front/src/modules/spreadsheet-import/utils/dataMutations.ts index a5579e3b48..39206fff35 100644 --- a/front/src/modules/spreadsheet-import/utils/dataMutations.ts +++ b/front/src/modules/spreadsheet-import/utils/dataMutations.ts @@ -3,7 +3,7 @@ import { v4 } from 'uuid'; import type { Errors, Meta, -} from '@/spreadsheet-import/components/steps/ValidationStep/types'; +} from '@/spreadsheet-import/steps/components/ValidationStep/types'; import type { Data, Fields, @@ -93,8 +93,9 @@ export const addErrorsAndRunHooks = ( case 'regex': { const regex = new RegExp(validation.value, validation.flags); data.forEach((entry, index) => { - const value = entry[field.key]?.toString() ?? ''; - if (!value.match(regex)) { + const value = entry[field.key]?.toString(); + + if (value && !value.match(regex)) { errors[index] = { ...errors[index], [field.key]: { @@ -108,6 +109,22 @@ export const addErrorsAndRunHooks = ( }); break; } + case 'function': { + data.forEach((entry, index) => { + const value = entry[field.key]?.toString(); + + if (value && !validation.isValid(value)) { + errors[index] = { + ...errors[index], + [field.key]: { + level: validation.level || 'error', + message: validation.errorMessage || 'Field is invalid', + }, + }; + } + }); + break; + } } }); }); diff --git a/front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts index 81dea1cf67..57d8ca0c8d 100644 --- a/front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts +++ b/front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts @@ -1,4 +1,4 @@ -import type { Columns } from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +import type { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import type { Fields } from '@/spreadsheet-import/types'; export const findUnmatchedRequiredFields = ( diff --git a/front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts b/front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts index 6b4b2e0b32..c15a901d36 100644 --- a/front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts +++ b/front/src/modules/spreadsheet-import/utils/getMatchedColumns.ts @@ -4,7 +4,7 @@ import type { Column, Columns, MatchColumnsProps, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import type { Field, Fields } from '@/spreadsheet-import/types'; import { findMatch } from './findMatch'; diff --git a/front/src/modules/spreadsheet-import/utils/normalizeTableData.ts b/front/src/modules/spreadsheet-import/utils/normalizeTableData.ts index 34c0129ccf..b151f7a58c 100644 --- a/front/src/modules/spreadsheet-import/utils/normalizeTableData.ts +++ b/front/src/modules/spreadsheet-import/utils/normalizeTableData.ts @@ -1,7 +1,7 @@ import { Columns, ColumnType, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import type { Data, Fields, RawData } from '@/spreadsheet-import/types'; import { normalizeCheckboxValue } from './normalizeCheckboxValue'; diff --git a/front/src/modules/spreadsheet-import/utils/setColumn.ts b/front/src/modules/spreadsheet-import/utils/setColumn.ts index 81bc89085c..ff6c32e95e 100644 --- a/front/src/modules/spreadsheet-import/utils/setColumn.ts +++ b/front/src/modules/spreadsheet-import/utils/setColumn.ts @@ -2,7 +2,7 @@ import { Column, ColumnType, MatchColumnsProps, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import type { Field } from '@/spreadsheet-import/types'; import { uniqueEntries } from './uniqueEntries'; diff --git a/front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts b/front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts index 836c0c8509..ae10ee3986 100644 --- a/front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts +++ b/front/src/modules/spreadsheet-import/utils/setIgnoreColumn.ts @@ -1,7 +1,7 @@ import { Column, ColumnType, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; export const setIgnoreColumn = ({ header, diff --git a/front/src/modules/spreadsheet-import/utils/setSubColumn.ts b/front/src/modules/spreadsheet-import/utils/setSubColumn.ts index e3694a9577..cf92beb6aa 100644 --- a/front/src/modules/spreadsheet-import/utils/setSubColumn.ts +++ b/front/src/modules/spreadsheet-import/utils/setSubColumn.ts @@ -3,7 +3,7 @@ import { MatchedOptions, MatchedSelectColumn, MatchedSelectOptionsColumn, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; export const setSubColumn = ( oldColumn: MatchedSelectColumn | MatchedSelectOptionsColumn, diff --git a/front/src/modules/spreadsheet-import/utils/uniqueEntries.ts b/front/src/modules/spreadsheet-import/utils/uniqueEntries.ts index 121bca2903..b346f2fac7 100644 --- a/front/src/modules/spreadsheet-import/utils/uniqueEntries.ts +++ b/front/src/modules/spreadsheet-import/utils/uniqueEntries.ts @@ -3,7 +3,7 @@ import uniqBy from 'lodash/uniqBy'; import type { MatchColumnsProps, MatchedOptions, -} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; export const uniqueEntries = ( data: MatchColumnsProps['data'], diff --git a/front/src/modules/ui/dialog/components/DialogProvider.tsx b/front/src/modules/ui/dialog/components/DialogProvider.tsx index 8694e71a4d..d1008e8ff5 100644 --- a/front/src/modules/ui/dialog/components/DialogProvider.tsx +++ b/front/src/modules/ui/dialog/components/DialogProvider.tsx @@ -19,7 +19,11 @@ export function DialogProvider({ children }: React.PropsWithChildren) { <> {children} {dialogState.queue.map((dialog) => ( - handleDialogClose(dialog.id)} /> + handleDialogClose(dialog.id)} + /> ))} ); diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index aa07289911..19067fc141 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -11,6 +11,7 @@ export { IconBrandGithub, IconBrandGoogle, IconBrandLinkedin, + IconBrandTwitter, IconBrandX, IconBriefcase, IconBuildingSkyscraper, diff --git a/front/src/modules/ui/input/radio/components/Radio.tsx b/front/src/modules/ui/input/radio/components/Radio.tsx index 13f6953816..ca12db113b 100644 --- a/front/src/modules/ui/input/radio/components/Radio.tsx +++ b/front/src/modules/ui/input/radio/components/Radio.tsx @@ -30,7 +30,7 @@ const Container = styled.div<{ labelPosition?: LabelPosition }>` `; type RadioInputProps = { - radioSize?: RadioSize; + 'radio-size'?: RadioSize; }; const RadioInput = styled(motion.input)` @@ -60,13 +60,13 @@ const RadioInput = styled(motion.input)` background-color: ${({ theme }) => theme.grayScale.gray0}; border-radius: 50%; content: ''; - height: ${({ radioSize }) => + height: ${({ 'radio-size': radioSize }) => radioSize === RadioSize.Large ? '8px' : '6px'}; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); - width: ${({ radioSize }) => + width: ${({ 'radio-size': radioSize }) => radioSize === RadioSize.Large ? '8px' : '6px'}; } } @@ -74,10 +74,10 @@ const RadioInput = styled(motion.input)` cursor: not-allowed; opacity: 0.12; } - height: ${({ radioSize }) => + height: ${({ 'radio-size': radioSize }) => radioSize === RadioSize.Large ? '18px' : '16px'}; position: relative; - width: ${({ radioSize }) => + width: ${({ 'radio-size': radioSize }) => radioSize === RadioSize.Large ? '18px' : '16px'}; `; @@ -134,7 +134,7 @@ export function Radio({ data-testid="input-radio" checked={checked} value={value} - radioSize={size} + radio-size={size} disabled={disabled} onChange={handleChange} initial={{ scale: 0.95 }} diff --git a/front/src/modules/ui/step-bar/components/Step.tsx b/front/src/modules/ui/step-bar/components/Step.tsx index c53ac95a1a..c0f27b9bbf 100644 --- a/front/src/modules/ui/step-bar/components/Step.tsx +++ b/front/src/modules/ui/step-bar/components/Step.tsx @@ -10,7 +10,7 @@ const Container = styled.div<{ isLast: boolean }>` flex-grow: ${({ isLast }) => (isLast ? '0' : '1')}; `; -const StepCircle = styled(motion.div)<{ isCurrent: boolean }>` +const StepCircle = styled(motion.div)` align-items: center; border-radius: 50%; border-style: solid; @@ -40,7 +40,7 @@ const StepLabel = styled.span<{ isActive: boolean }>` white-space: nowrap; `; -const StepLine = styled(motion.div)<{ isActive: boolean }>` +const StepLine = styled(motion.div)` height: 2px; margin-left: ${({ theme }) => theme.spacing(2)}; margin-right: ${({ theme }) => theme.spacing(2)}; @@ -92,7 +92,6 @@ export const Step = ({ return ( @@ -107,7 +106,6 @@ export const Step = ({ {label} {!isLast && ( diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index 59c58b1ee0..69cf81e164 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -99,6 +99,7 @@ type OwnProps = { onColumnsChange?: (columns: ViewFieldDefinition[]) => void; onSortsUpdate?: (sorts: Array>) => void; onViewsChange?: (views: TableView[]) => void; + onImport?: () => void; updateEntityMutation: any; }; @@ -108,6 +109,7 @@ export function EntityTable({ onColumnsChange, onSortsUpdate, onViewsChange, + onImport, updateEntityMutation, }: OwnProps) { const tableBodyRef = useRef(null); @@ -136,6 +138,7 @@ export function EntityTable({ onColumnsChange={onColumnsChange} onSortsUpdate={onSortsUpdate} onViewsChange={onViewsChange} + onImport={onImport} /> diff --git a/front/src/modules/ui/table/hooks/useUpsertEntityTableItems.ts b/front/src/modules/ui/table/hooks/useUpsertEntityTableItems.ts new file mode 100644 index 0000000000..ef620f3757 --- /dev/null +++ b/front/src/modules/ui/table/hooks/useUpsertEntityTableItems.ts @@ -0,0 +1,34 @@ +import { useRecoilCallback } from 'recoil'; + +import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState'; + +export function useUpsertEntityTableItems() { + return useRecoilCallback( + ({ set, snapshot }) => + (entities: T[]) => { + // Create a map of new entities for quick lookup. + const newEntityMap = new Map( + entities.map((entity) => [entity.id, entity]), + ); + + // Filter out entities that are already the same in the state. + const entitiesToUpdate = entities.filter((entity) => { + const currentEntity = snapshot + .getLoadable(tableEntitiesFamilyState(entity.id)) + .valueMaybe(); + + return ( + !currentEntity || + JSON.stringify(currentEntity) !== + JSON.stringify(newEntityMap.get(entity.id)) + ); + }); + + // Batch set state for the filtered entities. + for (const entity of entitiesToUpdate) { + set(tableEntitiesFamilyState(entity.id), entity); + } + }, + [], + ); +} diff --git a/front/src/modules/ui/table/hooks/useUpsertTableRowIds.ts b/front/src/modules/ui/table/hooks/useUpsertTableRowIds.ts new file mode 100644 index 0000000000..1396c4dfba --- /dev/null +++ b/front/src/modules/ui/table/hooks/useUpsertTableRowIds.ts @@ -0,0 +1,18 @@ +import { useRecoilCallback } from 'recoil'; + +import { tableRowIdsState } from '../states/tableRowIdsState'; + +export function useUpsertTableRowIds() { + return useRecoilCallback( + ({ set, snapshot }) => + (rowIds: string[]) => { + const currentRowIds = snapshot + .getLoadable(tableRowIdsState) + .valueOrThrow(); + + const uniqueRowIds = Array.from(new Set([...rowIds, ...currentRowIds])); + set(tableRowIdsState, uniqueRowIds); + }, + [], + ); +} diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx index dc951c3bb0..fc2cc072d8 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx @@ -9,7 +9,6 @@ import { useTheme } from '@emotion/react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; -import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport'; import { IconButton } from '@/ui/button/components/IconButton'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; @@ -51,6 +50,7 @@ import { TableOptionsDropdownSection } from './TableOptionsDropdownSection'; type TableOptionsDropdownButtonProps = { onColumnsChange?: (columns: ViewFieldDefinition[]) => void; onViewsChange?: (views: TableView[]) => void; + onImport?: () => void; HotkeyScope: TableOptionsHotkeyScope; }; @@ -61,12 +61,11 @@ enum Option { export const TableOptionsDropdownButton = ({ onColumnsChange, onViewsChange, + onImport, HotkeyScope, }: TableOptionsDropdownButtonProps) => { const theme = useTheme(); - const { openSpreadsheetImport } = useSpreadsheetImport(); - const [isUnfolded, setIsUnfolded] = useState(false); const [selectedOption, setSelectedOption] = useState