Import company and person from csv file (#1236)

* feat: wip implement back-end call csv import

* fix: rebase IconBrandTwitter missing

* feat: person and company csv import

* fix: test & clean

* fix: clean & test
This commit is contained in:
Jérémy M
2023-08-16 23:18:16 +02:00
committed by GitHub
parent 5890354d21
commit 8863bb0035
74 changed files with 950 additions and 312 deletions

View File

@ -37,7 +37,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"react": "^18.2.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-datepicker": "^4.11.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",

View File

@ -647,6 +647,18 @@ export type CompanyCreateInput = {
updatedAt?: InputMaybe<Scalars['DateTime']>; updatedAt?: InputMaybe<Scalars['DateTime']>;
}; };
export type CompanyCreateManyInput = {
accountOwnerId?: InputMaybe<Scalars['String']>;
address: Scalars['String'];
createdAt?: InputMaybe<Scalars['DateTime']>;
domainName: Scalars['String'];
employees?: InputMaybe<Scalars['Int']>;
id?: InputMaybe<Scalars['String']>;
linkedinUrl?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CompanyCreateNestedOneWithoutActivityTargetInput = { export type CompanyCreateNestedOneWithoutActivityTargetInput = {
connect?: InputMaybe<CompanyWhereUniqueInput>; connect?: InputMaybe<CompanyWhereUniqueInput>;
}; };
@ -953,6 +965,8 @@ export type Mutation = {
createEvent: Analytics; createEvent: Analytics;
createFavoriteForCompany: Favorite; createFavoriteForCompany: Favorite;
createFavoriteForPerson: Favorite; createFavoriteForPerson: Favorite;
createManyCompany: AffectedRows;
createManyPerson: AffectedRows;
createManyView: AffectedRows; createManyView: AffectedRows;
createManyViewField: AffectedRows; createManyViewField: AffectedRows;
createManyViewSort: AffectedRows; createManyViewSort: AffectedRows;
@ -1021,6 +1035,18 @@ export type MutationCreateFavoriteForPersonArgs = {
}; };
export type MutationCreateManyCompanyArgs = {
data: Array<CompanyCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateManyPersonArgs = {
data: Array<PersonCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateManyViewArgs = { export type MutationCreateManyViewArgs = {
data: Array<ViewCreateManyInput>; data: Array<ViewCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>; skipDuplicates?: InputMaybe<Scalars['Boolean']>;
@ -1383,6 +1409,22 @@ export type PersonCreateInput = {
xUrl?: InputMaybe<Scalars['String']>; xUrl?: InputMaybe<Scalars['String']>;
}; };
export type PersonCreateManyInput = {
avatarUrl?: InputMaybe<Scalars['String']>;
city?: InputMaybe<Scalars['String']>;
companyId?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
email?: InputMaybe<Scalars['String']>;
firstName?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['String']>;
jobTitle?: InputMaybe<Scalars['String']>;
lastName?: InputMaybe<Scalars['String']>;
linkedinUrl?: InputMaybe<Scalars['String']>;
phone?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
xUrl?: InputMaybe<Scalars['String']>;
};
export type PersonCreateNestedManyWithoutCompanyInput = { export type PersonCreateNestedManyWithoutCompanyInput = {
connect?: InputMaybe<Array<PersonWhereUniqueInput>>; connect?: InputMaybe<Array<PersonWhereUniqueInput>>;
}; };
@ -2851,6 +2893,13 @@ export type DeleteManyCompaniesMutationVariables = Exact<{
export type DeleteManyCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } }; export type DeleteManyCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } };
export type InsertManyCompanyMutationVariables = Exact<{
data: Array<CompanyCreateManyInput> | CompanyCreateManyInput;
}>;
export type InsertManyCompanyMutation = { __typename?: 'Mutation', createManyCompany: { __typename?: 'AffectedRows', count: number } };
export type InsertOneCompanyMutationVariables = Exact<{ export type InsertOneCompanyMutationVariables = Exact<{
data: CompanyCreateInput; data: CompanyCreateInput;
}>; }>;
@ -2916,6 +2965,13 @@ export type DeleteManyPersonMutationVariables = Exact<{
export type DeleteManyPersonMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } }; export type DeleteManyPersonMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } };
export type InsertManyPersonMutationVariables = Exact<{
data: Array<PersonCreateManyInput> | PersonCreateManyInput;
}>;
export type InsertManyPersonMutation = { __typename?: 'Mutation', createManyPerson: { __typename?: 'AffectedRows', count: number } };
export type InsertOnePersonMutationVariables = Exact<{ export type InsertOnePersonMutationVariables = Exact<{
data: PersonCreateInput; 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 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> | ViewCreateManyInput;
}>;
export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } };
export type CreateViewFieldsMutationVariables = Exact<{ export type CreateViewFieldsMutationVariables = Exact<{
data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput; data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput;
}>; }>;
@ -3168,6 +3217,13 @@ export type CreateViewSortsMutationVariables = Exact<{
export type CreateViewSortsMutation = { __typename?: 'Mutation', createManyViewSort: { __typename?: 'AffectedRows', count: number } }; export type CreateViewSortsMutation = { __typename?: 'Mutation', createManyViewSort: { __typename?: 'AffectedRows', count: number } };
export type CreateViewsMutationVariables = Exact<{
data: Array<ViewCreateManyInput> | ViewCreateManyInput;
}>;
export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewSortsMutationVariables = Exact<{ export type DeleteViewSortsMutationVariables = Exact<{
where: ViewSortWhereInput; 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 UpdateViewSortMutation = { __typename?: 'Mutation', viewSort: { __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string } };
export type GetViewsQueryVariables = Exact<{
where?: InputMaybe<ViewWhereInput>;
}>;
export type GetViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'View', id: string, name: string }> };
export type GetViewFieldsQueryVariables = Exact<{ export type GetViewFieldsQueryVariables = Exact<{
where?: InputMaybe<ViewFieldWhereInput>; where?: InputMaybe<ViewFieldWhereInput>;
orderBy?: InputMaybe<Array<ViewFieldOrderByWithRelationInput> | ViewFieldOrderByWithRelationInput>; orderBy?: InputMaybe<Array<ViewFieldOrderByWithRelationInput> | 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 GetViewSortsQuery = { __typename?: 'Query', viewSorts: Array<{ __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string }> };
export type GetViewsQueryVariables = Exact<{
where?: InputMaybe<ViewWhereInput>;
}>;
export type GetViewsQuery = { __typename?: 'Query', views: Array<{ __typename?: 'View', id: string, name: string }> };
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
@ -4149,6 +4205,39 @@ export function useDeleteManyCompaniesMutation(baseOptions?: Apollo.MutationHook
export type DeleteManyCompaniesMutationHookResult = ReturnType<typeof useDeleteManyCompaniesMutation>; export type DeleteManyCompaniesMutationHookResult = ReturnType<typeof useDeleteManyCompaniesMutation>;
export type DeleteManyCompaniesMutationResult = Apollo.MutationResult<DeleteManyCompaniesMutation>; export type DeleteManyCompaniesMutationResult = Apollo.MutationResult<DeleteManyCompaniesMutation>;
export type DeleteManyCompaniesMutationOptions = Apollo.BaseMutationOptions<DeleteManyCompaniesMutation, DeleteManyCompaniesMutationVariables>; export type DeleteManyCompaniesMutationOptions = Apollo.BaseMutationOptions<DeleteManyCompaniesMutation, DeleteManyCompaniesMutationVariables>;
export const InsertManyCompanyDocument = gql`
mutation InsertManyCompany($data: [CompanyCreateManyInput!]!) {
createManyCompany(data: $data) {
count
}
}
`;
export type InsertManyCompanyMutationFn = Apollo.MutationFunction<InsertManyCompanyMutation, InsertManyCompanyMutationVariables>;
/**
* __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<InsertManyCompanyMutation, InsertManyCompanyMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InsertManyCompanyMutation, InsertManyCompanyMutationVariables>(InsertManyCompanyDocument, options);
}
export type InsertManyCompanyMutationHookResult = ReturnType<typeof useInsertManyCompanyMutation>;
export type InsertManyCompanyMutationResult = Apollo.MutationResult<InsertManyCompanyMutation>;
export type InsertManyCompanyMutationOptions = Apollo.BaseMutationOptions<InsertManyCompanyMutation, InsertManyCompanyMutationVariables>;
export const InsertOneCompanyDocument = gql` export const InsertOneCompanyDocument = gql`
mutation InsertOneCompany($data: CompanyCreateInput!) { mutation InsertOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) { createOneCompany(data: $data) {
@ -4517,6 +4606,39 @@ export function useDeleteManyPersonMutation(baseOptions?: Apollo.MutationHookOpt
export type DeleteManyPersonMutationHookResult = ReturnType<typeof useDeleteManyPersonMutation>; export type DeleteManyPersonMutationHookResult = ReturnType<typeof useDeleteManyPersonMutation>;
export type DeleteManyPersonMutationResult = Apollo.MutationResult<DeleteManyPersonMutation>; export type DeleteManyPersonMutationResult = Apollo.MutationResult<DeleteManyPersonMutation>;
export type DeleteManyPersonMutationOptions = Apollo.BaseMutationOptions<DeleteManyPersonMutation, DeleteManyPersonMutationVariables>; export type DeleteManyPersonMutationOptions = Apollo.BaseMutationOptions<DeleteManyPersonMutation, DeleteManyPersonMutationVariables>;
export const InsertManyPersonDocument = gql`
mutation InsertManyPerson($data: [PersonCreateManyInput!]!) {
createManyPerson(data: $data) {
count
}
}
`;
export type InsertManyPersonMutationFn = Apollo.MutationFunction<InsertManyPersonMutation, InsertManyPersonMutationVariables>;
/**
* __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<InsertManyPersonMutation, InsertManyPersonMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InsertManyPersonMutation, InsertManyPersonMutationVariables>(InsertManyPersonDocument, options);
}
export type InsertManyPersonMutationHookResult = ReturnType<typeof useInsertManyPersonMutation>;
export type InsertManyPersonMutationResult = Apollo.MutationResult<InsertManyPersonMutation>;
export type InsertManyPersonMutationOptions = Apollo.BaseMutationOptions<InsertManyPersonMutation, InsertManyPersonMutationVariables>;
export const InsertOnePersonDocument = gql` export const InsertOnePersonDocument = gql`
mutation InsertOnePerson($data: PersonCreateInput!) { mutation InsertOnePerson($data: PersonCreateInput!) {
createOnePerson(data: $data) { createOnePerson(data: $data) {
@ -5763,39 +5885,6 @@ export function useGetUsersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<G
export type GetUsersQueryHookResult = ReturnType<typeof useGetUsersQuery>; export type GetUsersQueryHookResult = ReturnType<typeof useGetUsersQuery>;
export type GetUsersLazyQueryHookResult = ReturnType<typeof useGetUsersLazyQuery>; export type GetUsersLazyQueryHookResult = ReturnType<typeof useGetUsersLazyQuery>;
export type GetUsersQueryResult = Apollo.QueryResult<GetUsersQuery, GetUsersQueryVariables>; export type GetUsersQueryResult = Apollo.QueryResult<GetUsersQuery, GetUsersQueryVariables>;
export const CreateViewsDocument = gql`
mutation CreateViews($data: [ViewCreateManyInput!]!) {
createManyView(data: $data) {
count
}
}
`;
export type CreateViewsMutationFn = Apollo.MutationFunction<CreateViewsMutation, CreateViewsMutationVariables>;
/**
* __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<CreateViewsMutation, CreateViewsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewsMutation, CreateViewsMutationVariables>(CreateViewsDocument, options);
}
export type CreateViewsMutationHookResult = ReturnType<typeof useCreateViewsMutation>;
export type CreateViewsMutationResult = Apollo.MutationResult<CreateViewsMutation>;
export type CreateViewsMutationOptions = Apollo.BaseMutationOptions<CreateViewsMutation, CreateViewsMutationVariables>;
export const CreateViewFieldsDocument = gql` export const CreateViewFieldsDocument = gql`
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
createManyViewField(data: $data) { createManyViewField(data: $data) {
@ -5862,6 +5951,39 @@ export function useCreateViewSortsMutation(baseOptions?: Apollo.MutationHookOpti
export type CreateViewSortsMutationHookResult = ReturnType<typeof useCreateViewSortsMutation>; export type CreateViewSortsMutationHookResult = ReturnType<typeof useCreateViewSortsMutation>;
export type CreateViewSortsMutationResult = Apollo.MutationResult<CreateViewSortsMutation>; export type CreateViewSortsMutationResult = Apollo.MutationResult<CreateViewSortsMutation>;
export type CreateViewSortsMutationOptions = Apollo.BaseMutationOptions<CreateViewSortsMutation, CreateViewSortsMutationVariables>; export type CreateViewSortsMutationOptions = Apollo.BaseMutationOptions<CreateViewSortsMutation, CreateViewSortsMutationVariables>;
export const CreateViewsDocument = gql`
mutation CreateViews($data: [ViewCreateManyInput!]!) {
createManyView(data: $data) {
count
}
}
`;
export type CreateViewsMutationFn = Apollo.MutationFunction<CreateViewsMutation, CreateViewsMutationVariables>;
/**
* __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<CreateViewsMutation, CreateViewsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewsMutation, CreateViewsMutationVariables>(CreateViewsDocument, options);
}
export type CreateViewsMutationHookResult = ReturnType<typeof useCreateViewsMutation>;
export type CreateViewsMutationResult = Apollo.MutationResult<CreateViewsMutation>;
export type CreateViewsMutationOptions = Apollo.BaseMutationOptions<CreateViewsMutation, CreateViewsMutationVariables>;
export const DeleteViewSortsDocument = gql` export const DeleteViewSortsDocument = gql`
mutation DeleteViewSorts($where: ViewSortWhereInput!) { mutation DeleteViewSorts($where: ViewSortWhereInput!) {
deleteManyViewSort(where: $where) { deleteManyViewSort(where: $where) {
@ -6004,42 +6126,6 @@ export function useUpdateViewSortMutation(baseOptions?: Apollo.MutationHookOptio
export type UpdateViewSortMutationHookResult = ReturnType<typeof useUpdateViewSortMutation>; export type UpdateViewSortMutationHookResult = ReturnType<typeof useUpdateViewSortMutation>;
export type UpdateViewSortMutationResult = Apollo.MutationResult<UpdateViewSortMutation>; export type UpdateViewSortMutationResult = Apollo.MutationResult<UpdateViewSortMutation>;
export type UpdateViewSortMutationOptions = Apollo.BaseMutationOptions<UpdateViewSortMutation, UpdateViewSortMutationVariables>; export type UpdateViewSortMutationOptions = Apollo.BaseMutationOptions<UpdateViewSortMutation, UpdateViewSortMutationVariables>;
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<GetViewsQuery, GetViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetViewsQuery, GetViewsQueryVariables>(GetViewsDocument, options);
}
export function useGetViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetViewsQuery, GetViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetViewsQuery, GetViewsQueryVariables>(GetViewsDocument, options);
}
export type GetViewsQueryHookResult = ReturnType<typeof useGetViewsQuery>;
export type GetViewsLazyQueryHookResult = ReturnType<typeof useGetViewsLazyQuery>;
export type GetViewsQueryResult = Apollo.QueryResult<GetViewsQuery, GetViewsQueryVariables>;
export const GetViewFieldsDocument = gql` export const GetViewFieldsDocument = gql`
query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) { query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) {
viewFields: findManyViewField(where: $where, orderBy: $orderBy) { viewFields: findManyViewField(where: $where, orderBy: $orderBy) {
@ -6117,6 +6203,42 @@ export function useGetViewSortsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
export type GetViewSortsQueryHookResult = ReturnType<typeof useGetViewSortsQuery>; export type GetViewSortsQueryHookResult = ReturnType<typeof useGetViewSortsQuery>;
export type GetViewSortsLazyQueryHookResult = ReturnType<typeof useGetViewSortsLazyQuery>; export type GetViewSortsLazyQueryHookResult = ReturnType<typeof useGetViewSortsLazyQuery>;
export type GetViewSortsQueryResult = Apollo.QueryResult<GetViewSortsQuery, GetViewSortsQueryVariables>; export type GetViewSortsQueryResult = Apollo.QueryResult<GetViewSortsQuery, GetViewSortsQueryVariables>;
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<GetViewsQuery, GetViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetViewsQuery, GetViewsQueryVariables>(GetViewsDocument, options);
}
export function useGetViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetViewsQuery, GetViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetViewsQuery, GetViewsQueryVariables>(GetViewsDocument, options);
}
export type GetViewsQueryHookResult = ReturnType<typeof useGetViewsQuery>;
export type GetViewsLazyQueryHookResult = ReturnType<typeof useGetViewsLazyQuery>;
export type GetViewsQueryResult = Apollo.QueryResult<GetViewsQuery, GetViewsQueryVariables>;
export const DeleteCurrentWorkspaceDocument = gql` export const DeleteCurrentWorkspaceDocument = gql`
mutation DeleteCurrentWorkspace { mutation DeleteCurrentWorkspace {
deleteCurrentWorkspace { deleteCurrentWorkspace {

View File

@ -5,7 +5,6 @@ import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/components/SpreadsheetImportProvider';
import { DialogProvider } from '@/ui/dialog/components/DialogProvider'; import { DialogProvider } from '@/ui/dialog/components/DialogProvider';
import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider'; import { SnackBarProvider } from '@/ui/snack-bar/components/SnackBarProvider';
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
@ -34,11 +33,9 @@ root.render(
<AppThemeProvider> <AppThemeProvider>
<SnackBarProvider> <SnackBarProvider>
<DialogProvider> <DialogProvider>
<SpreadsheetImportProvider> <StrictMode>
<StrictMode> <App />
<App /> </StrictMode>
</StrictMode>
</SpreadsheetImportProvider>
</DialogProvider> </DialogProvider>
</SnackBarProvider> </SnackBarProvider>
</AppThemeProvider> </AppThemeProvider>

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const INSERT_MANY_COMPANY = gql`
mutation InsertManyCompany($data: [CompanyCreateManyInput!]!) {
createManyCompany(data: $data) {
count
}
}
`;

View File

@ -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<FieldCompanyMapping>();
const upsertEntityTableItems = useUpsertEntityTableItems();
const upsertTableRowIds = useUpsertTableRowIds();
const { enqueueSnackBar } = useSnackBar();
const [createManyCompany] = useInsertManyCompanyMutation();
const openCompanySpreadsheetImport = (
options?: Omit<
SpreadsheetOptions<FieldCompanyMapping>,
'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 };
}

View File

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { companyViewFields } from '@/companies/constants/companyViewFields'; import { companyViewFields } from '@/companies/constants/companyViewFields';
import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries'; import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries';
import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries'; import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState'; import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
@ -43,6 +44,7 @@ export function CompanyTable() {
viewFieldDefinitions: companyViewFields, viewFieldDefinitions: companyViewFields,
}); });
const { updateSorts } = useViewSorts({ availableSorts }); const { updateSorts } = useViewSorts({ availableSorts });
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const filters = useRecoilScopedValue( const filters = useRecoilScopedValue(
filtersScopedState, filtersScopedState,
@ -56,6 +58,10 @@ export function CompanyTable() {
const { setContextMenuEntries } = useCompanyTableContextMenuEntries(); const { setContextMenuEntries } = useCompanyTableContextMenuEntries();
const { setActionBarEntries } = useCompanyTableActionBarEntries(); const { setActionBarEntries } = useCompanyTableActionBarEntries();
function handleImport() {
openCompanySpreadsheetImport();
}
return ( return (
<> <>
<GenericEntityTableData <GenericEntityTableData
@ -81,6 +87,7 @@ export function CompanyTable() {
onColumnsChange={handleColumnsChange} onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
onViewsChange={handleViewsChange} onViewsChange={handleViewsChange}
onImport={handleImport}
updateEntityMutation={({ updateEntityMutation={({
variables, variables,
}: { }: {

View File

@ -0,0 +1,80 @@
import {
IconBrandLinkedin,
IconBuildingSkyscraper,
IconMail,
IconMap,
IconUsers,
} from '@/ui/icon';
export const fieldsForCompany = [
{
icon: <IconBuildingSkyscraper />,
label: 'Name',
key: 'name',
alternateMatches: ['name', 'company name', 'company'],
fieldType: {
type: 'input',
},
example: 'Tim',
validations: [
{
rule: 'required',
errorMessage: 'Name is required',
level: 'error',
},
],
},
{
icon: <IconMail />,
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: <IconBrandLinkedin />,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
fieldType: {
type: 'input',
},
example: 'https://www.linkedin.com/in/apple',
},
{
icon: <IconMap />,
label: 'Address',
key: 'address',
fieldType: {
type: 'input',
},
example: 'Maple street',
validations: [
{
rule: 'required',
errorMessage: 'Address is required',
level: 'error',
},
],
},
{
icon: <IconUsers />,
label: 'Employees',
key: 'employees',
alternateMatches: ['employees', 'total employees', 'number of employees'],
fieldType: {
type: 'input',
},
example: '150',
},
] as const;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const INSERT_MANY_PERSON = gql`
mutation InsertManyPerson($data: [PersonCreateManyInput!]!) {
createManyPerson(data: $data) {
count
}
}
`;

View File

@ -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<FieldPersonMapping>();
const upsertEntityTableItems = useUpsertEntityTableItems();
const upsertTableRowIds = useUpsertTableRowIds();
const { enqueueSnackBar } = useSnackBar();
const [createManyPerson] = useInsertManyPersonMutation();
const openPersonSpreadsheetImport = (
options?: Omit<
SpreadsheetOptions<FieldPersonMapping>,
'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 };
}

View File

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { peopleViewFields } from '@/people/constants/peopleViewFields'; import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries'; import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries'; import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState'; import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
@ -35,6 +36,7 @@ export function PeopleTable() {
); );
const [updateEntityMutation] = useUpdateOnePersonMutation(); const [updateEntityMutation] = useUpdateOnePersonMutation();
const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertEntityTableItem = useUpsertEntityTableItem();
const { openPersonSpreadsheetImport } = useSpreadsheetPersonImport();
const objectId = 'person'; const objectId = 'person';
const { handleViewsChange } = useTableViews({ objectId }); const { handleViewsChange } = useTableViews({ objectId });
@ -56,6 +58,10 @@ export function PeopleTable() {
const { setContextMenuEntries } = usePersonTableContextMenuEntries(); const { setContextMenuEntries } = usePersonTableContextMenuEntries();
const { setActionBarEntries } = usePersonTableActionBarEntries(); const { setActionBarEntries } = usePersonTableActionBarEntries();
function handleImport() {
openPersonSpreadsheetImport();
}
return ( return (
<> <>
<GenericEntityTableData <GenericEntityTableData
@ -81,6 +87,7 @@ export function PeopleTable() {
onColumnsChange={handleColumnsChange} onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
onViewsChange={handleViewsChange} onViewsChange={handleViewsChange}
onImport={handleImport}
updateEntityMutation={({ updateEntityMutation={({
variables, variables,
}: { }: {

View File

@ -0,0 +1,120 @@
import { isValidPhoneNumber } from 'libphonenumber-js';
import {
IconBrandLinkedin,
IconBrandTwitter,
IconBriefcase,
IconMail,
IconMap,
IconUser,
} from '@/ui/icon';
export const fieldsForPerson = [
{
icon: <IconUser />,
label: 'Firstname',
key: 'firstName',
alternateMatches: ['first name', 'first', 'firstname'],
fieldType: {
type: 'input',
},
example: 'Tim',
validations: [
{
rule: 'required',
errorMessage: 'Firstname is required',
level: 'error',
},
],
},
{
icon: <IconUser />,
label: 'Lastname',
key: 'lastName',
alternateMatches: ['last name', 'last', 'lastname'],
fieldType: {
type: 'input',
},
example: 'Cook',
validations: [
{
rule: 'required',
errorMessage: 'Lastname is required',
level: 'error',
},
],
},
{
icon: <IconMail />,
label: 'Email',
key: 'email',
alternateMatches: ['email', 'mail'],
fieldType: {
type: 'input',
},
example: 'tim@apple.dev',
validations: [
{
rule: 'required',
errorMessage: 'email is required',
level: 'error',
},
],
},
{
icon: <IconBrandLinkedin />,
label: 'Linkedin URL',
key: 'linkedinUrl',
alternateMatches: ['linkedIn', 'linkedin', 'linkedin url'],
fieldType: {
type: 'input',
},
example: 'https://www.linkedin.com/in/timcook',
},
{
icon: <IconBrandTwitter />,
label: 'X URL',
key: 'xUrl',
alternateMatches: ['x', 'x url'],
fieldType: {
type: 'input',
},
example: 'https://x.com/tim_cook',
},
{
icon: <IconBriefcase />,
label: 'Job title',
key: 'jobTitle',
alternateMatches: ['job', 'job title'],
fieldType: {
type: 'input',
},
example: 'CEO',
},
{
icon: <IconBriefcase />,
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: <IconMap />,
label: 'City',
key: 'city',
fieldType: {
type: 'input',
},
example: 'Seattle',
},
] as const;

View File

@ -195,6 +195,7 @@ export const MatchColumnSelect = ({
value?.value !== option.value && value?.value !== option.value &&
createPortal( createPortal(
<AppTooltip <AppTooltip
key={option.value}
anchorSelect={`#${option.value}`} anchorSelect={`#${option.value}`}
content="You are already importing this column." content="You are already importing this column."
place="right" place="right"

View File

@ -1,7 +1,7 @@
import type React from 'react'; import type React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { ModalCloseButton } from './ModalCloseButton'; import { ModalCloseButton } from './ModalCloseButton';
@ -27,7 +27,7 @@ type Props = {
}; };
export const ModalWrapper = ({ children, isOpen, onClose }: Props) => { export const ModalWrapper = ({ children, isOpen, onClose }: Props) => {
const { rtl } = useRsi(); const { rtl } = useSpreadsheetImportInternal();
return ( return (
<StyledModal isOpen={isOpen}> <StyledModal isOpen={isOpen}>

View File

@ -1,25 +1,21 @@
import { createContext } from 'react'; import { createContext } from 'react';
import type { RsiProps } from '@/spreadsheet-import/types'; import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
export const RsiContext = createContext({} as any); export const RsiContext = createContext({} as any);
type ProvidersProps<T extends string> = { type ProvidersProps<T extends string> = {
children: React.ReactNode; children: React.ReactNode;
rsiValues: RsiProps<T>; values: SpreadsheetOptions<T>;
}; };
export const rootId = 'chakra-modal-rsi';
export const Providers = <T extends string>({ export const Providers = <T extends string>({
children, children,
rsiValues, values,
}: ProvidersProps<T>) => { }: ProvidersProps<T>) => {
if (!rsiValues.fields) { if (!values.fields) {
throw new Error('Fields must be provided to spreadsheet-import'); throw new Error('Fields must be provided to spreadsheet-import');
} }
return ( return <RsiContext.Provider value={values}>{children}</RsiContext.Provider>;
<RsiContext.Provider value={rsiValues}>{children}</RsiContext.Provider>
);
}; };

View File

@ -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<RsiProps<any>> = {
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 = <T extends string>(props: RsiProps<T>) => {
return (
<Providers rsiValues={props}>
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
<Steps />
</ModalWrapper>
</Providers>
);
};
SpreadsheetImport.defaultProps = defaultRSIProps;

View File

@ -1,7 +1,7 @@
import DataGrid, { DataGridProps } from 'react-data-grid'; import DataGrid, { DataGridProps } from 'react-data-grid';
import styled from '@emotion/styled'; 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'; import { rgba } from '@/ui/theme/constants/colors';
const StyledDataGrid = styled(DataGrid)` const StyledDataGrid = styled(DataGrid)`
@ -112,7 +112,7 @@ type Props<Data> = DataGridProps<Data> & {
}; };
export const Table = <Data,>(props: Props<Data>) => { export const Table = <Data,>(props: Props<Data>) => {
const { rtl } = useRsi(); const { rtl } = useSpreadsheetImportInternal();
return ( return (
<StyledDataGrid direction={rtl ? 'rtl' : 'ltr'} rowHeight={52} {...props} /> <StyledDataGrid direction={rtl ? 'rtl' : 'ltr'} rowHeight={52} {...props} />

View File

@ -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 = <T extends string>() =>
useContext<SetRequired<RsiProps<T>, keyof typeof defaultRSIProps>>(
RsiContext,
);

View File

@ -1,13 +1,13 @@
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState'; 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<T extends string>() {
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState); const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState);
const openSpreadsheetImport = ( const openSpreadsheetImport = (
options: Omit<RsiProps<string>, 'isOpen' | 'onClose'>, options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'>,
) => { ) => {
setSpreadSheetImport({ setSpreadSheetImport({
isOpen: true, isOpen: true,

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react'; 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 steps = ['uploadStep', 'matchColumnsStep', 'validationStep'] as const;
const initialStepNumber = useMemo(() => { const initialStepNumber = useMemo(() => {

View File

@ -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 = <T extends string>() =>
useContext<
SetRequired<
SpreadsheetOptions<T>,
keyof typeof defaultSpreadsheetImportProps
>
>(RsiContext);

View File

@ -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<SpreadsheetOptions<any>> = {
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 = <T extends string>(
props: SpreadsheetOptions<T>,
) => {
return (
<Providers values={props}>
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
<Steps />
</ModalWrapper>
</Providers>
);
};
SpreadsheetImport.defaultProps = defaultSpreadsheetImportProps;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { spreadsheetImportState } from '../states/spreadsheetImportState'; import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
import { SpreadsheetImport } from './SpreadsheetImport'; import { SpreadsheetImport } from './SpreadsheetImport';

View File

@ -1,13 +1,13 @@
import { atom } from 'recoil'; import { atom } from 'recoil';
import { RsiProps } from '../types'; import { SpreadsheetOptions } from '../types';
export type SpreadsheetImportState<T extends string> = { export type SpreadsheetImportState<T extends string> = {
isOpen: boolean; isOpen: boolean;
options: Omit<RsiProps<T>, 'isOpen' | 'onClose'> | null; options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'> | null;
}; };
export const spreadsheetImportState = atom<SpreadsheetImportState<string>>({ export const spreadsheetImportState = atom<SpreadsheetImportState<any>>({
key: 'spreadsheetImportState', key: 'spreadsheetImportState',
default: { default: {
isOpen: false, isOpen: false,

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
import { Heading } from '@/spreadsheet-import/components/core/Heading'; import { Heading } from '@/spreadsheet-import/components/Heading';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import type { Field, RawData } from '@/spreadsheet-import/types'; import type { Field, RawData } from '@/spreadsheet-import/types';
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields'; import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns'; import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
@ -114,7 +114,8 @@ export const MatchColumnsStep = <T extends string>({
const { enqueueDialog } = useDialog(); const { enqueueDialog } = useDialog();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const dataExample = data.slice(0, 2); const dataExample = data.slice(0, 2);
const { fields, autoMapHeaders, autoMapDistance } = useRsi<T>(); const { fields, autoMapHeaders, autoMapDistance } =
useSpreadsheetImportInternal<T>();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [columns, setColumns] = useState<Columns<T>>( const [columns, setColumns] = useState<Columns<T>>(
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them // Do not remove spread, it indexes empty array elements, otherwise map() skips over them

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { SelectOption } from '@/spreadsheet-import/types'; import { SelectOption } from '@/spreadsheet-import/types';
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions'; import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
@ -35,7 +35,7 @@ export const SubMatchingSelect = <T extends string>({
column, column,
onSubChange, onSubChange,
}: Props<T>) => { }: Props<T>) => {
const { fields } = useRsi<T>(); const { fields } = useSpreadsheetImportInternal<T>();
const options = getFieldOptions(fields, column.value) as SelectOption[]; const options = getFieldOptions(fields, column.value) as SelectOption[];
const value = options.find((opt) => opt.value === option.value); const value = options.find((opt) => opt.value === option.value);

View File

@ -8,8 +8,8 @@ import {
} from '@chakra-ui/accordion'; } from '@chakra-ui/accordion';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import type { Fields } from '@/spreadsheet-import/types'; import type { Fields } from '@/spreadsheet-import/types';
import { IconChevronDown, IconForbid } from '@/ui/icon'; import { IconChevronDown, IconForbid } from '@/ui/icon';
@ -86,7 +86,7 @@ export const TemplateColumn = <T extends string>({
onChange, onChange,
onSubChange, onSubChange,
}: TemplateColumnProps<T>) => { }: TemplateColumnProps<T>) => {
const { fields } = useRsi<T>(); const { fields } = useSpreadsheetImportInternal<T>();
const column = columns[columnIndex]; const column = columns[columnIndex];
const isIgnored = column.type === ColumnType.ignored; const isIgnored = column.type === ColumnType.ignored;
const isSelect = 'matchedOptions' in column; const isSelect = 'matchedOptions' in column;

View File

@ -1,8 +1,8 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
import { Heading } from '@/spreadsheet-import/components/core/Heading'; import { Heading } from '@/spreadsheet-import/components/Heading';
import type { RawData } from '@/spreadsheet-import/types'; import type { RawData } from '@/spreadsheet-import/types';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react'; 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 type { RawData } from '@/spreadsheet-import/types';
import { generateSelectionColumns } from './SelectColumn'; import { generateSelectionColumns } from './SelectColumn';

View File

@ -1,13 +1,12 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import styled from '@emotion/styled'; 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 { Radio } from '@/ui/input/radio/components/Radio';
import { RadioGroup } from '@/ui/input/radio/components/RadioGroup'; import { RadioGroup } from '@/ui/input/radio/components/RadioGroup';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { ContinueButton } from '../../core/ContinueButton';
const Content = styled(Modal.Content)` const Content = styled(Modal.Content)`
align-items: center; align-items: center;
`; `;

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
import { useRsiInitialStep } from '@/spreadsheet-import/hooks/useRsiInitialStep'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { Modal } from '@/ui/modal/components/Modal'; import { Modal } from '@/ui/modal/components/Modal';
import { StepBar } from '@/ui/step-bar/components/StepBar'; import { StepBar } from '@/ui/step-bar/components/StepBar';
import { useStepBar } from '@/ui/step-bar/hooks/useStepBar'; import { useStepBar } from '@/ui/step-bar/hooks/useStepBar';
@ -24,9 +24,11 @@ const stepTitles = {
} as const; } as const;
export const Steps = () => { 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({ const { nextStep, activeStep } = useStepBar({
initialStep, initialStep,

View File

@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import type XLSX from 'xlsx-ugnis'; 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 type { RawData } from '@/spreadsheet-import/types';
import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'; import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords';
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
@ -29,6 +29,7 @@ export enum StepType {
selectHeader = 'selectHeader', selectHeader = 'selectHeader',
matchColumns = 'matchColumns', matchColumns = 'matchColumns',
validateData = 'validateData', validateData = 'validateData',
loading = 'loading',
} }
export type StepState = export type StepState =
| { | {
@ -50,6 +51,9 @@ export type StepState =
| { | {
type: StepType.validateData; type: StepType.validateData;
data: any[]; data: any[];
}
| {
type: StepType.loading;
}; };
interface Props { interface Props {
@ -58,7 +62,7 @@ interface Props {
export const UploadFlow = ({ nextStep }: Props) => { export const UploadFlow = ({ nextStep }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { initialStepState } = useRsi(); const { initialStepState } = useSpreadsheetImportInternal();
const [state, setState] = useState<StepState>( const [state, setState] = useState<StepState>(
initialStepState || { type: StepType.upload }, initialStepState || { type: StepType.upload },
); );
@ -68,7 +72,7 @@ export const UploadFlow = ({ nextStep }: Props) => {
uploadStepHook, uploadStepHook,
selectHeaderStepHook, selectHeaderStepHook,
matchColumnsStepHook, matchColumnsStepHook,
} = useRsi(); } = useSpreadsheetImportInternal();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const errorToast = useCallback( const errorToast = useCallback(
@ -191,7 +195,18 @@ export const UploadFlow = ({ nextStep }: Props) => {
if (!uploadedFile) { if (!uploadedFile) {
throw new Error('File not found'); throw new Error('File not found');
} }
return <ValidationStep initialData={state.data} file={uploadedFile} />; return (
<ValidationStep
initialData={state.data}
file={uploadedFile}
onSubmitStart={() =>
setState({
type: StepType.loading,
})
}
/>
);
case StepType.loading:
default: default:
return ( return (
<ProgressBarContainer> <ProgressBarContainer>

View File

@ -3,7 +3,7 @@ import { useDropzone } from 'react-dropzone';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import * as XLSX from 'xlsx-ugnis'; 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 { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
import { MainButton } from '@/ui/button/components/MainButton'; import { MainButton } from '@/ui/button/components/MainButton';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar'; import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
@ -83,7 +83,7 @@ type DropZoneProps = {
}; };
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => { export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
const { maxFileSize, dateFormat, parseRaw } = useRsi(); const { maxFileSize, dateFormat, parseRaw } = useSpreadsheetImportInternal();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react'; 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 type { Fields } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow'; import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';

View File

@ -2,10 +2,10 @@ import { useCallback, useMemo, useState } from 'react';
import type { RowsChangeData } from 'react-data-grid'; import type { RowsChangeData } from 'react-data-grid';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton'; import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
import { Heading } from '@/spreadsheet-import/components/core/Heading'; import { Heading } from '@/spreadsheet-import/components/Heading';
import { Table } from '@/spreadsheet-import/components/core/Table'; import { Table } from '@/spreadsheet-import/components/Table';
import { useRsi } from '@/spreadsheet-import/hooks/useRsi'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import type { Data } from '@/spreadsheet-import/types'; import type { Data } from '@/spreadsheet-import/types';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { Button, ButtonVariant } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
@ -56,14 +56,17 @@ const NoRowsContainer = styled.div`
type Props<T extends string> = { type Props<T extends string> = {
initialData: Data<T>[]; initialData: Data<T>[];
file: File; file: File;
onSubmitStart?: () => void;
}; };
export const ValidationStep = <T extends string>({ export const ValidationStep = <T extends string>({
initialData, initialData,
file, file,
onSubmitStart,
}: Props<T>) => { }: Props<T>) => {
const { enqueueDialog } = useDialog(); const { enqueueDialog } = useDialog();
const { fields, onClose, onSubmit, rowHook, tableHook } = useRsi<T>(); const { fields, onClose, onSubmit, rowHook, tableHook } =
useSpreadsheetImportInternal<T>();
const [data, setData] = useState<(Data<T> & Meta)[]>( const [data, setData] = useState<(Data<T> & Meta)[]>(
useMemo( useMemo(
@ -146,7 +149,8 @@ export const ValidationStep = <T extends string>({
}, },
{ validData: [] as Data<T>[], invalidData: [] as Data<T>[], all: data }, { validData: [] as Data<T>[], invalidData: [] as Data<T>[], all: data },
); );
onSubmit(calculatedData, file); onSubmitStart?.();
await onSubmit(calculatedData, file);
onClose(); onClose();
}; };
const onContinue = () => { const onContinue = () => {

View File

@ -2,7 +2,7 @@ import { Column, useRowSelection } from 'react-data-grid';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import styled from '@emotion/styled'; 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 type { Data, Fields } from '@/spreadsheet-import/types';
import { import {
Checkbox, Checkbox,
@ -118,14 +118,13 @@ export const generateColumns = <T extends string>(
), ),
editable: column.fieldType.type !== 'checkbox', editable: column.fieldType.type !== 'checkbox',
editor: ({ row, onRowChange, onClose }) => { editor: ({ row, onRowChange, onClose }) => {
const columnKey = column.key as keyof (Data<T> & Meta);
let component; let component;
switch (column.fieldType.type) { switch (column.fieldType.type) {
case 'select': { case 'select': {
const value = column.fieldType.options.find( const value = column.fieldType.options.find(
(option) => (option) => option.value === (row[columnKey] as string),
option.value ===
(row[column.key as keyof (Data<T> & Meta)] as string),
); );
component = ( component = (
@ -139,7 +138,7 @@ export const generateColumns = <T extends string>(
: value : value
} }
onChange={(value) => { onChange={(value) => {
onRowChange({ ...row, [column.key]: value?.value }, true); onRowChange({ ...row, [columnKey]: value?.value }, true);
}} }}
options={column.fieldType.options} options={column.fieldType.options}
/> />
@ -149,9 +148,9 @@ export const generateColumns = <T extends string>(
default: default:
component = ( component = (
<TextInput <TextInput
value={row[column.key] as string} value={row[columnKey] as string}
onChange={(value: string) => { onChange={(value: string) => {
onRowChange({ ...row, [column.key]: value }); onRowChange({ ...row, [columnKey]: value });
}} }}
autoFocus={true} autoFocus={true}
onBlur={() => onClose(true)} onBlur={() => onClose(true)}
@ -165,23 +164,24 @@ export const generateColumns = <T extends string>(
editOnClick: true, editOnClick: true,
}, },
formatter: ({ row, onRowChange }) => { formatter: ({ row, onRowChange }) => {
const columnKey = column.key as keyof (Data<T> & Meta);
let component; let component;
switch (column.fieldType.type) { switch (column.fieldType.type) {
case 'checkbox': case 'checkbox':
component = ( component = (
<ToggleContainer <ToggleContainer
id={`${column.key}-${row.__index}`} id={`${columnKey}-${row.__index}`}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
}} }}
> >
<Toggle <Toggle
value={row[column.key] as boolean} value={row[columnKey] as boolean}
onChange={() => { onChange={() => {
onRowChange({ onRowChange({
...row, ...row,
[column.key]: !row[column.key as T], [columnKey]: !row[columnKey],
}); });
}} }}
/> />
@ -190,30 +190,30 @@ export const generateColumns = <T extends string>(
break; break;
case 'select': case 'select':
component = ( component = (
<DefaultContainer id={`${column.key}-${row.__index}`}> <DefaultContainer id={`${columnKey}-${row.__index}`}>
{column.fieldType.options.find( {column.fieldType.options.find(
(option) => option.value === row[column.key as T], (option) => option.value === row[columnKey as T],
)?.label || null} )?.label || null}
</DefaultContainer> </DefaultContainer>
); );
break; break;
default: default:
component = ( component = (
<DefaultContainer id={`${column.key}-${row.__index}`}> <DefaultContainer id={`${columnKey}-${row.__index}`}>
{row[column.key as T]} {row[columnKey]}
</DefaultContainer> </DefaultContainer>
); );
} }
if (row.__errors?.[column.key]) { if (row.__errors?.[columnKey]) {
return ( return (
<> <>
{component} {component}
{createPortal( {createPortal(
<AppTooltip <AppTooltip
anchorSelect={`#${column.key}-${row.__index}`} anchorSelect={`#${columnKey}-${row.__index}`}
place="top" place="top"
content={row.__errors?.[column.key]?.message} content={row.__errors?.[columnKey]?.message}
/>, />,
document.body, document.body,
)} )}

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react'; import { Meta } from '@storybook/react';
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/core/Providers'; import { Providers } from '@/spreadsheet-import/components/Providers';
import { MatchColumnsStep } from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
const meta: Meta<typeof MatchColumnsStep> = { const meta: Meta<typeof MatchColumnsStep> = {
@ -58,7 +58,7 @@ const mockData = [
export function Default() { export function Default() {
return ( return (
<Providers rsiValues={mockRsiValues}> <Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}> <ModalWrapper isOpen={true} onClose={() => null}>
<MatchColumnsStep <MatchColumnsStep
headerValues={mockData[0] as string[]} headerValues={mockData[0] as string[]}

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react'; import { Meta } from '@storybook/react';
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/core/Providers'; import { Providers } from '@/spreadsheet-import/components/Providers';
import { SelectHeaderStep } from '@/spreadsheet-import/components/steps/SelectHeaderStep/SelectHeaderStep'; import { SelectHeaderStep } from '@/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep';
import { import {
headerSelectionTableFields, headerSelectionTableFields,
mockRsiValues, mockRsiValues,
@ -20,7 +20,7 @@ export default meta;
export function Default() { export function Default() {
return ( return (
<Providers rsiValues={mockRsiValues}> <Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}> <ModalWrapper isOpen={true} onClose={() => null}>
<SelectHeaderStep <SelectHeaderStep
data={headerSelectionTableFields} data={headerSelectionTableFields}

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react'; import { Meta } from '@storybook/react';
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/core/Providers'; import { Providers } from '@/spreadsheet-import/components/Providers';
import { SelectSheetStep } from '@/spreadsheet-import/components/steps/SelectSheetStep/SelectSheetStep'; import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
const meta: Meta<typeof SelectSheetStep> = { const meta: Meta<typeof SelectSheetStep> = {
@ -19,7 +19,7 @@ const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
export function Default() { export function Default() {
return ( return (
<Providers rsiValues={mockRsiValues}> <Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}> <ModalWrapper isOpen={true} onClose={() => null}>
<SelectSheetStep <SelectSheetStep
sheetNames={sheetNames} sheetNames={sheetNames}

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react'; import { Meta } from '@storybook/react';
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/core/Providers'; import { Providers } from '@/spreadsheet-import/components/Providers';
import { UploadStep } from '@/spreadsheet-import/components/steps/UploadStep/UploadStep'; import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep';
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
const meta: Meta<typeof UploadStep> = { const meta: Meta<typeof UploadStep> = {
@ -17,7 +17,7 @@ export default meta;
export function Default() { export function Default() {
return ( return (
<Providers rsiValues={mockRsiValues}> <Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}> <ModalWrapper isOpen={true} onClose={() => null}>
<UploadStep onContinue={() => Promise.resolve()} /> <UploadStep onContinue={() => Promise.resolve()} />
</ModalWrapper> </ModalWrapper>

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react'; import { Meta } from '@storybook/react';
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { Providers } from '@/spreadsheet-import/components/core/Providers'; import { Providers } from '@/spreadsheet-import/components/Providers';
import { ValidationStep } from '@/spreadsheet-import/components/steps/ValidationStep/ValidationStep'; import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep';
import { import {
editableTableInitialData, editableTableInitialData,
mockRsiValues, mockRsiValues,
@ -22,7 +22,7 @@ const file = new File([''], 'file.csv');
export function Default() { export function Default() {
return ( return (
<Providers rsiValues={mockRsiValues}> <Providers values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}> <ModalWrapper isOpen={true} onClose={() => null}>
<ValidationStep initialData={editableTableInitialData} file={file} /> <ValidationStep initialData={editableTableInitialData} file={file} />
</ModalWrapper> </ModalWrapper>

View File

@ -1,5 +1,5 @@
import { defaultRSIProps } from '@/spreadsheet-import/components/SpreadsheetImport'; import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
import type { RsiProps } from '@/spreadsheet-import/types'; import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
const fields = [ const fields = [
{ {
@ -87,13 +87,14 @@ const fields = [
}, },
] as const; ] as const;
const mockComponentBehaviourForTypes = <T extends string>(props: RsiProps<T>) => const mockComponentBehaviourForTypes = <T extends string>(
props; props: SpreadsheetOptions<T>,
) => props;
export const mockRsiValues = mockComponentBehaviourForTypes({ export const mockRsiValues = mockComponentBehaviourForTypes({
...defaultRSIProps, ...defaultSpreadsheetImportProps,
fields: fields, fields: fields,
onSubmit: (data) => { onSubmit: async (data) => {
console.log(data.all.map((value) => value)); console.log(data.all.map((value) => value));
}, },
isOpen: true, isOpen: true,

View File

@ -1,16 +1,16 @@
import { ReadonlyDeep } from 'type-fest'; import { ReadonlyDeep } from 'type-fest';
import { Columns } from '../components/steps/MatchColumnsStep/MatchColumnsStep'; import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { StepState } from '../components/steps/UploadFlow'; import { StepState } from '@/spreadsheet-import/steps/components/UploadFlow';
import { Meta } from '../components/steps/ValidationStep/types'; import { Meta } from '@/spreadsheet-import/steps/components/ValidationStep/types';
export type RsiProps<T extends string> = { export type SpreadsheetOptions<Keys extends string> = {
// Is modal visible. // Is modal visible.
isOpen: boolean; isOpen: boolean;
// callback when RSI is closed before final submit // callback when RSI is closed before final submit
onClose: () => void; onClose: () => void;
// Field description for requested data // Field description for requested data
fields: Fields<T>; fields: Fields<Keys>;
// Runs after file upload step, receives and returns raw sheet data // Runs after file upload step, receives and returns raw sheet data
uploadStepHook?: (data: RawData[]) => Promise<RawData[]>; uploadStepHook?: (data: RawData[]) => Promise<RawData[]>;
// Runs after header selection step, receives and returns raw sheet data // Runs after header selection step, receives and returns raw sheet data
@ -20,16 +20,16 @@ export type RsiProps<T extends string> = {
) => Promise<{ headerValues: RawData; data: RawData[] }>; ) => Promise<{ headerValues: RawData; data: RawData[] }>;
// Runs once before validation step, used for data mutations and if you want to change how columns were matched // Runs once before validation step, used for data mutations and if you want to change how columns were matched
matchColumnsStepHook?: ( matchColumnsStepHook?: (
table: Data<T>[], table: Data<Keys>[],
rawData: RawData[], rawData: RawData[],
columns: Columns<T>, columns: Columns<Keys>,
) => Promise<Data<T>[]>; ) => Promise<Data<Keys>[]>;
// Runs after column matching and on entry change // Runs after column matching and on entry change
rowHook?: RowHook<T>; rowHook?: RowHook<Keys>;
// Runs after column matching and on entry change // Runs after column matching and on entry change
tableHook?: TableHook<T>; tableHook?: TableHook<Keys>;
// Function called after user finishes the flow // Function called after user finishes the flow
onSubmit: (data: Result<T>, file: File) => void; onSubmit: (data: Result<Keys>, file: File) => Promise<void>;
// Allows submitting with errors. Default: true // Allows submitting with errors. Default: true
allowInvalidSubmit?: boolean; allowInvalidSubmit?: boolean;
// Theme configuration passed to underlying Chakra-UI // Theme configuration passed to underlying Chakra-UI
@ -110,7 +110,8 @@ export type Input = {
export type Validation = export type Validation =
| RequiredValidation | RequiredValidation
| UniqueValidation | UniqueValidation
| RegexValidation; | RegexValidation
| FunctionValidation;
export type RequiredValidation = { export type RequiredValidation = {
rule: 'required'; rule: 'required';
@ -133,6 +134,13 @@ export type RegexValidation = {
level?: ErrorLevel; level?: ErrorLevel;
}; };
export type FunctionValidation = {
rule: 'function';
isValid: (value: string) => boolean;
errorMessage: string;
level?: ErrorLevel;
};
export type RowHook<T extends string> = ( export type RowHook<T extends string> = (
row: Data<T>, row: Data<T>,
addError: (fieldKey: T, error: Info) => void, addError: (fieldKey: T, error: Info) => void,

View File

@ -3,7 +3,7 @@ import { v4 } from 'uuid';
import type { import type {
Errors, Errors,
Meta, Meta,
} from '@/spreadsheet-import/components/steps/ValidationStep/types'; } from '@/spreadsheet-import/steps/components/ValidationStep/types';
import type { import type {
Data, Data,
Fields, Fields,
@ -93,8 +93,9 @@ export const addErrorsAndRunHooks = <T extends string>(
case 'regex': { case 'regex': {
const regex = new RegExp(validation.value, validation.flags); const regex = new RegExp(validation.value, validation.flags);
data.forEach((entry, index) => { data.forEach((entry, index) => {
const value = entry[field.key]?.toString() ?? ''; const value = entry[field.key]?.toString();
if (!value.match(regex)) {
if (value && !value.match(regex)) {
errors[index] = { errors[index] = {
...errors[index], ...errors[index],
[field.key]: { [field.key]: {
@ -108,6 +109,22 @@ export const addErrorsAndRunHooks = <T extends string>(
}); });
break; 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;
}
} }
}); });
}); });

View File

@ -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'; import type { Fields } from '@/spreadsheet-import/types';
export const findUnmatchedRequiredFields = <T extends string>( export const findUnmatchedRequiredFields = <T extends string>(

View File

@ -4,7 +4,7 @@ import type {
Column, Column,
Columns, Columns,
MatchColumnsProps, MatchColumnsProps,
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import type { Field, Fields } from '@/spreadsheet-import/types'; import type { Field, Fields } from '@/spreadsheet-import/types';
import { findMatch } from './findMatch'; import { findMatch } from './findMatch';

View File

@ -1,7 +1,7 @@
import { import {
Columns, Columns,
ColumnType, 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 type { Data, Fields, RawData } from '@/spreadsheet-import/types';
import { normalizeCheckboxValue } from './normalizeCheckboxValue'; import { normalizeCheckboxValue } from './normalizeCheckboxValue';

View File

@ -2,7 +2,7 @@ import {
Column, Column,
ColumnType, ColumnType,
MatchColumnsProps, MatchColumnsProps,
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import type { Field } from '@/spreadsheet-import/types'; import type { Field } from '@/spreadsheet-import/types';
import { uniqueEntries } from './uniqueEntries'; import { uniqueEntries } from './uniqueEntries';

View File

@ -1,7 +1,7 @@
import { import {
Column, Column,
ColumnType, ColumnType,
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
export const setIgnoreColumn = <T extends string>({ export const setIgnoreColumn = <T extends string>({
header, header,

View File

@ -3,7 +3,7 @@ import {
MatchedOptions, MatchedOptions,
MatchedSelectColumn, MatchedSelectColumn,
MatchedSelectOptionsColumn, MatchedSelectOptionsColumn,
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
export const setSubColumn = <T>( export const setSubColumn = <T>(
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>, oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>,

View File

@ -3,7 +3,7 @@ import uniqBy from 'lodash/uniqBy';
import type { import type {
MatchColumnsProps, MatchColumnsProps,
MatchedOptions, MatchedOptions,
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep'; } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
export const uniqueEntries = <T extends string>( export const uniqueEntries = <T extends string>(
data: MatchColumnsProps<T>['data'], data: MatchColumnsProps<T>['data'],

View File

@ -19,7 +19,11 @@ export function DialogProvider({ children }: React.PropsWithChildren) {
<> <>
{children} {children}
{dialogState.queue.map((dialog) => ( {dialogState.queue.map((dialog) => (
<Dialog {...dialog} onClose={() => handleDialogClose(dialog.id)} /> <Dialog
key={dialog.id}
{...dialog}
onClose={() => handleDialogClose(dialog.id)}
/>
))} ))}
</> </>
); );

View File

@ -11,6 +11,7 @@ export {
IconBrandGithub, IconBrandGithub,
IconBrandGoogle, IconBrandGoogle,
IconBrandLinkedin, IconBrandLinkedin,
IconBrandTwitter,
IconBrandX, IconBrandX,
IconBriefcase, IconBriefcase,
IconBuildingSkyscraper, IconBuildingSkyscraper,

View File

@ -30,7 +30,7 @@ const Container = styled.div<{ labelPosition?: LabelPosition }>`
`; `;
type RadioInputProps = { type RadioInputProps = {
radioSize?: RadioSize; 'radio-size'?: RadioSize;
}; };
const RadioInput = styled(motion.input)<RadioInputProps>` const RadioInput = styled(motion.input)<RadioInputProps>`
@ -60,13 +60,13 @@ const RadioInput = styled(motion.input)<RadioInputProps>`
background-color: ${({ theme }) => theme.grayScale.gray0}; background-color: ${({ theme }) => theme.grayScale.gray0};
border-radius: 50%; border-radius: 50%;
content: ''; content: '';
height: ${({ radioSize }) => height: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'}; radioSize === RadioSize.Large ? '8px' : '6px'};
left: 50%; left: 50%;
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: ${({ radioSize }) => width: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '8px' : '6px'}; radioSize === RadioSize.Large ? '8px' : '6px'};
} }
} }
@ -74,10 +74,10 @@ const RadioInput = styled(motion.input)<RadioInputProps>`
cursor: not-allowed; cursor: not-allowed;
opacity: 0.12; opacity: 0.12;
} }
height: ${({ radioSize }) => height: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'}; radioSize === RadioSize.Large ? '18px' : '16px'};
position: relative; position: relative;
width: ${({ radioSize }) => width: ${({ 'radio-size': radioSize }) =>
radioSize === RadioSize.Large ? '18px' : '16px'}; radioSize === RadioSize.Large ? '18px' : '16px'};
`; `;
@ -134,7 +134,7 @@ export function Radio({
data-testid="input-radio" data-testid="input-radio"
checked={checked} checked={checked}
value={value} value={value}
radioSize={size} radio-size={size}
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
initial={{ scale: 0.95 }} initial={{ scale: 0.95 }}

View File

@ -10,7 +10,7 @@ const Container = styled.div<{ isLast: boolean }>`
flex-grow: ${({ isLast }) => (isLast ? '0' : '1')}; flex-grow: ${({ isLast }) => (isLast ? '0' : '1')};
`; `;
const StepCircle = styled(motion.div)<{ isCurrent: boolean }>` const StepCircle = styled(motion.div)`
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
border-style: solid; border-style: solid;
@ -40,7 +40,7 @@ const StepLabel = styled.span<{ isActive: boolean }>`
white-space: nowrap; white-space: nowrap;
`; `;
const StepLine = styled(motion.div)<{ isActive: boolean }>` const StepLine = styled(motion.div)`
height: 2px; height: 2px;
margin-left: ${({ theme }) => theme.spacing(2)}; margin-left: ${({ theme }) => theme.spacing(2)};
margin-right: ${({ theme }) => theme.spacing(2)}; margin-right: ${({ theme }) => theme.spacing(2)};
@ -92,7 +92,6 @@ export const Step = ({
return ( return (
<Container isLast={isLast}> <Container isLast={isLast}>
<StepCircle <StepCircle
isCurrent={isActive}
variants={variantsCircle} variants={variantsCircle}
animate={isActive ? 'active' : 'inactive'} animate={isActive ? 'active' : 'inactive'}
> >
@ -107,7 +106,6 @@ export const Step = ({
<StepLabel isActive={isActive}>{label}</StepLabel> <StepLabel isActive={isActive}>{label}</StepLabel>
{!isLast && ( {!isLast && (
<StepLine <StepLine
isActive={isActive}
variants={variantsLine} variants={variantsLine}
animate={isActive ? 'active' : 'inactive'} animate={isActive ? 'active' : 'inactive'}
/> />

View File

@ -99,6 +99,7 @@ type OwnProps<SortField> = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void; onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void; onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
updateEntityMutation: any; updateEntityMutation: any;
}; };
@ -108,6 +109,7 @@ export function EntityTable<SortField>({
onColumnsChange, onColumnsChange,
onSortsUpdate, onSortsUpdate,
onViewsChange, onViewsChange,
onImport,
updateEntityMutation, updateEntityMutation,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const tableBodyRef = useRef<HTMLDivElement>(null); const tableBodyRef = useRef<HTMLDivElement>(null);
@ -136,6 +138,7 @@ export function EntityTable<SortField>({
onColumnsChange={onColumnsChange} onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate} onSortsUpdate={onSortsUpdate}
onViewsChange={onViewsChange} onViewsChange={onViewsChange}
onImport={onImport}
/> />
<StyledTableWrapper> <StyledTableWrapper>
<StyledTable> <StyledTable>

View File

@ -0,0 +1,34 @@
import { useRecoilCallback } from 'recoil';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
export function useUpsertEntityTableItems() {
return useRecoilCallback(
({ set, snapshot }) =>
<T extends { id: string }>(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);
}
},
[],
);
}

View File

@ -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);
},
[],
);
}

View File

@ -9,7 +9,6 @@ import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
@ -51,6 +50,7 @@ import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = { type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void; onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onViewsChange?: (views: TableView[]) => void; onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
HotkeyScope: TableOptionsHotkeyScope; HotkeyScope: TableOptionsHotkeyScope;
}; };
@ -61,12 +61,11 @@ enum Option {
export const TableOptionsDropdownButton = ({ export const TableOptionsDropdownButton = ({
onColumnsChange, onColumnsChange,
onViewsChange, onViewsChange,
onImport,
HotkeyScope, HotkeyScope,
}: TableOptionsDropdownButtonProps) => { }: TableOptionsDropdownButtonProps) => {
const theme = useTheme(); const theme = useTheme();
const { openSpreadsheetImport } = useSpreadsheetImport();
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>( const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined, undefined,
@ -94,16 +93,6 @@ export const TableOptionsDropdownButton = ({
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope(); } = usePreviousHotkeyScope();
function handleImport() {
openSpreadsheetImport({
onSubmit: (datam, file) => {
console.log('datam', datam);
console.log('file', file);
},
fields: [],
});
}
const handleColumnVisibilityChange = useCallback( const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => { (columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) => const nextColumns = columns.map((column) =>
@ -245,8 +234,8 @@ export const TableOptionsDropdownButton = ({
<IconTag size={theme.icon.size.md} /> <IconTag size={theme.icon.size.md} />
Properties Properties
</DropdownMenuItem> </DropdownMenuItem>
{false && ( {onImport && (
<DropdownMenuItem onClick={handleImport}> <DropdownMenuItem onClick={onImport}>
<IconFileImport size={theme.icon.size.md} /> <IconFileImport size={theme.icon.size.md} />
Import Import
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -26,6 +26,7 @@ type OwnProps<SortField> = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void; onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void; onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
}; };
export function TableHeader<SortField>({ export function TableHeader<SortField>({
@ -34,6 +35,7 @@ export function TableHeader<SortField>({
onColumnsChange, onColumnsChange,
onSortsUpdate, onSortsUpdate,
onViewsChange, onViewsChange,
onImport,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>( const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortScopedState, sortScopedState,
@ -82,6 +84,7 @@ export function TableHeader<SortField>({
isPrimaryButton isPrimaryButton
/> />
<TableOptionsDropdownButton <TableOptionsDropdownButton
onImport={onImport}
onColumnsChange={onColumnsChange} onColumnsChange={onColumnsChange}
onViewsChange={onViewsChange} onViewsChange={onViewsChange}
HotkeyScope={TableOptionsHotkeyScope.Dropdown} HotkeyScope={TableOptionsHotkeyScope.Dropdown}

View File

@ -6,6 +6,7 @@ import { v4 } from 'uuid';
import { CompanyTable } from '@/companies/table/components/CompanyTable'; import { CompanyTable } from '@/companies/table/components/CompanyTable';
import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery'; import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { IconBuildingSkyscraper } from '@/ui/icon'; import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
@ -64,21 +65,23 @@ export function Companies() {
const theme = useTheme(); const theme = useTheme();
return ( return (
<WithTopBarContainer <SpreadsheetImportProvider>
title="Companies" <WithTopBarContainer
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />} title="Companies"
onAddButtonClick={handleAddButtonClick} icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
> onAddButtonClick={handleAddButtonClick}
<RecoilScope
scopeId="companies"
SpecificContext={TableRecoilScopeContext}
> >
<StyledTableContainer> <RecoilScope
<CompanyTable /> scopeId="companies"
</StyledTableContainer> SpecificContext={TableRecoilScopeContext}
<EntityTableActionBar /> >
<EntityTableContextMenu /> <StyledTableContainer>
</RecoilScope> <CompanyTable />
</WithTopBarContainer> </StyledTableContainer>
<EntityTableActionBar />
<EntityTableContextMenu />
</RecoilScope>
</WithTopBarContainer>
</SpreadsheetImportProvider>
); );
} }

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { PeopleTable } from '@/people/table/components/PeopleTable'; import { PeopleTable } from '@/people/table/components/PeopleTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { IconUser } from '@/ui/icon'; import { IconUser } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
@ -56,18 +57,20 @@ export function People() {
const theme = useTheme(); const theme = useTheme();
return ( return (
<RecoilScope scopeId="people" SpecificContext={TableRecoilScopeContext}> <SpreadsheetImportProvider>
<WithTopBarContainer <RecoilScope scopeId="people" SpecificContext={TableRecoilScopeContext}>
title="People" <WithTopBarContainer
icon={<IconUser size={theme.icon.size.sm} />} title="People"
onAddButtonClick={handleAddButtonClick} icon={<IconUser size={theme.icon.size.sm} />}
> onAddButtonClick={handleAddButtonClick}
<StyledTableContainer> >
<PeopleTable /> <StyledTableContainer>
</StyledTableContainer> <PeopleTable />
<EntityTableActionBar /> </StyledTableContainer>
<EntityTableContextMenu /> <EntityTableActionBar />
</WithTopBarContainer> <EntityTableContextMenu />
</RecoilScope> </WithTopBarContainer>
</RecoilScope>
</SpreadsheetImportProvider>
); );
} }

View File

@ -8083,10 +8083,10 @@ clsx@1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
clsx@^1.1.1: clsx@^2.0.0:
version "1.2.1" version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
cmdk@^0.2.0: cmdk@^0.2.0:
version "0.2.0" version "0.2.0"
@ -15937,12 +15937,12 @@ react-colorful@^5.1.2:
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
react-data-grid@7.0.0-beta.13: react-data-grid@^7.0.0-beta.36:
version "7.0.0-beta.13" version "7.0.0-beta.36"
resolved "https://registry.yarnpkg.com/react-data-grid/-/react-data-grid-7.0.0-beta.13.tgz#c0728258cbbb033af611eed50ed124744a76ee76" resolved "https://registry.yarnpkg.com/react-data-grid/-/react-data-grid-7.0.0-beta.36.tgz#d607b8f35fc1b90cfae078e9ae89ccbe29279f9f"
integrity sha512-vhBdkC2KqAawmmzYTcNlfhfjcYFQsinNr5pPTUG6/3DzLfYWo1S6nl48wgPWgyD9uDclV3H5NWvKSSwQTTsYMQ== integrity sha512-HU+qVusA9UQU1bQsqKjkyRS4beL95Plfv5BoJ2Qc/wU1HsqxAM5K7NVqrmPQhYKUO+GxdkJkl2aDYSTgXk81Ww==
dependencies: dependencies:
clsx "^1.1.1" clsx "^2.0.0"
react-datepicker@^4.11.0: react-datepicker@^4.11.0:
version "4.16.0" version "4.16.0"

4
infra/dev/yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

View File

@ -27,6 +27,7 @@ import {
import { UserAbility } from 'src/decorators/user-ability.decorator'; import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { FindUniqueCompanyArgs } from 'src/core/@generated/company/find-unique-company.args'; import { FindUniqueCompanyArgs } from 'src/core/@generated/company/find-unique-company.args';
import { CreateManyCompanyArgs } from 'src/core/@generated/company/create-many-company.args';
import { CompanyService } from './company.service'; import { CompanyService } from './company.service';
@ -123,4 +124,22 @@ export class CompanyResolver {
select: prismaSelect.value, select: prismaSelect.value,
} as Prisma.CompanyCreateArgs); } as Prisma.CompanyCreateArgs);
} }
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateCompanyAbilityHandler)
async createManyCompany(
@Args() args: CreateManyCompanyArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Prisma.BatchPayload> {
return this.companyService.createMany({
data: args.data.map((company) => ({
...company,
workspaceId: workspace.id,
})),
skipDuplicates: args.skipDuplicates,
});
}
} }

View File

@ -39,6 +39,7 @@ import { AppAbility } from 'src/ability/ability.factory';
import { Workspace } from 'src/core/@generated/workspace/workspace.model'; import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { FileUploadService } from 'src/core/file/services/file-upload.service'; import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { CreateManyPersonArgs } from 'src/core/@generated/person/create-many-person.args';
import { PersonService } from './person.service'; import { PersonService } from './person.service';
@ -165,6 +166,24 @@ export class PersonResolver {
} as Prisma.PersonCreateArgs); } as Prisma.PersonCreateArgs);
} }
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreatePersonAbilityHandler)
async createManyPerson(
@Args() args: CreateManyPersonArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<Prisma.BatchPayload> {
return this.personService.createMany({
data: args.data.map((person) => ({
...person,
workspaceId: workspace.id,
})),
skipDuplicates: args.skipDuplicates,
});
}
@Mutation(() => String) @Mutation(() => String)
@UseGuards(AbilityGuard) @UseGuards(AbilityGuard)
@CheckAbilities(UpdatePersonAbilityHandler) @CheckAbilities(UpdatePersonAbilityHandler)