Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
55fa10f8
Commit
55fa10f8
authored
Dec 08, 2021
by
Kushal Pandya
Committed by
Natalia Tepluhina
Dec 08, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for editing feature title
parent
6ba31f27
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
283 additions
and
21 deletions
+283
-21
app/assets/javascripts/work_items/components/item_title.vue
app/assets/javascripts/work_items/components/item_title.vue
+71
-0
app/assets/javascripts/work_items/graphql/resolvers.js
app/assets/javascripts/work_items/graphql/resolvers.js
+25
-0
app/assets/javascripts/work_items/graphql/typedefs.graphql
app/assets/javascripts/work_items/graphql/typedefs.graphql
+10
-0
app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
...ipts/work_items/graphql/update_work_item.mutation.graphql
+18
-0
app/assets/javascripts/work_items/pages/create_work_item.vue
app/assets/javascripts/work_items/pages/create_work_item.vue
+7
-9
app/assets/javascripts/work_items/pages/work_item_root.vue
app/assets/javascripts/work_items/pages/work_item_root.vue
+33
-5
app/assets/stylesheets/framework/common.scss
app/assets/stylesheets/framework/common.scss
+7
-0
locale/gitlab.pot
locale/gitlab.pot
+4
-1
spec/frontend/work_items/components/item_title_spec.js
spec/frontend/work_items/components/item_title_spec.js
+56
-0
spec/frontend/work_items/mock_data.js
spec/frontend/work_items/mock_data.js
+19
-0
spec/frontend/work_items/pages/create_work_item_spec.js
spec/frontend/work_items/pages/create_work_item_spec.js
+5
-3
spec/frontend/work_items/pages/work_item_root_spec.js
spec/frontend/work_items/pages/work_item_root_spec.js
+28
-3
No files found.
app/assets/javascripts/work_items/components/item_title.vue
0 → 100644
View file @
55fa10f8
<
script
>
import
{
escape
}
from
'
lodash
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
props
:
{
initialTitle
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
placeholder
:
{
type
:
String
,
required
:
false
,
default
:
__
(
'
Add a title...
'
),
},
disabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
title
:
this
.
initialTitle
,
};
},
methods
:
{
getSanitizedTitle
(
inputEl
)
{
const
{
innerText
}
=
inputEl
;
return
escape
(
innerText
);
},
handleBlur
({
target
})
{
this
.
$emit
(
'
title-changed
'
,
this
.
getSanitizedTitle
(
target
));
},
handleInput
({
target
})
{
this
.
$emit
(
'
title-input
'
,
this
.
getSanitizedTitle
(
target
));
},
handleSubmit
()
{
this
.
$refs
.
titleEl
.
blur
();
},
},
};
</
script
>
<
template
>
<h2
class=
"gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
:class=
"
{ 'gl-cursor-not-allowed': disabled }"
data-testid="title"
aria-labelledby="item-title"
>
<span
id=
"item-title"
ref=
"titleEl"
role=
"textbox"
:aria-label=
"__('Title')"
:data-placeholder=
"placeholder"
:contenteditable=
"!disabled"
class=
"gl-pseudo-placeholder"
@
blur=
"handleBlur"
@
keyup=
"handleInput"
@
keydown.enter.exact=
"handleSubmit"
@
keydown
.
ctrl
.
u
.
prevent
@
keydown
.
meta
.
u
.
prevent
@
keydown
.
ctrl
.
b
.
prevent
@
keydown
.
meta
.
b
.
prevent
>
{{
title
}}
</span
>
</h2>
</
template
>
app/assets/javascripts/work_items/graphql/resolvers.js
View file @
55fa10f8
...
@@ -29,5 +29,30 @@ export const resolvers = {
...
@@ -29,5 +29,30 @@ export const resolvers = {
workItem
,
workItem
,
};
};
},
},
updateWorkItem
(
_
,
{
input
},
{
cache
})
{
const
workItemTitle
=
{
__typename
:
'
TitleWidget
'
,
type
:
'
TITLE
'
,
enabled
:
true
,
contentText
:
input
.
title
,
};
const
workItem
=
{
__typename
:
'
WorkItem
'
,
type
:
'
FEATURE
'
,
id
:
input
.
id
,
widgets
:
{
__typename
:
'
WorkItemWidgetConnection
'
,
nodes
:
[
workItemTitle
],
},
};
cache
.
writeQuery
({
query
:
workItemQuery
,
variables
:
{
id
:
input
.
id
},
data
:
{
workItem
}
});
return
{
__typename
:
'
UpdateWorkItemPayload
'
,
workItem
,
};
},
},
},
};
};
app/assets/javascripts/work_items/graphql/typedefs.graphql
View file @
55fa10f8
...
@@ -37,14 +37,24 @@ type CreateWorkItemInput {
...
@@ -37,14 +37,24 @@ type CreateWorkItemInput {
title
:
String
!
title
:
String
!
}
}
type
UpdateWorkItemInput
{
id
:
ID
!
title
:
String
}
type
CreateWorkItemPayload
{
type
CreateWorkItemPayload
{
workItem
:
WorkItem
!
workItem
:
WorkItem
!
}
}
type
UpdateWorkItemPayload
{
workItem
:
WorkItem
!
}
extend
type
Query
{
extend
type
Query
{
workItem
(
id
:
ID
!):
WorkItem
!
workItem
(
id
:
ID
!):
WorkItem
!
}
}
extend
type
Mutation
{
extend
type
Mutation
{
createWorkItem
(
input
:
CreateWorkItemInput
!):
CreateWorkItemPayload
!
createWorkItem
(
input
:
CreateWorkItemInput
!):
CreateWorkItemPayload
!
updateWorkItem
(
input
:
UpdateWorkItemInput
!):
UpdateWorkItemPayload
!
}
}
app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
0 → 100644
View file @
55fa10f8
#import './widget.fragment.graphql'
mutation
updateWorkItem
(
$input
:
UpdateWorkItemInput
)
{
updateWorkItem
(
input
:
$input
)
@client
{
workItem
{
id
type
widgets
{
nodes
{
...
WidgetBase
...
on
TitleWidget
{
contentText
}
}
}
}
}
}
app/assets/javascripts/work_items/pages/create_work_item.vue
View file @
55fa10f8
...
@@ -2,10 +2,13 @@
...
@@ -2,10 +2,13 @@
import
{
GlButton
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
createWorkItemMutation
from
'
../graphql/create_work_item.mutation.graphql
'
;
import
createWorkItemMutation
from
'
../graphql/create_work_item.mutation.graphql
'
;
import
ItemTitle
from
'
../components/item_title.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
GlButton
,
GlButton
,
GlAlert
,
GlAlert
,
ItemTitle
,
},
},
data
()
{
data
()
{
return
{
return
{
...
@@ -37,6 +40,9 @@ export default {
...
@@ -37,6 +40,9 @@ export default {
this
.
error
=
true
;
this
.
error
=
true
;
}
}
},
},
handleTitleInput
(
title
)
{
this
.
title
=
title
;
},
},
},
};
};
</
script
>
</
script
>
...
@@ -46,15 +52,7 @@ export default {
...
@@ -46,15 +52,7 @@ export default {
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = false"
>
{{
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = false"
>
{{
__
(
'
Something went wrong when creating a work item. Please try again
'
)
__
(
'
Something went wrong when creating a work item. Please try again
'
)
}}
</gl-alert>
}}
</gl-alert>
<label
for=
"title"
class=
"gl-sr-only"
>
{{
__
(
'
Title
'
)
}}
</label>
<item-title
data-testid=
"title-input"
@
title-input=
"handleTitleInput"
/>
<input
id=
"title"
v-model.trim=
"title"
type=
"text"
class=
"gl-font-size-h-display gl-font-weight-bold gl-my-5 gl-border-none gl-w-full gl-pl-2"
data-testid=
"title-input"
:placeholder=
"__('Add a title…')"
/>
<div
class=
"gl-bg-gray-10 gl-py-5 gl-px-6"
>
<div
class=
"gl-bg-gray-10 gl-py-5 gl-px-6"
>
<gl-button
<gl-button
variant=
"confirm"
variant=
"confirm"
...
...
app/assets/javascripts/work_items/pages/work_item_root.vue
View file @
55fa10f8
<
script
>
<
script
>
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
workItemQuery
from
'
../graphql/work_item.query.graphql
'
;
import
workItemQuery
from
'
../graphql/work_item.query.graphql
'
;
import
updateWorkItemMutation
from
'
../graphql/update_work_item.mutation.graphql
'
;
import
{
widgetTypes
}
from
'
../constants
'
;
import
{
widgetTypes
}
from
'
../constants
'
;
import
ItemTitle
from
'
../components/item_title.vue
'
;
export
default
{
export
default
{
components
:
{
ItemTitle
,
GlAlert
,
},
props
:
{
props
:
{
id
:
{
id
:
{
type
:
String
,
type
:
String
,
...
@@ -12,6 +20,7 @@ export default {
...
@@ -12,6 +20,7 @@ export default {
data
()
{
data
()
{
return
{
return
{
workItem
:
null
,
workItem
:
null
,
error
:
false
,
};
};
},
},
apollo
:
{
apollo
:
{
...
@@ -29,20 +38,39 @@ export default {
...
@@ -29,20 +38,39 @@ export default {
return
this
.
workItem
?.
widgets
?.
nodes
?.
find
((
widget
)
=>
widget
.
type
===
widgetTypes
.
title
);
return
this
.
workItem
?.
widgets
?.
nodes
?.
find
((
widget
)
=>
widget
.
type
===
widgetTypes
.
title
);
},
},
},
},
methods
:
{
async
updateWorkItem
(
title
)
{
try
{
await
this
.
$apollo
.
mutate
({
mutation
:
updateWorkItemMutation
,
variables
:
{
input
:
{
id
:
this
.
id
,
title
,
},
},
});
}
catch
{
this
.
error
=
true
;
}
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<section>
<section>
<gl-alert
v-if=
"error"
variant=
"danger"
@
dismiss=
"error = false"
>
{{
__
(
'
Something went wrong while updating work item. Please try again
'
)
}}
</gl-alert>
<!-- Title widget placeholder -->
<!-- Title widget placeholder -->
<div>
<div>
<
h2
<
item-title
v-if=
"titleWidgetData"
v-if=
"titleWidgetData"
class=
"gl-font-weight-normal gl-sm-font-weight-bold gl-my-5
"
:initial-title=
"titleWidgetData.contentText
"
data-testid=
"title"
data-testid=
"title"
>
@
title-changed=
"updateWorkItem"
{{
titleWidgetData
.
contentText
}}
/>
</h2>
</div>
</div>
</section>
</section>
</
template
>
</
template
>
app/assets/stylesheets/framework/common.scss
View file @
55fa10f8
...
@@ -479,6 +479,13 @@ img.emoji {
...
@@ -479,6 +479,13 @@ img.emoji {
border-top
:
1px
solid
$border-color
;
border-top
:
1px
solid
$border-color
;
}
}
.gl-pseudo-placeholder
:empty::before
{
content
:
attr
(
data-placeholder
);
font-weight
:
$gl-font-weight-normal
;
color
:
$gl-text-color-secondary
;
cursor
:
text
;
}
/**
/**
🚨 Do not use these classes — they clash with the Gitlab UI design system and will be removed. 🚨
🚨 Do not use these classes — they clash with the Gitlab UI design system and will be removed. 🚨
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
...
...
locale/gitlab.pot
View file @
55fa10f8
...
@@ -2036,7 +2036,7 @@ msgstr ""
...
@@ -2036,7 +2036,7 @@ msgstr ""
msgid "Add a task list"
msgid "Add a task list"
msgstr ""
msgstr ""
msgid "Add a title
…
"
msgid "Add a title
...
"
msgstr ""
msgstr ""
msgid "Add a to do"
msgid "Add a to do"
...
@@ -32840,6 +32840,9 @@ msgstr ""
...
@@ -32840,6 +32840,9 @@ msgstr ""
msgid "Something went wrong while updating assignees"
msgid "Something went wrong while updating assignees"
msgstr ""
msgstr ""
msgid "Something went wrong while updating work item. Please try again"
msgstr ""
msgid "Something went wrong while updating your list settings"
msgid "Something went wrong while updating your list settings"
msgstr ""
msgstr ""
...
...
spec/frontend/work_items/components/item_title_spec.js
0 → 100644
View file @
55fa10f8
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
escape
}
from
'
lodash
'
;
import
ItemTitle
from
'
~/work_items/components/item_title.vue
'
;
jest
.
mock
(
'
lodash/escape
'
,
()
=>
jest
.
fn
((
fn
)
=>
fn
));
const
createComponent
=
({
initialTitle
=
'
Sample title
'
,
disabled
=
false
}
=
{})
=>
shallowMount
(
ItemTitle
,
{
propsData
:
{
initialTitle
,
disabled
,
},
});
describe
(
'
ItemTitle
'
,
()
=>
{
let
wrapper
;
const
mockUpdatedTitle
=
'
Updated title
'
;
const
findInputEl
=
()
=>
wrapper
.
find
(
'
span#item-title
'
);
beforeEach
(()
=>
{
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders title contents
'
,
()
=>
{
expect
(
findInputEl
().
attributes
()).
toMatchObject
({
'
data-placeholder
'
:
'
Add a title...
'
,
contenteditable
:
'
true
'
,
});
expect
(
findInputEl
().
text
()).
toBe
(
'
Sample title
'
);
});
it
(
'
renders title contents with editing disabled
'
,
()
=>
{
wrapper
=
createComponent
({
disabled
:
true
,
});
expect
(
wrapper
.
classes
()).
toContain
(
'
gl-cursor-not-allowed
'
);
expect
(
findInputEl
().
attributes
(
'
contenteditable
'
)).
toBe
(
'
false
'
);
});
it
.
each
`
eventName | sourceEvent
${
'
title-changed
'
}
|
${
'
blur
'
}
${
'
title-input
'
}
|
${
'
keyup
'
}
`
(
'
emits "$eventName" event on input $sourceEvent
'
,
async
({
eventName
,
sourceEvent
})
=>
{
findInputEl
().
element
.
innerText
=
mockUpdatedTitle
;
await
findInputEl
().
trigger
(
sourceEvent
);
expect
(
wrapper
.
emitted
(
eventName
)).
toBeTruthy
();
expect
(
escape
).
toHaveBeenCalledWith
(
mockUpdatedTitle
);
});
});
spec/frontend/work_items/mock_data.js
View file @
55fa10f8
...
@@ -15,3 +15,22 @@ export const workItemQueryResponse = {
...
@@ -15,3 +15,22 @@ export const workItemQueryResponse = {
},
},
},
},
};
};
export
const
updateWorkItemMutationResponse
=
{
__typename
:
'
UpdateWorkItemPayload
'
,
workItem
:
{
__typename
:
'
WorkItem
'
,
id
:
'
1
'
,
widgets
:
{
__typename
:
'
WorkItemWidgetConnection
'
,
nodes
:
[
{
__typename
:
'
TitleWidget
'
,
type
:
'
TITLE
'
,
enabled
:
true
,
contentText
:
'
Updated title
'
,
},
],
},
},
};
spec/frontend/work_items/pages/create_work_item_spec.js
View file @
55fa10f8
...
@@ -5,6 +5,7 @@ import { shallowMount } from '@vue/test-utils';
...
@@ -5,6 +5,7 @@ import { shallowMount } from '@vue/test-utils';
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
CreateWorkItem
from
'
~/work_items/pages/create_work_item.vue
'
;
import
CreateWorkItem
from
'
~/work_items/pages/create_work_item.vue
'
;
import
ItemTitle
from
'
~/work_items/components/item_title.vue
'
;
import
{
resolvers
}
from
'
~/work_items/graphql/resolvers
'
;
import
{
resolvers
}
from
'
~/work_items/graphql/resolvers
'
;
Vue
.
use
(
VueApollo
);
Vue
.
use
(
VueApollo
);
...
@@ -14,9 +15,9 @@ describe('Create work item component', () => {
...
@@ -14,9 +15,9 @@ describe('Create work item component', () => {
let
fakeApollo
;
let
fakeApollo
;
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findTitleInput
=
()
=>
wrapper
.
findComponent
(
ItemTitle
);
const
findCreateButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-button"]
'
);
const
findCreateButton
=
()
=>
wrapper
.
find
(
'
[data-testid="create-button"]
'
);
const
findCancelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="cancel-button"]
'
);
const
findCancelButton
=
()
=>
wrapper
.
find
(
'
[data-testid="cancel-button"]
'
);
const
findTitleInput
=
()
=>
wrapper
.
find
(
'
[data-testid="title-input"]
'
);
const
createComponent
=
({
data
=
{}
}
=
{})
=>
{
const
createComponent
=
({
data
=
{}
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([],
resolvers
);
fakeApollo
=
createMockApollo
([],
resolvers
);
...
@@ -70,9 +71,10 @@ describe('Create work item component', () => {
...
@@ -70,9 +71,10 @@ describe('Create work item component', () => {
});
});
describe
(
'
when title input field has a text
'
,
()
=>
{
describe
(
'
when title input field has a text
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(
async
()
=>
{
const
mockTitle
=
'
Test title
'
;
createComponent
();
createComponent
();
findTitleInput
().
setValue
(
'
Test title
'
);
await
findTitleInput
().
vm
.
$emit
(
'
title-input
'
,
mockTitle
);
});
});
it
(
'
renders a non-disabled Create button
'
,
()
=>
{
it
(
'
renders a non-disabled Create button
'
,
()
=>
{
...
...
spec/frontend/work_items/pages/work_item_root_spec.js
View file @
55fa10f8
...
@@ -2,8 +2,12 @@ import Vue from 'vue';
...
@@ -2,8 +2,12 @@ import Vue from 'vue';
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
workItemQuery
from
'
~/work_items/graphql/work_item.query.graphql
'
;
import
workItemQuery
from
'
~/work_items/graphql/work_item.query.graphql
'
;
import
updateWorkItemMutation
from
'
~/work_items/graphql/update_work_item.mutation.graphql
'
;
import
WorkItemsRoot
from
'
~/work_items/pages/work_item_root.vue
'
;
import
WorkItemsRoot
from
'
~/work_items/pages/work_item_root.vue
'
;
import
ItemTitle
from
'
~/work_items/components/item_title.vue
'
;
import
{
resolvers
}
from
'
~/work_items/graphql/resolvers
'
;
import
{
workItemQueryResponse
}
from
'
../mock_data
'
;
import
{
workItemQueryResponse
}
from
'
../mock_data
'
;
Vue
.
use
(
VueApollo
);
Vue
.
use
(
VueApollo
);
...
@@ -14,10 +18,10 @@ describe('Work items root component', () => {
...
@@ -14,10 +18,10 @@ describe('Work items root component', () => {
let
wrapper
;
let
wrapper
;
let
fakeApollo
;
let
fakeApollo
;
const
findTitle
=
()
=>
wrapper
.
find
(
'
[data-testid="title"]
'
);
const
findTitle
=
()
=>
wrapper
.
find
Component
(
ItemTitle
);
const
createComponent
=
({
queryResponse
=
workItemQueryResponse
}
=
{})
=>
{
const
createComponent
=
({
queryResponse
=
workItemQueryResponse
}
=
{})
=>
{
fakeApollo
=
createMockApollo
();
fakeApollo
=
createMockApollo
(
[],
resolvers
);
fakeApollo
.
clients
.
defaultClient
.
cache
.
writeQuery
({
fakeApollo
.
clients
.
defaultClient
.
cache
.
writeQuery
({
query
:
workItemQuery
,
query
:
workItemQuery
,
variables
:
{
variables
:
{
...
@@ -43,7 +47,28 @@ describe('Work items root component', () => {
...
@@ -43,7 +47,28 @@ describe('Work items root component', () => {
createComponent
();
createComponent
();
expect
(
findTitle
().
exists
()).
toBe
(
true
);
expect
(
findTitle
().
exists
()).
toBe
(
true
);
expect
(
findTitle
().
text
()).
toBe
(
'
Test
'
);
expect
(
findTitle
().
props
(
'
initialTitle
'
)).
toBe
(
'
Test
'
);
});
it
(
'
updates the title when it is edited
'
,
async
()
=>
{
createComponent
();
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
,
'
mutate
'
);
const
mockUpdatedTitle
=
'
Updated title
'
;
await
findTitle
().
vm
.
$emit
(
'
title-changed
'
,
mockUpdatedTitle
);
expect
(
wrapper
.
vm
.
$apollo
.
mutate
).
toHaveBeenCalledWith
({
mutation
:
updateWorkItemMutation
,
variables
:
{
input
:
{
id
:
WORK_ITEM_ID
,
title
:
mockUpdatedTitle
,
},
},
});
await
waitForPromises
();
expect
(
findTitle
().
props
(
'
initialTitle
'
)).
toBe
(
mockUpdatedTitle
);
});
});
it
(
'
does not render the title if title is not in the widgets list
'
,
()
=>
{
it
(
'
does not render the title if title is not in the widgets list
'
,
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment