Commit d441bd66 authored by Michael Lunøe's avatar Michael Lunøe

Merge branch '328759-edit-table-structures-content-editor' into 'master'

Allow to open table editing dropdown from headers

See merge request gitlab-org/gitlab!69499
parents f947d245 3e4a349a
......@@ -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