diff options
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v196.go | 22 | ||||
-rw-r--r-- | models/project_board.go | 20 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 3 | ||||
-rw-r--r-- | routers/web/repo/projects.go | 3 | ||||
-rw-r--r-- | services/forms/repo_form.go | 1 | ||||
-rw-r--r-- | templates/repo/graph.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/projects/view.tmpl | 24 | ||||
-rw-r--r-- | templates/repo/settings/webhook/slack.tmpl | 2 | ||||
-rw-r--r-- | web_src/js/features/projects.js | 51 | ||||
-rw-r--r-- | web_src/js/index.js | 21 | ||||
-rw-r--r-- | web_src/less/_base.less | 15 | ||||
-rw-r--r-- | web_src/less/_repository.less | 13 | ||||
-rw-r--r-- | web_src/less/features/projects.less | 39 |
14 files changed, 187 insertions, 31 deletions
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 753ca063d9..33b094e48c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -344,6 +344,8 @@ var migrations = []Migration{ NewMigration("Add Branch Protection Unprotected Files Column", addBranchProtectionUnprotectedFilesColumn), // v195 -> v196 NewMigration("Add table commit_status_index", addTableCommitStatusIndex), + // v196 -> v197 + NewMigration("Add Color to ProjectBoard table", addColorColToProjectBoard), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v196.go b/models/migrations/v196.go new file mode 100644 index 0000000000..c0332c7bb4 --- /dev/null +++ b/models/migrations/v196.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "xorm.io/xorm" +) + +func addColorColToProjectBoard(x *xorm.Engine) error { + type ProjectBoard struct { + Color string `xorm:"VARCHAR(7)"` + } + + if err := x.Sync2(new(ProjectBoard)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/project_board.go b/models/project_board.go index 6a35868511..e6c9f03386 100644 --- a/models/project_board.go +++ b/models/project_board.go @@ -5,6 +5,9 @@ package models import ( + "fmt" + "regexp" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -32,12 +35,16 @@ const ( ProjectBoardTypeBugTriage ) +// BoardColorPattern is a regexp witch can validate BoardColor +var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") + // ProjectBoard is used to represent boards on a project type ProjectBoard struct { ID int64 `xorm:"pk autoincr"` Title string - Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board - Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board + Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Color string `xorm:"VARCHAR(7)"` ProjectID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` @@ -100,6 +107,10 @@ func createBoardsForProjectsType(sess *xorm.Session, project *Project) error { // NewProjectBoard adds a new project board to a given project func NewProjectBoard(board *ProjectBoard) error { + if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { + return fmt.Errorf("bad color code: %s", board.Color) + } + _, err := db.GetEngine(db.DefaultContext).Insert(board) return err } @@ -178,6 +189,11 @@ func updateProjectBoard(e db.Engine, board *ProjectBoard) error { fieldToUpdate = append(fieldToUpdate, "title") } + if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) { + return fmt.Errorf("bad color code: %s", board.Color) + } + fieldToUpdate = append(fieldToUpdate, "color") + _, err := e.ID(board.ID).Cols(fieldToUpdate...).Update(board) return err diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e6a29a5c8a..028c25a768 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -96,6 +96,7 @@ error = Error error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it. never = Never +color = Color [error] occurred = An error has occurred @@ -977,7 +978,6 @@ commit_graph = Commit Graph commit_graph.select = Select branches commit_graph.hide_pr_refs = Hide Pull Requests commit_graph.monochrome = Mono -commit_graph.color = Color blame = Blame normal_view = Normal View line = line @@ -1793,7 +1793,6 @@ settings.slack_username = Username settings.slack_icon_url = Icon URL settings.discord_username = Username settings.discord_icon_url = Icon URL -settings.slack_color = Color settings.event_desc = Trigger On: settings.event_push_only = Push Events settings.event_send_everything = All Events diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 556656e5b5..2490efc923 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -444,6 +444,7 @@ func AddBoardToProjectPost(ctx *context.Context) { if err := models.NewProjectBoard(&models.ProjectBoard{ ProjectID: project.ID, Title: form.Title, + Color: form.Color, CreatorID: ctx.User.ID, }); err != nil { ctx.ServerError("NewProjectBoard", err) @@ -513,6 +514,8 @@ func EditProjectBoard(ctx *context.Context) { board.Title = form.Title } + board.Color = form.Color + if form.Sorting != 0 { board.Sorting = form.Sorting } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1210d5dfc5..7c61be5e22 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -499,6 +499,7 @@ type UserCreateProjectForm struct { type EditProjectBoardForm struct { Title string `binding:"Required;MaxSize(100)"` Sorting int8 + Color string `binding:"MaxSize(7)"` } // _____ .__.__ __ diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index e3b5cd3878..963bf0449a 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -47,7 +47,7 @@ </div> </div> <button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "mr-2"}}{{.i18n.Tr "repo.commit_graph.monochrome"}}</button> - <button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "mr-2"}}{{.i18n.Tr "repo.commit_graph.color"}}</button> + <button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{.i18n.Tr "color"}}">{{svg "material-palette" 16 "mr-2"}}{{.i18n.Tr "color"}}</button> </div> </h2> <div class="ui dividing"></div> diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 9d08462b1c..082f4d8eb9 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -11,7 +11,7 @@ <a class="ui green button show-modal item" href="{{$.RepoLink}}/issues/new?project={{$.Project.ID}}">{{.i18n.Tr "repo.issues.new"}}</a> <a class="ui green button show-modal item" data-modal="#new-board-item">{{.i18n.Tr "new_project_board"}}</a> {{end}} - <div class="ui small modal" id="new-board-item"> + <div class="ui small modal new-board-modal" id="new-board-item"> <div class="header"> {{$.i18n.Tr "repo.projects.board.new"}} </div> @@ -22,6 +22,16 @@ <input class="new-board" id="new_board" name="title" required> </div> + <div class="field color-field"> + <label for="new_board_color">{{$.i18n.Tr "color"}}</label> + <div class="color picker column"> + <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_board_color_picker" name="color"> + <div class="column precolors"> + {{template "repo/issue/label_precolors"}} + </div> + </div> + </div> + <div class="text right actions"> <div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div> <button data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}" class="ui green button" id="new_board_submit">{{$.i18n.Tr "repo.projects.board.new_submit"}}</button> @@ -70,7 +80,7 @@ <div class="board"> {{ range $board := .Boards }} - <div class="ui segment board-column" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}"> + <div class="ui segment board-column" style="background: {{.Color}}!important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}"> <div class="board-column-header df ac sb"> <div class="ui large label board-label py-2">{{.Title}}</div> {{if and $.CanWriteProjects (not $.Repository.IsArchived) $.PageIsProjects (ne .ID 0)}} @@ -105,6 +115,16 @@ <input class="project-board-title" id="new_board_title" name="title" value="{{.Title}}" required> </div> + <div class="field color-field"> + <label for="new_board_color">{{$.i18n.Tr "color"}}</label> + <div class="color picker column"> + <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_board_color" name="color" value="{{.Color}}"> + <div class="column precolors"> + {{template "repo/issue/label_precolors"}} + </div> + </div> + </div> + <div class="text right actions"> <div class="ui cancel button">{{$.i18n.Tr "settings.cancel"}}</div> <button data-url="{{$.RepoLink}}/projects/{{$.Project.ID}}/{{.ID}}" class="ui red button">{{$.i18n.Tr "repo.projects.board.edit"}}</button> diff --git a/templates/repo/settings/webhook/slack.tmpl b/templates/repo/settings/webhook/slack.tmpl index d7b6eebf3b..12e112acca 100644 --- a/templates/repo/settings/webhook/slack.tmpl +++ b/templates/repo/settings/webhook/slack.tmpl @@ -20,7 +20,7 @@ <input id="icon_url" name="icon_url" value="{{.SlackHook.IconURL}}" placeholder="e.g. https://example.com/img/favicon.png"> </div> <div class="field"> - <label for="color">{{.i18n.Tr "repo.settings.slack_color"}}</label> + <label for="color">{{.i18n.Tr "color"}}</label> <input id="color" name="color" value="{{.SlackHook.Color}}" placeholder="e.g. #dd4b39, good, warning, danger"> </div> {{template "repo/settings/webhook/settings" .}} diff --git a/web_src/js/features/projects.js b/web_src/js/features/projects.js index 0d619cab70..c02b81d1c6 100644 --- a/web_src/js/features/projects.js +++ b/web_src/js/features/projects.js @@ -23,7 +23,7 @@ export default async function initProject() { if (parseInt($(column).data('sorting')) !== i) { $.ajax({ url: $(column).data('url'), - data: JSON.stringify({sorting: i}), + data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}), headers: { 'X-Csrf-Token': csrf, 'X-Remote': true, @@ -62,10 +62,17 @@ export default async function initProject() { } $('.edit-project-board').each(function () { - const projectTitleLabel = $(this).closest('.board-column-header').find('.board-label'); + const projectHeader = $(this).closest('.board-column-header'); + const projectTitleLabel = projectHeader.find('.board-label'); const projectTitleInput = $(this).find( '.content > .form > .field > .project-board-title', ); + const projectColorInput = $(this).find('.content > .form > .field #new_board_color'); + const boardColumn = $(this).closest('.board-column'); + + if (boardColumn.css('backgroundColor')) { + setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor'))); + } $(this) .find('.content > .form > .actions > .red') @@ -74,7 +81,7 @@ export default async function initProject() { $.ajax({ url: $(this).data('url'), - data: JSON.stringify({title: projectTitleInput.val()}), + data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}), headers: { 'X-Csrf-Token': csrf, 'X-Remote': true, @@ -84,6 +91,10 @@ export default async function initProject() { }).done(() => { projectTitleLabel.text(projectTitleInput.val()); projectTitleInput.closest('form').removeClass('dirty'); + if (projectColorInput.val()) { + setLabelColor(projectHeader, projectColorInput.val()); + } + boardColumn.attr('style', `background: ${projectColorInput.val()}!important`); $('.ui.modal').modal('hide'); }); }); @@ -127,10 +138,11 @@ export default async function initProject() { e.preventDefault(); const boardTitle = $('#new_board'); + const projectColorInput = $('#new_board_color_picker'); $.ajax({ url: $(this).data('url'), - data: JSON.stringify({title: boardTitle.val()}), + data: JSON.stringify({title: boardTitle.val(), color: projectColorInput.val()}), headers: { 'X-Csrf-Token': csrf, 'X-Remote': true, @@ -143,3 +155,34 @@ export default async function initProject() { }); }); } + +function setLabelColor(label, color) { + const red = getRelativeColor(parseInt(color.substr(1, 2), 16)); + const green = getRelativeColor(parseInt(color.substr(3, 2), 16)); + const blue = getRelativeColor(parseInt(color.substr(5, 2), 16)); + const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + + if (luminance > 0.179) { + label.removeClass('light-label').addClass('dark-label'); + } else { + label.removeClass('dark-label').addClass('light-label'); + } +} + +/** + * Inspired by W3C recommandation https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ +function getRelativeColor(color) { + color /= 255; + return color <= 0.03928 ? color / 12.92 : ((color + 0.055) / 1.055) ** 2.4; +} + +function rgbToHex(rgb) { + rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`; +} + +function hex(x) { + const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16]; +} diff --git a/web_src/js/index.js b/web_src/js/index.js index a092452e6b..5c1510fb89 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -159,13 +159,8 @@ function initLabelEdit() { $newLabelPanel.hide(); }); - createColorPicker($('.color-picker')); + initColorPicker(); - $('.precolors .color').on('click', function () { - const color_hex = $(this).data('color-hex'); - $('.color-picker').val(color_hex); - $('.minicolors-swatch-color').css('background-color', color_hex); - }); $('.edit-label-button').on('click', function () { $('.edit-label .color-picker').minicolors('value', $(this).data('color')); $('#label-modal-id').val($(this).data('id')); @@ -182,6 +177,16 @@ function initLabelEdit() { }); } +function initColorPicker() { + createColorPicker($('.color-picker')); + + $('.precolors .color').on('click', function () { + const color_hex = $(this).data('color-hex'); + $('.color-picker').val(color_hex); + $('.minicolors-swatch-color').css('background-color', color_hex); + }); +} + function updateIssuesMeta(url, action, issueIds, elementId) { return new Promise(((resolve) => { $.ajax({ @@ -2753,6 +2758,10 @@ $(document).ready(async () => { }); $('.show-modal.button').on('click', function () { $($(this).data('modal')).modal('show'); + const colorPickers = $($(this).data('modal')).find('.color-picker'); + if (colorPickers.length > 0) { + initColorPicker(); + } }); $('.delete-post.button').on('click', function () { const $this = $(this); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 4702cb281e..4e2782a4c8 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -114,6 +114,8 @@ --color-placeholder-text: #aaa; --color-editor-line-highlight: var(--color-primary-light-6); --color-project-board-bg: var(--color-secondary-light-4); + --color-project-board-dark-label: #555555; + --color-project-board-light-label: #a6aab5; --color-caret: var(--color-text-dark); --color-reaction-bg: #0000000a; --color-reaction-active-bg: var(--color-primary-alpha-20); @@ -2090,3 +2092,16 @@ table th[data-sortt-desc] { margin-top: -.5em; margin-bottom: -.5em; } + +.precolors { + padding-left: 0; + padding-right: 0; + margin: 3px 10px auto; + width: 120px; + + .color { + float: left; + width: 15px; + height: 15px; + } +} diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index b52045dae1..1e572ffa7e 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -2696,19 +2696,6 @@ width: 15px; height: 15px; } - - .precolors { - padding-left: 0; - padding-right: 0; - margin: 3px 10px auto; - width: 120px; - - .color { - float: left; - width: 15px; - height: 15px; - } - } } } diff --git a/web_src/less/features/projects.less b/web_src/less/features/projects.less index f9d97a2086..99722b6cc5 100644 --- a/web_src/less/features/projects.less +++ b/web_src/less/features/projects.less @@ -23,6 +23,21 @@ .board-column-header { display: flex; justify-content: space-between; + + &.dark-label { + color: var(--color-project-board-dark-label) !important; + + .board-label { + color: var(--color-project-board-dark-label) !important; + } + } + &.light-label { + color: var(--color-project-board-light-label) !important; + + .board-label { + color: var(--color-project-board-light-label) !important; + } + } } .board-label { @@ -81,3 +96,27 @@ .card-ghost * { opacity: 0; } + +.color-field .minicolors.minicolors-theme-default { + display: block; + + .minicolors-input { + height: 38px; + padding-left: 2rem; + } + + .minicolors-swatch { + top: 10px; + } +} + +.edit-project-board, +.new-board-modal { + .color.picker.column { + display: flex; + + .minicolors { + flex: 1; + } + } +} |