feat: add board options dropdown and pipeline stage creation (#1399)
* feat: add board options dropdown and pipeline stage creation Closes #1395 * refactor: code review - remove useCallback
This commit is contained in:
@ -1004,6 +1004,7 @@ export type Mutation = {
|
|||||||
createOneCompany: Company;
|
createOneCompany: Company;
|
||||||
createOnePerson: Person;
|
createOnePerson: Person;
|
||||||
createOnePipelineProgress: PipelineProgress;
|
createOnePipelineProgress: PipelineProgress;
|
||||||
|
createOnePipelineStage: PipelineStage;
|
||||||
createOneView: View;
|
createOneView: View;
|
||||||
createOneViewField: ViewField;
|
createOneViewField: ViewField;
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
@ -1130,6 +1131,11 @@ export type MutationCreateOnePipelineProgressArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOnePipelineStageArgs = {
|
||||||
|
data: PipelineStageCreateInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationCreateOneViewArgs = {
|
export type MutationCreateOneViewArgs = {
|
||||||
data: ViewCreateInput;
|
data: ViewCreateInput;
|
||||||
};
|
};
|
||||||
@ -1654,6 +1660,10 @@ export type PipelineCreateNestedOneWithoutPipelineProgressesInput = {
|
|||||||
connect?: InputMaybe<PipelineWhereUniqueInput>;
|
connect?: InputMaybe<PipelineWhereUniqueInput>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PipelineCreateNestedOneWithoutPipelineStagesInput = {
|
||||||
|
connect?: InputMaybe<PipelineWhereUniqueInput>;
|
||||||
|
};
|
||||||
|
|
||||||
export type PipelineOrderByWithRelationInput = {
|
export type PipelineOrderByWithRelationInput = {
|
||||||
createdAt?: InputMaybe<SortOrder>;
|
createdAt?: InputMaybe<SortOrder>;
|
||||||
icon?: InputMaybe<SortOrder>;
|
icon?: InputMaybe<SortOrder>;
|
||||||
@ -1707,6 +1717,10 @@ export type PipelineProgressCreateNestedManyWithoutPersonInput = {
|
|||||||
connect?: InputMaybe<Array<PipelineProgressWhereUniqueInput>>;
|
connect?: InputMaybe<Array<PipelineProgressWhereUniqueInput>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PipelineProgressCreateNestedManyWithoutPipelineStageInput = {
|
||||||
|
connect?: InputMaybe<Array<PipelineProgressWhereUniqueInput>>;
|
||||||
|
};
|
||||||
|
|
||||||
export type PipelineProgressCreateNestedManyWithoutPointOfContactInput = {
|
export type PipelineProgressCreateNestedManyWithoutPointOfContactInput = {
|
||||||
connect?: InputMaybe<Array<PipelineProgressWhereUniqueInput>>;
|
connect?: InputMaybe<Array<PipelineProgressWhereUniqueInput>>;
|
||||||
};
|
};
|
||||||
@ -1861,6 +1875,18 @@ export type PipelineStage = {
|
|||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PipelineStageCreateInput = {
|
||||||
|
color: Scalars['String'];
|
||||||
|
createdAt?: InputMaybe<Scalars['DateTime']>;
|
||||||
|
id?: InputMaybe<Scalars['String']>;
|
||||||
|
index?: InputMaybe<Scalars['Int']>;
|
||||||
|
name: Scalars['String'];
|
||||||
|
pipeline: PipelineCreateNestedOneWithoutPipelineStagesInput;
|
||||||
|
pipelineProgresses?: InputMaybe<PipelineProgressCreateNestedManyWithoutPipelineStageInput>;
|
||||||
|
type: Scalars['String'];
|
||||||
|
updatedAt?: InputMaybe<Scalars['DateTime']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type PipelineStageCreateNestedOneWithoutPipelineProgressesInput = {
|
export type PipelineStageCreateNestedOneWithoutPipelineProgressesInput = {
|
||||||
connect?: InputMaybe<PipelineStageWhereUniqueInput>;
|
connect?: InputMaybe<PipelineStageWhereUniqueInput>;
|
||||||
};
|
};
|
||||||
@ -3285,6 +3311,13 @@ export type CreateOneCompanyPipelineProgressMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type CreateOneCompanyPipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } };
|
export type CreateOneCompanyPipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } };
|
||||||
|
|
||||||
|
export type CreatePipelineStageMutationVariables = Exact<{
|
||||||
|
data: PipelineStageCreateInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreatePipelineStageMutation = { __typename?: 'Mutation', pipelineStage: { __typename?: 'PipelineStage', id: string, name: string, color: string } };
|
||||||
|
|
||||||
export type DeleteManyPipelineProgressMutationVariables = Exact<{
|
export type DeleteManyPipelineProgressMutationVariables = Exact<{
|
||||||
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||||
}>;
|
}>;
|
||||||
@ -5453,6 +5486,41 @@ export function useCreateOneCompanyPipelineProgressMutation(baseOptions?: Apollo
|
|||||||
export type CreateOneCompanyPipelineProgressMutationHookResult = ReturnType<typeof useCreateOneCompanyPipelineProgressMutation>;
|
export type CreateOneCompanyPipelineProgressMutationHookResult = ReturnType<typeof useCreateOneCompanyPipelineProgressMutation>;
|
||||||
export type CreateOneCompanyPipelineProgressMutationResult = Apollo.MutationResult<CreateOneCompanyPipelineProgressMutation>;
|
export type CreateOneCompanyPipelineProgressMutationResult = Apollo.MutationResult<CreateOneCompanyPipelineProgressMutation>;
|
||||||
export type CreateOneCompanyPipelineProgressMutationOptions = Apollo.BaseMutationOptions<CreateOneCompanyPipelineProgressMutation, CreateOneCompanyPipelineProgressMutationVariables>;
|
export type CreateOneCompanyPipelineProgressMutationOptions = Apollo.BaseMutationOptions<CreateOneCompanyPipelineProgressMutation, CreateOneCompanyPipelineProgressMutationVariables>;
|
||||||
|
export const CreatePipelineStageDocument = gql`
|
||||||
|
mutation CreatePipelineStage($data: PipelineStageCreateInput!) {
|
||||||
|
pipelineStage: createOnePipelineStage(data: $data) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreatePipelineStageMutationFn = Apollo.MutationFunction<CreatePipelineStageMutation, CreatePipelineStageMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreatePipelineStageMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreatePipelineStageMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreatePipelineStageMutation` 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 [createPipelineStageMutation, { data, loading, error }] = useCreatePipelineStageMutation({
|
||||||
|
* variables: {
|
||||||
|
* data: // value for 'data'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreatePipelineStageMutation(baseOptions?: Apollo.MutationHookOptions<CreatePipelineStageMutation, CreatePipelineStageMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreatePipelineStageMutation, CreatePipelineStageMutationVariables>(CreatePipelineStageDocument, options);
|
||||||
|
}
|
||||||
|
export type CreatePipelineStageMutationHookResult = ReturnType<typeof useCreatePipelineStageMutation>;
|
||||||
|
export type CreatePipelineStageMutationResult = Apollo.MutationResult<CreatePipelineStageMutation>;
|
||||||
|
export type CreatePipelineStageMutationOptions = Apollo.BaseMutationOptions<CreatePipelineStageMutation, CreatePipelineStageMutationVariables>;
|
||||||
export const DeleteManyPipelineProgressDocument = gql`
|
export const DeleteManyPipelineProgressDocument = gql`
|
||||||
mutation DeleteManyPipelineProgress($ids: [String!]) {
|
mutation DeleteManyPipelineProgress($ids: [String!]) {
|
||||||
deleteManyPipelineProgress(where: {id: {in: $ids}}) {
|
deleteManyPipelineProgress(where: {id: {in: $ids}}) {
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CREATE_PIPELINE_STAGE = gql`
|
||||||
|
mutation CreatePipelineStage($data: PipelineStageCreateInput!) {
|
||||||
|
pipelineStage: createOnePipelineStage(data: $data) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
34
front/src/modules/pipeline/hooks/usePipelineStages.ts
Normal file
34
front/src/modules/pipeline/hooks/usePipelineStages.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import type { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
|
||||||
|
import { useCreatePipelineStageMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { GET_PIPELINES } from '../graphql/queries/getPipelines';
|
||||||
|
import { currentPipelineState } from '../states/currentPipelineState';
|
||||||
|
|
||||||
|
export const usePipelineStages = () => {
|
||||||
|
const currentPipeline = useRecoilValue(currentPipelineState);
|
||||||
|
|
||||||
|
const [createPipelineStageMutation] = useCreatePipelineStageMutation();
|
||||||
|
|
||||||
|
const handlePipelineStageAdd = async (boardColumn: BoardColumnDefinition) => {
|
||||||
|
if (!currentPipeline?.id) return;
|
||||||
|
|
||||||
|
return createPipelineStageMutation({
|
||||||
|
variables: {
|
||||||
|
data: {
|
||||||
|
color: boardColumn.colorCode,
|
||||||
|
id: boardColumn.id,
|
||||||
|
index: boardColumn.index,
|
||||||
|
name: boardColumn.title,
|
||||||
|
pipeline: { connect: { id: currentPipeline.id } },
|
||||||
|
type: 'ongoing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handlePipelineStageAdd };
|
||||||
|
};
|
||||||
@ -1,18 +1,26 @@
|
|||||||
import { Context, ReactNode, useCallback, useState } from 'react';
|
import { Context, ReactNode, useCallback, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||||
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||||
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
import { TopBar } from '@/ui/top-bar/TopBar';
|
||||||
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
|
|
||||||
|
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
|
import { BoardOptionsHotkeyScope } from '../types/BoardOptionsHotkeyScope';
|
||||||
|
|
||||||
|
import { BoardOptionsDropdown } from './BoardOptionsDropdown';
|
||||||
|
|
||||||
type OwnProps<SortField> = {
|
type OwnProps<SortField> = {
|
||||||
viewName: string;
|
viewName: string;
|
||||||
viewIcon?: ReactNode;
|
viewIcon?: ReactNode;
|
||||||
availableSorts?: Array<SortType<SortField>>;
|
availableSorts?: Array<SortType<SortField>>;
|
||||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||||
|
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
context: Context<string | null>;
|
context: Context<string | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,6 +39,7 @@ export function BoardHeader<SortField>({
|
|||||||
viewIcon,
|
viewIcon,
|
||||||
availableSorts,
|
availableSorts,
|
||||||
onSortsUpdate,
|
onSortsUpdate,
|
||||||
|
onStageAdd,
|
||||||
context,
|
context,
|
||||||
}: OwnProps<SortField>) {
|
}: OwnProps<SortField>) {
|
||||||
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
|
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
|
||||||
@ -56,41 +65,47 @@ export function BoardHeader<SortField>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopBar
|
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
|
||||||
displayBottomBorder={false}
|
<TopBar
|
||||||
leftComponent={
|
displayBottomBorder={false}
|
||||||
<>
|
leftComponent={
|
||||||
<StyledIcon>{viewIcon}</StyledIcon>
|
<>
|
||||||
{viewName}
|
<StyledIcon>{viewIcon}</StyledIcon>
|
||||||
</>
|
{viewName}
|
||||||
}
|
</>
|
||||||
rightComponent={
|
}
|
||||||
<>
|
rightComponent={
|
||||||
<FilterDropdownButton
|
<>
|
||||||
|
<FilterDropdownButton
|
||||||
|
context={context}
|
||||||
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
|
/>
|
||||||
|
<SortDropdownButton<SortField>
|
||||||
|
context={context}
|
||||||
|
isSortSelected={sorts.length > 0}
|
||||||
|
availableSorts={availableSorts || []}
|
||||||
|
onSortSelect={sortSelect}
|
||||||
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
|
/>
|
||||||
|
<BoardOptionsDropdown
|
||||||
|
customHotkeyScope={{ scope: BoardOptionsHotkeyScope.Dropdown }}
|
||||||
|
onStageAdd={onStageAdd}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
bottomComponent={
|
||||||
|
<SortAndFilterBar
|
||||||
context={context}
|
context={context}
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
sorts={sorts}
|
||||||
|
onRemoveSort={sortUnselect}
|
||||||
|
onCancelClick={() => {
|
||||||
|
innerSetSorts([]);
|
||||||
|
onSortsUpdate?.([]);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SortDropdownButton<SortField>
|
}
|
||||||
context={context}
|
/>
|
||||||
isSortSelected={sorts.length > 0}
|
</RecoilScope>
|
||||||
availableSorts={availableSorts || []}
|
|
||||||
onSortSelect={sortSelect}
|
|
||||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
bottomComponent={
|
|
||||||
<SortAndFilterBar
|
|
||||||
context={context}
|
|
||||||
sorts={sorts}
|
|
||||||
onRemoveSort={sortUnselect}
|
|
||||||
onCancelClick={() => {
|
|
||||||
innerSetSorts([]);
|
|
||||||
onSortsUpdate && onSortsUpdate([]);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,32 @@
|
|||||||
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
||||||
|
import type { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
|
||||||
|
import { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
|
|
||||||
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
||||||
import { BoardOptionsDropdownContent } from './BoardOptionsDropdownContent';
|
import { BoardOptionsDropdownContent } from './BoardOptionsDropdownContent';
|
||||||
|
|
||||||
export function BoardOptionsDropdown() {
|
type BoardOptionsDropdownProps = {
|
||||||
|
customHotkeyScope: HotkeyScope;
|
||||||
|
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BoardOptionsDropdown({
|
||||||
|
customHotkeyScope,
|
||||||
|
onStageAdd,
|
||||||
|
}: BoardOptionsDropdownProps) {
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
dropdownKey="options"
|
|
||||||
buttonComponents={<BoardOptionsDropdownButton />}
|
buttonComponents={<BoardOptionsDropdownButton />}
|
||||||
dropdownComponents={<BoardOptionsDropdownContent />}
|
dropdownComponents={
|
||||||
></DropdownButton>
|
<BoardOptionsDropdownContent
|
||||||
|
customHotkeyScope={customHotkeyScope}
|
||||||
|
onStageAdd={onStageAdd}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={customHotkeyScope}
|
||||||
|
dropdownKey={BoardOptionsDropdownKey}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
|
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
|
||||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||||
|
|
||||||
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
|
|
||||||
export function BoardOptionsDropdownButton() {
|
export function BoardOptionsDropdownButton() {
|
||||||
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
|
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
|
||||||
key: 'options',
|
key: BoardOptionsDropdownKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
|
|||||||
@ -1,54 +1,153 @@
|
|||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
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 { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
||||||
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
||||||
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
|
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
|
||||||
import { IconChevronLeft } from '@/ui/icon';
|
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||||
|
import {
|
||||||
|
IconChevronLeft,
|
||||||
|
IconChevronRight,
|
||||||
|
IconLayoutKanban,
|
||||||
|
IconPlus,
|
||||||
|
IconSettings,
|
||||||
|
} from '@/ui/icon';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
|
||||||
type BoardOptionsDropdownMenu = 'options' | 'fields';
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
|
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
|
import { BoardOptionsDropdownKey } from '../types/BoardOptionsDropdownKey';
|
||||||
|
|
||||||
export function BoardOptionsDropdownContent() {
|
type BoardOptionsDropdownContentProps = {
|
||||||
|
customHotkeyScope: HotkeyScope;
|
||||||
|
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledIconSettings = styled(IconSettings)`
|
||||||
|
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
enum BoardOptionsMenu {
|
||||||
|
StageCreation = 'StageCreation',
|
||||||
|
Stages = 'Stages',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoardOptionsDropdownContent({
|
||||||
|
customHotkeyScope,
|
||||||
|
onStageAdd,
|
||||||
|
}: BoardOptionsDropdownContentProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [menuShown, setMenuShown] =
|
const stageInputRef = useRef<HTMLInputElement>(null);
|
||||||
useState<BoardOptionsDropdownMenu>('options');
|
|
||||||
|
|
||||||
function handleFieldsClick() {
|
const [currentMenu, setCurrentMenu] = useState<
|
||||||
setMenuShown('fields');
|
BoardOptionsMenu | undefined
|
||||||
}
|
>();
|
||||||
|
|
||||||
function handleMenuHeaderClick() {
|
const [boardColumns, setBoardColumns] = useRecoilState(boardColumnsState);
|
||||||
setMenuShown('options');
|
|
||||||
}
|
const resetMenu = () => setCurrentMenu(undefined);
|
||||||
|
|
||||||
|
const handleStageSubmit = () => {
|
||||||
|
if (
|
||||||
|
currentMenu !== BoardOptionsMenu.StageCreation ||
|
||||||
|
!stageInputRef?.current?.value
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const columnToCreate = {
|
||||||
|
id: v4(),
|
||||||
|
colorCode: 'gray',
|
||||||
|
index: boardColumns.length,
|
||||||
|
title: stageInputRef.current.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
setBoardColumns((previousBoardColumns) => [
|
||||||
|
...previousBoardColumns,
|
||||||
|
columnToCreate,
|
||||||
|
]);
|
||||||
|
onStageAdd?.(columnToCreate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { closeDropdownButton } = useDropdownButton({
|
||||||
|
key: BoardOptionsDropdownKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.Escape,
|
||||||
|
() => {
|
||||||
|
closeDropdownButton();
|
||||||
|
},
|
||||||
|
customHotkeyScope.scope,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
Key.Enter,
|
||||||
|
() => {
|
||||||
|
handleStageSubmit();
|
||||||
|
closeDropdownButton();
|
||||||
|
},
|
||||||
|
customHotkeyScope.scope,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDropdownMenu>
|
<StyledDropdownMenu>
|
||||||
{menuShown === 'options' ? (
|
{!currentMenu && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader>Options</DropdownMenuHeader>
|
<DropdownMenuHeader>
|
||||||
|
<StyledIconSettings size={theme.icon.size.md} />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuHeader>
|
||||||
<StyledDropdownMenuSeparator />
|
<StyledDropdownMenuSeparator />
|
||||||
<StyledDropdownMenuItemsContainer>
|
<StyledDropdownMenuItemsContainer>
|
||||||
<DropdownMenuItem onClick={handleFieldsClick}>
|
<DropdownMenuItem
|
||||||
Fields
|
onClick={() => setCurrentMenu(BoardOptionsMenu.Stages)}
|
||||||
|
>
|
||||||
|
<IconLayoutKanban size={theme.icon.size.md} />
|
||||||
|
Stages
|
||||||
|
<StyledIconChevronRight size={theme.icon.size.sm} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</StyledDropdownMenuItemsContainer>
|
</StyledDropdownMenuItemsContainer>
|
||||||
</>
|
</>
|
||||||
) : (
|
)}
|
||||||
menuShown === 'fields' && (
|
{currentMenu === BoardOptionsMenu.Stages && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuHeader
|
<DropdownMenuHeader
|
||||||
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
|
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
|
||||||
onClick={handleMenuHeaderClick}
|
onClick={resetMenu}
|
||||||
|
>
|
||||||
|
Stages
|
||||||
|
</DropdownMenuHeader>
|
||||||
|
<StyledDropdownMenuSeparator />
|
||||||
|
<StyledDropdownMenuItemsContainer>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => setCurrentMenu(BoardOptionsMenu.StageCreation)}
|
||||||
>
|
>
|
||||||
Fields
|
<IconPlus size={theme.icon.size.md} />
|
||||||
</DropdownMenuHeader>
|
Add stage
|
||||||
<StyledDropdownMenuSeparator />
|
</DropdownMenuItem>
|
||||||
{}
|
</StyledDropdownMenuItemsContainer>
|
||||||
</>
|
</>
|
||||||
)
|
)}
|
||||||
|
{currentMenu === BoardOptionsMenu.StageCreation && (
|
||||||
|
<DropdownMenuInput
|
||||||
|
autoFocus
|
||||||
|
placeholder="New stage"
|
||||||
|
ref={stageInputRef}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledDropdownMenu>
|
</StyledDropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { useSetCardSelected } from '../hooks/useSetCardSelected';
|
|||||||
import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds';
|
import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds';
|
||||||
import { boardColumnsState } from '../states/boardColumnsState';
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
import { BoardColumnRecoilScopeContext } from '../states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
|
import { BoardColumnRecoilScopeContext } from '../states/recoil-scope-contexts/BoardColumnRecoilScopeContext';
|
||||||
|
import type { BoardColumnDefinition } from '../types/BoardColumnDefinition';
|
||||||
import { BoardOptions } from '../types/BoardOptions';
|
import { BoardOptions } from '../types/BoardOptions';
|
||||||
|
|
||||||
import { EntityBoardColumn } from './EntityBoardColumn';
|
import { EntityBoardColumn } from './EntityBoardColumn';
|
||||||
@ -44,12 +45,14 @@ export function EntityBoard({
|
|||||||
boardOptions,
|
boardOptions,
|
||||||
updateSorts,
|
updateSorts,
|
||||||
onEditColumnTitle,
|
onEditColumnTitle,
|
||||||
|
onStageAdd,
|
||||||
}: {
|
}: {
|
||||||
boardOptions: BoardOptions;
|
boardOptions: BoardOptions;
|
||||||
updateSorts: (
|
updateSorts: (
|
||||||
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
|
sorts: Array<SelectedSortType<PipelineProgressOrderByWithRelationInput>>,
|
||||||
) => void;
|
) => void;
|
||||||
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
|
onEditColumnTitle: (columnId: string, title: string, color: string) => void;
|
||||||
|
onStageAdd?: (boardColumn: BoardColumnDefinition) => void;
|
||||||
}) {
|
}) {
|
||||||
const [boardColumns] = useRecoilState(boardColumnsState);
|
const [boardColumns] = useRecoilState(boardColumnsState);
|
||||||
const setCardSelected = useSetCardSelected();
|
const setCardSelected = useSetCardSelected();
|
||||||
@ -130,6 +133,7 @@ export function EntityBoard({
|
|||||||
viewIcon={<IconList size={theme.icon.size.md} />}
|
viewIcon={<IconList size={theme.icon.size.md} />}
|
||||||
availableSorts={boardOptions.sorts}
|
availableSorts={boardOptions.sorts}
|
||||||
onSortsUpdate={updateSorts}
|
onSortsUpdate={updateSorts}
|
||||||
|
onStageAdd={onStageAdd}
|
||||||
context={CompanyBoardRecoilScopeContext}
|
context={CompanyBoardRecoilScopeContext}
|
||||||
/>
|
/>
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const BoardOptionsDropdownKey = 'board-options';
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export enum BoardOptionsHotkeyScope {
|
||||||
|
Dropdown = 'board-options-dropdown',
|
||||||
|
}
|
||||||
@ -41,6 +41,7 @@ export {
|
|||||||
IconHeart,
|
IconHeart,
|
||||||
IconHelpCircle,
|
IconHelpCircle,
|
||||||
IconInbox,
|
IconInbox,
|
||||||
|
IconLayoutKanban,
|
||||||
IconLayoutSidebarLeftCollapse,
|
IconLayoutSidebarLeftCollapse,
|
||||||
IconLayoutSidebarRightCollapse,
|
IconLayoutSidebarRightCollapse,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useTheme } from '@emotion/react';
|
|||||||
import { HooksCompanyBoard } from '@/companies/components/HooksCompanyBoard';
|
import { HooksCompanyBoard } from '@/companies/components/HooksCompanyBoard';
|
||||||
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext';
|
||||||
import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton';
|
import { PipelineAddButton } from '@/pipeline/components/PipelineAddButton';
|
||||||
|
import { usePipelineStages } from '@/pipeline/hooks/usePipelineStages';
|
||||||
import { EntityBoard } from '@/ui/board/components/EntityBoard';
|
import { EntityBoard } from '@/ui/board/components/EntityBoard';
|
||||||
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
|
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
|
||||||
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
|
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
|
||||||
@ -43,6 +44,8 @@ export function Opportunities() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { handlePipelineStageAdd } = usePipelineStages();
|
||||||
|
|
||||||
const [updatePipelineStage] = useUpdatePipelineStageMutation();
|
const [updatePipelineStage] = useUpdatePipelineStageMutation();
|
||||||
|
|
||||||
function handleEditColumnTitle(
|
function handleEditColumnTitle(
|
||||||
@ -86,6 +89,7 @@ export function Opportunities() {
|
|||||||
boardOptions={opportunitiesBoardOptions}
|
boardOptions={opportunitiesBoardOptions}
|
||||||
updateSorts={updateSorts}
|
updateSorts={updateSorts}
|
||||||
onEditColumnTitle={handleEditColumnTitle}
|
onEditColumnTitle={handleEditColumnTitle}
|
||||||
|
onStageAdd={handlePipelineStageAdd}
|
||||||
/>
|
/>
|
||||||
<EntityBoardActionBar />
|
<EntityBoardActionBar />
|
||||||
<EntityBoardContextMenu />
|
<EntityBoardContextMenu />
|
||||||
|
|||||||
@ -126,6 +126,7 @@ export class AbilityFactory {
|
|||||||
|
|
||||||
// PipelineStage
|
// PipelineStage
|
||||||
can(AbilityAction.Read, 'PipelineStage', { workspaceId: workspace.id });
|
can(AbilityAction.Read, 'PipelineStage', { workspaceId: workspace.id });
|
||||||
|
can(AbilityAction.Create, 'PipelineStage', { workspaceId: workspace.id });
|
||||||
can(AbilityAction.Update, 'PipelineStage', { workspaceId: workspace.id });
|
can(AbilityAction.Update, 'PipelineStage', { workspaceId: workspace.id });
|
||||||
|
|
||||||
// PipelineProgress
|
// PipelineProgress
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Resolver, Args, Query, Mutation } from '@nestjs/graphql';
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
|
||||||
import { accessibleBy } from '@casl/prisma';
|
import { accessibleBy } from '@casl/prisma';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma, Workspace } from '@prisma/client';
|
||||||
|
|
||||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
import { PipelineStage } from 'src/core/@generated/pipeline-stage/pipeline-stage.model';
|
import { PipelineStage } from 'src/core/@generated/pipeline-stage/pipeline-stage.model';
|
||||||
@ -11,6 +11,7 @@ import { PipelineStageService } from 'src/core/pipeline/services/pipeline-stage.
|
|||||||
import { AbilityGuard } from 'src/guards/ability.guard';
|
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||||
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||||
import {
|
import {
|
||||||
|
CreatePipelineStageAbilityHandler,
|
||||||
ReadPipelineStageAbilityHandler,
|
ReadPipelineStageAbilityHandler,
|
||||||
UpdatePipelineStageAbilityHandler,
|
UpdatePipelineStageAbilityHandler,
|
||||||
} from 'src/ability/handlers/pipeline-stage.ability-handler';
|
} from 'src/ability/handlers/pipeline-stage.ability-handler';
|
||||||
@ -21,12 +22,34 @@ import {
|
|||||||
PrismaSelect,
|
PrismaSelect,
|
||||||
} from 'src/decorators/prisma-select.decorator';
|
} from 'src/decorators/prisma-select.decorator';
|
||||||
import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args';
|
import { UpdateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/update-one-pipeline-stage.args';
|
||||||
|
import { CreateOnePipelineStageArgs } from 'src/core/@generated/pipeline-stage/create-one-pipeline-stage.args';
|
||||||
|
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver(() => PipelineStage)
|
@Resolver(() => PipelineStage)
|
||||||
export class PipelineStageResolver {
|
export class PipelineStageResolver {
|
||||||
constructor(private readonly pipelineStageService: PipelineStageService) {}
|
constructor(private readonly pipelineStageService: PipelineStageService) {}
|
||||||
|
|
||||||
|
@Mutation(() => PipelineStage, {
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
@UseGuards(AbilityGuard)
|
||||||
|
@CheckAbilities(CreatePipelineStageAbilityHandler)
|
||||||
|
async createOnePipelineStage(
|
||||||
|
@Args() args: CreateOnePipelineStageArgs,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
@PrismaSelector({ modelName: 'PipelineStage' })
|
||||||
|
prismaSelect: PrismaSelect<'PipelineStage'>,
|
||||||
|
): Promise<Partial<PipelineStage>> {
|
||||||
|
return this.pipelineStageService.create({
|
||||||
|
data: {
|
||||||
|
...args.data,
|
||||||
|
workspace: { connect: { id: workspace.id } },
|
||||||
|
},
|
||||||
|
select: prismaSelect.value,
|
||||||
|
} as Prisma.PipelineStageCreateArgs);
|
||||||
|
}
|
||||||
|
|
||||||
@Query(() => [PipelineStage])
|
@Query(() => [PipelineStage])
|
||||||
@UseGuards(AbilityGuard)
|
@UseGuards(AbilityGuard)
|
||||||
@CheckAbilities(ReadPipelineStageAbilityHandler)
|
@CheckAbilities(ReadPipelineStageAbilityHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user