diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 241b654df5c0f2ce3aa21fed91cefe921b4ccab3..f9da68b8a9c2af84721d3d543a5864790d66d282 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -163,6 +163,16 @@ export default {
required: false,
default: () => [],
},
+ eeTypeTokenOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ eeWorkItemTypes: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -239,14 +249,17 @@ export default {
state: this.state,
...this.pageParams,
...this.apiFilterParams,
- types: this.apiFilterParams.types || defaultWorkItemTypes,
+ types: this.apiFilterParams.types || this.defaultWorkItemTypes,
};
},
namespace() {
return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
},
+ defaultWorkItemTypes() {
+ return [...defaultWorkItemTypes, ...this.eeWorkItemTypes];
+ },
typeTokenOptions() {
- return defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION);
+ return [...defaultTypeTokenOptions, TYPE_TOKEN_TASK_OPTION, ...this.eeTypeTokenOptions];
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
@@ -872,7 +885,7 @@ export default {
{{ issuable.downvotes }}
-
+
@@ -908,6 +921,10 @@ export default {
:svg-path="emptyStateSvgPath"
/>
+
+
+
+
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 01aa9fd900931b739a729aff51d30e2238628e64..262aafdd0bf773f01c3c88c392db884e37a4ae1d 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -149,6 +149,11 @@ export const specialFilterValues = [
];
export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
+export const TYPE_TOKEN_OBJECTIVE_OPTION = {
+ icon: 'issue-type-issue',
+ title: 'objective',
+ value: 'objective',
+};
// This should be consistent with Issue::TYPES_FOR_LIST in the backend
// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 5e04dd1971cce65edf777e6d5aac32b6bdafe648..c5845eb8e4077eb9ab2c271ae0b8e6cd0d20d48d 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -77,6 +77,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature,
hasIterationsFeature,
hasScopedLabelsFeature,
+ hasOkrsFeature,
importCsvIssuesPath,
initialEmail,
initialSort,
@@ -127,6 +128,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ hasOkrsFeature: parseBoolean(hasOkrsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index dd3d7c8f4d6ed1f1380484f60cad3c360098d041..5b6c5bf6e0336159ad7db1d5b2930ae132197111 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -331,6 +331,7 @@ export default {
+
-
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 7fcbb8c07fec9346d2b03c684e262b63341a995b..582532b51fd77e1118e71205def227c1246d2f5a 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -27,7 +27,6 @@ export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK';
export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
-
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
@@ -120,6 +119,10 @@ export const WORK_ITEMS_TREE_TEXT_MAP = {
export const FORM_TYPES = {
create: 'create',
add: 'add',
+ [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
+ icon: `issue-type-issue`,
+ name: s__('WorkItem|Objective'),
+ },
};
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
diff --git a/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue b/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 223fc7055994ad86e83ca6fdfec5ca5b51591409..a293727ac45b137ce29d0d83a16331e00fb96558 100644
--- a/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/ee/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -1,6 +1,7 @@
-
-
+
+
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/work_items/components/create_work_item_objective.vue b/ee/app/assets/javascripts/work_items/components/create_work_item_objective.vue
new file mode 100644
index 0000000000000000000000000000000000000000..886066f5352f48cf1f67bf32f100cedc66efb5ca
--- /dev/null
+++ b/ee/app/assets/javascripts/work_items/components/create_work_item_objective.vue
@@ -0,0 +1,132 @@
+
+
+
+ {{ error }}
+
+
+
diff --git a/ee/spec/frontend/issues/list/components/issues_list_app_spec.js b/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
index c0eb82cc9fb19e8e44e4d7047e6f53cd379fa759..81f54f7dda6dae2121e9fe12577f0f1fe2e27cc3 100644
--- a/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/ee/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -9,7 +9,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { CREATED_DESC } from '~/issues/list/constants';
+import { CREATED_DESC, TYPE_TOKEN_OBJECTIVE_OPTION } from '~/issues/list/constants';
+import CEIssuesListApp from '~/issues/list/components/issues_list_app.vue';
+import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -52,6 +54,7 @@ describe('EE IssuesListApp component', () => {
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
hasScopedLabelsFeature: true,
+ hasOkrsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
isAnonymousSearchDisabled: false,
@@ -75,8 +78,11 @@ describe('EE IssuesListApp component', () => {
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findIssueListApp = () => wrapper.findComponent(CEIssuesListApp);
+
const mountComponent = ({
provide = {},
+ okrsMvc = false,
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
} = {}) => {
@@ -89,6 +95,9 @@ describe('EE IssuesListApp component', () => {
return mount(IssuesListApp, {
apolloProvider,
provide: {
+ glFeatures: {
+ okrsMvc,
+ },
...defaultProvide,
...provide,
},
@@ -113,6 +122,58 @@ describe('EE IssuesListApp component', () => {
});
});
+ describe('workItemTypes', () => {
+ describe.each`
+ hasOkrsFeature | okrsMvc | eeWorkItemTypes | message
+ ${false} | ${true} | ${[]} | ${'NOT include'}
+ ${true} | ${false} | ${[]} | ${'NOT include'}
+ ${true} | ${true} | ${[WORK_ITEM_TYPE_ENUM_OBJECTIVE]} | ${'include'}
+ `(
+ 'when hasOkrsFeature is "$hasOkrsFeature" and okrsMvc is "$okrsMvc"',
+ ({ hasOkrsFeature, okrsMvc, eeWorkItemTypes, message }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ hasOkrsFeature,
+ },
+ okrsMvc,
+ });
+ });
+
+ it(`should ${message} objective in work item types`, () => {
+ expect(findIssueListApp().props('eeWorkItemTypes')).toMatchObject(eeWorkItemTypes);
+ });
+ },
+ );
+ });
+
+ describe('typeTokenOptions', () => {
+ describe.each`
+ hasOkrsFeature | okrsMvc | eeWorkItemTypeTokens | message
+ ${false} | ${true} | ${[]} | ${'NOT include'}
+ ${true} | ${false} | ${[]} | ${'NOT include'}
+ ${true} | ${true} | ${[TYPE_TOKEN_OBJECTIVE_OPTION]} | ${'include'}
+ `(
+ 'when hasOkrsFeature is "$hasOkrsFeature" and okrsMvc is "$okrsMvc"',
+ ({ hasOkrsFeature, okrsMvc, eeWorkItemTypeTokens, message }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ hasOkrsFeature,
+ },
+ okrsMvc,
+ });
+ });
+
+ it(`should ${message} objective in type tokens`, () => {
+ expect(findIssueListApp().props('eeTypeTokenOptions')).toMatchObject(
+ eeWorkItemTypeTokens,
+ );
+ });
+ },
+ );
+ });
+
describe('tokens', () => {
const mockCurrentUser = {
id: 1,
diff --git a/ee/spec/frontend/work_items/components/create_work_item_objective_spec.js b/ee/spec/frontend/work_items/components/create_work_item_objective_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1bff2cb3121c03adae347fab717fa6527ac5872b
--- /dev/null
+++ b/ee/spec/frontend/work_items/components/create_work_item_objective_spec.js
@@ -0,0 +1,118 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert, GlFormInput } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import CreateWorkItemObjective from 'ee/work_items/components/create_work_item_objective.vue';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
+import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+describe('Create work item Objective component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
+ const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTitleInput = () => wrapper.findComponent(GlFormInput);
+ const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
+
+ const createComponent = ({
+ data = {},
+ props = {},
+ queryHandler = querySuccessHandler,
+ mutationHandler = createWorkItemSuccessHandler,
+ fetchByIid = false,
+ } = {}) => {
+ fakeApollo = createMockApollo(
+ [
+ [projectWorkItemTypesQuery, queryHandler],
+ [createWorkItemMutation, mutationHandler],
+ ],
+ {},
+ { typePolicies: { Project: { merge: true } } },
+ );
+ wrapper = shallowMount(CreateWorkItemObjective, {
+ apolloProvider: fakeApollo,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...props,
+ },
+ mocks: {
+ $router: {
+ go: jest.fn(),
+ },
+ },
+ provide: {
+ fullPath: 'full-path',
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ it('does not render error by default', () => {
+ createComponent();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('renders a disabled Create button when title input is empty', () => {
+ createComponent();
+
+ expect(findCreateButton().props('disabled')).toBe(true);
+ });
+
+ it('hides the alert on dismissing the error', async () => {
+ createComponent({ data: { error: true } });
+
+ expect(findAlert().exists()).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ describe('when title input field has a text', () => {
+ beforeEach(async () => {
+ const mockTitle = 'Test title';
+ createComponent();
+ await waitForPromises();
+ findTitleInput().vm.$emit('input', mockTitle);
+ });
+
+ it('renders a enabled Create button', () => {
+ expect(findCreateButton().props('disabled')).toBe(false);
+ });
+ });
+
+ it('shows an alert on mutation error', async () => {
+ createComponent({ mutationHandler: errorHandler });
+ await waitForPromises();
+
+ findTitleInput().vm.$emit('input', 'some title');
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(
+ 'Something went wrong when creating work item. Please try again.',
+ );
+ });
+});
diff --git a/ee/spec/frontend/work_items/mock_data.js b/ee/spec/frontend/work_items/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..a8bb2cf4330d98b2d4fc5b848e0a8db980f7665c
--- /dev/null
+++ b/ee/spec/frontend/work_items/mock_data.js
@@ -0,0 +1,50 @@
+export const projectWorkItemTypesQueryResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemTypes: {
+ nodes: [
+ { id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
+ { id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
+ { id: 'gid://gitlab/WorkItems::Type/3', name: 'Task' },
+ ],
+ },
+ },
+ },
+};
+
+export const createWorkItemMutationResponse = {
+ data: {
+ workItemCreate: {
+ __typename: 'WorkItemCreatePayload',
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ },
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
+ widgets: [],
+ },
+ errors: [],
+ },
+ },
+};
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3995ce97c92608956e99bc947e6c72d71809cd68..e396237cca5e6f02e32b83244a456e741f1c76be 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -46359,6 +46359,9 @@ msgstr ""
msgid "WorkItem|Collapse tasks"
msgstr ""
+msgid "WorkItem|Create Objective"
+msgstr ""
+
msgid "WorkItem|Create task"
msgstr ""