diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index 34c99db435a5d2e6016161cd25b50eeba57a1c92..a96c81494c12952cd25800c6067f4c327b02fa40 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -19,11 +19,14 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['id', 'issuableType'],
+ inject: ['id', 'issuableType', 'enabled'],
computed: {
showConvertToTaskItem() {
return allowedTypes.includes(this.issuableType);
},
+ isEnabledTaskListItem() {
+ return this.enabled;
+ },
},
methods: {
convertToTask() {
@@ -32,6 +35,12 @@ export default {
deleteTaskListItem() {
eventHub.$emit('delete-task-list-item', this.eventPayload());
},
+ disableTaskListItem() {
+ eventHub.$emit('disable-task-list-item', this.eventPayload());
+ },
+ enableTaskListItem() {
+ eventHub.$emit('enable-task-list-item', this.eventPayload());
+ },
eventPayload() {
return {
id: this.id,
@@ -58,12 +67,33 @@ export default {
v-if="showConvertToTaskItem"
class="!gl-ml-2"
data-testid="convert"
+ :disabled="!isEnabledTaskListItem"
@action="convertToTask"
>
{{ s__('WorkItem|Convert to child item') }}
+
+
+ {{ s__('WorkItem| Disable list item') }}
+
+
+
+
+ {{ s__('WorkItem| Enable list item') }}
+
+
{
};
};
+export const disableTaskListItem = (description, sourcepos) => {
+ const descriptionLines = description.split(NEWLINE);
+ const startIndex = getSourceposRows(sourcepos)[0];
+
+ if (descriptionLines[startIndex].includes('[ ]')) {
+ descriptionLines[startIndex] = descriptionLines[startIndex].replace('[ ]', '[~]');
+ } else if (descriptionLines[startIndex].includes('[x]')) {
+ descriptionLines[startIndex] = descriptionLines[startIndex].replace('[x]', '[~]');
+ } else if (descriptionLines[startIndex].includes('[X]')) {
+ descriptionLines[startIndex] = descriptionLines[startIndex].replace('[X]', '[~]');
+ }
+
+ return {
+ newDescription: descriptionLines.join(NEWLINE),
+ };
+};
+
+export const enableTaskListItem = (description, sourcepos) => {
+ const descriptionLines = description.split(NEWLINE);
+ const startIndex = getSourceposRows(sourcepos)[0];
+
+ if (descriptionLines[startIndex].includes('[~]')) {
+ descriptionLines[startIndex] = descriptionLines[startIndex].replace('[~]', '[ ]');
+ }
+
+ return {
+ newDescription: descriptionLines.join(NEWLINE),
+ };
+};
+
+export const isDisabledTaskListItem = (description, sourcepos) => {
+ const descriptionLines = description.split(NEWLINE);
+ const startIndex = getSourceposRows(sourcepos)[0];
+
+ if (descriptionLines[startIndex].includes('[~]')) {
+ return true;
+ }
+ return false;
+};
+
/**
* Given a title and description for a task:
*
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 6663d7e20aa5c0e93f1e319bf583c55c0415fe6b..c565836ecbe53f2d7fea2b85ccb809c6d8223cf2 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -11,6 +11,8 @@ import { InternalEvents } from '~/tracking';
import {
convertDescriptionWithNewSort,
deleteTaskListItem,
+ disableTaskListItem,
+ enableTaskListItem,
extractTaskTitleAndDescription,
insertNextToTaskListItemText,
} from '~/issues/show/utils';
@@ -135,6 +137,8 @@ export default {
async mounted() {
eventHub.$on('convert-task-list-item', this.convertTaskListItem);
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
+ eventHub.$on('disable-task-list-item', this.disableTaskListItem);
+ eventHub.$on('enable-task-list-item', this.enableTaskListItem);
window.addEventListener('hashchange', (e) => this.truncateOrScrollToAnchor(e));
await this.$nextTick();
@@ -143,6 +147,8 @@ export default {
beforeDestroy() {
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
+ eventHub.$off('disable-task-list-item', this.disableTaskListItem);
+ eventHub.$off('enable-task-list-item', this.enableTaskListItem);
window.removeEventListener('hashchange', this.truncateOrScrollToAnchor);
this.removeAllPointerEventListeners();
@@ -242,20 +248,23 @@ export default {
},
renderTaskListItemActions() {
const taskListItems = this.$el.querySelectorAll?.(
- '.task-list-item:not(.inapplicable, table .task-list-item)',
+ '.task-list-item:not(table .task-list-item',
);
taskListItems?.forEach((listItem) => {
- const dropdown = this.createTaskListItemActions();
+ const enabled =
+ listItem.querySelectorAll('.task-list-item-checkbox[disabled]').length === 0;
+
+ const dropdown = this.createTaskListItemActions(enabled);
insertNextToTaskListItemText(dropdown, listItem);
this.addPointerEventListeners(listItem, '.task-list-item-actions');
this.hasTaskListItemActions = true;
});
},
- createTaskListItemActions() {
+ createTaskListItemActions(enabled) {
const app = new Vue({
el: document.createElement('div'),
- provide: { id: this.workItemId, issuableType: this.workItemType },
+ provide: { id: this.workItemId, issuableType: this.workItemType, enabled },
render: (createElement) => createElement(TaskListItemActions),
});
return app.$el;
@@ -322,6 +331,20 @@ export default {
const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
this.$emit('descriptionUpdated', newDescription);
},
+ disableTaskListItem({ id, sourcepos }) {
+ if (this.workItemId !== id) {
+ return;
+ }
+ const { newDescription } = disableTaskListItem(this.descriptionText, sourcepos);
+ this.$emit('descriptionUpdated', newDescription);
+ },
+ enableTaskListItem({ id, sourcepos }) {
+ if (this.workItemId !== id) {
+ return;
+ }
+ const { newDescription } = enableTaskListItem(this.descriptionText, sourcepos);
+ this.$emit('descriptionUpdated', newDescription);
+ },
handleWorkItemCreated() {
this.$emit('descriptionUpdated', this.newDescription);
},
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 66382c517adffde1d9e5a1fe31d01dca0fba032f..107f8465fdf21459548d406e7d4fe78463f963fb 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -609,7 +609,8 @@
}
// Strikethrough block-level items in non-tight lists as a whole; we don't do sublists.
- > p, div {
+ > p,
+ > div:not(.gl-disclosure-dropdown) {
text-decoration: line-through;
@apply gl-text-disabled;
}
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index 84877bb26e3d512501835b1107e64c9866c4241e..bf4b0fc344c9bccdaa01c4c6b2b12a499f4802a6 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -25,7 +25,7 @@
}
}
- .has-task-list-item-actions > :is(ul, ol) > li {
+ .has-task-list-item-actions :is(ul, ol) li {
margin-inline-end: 2rem;
}
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a3c97604174f0416cdab768312320af279d0f95d..10f9ed6c42287d094510e859fbe265e58fd2ca42 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -76103,6 +76103,12 @@ msgstr ""
msgid "WorkItems|Your preferences"
msgstr ""
+msgid "WorkItem| Disable list item"
+msgstr ""
+
+msgid "WorkItem| Enable list item"
+msgstr ""
+
msgid "WorkItem|%d item"
msgid_plural "WorkItem|%d items"
msgstr[0] ""
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index a85d1cb009d4239997a539418bc897060082c446..09f4505421525fbed26cd773fbc63e4309ac0386 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -14,8 +14,10 @@ describe('TaskListItemActions component', () => {
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findConvertToChildItemItem = () => wrapper.findByTestId('convert');
const findDeleteItem = () => wrapper.findByTestId('delete');
+ const findDisableItem = () => wrapper.findByTestId('disable');
+ const findEnableItem = () => wrapper.findByTestId('enable');
- const mountComponent = ({ issuableType = TYPE_ISSUE } = {}) => {
+ const mountComponent = ({ issuableType = TYPE_ISSUE, enabled = true } = {}) => {
setHTMLFixture(`
@@ -26,6 +28,7 @@ describe('TaskListItemActions component', () => {
provide: {
id: 'gid://gitlab/WorkItem/818',
issuableType,
+ enabled,
},
attachTo: 'div',
});
@@ -65,7 +68,7 @@ describe('TaskListItemActions component', () => {
});
});
- describe('events', () => {
+ describe('events for enabled items', () => {
beforeEach(() => {
mountComponent();
});
@@ -87,5 +90,45 @@ describe('TaskListItemActions component', () => {
sourcepos: '3:1-3:10',
});
});
+ it('emits event when `Disable` dropdown item is clicked', () => {
+ findDisableItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('disable-task-list-item', {
+ id: 'gid://gitlab/WorkItem/818',
+ sourcepos: '3:1-3:10',
+ });
+ });
+ });
+
+ describe('events for disabled items', () => {
+ beforeEach(() => {
+ mountComponent({ enabled: false });
+ });
+
+ it('emits event when `Convert to child item` dropdown item is clicked', () => {
+ findConvertToChildItemItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', {
+ id: 'gid://gitlab/WorkItem/818',
+ sourcepos: '3:1-3:10',
+ });
+ });
+
+ it('emits event when `Delete` dropdown item is clicked', () => {
+ findDeleteItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', {
+ id: 'gid://gitlab/WorkItem/818',
+ sourcepos: '3:1-3:10',
+ });
+ });
+ it('emits event when `Enable` dropdown item is clicked', () => {
+ findEnableItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable-task-list-item', {
+ id: 'gid://gitlab/WorkItem/818',
+ sourcepos: '3:1-3:10',
+ });
+ });
});
});
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
index dc1abd1cd65fd0aab4758dfd3dd280c0341dad28..dfc3da03080fe7aa543812403ca0ef0f2a38bc38 100644
--- a/spec/frontend/issues/show/utils_spec.js
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -2,6 +2,8 @@ import { setHTMLFixture } from 'helpers/fixtures';
import {
convertDescriptionWithNewSort,
deleteTaskListItem,
+ disableTaskListItem,
+ enableTaskListItem,
extractTaskTitleAndDescription,
insertNextToTaskListItemText,
} from '~/issues/show/utils';
@@ -354,6 +356,82 @@ paragraph text
});
});
+ describe('disableTaskListItem', () => {
+ it('disable unchecked item', () => {
+ const description = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+ const sourcepos = '4:4-4:14';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [~] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+
+ expect(disableTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ });
+ });
+
+ it('disable checked item', () => {
+ const description = `Tasks
+
+1. [ ] item 1
+ 1. [x] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+ const sourcepos = '4:4-4:14';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [~] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+
+ expect(disableTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ });
+ });
+ });
+
+ describe('enableTaskListItem', () => {
+ it('enable item', () => {
+ const description = `Tasks
+
+1. [ ] item 1
+ 1. [~] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+ const sourcepos = '4:4-4:14';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6`;
+
+ expect(enableTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ });
+ });
+ });
+
describe('extractTaskTitleAndDescription', () => {
const description = `A multi-line
description`;