commit
16b1c62719
|
@ -4,7 +4,7 @@
|
|||
## 1.6.0 ??? (Unreleased)
|
||||
|
||||
### Features
|
||||
- ...
|
||||
- Added custom fields per project for user stories, tasks and issues.
|
||||
|
||||
### Misc
|
||||
- New contrib plugin for hipchat (by Δndrea Stagi)
|
||||
|
|
|
@ -39,17 +39,23 @@ taiga.sessionId = taiga.generateUniqueSessionIdentifier()
|
|||
configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider, tgLoaderProvider) ->
|
||||
$routeProvider.when("/",
|
||||
{templateUrl: "project/projects.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
$routeProvider.when("/project/:pslug/",
|
||||
{templateUrl: "project/project.html"})
|
||||
$routeProvider.when("/project/:pslug/backlog",
|
||||
{templateUrl: "backlog/backlog.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
$routeProvider.when("/project/:pslug/taskboard/:sslug",
|
||||
{templateUrl: "taskboard/taskboard.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
$routeProvider.when("/project/:pslug/search",
|
||||
{templateUrl: "search/search.html", reloadOnSearch: false})
|
||||
|
||||
$routeProvider.when("/project/:pslug/backlog",
|
||||
{templateUrl: "backlog/backlog.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
$routeProvider.when("/project/:pslug/kanban",
|
||||
{templateUrl: "kanban/kanban.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
# Milestone
|
||||
$routeProvider.when("/project/:pslug/taskboard/:sslug",
|
||||
{templateUrl: "taskboard/taskboard.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
# User stories
|
||||
$routeProvider.when("/project/:pslug/us/:usref",
|
||||
{templateUrl: "us/us-detail.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
@ -74,7 +80,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
$routeProvider.when("/project/:pslug/issue/:issueref",
|
||||
{templateUrl: "issue/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}})
|
||||
|
||||
# Admin
|
||||
# Admin - Project Profile
|
||||
$routeProvider.when("/project/:pslug/admin/project-profile/details",
|
||||
{templateUrl: "admin/admin-project-profile.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-profile/default-values",
|
||||
|
@ -83,12 +89,17 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
{templateUrl: "admin/admin-project-modules.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-profile/export",
|
||||
{templateUrl: "admin/admin-project-export.html"})
|
||||
# Admin Project Values
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/us-status",
|
||||
{templateUrl: "admin/admin-project-values-us-status.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/us-points",
|
||||
{templateUrl: "admin/admin-project-values-us-points.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/us-extras",
|
||||
{templateUrl: "admin/admin-project-values-us-extras.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/task-status",
|
||||
{templateUrl: "admin/admin-project-values-task-status.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/task-extras",
|
||||
{templateUrl: "admin/admin-project-values-task-extras.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/issue-status",
|
||||
{templateUrl: "admin/admin-project-values-issue-status.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/issue-types",
|
||||
|
@ -97,10 +108,15 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
{templateUrl: "admin/admin-project-values-issue-priorities.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/issue-severities",
|
||||
{templateUrl: "admin/admin-project-values-issue-severities.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/project-values/issue-extras",
|
||||
{templateUrl: "admin/admin-project-values-issue-extras.html"})
|
||||
# Admin - Memberships
|
||||
$routeProvider.when("/project/:pslug/admin/memberships",
|
||||
{templateUrl: "admin/admin-memberships.html"})
|
||||
# Admin - Roles
|
||||
$routeProvider.when("/project/:pslug/admin/roles",
|
||||
{templateUrl: "admin/admin-roles.html"})
|
||||
# Admin - Third Parties
|
||||
$routeProvider.when("/project/:pslug/admin/third-parties/webhooks",
|
||||
{templateUrl: "admin/admin-third-parties-webhooks.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/third-parties/github",
|
||||
|
@ -109,6 +125,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
{templateUrl: "admin/admin-third-parties-gitlab.html"})
|
||||
$routeProvider.when("/project/:pslug/admin/third-parties/bitbucket",
|
||||
{templateUrl: "admin/admin-third-parties-bitbucket.html"})
|
||||
# Admin - Contrib Plugins
|
||||
$routeProvider.when("/project/:pslug/admin/contrib/:plugin",
|
||||
{templateUrl: "contrib/main.html"})
|
||||
|
||||
|
|
|
@ -320,3 +320,299 @@ ColorSelectionDirective = () ->
|
|||
}
|
||||
|
||||
module.directive("tgColorSelection", ColorSelectionDirective)
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Custom Attributes Controller
|
||||
#############################################################################
|
||||
|
||||
class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||
@.$inject = [
|
||||
"$scope",
|
||||
"$rootScope",
|
||||
"$tgRepo",
|
||||
"$tgResources",
|
||||
"$routeParams",
|
||||
"$q",
|
||||
"$tgLocation",
|
||||
"$tgNavUrls",
|
||||
"$appTitle",
|
||||
]
|
||||
|
||||
constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appTitle) ->
|
||||
@scope.project = {}
|
||||
|
||||
promise = @.loadInitialData()
|
||||
|
||||
promise.then () =>
|
||||
@appTitle.set("Project Custom Attributes - " + @scope.sectionName + " - " + @scope.project.name)
|
||||
|
||||
promise.then null, @.onInitialDataError.bind(@)
|
||||
|
||||
loadInitialData: =>
|
||||
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
|
||||
@scope.projectId = data.project
|
||||
return data
|
||||
|
||||
return promise.then( => @q.all([
|
||||
@.loadProject(),
|
||||
@.loadCustomAttributes(),
|
||||
]))
|
||||
|
||||
#########################
|
||||
# Project
|
||||
#########################
|
||||
|
||||
loadProject: =>
|
||||
return @rs.projects.get(@scope.projectId).then (project) =>
|
||||
@scope.project = project
|
||||
@scope.$emit('project:loaded', project)
|
||||
return project
|
||||
|
||||
#########################
|
||||
# Custom Attribute
|
||||
#########################
|
||||
|
||||
loadCustomAttributes: =>
|
||||
return @rs.customAttributes[@scope.type].list(@scope.projectId).then (customAttributes) =>
|
||||
@scope.customAttributes = customAttributes
|
||||
@scope.maxOrder = _.max(customAttributes, "order").order
|
||||
return customAttributes
|
||||
|
||||
createCustomAttribute: (attrValues) =>
|
||||
return @repo.create("custom-attributes/#{@scope.type}", attrValues)
|
||||
|
||||
saveCustomAttribute: (attrModel) =>
|
||||
return @repo.save(attrModel)
|
||||
|
||||
deleteCustomAttribute: (attrModel) =>
|
||||
return @repo.remove(attrModel)
|
||||
|
||||
moveCustomAttributes: (attrModel, newIndex) =>
|
||||
customAttributes = @scope.customAttributes
|
||||
r = customAttributes.indexOf(attrModel)
|
||||
customAttributes.splice(r, 1)
|
||||
customAttributes.splice(newIndex, 0, attrModel)
|
||||
|
||||
_.each customAttributes, (val, idx) ->
|
||||
val.order = idx
|
||||
|
||||
@repo.saveAll(customAttributes)
|
||||
|
||||
|
||||
module.controller("ProjectCustomAttributesController", ProjectCustomAttributesController)
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Custom Attributes Directive
|
||||
#############################################################################
|
||||
|
||||
ProjectCustomAttributesDirective = ($log, $confirm, animationFrame) ->
|
||||
link = ($scope, $el, $attrs) ->
|
||||
$ctrl = $el.controller()
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
##################################
|
||||
# Drag & Drop
|
||||
##################################
|
||||
sortableEl = $el.find(".js-sortable")
|
||||
|
||||
sortableEl.sortable({
|
||||
handle: ".js-view-custom-field",
|
||||
dropOnEmpty: true
|
||||
revert: 400
|
||||
axis: "y"
|
||||
})
|
||||
|
||||
sortableEl.on "sortstop", (event, ui) ->
|
||||
itemEl = ui.item
|
||||
itemAttr = itemEl.scope().attr
|
||||
itemIndex = itemEl.index()
|
||||
$ctrl.moveCustomAttributes(itemAttr, itemIndex)
|
||||
|
||||
##################################
|
||||
# New custom attribute
|
||||
##################################
|
||||
|
||||
showCreateForm = ->
|
||||
$el.find(".js-new-custom-field").removeClass("hidden")
|
||||
|
||||
hideCreateForm = ->
|
||||
$el.find(".js-new-custom-field").addClass("hidden")
|
||||
|
||||
showAddButton = ->
|
||||
$el.find(".js-add-custom-field-button").removeClass("hidden")
|
||||
|
||||
hideAddButton = ->
|
||||
$el.find(".js-add-custom-field-button").addClass("hidden")
|
||||
|
||||
showCancelButton = ->
|
||||
$el.find(".js-cancel-new-custom-field-button").removeClass("hidden")
|
||||
|
||||
hideCancelButton = ->
|
||||
$el.find(".js-cancel-new-custom-field-button").addClass("hidden")
|
||||
|
||||
resetNewAttr = ->
|
||||
$scope.newAttr = {}
|
||||
|
||||
create = (formEl) ->
|
||||
form = formEl.checksley()
|
||||
return if not form.validate()
|
||||
|
||||
onSucces = =>
|
||||
$ctrl.loadCustomAttributes()
|
||||
hideCreateForm()
|
||||
resetNewAttr()
|
||||
$confirm.notify("success")
|
||||
|
||||
onError = (data) =>
|
||||
form.setErrors(data)
|
||||
$confirm.notify("error")
|
||||
|
||||
attr = $scope.newAttr
|
||||
attr.project = $scope.projectId
|
||||
attr.order = if $scope.maxOrder then $scope.maxOrder + 1 else 1
|
||||
|
||||
$ctrl.createCustomAttribute(attr).then(onSucces, onError)
|
||||
|
||||
cancelCreate = ->
|
||||
hideCreateForm()
|
||||
resetNewAttr()
|
||||
|
||||
$scope.$watch "customAttributes", (customAttributes) ->
|
||||
return if not customAttributes
|
||||
|
||||
if customAttributes.length == 0
|
||||
hideCancelButton()
|
||||
hideAddButton()
|
||||
showCreateForm()
|
||||
else
|
||||
hideCreateForm()
|
||||
showAddButton()
|
||||
showCancelButton()
|
||||
|
||||
$el.on "click", ".js-add-custom-field-button", (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
showCreateForm()
|
||||
|
||||
$el.on "click", ".js-create-custom-field-button", debounce 2000, (event) ->
|
||||
event.preventDefault()
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
|
||||
create(formEl)
|
||||
|
||||
$el.on "click", ".js-cancel-new-custom-field-button", (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
cancelCreate()
|
||||
|
||||
$el.on "keyup", ".js-new-custom-field input", (event) ->
|
||||
if event.keyCode == 13 # Enter
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
create(formEl)
|
||||
else if event.keyCode == 27 # Esc
|
||||
cancelCreate()
|
||||
|
||||
##################################
|
||||
# Edit custom attribute
|
||||
##################################
|
||||
|
||||
showEditForm = (formEl) ->
|
||||
formEl.find(".js-view-custom-field").addClass("hidden")
|
||||
formEl.find(".js-edit-custom-field").removeClass("hidden")
|
||||
|
||||
hideEditForm = (formEl) ->
|
||||
formEl.find(".js-edit-custom-field").addClass("hidden")
|
||||
formEl.find(".js-view-custom-field").removeClass("hidden")
|
||||
|
||||
revertChangesInCustomAttribute = (formEl) ->
|
||||
$scope.$apply ->
|
||||
formEl.scope().attr.revert()
|
||||
|
||||
update = (formEl) ->
|
||||
form = formEl.checksley()
|
||||
return if not form.validate()
|
||||
|
||||
onSucces = =>
|
||||
$ctrl.loadCustomAttributes()
|
||||
hideEditForm(formEl)
|
||||
$confirm.notify("success")
|
||||
|
||||
onError = (data) =>
|
||||
form.setErrors(data)
|
||||
$confirm.notify("error")
|
||||
|
||||
attr = formEl.scope().attr
|
||||
$ctrl.saveCustomAttribute(attr).then(onSucces, onError)
|
||||
|
||||
cancelUpdate = (formEl) ->
|
||||
hideEditForm(formEl)
|
||||
revertChangesInCustomAttribute(formEl)
|
||||
|
||||
$el.on "click", ".js-edit-custom-field-button", (event) ->
|
||||
event.preventDefault()
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
|
||||
showEditForm(formEl)
|
||||
|
||||
$el.on "click", ".js-update-custom-field-button", debounce 2000, (event) ->
|
||||
event.preventDefault()
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
|
||||
update(formEl)
|
||||
|
||||
$el.on "click", ".js-cancel-edit-custom-field-button", (event) ->
|
||||
event.preventDefault()
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
|
||||
cancelUpdate(formEl)
|
||||
|
||||
$el.on "keyup", ".js-edit-custom-field input", (event) ->
|
||||
if event.keyCode == 13 # Enter
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
update(formEl)
|
||||
else if event.keyCode == 27 # Esc
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
cancelUpdate(formEl)
|
||||
|
||||
##################################
|
||||
# Delete custom attribute
|
||||
##################################
|
||||
|
||||
deleteCustomAttribute = (formEl) ->
|
||||
attr = formEl.scope().attr
|
||||
|
||||
title = "Delete custom attribute" # i18n
|
||||
subtitle = "Remeber that all values in this custom field will be deleted.</br> Are you sure you want to continue?"
|
||||
message = attr.name
|
||||
$confirm.ask(title, subtitle, message).then (finish) ->
|
||||
onSucces = ->
|
||||
$ctrl.loadCustomAttributes().finally ->
|
||||
finish()
|
||||
|
||||
onError = ->
|
||||
finish(false)
|
||||
$confirm.notify("error", null, "We have not been able to delete '#{message}'.")
|
||||
|
||||
$ctrl.deleteCustomAttribute(attr).then(onSucces, onError)
|
||||
|
||||
$el.on "click", ".js-delete-custom-field-button", debounce 2000, (event) ->
|
||||
event.preventDefault()
|
||||
target = angular.element(event.currentTarget)
|
||||
formEl = target.closest("form")
|
||||
|
||||
deleteCustomAttribute(formEl)
|
||||
|
||||
return {link: link}
|
||||
|
||||
module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", ProjectCustomAttributesDirective])
|
||||
|
|
|
@ -86,11 +86,14 @@ urls = {
|
|||
"project-admin-project-profile-export": "/project/:project/admin/project-profile/export"
|
||||
"project-admin-project-values-us-status": "/project/:project/admin/project-values/us-status"
|
||||
"project-admin-project-values-us-points": "/project/:project/admin/project-values/us-points"
|
||||
"project-admin-project-values-us-extras": "/project/:project/admin/project-values/us-extras"
|
||||
"project-admin-project-values-task-status": "/project/:project/admin/project-values/task-status"
|
||||
"project-admin-project-values-task-extras": "/project/:project/admin/project-values/task-extras"
|
||||
"project-admin-project-values-issue-status": "/project/:project/admin/project-values/issue-status"
|
||||
"project-admin-project-values-issue-types": "/project/:project/admin/project-values/issue-types"
|
||||
"project-admin-project-values-issue-priorities": "/project/:project/admin/project-values/issue-priorities"
|
||||
"project-admin-project-values-issue-severities": "/project/:project/admin/project-values/issue-severities"
|
||||
"project-admin-project-values-issue-extras": "/project/:project/admin/project-values/issue-extras"
|
||||
"project-admin-memberships": "/project/:project/admin/memberships"
|
||||
"project-admin-roles": "/project/:project/admin/roles"
|
||||
"project-admin-third-parties-webhooks": "/project/:project/admin/third-parties/webhooks"
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
###
|
||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
|
||||
#
|
||||
# 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: modules/common/custom-field-values.coffee
|
||||
###
|
||||
|
||||
taiga = @.taiga
|
||||
bindMethods = @.taiga.bindMethods
|
||||
bindOnce = @.taiga.bindOnce
|
||||
debounce = @.taiga.debounce
|
||||
generateHash = taiga.generateHash
|
||||
|
||||
module = angular.module("taigaCommon")
|
||||
|
||||
|
||||
class CustomAttributesValuesController extends taiga.Controller
|
||||
@.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q"]
|
||||
|
||||
constructor: (@scope, @rootscope, @repo, @rs, @confirm, @q) ->
|
||||
bindMethods(@)
|
||||
@.type = null
|
||||
@.objectId = null
|
||||
@.projectId = null
|
||||
@.customAttributes = []
|
||||
@.customAttributesValues = null
|
||||
|
||||
initialize: (type, objectId) ->
|
||||
@.project = @scope.project
|
||||
@.type = type
|
||||
@.objectId = objectId
|
||||
@.projectId = @scope.projectId
|
||||
|
||||
loadCustomAttributesValues: ->
|
||||
return @.customAttributesValues if not @.objectId
|
||||
return @rs.customAttributesValues[@.type].get(@.objectId).then (customAttributesValues) =>
|
||||
@.customAttributes = @.project["#{@.type}_custom_attributes"]
|
||||
@.customAttributesValues = customAttributesValues
|
||||
return customAttributesValues
|
||||
|
||||
getAttributeValue: (attribute) ->
|
||||
attributeValue = _.clone(attribute, false)
|
||||
attributeValue.value = @.customAttributesValues.attributes_values[attribute.id]
|
||||
return attributeValue
|
||||
|
||||
updateAttributeValue: (attributeValue) ->
|
||||
onSuccess = =>
|
||||
@rootscope.$broadcast("custom-attributes-values:edit")
|
||||
|
||||
onError = (response) =>
|
||||
@confirm.notify("error")
|
||||
return @q.reject()
|
||||
|
||||
# We need to update the full array so angular understand the model is modified
|
||||
attributesValues = _.clone(@.customAttributesValues.attributes_values, true)
|
||||
attributesValues[attributeValue.id] = attributeValue.value
|
||||
@.customAttributesValues.attributes_values = attributesValues
|
||||
@.customAttributesValues.id = @.objectId
|
||||
return @repo.save(@.customAttributesValues).then(onSuccess, onError)
|
||||
|
||||
|
||||
CustomAttributesValuesDirective = ($templates, $storage) ->
|
||||
template = $templates.get("custom-attributes/custom-attributes-values.html", true)
|
||||
collapsedHash = (type) ->
|
||||
return generateHash(["custom-attributes-collapsed", type])
|
||||
|
||||
link = ($scope, $el, $attrs, $ctrls) ->
|
||||
$ctrl = $ctrls[0]
|
||||
$model = $ctrls[1]
|
||||
|
||||
bindOnce $scope, $attrs.ngModel, (value) ->
|
||||
$ctrl.initialize($attrs.type, value.id)
|
||||
$ctrl.loadCustomAttributesValues()
|
||||
|
||||
$el.on "click", ".custom-fields-header a", ->
|
||||
hash = collapsedHash($attrs.type)
|
||||
collapsed = not($storage.get(hash) or false)
|
||||
$storage.set(hash, collapsed)
|
||||
if collapsed
|
||||
$el.find(".custom-fields-header a").removeClass("open")
|
||||
$el.find(".custom-fields-body").removeClass("open")
|
||||
else
|
||||
$el.find(".custom-fields-header a").addClass("open")
|
||||
$el.find(".custom-fields-body").addClass("open")
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
templateFn = ($el, $attrs) ->
|
||||
collapsed = $storage.get(collapsedHash($attrs.type)) or false
|
||||
|
||||
return template({
|
||||
requiredEditionPerm: $attrs.requiredEditionPerm
|
||||
collapsed: collapsed
|
||||
})
|
||||
|
||||
return {
|
||||
require: ["tgCustomAttributesValues", "ngModel"]
|
||||
controller: CustomAttributesValuesController
|
||||
controllerAs: "ctrl"
|
||||
restrict: "AE"
|
||||
scope: true
|
||||
link: link
|
||||
template: templateFn
|
||||
}
|
||||
|
||||
module.directive("tgCustomAttributesValues", ["$tgTemplate", "$tgStorage", CustomAttributesValuesDirective])
|
||||
|
||||
|
||||
CustomAttributeValueDirective = ($template, $selectedText) ->
|
||||
template = $template.get("custom-attributes/custom-attribute-value.html", true)
|
||||
templateEdit = $template.get("custom-attributes/custom-attribute-value-edit.html", true)
|
||||
|
||||
link = ($scope, $el, $attrs, $ctrl) ->
|
||||
render = (attributeValue, edit=false) ->
|
||||
value = attributeValue.value
|
||||
ctx = {
|
||||
id: attributeValue.id
|
||||
name: attributeValue.name
|
||||
description: attributeValue.description
|
||||
value: value
|
||||
isEditable: isEditable()
|
||||
}
|
||||
|
||||
if edit or not value
|
||||
html = templateEdit(ctx)
|
||||
else
|
||||
html = template(ctx)
|
||||
|
||||
$el.html(html)
|
||||
|
||||
isEditable = ->
|
||||
permissions = $scope.project.my_permissions
|
||||
requiredEditionPerm = $attrs.requiredEditionPerm
|
||||
return permissions.indexOf(requiredEditionPerm) > -1
|
||||
|
||||
saveAttributeValue = ->
|
||||
attributeValue.value = $el.find("input").val()
|
||||
|
||||
$scope.$apply ->
|
||||
$ctrl.updateAttributeValue(attributeValue).then ->
|
||||
render(attributeValue, false)
|
||||
|
||||
$el.on "keyup", "input[name=description]", (event) ->
|
||||
if event.keyCode == 13
|
||||
submit(event)
|
||||
else if event.keyCode == 27
|
||||
render(attributeValue, false)
|
||||
|
||||
## Actions (on view mode)
|
||||
$el.on "click", ".custom-field-value.read-mode", ->
|
||||
return if not isEditable()
|
||||
return if $selectedText.get().length
|
||||
render(attributeValue, true)
|
||||
$el.find("input[name='description']").focus().select()
|
||||
|
||||
$el.on "click", "a.icon-edit", (event) ->
|
||||
event.preventDefault()
|
||||
render(attributeValue, true)
|
||||
$el.find("input[name='description']").focus().select()
|
||||
|
||||
## Actions (on edit mode)
|
||||
submit = debounce 2000, (event) =>
|
||||
event.preventDefault()
|
||||
saveAttributeValue()
|
||||
|
||||
$el.on "submit", "form", submit
|
||||
$el.on "click", "a.icon-floppy", submit
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
# Bootstrap
|
||||
attributeValue = $scope.$eval($attrs.tgCustomAttributeValue)
|
||||
render(attributeValue)
|
||||
|
||||
return {
|
||||
link: link
|
||||
require: "^tgCustomAttributesValues"
|
||||
restrict: "AE"
|
||||
}
|
||||
|
||||
module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", CustomAttributeValueDirective])
|
|
@ -173,6 +173,34 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
|
|||
|
||||
return _.flatten(attachments).join("\n")
|
||||
|
||||
renderCustomAttributesEntry = (value) ->
|
||||
customAttributes = _.map value, (changes, type) ->
|
||||
if type == "new"
|
||||
return _.map changes, (change) ->
|
||||
return templateChangeGeneric({
|
||||
name: change.name,
|
||||
from: formatChange(""),
|
||||
to: formatChange(change.value)
|
||||
})
|
||||
else if type == "deleted"
|
||||
return _.map changes, (change) ->
|
||||
# TODO: i18n
|
||||
return templateChangeDiff({
|
||||
name: "deleted custom attribute",
|
||||
diff: change.name
|
||||
})
|
||||
else
|
||||
return _.map changes, (change) ->
|
||||
customAttrsChanges = _.map change.changes, (values) ->
|
||||
return templateChangeGeneric({
|
||||
name: change.name
|
||||
from: formatChange(values[0])
|
||||
to: formatChange(values[1])
|
||||
})
|
||||
return _.flatten(customAttrsChanges).join("\n")
|
||||
|
||||
return _.flatten(customAttributes).join("\n")
|
||||
|
||||
renderChangeEntry = (field, value) ->
|
||||
if field == "description"
|
||||
return templateChangeDiff({name: getHumanizedFieldName("description"), diff: value[1]})
|
||||
|
@ -182,6 +210,8 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm) ->
|
|||
return templateChangePoints({points: value})
|
||||
else if field == "attachments"
|
||||
return renderAttachmentEntry(value)
|
||||
else if field == "custom_attributes"
|
||||
return renderCustomAttributesEntry(value)
|
||||
else if field in ["tags", "watchers"]
|
||||
name = getHumanizedFieldName(field)
|
||||
removed = _.difference(value[0], value[1])
|
||||
|
|
|
@ -85,6 +85,9 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@rootscope.$broadcast("history:reload")
|
||||
@.loadIssue()
|
||||
|
||||
@scope.$on "custom-attributes-values:edit", =>
|
||||
@rootscope.$broadcast("history:reload")
|
||||
|
||||
initializeOnDeleteGoToUrl: ->
|
||||
ctx = {project: @scope.project.slug}
|
||||
if @scope.project.is_issues_activated
|
||||
|
|
|
@ -102,6 +102,16 @@ urls = {
|
|||
"attachments/task": "/tasks/attachments"
|
||||
"attachments/wiki_page": "/wiki/attachments"
|
||||
|
||||
# Custom Attributess
|
||||
"custom-attributes/userstory": "/userstory-custom-attributes"
|
||||
"custom-attributes/issue": "/issue-custom-attributes"
|
||||
"custom-attributes/task": "/task-custom-attributes"
|
||||
|
||||
# Custom field values
|
||||
"custom-attributes-values/userstory": "/userstories/custom-attributes-values"
|
||||
"custom-attributes-values/issue": "/issues/custom-attributes-values"
|
||||
"custom-attributes-values/task": "/tasks/custom-attributes-values"
|
||||
|
||||
# Feedback
|
||||
"feedback": "/feedback"
|
||||
|
||||
|
@ -133,6 +143,8 @@ module.run([
|
|||
"$log",
|
||||
"$tgResources",
|
||||
"$tgProjectsResourcesProvider",
|
||||
"$tgCustomAttributesResourcesProvider",
|
||||
"$tgCustomAttributesValuesResourcesProvider",
|
||||
"$tgMembershipsResourcesProvider",
|
||||
"$tgNotifyPoliciesResourcesProvider",
|
||||
"$tgInvitationsResourcesProvider",
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
###
|
||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
|
||||
#
|
||||
# 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: modules/resources/custom-field-values.coffee
|
||||
###
|
||||
|
||||
taiga = @.taiga
|
||||
|
||||
resourceProvider = ($repo) ->
|
||||
_get = (objectId, resource) ->
|
||||
return $repo.queryOne(resource, objectId)
|
||||
|
||||
service = {
|
||||
userstory: {
|
||||
get: (objectId) -> _get(objectId, "custom-attributes-values/userstory")
|
||||
}
|
||||
task: {
|
||||
get: (objectId) -> _get(objectId, "custom-attributes-values/task")
|
||||
}
|
||||
issue: {
|
||||
get: (objectId) -> _get(objectId, "custom-attributes-values/issue")
|
||||
}
|
||||
}
|
||||
|
||||
return (instance) ->
|
||||
instance.customAttributesValues = service
|
||||
|
||||
module = angular.module("taigaResources")
|
||||
module.factory("$tgCustomAttributesValuesResourcesProvider", ["$tgRepo", resourceProvider])
|
|
@ -0,0 +1,48 @@
|
|||
###
|
||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
|
||||
#
|
||||
# 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: modules/resources/projects.coffee
|
||||
###
|
||||
|
||||
|
||||
taiga = @.taiga
|
||||
sizeFormat = @.taiga.sizeFormat
|
||||
|
||||
|
||||
resourceProvider = ($repo) ->
|
||||
_list = (projectId, resource) ->
|
||||
return $repo.queryMany(resource, {project: projectId})
|
||||
|
||||
service = {
|
||||
userstory:{
|
||||
list: (projectId) -> _list(projectId, "custom-attributes/userstory")
|
||||
}
|
||||
task:{
|
||||
list: (projectId) -> _list(projectId, "custom-attributes/task")
|
||||
}
|
||||
issue: {
|
||||
list: (projectId) -> _list(projectId, "custom-attributes/issue")
|
||||
}
|
||||
}
|
||||
|
||||
return (instance) ->
|
||||
instance.customAttributes = service
|
||||
|
||||
|
||||
module = angular.module("taigaResources")
|
||||
module.factory("$tgCustomAttributesResourcesProvider", ["$tgRepo", resourceProvider])
|
|
@ -71,6 +71,8 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@rootscope.$broadcast("history:reload")
|
||||
@scope.$on "attachment:delete", =>
|
||||
@rootscope.$broadcast("history:reload")
|
||||
@scope.$on "custom-attributes-values:edit", =>
|
||||
@rootscope.$broadcast("history:reload")
|
||||
|
||||
initializeOnDeleteGoToUrl: ->
|
||||
ctx = {project: @scope.project.slug}
|
||||
|
|
|
@ -80,6 +80,9 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@scope.$on "attachment:delete", =>
|
||||
@rootscope.$broadcast("history:reload")
|
||||
|
||||
@scope.$on "custom-attributes-values:edit", =>
|
||||
@rootscope.$broadcast("history:reload")
|
||||
|
||||
initializeOnDeleteGoToUrl: ->
|
||||
ctx = {project: @scope.project.slug}
|
||||
@scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
div.wrapper(tg-project-custom-attributes, ng-controller="ProjectCustomAttributesController as ctrl",
|
||||
ng-init="section='admin'; type='issue'; sectionName='Issue extra'")
|
||||
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
|
||||
include ../includes/modules/admin-menu
|
||||
|
||||
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-issue-extras")
|
||||
include ../includes/modules/admin-submenu-project-values
|
||||
|
||||
section.main.admin-common
|
||||
include ../includes/components/mainTitle
|
||||
p.admin-subtitle Specify here issue custom fields. The new field will appear on your issue detail.
|
||||
|
||||
div.custom-field-options
|
||||
a.button.button-green.js-add-custom-field-button(href="",title="Add a custom field in issues")
|
||||
| Add custom field
|
||||
|
||||
include ../includes/modules/admin/admin-custom-attributes
|
|
@ -0,0 +1,17 @@
|
|||
div.wrapper(tg-project-custom-attributes, ng-controller="ProjectCustomAttributesController as ctrl",
|
||||
ng-init="section='admin'; type='task'; sectionName='Task extra'")
|
||||
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
|
||||
include ../includes/modules/admin-menu
|
||||
|
||||
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-task-extras")
|
||||
include ../includes/modules/admin-submenu-project-values
|
||||
|
||||
section.main.admin-common
|
||||
include ../includes/components/mainTitle
|
||||
p.admin-subtitle Specify here task custom fields. The new field will appear on your task detail.
|
||||
|
||||
div.custom-field-options
|
||||
a.button.button-green.js-add-custom-field-button(href="",title="Add a custom field in tasks")
|
||||
| Add custom field
|
||||
|
||||
include ../includes/modules/admin/admin-custom-attributes
|
|
@ -0,0 +1,17 @@
|
|||
div.wrapper(tg-project-custom-attributes, ng-controller="ProjectCustomAttributesController as ctrl",
|
||||
ng-init="section='admin'; type='userstory'; sectionName='US extra'")
|
||||
sidebar.menu-secondary.sidebar(tg-admin-navigation="project-values")
|
||||
include ../includes/modules/admin-menu
|
||||
|
||||
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-us-extras")
|
||||
include ../includes/modules/admin-submenu-project-values
|
||||
|
||||
section.main.admin-common
|
||||
include ../includes/components/mainTitle
|
||||
p.admin-subtitle Specify here user story custom fields. The new field will appear on your user story detail.
|
||||
|
||||
div.custom-field-options
|
||||
a.button.button-green.js-add-custom-field-button(href="",title="Add a custom field in user stories")
|
||||
| Add custom field
|
||||
|
||||
include ../includes/modules/admin/admin-custom-attributes
|
|
@ -0,0 +1,14 @@
|
|||
form.custom-field-single.editable
|
||||
div.custom-field-data
|
||||
label.custom-field-name(for="custom-field-description")
|
||||
<%- name %>
|
||||
<% if (description){ %>
|
||||
span.custom-field-description
|
||||
<%- description %>
|
||||
<% } %>
|
||||
|
||||
div.custom-field-value
|
||||
input#custom-field-description(name="description", type="text", value!="<%- value %>")
|
||||
|
||||
div.custom-field-options
|
||||
a.icon.icon-floppy(href="", title="Save Custom Field")
|
|
@ -0,0 +1,17 @@
|
|||
div.custom-field-single
|
||||
div.custom-field-data
|
||||
span.custom-field-name
|
||||
<%- name %>
|
||||
<% if (description){ %>
|
||||
span.custom-field-description
|
||||
<%- description %>
|
||||
<% } %>
|
||||
|
||||
div.custom-field-value.read-mode
|
||||
span
|
||||
<%- value %>
|
||||
|
||||
<% if (isEditable) { %>
|
||||
div.custom-field-options
|
||||
a.icon.icon-edit(href="", title="Edit Custom Field")
|
||||
<% } %>
|
|
@ -0,0 +1,7 @@
|
|||
section.duty-custom-fields(ng-show="ctrl.customAttributes.length")
|
||||
div.custom-fields-header
|
||||
span Custom Fields
|
||||
// Remove .open class on click on this button in both .icon and .custom-fields-body to close
|
||||
a.icon.icon-arrow-bottom(class!="<% if (!collapsed) { %>open<% } %>")
|
||||
div.custom-fields-body(class!="<% if (!collapsed) { %>open<% } %>")
|
||||
div(ng-repeat="att in ctrl.customAttributes", tg-custom-attribute-value="ctrl.getAttributeValue(att)", required-edition-perm!="<%- requiredEditionPerm %>")
|
|
@ -14,11 +14,21 @@ section.admin-submenu
|
|||
span.title US points
|
||||
span.icon.icon-arrow-right
|
||||
|
||||
li#adminmenu-values-us-extras
|
||||
a(href="", tg-nav="project-admin-project-values-us-extras:project=project.slug")
|
||||
span.title US extras
|
||||
span.icon.icon-arrow-right
|
||||
|
||||
li#adminmenu-values-task-status
|
||||
a(href="", tg-nav="project-admin-project-values-task-status:project=project.slug")
|
||||
span.title Task statuses
|
||||
span.icon.icon-arrow-right
|
||||
|
||||
li#adminmenu-values-task-extras
|
||||
a(href="", tg-nav="project-admin-project-values-task-extras:project=project.slug")
|
||||
span.title Task extras
|
||||
span.icon.icon-arrow-right
|
||||
|
||||
li#adminmenu-values-issue-status
|
||||
a(href="", tg-nav="project-admin-project-values-issue-status:project=project.slug")
|
||||
span.title Issue statuses
|
||||
|
@ -38,3 +48,8 @@ section.admin-submenu
|
|||
a(href="", tg-nav="project-admin-project-values-issue-severities:project=project.slug")
|
||||
span.title Issue Severities
|
||||
span.icon.icon-arrow-right
|
||||
|
||||
li#adminmenu-values-issue-extras
|
||||
a(href="", tg-nav="project-admin-project-values-issue-extras:project=project.slug")
|
||||
span.title Issue extras
|
||||
span.icon.icon-arrow-right
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
section.custom-fields-table.basic-table
|
||||
div.table-header
|
||||
div.row
|
||||
div.custom-name
|
||||
span Name
|
||||
div.custom-description
|
||||
span Description
|
||||
div.custom-options
|
||||
|
||||
div.table-body
|
||||
div.js-sortable
|
||||
form.js-form(ng-repeat="attr in customAttributes track by attr.id")
|
||||
div.row.single-custom-field.js-view-custom-field
|
||||
span.icon.icon-drag-v
|
||||
div.custom-name
|
||||
span {{ attr.name }}
|
||||
div.custom-description
|
||||
span {{ attr.description }}
|
||||
div.custom-options
|
||||
div.custom-options-wrapper
|
||||
a.js-edit-custom-field-button.icon.icon-edit(href="", title="Edit Custom Field")
|
||||
a.js-delete-custom-field-button.icon.icon-delete(href="", title="Delete Custom Field")
|
||||
|
||||
div.row.single-custom-field.js-edit-custom-field.hidden
|
||||
fieldset.custom-name
|
||||
input(type="text", name="name", placeholder="Set your custom field name",
|
||||
ng-model="attr.name", data-required="true" data-maxlength="64")
|
||||
fieldset.custom-description
|
||||
input(type="text", name="description", placeholder="Set your custom field description",
|
||||
ng-model="attr.description")
|
||||
|
||||
fieldset.custom-options
|
||||
div.custom-options-wrapper
|
||||
a.js-update-custom-field-button.icon.icon-floppy(href="", title="Update Custom Field")
|
||||
a.js-cancel-edit-custom-field-button.icon.icon-delete(href="", title="Cancel edition")
|
||||
|
||||
form.row.single-custom-field.js-new-custom-field.hidden
|
||||
fieldset.custom-name
|
||||
input(type="text", name="name", placeholder="Set your custom field name",
|
||||
ng-model="newAttr.name", data-required="true", data-maxlength="64")
|
||||
fieldset.custom-description
|
||||
input(type="text", name="description", placeholder="Set your custom field description",
|
||||
ng-model="newAttr.description")
|
||||
|
||||
fieldset.custom-options
|
||||
div.custom-options-wrapper
|
||||
a.js-create-custom-field-button.icon.icon-floppy(href="", title="Save Custom Field")
|
||||
a.js-cancel-new-custom-field-button.icon.icon-delete(href="", title="Cancel creation")
|
|
@ -35,6 +35,9 @@ div.wrapper(ng-controller="IssueDetailController as ctrl",
|
|||
|
||||
section.duty-content(tg-editable-description, ng-model="issue", required-perm="modify_issue")
|
||||
|
||||
// Custom Fields
|
||||
tg-custom-attributes-values(ng-model="issue", type="issue", project="project", required-edition-perm="modify_issue")
|
||||
|
||||
tg-attachments(ng-model="issue", type="issue")
|
||||
tg-history(ng-model="issue", type="issue")
|
||||
|
||||
|
|
|
@ -40,6 +40,9 @@ div.wrapper(ng-controller="TaskDetailController as ctrl",
|
|||
|
||||
section.duty-content(tg-editable-description, ng-model="task", required-perm="modify_task")
|
||||
|
||||
// Custom Fields
|
||||
tg-custom-attributes-values(ng-model="task", type="task", project="project", required-edition-perm="modify_task")
|
||||
|
||||
tg-attachments(ng-model="task", type="task")
|
||||
tg-history(ng-model="task", type="task")
|
||||
|
||||
|
|
|
@ -39,6 +39,9 @@ div.wrapper(ng-controller="UserStoryDetailController as ctrl",
|
|||
|
||||
section.duty-content(tg-editable-description, ng-model="us", required-perm="modify_us")
|
||||
|
||||
// Custom Fields
|
||||
tg-custom-attributes-values(ng-model="us", type="userstory", project="project", required-edition-perm="modify_us")
|
||||
|
||||
include ../includes/modules/related-tasks
|
||||
|
||||
tg-attachments(ng-model="us", type="us")
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
.custom-field-options {
|
||||
margin-bottom: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.custom-fields-table {
|
||||
.row {
|
||||
border-bottom: 0;
|
||||
padding: .5rem 0;
|
||||
|
||||
}
|
||||
.table-header {
|
||||
@extend %bold;
|
||||
border-bottom: 1px solid $gray-light;
|
||||
.custom-name span,
|
||||
.custom-description span {
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
}
|
||||
.table-body {
|
||||
.row:hover {
|
||||
background: rgba($fresh-taiga, .05);
|
||||
cursor: move;
|
||||
transition: background .2s linear;
|
||||
.icon-drag-v,
|
||||
.custom-options-wrapper {
|
||||
opacity: 1;
|
||||
transition: opacity .2s linear;
|
||||
}
|
||||
}
|
||||
form {
|
||||
&.row:hover {
|
||||
background: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.custom-description {
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
.single-custom-field {
|
||||
border-bottom: 1px solid $whitish;
|
||||
color: $gray;
|
||||
}
|
||||
.icon-drag-v {
|
||||
color: $gray-light;
|
||||
opacity: 0;
|
||||
padding: 0 .1rem;
|
||||
transition: color .2s linear;
|
||||
vertical-align: middle;
|
||||
&:hover {
|
||||
color: $gray;
|
||||
cursor: move;
|
||||
transition: color .2s linear;
|
||||
}
|
||||
}
|
||||
.custom-name,
|
||||
.custom-description {
|
||||
color: $gray;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
.custom-name {
|
||||
flex-basis: 25%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.custom-description {
|
||||
@include ellipsis(100%);
|
||||
flex-basis: 90%;
|
||||
flex-grow: 8;
|
||||
}
|
||||
.custom-options {
|
||||
flex-basis: 100px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
a {
|
||||
color: $gray-light;
|
||||
margin-right: .5rem;
|
||||
transition: color .2s linear;
|
||||
vertical-align: middle;
|
||||
&:hover {
|
||||
color: $green-taiga;
|
||||
transition: color .2s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-options-wrapper {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
}
|
||||
form {
|
||||
.custom-options-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
.duty-custom-fields {
|
||||
margin-bottom: 2rem;
|
||||
.custom-fields-header {
|
||||
@extend %bold;
|
||||
align-content: space-between;
|
||||
align-items: center;
|
||||
background: $whitish;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1rem;
|
||||
.icon-arrow-bottom {
|
||||
@extend %large;
|
||||
cursor: pointer;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform .2s linear;
|
||||
&.open {
|
||||
transform: rotate(0);
|
||||
transition: transform .2s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-fields-body {
|
||||
@include slide(1000px, hidden, $min: 0);
|
||||
}
|
||||
.custom-field-single {
|
||||
border-bottom: 1px solid $whitish;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
&:hover {
|
||||
.custom-field-options {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.editable {
|
||||
.custom-field-options {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
}
|
||||
.custom-field-options {
|
||||
opacity: 0;
|
||||
transition: opacity .2s linear;
|
||||
a {
|
||||
color: $gray-light;
|
||||
}
|
||||
a:hover {
|
||||
color: $green-taiga;
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-field-data {
|
||||
flex: 0;
|
||||
flex-basis: 200px;
|
||||
.custom-field-name {
|
||||
@extend %bold;
|
||||
display: block;
|
||||
}
|
||||
.custom-field-description {
|
||||
@extend %small;
|
||||
color: $gray-light;
|
||||
display: block;
|
||||
line-height: .9rem;
|
||||
}
|
||||
}
|
||||
.custom-field-options {
|
||||
margin: 0;
|
||||
}
|
||||
.custom-field-value {
|
||||
flex: 1;
|
||||
padding: 0 1rem 0 2rem;
|
||||
}
|
||||
form {
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ exports.files = function () {
|
|||
'modules/common/history',
|
||||
'modules/common/wizard',
|
||||
'modules/common/external-reference',
|
||||
'modules/common/custom-fields',
|
||||
|
||||
//Project modules
|
||||
'modules/home-projects-list',
|
||||
|
@ -125,6 +126,7 @@ exports.files = function () {
|
|||
'modules/admin/admin-membership-table',
|
||||
'modules/admin/admin-project-profile',
|
||||
'modules/admin/default-values',
|
||||
'modules/admin/admin-custom-attributes',
|
||||
'modules/admin/project-values',
|
||||
'modules/admin/third-parties',
|
||||
'modules/admin/admin-third-parties-webhooks',
|
||||
|
|
Loading…
Reference in New Issue