feat: delete views from views dropdown (#1234)

Closes #1129

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Thaïs
2023-08-16 23:27:03 +02:00
committed by GitHub
parent 8863bb0035
commit a24e1e4dc9
17 changed files with 188 additions and 24 deletions

View File

@ -982,6 +982,7 @@ export type Mutation = {
deleteManyCompany: AffectedRows;
deleteManyPerson: AffectedRows;
deleteManyPipelineProgress: AffectedRows;
deleteManyView: AffectedRows;
deleteManyViewSort: AffectedRows;
deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember;
@ -1120,6 +1121,11 @@ export type MutationDeleteManyPipelineProgressArgs = {
};
export type MutationDeleteManyViewArgs = {
where?: InputMaybe<ViewWhereInput>;
};
export type MutationDeleteManyViewSortArgs = {
where?: InputMaybe<ViewSortWhereInput>;
};
@ -3224,6 +3230,15 @@ export type CreateViewsMutationVariables = Exact<{
export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewsMutationVariables = Exact<{
where: ViewWhereInput;
}>;
export type DeleteViewsMutation = { __typename?: 'Mutation', deleteManyView: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewSortsMutationVariables = Exact<{
where: ViewSortWhereInput;
}>;
@ -5984,6 +5999,41 @@ export function useCreateViewsMutation(baseOptions?: Apollo.MutationHookOptions<
export type CreateViewsMutationHookResult = ReturnType<typeof useCreateViewsMutation>;
export type CreateViewsMutationResult = Apollo.MutationResult<CreateViewsMutation>;
export type CreateViewsMutationOptions = Apollo.BaseMutationOptions<CreateViewsMutation, CreateViewsMutationVariables>;
export const DeleteViewsDocument = gql`
mutation DeleteViews($where: ViewWhereInput!) {
deleteManyView(where: $where) {
count
}
}
`;
export type DeleteViewsMutationFn = Apollo.MutationFunction<DeleteViewsMutation, DeleteViewsMutationVariables>;
/**
* __useDeleteViewsMutation__
*
* To run a mutation, you first call `useDeleteViewsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteViewsMutation` 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 [deleteViewsMutation, { data, loading, error }] = useDeleteViewsMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useDeleteViewsMutation(baseOptions?: Apollo.MutationHookOptions<DeleteViewsMutation, DeleteViewsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteViewsMutation, DeleteViewsMutationVariables>(DeleteViewsDocument, options);
}
export type DeleteViewsMutationHookResult = ReturnType<typeof useDeleteViewsMutation>;
export type DeleteViewsMutationResult = Apollo.MutationResult<DeleteViewsMutation>;
export type DeleteViewsMutationOptions = Apollo.BaseMutationOptions<DeleteViewsMutation, DeleteViewsMutationVariables>;
export const DeleteViewSortsDocument = gql`
mutation DeleteViewSorts($where: ViewSortWhereInput!) {
deleteManyViewSort(where: $where) {

View File

@ -43,7 +43,8 @@ export function CompanyTable() {
objectName: objectId,
viewFieldDefinitions: companyViewFields,
});
const { updateSorts } = useViewSorts({ availableSorts });
const { handleSortsChange } = useViewSorts({ availableSorts });
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const filters = useRecoilScopedValue(
@ -85,7 +86,7 @@ export function CompanyTable() {
viewName="All Companies"
availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined}
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
onViewsChange={handleViewsChange}
onImport={handleImport}
updateEntityMutation={({

View File

@ -44,7 +44,7 @@ export function PeopleTable() {
objectName: objectId,
viewFieldDefinitions: peopleViewFields,
});
const { updateSorts } = useViewSorts({ availableSorts });
const { handleSortsChange } = useViewSorts({ availableSorts });
const filters = useRecoilScopedValue(
filtersScopedState,
@ -85,7 +85,7 @@ export function PeopleTable() {
viewName="All People"
availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined}
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
onViewsChange={handleViewsChange}
onImport={handleImport}
updateEntityMutation={({

View File

@ -129,8 +129,7 @@ export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) {
: column,
);
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
(onColumnsChange ?? setColumns)(nextColumns);
}
set(resizeFieldOffsetState, 0);
@ -159,8 +158,7 @@ export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) {
column.id === columnId ? { ...column, isVisible: true } : column,
);
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
(onColumnsChange ?? setColumns)(nextColumns);
},
[columns, onColumnsChange, setColumns],
);

View File

@ -101,8 +101,7 @@ export const TableOptionsDropdownButton = ({
: column,
);
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
(onColumnsChange ?? setColumns)(nextColumns);
},
[columns, onColumnsChange, setColumns],
);

View File

@ -8,10 +8,17 @@ 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 {
IconChevronDown,
IconList,
IconPencil,
IconPlus,
IconTrash,
} from '@/ui/icon';
import {
currentTableViewIdState,
currentTableViewState,
type TableView,
tableViewEditModeState,
tableViewsState,
} from '@/ui/table/states/tableViewsState';
@ -41,11 +48,13 @@ const StyledViewIcon = styled(IconList)`
type TableViewsDropdownButtonProps = {
defaultViewName: string;
HotkeyScope: TableViewsHotkeyScope;
onViewsChange?: (views: TableView[]) => void;
};
export const TableViewsDropdownButton = ({
defaultViewName,
HotkeyScope,
onViewsChange,
}: TableViewsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
@ -54,7 +63,10 @@ export const TableViewsDropdownButton = ({
currentTableViewState,
TableRecoilScopeContext,
);
const views = useRecoilScopedValue(tableViewsState, TableRecoilScopeContext);
const [views, setViews] = useRecoilScopedState(
tableViewsState,
TableRecoilScopeContext,
);
const [, setCurrentViewId] = useRecoilScopedState(
currentTableViewIdState,
TableRecoilScopeContext,
@ -88,6 +100,18 @@ export const TableViewsDropdownButton = ({
[setViewEditMode],
);
const handleDeleteViewButtonClick = useCallback(
(event: MouseEvent<HTMLButtonElement>, viewId: string) => {
event.stopPropagation();
if (currentView?.id === viewId) setCurrentViewId(undefined);
(onViewsChange ?? setViews)(views.filter((view) => view.id !== viewId));
setIsUnfolded(false);
},
[currentView?.id, onViewsChange, setCurrentViewId, setViews, views],
);
useEffect(() => {
isUnfolded
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
@ -124,12 +148,18 @@ export const TableViewsDropdownButton = ({
{views.map((view) => (
<DropdownMenuItem
key={view.id}
actions={
actions={[
<IconButton
key="edit"
onClick={(event) => handleEditViewButtonClick(event, view.id)}
icon={<IconPencil size={theme.icon.size.sm} />}
/>
}
/>,
<IconButton
key="delete"
onClick={(event) => handleDeleteViewButtonClick(event, view.id)}
icon={<IconTrash size={theme.icon.size.sm} />}
/>,
]}
onClick={() => handleViewSelect(view.id)}
>
<IconList size={theme.icon.size.md} />

View File

@ -64,6 +64,7 @@ export function TableHeader<SortField>({
leftComponent={
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
HotkeyScope={TableViewsHotkeyScope.Dropdown}
/>
}

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const DELETE_VIEWS = gql`
mutation DeleteViews($where: ViewWhereInput!) {
deleteManyView(where: $where) {
count
}
}
`;

View File

@ -131,6 +131,8 @@ export const useTableViewFields = ({
const handleColumnsChange = useCallback(
async (nextColumns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
setColumns(nextColumns);
const viewFieldsToCreate = nextColumns.filter(
(nextColumn) => !columnsById[nextColumn.id],
);
@ -144,7 +146,7 @@ export const useTableViewFields = ({
);
await updateViewFields(viewFieldsToUpdate);
},
[columnsById, createViewFields, updateViewFields],
[columnsById, createViewFields, setColumns, updateViewFields],
);
return { handleColumnsChange };

View File

@ -11,6 +11,7 @@ import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoi
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewsMutation,
useDeleteViewsMutation,
useGetViewsQuery,
useUpdateViewMutation,
ViewType,
@ -34,6 +35,7 @@ export const useTableViews = ({
const [createViewsMutation] = useCreateViewsMutation();
const [updateViewMutation] = useUpdateViewMutation();
const [deleteViewsMutation] = useDeleteViewsMutation();
const createViews = useCallback(
(views: TableView[]) => {
@ -72,6 +74,22 @@ export const useTableViews = ({
[updateViewMutation],
);
const deleteViews = useCallback(
(viewIds: string[]) => {
if (!viewIds.length) return;
return deleteViewsMutation({
variables: {
where: {
id: { in: viewIds },
},
},
refetchQueries: [getOperationName(GET_VIEWS) ?? ''],
});
},
[deleteViewsMutation],
);
useGetViewsQuery({
variables: {
where: {
@ -90,6 +108,8 @@ export const useTableViews = ({
const handleViewsChange = useCallback(
async (nextViews: TableView[]) => {
setViews(nextViews);
const viewsToCreate = nextViews.filter(
(nextView) => !viewsById[nextView.id],
);
@ -101,8 +121,14 @@ export const useTableViews = ({
viewsById[nextView.id].name !== nextView.name,
);
await updateViewFields(viewsToUpdate);
const nextViewIds = nextViews.map((nextView) => nextView.id);
const viewIdsToDelete = Object.keys(viewsById).filter(
(previousViewId) => !nextViewIds.includes(previousViewId),
);
return deleteViews(viewIdsToDelete);
},
[createViews, updateViewFields, viewsById],
[createViews, deleteViews, setViews, updateViewFields, viewsById],
);
return { handleViewsChange };

View File

@ -136,10 +136,12 @@ export const useViewSorts = <SortField>({
[currentViewId, deleteViewSortsMutation],
);
const updateSorts = useCallback(
const handleSortsChange = useCallback(
async (nextSorts: SelectedSortType<SortField>[]) => {
if (!currentViewId) return;
setSorts(nextSorts);
const sortsToCreate = nextSorts.filter(
(nextSort) => !sortsByKey[nextSort.key],
);
@ -162,10 +164,11 @@ export const useViewSorts = <SortField>({
createViewSorts,
currentViewId,
deleteViewSorts,
setSorts,
sortsByKey,
updateViewSorts,
],
);
return { updateSorts };
return { handleSortsChange };
};

View File

@ -140,17 +140,17 @@ export class AbilityFactory {
can(AbilityAction.Read, 'View', { workspaceId: workspace.id });
can(AbilityAction.Create, 'View', { workspaceId: workspace.id });
can(AbilityAction.Update, 'View', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'View', { workspaceId: workspace.id });
// ViewField
can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Update, 'ViewField', { workspaceId: workspace.id });
//Favorite
can(AbilityAction.Read, 'Favorite', { workspaceId: workspace.id });
can(AbilityAction.Create, 'Favorite');
can(AbilityAction.Delete, 'Favorite', {
workspaceId: workspace.id,
});
can(AbilityAction.Delete, 'Favorite', { workspaceId: workspace.id });
// ViewSort
can(AbilityAction.Read, 'ViewSort', { workspaceId: workspace.id });

View File

@ -114,6 +114,7 @@ import {
CreateViewAbilityHandler,
ReadViewAbilityHandler,
UpdateViewAbilityHandler,
DeleteViewAbilityHandler,
} from './handlers/view.ability-handler';
@Global()
@ -207,6 +208,7 @@ import {
ReadViewAbilityHandler,
CreateViewAbilityHandler,
UpdateViewAbilityHandler,
DeleteViewAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
@ -305,6 +307,7 @@ import {
ReadViewAbilityHandler,
CreateViewAbilityHandler,
UpdateViewAbilityHandler,
DeleteViewAbilityHandler,
// ViewField
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,

View File

@ -77,3 +77,19 @@ export class UpdateViewAbilityHandler implements IAbilityHandler {
return ability.can(AbilityAction.Update, subject('View', view));
}
}
@Injectable()
export class DeleteViewAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewArgs>();
const view = await this.prismaService.client.view.findFirst({
where: args.where,
});
assert(view, '', NotFoundException);
return ability.can(AbilityAction.Delete, subject('View', view));
}
}

View File

@ -7,6 +7,7 @@ import { Prisma, Workspace } from '@prisma/client';
import { AppAbility } from 'src/ability/ability.factory';
import {
CreateViewAbilityHandler,
DeleteViewAbilityHandler,
ReadViewAbilityHandler,
UpdateViewAbilityHandler,
} from 'src/ability/handlers/view.ability-handler';
@ -25,6 +26,7 @@ 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';
import { DeleteManyViewArgs } from 'src/core/@generated/view/delete-many-view.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => View)
@ -84,4 +86,17 @@ export class ViewResolver {
select: prismaSelect.value,
} as Prisma.ViewUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteViewAbilityHandler)
async deleteManyView(
@Args() args: DeleteManyViewArgs,
): Promise<AffectedRows> {
return this.viewService.deleteMany({
where: args.where,
});
}
}

View File

@ -0,0 +1,11 @@
-- DropForeignKey
ALTER TABLE "viewFields" DROP CONSTRAINT "viewFields_viewId_fkey";
-- DropForeignKey
ALTER TABLE "viewSorts" DROP CONSTRAINT "viewSorts_viewId_fkey";
-- AddForeignKey
ALTER TABLE "viewSorts" ADD CONSTRAINT "viewSorts_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "views"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "viewFields" ADD CONSTRAINT "viewFields_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "views"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -606,7 +606,7 @@ model ViewSort {
key String
name String
view View @relation(fields: [viewId], references: [id])
view View @relation(fields: [viewId], references: [id], onDelete: Cascade)
viewId String
/// @TypeGraphQL.omit(input: true, output: true)
@ -629,7 +629,7 @@ model ViewField {
objectName String
sizeInPx Int
view View? @relation(fields: [viewId], references: [id])
view View? @relation(fields: [viewId], references: [id], onDelete: Cascade)
viewId String?
/// @TypeGraphQL.omit(input: true, output: true)