feat: add views dropdown (list, add & edit views) (#1220)

Closes #1218
This commit is contained in:
Thaïs
2023-08-15 21:08:02 +02:00
committed by GitHub
parent 7a330b4a02
commit 4e654654da
36 changed files with 1037 additions and 212 deletions

View File

@ -953,6 +953,7 @@ export type Mutation = {
createEvent: Analytics; createEvent: Analytics;
createFavoriteForCompany: Favorite; createFavoriteForCompany: Favorite;
createFavoriteForPerson: Favorite; createFavoriteForPerson: Favorite;
createManyView: AffectedRows;
createManyViewField: AffectedRows; createManyViewField: AffectedRows;
createManyViewSort: AffectedRows; createManyViewSort: AffectedRows;
createOneActivity: Activity; createOneActivity: Activity;
@ -978,6 +979,7 @@ export type Mutation = {
updateOnePerson?: Maybe<Person>; updateOnePerson?: Maybe<Person>;
updateOnePipelineProgress?: Maybe<PipelineProgress>; updateOnePipelineProgress?: Maybe<PipelineProgress>;
updateOnePipelineStage?: Maybe<PipelineStage>; updateOnePipelineStage?: Maybe<PipelineStage>;
updateOneView: View;
updateOneViewField: ViewField; updateOneViewField: ViewField;
updateOneViewSort: ViewSort; updateOneViewSort: ViewSort;
updateUser: User; updateUser: User;
@ -1019,6 +1021,12 @@ export type MutationCreateFavoriteForPersonArgs = {
}; };
export type MutationCreateManyViewArgs = {
data: Array<ViewCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateManyViewFieldArgs = { export type MutationCreateManyViewFieldArgs = {
data: Array<ViewFieldCreateManyInput>; data: Array<ViewFieldCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>; skipDuplicates?: InputMaybe<Scalars['Boolean']>;
@ -1143,6 +1151,12 @@ export type MutationUpdateOnePipelineStageArgs = {
}; };
export type MutationUpdateOneViewArgs = {
data: ViewUpdateInput;
where: ViewWhereUniqueInput;
};
export type MutationUpdateOneViewFieldArgs = { export type MutationUpdateOneViewFieldArgs = {
data: ViewFieldUpdateInput; data: ViewFieldUpdateInput;
where: ViewFieldWhereUniqueInput; where: ViewFieldWhereUniqueInput;
@ -1866,6 +1880,7 @@ export type Query = {
findManyPipelineProgress: Array<PipelineProgress>; findManyPipelineProgress: Array<PipelineProgress>;
findManyPipelineStage: Array<PipelineStage>; findManyPipelineStage: Array<PipelineStage>;
findManyUser: Array<User>; findManyUser: Array<User>;
findManyView: Array<View>;
findManyViewField: Array<ViewField>; findManyViewField: Array<ViewField>;
findManyViewSort: Array<ViewSort>; findManyViewSort: Array<ViewSort>;
findManyWorkspaceMember: Array<WorkspaceMember>; findManyWorkspaceMember: Array<WorkspaceMember>;
@ -1955,6 +1970,16 @@ export type QueryFindManyUserArgs = {
}; };
export type QueryFindManyViewArgs = {
cursor?: InputMaybe<ViewWhereUniqueInput>;
distinct?: InputMaybe<Array<ViewScalarFieldEnum>>;
orderBy?: InputMaybe<Array<ViewOrderByWithRelationInput>>;
skip?: InputMaybe<Scalars['Int']>;
take?: InputMaybe<Scalars['Int']>;
where?: InputMaybe<ViewWhereInput>;
};
export type QueryFindManyViewFieldArgs = { export type QueryFindManyViewFieldArgs = {
cursor?: InputMaybe<ViewFieldWhereUniqueInput>; cursor?: InputMaybe<ViewFieldWhereUniqueInput>;
distinct?: InputMaybe<Array<ViewFieldScalarFieldEnum>>; distinct?: InputMaybe<Array<ViewFieldScalarFieldEnum>>;
@ -2283,6 +2308,13 @@ export type View = {
type: ViewType; type: ViewType;
}; };
export type ViewCreateManyInput = {
id?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
objectId: Scalars['String'];
type: ViewType;
};
export type ViewCreateNestedOneWithoutFieldsInput = { export type ViewCreateNestedOneWithoutFieldsInput = {
connect?: InputMaybe<ViewWhereUniqueInput>; connect?: InputMaybe<ViewWhereUniqueInput>;
}; };
@ -2361,6 +2393,12 @@ export type ViewFieldUpdateInput = {
view?: InputMaybe<ViewUpdateOneWithoutFieldsNestedInput>; view?: InputMaybe<ViewUpdateOneWithoutFieldsNestedInput>;
}; };
export type ViewFieldUpdateManyWithoutViewNestedInput = {
connect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>;
set?: InputMaybe<Array<ViewFieldWhereUniqueInput>>;
};
export type ViewFieldUpdateManyWithoutWorkspaceNestedInput = { export type ViewFieldUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>; connect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>; disconnect?: InputMaybe<Array<ViewFieldWhereUniqueInput>>;
@ -2406,6 +2444,14 @@ export type ViewRelationFilter = {
isNot?: InputMaybe<ViewWhereInput>; isNot?: InputMaybe<ViewWhereInput>;
}; };
export enum ViewScalarFieldEnum {
Id = 'id',
Name = 'name',
ObjectId = 'objectId',
Type = 'type',
WorkspaceId = 'workspaceId'
}
export type ViewSort = { export type ViewSort = {
__typename?: 'ViewSort'; __typename?: 'ViewSort';
direction: ViewSortDirection; direction: ViewSortDirection;
@ -2460,6 +2506,12 @@ export type ViewSortUpdateInput = {
view?: InputMaybe<ViewUpdateOneRequiredWithoutSortsNestedInput>; view?: InputMaybe<ViewUpdateOneRequiredWithoutSortsNestedInput>;
}; };
export type ViewSortUpdateManyWithoutViewNestedInput = {
connect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
set?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
};
export type ViewSortUpdateManyWithoutWorkspaceNestedInput = { export type ViewSortUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewSortWhereUniqueInput>>; connect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewSortWhereUniqueInput>>; disconnect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
@ -2491,6 +2543,15 @@ export enum ViewType {
Table = 'Table' Table = 'Table'
} }
export type ViewUpdateInput = {
fields?: InputMaybe<ViewFieldUpdateManyWithoutViewNestedInput>;
id?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
objectId?: InputMaybe<Scalars['String']>;
sorts?: InputMaybe<ViewSortUpdateManyWithoutViewNestedInput>;
type?: InputMaybe<ViewType>;
};
export type ViewUpdateManyWithoutWorkspaceNestedInput = { export type ViewUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewWhereUniqueInput>>; connect?: InputMaybe<Array<ViewWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewWhereUniqueInput>>; disconnect?: InputMaybe<Array<ViewWhereUniqueInput>>;
@ -3086,6 +3147,13 @@ 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;
}>; }>;
@ -3107,6 +3175,14 @@ export type DeleteViewSortsMutationVariables = Exact<{
export type DeleteViewSortsMutation = { __typename?: 'Mutation', deleteManyViewSort: { __typename?: 'AffectedRows', count: number } }; export type DeleteViewSortsMutation = { __typename?: 'Mutation', deleteManyViewSort: { __typename?: 'AffectedRows', count: number } };
export type UpdateViewMutationVariables = Exact<{
data: ViewUpdateInput;
where: ViewWhereUniqueInput;
}>;
export type UpdateViewMutation = { __typename?: 'Mutation', updateOneView: { __typename?: 'View', id: string, name: string } };
export type UpdateViewFieldMutationVariables = Exact<{ export type UpdateViewFieldMutationVariables = Exact<{
data: ViewFieldUpdateInput; data: ViewFieldUpdateInput;
where: ViewFieldWhereUniqueInput; where: ViewFieldWhereUniqueInput;
@ -3123,6 +3199,13 @@ 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>;
@ -5680,6 +5763,39 @@ 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) {
@ -5779,6 +5895,41 @@ export function useDeleteViewSortsMutation(baseOptions?: Apollo.MutationHookOpti
export type DeleteViewSortsMutationHookResult = ReturnType<typeof useDeleteViewSortsMutation>; export type DeleteViewSortsMutationHookResult = ReturnType<typeof useDeleteViewSortsMutation>;
export type DeleteViewSortsMutationResult = Apollo.MutationResult<DeleteViewSortsMutation>; export type DeleteViewSortsMutationResult = Apollo.MutationResult<DeleteViewSortsMutation>;
export type DeleteViewSortsMutationOptions = Apollo.BaseMutationOptions<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>; export type DeleteViewSortsMutationOptions = Apollo.BaseMutationOptions<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>;
export const UpdateViewDocument = gql`
mutation UpdateView($data: ViewUpdateInput!, $where: ViewWhereUniqueInput!) {
updateOneView(data: $data, where: $where) {
id
name
}
}
`;
export type UpdateViewMutationFn = Apollo.MutationFunction<UpdateViewMutation, UpdateViewMutationVariables>;
/**
* __useUpdateViewMutation__
*
* To run a mutation, you first call `useUpdateViewMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateViewMutation` 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 [updateViewMutation, { data, loading, error }] = useUpdateViewMutation({
* variables: {
* data: // value for 'data'
* where: // value for 'where'
* },
* });
*/
export function useUpdateViewMutation(baseOptions?: Apollo.MutationHookOptions<UpdateViewMutation, UpdateViewMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateViewMutation, UpdateViewMutationVariables>(UpdateViewDocument, options);
}
export type UpdateViewMutationHookResult = ReturnType<typeof useUpdateViewMutation>;
export type UpdateViewMutationResult = Apollo.MutationResult<UpdateViewMutation>;
export type UpdateViewMutationOptions = Apollo.BaseMutationOptions<UpdateViewMutation, UpdateViewMutationVariables>;
export const UpdateViewFieldDocument = gql` export const UpdateViewFieldDocument = gql`
mutation UpdateViewField($data: ViewFieldUpdateInput!, $where: ViewFieldWhereUniqueInput!) { mutation UpdateViewField($data: ViewFieldUpdateInput!, $where: ViewFieldWhereUniqueInput!) {
updateOneViewField(data: $data, where: $where) { updateOneViewField(data: $data, where: $where) {
@ -5853,6 +6004,42 @@ 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) {

View File

@ -1,5 +1,4 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { companyViewFields } from '@/companies/constants/companyViewFields'; import { companyViewFields } from '@/companies/constants/companyViewFields';
import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries'; import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries';
@ -7,15 +6,15 @@ import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyT
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';
import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields'; import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useTableViews } from '@/views/hooks/useTableViews';
import { useViewSorts } from '@/views/hooks/useViewSorts'; import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
SortOrder, SortOrder,
UpdateOneCompanyMutationVariables, UpdateOneCompanyMutationVariables,
@ -26,7 +25,10 @@ import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts'; import { availableSorts } from '~/pages/companies/companies-sorts';
export function CompanyTable() { export function CompanyTable() {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const orderBy = useRecoilScopedValue( const orderBy = useRecoilScopedValue(
sortsOrderByScopedState, sortsOrderByScopedState,
TableRecoilScopeContext, TableRecoilScopeContext,
@ -34,14 +36,13 @@ export function CompanyTable() {
const [updateEntityMutation] = useUpdateOneCompanyMutation(); const [updateEntityMutation] = useUpdateOneCompanyMutation();
const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertEntityTableItem = useUpsertEntityTableItem();
const objectId = 'company';
const { handleViewsChange } = useTableViews({ objectId });
const { handleColumnsChange } = useTableViewFields({ const { handleColumnsChange } = useTableViewFields({
objectName: 'company', objectName: objectId,
viewFieldDefinitions: companyViewFields, viewFieldDefinitions: companyViewFields,
}); });
const { updateSorts } = useViewSorts({ const { updateSorts } = useViewSorts({ availableSorts });
availableSorts,
Context: TableRecoilScopeContext,
});
const filters = useRecoilScopedValue( const filters = useRecoilScopedValue(
filtersScopedState, filtersScopedState,
@ -76,10 +77,10 @@ export function CompanyTable() {
/> />
<EntityTable <EntityTable
viewName="All Companies" viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts} availableSorts={availableSorts}
onColumnsChange={handleColumnsChange} onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
onViewsChange={handleViewsChange}
updateEntityMutation={({ updateEntityMutation={({
variables, variables,
}: { }: {

View File

@ -1,4 +1,3 @@
import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { useUpdateOneCompanyMutation } from '~/generated/graphql'; import { useUpdateOneCompanyMutation } from '~/generated/graphql';
import { availableSorts } from '~/pages/companies/companies-sorts'; import { availableSorts } from '~/pages/companies/companies-sorts';
@ -11,7 +10,6 @@ export function CompanyTableMockMode() {
<CompanyTableMockData /> <CompanyTableMockData />
<EntityTable <EntityTable
viewName="All Companies" viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts} availableSorts={availableSorts}
updateEntityMutation={[useUpdateOneCompanyMutation()]} updateEntityMutation={[useUpdateOneCompanyMutation()]}
/> />

View File

@ -1,5 +1,4 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { peopleViewFields } from '@/people/constants/peopleViewFields'; import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries'; import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
@ -7,15 +6,15 @@ import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableAct
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';
import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable'; import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields'; import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useTableViews } from '@/views/hooks/useTableViews';
import { useViewSorts } from '@/views/hooks/useViewSorts'; import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
SortOrder, SortOrder,
UpdateOnePersonMutationVariables, UpdateOnePersonMutationVariables,
@ -26,7 +25,10 @@ import { peopleFilters } from '~/pages/people/people-filters';
import { availableSorts } from '~/pages/people/people-sorts'; import { availableSorts } from '~/pages/people/people-sorts';
export function PeopleTable() { export function PeopleTable() {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const orderBy = useRecoilScopedValue( const orderBy = useRecoilScopedValue(
sortsOrderByScopedState, sortsOrderByScopedState,
TableRecoilScopeContext, TableRecoilScopeContext,
@ -34,14 +36,13 @@ export function PeopleTable() {
const [updateEntityMutation] = useUpdateOnePersonMutation(); const [updateEntityMutation] = useUpdateOnePersonMutation();
const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertEntityTableItem = useUpsertEntityTableItem();
const objectId = 'person';
const { handleViewsChange } = useTableViews({ objectId });
const { handleColumnsChange } = useTableViewFields({ const { handleColumnsChange } = useTableViewFields({
objectName: 'person', objectName: objectId,
viewFieldDefinitions: peopleViewFields, viewFieldDefinitions: peopleViewFields,
}); });
const { updateSorts } = useViewSorts({ const { updateSorts } = useViewSorts({ availableSorts });
availableSorts,
Context: TableRecoilScopeContext,
});
const filters = useRecoilScopedValue( const filters = useRecoilScopedValue(
filtersScopedState, filtersScopedState,
@ -76,10 +77,10 @@ export function PeopleTable() {
/> />
<EntityTable <EntityTable
viewName="All People" viewName="All People"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts} availableSorts={availableSorts}
onColumnsChange={handleColumnsChange} onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
onViewsChange={handleViewsChange}
updateEntityMutation={({ updateEntityMutation={({
variables, variables,
}: { }: {

View File

@ -0,0 +1,34 @@
import { forwardRef, InputHTMLAttributes } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/theme/constants/effects';
export const DropdownMenuInputContainer = styled.div`
--vertical-padding: ${({ theme }) => theme.spacing(1)};
align-items: center;
display: flex;
flex-direction: row;
height: calc(36px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) 0;
width: calc(100%);
`;
const StyledInput = styled.input`
font-size: ${({ theme }) => theme.font.size.sm};
${textInputStyle}
width: 100%;
`;
export const DropdownMenuInput = forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
>((props, ref) => (
<DropdownMenuInputContainer>
<StyledInput autoComplete="off" placeholder="Search" {...props} ref={ref} />
</DropdownMenuInputContainer>
));

View File

@ -1,39 +0,0 @@
import { InputHTMLAttributes } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/theme/constants/effects';
export const DropdownMenuSearchContainer = styled.div`
--vertical-padding: ${({ theme }) => theme.spacing(1)};
align-items: center;
display: flex;
flex-direction: row;
height: calc(36px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) 0;
width: calc(100%);
`;
const StyledEditModeSearchInput = styled.input`
font-size: ${({ theme }) => theme.font.size.sm};
${textInputStyle}
width: 100%;
`;
export function DropdownMenuSearch(
props: InputHTMLAttributes<HTMLInputElement>,
) {
return (
<DropdownMenuSearchContainer>
<StyledEditModeSearchInput
autoComplete="off"
{...props}
placeholder={props.placeholder ?? 'Search'}
/>
</DropdownMenuSearchContainer>
);
}

View File

@ -11,9 +11,9 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenu } from '../DropdownMenu'; import { DropdownMenu } from '../DropdownMenu';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem'; import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader'; import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput';
import { DropdownMenuItem } from '../DropdownMenuItem'; import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
import { DropdownMenuSearch } from '../DropdownMenuSearch';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '../DropdownMenuSeparator'; import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
import { DropdownMenuSubheader } from '../DropdownMenuSubheader'; import { DropdownMenuSubheader } from '../DropdownMenuSubheader';
@ -256,7 +256,7 @@ export const LoadingMenu: Story = {
...WithContentBelow, ...WithContentBelow,
render: () => ( render: () => (
<DropdownMenu> <DropdownMenu>
<DropdownMenuSearch value={'query'} autoFocus /> <DropdownMenuInput value={'query'} autoFocus />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuSkeletonItem /> <DropdownMenuSkeletonItem />
@ -269,7 +269,7 @@ export const Search: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <DropdownMenu {...args}>
<DropdownMenuSearch /> <DropdownMenuInput />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => ( {mockSelectArray.map(({ name }) => (

View File

@ -4,19 +4,18 @@ import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { DropdownMenuContainer } from './DropdownMenuContainer'; import { DropdownMenuContainer } from './DropdownMenuContainer';
type OwnProps = { type OwnProps = {
label: string; anchor?: 'left' | 'right';
label: ReactNode;
isActive: boolean; isActive: boolean;
children?: ReactNode; children?: ReactNode;
isUnfolded?: boolean; isUnfolded?: boolean;
icon?: ReactNode; icon?: ReactNode;
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void; onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
resetState?: () => void; resetState?: () => void;
HotkeyScope: FiltersHotkeyScope; HotkeyScope: string;
color?: string; color?: string;
}; };
@ -59,7 +58,12 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
} }
`; `;
const StyledDropdownMenuContainer = styled(DropdownMenuContainer)`
z-index: 2;
`;
function DropdownButton({ function DropdownButton({
anchor,
label, label,
isActive, isActive,
children, children,
@ -99,9 +103,9 @@ function DropdownButton({
{label} {label}
</StyledDropdownButton> </StyledDropdownButton>
{isUnfolded && ( {isUnfolded && (
<DropdownMenuContainer onClose={onOutsideClick}> <StyledDropdownMenuContainer anchor={anchor} onClose={onOutsideClick}>
{children} {children}
</DropdownMenuContainer> </StyledDropdownMenuContainer>
)} )}
</StyledDropdownButtonContainer> </StyledDropdownButtonContainer>
); );

View File

@ -1,23 +1,32 @@
import { useRef } from 'react'; import { type HTMLAttributes, useRef } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { DropdownMenu } from '../../dropdown/components/DropdownMenu'; export const StyledDropdownMenuContainer = styled.ul<{
anchor: 'left' | 'right';
export const StyledDropdownMenuContainer = styled.ul` }>`
padding: 0;
position: absolute; position: absolute;
right: 0; ${({ anchor }) => {
if (anchor === 'right') return 'right: 0';
}};
top: 14px; top: 14px;
`; `;
export function DropdownMenuContainer({ type DropdownMenuContainerProps = {
children, anchor?: 'left' | 'right';
onClose,
}: {
children: React.ReactNode; children: React.ReactNode;
onClose?: () => void; onClose?: () => void;
}) { } & HTMLAttributes<HTMLUListElement>;
export function DropdownMenuContainer({
anchor = 'right',
children,
onClose,
...props
}: DropdownMenuContainerProps) {
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
useListenClickOutside({ useListenClickOutside({
@ -28,7 +37,7 @@ export function DropdownMenuContainer({
}); });
return ( return (
<StyledDropdownMenuContainer data-select-disable> <StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}>
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu> <DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
</StyledDropdownMenuContainer> </StyledDropdownMenuContainer>
); );

View File

@ -1,6 +1,6 @@
import { ChangeEvent, Context } from 'react'; import { ChangeEvent, Context } from 'react';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState'; import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState'; import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState'; import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
@ -27,7 +27,7 @@ export function FilterDropdownEntitySearchInput({
return ( return (
filterDefinitionUsedInDropdown && filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && ( selectedOperandInDropdown && (
<DropdownMenuSearch <DropdownMenuInput
type="text" type="text"
value={filterDropdownSearchInput} value={filterDropdownSearchInput}
placeholder={filterDefinitionUsedInDropdown.label} placeholder={filterDefinitionUsedInDropdown.label}

View File

@ -1,6 +1,6 @@
import { ChangeEvent, Context } from 'react'; import { ChangeEvent, Context } from 'react';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRemoveFilter } from '../hooks/useRemoveFilter'; import { useRemoveFilter } from '../hooks/useRemoveFilter';
@ -29,7 +29,7 @@ export function FilterDropdownNumberSearchInput({
return ( return (
filterDefinitionUsedInDropdown && filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && ( selectedOperandInDropdown && (
<DropdownMenuSearch <DropdownMenuInput
type="number" type="number"
placeholder={filterDefinitionUsedInDropdown.label} placeholder={filterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {

View File

@ -1,6 +1,6 @@
import { ChangeEvent, Context } from 'react'; import { ChangeEvent, Context } from 'react';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited'; import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
@ -36,7 +36,7 @@ export function FilterDropdownTextSearchInput({
return ( return (
filterDefinitionUsedInDropdown && filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && ( selectedOperandInDropdown && (
<DropdownMenuSearch <DropdownMenuInput
type="text" type="text"
placeholder={filterDefinitionUsedInDropdown.label} placeholder={filterDefinitionUsedInDropdown.label}
value={filterCurrentlyEdited?.value ?? filterDropdownSearchInput} value={filterCurrentlyEdited?.value ?? filterDropdownSearchInput}

View File

@ -1,58 +1,60 @@
export { IconAddressBook } from './components/IconAddressBook'; export { IconAddressBook } from './components/IconAddressBook';
export { IconBuildingSkyscraper } from '@tabler/icons-react'; export {
export { IconMessageCircle as IconComment } from '@tabler/icons-react'; IconAlertCircle,
export { IconCheck } from '@tabler/icons-react'; IconAlertTriangle,
export { IconTrash } from '@tabler/icons-react'; IconArchive,
export { IconLayoutSidebarRightCollapse } from '@tabler/icons-react'; IconArrowNarrowDown,
export { IconLayoutSidebarLeftCollapse } from '@tabler/icons-react'; IconArrowNarrowUp,
export { IconUser } from '@tabler/icons-react'; IconArrowRight,
export { IconBell } from '@tabler/icons-react'; IconArrowUpRight,
export { IconList } from '@tabler/icons-react'; IconBell,
export { IconInbox } from '@tabler/icons-react'; IconBrandGithub,
export { IconSearch } from '@tabler/icons-react'; IconBrandGoogle,
export { IconArchive } from '@tabler/icons-react'; IconBrandLinkedin,
export { IconSettings } from '@tabler/icons-react'; IconBrandX,
export { IconLogout } from '@tabler/icons-react'; IconBriefcase,
export { IconColorSwatch } from '@tabler/icons-react'; IconBuildingSkyscraper,
export { IconProgressCheck } from '@tabler/icons-react'; IconCalendar,
export { IconX } from '@tabler/icons-react'; IconCalendarEvent,
export { IconChevronLeft } from '@tabler/icons-react'; IconCheck,
export { IconBriefcase } from '@tabler/icons-react'; IconCheckbox,
export { IconPlus } from '@tabler/icons-react'; IconChevronDown,
export { IconMinus } from '@tabler/icons-react'; IconChevronLeft,
export { IconLink } from '@tabler/icons-react'; IconChevronsRight,
export { IconBrandLinkedin } from '@tabler/icons-react'; IconCircleDot,
export { IconUsers } from '@tabler/icons-react'; IconCirclePlus,
export { IconCalendarEvent } from '@tabler/icons-react'; IconColorSwatch,
export { IconMap } from '@tabler/icons-react'; IconMessageCircle as IconComment,
export { IconMail } from '@tabler/icons-react'; IconCopy,
export { IconPhone } from '@tabler/icons-react'; IconCurrencyDollar,
export { IconTargetArrow } from '@tabler/icons-react'; IconEye,
export { IconChevronDown } from '@tabler/icons-react'; IconEyeOff,
export { IconArrowNarrowDown } from '@tabler/icons-react'; IconFileUpload,
export { IconArrowNarrowUp } from '@tabler/icons-react'; IconHeart,
export { IconArrowRight } from '@tabler/icons-react'; IconHelpCircle,
export { IconArrowUpRight } from '@tabler/icons-react'; IconInbox,
export { IconBrandGoogle } from '@tabler/icons-react'; IconLayoutSidebarLeftCollapse,
export { IconUpload } from '@tabler/icons-react'; IconLayoutSidebarRightCollapse,
export { IconFileUpload } from '@tabler/icons-react'; IconLink,
export { IconChevronsRight } from '@tabler/icons-react'; IconList,
export { IconNotes } from '@tabler/icons-react'; IconLogout,
export { IconCirclePlus } from '@tabler/icons-react'; IconMail,
export { IconCheckbox } from '@tabler/icons-react'; IconMap,
export { IconTimelineEvent } from '@tabler/icons-react'; IconMinus,
export { IconAlertCircle } from '@tabler/icons-react'; IconNotes,
export { IconEye } from '@tabler/icons-react'; IconPencil,
export { IconEyeOff } from '@tabler/icons-react'; IconPhone,
export { IconAlertTriangle } from '@tabler/icons-react'; IconPlus,
export { IconCopy } from '@tabler/icons-react'; IconProgressCheck,
export { IconCurrencyDollar } from '@tabler/icons-react'; IconSearch,
export { IconUserCircle } from '@tabler/icons-react'; IconSettings,
export { IconCalendar } from '@tabler/icons-react'; IconTag,
export { IconPencil } from '@tabler/icons-react'; IconTargetArrow,
export { IconCircleDot } from '@tabler/icons-react'; IconTimelineEvent,
export { IconHeart } from '@tabler/icons-react'; IconTrash,
export { IconBrandX } from '@tabler/icons-react'; IconUpload,
export { IconTag } from '@tabler/icons-react'; IconUser,
export { IconHelpCircle } from '@tabler/icons-react'; IconUserCircle,
export { IconBrandGithub } from '@tabler/icons-react'; IconUsers,
IconX,
} from '@tabler/icons-react';

View File

@ -3,9 +3,9 @@ import debounce from 'lodash.debounce';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem'; import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
@ -73,7 +73,7 @@ export function MultipleEntitySelect<
return ( return (
<DropdownMenu ref={containerRef}> <DropdownMenu ref={containerRef}>
<DropdownMenuSearch <DropdownMenuInput
value={searchFilter} value={searchFilter}
onChange={handleFilterChange} onChange={handleFilterChange}
autoFocus autoFocus

View File

@ -2,9 +2,9 @@ import { useRef } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { IconPlus } from '@/ui/icon'; import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -65,7 +65,7 @@ export function SingleEntitySelect<
ref={containerRef} ref={containerRef}
width={width} width={width}
> >
<DropdownMenuSearch <DropdownMenuInput
value={searchFilter} value={searchFilter}
onChange={handleSearchFilterChange} onChange={handleSearchFilterChange}
autoFocus autoFocus

View File

@ -14,6 +14,7 @@ import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus'; import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection'; import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState'; import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
import type { TableView } from '../states/tableViewsState';
import { TableHeader } from '../table-header/components/TableHeader'; import { TableHeader } from '../table-header/components/TableHeader';
import { EntityTableBody } from './EntityTableBody'; import { EntityTableBody } from './EntityTableBody';
@ -97,16 +98,16 @@ type OwnProps<SortField> = {
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void; onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onRowSelectionChange?: (rowSelection: string[]) => void; onViewsChange?: (views: TableView[]) => void;
updateEntityMutation: any; updateEntityMutation: any;
}; };
export function EntityTable<SortField>({ export function EntityTable<SortField>({
viewName, viewName,
viewIcon,
availableSorts, availableSorts,
onColumnsChange, onColumnsChange,
onSortsUpdate, onSortsUpdate,
onViewsChange,
updateEntityMutation, updateEntityMutation,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const tableBodyRef = useRef<HTMLDivElement>(null); const tableBodyRef = useRef<HTMLDivElement>(null);
@ -131,10 +132,10 @@ export function EntityTable<SortField>({
<StyledTableContainer ref={tableBodyRef}> <StyledTableContainer ref={tableBodyRef}>
<TableHeader <TableHeader
viewName={viewName} viewName={viewName}
viewIcon={viewIcon}
availableSorts={availableSorts} availableSorts={availableSorts}
onColumnsChange={onColumnsChange} onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate} onSortsUpdate={onSortsUpdate}
onViewsChange={onViewsChange}
/> />
<StyledTableWrapper> <StyledTableWrapper>
<StyledTable> <StyledTable>

View File

@ -1,30 +1,50 @@
import { useCallback, useState } from 'react'; import {
type FormEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
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 { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { import type {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField'; } from '@/ui/editable-field/types/ViewField';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton'; import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon'; import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
import { import {
hiddenTableColumnsState, hiddenTableColumnsState,
tableColumnsState, tableColumnsState,
visibleTableColumnsState, visibleTableColumnsState,
} from '@/ui/table/states/tableColumnsState'; } from '@/ui/table/states/tableColumnsState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import {
type TableView,
tableViewEditModeState,
tableViewsByIdState,
tableViewsState,
} from '../../states/tableViewsState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection'; import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = { type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void; onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
HotkeyScope: FiltersHotkeyScope; onViewsChange?: (views: TableView[]) => void;
HotkeyScope: TableOptionsHotkeyScope;
}; };
enum Option { enum Option {
@ -33,17 +53,37 @@ enum Option {
export const TableOptionsDropdownButton = ({ export const TableOptionsDropdownButton = ({
onColumnsChange, onColumnsChange,
onViewsChange,
HotkeyScope, HotkeyScope,
}: TableOptionsDropdownButtonProps) => { }: TableOptionsDropdownButtonProps) => {
const theme = useTheme(); const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>( const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined, undefined,
); );
const viewEditInputRef = useRef<HTMLInputElement>(null);
const [columns, setColumns] = useRecoilState(tableColumnsState); const [columns, setColumns] = useRecoilState(tableColumnsState);
const [viewEditMode, setViewEditMode] = useRecoilState(
tableViewEditModeState,
);
const [views, setViews] = useRecoilScopedState(
tableViewsState,
TableRecoilScopeContext,
);
const visibleColumns = useRecoilValue(visibleTableColumnsState); const visibleColumns = useRecoilValue(visibleTableColumnsState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState); const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const viewsById = useRecoilScopedValue(
tableViewsByIdState,
TableRecoilScopeContext,
);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleColumnVisibilityChange = useCallback( const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => { (columnId: string, nextIsVisible: boolean) => {
@ -79,25 +119,109 @@ export const TableOptionsDropdownButton = ({
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length], [handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
); );
const resetViewEditMode = useCallback(() => {
setViewEditMode({ mode: undefined, viewId: undefined });
if (viewEditInputRef.current) {
viewEditInputRef.current.value = '';
}
}, [setViewEditMode]);
const handleViewNameSubmit = useCallback(
(event?: FormEvent) => {
event?.preventDefault();
if (viewEditMode.mode && viewEditInputRef.current?.value) {
const name = viewEditInputRef.current.value;
const nextViews =
viewEditMode.mode === 'create'
? [...views, { id: v4(), name }]
: views.map((view) =>
view.id === viewEditMode.viewId ? { ...view, name } : view,
);
(onViewsChange ?? setViews)(nextViews);
}
resetViewEditMode();
},
[
onViewsChange,
resetViewEditMode,
setViews,
viewEditMode.mode,
viewEditMode.viewId,
views,
],
);
const handleSelectOption = useCallback(
(option: Option) => {
handleViewNameSubmit();
setIsUnfolded(true);
setSelectedOption(option);
},
[handleViewNameSubmit],
);
const resetSelectedOption = useCallback(() => { const resetSelectedOption = useCallback(() => {
setSelectedOption(undefined); setSelectedOption(undefined);
}, []); }, []);
const handleUnfoldedChange = useCallback(
(nextIsUnfolded: boolean) => {
setIsUnfolded(nextIsUnfolded);
if (!nextIsUnfolded) {
handleViewNameSubmit();
resetSelectedOption();
}
},
[handleViewNameSubmit, resetSelectedOption],
);
useEffect(() => {
isUnfolded || viewEditMode.mode
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
: goBackToPreviousHotkeyScope();
}, [
HotkeyScope,
goBackToPreviousHotkeyScope,
isUnfolded,
setHotkeyScopeAndMemorizePreviousScope,
viewEditMode.mode,
]);
return ( return (
<DropdownButton <DropdownButton
label="Options" label="Options"
isActive={false} isActive={false}
isUnfolded={isUnfolded} isUnfolded={isUnfolded || !!viewEditMode.mode}
onIsUnfoldedChange={setIsUnfolded} onIsUnfoldedChange={handleUnfoldedChange}
HotkeyScope={HotkeyScope} HotkeyScope={HotkeyScope}
> >
{!selectedOption && ( {!selectedOption && (
<> <>
<DropdownMenuHeader>View settings</DropdownMenuHeader> {!!viewEditMode.mode ? (
<DropdownMenuInput
ref={viewEditInputRef}
autoFocus
placeholder={
viewEditMode.mode === 'create' ? 'New view' : 'View name'
}
defaultValue={
viewEditMode.viewId
? viewsById[viewEditMode.viewId]?.name
: undefined
}
/>
) : (
<DropdownMenuHeader>View settings</DropdownMenuHeader>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<DropdownMenuItem <DropdownMenuItem
onClick={() => setSelectedOption(Option.Properties)} onClick={() => handleSelectOption(Option.Properties)}
> >
<IconTag size={theme.icon.size.md} /> <IconTag size={theme.icon.size.md} />
Properties Properties

View File

@ -0,0 +1,149 @@
import { type MouseEvent, useCallback, useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { IconChevronDown, IconList, IconPencil, IconPlus } from '@/ui/icon';
import {
currentTableViewIdState,
currentTableViewState,
tableViewEditModeState,
tableViewsState,
} from '@/ui/table/states/tableViewsState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope';
const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledDropdownLabelAdornments = styled.span`
align-items: center;
color: ${({ theme }) => theme.grayScale.gray35};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledViewIcon = styled(IconList)`
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type TableViewsDropdownButtonProps = {
defaultViewName: string;
HotkeyScope: TableViewsHotkeyScope;
};
export const TableViewsDropdownButton = ({
defaultViewName,
HotkeyScope,
}: TableViewsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const currentView = useRecoilScopedValue(
currentTableViewState,
TableRecoilScopeContext,
);
const views = useRecoilScopedValue(tableViewsState, TableRecoilScopeContext);
const [, setCurrentViewId] = useRecoilScopedState(
currentTableViewIdState,
TableRecoilScopeContext,
);
const setViewEditMode = useSetRecoilState(tableViewEditModeState);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleViewSelect = useCallback(
(viewId?: string) => {
setCurrentViewId(viewId);
setIsUnfolded(false);
},
[setCurrentViewId],
);
const handleAddViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined });
setIsUnfolded(false);
}, [setViewEditMode]);
const handleEditViewButtonClick = useCallback(
(event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation();
setViewEditMode({ mode: 'edit', viewId });
setIsUnfolded(false);
},
[setViewEditMode],
);
useEffect(() => {
isUnfolded
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
: goBackToPreviousHotkeyScope();
}, [
HotkeyScope,
goBackToPreviousHotkeyScope,
isUnfolded,
setHotkeyScopeAndMemorizePreviousScope,
]);
return (
<DropdownButton
label={
<>
<StyledViewIcon size={theme.icon.size.md} />
{currentView?.name || defaultViewName}{' '}
<StyledDropdownLabelAdornments>
· {views.length + 1} <IconChevronDown size={theme.icon.size.sm} />
</StyledDropdownLabelAdornments>
</>
}
isActive={false}
isUnfolded={isUnfolded}
onIsUnfoldedChange={setIsUnfolded}
anchor="left"
HotkeyScope={HotkeyScope}
>
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem onClick={() => handleViewSelect(undefined)}>
<IconList size={theme.icon.size.md} />
{defaultViewName}
</DropdownMenuItem>
{views.map((view) => (
<DropdownMenuItem
key={view.id}
actions={
<IconButton
onClick={(event) => handleEditViewButtonClick(event, view.id)}
icon={<IconPencil size={theme.icon.size.sm} />}
/>
}
onClick={() => handleViewSelect(view.id)}
>
<IconList size={theme.icon.size.md} />
{view.name}
</DropdownMenuItem>
))}
</StyledDropdownMenuItemsContainer>
<DropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleAddViewButtonClick}>
<IconPlus size={theme.icon.size.md} />
Add view
</DropdownMenuItem>
</StyledDropdownMenuItemsContainer>
</DropdownButton>
);
};

View File

@ -0,0 +1,50 @@
import { atom, atomFamily, selectorFamily } from 'recoil';
export type TableView = { id: string; name: string };
export const tableViewsState = atomFamily<TableView[], string>({
key: 'tableViewsState',
default: [],
});
export const tableViewsByIdState = selectorFamily<
Record<string, TableView>,
string
>({
key: 'tableViewsByIdState',
get:
(scopeId) =>
({ get }) =>
get(tableViewsState(scopeId)).reduce<Record<string, TableView>>(
(result, view) => ({ ...result, [view.id]: view }),
{},
),
});
export const currentTableViewIdState = atomFamily<string | undefined, string>({
key: 'currentTableViewIdState',
default: undefined,
});
export const currentTableViewState = selectorFamily<
TableView | undefined,
string
>({
key: 'currentTableViewState',
get:
(scopeId) =>
({ get }) => {
const currentViewId = get(currentTableViewIdState(scopeId));
return currentViewId
? get(tableViewsByIdState(scopeId))[currentViewId]
: undefined;
},
});
export const tableViewEditModeState = atom<{
mode: 'create' | 'edit' | undefined;
viewId: string | undefined;
}>({
key: 'tableViewEditModeState',
default: { mode: undefined, viewId: undefined },
});

View File

@ -1,5 +1,4 @@
import { ReactNode, useCallback } from 'react'; import { useCallback } from 'react';
import styled from '@emotion/styled';
import type { import type {
ViewFieldDefinition, ViewFieldDefinition,
@ -15,32 +14,26 @@ import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableO
import { TopBar } from '@/ui/top-bar/TopBar'; import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import type { TableView } from '../../states/tableViewsState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope';
type OwnProps<SortField> = { type OwnProps<SortField> = {
viewName: string; viewName: string;
viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<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;
}; };
const StyledIcon = styled.div`
display: flex;
margin-left: ${({ theme }) => theme.spacing(1)};
margin-right: ${({ theme }) => theme.spacing(2)};
& > svg {
font-size: ${({ theme }) => theme.icon.size.sm};
}
`;
export function TableHeader<SortField>({ export function TableHeader<SortField>({
viewName, viewName,
viewIcon,
availableSorts, availableSorts,
onColumnsChange, onColumnsChange,
onSortsUpdate, onSortsUpdate,
onViewsChange,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>( const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortScopedState, sortScopedState,
@ -67,10 +60,10 @@ export function TableHeader<SortField>({
return ( return (
<TopBar <TopBar
leftComponent={ leftComponent={
<> <TableViewsDropdownButton
<StyledIcon>{viewIcon}</StyledIcon> defaultViewName={viewName}
{viewName} HotkeyScope={TableViewsHotkeyScope.Dropdown}
</> />
} }
displayBottomBorder={false} displayBottomBorder={false}
rightComponent={ rightComponent={
@ -90,7 +83,8 @@ export function TableHeader<SortField>({
/> />
<TableOptionsDropdownButton <TableOptionsDropdownButton
onColumnsChange={onColumnsChange} onColumnsChange={onColumnsChange}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} onViewsChange={onViewsChange}
HotkeyScope={TableOptionsHotkeyScope.Dropdown}
/> />
</> </>
} }

View File

@ -0,0 +1,3 @@
export enum TableOptionsHotkeyScope {
Dropdown = 'table-options-dropdown',
}

View File

@ -0,0 +1,3 @@
export enum TableViewsHotkeyScope {
Dropdown = 'table-views-dropdown',
}

View File

@ -4,13 +4,15 @@ import { v4 } from 'uuid';
import { RecoilScopeContext } from '../states/RecoilScopeContext'; import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function RecoilScope({ export function RecoilScope({
SpecificContext,
children, children,
scopeId,
SpecificContext,
}: { }: {
SpecificContext?: Context<string | null>;
children: React.ReactNode; children: React.ReactNode;
scopeId?: string;
SpecificContext?: Context<string | null>;
}) { }) {
const currentScopeId = useRef(v4()); const currentScopeId = useRef(scopeId || v4());
return SpecificContext ? ( return SpecificContext ? (
<SpecificContext.Provider value={currentScopeId.current}> <SpecificContext.Provider value={currentScopeId.current}>

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const CREATE_VIEWS = gql`
mutation CreateViews($data: [ViewCreateManyInput!]!) {
createManyView(data: $data) {
count
}
}
`;

View File

@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const UPDATE_VIEW = gql`
mutation UpdateView($data: ViewUpdateInput!, $where: ViewWhereUniqueInput!) {
updateOneView(data: $data, where: $where) {
id
name
}
}
`;

View File

@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const GET_VIEWS = gql`
query GetViews($where: ViewWhereInput) {
views: findManyView(where: $where) {
id
name
}
}
`;

View File

@ -7,11 +7,13 @@ import {
ViewFieldMetadata, ViewFieldMetadata,
ViewFieldTextMetadata, ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField'; } from '@/ui/editable-field/types/ViewField';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { import {
tableColumnsByIdState, tableColumnsByIdState,
tableColumnsState, tableColumnsState,
} from '@/ui/table/states/tableColumnsState'; } from '@/ui/table/states/tableColumnsState';
import { currentViewIdState } from '@/views/states/currentViewIdState'; import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { import {
SortOrder, SortOrder,
useCreateViewFieldsMutation, useCreateViewFieldsMutation,
@ -45,7 +47,10 @@ export const useTableViewFields = ({
objectName: 'company' | 'person'; objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[]; viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => { }) => {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const setColumns = useSetRecoilState(tableColumnsState); const setColumns = useSetRecoilState(tableColumnsState);
const columnsById = useRecoilValue(tableColumnsByIdState); const columnsById = useRecoilValue(tableColumnsByIdState);

View File

@ -0,0 +1,109 @@
import { useCallback } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import {
type TableView,
tableViewsByIdState,
tableViewsState,
} from '@/ui/table/states/tableViewsState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewsMutation,
useGetViewsQuery,
useUpdateViewMutation,
ViewType,
} from '~/generated/graphql';
import { GET_VIEWS } from '../graphql/queries/getViews';
export const useTableViews = ({
objectId,
}: {
objectId: 'company' | 'person';
}) => {
const [, setViews] = useRecoilScopedState(
tableViewsState,
TableRecoilScopeContext,
);
const viewsById = useRecoilScopedValue(
tableViewsByIdState,
TableRecoilScopeContext,
);
const [createViewsMutation] = useCreateViewsMutation();
const [updateViewMutation] = useUpdateViewMutation();
const createViews = useCallback(
(views: TableView[]) => {
if (!views.length) return;
return createViewsMutation({
variables: {
data: views.map((view) => ({
...view,
objectId,
type: ViewType.Table,
})),
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
},
[createViewsMutation, objectId],
);
const updateViewFields = useCallback(
(views: TableView[]) => {
if (!views.length) return;
return Promise.all(
views.map((view) =>
updateViewMutation({
variables: {
data: { name: view.name },
where: { id: view.id },
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
}),
),
);
},
[updateViewMutation],
);
useGetViewsQuery({
variables: {
where: {
objectId: { equals: objectId },
},
},
onCompleted: (data) => {
setViews(
data.views.map((view) => ({
id: view.id,
name: view.name,
})),
);
},
});
const handleViewsChange = useCallback(
async (nextViews: TableView[]) => {
const viewsToCreate = nextViews.filter(
(nextView) => !viewsById[nextView.id],
);
await createViews(viewsToCreate);
const viewsToUpdate = nextViews.filter(
(nextView) =>
viewsById[nextView.id] &&
viewsById[nextView.id].name !== nextView.name,
);
await updateViewFields(viewsToUpdate);
},
[createViews, updateViewFields, viewsById],
);
return { handleViewsChange };
};

View File

@ -1,6 +1,5 @@
import { Context, useCallback } from 'react'; import { useCallback, useEffect } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { import {
sortsByKeyScopedState, sortsByKeyScopedState,
@ -10,9 +9,10 @@ import type {
SelectedSortType, SelectedSortType,
SortType, SortType,
} from '@/ui/filter-n-sort/types/interface'; } from '@/ui/filter-n-sort/types/interface';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
useCreateViewSortsMutation, useCreateViewSortsMutation,
useDeleteViewSortsMutation, useDeleteViewSortsMutation,
@ -25,14 +25,25 @@ import { GET_VIEW_SORTS } from '../graphql/queries/getViewSorts';
export const useViewSorts = <SortField>({ export const useViewSorts = <SortField>({
availableSorts, availableSorts,
Context,
}: { }: {
availableSorts: SortType<SortField>[]; availableSorts: SortType<SortField>[];
Context?: Context<string | null>;
}) => { }) => {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilScopedValue(
const [, setSorts] = useRecoilScopedState(sortScopedState, Context); currentTableViewIdState,
const sortsByKey = useRecoilScopedValue(sortsByKeyScopedState, Context); TableRecoilScopeContext,
);
const [, setSorts] = useRecoilScopedState(
sortScopedState,
TableRecoilScopeContext,
);
const sortsByKey = useRecoilScopedValue(
sortsByKeyScopedState,
TableRecoilScopeContext,
);
useEffect(() => {
if (!currentViewId) setSorts([]);
}, [currentViewId, setSorts]);
useGetViewSortsQuery({ useGetViewSortsQuery({
skip: !currentViewId, skip: !currentViewId,
@ -44,11 +55,19 @@ export const useViewSorts = <SortField>({
onCompleted: (data) => { onCompleted: (data) => {
setSorts( setSorts(
data.viewSorts data.viewSorts
.map((viewSort) => ({ .map((viewSort) => {
...availableSorts.find((sort) => sort.key === viewSort.key), const availableSort = availableSorts.find(
label: viewSort.name, (sort) => sort.key === viewSort.key,
order: viewSort.direction.toLowerCase(), );
}))
return availableSort
? {
...availableSort,
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}
: undefined;
})
.filter((sort): sort is SelectedSortType<SortField> => !!sort), .filter((sort): sort is SelectedSortType<SortField> => !!sort),
); );
}, },

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const currentViewIdState = atom<string | undefined>({
key: 'currentViewIdState',
default: undefined,
});

View File

@ -68,7 +68,10 @@ export function Companies() {
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />} icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
onAddButtonClick={handleAddButtonClick} onAddButtonClick={handleAddButtonClick}
> >
<RecoilScope SpecificContext={TableRecoilScopeContext}> <RecoilScope
scopeId="companies"
SpecificContext={TableRecoilScopeContext}
>
<StyledTableContainer> <StyledTableContainer>
<CompanyTable /> <CompanyTable />
</StyledTableContainer> </StyledTableContainer>

View File

@ -56,7 +56,7 @@ export function People() {
const theme = useTheme(); const theme = useTheme();
return ( return (
<RecoilScope SpecificContext={TableRecoilScopeContext}> <RecoilScope scopeId="people" SpecificContext={TableRecoilScopeContext}>
<WithTopBarContainer <WithTopBarContainer
title="People" title="People"
icon={<IconUser size={theme.icon.size.sm} />} icon={<IconUser size={theme.icon.size.sm} />}

View File

@ -110,6 +110,11 @@ import {
UpdateViewSortAbilityHandler, UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler, DeleteViewSortAbilityHandler,
} from './handlers/view-sort.ability-handler'; } from './handlers/view-sort.ability-handler';
import {
CreateViewAbilityHandler,
ReadViewAbilityHandler,
UpdateViewAbilityHandler,
} from './handlers/view.ability-handler';
@Global() @Global()
@Module({ @Module({
@ -194,14 +199,18 @@ import {
CreatePipelineProgressAbilityHandler, CreatePipelineProgressAbilityHandler,
UpdatePipelineProgressAbilityHandler, UpdatePipelineProgressAbilityHandler,
DeletePipelineProgressAbilityHandler, DeletePipelineProgressAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
//Favorite //Favorite
ReadFavoriteAbilityHandler, ReadFavoriteAbilityHandler,
CreateFavoriteAbilityHandler, CreateFavoriteAbilityHandler,
DeleteFavoriteAbilityHandler, DeleteFavoriteAbilityHandler,
// View
ReadViewAbilityHandler,
CreateViewAbilityHandler,
UpdateViewAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewSort // ViewSort
ReadViewSortAbilityHandler, ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler, CreateViewSortAbilityHandler,
@ -288,14 +297,18 @@ import {
CreatePipelineProgressAbilityHandler, CreatePipelineProgressAbilityHandler,
UpdatePipelineProgressAbilityHandler, UpdatePipelineProgressAbilityHandler,
DeletePipelineProgressAbilityHandler, DeletePipelineProgressAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
//Favorite //Favorite
ReadFavoriteAbilityHandler, ReadFavoriteAbilityHandler,
CreateFavoriteAbilityHandler, CreateFavoriteAbilityHandler,
DeleteFavoriteAbilityHandler, DeleteFavoriteAbilityHandler,
// View
ReadViewAbilityHandler,
CreateViewAbilityHandler,
UpdateViewAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewSort // ViewSort
ReadViewSortAbilityHandler, ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler, CreateViewSortAbilityHandler,

View File

@ -0,0 +1,87 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client';
import { AppAbility } from 'src/ability/ability.factory';
import {
CreateViewAbilityHandler,
ReadViewAbilityHandler,
UpdateViewAbilityHandler,
} from 'src/ability/handlers/view.ability-handler';
import { FindManyViewArgs } from 'src/core/@generated/view/find-many-view.args';
import { View } from 'src/core/@generated/view/view.model';
import { ViewService } from 'src/core/view/services/view.service';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UpdateOneViewArgs } from 'src/core/@generated/view/update-one-view.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { CreateManyViewArgs } from 'src/core/@generated/view/create-many-view.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => View)
export class ViewResolver {
constructor(private readonly viewService: ViewService) {}
@Mutation(() => AffectedRows)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewAbilityHandler)
async createManyView(
@Args() args: CreateManyViewArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<AffectedRows> {
return this.viewService.createMany({
data: args.data.map((data) => ({
...data,
workspaceId: workspace.id,
})),
});
}
@Query(() => [View])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadViewAbilityHandler)
async findManyView(
@Args() args: FindManyViewArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'View' })
prismaSelect: PrismaSelect<'View'>,
): Promise<Partial<View>[]> {
return this.viewService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).View],
}
: accessibleBy(ability).View,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => View)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateViewAbilityHandler)
async updateOneView(
@Args() args: UpdateOneViewArgs,
@PrismaSelector({ modelName: 'View' })
prismaSelect: PrismaSelect<'View'>,
): Promise<Partial<View>> {
return this.viewService.update({
data: args.data,
where: args.where,
select: prismaSelect.value,
} as Prisma.ViewUpdateArgs);
}
}

View File

@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ViewService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.view.findFirst;
findFirstOrThrow = this.prismaService.client.view.findFirstOrThrow;
findUnique = this.prismaService.client.view.findUnique;
findUniqueOrThrow = this.prismaService.client.view.findUniqueOrThrow;
findMany = this.prismaService.client.view.findMany;
// Create
create = this.prismaService.client.view.create;
createMany = this.prismaService.client.view.createMany;
// Update
update = this.prismaService.client.view.update;
upsert = this.prismaService.client.view.upsert;
updateMany = this.prismaService.client.view.updateMany;
// Delete
delete = this.prismaService.client.view.delete;
deleteMany = this.prismaService.client.view.deleteMany;
// Aggregate
aggregate = this.prismaService.client.view.aggregate;
// Count
count = this.prismaService.client.view.count;
// GroupBy
groupBy = this.prismaService.client.view.groupBy;
}

View File

@ -4,11 +4,15 @@ import { ViewFieldService } from './services/view-field.service';
import { ViewFieldResolver } from './resolvers/view-field.resolver'; import { ViewFieldResolver } from './resolvers/view-field.resolver';
import { ViewSortService } from './services/view-sort.service'; import { ViewSortService } from './services/view-sort.service';
import { ViewSortResolver } from './resolvers/view-sort.resolver'; import { ViewSortResolver } from './resolvers/view-sort.resolver';
import { ViewService } from './services/view.service';
import { ViewResolver } from './resolvers/view.resolver';
@Module({ @Module({
providers: [ providers: [
ViewService,
ViewFieldService, ViewFieldService,
ViewSortService, ViewSortService,
ViewResolver,
ViewFieldResolver, ViewFieldResolver,
ViewSortResolver, ViewSortResolver,
], ],