Display user-defined due dates colors
parent
b90f70d846
commit
1e56104e58
|
@ -113,8 +113,8 @@ class ProjectValuesController extends taiga.Controller
|
||||||
unwatch()
|
unwatch()
|
||||||
loadValues: =>
|
loadValues: =>
|
||||||
return @rs[@scope.resource].listValues(@scope.projectId, @scope.type).then (values) =>
|
return @rs[@scope.resource].listValues(@scope.projectId, @scope.type).then (values) =>
|
||||||
@scope.values = values
|
if values.length
|
||||||
@scope.maxValueOrder = _.maxBy(values, "order").order
|
@scope.maxValueOrder = _.maxBy(values, "order").order
|
||||||
return values
|
return values
|
||||||
|
|
||||||
moveValue: (ctx, itemValue, itemIndex) =>
|
moveValue: (ctx, itemValue, itemIndex) =>
|
||||||
|
@ -130,6 +130,46 @@ class ProjectValuesController extends taiga.Controller
|
||||||
module.controller("ProjectValuesController", ProjectValuesController)
|
module.controller("ProjectValuesController", ProjectValuesController)
|
||||||
|
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## Project due dates values Controller
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
class ProjectDueDatesValuesController extends ProjectValuesController
|
||||||
|
@.$inject = [
|
||||||
|
"$scope",
|
||||||
|
"$rootScope",
|
||||||
|
"$tgRepo",
|
||||||
|
"$tgConfirm",
|
||||||
|
"$tgResources",
|
||||||
|
]
|
||||||
|
|
||||||
|
loadValues: =>
|
||||||
|
return @rs[@scope.resource].listValues(@scope.projectId, @scope.type).then (values) =>
|
||||||
|
if values.length
|
||||||
|
@scope.maxValueOrder = _.maxBy(values, "order").order
|
||||||
|
@displayValues(values)
|
||||||
|
else
|
||||||
|
@createDefaultValues()
|
||||||
|
return values
|
||||||
|
|
||||||
|
createDefaultValues: =>
|
||||||
|
if !@rs[@scope.resource].createDefaultValues?
|
||||||
|
return
|
||||||
|
return @rs[@scope.resource].createDefaultValues(@scope.projectId, @scope.type).then (response) =>
|
||||||
|
values = response.data
|
||||||
|
if values.length
|
||||||
|
@scope.maxValueOrder = _.maxBy(values, "order").order
|
||||||
|
@displayValues(values)
|
||||||
|
return values
|
||||||
|
|
||||||
|
displayValues: (values) =>
|
||||||
|
_.each values, (value, index) ->
|
||||||
|
value.days_to_due_abs = if value.days_to_due != null then Math.abs(value.days_to_due) else null
|
||||||
|
value.sign = if value.days_to_due >= 0 then 1 else -1
|
||||||
|
@scope.values = values
|
||||||
|
|
||||||
|
module.controller("ProjectDueDatesValuesController", ProjectDueDatesValuesController)
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Project values directive
|
## Project values directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
@ -288,6 +328,11 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra
|
||||||
target = angular.element(event.currentTarget)
|
target = angular.element(event.currentTarget)
|
||||||
saveValue(target)
|
saveValue(target)
|
||||||
|
|
||||||
|
$el.on "click", ".save", (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
target = angular.element(event.currentTarget)
|
||||||
|
saveValue(target)
|
||||||
|
|
||||||
$el.on "click", ".cancel", (event) ->
|
$el.on "click", ".cancel", (event) ->
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
target = angular.element(event.currentTarget)
|
target = angular.element(event.currentTarget)
|
||||||
|
@ -332,6 +377,45 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra
|
||||||
module.directive("tgProjectValues", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame",
|
module.directive("tgProjectValues", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame",
|
||||||
"$translate", "$rootScope", "tgProjectService", ProjectValuesDirective])
|
"$translate", "$rootScope", "tgProjectService", ProjectValuesDirective])
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## Project due dates values directive
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
ProjectDueDatesValues = ($log, $repo, $confirm, $location, animationFrame, $translate, $rootscope, projectService) ->
|
||||||
|
parentDirective = ProjectValuesDirective($log, $repo, $confirm, $location, animationFrame,
|
||||||
|
$translate, $rootscope, projectService)
|
||||||
|
|
||||||
|
linkDueDateStatusValue = ($scope, $el, $attrs) ->
|
||||||
|
_setDaysToDue = (value) ->
|
||||||
|
value.days_to_due = value.days_to_due_abs * value.sign
|
||||||
|
|
||||||
|
_valueFromEventTarget = (event) ->
|
||||||
|
target = angular.element(event.currentTarget)
|
||||||
|
row = target.parents(".row.table-main")
|
||||||
|
formEl = target.parents("form")
|
||||||
|
return formEl.scope().value
|
||||||
|
|
||||||
|
$el.on "input", ".days-to-due-abs", (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
value = _valueFromEventTarget(event)
|
||||||
|
$scope.$apply ->
|
||||||
|
_setDaysToDue(value)
|
||||||
|
|
||||||
|
$el.on "click", ".days-to-due-sign", (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
value = _valueFromEventTarget(event)
|
||||||
|
$scope.$apply ->
|
||||||
|
value.sign = value.sign * -1
|
||||||
|
_setDaysToDue(value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
link: ($scope, $el, $attrs) ->
|
||||||
|
parentDirective.link($scope, $el, $attrs)
|
||||||
|
linkDueDateStatusValue($scope, $el, $attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.directive("tgProjectDueDatesValues", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame",
|
||||||
|
"$translate", "$rootScope", "tgProjectService", ProjectDueDatesValues])
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Color selection directive
|
## Color selection directive
|
||||||
|
|
|
@ -376,7 +376,7 @@ module.directive("tgLightboxClose", [LightboxClose])
|
||||||
|
|
||||||
Svg = () ->
|
Svg = () ->
|
||||||
template = """
|
template = """
|
||||||
<svg class="{{ 'icon ' + svgIcon }}">
|
<svg class="{{ 'icon ' + svgIcon }}" style="fill: {{ svgFill }}">
|
||||||
<use xlink:href="" ng-attr-xlink:href="{{ '#' + svgIcon }}">
|
<use xlink:href="" ng-attr-xlink:href="{{ '#' + svgIcon }}">
|
||||||
<title ng-if="svgTitle">{{svgTitle}}</title>
|
<title ng-if="svgTitle">{{svgTitle}}</title>
|
||||||
<title ng-if="svgTitleTranslate">{{svgTitleTranslate | translate: svgTitleTranslateValues}}</title>
|
<title ng-if="svgTitleTranslate">{{svgTitleTranslate | translate: svgTitleTranslateValues}}</title>
|
||||||
|
@ -389,7 +389,8 @@ Svg = () ->
|
||||||
svgIcon: "@",
|
svgIcon: "@",
|
||||||
svgTitle: "@",
|
svgTitle: "@",
|
||||||
svgTitleTranslate: "@",
|
svgTitleTranslate: "@",
|
||||||
svgTitleTranslateValues: "="
|
svgTitleTranslateValues: "=",
|
||||||
|
svgFill: "="
|
||||||
},
|
},
|
||||||
template: template
|
template: template
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,15 +82,18 @@ urls = {
|
||||||
"project-transfer-request": "/projects/%s/transfer_request"
|
"project-transfer-request": "/projects/%s/transfer_request"
|
||||||
"project-transfer-start": "/projects/%s/transfer_start"
|
"project-transfer-start": "/projects/%s/transfer_start"
|
||||||
|
|
||||||
# Project Values - Choises
|
# Project Values - Attributes
|
||||||
"epic-statuses": "/epic-statuses"
|
"epic-statuses": "/epic-statuses"
|
||||||
"userstory-statuses": "/userstory-statuses"
|
"userstory-statuses": "/userstory-statuses"
|
||||||
"userstory-due-date-statuses": "/userstory-duedates"
|
"userstory-due-dates": "/userstory-due-dates"
|
||||||
|
"userstory-due-dates-create-default": "/userstory-due-dates/create_default"
|
||||||
"points": "/points"
|
"points": "/points"
|
||||||
"task-statuses": "/task-statuses"
|
"task-statuses": "/task-statuses"
|
||||||
"task-due-date-statuses": "/task-due-date-statuses"
|
"task-due-dates": "/task-due-dates"
|
||||||
|
"task-due-dates-create-default": "/task-due-dates/create_default"
|
||||||
"issue-statuses": "/issue-statuses"
|
"issue-statuses": "/issue-statuses"
|
||||||
"issue-due-date-statuses": "/issue-due-date-statuses"
|
"issue-due-dates": "/issue-due-dates"
|
||||||
|
"issue-due-dates-create-default": "/issue-due-dates/create_default"
|
||||||
"issue-types": "/issue-types"
|
"issue-types": "/issue-types"
|
||||||
"priorities": "/priorities"
|
"priorities": "/priorities"
|
||||||
"severities": "/severities"
|
"severities": "/severities"
|
||||||
|
|
|
@ -89,6 +89,11 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||||
service.storeQueryParams(projectId, params)
|
service.storeQueryParams(projectId, params)
|
||||||
return $repo.queryMany(type, params)
|
return $repo.queryMany(type, params)
|
||||||
|
|
||||||
|
service.createDefaultValues = (projectId, type) ->
|
||||||
|
data = {"project_id": projectId}
|
||||||
|
url = $urls.resolve("#{type}-create-default")
|
||||||
|
return $http.post(url, data)
|
||||||
|
|
||||||
service.storeQueryParams = (projectId, params) ->
|
service.storeQueryParams = (projectId, params) ->
|
||||||
ns = "#{projectId}:#{hashSuffix}"
|
ns = "#{projectId}:#{hashSuffix}"
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
|
|
|
@ -94,6 +94,11 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
params = {"project": projectId}
|
params = {"project": projectId}
|
||||||
return $repo.queryMany(type, params)
|
return $repo.queryMany(type, params)
|
||||||
|
|
||||||
|
service.createDefaultValues = (projectId, type) ->
|
||||||
|
data = {"project_id": projectId}
|
||||||
|
url = $urls.resolve("#{type}-create-default")
|
||||||
|
return $http.post(url, data)
|
||||||
|
|
||||||
service.storeQueryParams = (projectId, params) ->
|
service.storeQueryParams = (projectId, params) ->
|
||||||
ns = "#{projectId}:#{hashSuffix}"
|
ns = "#{projectId}:#{hashSuffix}"
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
|
|
|
@ -115,9 +115,13 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) ->
|
||||||
service.listValues = (projectId, type) ->
|
service.listValues = (projectId, type) ->
|
||||||
params = {"project": projectId}
|
params = {"project": projectId}
|
||||||
service.storeQueryParams(projectId, params)
|
service.storeQueryParams(projectId, params)
|
||||||
console.log type
|
|
||||||
return $repo.queryMany(type, params)
|
return $repo.queryMany(type, params)
|
||||||
|
|
||||||
|
service.createDefaultValues = (projectId, type) ->
|
||||||
|
data = {"project_id": projectId}
|
||||||
|
url = $urls.resolve("#{type}-create-default")
|
||||||
|
return $http.post(url, data)
|
||||||
|
|
||||||
service.storeQueryParams = (projectId, params) ->
|
service.storeQueryParams = (projectId, params) ->
|
||||||
ns = "#{projectId}:#{hashSuffix}"
|
ns = "#{projectId}:#{hashSuffix}"
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
|
|
|
@ -620,11 +620,16 @@
|
||||||
"SELECTED": "Selected"
|
"SELECTED": "Selected"
|
||||||
},
|
},
|
||||||
"PROJECT_DUE_DATE_STATUS": {
|
"PROJECT_DUE_DATE_STATUS": {
|
||||||
"TITLE": "Due dates statuses",
|
"TITLE": "Due Dates",
|
||||||
"SUBTITLE": "Specify the due date statuses your user stories, tasks and issues will go through",
|
"SUBTITLE": "Specify the due dates your user stories, tasks and issues will go through if selected",
|
||||||
"US_TITLE": "User Story Due Date Statuses",
|
"US_TITLE": "User Story Due Date status",
|
||||||
"TASK_TITLE": "Task Due Date Statuses",
|
"ACTION_ADD_STATUS": "Add new status",
|
||||||
"ISSUE_TITLE": "Issue Due Date Statuses"
|
"TASK_TITLE": "Task Due Date status",
|
||||||
|
"ISSUE_TITLE": "Issue Due Date status",
|
||||||
|
"DAYS_TO_DUE_DATE": "Days to due date",
|
||||||
|
"BEFORE_AFTER": "Before/after",
|
||||||
|
"BEFORE": "Before",
|
||||||
|
"AFTER": "After"
|
||||||
},
|
},
|
||||||
"ROLES": {
|
"ROLES": {
|
||||||
"PAGE_TITLE": "Roles - {{projectName}}",
|
"PAGE_TITLE": "Roles - {{projectName}}",
|
||||||
|
@ -726,7 +731,8 @@
|
||||||
"LABEL_SEVERITY": "Default value for severity selector"
|
"LABEL_SEVERITY": "Default value for severity selector"
|
||||||
},
|
},
|
||||||
"STATUS": {
|
"STATUS": {
|
||||||
"PLACEHOLDER_WRITE_STATUS_NAME": "Write a name for the new status"
|
"PLACEHOLDER_WRITE_STATUS_NAME": "Write a name for the new status",
|
||||||
|
"PLACEHOLDER_DAYS_TO_DUE_DATE": "Write a number of days to due date"
|
||||||
},
|
},
|
||||||
"TYPES": {
|
"TYPES": {
|
||||||
"PLACEHOLDER_WRITE_NAME": "Write a name for the new element"
|
"PLACEHOLDER_WRITE_NAME": "Write a name for the new element"
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
tg-due-date.statistic.card-due-date(
|
tg-due-date.statistic.card-due-date(
|
||||||
due-date="vm.item.getIn(['model', 'due_date'])"
|
due-date="vm.item.getIn(['model', 'due_date'])"
|
||||||
is-closed="vm.item.getIn(['model', 'is_closed'])"
|
is-closed="vm.item.getIn(['model', 'is_closed'])"
|
||||||
|
obj-type="task"
|
||||||
)
|
)
|
||||||
.statistic.card-iocaine(
|
.statistic.card-iocaine(
|
||||||
ng-if="vm.item.getIn(['model', 'is_iocaine'])"
|
ng-if="vm.item.getIn(['model', 'is_iocaine'])"
|
||||||
|
|
|
@ -32,6 +32,11 @@ class StoryHeaderController
|
||||||
@.editMode = false
|
@.editMode = false
|
||||||
@.loadingSubject = false
|
@.loadingSubject = false
|
||||||
@.originalSubject = @.item.subject
|
@.originalSubject = @.item.subject
|
||||||
|
@.objType = {
|
||||||
|
'tasks': 'task',
|
||||||
|
'issues': 'issue',
|
||||||
|
'userstories': 'us',
|
||||||
|
}[@.item._name]
|
||||||
|
|
||||||
_checkNav: () ->
|
_checkNav: () ->
|
||||||
if @.item.neighbors.previous?.ref?
|
if @.item.neighbors.previous?.ref?
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
due-date="vm.item.due_date"
|
due-date="vm.item.due_date"
|
||||||
is-closed="vm.item.is_closed"
|
is-closed="vm.item.is_closed"
|
||||||
ng-if="vm.item.due_date"
|
ng-if="vm.item.due_date"
|
||||||
|
obj-type="{{ vm.objType }}"
|
||||||
)
|
)
|
||||||
.edit-title-wrapper(ng-if="vm.editMode")
|
.edit-title-wrapper(ng-if="vm.editMode")
|
||||||
input.edit-title-input.e2e-title-input(
|
input.edit-title-input.e2e-title-input(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
label.due-date-button.button-gray.is-editable(
|
label.due-date-button.button-gray.is-editable(
|
||||||
ng-if="vm.visible()"
|
ng-if="vm.visible()"
|
||||||
ng-disabled="vm.disabled()"
|
ng-disabled="vm.disabled()"
|
||||||
ng-class="vm.color()"
|
ng-style="{background: vm.color()}"
|
||||||
ng-attr-title="{{ vm.title() }}"
|
ng-attr-title="{{ vm.title() }}"
|
||||||
ng-click="vm.setDueDate()"
|
ng-click="vm.setDueDate()"
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,9 +22,15 @@ class DueDateController
|
||||||
"$translate"
|
"$translate"
|
||||||
"tgLightboxFactory"
|
"tgLightboxFactory"
|
||||||
"tgProjectService"
|
"tgProjectService"
|
||||||
|
"$rootScope"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@translate, @tgLightboxFactory, @projectService) ->
|
constructor: (@translate, @tgLightboxFactory, @projectService, @rootscope) ->
|
||||||
|
@.defaultConfig = [
|
||||||
|
{"color": "#9dce0a", "name": "normal due", "days_to_due": null, "by_default": true},
|
||||||
|
{"color": "#ff9900", "name": "due soon", "days_to_due": 14, "by_default": false},
|
||||||
|
{"color": "#ff8a84", "name": "past due", "days_to_due": 0, "by_default": false}
|
||||||
|
]
|
||||||
|
|
||||||
visible: () ->
|
visible: () ->
|
||||||
return @.format == 'button' or @.dueDate?
|
return @.format == 'button' or @.dueDate?
|
||||||
|
@ -33,14 +39,7 @@ class DueDateController
|
||||||
return @.isClosed
|
return @.isClosed
|
||||||
|
|
||||||
color: () ->
|
color: () ->
|
||||||
colors = {
|
return @.getStatus()?.color || null
|
||||||
'no_longer_applicable': 'closed',
|
|
||||||
'due_soon': 'due-soon',
|
|
||||||
'past_due': 'past-due',
|
|
||||||
'set': 'due-set',
|
|
||||||
'not_set': 'not-set',
|
|
||||||
}
|
|
||||||
return colors[@status()] or ''
|
|
||||||
|
|
||||||
title: () ->
|
title: () ->
|
||||||
if @.dueDate
|
if @.dueDate
|
||||||
|
@ -49,36 +48,48 @@ class DueDateController
|
||||||
return @translate.instant('COMMON.DUE_DATE.TITLE_ACTION_SET_DUE_DATE')
|
return @translate.instant('COMMON.DUE_DATE.TITLE_ACTION_SET_DUE_DATE')
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
status: () ->
|
getStatus: (options) ->
|
||||||
if !@.dueDate
|
if !@.dueDate
|
||||||
return 'not_set'
|
return null
|
||||||
|
|
||||||
project = @projectService.project.toJS()
|
project = @projectService.project.toJS()
|
||||||
project.due_soon_threshold = 14 # TODO get value from taiga-back
|
options = project["#{@.objType}_duedates"]
|
||||||
|
|
||||||
|
if !options
|
||||||
|
options = @.defaultConfig
|
||||||
|
|
||||||
|
return @._getAppearance(options)
|
||||||
|
|
||||||
|
_getDefaultAppearance: (options) ->
|
||||||
|
defaultAppearance = null
|
||||||
|
_.map options, (option) ->
|
||||||
|
if option.by_default == true
|
||||||
|
defaultAppearance = option
|
||||||
|
return defaultAppearance
|
||||||
|
|
||||||
|
_getAppearance: (options) ->
|
||||||
|
currentAppearance = @._getDefaultAppearance(options)
|
||||||
|
options = _.sortBy(options, (o) -> - o.days_to_due) # sort desc
|
||||||
|
|
||||||
dueDate = moment(@.dueDate)
|
dueDate = moment(@.dueDate)
|
||||||
now = moment()
|
now = moment()
|
||||||
|
_.map options, (appearance) ->
|
||||||
|
if appearance.days_to_due == null
|
||||||
|
return
|
||||||
|
limitDate = moment(dueDate - moment.duration(appearance.days_to_due, "days"))
|
||||||
|
if now >= limitDate
|
||||||
|
currentAppearance = appearance
|
||||||
|
|
||||||
if @.isClosed
|
return currentAppearance
|
||||||
return 'no_longer_applicable'
|
|
||||||
else if now > dueDate
|
|
||||||
return 'past_due'
|
|
||||||
else if now.add(moment.duration(project.due_soon_threshold, "days")) >= dueDate
|
|
||||||
return 'due_soon'
|
|
||||||
return 'set'
|
|
||||||
|
|
||||||
_formatTitle: () ->
|
_formatTitle: () ->
|
||||||
titles = {
|
|
||||||
'no_longer_applicable': 'COMMON.DUE_DATE.NO_LONGER_APPLICABLE',
|
|
||||||
'due_soon': 'COMMON.DUE_DATE.DUE_SOON',
|
|
||||||
'past_due': 'COMMON.DUE_DATE.PAST_DUE',
|
|
||||||
}
|
|
||||||
prettyDate = @translate.instant("COMMON.PICKERDATE.FORMAT")
|
prettyDate = @translate.instant("COMMON.PICKERDATE.FORMAT")
|
||||||
formatedDate = moment(@.dueDate).format(prettyDate)
|
formatedDate = moment(@.dueDate).format(prettyDate)
|
||||||
|
|
||||||
status = @status()
|
status = @.getStatus()
|
||||||
if not titles[status]
|
if status?.name
|
||||||
return formatedDate
|
return "#{formatedDate} (#{status.name})"
|
||||||
return "#{formatedDate} (#{@translate.instant(titles[status])})"
|
return formatedDate
|
||||||
|
|
||||||
setDueDate: () ->
|
setDueDate: () ->
|
||||||
return if @.disabled()
|
return if @.disabled()
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: color-selector.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "DueDate", ->
|
||||||
|
provide = null
|
||||||
|
controller = null
|
||||||
|
ctrl = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
dueDateConfig = {
|
||||||
|
'us': [
|
||||||
|
{"color": "#ff0000", "name": "past due", "days_to_due": 0, "by_default": false},
|
||||||
|
{"color": "#00ff00", "name": "normal due", "days_to_due": null, "by_default": true}
|
||||||
|
{"color": "#333333", "name": "distant past due", "days_to_due": -7, "by_default": false}
|
||||||
|
{"color": "#ffff00", "name": "due soon", "days_to_due": 14, "by_default": false},
|
||||||
|
],
|
||||||
|
'task': [
|
||||||
|
{"color": "#ff0000", "name": "past due", "days_to_due": 0, "by_default": false},
|
||||||
|
{"color": "#00ff00", "name": "normal due", "days_to_due": null, "by_default": true}
|
||||||
|
{"color": "#ffff00", "name": "due soon", "days_to_due": 3, "by_default": false}
|
||||||
|
],
|
||||||
|
'issue': [
|
||||||
|
{"color": "#550000", "name": "past due", "days_to_due": 0, "by_default": false},
|
||||||
|
{"color": "#00ff00", "name": "normal due", "days_to_due": null, "by_default": true},
|
||||||
|
{"color": "#AAAA00", "name": "due soon", "days_to_due": 7, "by_default": false},
|
||||||
|
{"color": "#333333", "name": "distant past due", "days_to_due": -10, "by_default": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
_mockTranslate = () ->
|
||||||
|
mocks.$translate = {
|
||||||
|
instant: (index) ->
|
||||||
|
if index == "COMMON.PICKERDATE.FORMAT"
|
||||||
|
return "DD MMM YYYY"
|
||||||
|
return 'Set due date'
|
||||||
|
}
|
||||||
|
provide.value "$translate", mocks.$translate
|
||||||
|
|
||||||
|
_mockTgProjectService = () ->
|
||||||
|
mocks.tgProjectService = {
|
||||||
|
project: {
|
||||||
|
toJS: () -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provide.value "tgProjectService", mocks.tgProjectService
|
||||||
|
|
||||||
|
_mockTgLightboxFactory = () ->
|
||||||
|
mocks.tgLightboxFactory = {}
|
||||||
|
provide.value "tgLightboxFactory", mocks.tgLightboxFactory
|
||||||
|
|
||||||
|
_mockLog = () ->
|
||||||
|
mocks.log = {
|
||||||
|
error: (msg) -> return msg
|
||||||
|
}
|
||||||
|
provide.value "$log", mocks.log
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
_mockTranslate()
|
||||||
|
_mockTgLightboxFactory()
|
||||||
|
_mockTgProjectService()
|
||||||
|
_mockLog()
|
||||||
|
return null
|
||||||
|
|
||||||
|
_inject = ->
|
||||||
|
inject ($controller) ->
|
||||||
|
controller = $controller
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaComponents"
|
||||||
|
_mocks()
|
||||||
|
_inject()
|
||||||
|
|
||||||
|
describe "when is not set", ->
|
||||||
|
beforeEach ->
|
||||||
|
ctrl = controller "DueDateCtrl"
|
||||||
|
ctrl.dueDate = null
|
||||||
|
ctrl.format = 'button'
|
||||||
|
|
||||||
|
it "get title", () ->
|
||||||
|
expect(ctrl.title()).to.be.eql('Set due date')
|
||||||
|
|
||||||
|
it "get color", () ->
|
||||||
|
expect(ctrl.color()).to.be.eql(null)
|
||||||
|
|
||||||
|
describe "when is set", ->
|
||||||
|
normalDue = ['normal due', '#9dce0a']
|
||||||
|
dueSoon = ['due soon', '#ff8a84']
|
||||||
|
pastDue = ['past due', '#ff9900']
|
||||||
|
|
||||||
|
runs = [
|
||||||
|
{ objType: 'us', days: -1, expect: pastDue },
|
||||||
|
{ objType: 'us', days: 15, expect: normalDue },
|
||||||
|
{ objType: 'us', days: 2, expect: dueSoon },
|
||||||
|
{ objType: 'task', days: -3, expect: pastDue },
|
||||||
|
{ objType: 'task', days: 18, expect: normalDue },
|
||||||
|
{ objType: 'task', days: 5, expect: dueSoon },
|
||||||
|
{ objType: 'issue', days: -5, expect: pastDue },
|
||||||
|
{ objType: 'issue', days: 20, expect: normalDue },
|
||||||
|
{ objType: 'issue', days: 8, expect: dueSoon },
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
ctrl = controller "DueDateCtrl"
|
||||||
|
ctrl.format = 'button'
|
||||||
|
|
||||||
|
runs.forEach (run) ->
|
||||||
|
it "get appearance in #{run.objType} view with #{run.days} days left", () ->
|
||||||
|
ctrl.objType = run.objType
|
||||||
|
ctrl.dueDate = moment().add(moment.duration(run.days, "days"))
|
||||||
|
formatedDate = ctrl.dueDate.format('DD MMM YYYY')
|
||||||
|
expect(ctrl.title()).to.be.eql("#{formatedDate} (#{run.expect[0]})")
|
||||||
|
expect(ctrl.color()).to.be.eql(run.expect[1])
|
|
@ -1,6 +1,6 @@
|
||||||
tg-svg.due-date-icon(
|
tg-svg.due-date-icon(
|
||||||
ng-if="vm.visible()"
|
ng-if="vm.visible()"
|
||||||
svg-icon="icon-clock"
|
svg-icon="icon-clock"
|
||||||
ng-class="vm.color()"
|
svg-fill="vm.color()"
|
||||||
ng-attr-title="{{ vm.title() }}"
|
ng-attr-title="{{ vm.title() }}"
|
||||||
)
|
)
|
|
@ -62,6 +62,7 @@ dueDatePopoverDirective = ($translate, datePickerConfigService) ->
|
||||||
dueDate: '=',
|
dueDate: '=',
|
||||||
isClosed: '=',
|
isClosed: '=',
|
||||||
item: '=',
|
item: '=',
|
||||||
|
objType: '@',
|
||||||
format: '@',
|
format: '@',
|
||||||
notAutoSave: '='
|
notAutoSave: '='
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.due-date-button-wrapper
|
.due-date-button-wrapper
|
||||||
label.due-date-button.button-gray.is-editable.date-picker-popover-trigger(
|
label.due-date-button.button-gray.is-editable.date-picker-popover-trigger(
|
||||||
ng-disabled="vm.disabled()"
|
ng-disabled="vm.disabled()"
|
||||||
ng-class="vm.color()"
|
ng-style="{background: vm.color()}"
|
||||||
ng-attr-title="{{ vm.title() }}"
|
ng-attr-title="{{ vm.title() }}"
|
||||||
)
|
)
|
||||||
tg-svg(svg-icon="icon-clock")
|
tg-svg(svg-icon="icon-clock")
|
||||||
|
|
|
@ -70,6 +70,7 @@ dueDateDirective = ($translate, datePickerConfigService) ->
|
||||||
dueDate: '=',
|
dueDate: '=',
|
||||||
isClosed: '=',
|
isClosed: '=',
|
||||||
item: '=',
|
item: '=',
|
||||||
|
objType: '@',
|
||||||
format: '@',
|
format: '@',
|
||||||
notAutoSave: '='
|
notAutoSave: '='
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,20 +15,20 @@ div.wrapper(ng-controller="ProjectValuesSectionController",
|
||||||
include ../includes/components/mainTitle
|
include ../includes/components/mainTitle
|
||||||
p.admin-subtitle(translate="ADMIN.PROJECT_DUE_DATE_STATUS.SUBTITLE")
|
p.admin-subtitle(translate="ADMIN.PROJECT_DUE_DATE_STATUS.SUBTITLE")
|
||||||
|
|
||||||
div.admin-attributes-section(tg-project-values, type="userstory-due-date-statuses",
|
div.admin-attributes-section(tg-project-due-dates-values, type="userstory-due-dates",
|
||||||
ng-controller="ProjectValuesController as ctrl",
|
ng-controller="ProjectDueDatesValuesController as ctrl",
|
||||||
ng-init="section='admin'; resource='userstories'; type='userstory-due-date-statuses'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.US_TITLE'",
|
ng-init="section='admin'; resource='userstories'; type='userstory-due-dates'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.US_TITLE'",
|
||||||
objName="status")
|
objName="status")
|
||||||
include ../includes/modules/admin/project-due-date-status
|
include ../includes/modules/admin/project-due-date-status
|
||||||
|
|
||||||
div.admin-attributes-section(tg-project-values, type="task-due-date-statuses",
|
div.admin-attributes-section(tg-project-due-dates-values, type="task-due-dates",
|
||||||
ng-controller="ProjectValuesController as ctrl",
|
ng-controller="ProjectDueDatesValuesController as ctrl",
|
||||||
ng-init="section='admin'; resource='tasks'; type='task-due-date-statuses'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.TASK_TITLE'"
|
ng-init="section='admin'; resource='tasks'; type='task-due-dates'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.TASK_TITLE'"
|
||||||
objName="status")
|
objName="status")
|
||||||
include ../includes/modules/admin/project-due-date-status
|
include ../includes/modules/admin/project-due-date-status
|
||||||
|
|
||||||
div.admin-attributes-section(tg-project-values, type="issue-due-date-statuses",
|
div.admin-attributes-section(tg-project-due-dates-values, type="issue-due-dates",
|
||||||
ng-controller="ProjectValuesController as ctrl",
|
ng-controller="ProjectDueDatesValuesController as ctrl",
|
||||||
ng-init="section='admin'; resource='issues'; type='issue-due-date-statuses'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.ISSUE_TITLE'",
|
ng-init="section='admin'; resource='issues'; type='issue-due-dates'; sectionName='ADMIN.PROJECT_DUE_DATE_STATUS.ISSUE_TITLE'",
|
||||||
objName="status")
|
objName="status")
|
||||||
include ../includes/modules/admin/project-due-date-status
|
include ../includes/modules/admin/project-due-date-status
|
||||||
|
|
|
@ -22,6 +22,7 @@ div.ticket-detail-settings
|
||||||
due-date="obj.due_date"
|
due-date="obj.due_date"
|
||||||
is-closed="obj.is_closed"
|
is-closed="obj.is_closed"
|
||||||
item="obj"
|
item="obj"
|
||||||
|
obj-type="issue"
|
||||||
not-auto-save="true"
|
not-auto-save="true"
|
||||||
)
|
)
|
||||||
div
|
div
|
||||||
|
|
|
@ -8,6 +8,7 @@ div.ticket-detail-settings
|
||||||
due-date="obj.due_date"
|
due-date="obj.due_date"
|
||||||
is-closed="obj.is_closed"
|
is-closed="obj.is_closed"
|
||||||
item="obj"
|
item="obj"
|
||||||
|
obj-type="task"
|
||||||
not-auto-save="true"
|
not-auto-save="true"
|
||||||
)
|
)
|
||||||
div
|
div
|
||||||
|
|
|
@ -11,6 +11,7 @@ div.ticket-detail-settings
|
||||||
due-date="obj.due_date"
|
due-date="obj.due_date"
|
||||||
is-closed="obj.is_closed"
|
is-closed="obj.is_closed"
|
||||||
item="obj"
|
item="obj"
|
||||||
|
obj-type="task"
|
||||||
not-auto-save="true"
|
not-auto-save="true"
|
||||||
)
|
)
|
||||||
div
|
div
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
due-date="us.due_date"
|
due-date="us.due_date"
|
||||||
is-closed="us.is_closed"
|
is-closed="us.is_closed"
|
||||||
ng-if="us.due_date"
|
ng-if="us.due_date"
|
||||||
|
obj-type="us"
|
||||||
)
|
)
|
||||||
tg-belong-to-epics(
|
tg-belong-to-epics(
|
||||||
format="pill"
|
format="pill"
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
section.colors-table.admin-status-table
|
section.colors-table.admin-status-table
|
||||||
div.project-values-title
|
div.project-values-title
|
||||||
h2 {{ sectionName | translate }}
|
h2 {{ sectionName | translate }}
|
||||||
a.button.button-gray.show-add-new(href="", title="{{'ADMIN.US_STATUS.ACTION_ADD_STATUS' | translate}}")
|
a.button.button-gray.show-add-new(href="", title="{{'ADMIN.PROJECT_DUE_DATE_STATUS.ACTION_ADD_STATUS' | translate}}")
|
||||||
span(translate="ADMIN.US_STATUS.ACTION_ADD_STATUS")
|
span(translate="ADMIN.PROJECT_DUE_DATE_STATUS.ACTION_ADD_STATUS")
|
||||||
|
|
||||||
div.table-header
|
div.table-header
|
||||||
div.row
|
div.row
|
||||||
div.color-column(translate="COMMON.FIELDS.COLOR")
|
div.color-column(translate="COMMON.FIELDS.COLOR")
|
||||||
div.status-name(translate="COMMON.FIELDS.NAME")
|
div.status-name(translate="COMMON.FIELDS.NAME")
|
||||||
div.status-slug(translate="COMMON.FIELDS.SLUG")
|
div.thresold-column(translate="ADMIN.PROJECT_DUE_DATE_STATUS.DAYS_TO_DUE_DATE")
|
||||||
div.thresold-column(translate="COMMON.FIELDS.THRESHOLD")
|
div.before-after-column(translate="ADMIN.PROJECT_DUE_DATE_STATUS.BEFORE_AFTER")
|
||||||
div.options-column
|
div.options-column
|
||||||
|
|
||||||
div.table-main
|
div.table-main
|
||||||
|
@ -24,11 +24,13 @@ section.colors-table.admin-status-table
|
||||||
div.status-name
|
div.status-name
|
||||||
span {{ value.name }}
|
span {{ value.name }}
|
||||||
|
|
||||||
div.status-slug
|
div.thresold-column
|
||||||
span {{ value.slug }}
|
span(ng-if="value.days_to_due_abs != null") {{ value.days_to_due_abs }}
|
||||||
|
|
||||||
div.is-closed-column
|
div.before-after-column
|
||||||
span {{ value.days_to_due }}
|
span(ng-if="value.days_to_due_abs", ng-switch="value.days_to_due >= 0")
|
||||||
|
span(ng-switch-when="true") {{ 'ADMIN.PROJECT_DUE_DATE_STATUS.BEFORE' | translate }}
|
||||||
|
span(ng-switch-when="false") {{ 'ADMIN.PROJECT_DUE_DATE_STATUS.AFTER' | translate }}
|
||||||
|
|
||||||
div.options-column
|
div.options-column
|
||||||
a.edit-value(href="")
|
a.edit-value(href="")
|
||||||
|
@ -36,7 +38,7 @@ section.colors-table.admin-status-table
|
||||||
title="{{'ADMIN.COMMON.TITLE_ACTION_EDIT_VALUE' | translate}}",
|
title="{{'ADMIN.COMMON.TITLE_ACTION_EDIT_VALUE' | translate}}",
|
||||||
svg-icon="icon-edit"
|
svg-icon="icon-edit"
|
||||||
)
|
)
|
||||||
a.delete-value(href="")
|
a.delete-value(href="", ng-if="!value.by_default")
|
||||||
tg-svg(
|
tg-svg(
|
||||||
title="{{'ADMIN.COMMON.TITLE_ACTION_DELETE_VALUE' | translate}}"
|
title="{{'ADMIN.COMMON.TITLE_ACTION_DELETE_VALUE' | translate}}"
|
||||||
svg-icon="icon-trash"
|
svg-icon="icon-trash"
|
||||||
|
@ -60,8 +62,33 @@ section.colors-table.admin-status-table
|
||||||
data-maxlength="255"
|
data-maxlength="255"
|
||||||
)
|
)
|
||||||
|
|
||||||
div.thresold-column
|
div.thresold-column(ng-if="!value.by_default")
|
||||||
input(type="number", name="days_to_due", ng-model="value.days_to_due")
|
input.days-to-due-abs(type="number", min="0", name="days_to_due_abs", ng-model="value.days_to_due_abs")
|
||||||
|
|
||||||
|
div.before-after-column
|
||||||
|
div.before-after-selector(ng-if="!value.by_default && value.days_to_due_abs != 0")
|
||||||
|
.before-after-selector-single(ng-class="{'checked': value.sign > 0 }")
|
||||||
|
input(
|
||||||
|
type='radio'
|
||||||
|
id="due-date-status-before"
|
||||||
|
name="due-date-status-after"
|
||||||
|
ng-model="value.sign"
|
||||||
|
value="1"
|
||||||
|
)
|
||||||
|
label.days-to-due-sign(
|
||||||
|
for="due-date-status-before",
|
||||||
|
) {{ 'ADMIN.PROJECT_DUE_DATE_STATUS.BEFORE' | translate }}
|
||||||
|
.before-after-selector-single(ng-class="{'checked': value.sign < 0 }")
|
||||||
|
input(
|
||||||
|
type='radio'
|
||||||
|
id="due-date-status-after"
|
||||||
|
name="due-date-status-after"
|
||||||
|
ng-model="value.sign"
|
||||||
|
value="-1"
|
||||||
|
)
|
||||||
|
label.days-to-due-sign(
|
||||||
|
for="due-date-status-after",
|
||||||
|
) {{ 'ADMIN.PROJECT_DUE_DATE_STATUS.AFTER' | translate }}
|
||||||
|
|
||||||
div.options-column
|
div.options-column
|
||||||
a.save.e2e-save(href="", title="{{'COMMON.SAVE' | translate}}")
|
a.save.e2e-save(href="", title="{{'COMMON.SAVE' | translate}}")
|
||||||
|
@ -90,7 +117,13 @@ section.colors-table.admin-status-table
|
||||||
)
|
)
|
||||||
|
|
||||||
div.thresold-column
|
div.thresold-column
|
||||||
input(type="number")
|
input(
|
||||||
|
name="days_to_due"
|
||||||
|
type="number"
|
||||||
|
placeholder="{{'ADMIN.STATUS.PLACEHOLDER_DAYS_TO_DUE_DATE' | translate}}"
|
||||||
|
ng-model="newValue.days_to_due"
|
||||||
|
data-required="true"
|
||||||
|
)
|
||||||
|
|
||||||
div.options-column
|
div.options-column
|
||||||
a.add-new.e2e-save(href="", title="{{'COMMON.ADD' | translate}}")
|
a.add-new.e2e-save(href="", title="{{'COMMON.ADD' | translate}}")
|
||||||
|
|
|
@ -58,6 +58,7 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}")
|
||||||
due-date="issue.due_date"
|
due-date="issue.due_date"
|
||||||
is-closed="us.is_closed"
|
is-closed="us.is_closed"
|
||||||
ng-if="issue.due_date"
|
ng-if="issue.due_date"
|
||||||
|
obj-type="issue"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ div.wrapper(
|
||||||
format="button"
|
format="button"
|
||||||
is-closed="issue.is_closed"
|
is-closed="issue.is_closed"
|
||||||
item="issue"
|
item="issue"
|
||||||
|
obj-type="issue"
|
||||||
tg-check-permission="modify_issue"
|
tg-check-permission="modify_issue"
|
||||||
)
|
)
|
||||||
tg-promote-issue-to-us-button(
|
tg-promote-issue-to-us-button(
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
span(ng-non-bindable) <%= emojify(task.subject) %>
|
span(ng-non-bindable) <%= emojify(task.subject) %>
|
||||||
tg-due-date(
|
tg-due-date(
|
||||||
due-date="task.due_date"
|
due-date="task.due_date"
|
||||||
|
obj-type="task"
|
||||||
ng-if="task.due_date"
|
ng-if="task.due_date"
|
||||||
)
|
)
|
||||||
.task-settings
|
.task-settings
|
||||||
|
|
|
@ -107,6 +107,7 @@ div.wrapper(
|
||||||
format="button"
|
format="button"
|
||||||
is-closed="task.is_closed"
|
is-closed="task.is_closed"
|
||||||
item="task"
|
item="task"
|
||||||
|
obj-type="task"
|
||||||
tg-check-permission="modify_task"
|
tg-check-permission="modify_task"
|
||||||
)
|
)
|
||||||
tg-task-is-iocaine-button(ng-model="task")
|
tg-task-is-iocaine-button(ng-model="task")
|
||||||
|
|
|
@ -132,6 +132,7 @@ div.wrapper(
|
||||||
format="button"
|
format="button"
|
||||||
is-closed="us.is_closed"
|
is-closed="us.is_closed"
|
||||||
item="us"
|
item="us"
|
||||||
|
obj-type="us"
|
||||||
tg-check-permission="modify_us"
|
tg-check-permission="modify_us"
|
||||||
)
|
)
|
||||||
tg-us-team-requirement-button(ng-model="us")
|
tg-us-team-requirement-button(ng-model="us")
|
||||||
|
|
|
@ -58,11 +58,17 @@
|
||||||
.is-archived-column,
|
.is-archived-column,
|
||||||
.is-closed-column,
|
.is-closed-column,
|
||||||
.options-column,
|
.options-column,
|
||||||
|
.thresold-column,
|
||||||
|
.before-after-column,
|
||||||
.status-wip-limit {
|
.status-wip-limit {
|
||||||
flex-basis: 100px;
|
flex-basis: 100px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.before-after-column {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-name,
|
.status-name,
|
||||||
.color-name {
|
.color-name {
|
||||||
flex-basis: 150px;
|
flex-basis: 150px;
|
||||||
|
@ -115,6 +121,43 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.before-after-selector {
|
||||||
|
display: flex;
|
||||||
|
font-size: .9rem;
|
||||||
|
input {
|
||||||
|
display: none;
|
||||||
|
+label {
|
||||||
|
background: rgba($whitish, .7);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: .5rem .25rem;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: background .2s ease-in;
|
||||||
|
}
|
||||||
|
+label:hover {
|
||||||
|
background: rgba($primary-light, .3);
|
||||||
|
transition: background .2s ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.before-after-selector-single {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: .25rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
&.checked label {
|
||||||
|
background: $primary-light;
|
||||||
|
color: $white;
|
||||||
|
transition: background .2s ease-in;
|
||||||
|
}
|
||||||
|
&.checked label:hover {
|
||||||
|
background: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.options-column {
|
.options-column {
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
Loading…
Reference in New Issue