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'; ...@@ -4,8 +4,11 @@ import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables'; import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { __ } from '~/locale'; import { __ } from '~/locale';
const TABLE_CELL_HEADER = 'th';
const TABLE_CELL_BODY = 'td';
export default { export default {
name: 'TableCellWrapper', name: 'TableCellBaseWrapper',
components: { components: {
NodeViewWrapper, NodeViewWrapper,
NodeViewContent, NodeViewContent,
...@@ -14,6 +17,11 @@ export default { ...@@ -14,6 +17,11 @@ export default {
GlDropdownDivider, GlDropdownDivider,
}, },
props: { props: {
cellType: {
type: String,
validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
required: true,
},
editor: { editor: {
type: Object, type: Object,
required: true, required: true,
...@@ -37,6 +45,9 @@ export default { ...@@ -37,6 +45,9 @@ export default {
totalCols() { totalCols() {
return this.selectedRect?.map.width; return this.selectedRect?.map.width;
}, },
isTableBodyCell() {
return this.cellType === TABLE_CELL_BODY;
},
}, },
mounted() { mounted() {
this.editor.on('selectionUpdate', this.handleSelectionUpdate); this.editor.on('selectionUpdate', this.handleSelectionUpdate);
...@@ -83,7 +94,11 @@ export default { ...@@ -83,7 +94,11 @@ export default {
}; };
</script> </script>
<template> <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"> <span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
<gl-dropdown <gl-dropdown
ref="dropdown" ref="dropdown"
...@@ -104,14 +119,14 @@ export default { ...@@ -104,14 +119,14 @@ export default {
<gl-dropdown-item @click="runCommand('addColumnAfter')"> <gl-dropdown-item @click="runCommand('addColumnAfter')">
{{ $options.i18n.insertColumnAfter }} {{ $options.i18n.insertColumnAfter }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item @click="runCommand('addRowBefore')"> <gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
{{ $options.i18n.insertRowBefore }} {{ $options.i18n.insertRowBefore }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item @click="runCommand('addRowAfter')"> <gl-dropdown-item @click="runCommand('addRowAfter')">
{{ $options.i18n.insertRowAfter }} {{ $options.i18n.insertRowAfter }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider /> <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 }} {{ $options.i18n.deleteRow }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')"> <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 { TableCell } from '@tiptap/extension-table-cell';
import { VueNodeViewRenderer } from '@tiptap/vue-2'; 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'; import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({ export default TableCell.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*', content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
addNodeView() { addNodeView() {
return VueNodeViewRenderer(TableCellWrapper); return VueNodeViewRenderer(TableCellBodyWrapper);
}, },
}); });
import { TableHeader } from '@tiptap/extension-table-header'; 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'; import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({ export default TableHeader.extend({
content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*', content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
addNodeView() {
return VueNodeViewRenderer(TableCellHeaderWrapper);
},
}); });
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2'; import { NodeViewWrapper } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from 'prosemirror-tables'; import { selectedRect as getSelectedRect } from 'prosemirror-tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; 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'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
jest.mock('prosemirror-tables'); jest.mock('prosemirror-tables');
describe('content/components/wrappers/table_cell', () => { describe('content/components/wrappers/table_cell_base', () => {
let wrapper; let wrapper;
let editor; let editor;
let getPos; let getPos;
const createWrapper = async () => { const createWrapper = async (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellWrapper, { wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: { propsData: {
editor, editor,
getPos, getPos,
...propsData,
}, },
}); });
}; };
...@@ -64,7 +66,7 @@ describe('content/components/wrappers/table_cell', () => { ...@@ -64,7 +66,7 @@ describe('content/components/wrappers/table_cell', () => {
setCurrentPositionInCell(); setCurrentPositionInCell();
createWrapper(); createWrapper();
await wrapper.vm.$nextTick(); await nextTick();
expect(findDropdown().props()).toMatchObject({ expect(findDropdown().props()).toMatchObject({
category: 'tertiary', category: 'tertiary',
...@@ -81,7 +83,7 @@ describe('content/components/wrappers/table_cell', () => { ...@@ -81,7 +83,7 @@ describe('content/components/wrappers/table_cell', () => {
it('does not display dropdown when selection cursor is not on the cell', async () => { it('does not display dropdown when selection cursor is not on the cell', async () => {
createWrapper(); createWrapper();
await wrapper.vm.$nextTick(); await nextTick();
expect(findDropdown().exists()).toBe(false); expect(findDropdown().exists()).toBe(false);
}); });
...@@ -97,7 +99,7 @@ describe('content/components/wrappers/table_cell', () => { ...@@ -97,7 +99,7 @@ describe('content/components/wrappers/table_cell', () => {
}); });
createWrapper(); createWrapper();
await wrapper.vm.$nextTick(); await nextTick();
mockDropdownHide(); mockDropdownHide();
}); });
...@@ -136,7 +138,7 @@ describe('content/components/wrappers/table_cell', () => { ...@@ -136,7 +138,7 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' }); emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
await wrapper.vm.$nextTick(); await nextTick();
findDropdownItemWithLabel('Delete row').vm.$emit('click'); findDropdownItemWithLabel('Delete row').vm.$emit('click');
...@@ -154,11 +156,38 @@ describe('content/components/wrappers/table_cell', () => { ...@@ -154,11 +156,38 @@ describe('content/components/wrappers/table_cell', () => {
emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' }); emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
await wrapper.vm.$nextTick(); await nextTick();
findDropdownItemWithLabel('Delete column').vm.$emit('click'); findDropdownItemWithLabel('Delete column').vm.$emit('click');
expect(mocks.deleteColumn).toHaveBeenCalled(); 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