Skip to content
Snippets Groups Projects
Commit 71f7baa8 authored by Coung Ngo's avatar Coung Ngo
Browse files

Merge branch '377307-add-ability-to-delete-task-list-item' into 'master'

Add ability to delete task list items

See merge request !108582



Merged-by: default avatarCoung Ngo <cngo@gitlab.com>
Approved-by: default avatarKatie Macoy <kmacoy@gitlab.com>
Approved-by: default avatarArtur Fedorov <afedorov@gitlab.com>
Reviewed-by: default avatarCoung Ngo <cngo@gitlab.com>
Reviewed-by: default avatarArtur Fedorov <afedorov@gitlab.com>
Reviewed-by: Nick Leonard's avatarNick Leonard <nleonard@gitlab.com>
parents b8df16f7 44cf8be6
No related branches found
No related tags found
No related merge requests found
Pipeline #753549878 passed
Pipeline: GitLab

#753563248

    Pipeline: GitLab

    #753553602

      ......@@ -453,7 +453,7 @@ export default {
      }
      },
      handleListItemReorder(description) {
      handleSaveDescription(description) {
      this.updateFormState();
      this.setFormState({ description });
      this.updateIssuable();
      ......@@ -573,7 +573,7 @@ export default {
      :update-url="updateEndpoint"
      :lock-version="state.lock_version"
      :is-updating="formState.updateLoading"
      @listItemReorder="handleListItemReorder"
      @saveDescription="handleSaveDescription"
      @taskListUpdateStarted="taskListUpdateStarted"
      @taskListUpdateSucceeded="taskListUpdateSucceeded"
      @taskListUpdateFailed="taskListUpdateFailed"
      ......
      <script>
      import { GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
      import { GlModalDirective, GlToast } from '@gitlab/ui';
      import $ from 'jquery';
      import { uniqueId } from 'lodash';
      import Sortable from 'sortablejs';
      import Vue from 'vue';
      import SafeHtml from '~/vue_shared/directives/safe_html';
      ......@@ -18,7 +19,6 @@ import Tracking from '~/tracking';
      import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
      import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
      import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
      import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
      import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
      import {
      ......@@ -29,8 +29,10 @@ import {
      WIDGET_TYPE_DESCRIPTION,
      } from '~/work_items/constants';
      import { renderGFM } from '~/behaviors/markdown/render_gfm';
      import eventHub from '../event_hub';
      import animateMixin from '../mixins/animate';
      import { convertDescriptionWithNewSort } from '../utils';
      import { convertDescriptionWithDeletedTaskListItem, convertDescriptionWithNewSort } from '../utils';
      import TaskListItemActions from './task_list_item_actions.vue';
      Vue.use(GlToast);
      ......@@ -44,7 +46,6 @@ export default {
      GlModal: GlModalDirective,
      },
      components: {
      GlTooltip,
      WorkItemDetailModal,
      },
      mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
      ......@@ -98,10 +99,10 @@ export default {
      const workItemId = getParameterByName('work_item_id');
      return {
      hasTaskListItemActions: false,
      preAnimation: false,
      pulseAnimation: false,
      initialUpdate: true,
      taskButtons: [],
      activeTask: {},
      workItemId: isPositiveInteger(workItemId)
      ? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
      ......@@ -164,6 +165,8 @@ export default {
      },
      },
      mounted() {
      eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
      this.renderGFM();
      this.updateTaskStatusText();
      ......@@ -175,6 +178,8 @@ export default {
      }
      },
      beforeDestroy() {
      eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
      this.removeAllPointerEventListeners();
      },
      methods: {
      ......@@ -198,7 +203,7 @@ export default {
      this.renderSortableLists();
      if (this.workItemsEnabled) {
      this.renderTaskActions();
      this.renderTaskListItemActions();
      }
      }
      },
      ......@@ -223,7 +228,7 @@ export default {
      handle: '.drag-icon',
      onUpdate: (event) => {
      const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
      this.$emit('listItemReorder', description);
      this.$emit('saveDescription', description);
      },
      }),
      );
      ......@@ -232,25 +237,25 @@ export default {
      createDragIconElement() {
      const container = document.createElement('div');
      // eslint-disable-next-line no-unsanitized/property
      container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
      container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-opacity-0" role="img" aria-hidden="true">
      <use href="${gon.sprite_icons}#drag-vertical"></use>
      </svg>`;
      return container.firstChild;
      },
      addPointerEventListeners(listItem, iconSelector) {
      addPointerEventListeners(listItem, elementSelector) {
      const pointeroverListener = (event) => {
      const icon = event.target.closest('li').querySelector(iconSelector);
      if (!icon || isDragging() || this.isUpdating) {
      const element = event.target.closest('li').querySelector(elementSelector);
      if (!element || isDragging() || this.isUpdating) {
      return;
      }
      icon.style.visibility = 'visible';
      element.classList.add('gl-opacity-10');
      };
      const pointeroutListener = (event) => {
      const icon = event.target.closest('li').querySelector(iconSelector);
      if (!icon) {
      const element = event.target.closest('li').querySelector(elementSelector);
      if (!element) {
      return;
      }
      icon.style.visibility = 'hidden';
      element.classList.remove('gl-opacity-10');
      };
      // We use pointerover/pointerout instead of CSS so that when we hover over a
      ......@@ -279,11 +284,9 @@ export default {
      taskListUpdateStarted() {
      this.$emit('taskListUpdateStarted');
      },
      taskListUpdateSuccess() {
      this.$emit('taskListUpdateSucceeded');
      },
      taskListUpdateError() {
      createAlert({
      message: sprintf(
      ......@@ -298,7 +301,6 @@ export default {
      this.$emit('taskListUpdateFailed');
      },
      updateTaskStatusText() {
      const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
      const $issuableHeader = $('.issuable-meta');
      ......@@ -317,15 +319,28 @@ export default {
      $tasksShort.text('');
      }
      },
      renderTaskActions() {
      createTaskListItemActions(toggleClass) {
      const app = new Vue({
      el: document.createElement('div'),
      provide: { toggleClass },
      render: (createElement) => createElement(TaskListItemActions),
      });
      return app.$el;
      },
      deleteTaskListItem(sourcepos) {
      this.$emit(
      'saveDescription',
      convertDescriptionWithDeletedTaskListItem(this.descriptionText, sourcepos),
      );
      },
      renderTaskListItemActions() {
      if (!this.$el?.querySelectorAll) {
      return;
      }
      this.taskButtons = [];
      const taskListFields = this.$el.querySelectorAll('.task-list-item:not(.inapplicable)');
      taskListFields.forEach((item, index) => {
      taskListFields.forEach((item) => {
      const taskLink = item.querySelector('.gfm-issue');
      if (taskLink) {
      const { issue, referenceType, issueType } = taskLink.dataset;
      ......@@ -351,31 +366,11 @@ export default {
      });
      return;
      }
      this.addPointerEventListeners(item, '.js-add-task');
      const button = document.createElement('button');
      button.classList.add(
      'btn',
      'btn-default',
      'btn-md',
      'gl-button',
      'btn-default-tertiary',
      'gl-visibility-hidden',
      'gl-p-0!',
      'gl-mt-n1',
      'gl-ml-3',
      'js-add-task',
      );
      button.id = `js-task-button-${index}`;
      this.taskButtons.push(button.id);
      // eslint-disable-next-line no-unsanitized/property
      button.innerHTML = `
      <svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
      <use href="${gon.sprite_icons}#doc-new"></use>
      </svg>
      `;
      button.setAttribute('aria-label', s__('WorkItem|Create task'));
      button.addEventListener('click', () => this.handleCreateTask(button));
      this.insertButtonNextToTaskText(item, button);
      const toggleClass = uniqueId('task-list-item-actions-');
      this.addPointerEventListeners(item, `.${toggleClass}`);
      this.insertNextToTaskListItemText(this.createTaskListItemActions(toggleClass), item);
      this.hasTaskListItemActions = true;
      });
      },
      addHoverListeners(taskLink, id) {
      ......@@ -391,19 +386,20 @@ export default {
      }
      });
      },
      insertButtonNextToTaskText(listItem, button) {
      const paragraph = Array.from(listItem.children).find((element) => element.tagName === 'P');
      const lastChild = listItem.lastElementChild;
      insertNextToTaskListItemText(element, listItem) {
      const children = Array.from(listItem.children);
      const paragraph = children.find((el) => el.tagName === 'P');
      const list = children.find((el) => el.classList.contains('task-list'));
      if (paragraph) {
      // If there's a `p` element, then it's a multi-paragraph task item
      // and the task text exists within the `p` element as the last child
      paragraph.append(button);
      } else if (lastChild.tagName === 'OL' || lastChild.tagName === 'UL') {
      paragraph.append(element);
      } else if (list) {
      // Otherwise, the task item can have a child list which exists directly after the task text
      lastChild.insertAdjacentElement('beforebegin', button);
      list.insertAdjacentElement('beforebegin', element);
      } else {
      // Otherwise, the task item is a simple one where the task text exists as the last child
      listItem.append(button);
      listItem.append(element);
      }
      },
      setActiveTask(el) {
      ......@@ -492,14 +488,7 @@ export default {
      </script>
      <template>
      <div
      v-if="descriptionHtml"
      :class="{
      'js-task-list-container': canUpdate,
      'work-items-enabled': workItemsEnabled,
      }"
      class="description"
      >
      <div v-if="descriptionHtml" :class="{ 'js-task-list-container': canUpdate }" class="description">
      <div
      ref="gfm-content"
      v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
      ......@@ -507,10 +496,10 @@ export default {
      :class="{
      'issue-realtime-pre-pulse': preAnimation,
      'issue-realtime-trigger-pulse': pulseAnimation,
      'has-task-list-item-actions': hasTaskListItemActions,
      }"
      class="md"
      ></div>
      <textarea
      v-if="descriptionText"
      :value="descriptionText"
      ......@@ -531,10 +520,5 @@ export default {
      @workItemDeleted="handleDeleteTask"
      @close="closeWorkItemDetailModal"
      />
      <template v-if="workItemsEnabled">
      <gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
      {{ s__('WorkItem|Create task') }}
      </gl-tooltip>
      </template>
      </div>
      </template>
      <script>
      import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
      import { __, s__ } from '~/locale';
      import eventHub from '../event_hub';
      export default {
      i18n: {
      delete: __('Delete'),
      taskActions: s__('WorkItem|Task actions'),
      },
      components: {
      GlDropdown,
      GlDropdownItem,
      },
      inject: ['toggleClass'],
      methods: {
      deleteTaskListItem() {
      eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos);
      },
      },
      };
      </script>
      <template>
      <gl-dropdown
      class="task-list-item-actions-wrapper"
      category="tertiary"
      icon="ellipsis_v"
      lazy
      no-caret
      right
      :text="$options.i18n.taskActions"
      text-sr-only
      :toggle-class="`task-list-item-actions gl-opacity-0 gl-p-2! ${toggleClass}`"
      >
      <gl-dropdown-item variant="danger" @click="deleteTaskListItem">
      {{ $options.i18n.delete }}
      </gl-dropdown-item>
      </gl-dropdown>
      </template>
      ......@@ -93,3 +93,78 @@ export const convertDescriptionWithNewSort = (description, list) => {
      return descriptionLines.join(NEWLINE);
      };
      const bulletTaskListItemRegex = /^\s*[-*]\s+\[.]/;
      const numericalTaskListItemRegex = /^\s*[0-9]\.\s+\[.]/;
      /**
      * Checks whether the line of markdown contains a task list item,
      * i.e. `- [ ]`, `* [ ]`, or `1. [ ]`.
      *
      * @param {String} line A line of markdown
      * @returns {boolean} `true` if the line contains a task list item, otherwise `false`
      */
      const containsTaskListItem = (line) =>
      bulletTaskListItemRegex.test(line) || numericalTaskListItemRegex.test(line);
      /**
      * Deletes a task list item from the description.
      *
      * Starting from the task list item, it deletes each line until it hits a nested
      * task list item and reduces the indentation of each line from this line onwards.
      *
      * For example, for a given description like:
      *
      * <pre>
      * 1. [ ] item 1
      *
      * paragraph text
      *
      * 1. [ ] item 2
      *
      * paragraph text
      *
      * 1. [ ] item 3
      * </pre>
      *
      * Then when prompted to delete item 1, this function will return:
      *
      * <pre>
      * 1. [ ] item 2
      *
      * paragraph text
      *
      * 1. [ ] item 3
      * </pre>
      *
      * @param {String} description Description in markdown format
      * @param {String} sourcepos Source position in format `23:3-23:14`
      * @returns {String} Markdown with the deleted task list item
      */
      export const convertDescriptionWithDeletedTaskListItem = (description, sourcepos) => {
      const descriptionLines = description.split(NEWLINE);
      const [startIndex, endIndex] = getSourceposRows(sourcepos);
      let indentation = 0;
      let linesToDelete = 1;
      let reduceIndentation = false;
      for (let i = startIndex + 1; i <= endIndex; i += 1) {
      if (reduceIndentation) {
      descriptionLines[i] = descriptionLines[i].slice(indentation);
      } else if (containsTaskListItem(descriptionLines[i])) {
      reduceIndentation = true;
      const firstLine = descriptionLines[startIndex];
      const currentLine = descriptionLines[i];
      const firstLineIndentation = firstLine.length - firstLine.trimStart().length;
      const currentLineIndentation = currentLine.length - currentLine.trimStart().length;
      indentation = currentLineIndentation - firstLineIndentation;
      descriptionLines[i] = descriptionLines[i].slice(indentation);
      } else {
      linesToDelete += 1;
      }
      }
      descriptionLines.splice(startIndex, linesToDelete);
      return descriptionLines.join(NEWLINE);
      };
      ......@@ -12,6 +12,21 @@
      padding-inline-end: 1rem;
      width: 2rem;
      }
      .task-list-item-actions-wrapper {
      position: absolute;
      inset-block-start: 0;
      inset-inline-end: -2rem;
      }
      .task-list-item-actions-wrapper.show .task-list-item-actions,
      .task-list-item-actions:is(:focus, :hover) {
      opacity: 1;
      }
      }
      .md.has-task-list-item-actions > :is(ul, ol) > li {
      margin-inline-end: 1.5rem;
      }
      ul.task-list > li.task-list-item {
      ......@@ -21,6 +36,10 @@
      inset-inline-start: -0.6rem;
      }
      }
      .dropdown-item.text-danger p {
      color: var(--red-500, $red-500); /* Override typography.scss making text black */
      }
      }
      .is-ghost {
      ......
      ......@@ -47760,9 +47760,6 @@ msgstr ""
      msgid "WorkItem|Create objective"
      msgstr ""
       
      msgid "WorkItem|Create task"
      msgstr ""
      msgid "WorkItem|Create work item"
      msgstr ""
       
      ......@@ -47901,6 +47898,9 @@ msgstr ""
      msgid "WorkItem|Task"
      msgstr ""
       
      msgid "WorkItem|Task actions"
      msgstr ""
      msgid "WorkItem|Task deleted"
      msgstr ""
       
      ......@@ -645,10 +645,10 @@ describe('Issuable output', () => {
      });
      });
      describe('listItemReorder event', () => {
      describe('saveDescription event', () => {
      it('makes request to update issue', async () => {
      const description = 'I have been updated!';
      findDescription().vm.$emit('listItemReorder', description);
      findDescription().vm.$emit('saveDescription', description);
      await waitForPromises();
      expect(mock.history.put[0].data).toContain(description);
      ......
      import $ from 'jquery';
      import Vue, { nextTick } from 'vue';
      import VueApollo from 'vue-apollo';
      import { GlTooltip, GlModal } from '@gitlab/ui';
      import { GlModal } from '@gitlab/ui';
      import setWindowLocation from 'helpers/set_window_location_helper';
      import { stubComponent } from 'helpers/stub_component';
      import { TEST_HOST } from 'helpers/test_constants';
      ......@@ -10,9 +9,8 @@ import { mockTracking } from 'helpers/tracking_helper';
      import createMockApollo from 'helpers/mock_apollo_helper';
      import waitForPromises from 'helpers/wait_for_promises';
      import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
      import { createAlert } from '~/flash';
      import Description from '~/issues/show/components/description.vue';
      import eventHub from '~/issues/show/event_hub';
      import { updateHistory } from '~/lib/utils/url_utility';
      import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
      import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
      ......@@ -31,7 +29,6 @@ import {
      descriptionHtmlWithTask,
      } from '../mock_data/mock_data';
      jest.mock('~/flash');
      jest.mock('~/lib/utils/url_utility', () => ({
      ...jest.requireActual('~/lib/utils/url_utility'),
      updateHistory: jest.fn(),
      ......@@ -65,11 +62,8 @@ describe('Description component', () => {
      const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
      const findTextarea = () => wrapper.find('[data-testid="textarea"]');
      const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
      const findConvertToTaskButton = () => wrapper.find('.js-add-task');
      const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
      const findTaskLink = () => wrapper.find('a.gfm-issue');
      const findTooltips = () => wrapper.findAllComponents(GlTooltip);
      const findModal = () => wrapper.findComponent(GlModal);
      const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
      ......@@ -125,10 +119,6 @@ describe('Description component', () => {
      }
      });
      afterEach(() => {
      wrapper.destroy();
      });
      afterAll(() => {
      $('.issuable-meta .flash-container').remove();
      });
      ......@@ -311,13 +301,6 @@ describe('Description component', () => {
      expect(findTaskActionButtons()).toHaveLength(3);
      });
      it('renders a list of tooltips corresponding to checkboxes in description HTML', () => {
      expect(findTooltips()).toHaveLength(3);
      expect(findTooltips().at(0).props('target')).toBe(
      findTaskActionButtons().at(0).attributes('id'),
      );
      });
      it('does not show a modal by default', () => {
      expect(findModal().exists()).toBe(false);
      });
      ......@@ -331,50 +314,29 @@ describe('Description component', () => {
      });
      });
      describe('creating work item from checklist item', () => {
      it('emits `updateDescription` after creating new work item', async () => {
      createComponent({
      props: {
      descriptionHtml: descriptionHtmlWithCheckboxes,
      },
      provide: {
      glFeatures: {
      workItemsCreateFromMarkdown: true,
      },
      },
      });
      const newDescription = `<p>New description</p>`;
      describe('task list item actions', () => {
      describe('deleting the task list item', () => {
      it('emits an event to update the description with the deleted task list item', () => {
      const descriptionText = `Tasks
      await findConvertToTaskButton().trigger('click');
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 3
      1. [ ] item 4;`;
      const newDescriptionText = `Tasks
      await waitForPromises();
      1. [ ] item 1
      1. [ ] item 3
      1. [ ] item 4;`;
      createComponent({
      props: { descriptionText },
      provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
      });
      expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
      });
      eventHub.$emit('delete-task-list-item', '4:4-5:19');
      it('shows flash message when creating task fails', async () => {
      createComponent({
      props: {
      descriptionHtml: descriptionHtmlWithCheckboxes,
      },
      provide: {
      glFeatures: {
      workItemsCreateFromMarkdown: true,
      },
      },
      createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
      expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
      });
      await findConvertToTaskButton().trigger('click');
      await waitForPromises();
      expect(createAlert).toHaveBeenCalledWith(
      expect.objectContaining({
      message: 'Something went wrong when creating task. Please try again.',
      }),
      );
      });
      });
      ......
      import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
      import { shallowMount } from '@vue/test-utils';
      import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
      import eventHub from '~/issues/show/event_hub';
      describe('TaskListItemActions component', () => {
      let wrapper;
      const findGlDropdown = () => wrapper.findComponent(GlDropdown);
      const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
      const mountComponent = () => {
      const li = document.createElement('li');
      li.dataset.sourcepos = '3:1-3:10';
      li.appendChild(document.createElement('div'));
      document.body.appendChild(li);
      wrapper = shallowMount(TaskListItemActions, {
      provide: { toggleClass: 'task-list-item-actions' },
      attachTo: document.querySelector('div'),
      });
      };
      beforeEach(() => {
      mountComponent();
      });
      it('renders dropdown', () => {
      expect(findGlDropdown().props()).toMatchObject({
      category: 'tertiary',
      icon: 'ellipsis_v',
      right: true,
      text: TaskListItemActions.i18n.taskActions,
      textSrOnly: true,
      });
      });
      it('emits event when `Delete` dropdown item is clicked', () => {
      jest.spyOn(eventHub, '$emit');
      findGlDropdownItem().vm.$emit('click');
      expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
      });
      });
      import { convertDescriptionWithNewSort } from '~/issues/show/utils';
      import {
      convertDescriptionWithDeletedTaskListItem,
      convertDescriptionWithNewSort,
      } from '~/issues/show/utils';
      describe('app/assets/javascripts/issues/show/utils.js', () => {
      describe('convertDescriptionWithNewSort', () => {
      ......@@ -137,4 +140,201 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
      expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
      });
      });
      describe('convertDescriptionWithDeletedTaskListItem', () => {
      const description = `Tasks
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 3
      1. [ ] item 4
      1. [ ] item 5
      1. [ ] item 6
      paragraph text
      1. [ ] item 7
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      /* The equivalent HTML for the above markdown
      <ol data-sourcepos="3:1-21:17">
      <li data-sourcepos="3:1-21:17">item 1
      <ol data-sourcepos="4:4-21:17">
      <li data-sourcepos="4:4-4:16">
      <p data-sourcepos="4:7-4:16">item 2</p>
      </li>
      <li data-sourcepos="5:4-7:19">
      <p data-sourcepos="5:7-5:16">item 3</p>
      <ol data-sourcepos="6:7-7:19">
      <li data-sourcepos="6:7-6:19">item 4</li>
      <li data-sourcepos="7:7-7:19">item 5</li>
      </ol>
      </li>
      <li data-sourcepos="8:4-11:0">
      <p data-sourcepos="8:7-8:16">item 6</p>
      <p data-sourcepos="10:7-10:20">paragraph text</p>
      </li>
      <li data-sourcepos="12:4-20:19">
      <p data-sourcepos="12:7-12:16">item 7</p>
      <p data-sourcepos="14:7-14:20">paragraph text</p>
      <ol data-sourcepos="16:7-20:19">
      <li data-sourcepos="16:7-19:0">
      <p data-sourcepos="16:10-16:19">item 8</p>
      <p data-sourcepos="18:10-18:23">paragraph text</p>
      </li>
      <li data-sourcepos="20:7-20:19">
      <p data-sourcepos="20:10-20:19">item 9</p>
      </li>
      </ol>
      </li>
      <li data-sourcepos="21:4-21:17">
      <p data-sourcepos="21:7-21:17">item 10</p>
      </li>
      </ol>
      </li>
      </ol>
      */
      it('deletes item with no children', () => {
      const sourcepos = '4:4-4:14';
      const newDescription = `Tasks
      1. [ ] item 1
      1. [ ] item 3
      1. [ ] item 4
      1. [ ] item 5
      1. [ ] item 6
      paragraph text
      1. [ ] item 7
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
      newDescription,
      );
      });
      it('deletes deeply nested item with no children', () => {
      const sourcepos = '6:7-6:19';
      const newDescription = `Tasks
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 3
      1. [ ] item 5
      1. [ ] item 6
      paragraph text
      1. [ ] item 7
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
      newDescription,
      );
      });
      it('deletes item with children and moves sub-tasks up a level', () => {
      const sourcepos = '5:4-7:19';
      const newDescription = `Tasks
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 4
      1. [ ] item 5
      1. [ ] item 6
      paragraph text
      1. [ ] item 7
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
      newDescription,
      );
      });
      it('deletes item with associated paragraph text', () => {
      const sourcepos = '8:4-11:0';
      const newDescription = `Tasks
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 3
      1. [ ] item 4
      1. [ ] item 5
      1. [ ] item 7
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
      newDescription,
      );
      });
      it('deletes item with associated paragraph text and moves sub-tasks up a level', () => {
      const sourcepos = '12:4-20:19';
      const newDescription = `Tasks
      1. [ ] item 1
      1. [ ] item 2
      1. [ ] item 3
      1. [ ] item 4
      1. [ ] item 5
      1. [ ] item 6
      paragraph text
      1. [ ] item 8
      paragraph text
      1. [ ] item 9
      1. [ ] item 10`;
      expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
      newDescription,
      );
      });
      });
      });
      0% Loading or .
      You are about to add 0 people to the discussion. Proceed with caution.
      Finish editing this message first!
      Please register or to comment