Commit 3e4a349a authored by Enrique Alcantara's avatar Enrique Alcantara

Allow to open table editing dropdown from headers

Display the table editing actions dropdown
in the table’s header as well. Do not allow
to insert rows before the table header

Changelog: changed
parent 15f1b494
......@@ -4,8 +4,11 @@ import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { __ } from '~/locale';
const TABLE_CELL_HEADER = 'th';
const TABLE_CELL_BODY = 'td';
export default {
name: 'TableCellWrapper',
name: 'TableCellBaseWrapper',
components: {
NodeViewWrapper,
NodeViewContent,
......@@ -14,6 +17,11 @@ export default {
GlDropdownDivider,
},
props: {
cellType: {
type: String,
validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
required: true,
},
editor: {
type: Object,
required: true,
......@@ -37,6 +45,9 @@ export default {
totalCols() {
return this.selectedRect?.map.width;
},
isTableBodyCell() {
return this.cellType === TABLE_CELL_BODY;
},
},
mounted() {
this.editor.on('selectionUpdate', this.handleSelectionUpdate);
......@@ -83,7 +94,11 @@ export default {
};
</script>
<template>
<node-view-wrapper class="gl-relative gl-padding-5 gl-min-w-10" as="td" @click="hideDropdown">
<node-view-wrapper
class="gl-relative gl-padding-5 gl-min-w-10"
:as="cellType"
@click="hideDropdown"
>
<span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
<gl-dropdown
ref="dropdown"
......@@ -104,14 +119,14 @@ export default {
<gl-dropdown-item @click="runCommand('addColumnAfter')">
{{ $options.i18n.insertColumnAfter }}
</gl-dropdown-item>
<gl-dropdown-item @click="runCommand('addRowBefore')">
<gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
{{ $options.i18n.insertRowBefore }}
</gl-dropdown-item>
<gl-dropdown-item @click="runCommand('addRowAfter')">
{{ $options.i18n.insertRowAfter }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item v-if="totalRows > 2" @click="runCommand('deleteRow')">
<gl-dropdown-item v-if="totalRows > 2 && isTableBodyCell" @click="runCommand('deleteRow')">
{{ $options.i18n.deleteRow }}
</gl-dropdown-item>
<gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')">
......
<script>
import TableCellBase from './table_cell_base.vue';
export default {
name: 'TableCellBody',
components: {
TableCellBase,
},
props: {
editor: {
type: Object,
required: true,
},
getPos: {
type: Function,
required: true,
},
},
};
</script>
<template>
<table-cell-base cell-type="td" v-bind="$props" />
</template>
<script>
import TableCellBase from './table_cell_base.vue';
export default {
name: 'TableCellHeader',
components: {
TableCellBase,
},
props: {
editor: {
type: Object,
required: true,
},
getPos: {
type: Function,
required: true,
},
},
};
</script>
<template>
<table-cell-base cell-type="th" v-bind="$props" />
</template>
import { TableCell } from '@tiptap/extension-table-cell';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellWrapper from '../components/wrappers/table_cell.vue';
import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
addNodeView() {
return VueNodeViewRenderer(TableCellWrapper);
return VueNodeViewRenderer(TableCellBodyWrapper);
},
});
import { TableHeader } from '@tiptap/extension-table-header';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
addNodeView() {
return VueNodeViewRenderer(TableCellHeaderWrapper);
},
});
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TableCellWrapper from '~/content_editor/components/wrappers/table_cell.vue';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
jest.mock('prosemirror-tables');
describe('content/components/wrappers/table_cell', () => {
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
let editor;
let getPos;
const createWrapper = async () => {
wrapper = shallowMountExtended(TableCellWrapper, {
const createWrapper = async (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
getPos,
...propsData,
},
});
};
......@@ -64,7 +66,7 @@ describe('content/components/wrappers/table_cell', () => {
setCurrentPositionInCell();
createWrapper();
await wrapper.vm.$nextTick();
await nextTick();
expect(findDropdown().props()).toMatchObject({
category: 'tertiary',
......@@ -81,7 +83,7 @@ describe('content/components/wrappers/table_cell', () => {
it('does not display dropdown when selection cursor is not on the cell', async () => {
createWrapper();
await wrapper.vm.$nextTick();
await nextTick();
expect(findDropdown().exists()).toBe(false);
});
......@@ -97,7 +99,7 @@ describe('content/components/wrappers/table_cell', () => {
});
createWrapper();
await wrapper.vm.$nextTick();
await nextTick();
mockDropdownHide();
});
......@@ -136,7 +138,7 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
await wrapper.vm.$nextTick();
await nextTick();
findDropdownItemWithLabel('Delete row').vm.$emit('click');
......@@ -154,11 +156,38 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
await wrapper.vm.$nextTick();
await nextTick();
findDropdownItemWithLabel('Delete column').vm.$emit('click');
expect(mocks.deleteColumn).toHaveBeenCalled();
});
describe('when current row is the table’s header', () => {
beforeEach(async () => {
// Remove 2 rows condition
getSelectedRect.mockReturnValue({
map: {
height: 3,
},
});
createWrapper({ cellType: 'th' });
await nextTick();
});
it('does not allow adding a row before the header', async () => {
expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
});
it('does not allow removing the header row', async () => {
createWrapper({ cellType: 'th' });
await nextTick();
expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import TableCellBodyWrapper from '~/content_editor/components/wrappers/table_cell_body.vue';
import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_body', () => {
let wrapper;
let editor;
let getPos;
const createWrapper = async () => {
wrapper = shallowMount(TableCellBodyWrapper, {
propsData: {
editor,
getPos,
},
});
};
beforeEach(() => {
getPos = jest.fn();
editor = createTestEditor({});
});
afterEach(() => {
wrapper.destroy();
});
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
getPos,
cellType: 'td',
});
});
});
import { shallowMount } from '@vue/test-utils';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import TableCellHeaderWrapper from '~/content_editor/components/wrappers/table_cell_header.vue';
import { createTestEditor } from '../../test_utils';
describe('content/components/wrappers/table_cell_header', () => {
let wrapper;
let editor;
let getPos;
const createWrapper = async () => {
wrapper = shallowMount(TableCellHeaderWrapper, {
propsData: {
editor,
getPos,
},
});
};
beforeEach(() => {
getPos = jest.fn();
editor = createTestEditor({});
});
afterEach(() => {
wrapper.destroy();
});
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
editor,
getPos,
cellType: 'th',
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment