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
7aea067a
Commit
7aea067a
authored
Apr 13, 2021
by
Florie Guibert
Committed by
Natalia Tepluhina
Apr 13, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Due date issue sidebar widget [RUN AS-IF-FOSS]
parent
07858743
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
392 additions
and
42 deletions
+392
-42
app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
...s/sidebar/components/due_date/sidebar_due_date_widget.vue
+203
-0
app/assets/javascripts/sidebar/constants.js
app/assets/javascripts/sidebar/constants.js
+9
-0
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+32
-0
app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
.../javascripts/sidebar/queries/issue_due_date.query.graphql
+10
-0
app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
...ts/sidebar/queries/update_issue_due_date.mutation.graphql
+9
-0
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+1
-35
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/features/issues/user_edits_issue_spec.rb
spec/features/issues/user_edits_issue_spec.rb
+6
-7
spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js
...debar/components/due_date/sidebar_due_date_widget_spec.js
+106
-0
spec/frontend/sidebar/mock_data.js
spec/frontend/sidebar/mock_data.js
+13
-0
No files found.
app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
0 → 100644
View file @
7aea067a
<
script
>
import
{
GlButton
,
GlIcon
,
GlDatepicker
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
dateInWords
,
formatDate
,
parsePikadayDate
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
dueDateQueries
}
from
'
~/sidebar/constants
'
;
const
hideDropdownEvent
=
new
CustomEvent
(
'
hiddenGlDropdown
'
,
{
bubbles
:
true
,
});
export
default
{
tracking
:
{
event
:
'
click_edit_button
'
,
label
:
'
right_sidebar
'
,
property
:
'
dueDate
'
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
components
:
{
GlButton
,
GlIcon
,
GlDatepicker
,
SidebarEditableItem
,
},
inject
:
[
'
fullPath
'
,
'
iid
'
,
'
canUpdate
'
],
props
:
{
issuableType
:
{
required
:
true
,
type
:
String
,
},
},
data
()
{
return
{
dueDate
:
null
,
loading
:
false
,
};
},
apollo
:
{
dueDate
:
{
query
()
{
return
dueDateQueries
[
this
.
issuableType
].
query
;
},
variables
()
{
return
{
fullPath
:
this
.
fullPath
,
iid
:
String
(
this
.
iid
),
};
},
update
(
data
)
{
return
data
.
workspace
?.
issuable
?.
dueDate
||
null
;
},
result
({
data
})
{
this
.
$emit
(
'
dueDateUpdated
'
,
data
.
workspace
?.
issuable
?.
dueDate
);
},
error
()
{
createFlash
({
message
:
sprintf
(
__
(
'
Something went wrong while setting %{issuableType} due date.
'
),
{
issuableType
:
this
.
issuableType
,
}),
});
},
},
},
computed
:
{
isLoading
()
{
return
this
.
$apollo
.
queries
.
dueDate
.
loading
||
this
.
loading
;
},
hasDueDate
()
{
return
this
.
dueDate
!==
null
;
},
parsedDueDate
()
{
if
(
!
this
.
hasDueDate
)
{
return
null
;
}
return
parsePikadayDate
(
this
.
dueDate
);
},
formattedDueDate
()
{
if
(
!
this
.
hasDueDate
)
{
return
this
.
$options
.
i18n
.
noDueDate
;
}
return
dateInWords
(
this
.
parsedDueDate
,
true
);
},
workspacePath
()
{
return
this
.
issuableType
===
IssuableType
.
Issue
?
{
projectPath
:
this
.
fullPath
,
}
:
{
groupPath
:
this
.
fullPath
,
};
},
},
methods
:
{
closeForm
()
{
this
.
$refs
.
editable
.
collapse
();
this
.
$el
.
dispatchEvent
(
hideDropdownEvent
);
this
.
$emit
(
'
closeForm
'
);
},
openDatePicker
()
{
this
.
$refs
.
datePicker
.
calendar
.
show
();
},
setDueDate
(
date
)
{
this
.
loading
=
true
;
this
.
$refs
.
editable
.
collapse
();
this
.
$apollo
.
mutate
({
mutation
:
dueDateQueries
[
this
.
issuableType
].
mutation
,
variables
:
{
input
:
{
...
this
.
workspacePath
,
iid
:
this
.
iid
,
dueDate
:
date
?
formatDate
(
date
,
'
yyyy-mm-dd
'
)
:
null
,
},
},
})
.
then
(
({
data
:
{
issuableSetDueDate
:
{
errors
},
},
})
=>
{
if
(
errors
.
length
)
{
createFlash
({
message
:
errors
[
0
],
});
}
else
{
this
.
$emit
(
'
closeForm
'
);
}
},
)
.
catch
(()
=>
{
createFlash
({
message
:
sprintf
(
__
(
'
Something went wrong while setting %{issuableType} due date.
'
),
{
issuableType
:
this
.
issuableType
,
}),
});
})
.
finally
(()
=>
{
this
.
loading
=
false
;
});
},
},
i18n
:
{
dueDate
:
__
(
'
Due date
'
),
noDueDate
:
__
(
'
None
'
),
removeDueDate
:
__
(
'
remove due date
'
),
},
};
</
script
>
<
template
>
<sidebar-editable-item
ref=
"editable"
:title=
"$options.i18n.dueDate"
:tracking=
"$options.tracking"
:loading=
"isLoading"
class=
"block"
data-testid=
"due-date"
@
open=
"openDatePicker"
>
<template
#collapsed
>
<div
v-gl-tooltip
:title=
"$options.i18n.dueDate"
class=
"sidebar-collapsed-icon"
>
<gl-icon
:size=
"16"
name=
"calendar"
/>
<span
class=
"collapse-truncated-title"
>
{{
formattedDueDate
}}
</span>
</div>
<div
class=
"gl-display-flex gl-align-items-center hide-collapsed"
>
<span
:class=
"hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid=
"sidebar-duedate-value"
>
{{
formattedDueDate
}}
</span>
<div
v-if=
"hasDueDate && canUpdate"
class=
"gl-display-flex"
>
<span
class=
"gl-px-2"
>
-
</span>
<gl-button
variant=
"link"
class=
"gl-text-gray-500!"
data-testid=
"reset-button"
:disabled=
"isLoading"
@
click=
"setDueDate(null)"
>
{{
$options
.
i18n
.
removeDueDate
}}
</gl-button>
</div>
</div>
</
template
>
<
template
#default
>
<gl-datepicker
ref=
"datePicker"
:value=
"parsedDueDate"
show-clear-button
@
input=
"setDueDate"
@
clear=
"setDueDate(null)"
/>
</
template
>
</sidebar-editable-item>
</template>
app/assets/javascripts/sidebar/constants.js
View file @
7aea067a
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
epicConfidentialQuery
from
'
~/sidebar/queries/epic_confidential.query.graphql
'
;
import
issueConfidentialQuery
from
'
~/sidebar/queries/issue_confidential.query.graphql
'
;
import
issueDueDateQuery
from
'
~/sidebar/queries/issue_due_date.query.graphql
'
;
import
issueReferenceQuery
from
'
~/sidebar/queries/issue_reference.query.graphql
'
;
import
mergeRequestReferenceQuery
from
'
~/sidebar/queries/merge_request_reference.query.graphql
'
;
import
updateEpicMutation
from
'
~/sidebar/queries/update_epic_confidential.mutation.graphql
'
;
import
updateIssueConfidentialMutation
from
'
~/sidebar/queries/update_issue_confidential.mutation.graphql
'
;
import
updateIssueDueDateMutation
from
'
~/sidebar/queries/update_issue_due_date.mutation.graphql
'
;
import
getIssueParticipants
from
'
~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
'
;
import
getMergeRequestParticipants
from
'
~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
'
;
import
updateAssigneesMutation
from
'
~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
'
;
...
...
@@ -42,3 +44,10 @@ export const referenceQueries = {
query
:
mergeRequestReferenceQuery
,
},
};
export
const
dueDateQueries
=
{
[
IssuableType
.
Issue
]:
{
query
:
issueDueDateQuery
,
mutation
:
updateIssueDueDateMutation
,
},
};
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
7aea067a
...
...
@@ -11,6 +11,7 @@ import {
}
from
'
~/lib/utils/common_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
SidebarConfidentialityWidget
from
'
~/sidebar/components/confidential/sidebar_confidentiality_widget.vue
'
;
import
SidebarDueDateWidget
from
'
~/sidebar/components/due_date/sidebar_due_date_widget.vue
'
;
import
SidebarReferenceWidget
from
'
~/sidebar/components/reference/sidebar_reference_widget.vue
'
;
import
{
apolloProvider
}
from
'
~/sidebar/graphql
'
;
import
Translate
from
'
../vue_shared/translate
'
;
...
...
@@ -168,6 +169,36 @@ function mountConfidentialComponent() {
});
}
function
mountDueDateComponent
()
{
const
el
=
document
.
getElementById
(
'
js-due-date-entry-point
'
);
if
(
!
el
)
{
return
;
}
const
{
fullPath
,
iid
,
editable
}
=
getSidebarOptions
();
// eslint-disable-next-line no-new
new
Vue
({
el
,
apolloProvider
,
components
:
{
SidebarDueDateWidget
,
},
provide
:
{
iid
:
String
(
iid
),
fullPath
,
canUpdate
:
editable
,
},
render
:
(
createElement
)
=>
createElement
(
'
sidebar-due-date-widget
'
,
{
props
:
{
issuableType
:
IssuableType
.
Issue
,
},
}),
});
}
function
mountReferenceComponent
()
{
const
el
=
document
.
getElementById
(
'
js-reference-entry-point
'
);
if
(
!
el
)
{
...
...
@@ -345,6 +376,7 @@ export function mountSidebar(mediator) {
mountAssigneesComponent
(
mediator
);
mountReviewersComponent
(
mediator
);
mountConfidentialComponent
(
mediator
);
mountDueDateComponent
(
mediator
);
mountReferenceComponent
(
mediator
);
mountLockComponent
();
mountParticipantsComponent
(
mediator
);
...
...
app/assets/javascripts/sidebar/queries/issue_due_date.query.graphql
0 → 100644
View file @
7aea067a
query
issueDueDate
(
$fullPath
:
ID
!,
$iid
:
String
)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
__typename
issuable
:
issue
(
iid
:
$iid
)
{
__typename
id
dueDate
}
}
}
app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
0 → 100644
View file @
7aea067a
mutation
updateIssueDueDate
(
$input
:
UpdateIssueInput
!)
{
issuableSetDueDate
:
updateIssue
(
input
:
$input
)
{
issuable
:
issue
{
id
dueDate
}
errors
}
}
app/views/shared/issuable/_sidebar.html.haml
View file @
7aea067a
...
...
@@ -70,41 +70,7 @@
=
_
(
'Time tracking'
)
=
loading_icon
(
css_class:
'gl-vertical-align-text-bottom'
)
-
if
issuable_sidebar
.
has_key?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon.has-tooltip
{
data:
{
placement:
'left'
,
container:
'body'
,
html:
'true'
,
boundary:
'viewport'
},
title:
sidebar_due_date_tooltip_label
(
issuable_sidebar
[
:due_date
])
}
=
sprite_icon
(
'calendar'
)
%span
.js-due-date-sidebar-value
=
issuable_sidebar
[
:due_date
].
try
(
:to_s
,
:medium
)
||
_
(
'None'
)
.title.hide-collapsed
=
_
(
'Due date'
)
=
loading_icon
(
css_class:
'gl-vertical-align-text-bottom hidden block-loading'
)
-
if
can_edit_issuable
=
link_to
_
(
'Edit'
),
'#'
,
class:
'js-sidebar-dropdown-toggle edit-link float-right'
,
data:
{
track_label:
"right_sidebar"
,
track_property:
"due_date"
,
track_event:
"click_edit_button"
,
track_value:
""
}
.value.hide-collapsed
%span
.value-content
-
if
issuable_sidebar
[
:due_date
]
%span
.bold
=
issuable_sidebar
[
:due_date
].
to_s
(
:medium
)
-
else
%span
.no-value
=
_
(
'None'
)
-
if
can_edit_issuable
%span
.no-value.js-remove-due-date-holder
{
class:
(
"hidden"
if
issuable_sidebar
[
:due_date
].
nil?
)
}
\-
%a
.js-remove-due-date
{
href:
"#"
,
role:
"button"
}
=
_
(
'remove due date'
)
-
if
can_edit_issuable
.selectbox.hide-collapsed
=
f
.
hidden_field
:due_date
,
value:
issuable_sidebar
[
:due_date
].
try
(
:strftime
,
'yy-mm-dd'
)
.dropdown
%button
.dropdown-menu-toggle.js-due-date-select
{
type:
'button'
,
data:
{
toggle:
'dropdown'
,
field_name:
"#{issuable_type}[due_date]"
,
ability_name:
issuable_type
,
issue_update:
issuable_sidebar
[
:issuable_json_path
],
display:
'static'
}
}
%span
.dropdown-toggle-text
=
_
(
'Due date'
)
=
sprite_icon
(
'chevron-down'
,
css_class:
"dropdown-menu-toggle-icon gl-top-3"
)
.dropdown-menu.dropdown-menu-due-date
=
dropdown_title
(
_
(
'Due date'
))
=
dropdown_content
do
.js-due-date-calendar
#js-due-date-entry-point
.js-sidebar-labels
{
data:
sidebar_labels_data
(
issuable_sidebar
,
@project
)
}
...
...
locale/gitlab.pot
View file @
7aea067a
...
...
@@ -29041,6 +29041,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while setting %{issuableType} due date."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
...
...
spec/features/issues/user_edits_issue_spec.rb
View file @
7aea067a
...
...
@@ -324,24 +324,23 @@ RSpec.describe "Issues > User edits issue", :js do
it
'adds due date to issue'
do
date
=
Date
.
today
.
at_beginning_of_month
+
2
.
days
page
.
within
'.due_date'
do
click_link
'Edit'
page
.
within
'[data-testid="due-date"]'
do
click_button
'Edit'
page
.
within
'.pika-single'
do
click_button
date
.
day
end
wait_for_requests
expect
(
find
(
'
.value
'
).
text
).
to
have_content
date
.
strftime
(
'%b %-d, %Y'
)
expect
(
find
(
'
[data-testid="sidebar-duedate-value"]
'
).
text
).
to
have_content
date
.
strftime
(
'%b %-d, %Y'
)
end
end
it
'removes due date from issue'
do
date
=
Date
.
today
.
at_beginning_of_month
+
2
.
days
page
.
within
'
.due_date
'
do
click_
link
'Edit'
page
.
within
'
[data-testid="due-date"]
'
do
click_
button
'Edit'
page
.
within
'.pika-single'
do
click_button
date
.
day
...
...
@@ -351,7 +350,7 @@ RSpec.describe "Issues > User edits issue", :js do
expect
(
page
).
to
have_no_content
'None'
click_
link
'remove due date'
click_
button
'remove due date'
expect
(
page
).
to
have_content
'None'
end
end
...
...
spec/frontend/sidebar/components/due_date/sidebar_due_date_widget_spec.js
0 → 100644
View file @
7aea067a
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
createFlash
from
'
~/flash
'
;
import
SidebarDueDateWidget
from
'
~/sidebar/components/due_date/sidebar_due_date_widget.vue
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
issueDueDateQuery
from
'
~/sidebar/queries/issue_due_date.query.graphql
'
;
import
{
issueDueDateResponse
}
from
'
../../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
Vue
.
use
(
VueApollo
);
describe
(
'
Sidebar Due date Widget
'
,
()
=>
{
let
wrapper
;
let
fakeApollo
;
const
date
=
'
2021-04-15
'
;
const
findEditableItem
=
()
=>
wrapper
.
findComponent
(
SidebarEditableItem
);
const
findFormattedDueDate
=
()
=>
wrapper
.
find
(
"
[data-testid='sidebar-duedate-value']
"
);
const
createComponent
=
({
dueDateQueryHandler
=
jest
.
fn
().
mockResolvedValue
(
issueDueDateResponse
()),
}
=
{})
=>
{
fakeApollo
=
createMockApollo
([[
issueDueDateQuery
,
dueDateQueryHandler
]]);
wrapper
=
shallowMount
(
SidebarDueDateWidget
,
{
apolloProvider
:
fakeApollo
,
provide
:
{
fullPath
:
'
group/project
'
,
iid
:
'
1
'
,
canUpdate
:
true
,
},
propsData
:
{
issuableType
:
'
issue
'
,
},
stubs
:
{
SidebarEditableItem
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
fakeApollo
=
null
;
});
it
(
'
passes a `loading` prop as true to editable item when query is loading
'
,
()
=>
{
createComponent
();
expect
(
findEditableItem
().
props
(
'
loading
'
)).
toBe
(
true
);
});
describe
(
'
when issue has no due date
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
({
dueDateQueryHandler
:
jest
.
fn
().
mockResolvedValue
(
issueDueDateResponse
(
null
)),
});
await
waitForPromises
();
});
it
(
'
passes a `loading` prop as false to editable item
'
,
()
=>
{
expect
(
findEditableItem
().
props
(
'
loading
'
)).
toBe
(
false
);
});
it
(
'
dueDate is null by default
'
,
()
=>
{
expect
(
findFormattedDueDate
().
text
()).
toBe
(
'
None
'
);
});
it
(
'
emits `dueDateUpdated` event with a `null` payload
'
,
()
=>
{
expect
(
wrapper
.
emitted
(
'
dueDateUpdated
'
)).
toEqual
([[
null
]]);
});
});
describe
(
'
when issue has due date
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
({
dueDateQueryHandler
:
jest
.
fn
().
mockResolvedValue
(
issueDueDateResponse
(
date
)),
});
await
waitForPromises
();
});
it
(
'
passes a `loading` prop as false to editable item
'
,
()
=>
{
expect
(
findEditableItem
().
props
(
'
loading
'
)).
toBe
(
false
);
});
it
(
'
has dueDate
'
,
()
=>
{
expect
(
findFormattedDueDate
().
text
()).
toBe
(
'
Apr 15, 2021
'
);
});
it
(
'
emits `dueDateUpdated` event with the date payload
'
,
()
=>
{
expect
(
wrapper
.
emitted
(
'
dueDateUpdated
'
)).
toEqual
([[
date
]]);
});
});
it
(
'
displays a flash message when query is rejected
'
,
async
()
=>
{
createComponent
({
dueDateQueryHandler
:
jest
.
fn
().
mockRejectedValue
(
'
Houston, we have a problem
'
),
});
await
waitForPromises
();
expect
(
createFlash
).
toHaveBeenCalled
();
});
});
spec/frontend/sidebar/mock_data.js
View file @
7aea067a
...
...
@@ -233,6 +233,19 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
export
const
issueDueDateResponse
=
(
dueDate
=
null
)
=>
({
data
:
{
workspace
:
{
__typename
:
'
Project
'
,
issuable
:
{
__typename
:
'
Issue
'
,
id
:
'
gid://gitlab/Issue/4
'
,
dueDate
,
},
},
},
});
export
const
issueReferenceResponse
=
(
reference
)
=>
({
data
:
{
workspace
:
{
...
...
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