Enable opportunity card deletion (#490)
* Add checkbox * Add state management for selected opportunities * Use recoil for selected items state, show action bar * Deduplicate code * Add delete action * Enable delete * Add color for selected cards * update board state on delete * Add stories * Enable empty board * Fix story * Handle dark mdoe * Nits * Rename module * Better naming * Fix naming confusion process<>progress
This commit is contained in:
@ -1704,6 +1704,13 @@ export type CreateOnePipelineProgressMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type CreateOnePipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } };
|
export type CreateOnePipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } };
|
||||||
|
|
||||||
|
export type DeleteManyPipelineProgressMutationVariables = Exact<{
|
||||||
|
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DeleteManyPipelineProgressMutation = { __typename?: 'Mutation', deleteManyPipelineProgress: { __typename?: 'AffectedRows', count: number } };
|
||||||
|
|
||||||
export type GetPeopleQueryVariables = Exact<{
|
export type GetPeopleQueryVariables = Exact<{
|
||||||
orderBy?: InputMaybe<Array<PersonOrderByWithRelationInput> | PersonOrderByWithRelationInput>;
|
orderBy?: InputMaybe<Array<PersonOrderByWithRelationInput> | PersonOrderByWithRelationInput>;
|
||||||
where?: InputMaybe<PersonWhereInput>;
|
where?: InputMaybe<PersonWhereInput>;
|
||||||
@ -2600,6 +2607,39 @@ export function useCreateOnePipelineProgressMutation(baseOptions?: Apollo.Mutati
|
|||||||
export type CreateOnePipelineProgressMutationHookResult = ReturnType<typeof useCreateOnePipelineProgressMutation>;
|
export type CreateOnePipelineProgressMutationHookResult = ReturnType<typeof useCreateOnePipelineProgressMutation>;
|
||||||
export type CreateOnePipelineProgressMutationResult = Apollo.MutationResult<CreateOnePipelineProgressMutation>;
|
export type CreateOnePipelineProgressMutationResult = Apollo.MutationResult<CreateOnePipelineProgressMutation>;
|
||||||
export type CreateOnePipelineProgressMutationOptions = Apollo.BaseMutationOptions<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>;
|
export type CreateOnePipelineProgressMutationOptions = Apollo.BaseMutationOptions<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>;
|
||||||
|
export const DeleteManyPipelineProgressDocument = gql`
|
||||||
|
mutation DeleteManyPipelineProgress($ids: [String!]) {
|
||||||
|
deleteManyPipelineProgress(where: {id: {in: $ids}}) {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type DeleteManyPipelineProgressMutationFn = Apollo.MutationFunction<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDeleteManyPipelineProgressMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDeleteManyPipelineProgressMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDeleteManyPipelineProgressMutation` 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 [deleteManyPipelineProgressMutation, { data, loading, error }] = useDeleteManyPipelineProgressMutation({
|
||||||
|
* variables: {
|
||||||
|
* ids: // value for 'ids'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDeleteManyPipelineProgressMutation(baseOptions?: Apollo.MutationHookOptions<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>(DeleteManyPipelineProgressDocument, options);
|
||||||
|
}
|
||||||
|
export type DeleteManyPipelineProgressMutationHookResult = ReturnType<typeof useDeleteManyPipelineProgressMutation>;
|
||||||
|
export type DeleteManyPipelineProgressMutationResult = Apollo.MutationResult<DeleteManyPipelineProgressMutation>;
|
||||||
|
export type DeleteManyPipelineProgressMutationOptions = Apollo.BaseMutationOptions<DeleteManyPipelineProgressMutation, DeleteManyPipelineProgressMutationVariables>;
|
||||||
export const GetPeopleDocument = gql`
|
export const GetPeopleDocument = gql`
|
||||||
query GetPeople($orderBy: [PersonOrderByWithRelationInput!], $where: PersonWhereInput, $limit: Int) {
|
query GetPeople($orderBy: [PersonOrderByWithRelationInput!], $where: PersonWhereInput, $limit: Int) {
|
||||||
people: findManyPerson(orderBy: $orderBy, where: $where, take: $limit) {
|
people: findManyPerson(orderBy: $orderBy, where: $where, take: $limit) {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from '../../ui/components/board/Board';
|
} from '../../ui/components/board/Board';
|
||||||
import { boardColumnsState } from '../states/boardColumnsState';
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
import { boardItemsState } from '../states/boardItemsState';
|
import { boardItemsState } from '../states/boardItemsState';
|
||||||
|
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||||
|
|
||||||
import { CompanyBoardCard } from './CompanyBoardCard';
|
import { CompanyBoardCard } from './CompanyBoardCard';
|
||||||
import { NewButton } from './NewButton';
|
import { NewButton } from './NewButton';
|
||||||
@ -69,19 +70,22 @@ export function Board({
|
|||||||
pipelineId,
|
pipelineId,
|
||||||
}: BoardProps) {
|
}: BoardProps) {
|
||||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||||
const [items, setItems] = useRecoilState(boardItemsState);
|
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||||
|
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||||
|
selectedBoardItemsState,
|
||||||
|
);
|
||||||
const [isInitialBoardLoaded, setIsInitialBoardLoaded] = useState(false);
|
const [isInitialBoardLoaded, setIsInitialBoardLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(initialItems).length === 0 || isInitialBoardLoaded) return;
|
|
||||||
setBoard(initialBoard);
|
setBoard(initialBoard);
|
||||||
setItems(initialItems);
|
if (Object.keys(initialItems).length === 0 || isInitialBoardLoaded) return;
|
||||||
|
setBoardItems(initialItems);
|
||||||
setIsInitialBoardLoaded(true);
|
setIsInitialBoardLoaded(true);
|
||||||
}, [
|
}, [
|
||||||
initialBoard,
|
initialBoard,
|
||||||
setBoard,
|
setBoard,
|
||||||
initialItems,
|
initialItems,
|
||||||
setItems,
|
setBoardItems,
|
||||||
setIsInitialBoardLoaded,
|
setIsInitialBoardLoaded,
|
||||||
isInitialBoardLoaded,
|
isInitialBoardLoaded,
|
||||||
]);
|
]);
|
||||||
@ -105,6 +109,16 @@ export function Board({
|
|||||||
[board, onUpdate, setBoard],
|
[board, onUpdate, setBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleSelect(itemKey: string) {
|
||||||
|
if (selectedBoardItems.includes(itemKey)) {
|
||||||
|
setSelectedBoardItems(
|
||||||
|
selectedBoardItems.filter((key) => key !== itemKey),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedBoardItems([...selectedBoardItems, itemKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return board.length > 0 ? (
|
return board.length > 0 ? (
|
||||||
<StyledBoard>
|
<StyledBoard>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
@ -117,7 +131,7 @@ export function Board({
|
|||||||
>
|
>
|
||||||
{board[columnIndex].itemKeys.map(
|
{board[columnIndex].itemKeys.map(
|
||||||
(itemKey, index) =>
|
(itemKey, index) =>
|
||||||
items[itemKey] && (
|
boardItems[itemKey] && (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={itemKey}
|
key={itemKey}
|
||||||
draggableId={itemKey}
|
draggableId={itemKey}
|
||||||
@ -129,7 +143,11 @@ export function Board({
|
|||||||
{...draggableProvided?.dragHandleProps}
|
{...draggableProvided?.dragHandleProps}
|
||||||
{...draggableProvided?.draggableProps}
|
{...draggableProvided?.draggableProps}
|
||||||
>
|
>
|
||||||
<CompanyBoardCard company={items[itemKey]} />
|
<CompanyBoardCard
|
||||||
|
company={boardItems[itemKey]}
|
||||||
|
selected={selectedBoardItems.includes(itemKey)}
|
||||||
|
onSelect={() => handleSelect(itemKey)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { EntityTableActionBarButton } from '@/ui/components/table/action-bar/EntityTableActionBarButton';
|
||||||
|
import { IconTrash } from '@/ui/icons/index';
|
||||||
|
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { GET_PIPELINES } from '../queries';
|
||||||
|
import { boardItemsState } from '../states/boardItemsState';
|
||||||
|
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||||
|
|
||||||
|
export function BoardActionBarButtonDeletePipelineProgress() {
|
||||||
|
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||||
|
selectedBoardItemsState,
|
||||||
|
);
|
||||||
|
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||||
|
|
||||||
|
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
|
||||||
|
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleDeleteClick() {
|
||||||
|
await deletePipelineProgress({
|
||||||
|
variables: {
|
||||||
|
ids: selectedBoardItems,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('boardItems', boardItems);
|
||||||
|
|
||||||
|
setBoardItems(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(boardItems).filter(
|
||||||
|
([key]) => !selectedBoardItems.includes(key),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setSelectedBoardItems([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityTableActionBarButton
|
||||||
|
label="Delete"
|
||||||
|
icon={<IconTrash size={16} />}
|
||||||
|
type="warning"
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,15 +3,21 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { Company } from '../../../generated/graphql';
|
import { Company } from '../../../generated/graphql';
|
||||||
import { PersonChip } from '../../people/components/PersonChip';
|
import { PersonChip } from '../../people/components/PersonChip';
|
||||||
|
import { Checkbox } from '../../ui/components/form/Checkbox';
|
||||||
import { IconCalendarEvent, IconUser, IconUsers } from '../../ui/icons';
|
import { IconCalendarEvent, IconUser, IconUsers } from '../../ui/icons';
|
||||||
import { getLogoUrlFromDomainName, humanReadableDate } from '../../utils/utils';
|
import { getLogoUrlFromDomainName, humanReadableDate } from '../../utils/utils';
|
||||||
|
|
||||||
const StyledBoardCard = styled.div`
|
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme, selected }) =>
|
||||||
|
selected ? theme.selectedCard : theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ theme, selected }) =>
|
||||||
|
selected ? theme.selectedCardHover : theme.background.tertiary};
|
||||||
|
}
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -56,17 +62,27 @@ type CompanyProp = Pick<
|
|||||||
'id' | 'name' | 'domainName' | 'employees' | 'createdAt' | 'accountOwner'
|
'id' | 'name' | 'domainName' | 'employees' | 'createdAt' | 'accountOwner'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function CompanyBoardCard({ company }: { company: CompanyProp }) {
|
export function CompanyBoardCard({
|
||||||
|
company,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
company: CompanyProp;
|
||||||
|
selected: boolean;
|
||||||
|
onSelect: (company: CompanyProp) => void;
|
||||||
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<StyledBoardCardWrapper>
|
<StyledBoardCardWrapper>
|
||||||
<StyledBoardCard>
|
<StyledBoardCard selected={selected}>
|
||||||
<StyledBoardCardHeader>
|
<StyledBoardCardHeader>
|
||||||
<img
|
<img
|
||||||
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
||||||
alt={`${company.name}-company-logo`}
|
alt={`${company.name}-company-logo`}
|
||||||
/>
|
/>
|
||||||
<span>{company.name}</span>
|
<span>{company.name}</span>
|
||||||
|
<div style={{ display: 'flex', flex: 1 }} />
|
||||||
|
<Checkbox checked={selected} onChange={() => onSelect(company)} />
|
||||||
</StyledBoardCardHeader>
|
</StyledBoardCardHeader>
|
||||||
<StyledBoardCardBody>
|
<StyledBoardCardBody>
|
||||||
<span>
|
<span>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { ActionBar } from '@/ui/components/action-bar/ActionBar';
|
||||||
|
|
||||||
|
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EntityBoardActionBar({ children }: OwnProps) {
|
||||||
|
const selectedBoardItems = useRecoilValue(selectedBoardItemsState);
|
||||||
|
return <ActionBar selectedIds={selectedBoardItems}>{children}</ActionBar>;
|
||||||
|
}
|
||||||
@ -24,7 +24,7 @@ type OwnProps = {
|
|||||||
export function NewButton({ pipelineId, columnId }: OwnProps) {
|
export function NewButton({ pipelineId, columnId }: OwnProps) {
|
||||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||||
const [items, setItems] = useRecoilState(boardItemsState);
|
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||||
|
|
||||||
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation();
|
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation();
|
||||||
const onEntitySelect = useCallback(
|
const onEntitySelect = useCallback(
|
||||||
@ -36,8 +36,8 @@ export function NewButton({ pipelineId, columnId }: OwnProps) {
|
|||||||
(column: Column) => column.id === columnId,
|
(column: Column) => column.id === columnId,
|
||||||
);
|
);
|
||||||
newBoard[destinationColumnIndex].itemKeys.push(newUuid);
|
newBoard[destinationColumnIndex].itemKeys.push(newUuid);
|
||||||
setItems({
|
setBoardItems({
|
||||||
...items,
|
...boardItems,
|
||||||
[newUuid]: {
|
[newUuid]: {
|
||||||
id: company.id,
|
id: company.id,
|
||||||
name: company.name,
|
name: company.name,
|
||||||
@ -62,8 +62,8 @@ export function NewButton({ pipelineId, columnId }: OwnProps) {
|
|||||||
pipelineId,
|
pipelineId,
|
||||||
board,
|
board,
|
||||||
setBoard,
|
setBoard,
|
||||||
items,
|
boardItems,
|
||||||
setItems,
|
setBoardItems,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode, useState } from 'react';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { Company } from '../../../../generated/graphql';
|
import { Company } from '../../../../generated/graphql';
|
||||||
@ -13,10 +13,22 @@ const meta: Meta<typeof CompanyBoardCard> = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof CompanyBoardCard>;
|
type Story = StoryObj<typeof CompanyBoardCard>;
|
||||||
|
|
||||||
|
const FakeSelectableCompanyBoardCard = () => {
|
||||||
|
const [selected, setSelected] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CompanyBoardCard
|
||||||
|
company={mockedCompaniesData[0] as Company}
|
||||||
|
selected={selected}
|
||||||
|
onSelect={() => setSelected(!selected)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const CompanyCompanyBoardCard: Story = {
|
export const CompanyCompanyBoardCard: Story = {
|
||||||
render: () => (
|
render: () => (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<CompanyBoardCard company={mockedCompaniesData[0] as Company} />
|
<FakeSelectableCompanyBoardCard />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
1
front/src/modules/pipeline-progress/services/index.ts
Normal file
1
front/src/modules/pipeline-progress/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './update';
|
||||||
9
front/src/modules/pipeline-progress/services/update.ts
Normal file
9
front/src/modules/pipeline-progress/services/update.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const DELETE_PIPELINE_PROGRESS = gql`
|
||||||
|
mutation DeleteManyPipelineProgress($ids: [String!]) {
|
||||||
|
deleteManyPipelineProgress(where: { id: { in: $ids } }) {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const selectedBoardItemsState = atom<string[]>({
|
||||||
|
key: 'selectedBoardItemsState',
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
65
front/src/modules/ui/components/action-bar/ActionBar.tsx
Normal file
65
front/src/modules/ui/components/action-bar/ActionBar.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
|
||||||
|
import { PositionType } from '@/ui/types/PositionType';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
selectedIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StyledContainerProps = {
|
||||||
|
position: PositionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<StyledContainerProps>`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: 8px;
|
||||||
|
bottom: ${(props) => (props.position.x ? 'auto' : '38px')};
|
||||||
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||||
|
display: flex;
|
||||||
|
height: 48px;
|
||||||
|
|
||||||
|
left: ${(props) => (props.position.x ? `${props.position.x}px` : '50%')};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
position: ${(props) => (props.position.x ? 'fixed' : 'absolute')};
|
||||||
|
top: ${(props) => (props.position.y ? `${props.position.y}px` : 'auto')};
|
||||||
|
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function ActionBar({ children, selectedIds }: OwnProps) {
|
||||||
|
const position = useRecoilValue(contextMenuPositionState);
|
||||||
|
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (!(event.target as HTMLElement).closest('.action-bar')) {
|
||||||
|
setContextMenuPosition({ x: null, y: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
|
||||||
|
// Cleanup the event listener when the component unmounts
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [setContextMenuPosition]);
|
||||||
|
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer className="action-bar" position={position}>
|
||||||
|
{children}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,66 +1,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
|
import { ActionBar } from '@/ui/components/action-bar/ActionBar';
|
||||||
import { selectedRowIdsState } from '@/ui/tables/states/selectedRowIdsState';
|
import { selectedRowIdsState } from '@/ui/tables/states/selectedRowIdsState';
|
||||||
import { PositionType } from '@/ui/types/PositionType';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: React.ReactNode | React.ReactNode[];
|
children: React.ReactNode | React.ReactNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type StyledContainerProps = {
|
|
||||||
position: PositionType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<StyledContainerProps>`
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
border-radius: 8px;
|
|
||||||
bottom: ${(props) => (props.position.x ? 'auto' : '38px')};
|
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
|
||||||
display: flex;
|
|
||||||
height: 48px;
|
|
||||||
|
|
||||||
left: ${(props) => (props.position.x ? `${props.position.x}px` : '50%')};
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
position: ${(props) => (props.position.x ? 'fixed' : 'absolute')};
|
|
||||||
top: ${(props) => (props.position.y ? `${props.position.y}px` : 'auto')};
|
|
||||||
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function EntityTableActionBar({ children }: OwnProps) {
|
export function EntityTableActionBar({ children }: OwnProps) {
|
||||||
const selectedRowIds = useRecoilValue(selectedRowIdsState);
|
const selectedRowIds = useRecoilValue(selectedRowIdsState);
|
||||||
const position = useRecoilValue(contextMenuPositionState);
|
|
||||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return <ActionBar selectedIds={selectedRowIds}>{children}</ActionBar>;
|
||||||
function handleClickOutside(event: MouseEvent) {
|
|
||||||
if (!(event.target as HTMLElement).closest('.action-bar')) {
|
|
||||||
setContextMenuPosition({ x: null, y: null });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
|
|
||||||
// Cleanup the event listener when the component unmounts
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [setContextMenuPosition]);
|
|
||||||
|
|
||||||
if (selectedRowIds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer className="action-bar" position={position}>
|
|
||||||
{children}
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,8 @@ export const lightTheme = {
|
|||||||
background: backgroundLight,
|
background: backgroundLight,
|
||||||
border: borderLight,
|
border: borderLight,
|
||||||
boxShadow: boxShadowLight,
|
boxShadow: boxShadowLight,
|
||||||
|
selectedCardHover: color.blue20,
|
||||||
|
selectedCard: color.blue10,
|
||||||
font: fontLight,
|
font: fontLight,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -40,6 +42,8 @@ export const darkTheme: ThemeType = {
|
|||||||
background: backgroundDark,
|
background: backgroundDark,
|
||||||
border: borderDark,
|
border: borderDark,
|
||||||
boxShadow: boxShadowDark,
|
boxShadow: boxShadowDark,
|
||||||
|
selectedCardHover: color.blue70,
|
||||||
|
selectedCard: color.blue80,
|
||||||
font: fontDark,
|
font: fontDark,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
import { BoardActionBarButtonDeletePipelineProgress } from '@/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress';
|
||||||
|
import { EntityBoardActionBar } from '@/pipeline-progress/components/EntityBoardActionBar';
|
||||||
import { IconTargetArrow } from '@/ui/icons/index';
|
import { IconTargetArrow } from '@/ui/icons/index';
|
||||||
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
||||||
|
|
||||||
@ -10,8 +12,8 @@ import {
|
|||||||
useGetPipelinesQuery,
|
useGetPipelinesQuery,
|
||||||
useUpdateOnePipelineProgressMutation,
|
useUpdateOnePipelineProgressMutation,
|
||||||
} from '../../generated/graphql';
|
} from '../../generated/graphql';
|
||||||
import { Board } from '../../modules/opportunities/components/Board';
|
import { Board } from '../../modules/pipeline-progress/components/Board';
|
||||||
import { useBoard } from '../../modules/opportunities/hooks/useBoard';
|
import { useBoard } from '../../modules/pipeline-progress/hooks/useBoard';
|
||||||
|
|
||||||
export function Opportunities() {
|
export function Opportunities() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -49,13 +51,18 @@ export function Opportunities() {
|
|||||||
icon={<IconTargetArrow size={theme.icon.size.md} />}
|
icon={<IconTargetArrow size={theme.icon.size.md} />}
|
||||||
>
|
>
|
||||||
{items && pipelineId ? (
|
{items && pipelineId ? (
|
||||||
<Board
|
<>
|
||||||
pipelineId={pipelineId}
|
<Board
|
||||||
columns={columns || []}
|
pipelineId={pipelineId}
|
||||||
initialBoard={initialBoard}
|
columns={columns || []}
|
||||||
initialItems={items}
|
initialBoard={initialBoard}
|
||||||
onUpdate={onUpdate}
|
initialItems={items}
|
||||||
/>
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
<EntityBoardActionBar>
|
||||||
|
<BoardActionBarButtonDeletePipelineProgress />
|
||||||
|
</EntityBoardActionBar>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -108,6 +108,9 @@ export class AbilityFactory {
|
|||||||
can(AbilityAction.Update, 'PipelineProgress', {
|
can(AbilityAction.Update, 'PipelineProgress', {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
can(AbilityAction.Delete, 'PipelineProgress', {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
|
||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user