From cfa01d6b013fb9f9ce44021b80f5987192186808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sat, 3 Feb 2024 17:45:32 +0100 Subject: [PATCH 01/46] feat(board-notes): add, edit and delete of BoardNotes --- models/project/board.go | 20 ++++ models/project/note.go | 159 +++++++++++++++++++++++++++ models/project/project.go | 19 ++++ routers/web/repo/projects.go | 120 ++++++++++++++++++++ routers/web/web.go | 11 ++ services/forms/repo_form.go | 6 + templates/projects/view.tmpl | 46 +++++++- templates/repo/note.tmpl | 71 ++++++++++++ web_src/css/features/projects.css | 4 + web_src/css/index.css | 1 + web_src/css/repo/note-card.css | 21 ++++ web_src/js/features/repo-projects.js | 100 +++++++++++++++-- 12 files changed, 566 insertions(+), 12 deletions(-) create mode 100644 models/project/note.go create mode 100644 templates/repo/note.tmpl create mode 100644 web_src/css/repo/note-card.css diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472c51..d49d59e8ffd59 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -82,6 +82,26 @@ func (b *Board) NumIssues(ctx context.Context) int { return int(c) } +// NumNotes return counter of all notes assigned to the board +func (b *Board) NumNotes(ctx context.Context) int { + c, err := db.GetEngine(ctx).Table("board_note"). + And("board_id=?", b.ID). + GroupBy("id"). + Cols("id"). + Count() + if err != nil { + return 0 + } + return int(c) +} + +// NumIssuesAndNotes return counter of all issues and notes assigned to the board +func (b *Board) NumIssuesAndNotes(ctx context.Context) int { + numIssues := b.NumIssues(ctx) + numNotes := b.NumNotes(ctx) + return numIssues + numNotes +} + func init() { db.RegisterModel(new(Board)) } diff --git a/models/project/note.go b/models/project/note.go new file mode 100644 index 0000000000000..855efddf62bf8 --- /dev/null +++ b/models/project/note.go @@ -0,0 +1,159 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +// BoardNote is used to represent a note on a boards +type BoardNote struct { + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + + BoardID int64 `xorm:"INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL"` + Creator *user_model.User `xorm:"-"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +type BoardNoteList = []*BoardNote + +// NotesOptions represents options of an note. +type NotesOptions struct { //nolint + db.Paginator + BoardID int64 +} + +func init() { + db.RegisterModel(new(BoardNote)) +} + +// GetBoardNoteById load notes assigned to the boards +func GetBoardNoteById(ctx context.Context, noteID int64) (*BoardNote, error) { + note := new(BoardNote) + + has, err := db.GetEngine(ctx).ID(noteID).Get(note) + if err != nil { + return nil, err + } else if !has { + return nil, ErrProjectBoardNoteNotExist{BoardNoteID: noteID} + } + + return note, nil +} + +// LoadNotesFromBoardList load notes assigned to the boards +func (p *Project) LoadNotesFromBoardList(ctx context.Context, bs BoardList) (map[int64]BoardNoteList, error) { + notesMap := make(map[int64]BoardNoteList, len(bs)) + for i := range bs { + il, err := LoadNotesFromBoard(ctx, bs[i]) + if err != nil { + return nil, err + } + notesMap[bs[i].ID] = il + } + return notesMap, nil +} + +// LoadNotesFromBoard load notes assigned to this board +func LoadNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { + noteList := make(BoardNoteList, 0, 5) + + if board.ID != 0 { + notes, err := BoardNotes(ctx, &NotesOptions{ + BoardID: board.ID, + }) + if err != nil { + return nil, err + } + noteList = notes + } + + return noteList, nil +} + +// BoardNotes returns a list of notes by given conditions. +func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) { + sess := db.GetEngine(ctx) + + if opts.BoardID != 0 { + if opts.BoardID > 0 { + sess.Where(builder.Eq{"board_id": opts.BoardID}) + } else { + sess.Where(builder.Eq{"board_id": 0}) + } + } + + notes := BoardNoteList{} + if err := sess.Find(¬es); err != nil { + return nil, fmt.Errorf("unable to query Notes: %w", err) + } + + for _, note := range notes { + creator := new(user_model.User) + has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) + if err != nil { + return nil, err + } + if !has { + return nil, user_model.ErrUserNotExist{UID: note.CreatorID} + } + note.Creator = creator + } + + return notes, nil +} + +// NewBoardNote adds a new note to a given board +func NewBoardNote(ctx context.Context, note *BoardNote) error { + _, err := db.GetEngine(ctx).Insert(note) + return err +} + +// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close. +func (note *BoardNote) GetLastEventTimestamp() timeutil.TimeStamp { + return max(note.CreatedUnix, note.UpdatedUnix) +} + +// GetLastEventLabel returns the localization label for the current note. +func (note *BoardNote) GetLastEventLabel() string { + if note.UpdatedUnix > note.CreatedUnix { + return "repo.projects.note.edited_by" + } + return "repo.projects.note.created_by" +} + +// UpdateBoardNote changes a BoardNote +func UpdateBoardNote(ctx context.Context, note *BoardNote) error { + var fieldToUpdate []string + + if note.Title != "" { + fieldToUpdate = append(fieldToUpdate, "title") + } + + fieldToUpdate = append(fieldToUpdate, "content") + + _, err := db.GetEngine(ctx).ID(note.ID).Cols(fieldToUpdate...).Update(note) + + return err +} + +// DeleteBoardNote removes the BoardNote from the project board. +func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { + if _, err := db.GetEngine(ctx).Delete(boardNote); err != nil { + return err + } + return nil +} diff --git a/models/project/project.go b/models/project/project.go index d2fca6cdc8a8a..eca49f85a0d15 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -85,6 +85,25 @@ func (err ErrProjectBoardNotExist) Unwrap() error { return util.ErrNotExist } +// ErrProjectBoardNoteNotExist represents a "ProjectBoardNotExist" kind of error. +type ErrProjectBoardNoteNotExist struct { + BoardNoteID int64 +} + +// IsErrProjectBoardNoteNotExist checks if an error is a ErrProjectBoardNoteNotExist +func IsErrProjectBoardNoteNotExist(err error) bool { + _, ok := err.(ErrProjectBoardNoteNotExist) + return ok +} + +func (err ErrProjectBoardNoteNotExist) Error() string { + return fmt.Sprintf("project board-note does not exist [id: %d]", err.BoardNoteID) +} + +func (err ErrProjectBoardNoteNotExist) Unwrap() error { + return util.ErrNotExist +} + // Project represents a project board type Project struct { ID int64 `xorm:"pk autoincr"` diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4908bb796d9dc..c2c9ccc410b18 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -324,6 +324,12 @@ func ViewProject(ctx *context.Context) { return } + notesMap, err := project.LoadNotesFromBoardList(ctx, boards) + if err != nil { + ctx.ServerError("LoadNotesOfBoards", err) + return + } + if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) for _, issuesList := range issuesMap { @@ -376,6 +382,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend + ctx.Data["NotesMap"] = notesMap ctx.HTML(http.StatusOK, tplProjectsView) } @@ -698,3 +705,116 @@ func MoveIssues(ctx *context.Context) { ctx.JSONOK() } + +func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.BoardNote) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return nil, nil + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return nil, nil + } + + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetProjectByID", err) + } + return nil, nil + } + + note, err := project_model.GetBoardNoteById(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + if project_model.IsErrProjectBoardNoteNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetBoardNoteById", err) + } + return nil, nil + } + boardID := ctx.ParamsInt64(":boardID") + if note.BoardID != boardID { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Board[%d] as expected", note.ID, boardID), + }) + return nil, nil + } + + if project.RepoID != ctx.Repo.Repository.ID { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", note.ID, ctx.Repo.Repository.ID), + }) + return nil, nil + } + return project, note +} + +func AddNoteToBoard(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.BoardNoteForm) + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + if err := project_model.NewBoardNote(ctx, &project_model.BoardNote{ + Title: form.Title, + Content: form.Content, + + BoardID: ctx.ParamsInt64(":boardID"), + CreatorID: ctx.Doer.ID, + }); err != nil { + ctx.ServerError("NewProjectBoard", err) + return + } + + ctx.JSONOK() +} + +func EditBoardNote(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.BoardNoteForm) + _, note := checkProjectBoardNoteChangePermissions(ctx) + if ctx.Written() { + return + } + + if form.Title != "" { + note.Title = form.Title + } + + if form.Content != "" { + note.Content = form.Content + } + + if err := project_model.UpdateBoardNote(ctx, note); err != nil { + ctx.ServerError("UpdateProjectBoardNote", err) + return + } + + ctx.JSONOK() +} + +func DeleteBoardNote(ctx *context.Context) { + _, boardNote := checkProjectBoardNoteChangePermissions(ctx) + + if err := project_model.DeleteBoardNote(ctx, boardNote); err != nil { + ctx.ServerError("DeleteBoardNote", err) + return + } + + ctx.JSONOK() +} + +func MoveBoardNote(ctx *context.Context) { + + ctx.JSONOK() +} diff --git a/routers/web/web.go b/routers/web/web.go index 92cf5132b45b6..c2cf620c175f9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1341,6 +1341,17 @@ func registerRoutes(m *web.Route) { m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard) m.Post("/move", repo.MoveIssues) + + m.Group("/note", func() { + m.Post("", web.Bind(forms.BoardNoteForm{}), repo.AddNoteToBoard) + + m.Group("/{noteID}", func() { + m.Put("", web.Bind(forms.BoardNoteForm{}), repo.EditBoardNote) + m.Delete("", repo.DeleteBoardNote) + + m.Post("/move", repo.MoveBoardNote) + }) + }) }) }) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 845eccf817d3a..c412ada440379 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -533,6 +533,12 @@ type EditProjectBoardForm struct { Color string `binding:"MaxSize(7)"` } +// BoardNoteForm is a form for editing/creating a note to a board +type BoardNoteForm struct { + Title string `binding:"Required;MaxSize(255)"` + Content string +} + // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index b3ad03c354988..f34d2461dc07d 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -70,7 +70,7 @@ <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> <div class="ui small circular grey label project-column-issue-count"> - {{.NumIssues ctx}} + {{.NumIssuesAndNotes ctx}} </div> {{.Title}} </div> @@ -80,6 +80,12 @@ {{svg "octicon-kebab-horizontal"}} </div> <div class="menu user-menu"> + <a class="item show-modal button show-add-project-column-note-modal" + data-modal="#add-project-column-note-modal-{{.ID}}" + data-url="{{$.Link}}/{{.ID}}/note"> + {{svg "octicon-note"}} + {{ctx.Locale.Tr "repo.projects.note.add"}} + </a> <a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.column.edit"}} @@ -110,6 +116,25 @@ {{ctx.Locale.Tr "repo.projects.column.delete"}} </a> + <div class="ui small modal" id="add-project-column-note-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.add"}} + </div> + <div class="content"> + <div class="required field"> + <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" required> + </div> + + <div class="field content-field"> + <label for="new-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> + <textarea id="new-project-column-note-content-{{.ID}}" name="content"></textarea> + </div> + </div> + + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.add"))}} + </div> + <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.column.edit"}} @@ -165,8 +190,23 @@ <div class="divider"></div> - <div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> - {{range (index $.IssuesMap .ID)}} + {{ $columnID := .ID }} + {{if or $canWriteProject (index $.NotesMap $columnID)}} + <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> + {{range (index $.NotesMap $columnID)}} + <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> + {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID))}} + </div> + {{end}} + </div> + + {{if or $canWriteProject (index $.IssuesMap $columnID)}} + <div class="divider"></div> + {{end}} + {{end}} + + <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> + {{range (index $.IssuesMap $columnID)}} <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> {{template "repo/issue/card" (dict "Issue" . "Page" $)}} </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl new file mode 100644 index 0000000000000..9e950d412d989 --- /dev/null +++ b/templates/repo/note.tmpl @@ -0,0 +1,71 @@ +{{with .BoardNote}} +<div class="content gt-p-0 gt-w-100"> + <div class="gt-df gt-items-start"> + <div class="project-column-note-card-icon"> + {{svg "octicon-note"}} + </div> + <span class="note-card-title muted project-column-note-title">{{.Title}}</span> + {{ if $.CanWriteProjects }} + <div class="ui dropdown jump item"> + <div class="gt-px-3"> + {{svg "octicon-kebab-horizontal"}} + </div> + <div class="menu user-menu left"> + <a class="item show-modal button show-edit-project-column-note-modal" + data-modal="#edit-project-column-note-modal-{{.ID}}" + data-url="{{$.Link}}/note/{{.ID}}"> + {{svg "octicon-pencil"}} + {{ctx.Locale.Tr "repo.projects.note.edit"}} + </a> + <a class="item show-modal button show-delete-project-column-note-modal" + data-modal="#delete-project-column-note-modal-{{.ID}}" + data-url="{{$.Link}}/note/{{.ID}}"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "repo.projects.note.delete"}} + </a> + + <div class="ui small modal" id="edit-project-column-note-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.edit"}} + </div> + <div class="content"> + <div class="required field"> + <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" required> + </div> + + <div class="field content-field"> + <label for="edit-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> + <textarea id="edit-project-column-note-content-{{.ID}}" name="content">{{.Content}}</textarea> + </div> + </div> + + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.edit"))}} + </div> + + <div class="ui g-modal-confirm modal" id="delete-project-column-note-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.delete"}} + </div> + <div class="content"> + <label> + {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} + </label> + </div> + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} + </div> + </div> + </div> + {{end}} + </div> + <div class="project-column-note-content"> + {{.Content}} + </div> + <div class="meta gt-my-2"> + <span class="text light grey muted-links"> + {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} + {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} + </span> + </div> +</div> +{{end}} \ No newline at end of file diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index f85430a2a80ed..3285b151463e4 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -60,6 +60,10 @@ gap: .25rem; } +.project-column > .note-cards { + flex: unset; +} + .project-column > .divider { margin: 5px 0; } diff --git a/web_src/css/index.css b/web_src/css/index.css index f893531b781cb..a83a9ebd01d6f 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -45,6 +45,7 @@ @import "./repo/issue-label.css"; @import "./repo/issue-list.css"; @import "./repo/list-header.css"; +@import "./repo/note-card.css"; @import "./repo/linebutton.css"; @import "./repo/wiki.css"; @import "./repo/header.css"; diff --git a/web_src/css/repo/note-card.css b/web_src/css/repo/note-card.css new file mode 100644 index 0000000000000..1d682111ed913 --- /dev/null +++ b/web_src/css/repo/note-card.css @@ -0,0 +1,21 @@ +.note-card { + display: flex; + flex-direction: column; + align-items: start; + border-radius: var(--border-radius); + padding: 8px 10px; + border: 1px solid var(--color-secondary); + background: var(--color-card); +} + +.note-card-icon, +.note-card-unpin { + margin-top: 1px; + flex-shrink: 0; +} + +.note-card-title { + flex: 1; + font-size: 18px; + margin-left: 4px; +} diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 5a2a7e72ef335..aa21cfd4074bd 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -11,17 +11,17 @@ function updateIssueCount(cards) { parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt; } -function createNewColumn(url, columnTitle, projectColorInput) { +function sendPostRequestAndUnsetDirty(url, form, data) { $.ajax({ url, - data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}), + data: JSON.stringify(data), headers: { 'X-Csrf-Token': csrfToken, }, contentType: 'application/json', method: 'POST', }).done(() => { - columnTitle.closest('form').removeClass('dirty'); + form.removeClass('dirty'); window.location.reload(); }); } @@ -87,15 +87,25 @@ async function initRepoProjectSortable() { }); for (const boardColumn of boardColumns) { - const boardCardList = boardColumn.getElementsByClassName('cards')[0]; - createSortable(boardCardList, { - group: 'shared', + const boardIssueCardList = boardColumn.getElementsByClassName('issue-cards')[0]; + const boardNoteCardList = boardColumn.getElementsByClassName('note-cards')[0]; + createSortable(boardIssueCardList, { + group: 'issue-shared', animation: 150, ghostClass: 'card-ghost', onAdd: moveIssue, onUpdate: moveIssue, delayOnTouchOnly: true, - delay: 500, + delay: 500 + }); + createSortable(boardNoteCardList, { + group: 'note-shared', + animation: 150, + ghostClass: 'card-ghost', + onAdd: () => { console.error('@TODO') }, + onUpdate: () => { console.error('@TODO') }, + delayOnTouchOnly: true, + delay: 500 }); } } @@ -183,7 +193,7 @@ export function initRepoProject() { }); }); - $('#new_project_column_submit').on('click', (e) => { + $('#new_project_column_submit').on('click', function (e) { e.preventDefault(); const columnTitle = $('#new_project_column'); const projectColorInput = $('#new_project_column_color_picker'); @@ -191,7 +201,79 @@ export function initRepoProject() { return; } const url = $(this).data('url'); - createNewColumn(url, columnTitle, projectColorInput); + const form = columnTitle.closest('form'); + sendPostRequestAndUnsetDirty(url, form, {title: columnTitle.val(), color: projectColorInput.val()}); + }); + + $('.show-add-project-column-note-modal').each(function () { + const addNoteUrl = $(this).data('url'); + const modalId = $(this).data('modal'); + const addModal = $(modalId); + + const confirmAddButton = addModal.find('.actions > .ok.button'); + confirmAddButton.on('click', (e) => { + e.preventDefault(); + const noteTitleInput = addModal.find('[name="title"]'); + const contentTextarea = addModal.find('[name="content"]'); + if (!noteTitleInput.val()) { + return; + } + const form = noteTitleInput.closest('form'); + sendPostRequestAndUnsetDirty(addNoteUrl, form, {title: noteTitleInput.val(), content: contentTextarea.val()}); + }); + }); + $('.show-edit-project-column-note-modal').each(function () { + const editNoteUrl = $(this).data('url'); + const modalId = $(this).data('modal'); + const wrapperCard = $(this).closest('.note-card'); + + const noteTitle = wrapperCard.find('.project-column-note-title'); + const noteContent = wrapperCard.find('.project-column-note-content'); + + const editModal = wrapperCard.find(modalId); + const noteTitleInput = editModal.find('[name="title"]'); + const noteContentTextarea = editModal.find('[name="content"]'); + + const confirmEditButton = editModal.find('.actions > .ok.button'); + confirmEditButton.on('click', (e) => { + e.preventDefault(); + + $.ajax({ + url: editNoteUrl, + data: JSON.stringify({title: noteTitleInput.val(), content: noteContentTextarea.val()}), + headers: { + 'X-Csrf-Token': csrfToken, + }, + contentType: 'application/json', + method: 'PUT', + }).done(() => { + noteTitle.text(noteTitleInput.val()); + noteContent.text(noteContentTextarea.val()); + editModal.removeClass('dirty'); + $('.ui.modal').modal('hide'); + }); + }); + }); + $('.show-delete-project-column-note-modal').each(function () { + const deleteNoteUrl = $(this).data('url'); + const modalId = $(this).data('modal'); + const deletModal = $(modalId); + + const confirmDeleteButton = deletModal.find('.actions > .ok.button'); + confirmDeleteButton.on('click', (e) => { + e.preventDefault(); + + $.ajax({ + url: deleteNoteUrl, + headers: { + 'X-Csrf-Token': csrfToken, + }, + contentType: 'application/json', + method: 'DELETE', + }).done(() => { + window.location.reload(); + }); + }); }); } From 79f5d58c441a8b34ea64af0d7603f04ca7fe3758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sat, 3 Feb 2024 18:02:11 +0100 Subject: [PATCH 02/46] feat(board-notes): styles for card-content and modals --- templates/projects/view.tmpl | 16 +++++++++------- templates/repo/note.tmpl | 28 ++++++++++++++++------------ web_src/css/repo/note-card.css | 6 ++++++ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index f34d2461dc07d..a951356164052 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -121,14 +121,16 @@ {{ctx.Locale.Tr "repo.projects.note.add"}} </div> <div class="content"> - <div class="required field"> - <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> - <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" required> - </div> + <div class="ui form"> + <div class="required field"> + <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" required> + </div> - <div class="field content-field"> - <label for="new-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> - <textarea id="new-project-column-note-content-{{.ID}}" name="content"></textarea> + <div class="field content-field"> + <label for="new-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> + <textarea id="new-project-column-note-content-{{.ID}}" name="content"></textarea> + </div> </div> </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 9e950d412d989..85fa7a9aff8fe 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -29,14 +29,16 @@ {{ctx.Locale.Tr "repo.projects.note.edit"}} </div> <div class="content"> - <div class="required field"> - <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> - <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" required> - </div> - - <div class="field content-field"> - <label for="edit-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> - <textarea id="edit-project-column-note-content-{{.ID}}" name="content">{{.Content}}</textarea> + <div class="ui form"> + <div class="required field"> + <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" required> + </div> + + <div class="field content-field"> + <label for="edit-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> + <textarea id="edit-project-column-note-content-{{.ID}}" name="content">{{.Content}}</textarea> + </div> </div> </div> @@ -58,10 +60,12 @@ </div> {{end}} </div> - <div class="project-column-note-content"> - {{.Content}} - </div> - <div class="meta gt-my-2"> + {{if and .Content (gt (len .Content) 0)}} + <div class="project-column-note-content"> + {{.Content}} + </div> + {{end}} + <div class="meta"> <span class="text light grey muted-links"> {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} diff --git a/web_src/css/repo/note-card.css b/web_src/css/repo/note-card.css index 1d682111ed913..5cf2b8dc3fc16 100644 --- a/web_src/css/repo/note-card.css +++ b/web_src/css/repo/note-card.css @@ -8,6 +8,12 @@ background: var(--color-card); } +.note-card > .content { + display: flex; + flex-direction: column; + row-gap: 0.5rem; +} + .note-card-icon, .note-card-unpin { margin-top: 1px; From c668d8eec2e58c1bcbbab6b7a9b62d76bb347016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sat, 3 Feb 2024 21:13:54 +0100 Subject: [PATCH 03/46] feat(board-notes): move notes like issues --- models/project/note.go | 65 +++++++++++--------- routers/web/repo/projects.go | 92 +++++++++++++++++++++++++++- routers/web/web.go | 3 +- templates/projects/view.tmpl | 4 +- web_src/css/features/projects.css | 10 +++ web_src/js/features/repo-projects.js | 41 ++++++++++--- 6 files changed, 173 insertions(+), 42 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index 855efddf62bf8..540dc8a949dd6 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -19,6 +19,7 @@ type BoardNote struct { ID int64 `xorm:"pk autoincr"` Title string `xorm:"TEXT NOT NULL"` Content string `xorm:"LONGTEXT"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` BoardID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` @@ -54,6 +55,17 @@ func GetBoardNoteById(ctx context.Context, noteID int64) (*BoardNote, error) { return note, nil } +// GetBoardNoteByIds return notes with the given IDs. +func GetBoardNoteByIds(ctx context.Context, noteIDs []int64) (BoardNoteList, error) { + notes := make(BoardNoteList, 0, len(noteIDs)) + + if err := db.GetEngine(ctx).In("id", noteIDs).Find(¬es); err != nil { + return nil, err + } + + return notes, nil +} + // LoadNotesFromBoardList load notes assigned to the boards func (p *Project) LoadNotesFromBoardList(ctx context.Context, bs BoardList) (map[int64]BoardNoteList, error) { notesMap := make(map[int64]BoardNoteList, len(bs)) @@ -69,35 +81,24 @@ func (p *Project) LoadNotesFromBoardList(ctx context.Context, bs BoardList) (map // LoadNotesFromBoard load notes assigned to this board func LoadNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { - noteList := make(BoardNoteList, 0, 5) - - if board.ID != 0 { - notes, err := BoardNotes(ctx, &NotesOptions{ - BoardID: board.ID, - }) - if err != nil { - return nil, err - } - noteList = notes + notes, err := BoardNotes(ctx, &NotesOptions{ + BoardID: board.ID, + }) + if err != nil { + return nil, err } - return noteList, nil + return notes, nil } // BoardNotes returns a list of notes by given conditions. func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) { sess := db.GetEngine(ctx) - if opts.BoardID != 0 { - if opts.BoardID > 0 { - sess.Where(builder.Eq{"board_id": opts.BoardID}) - } else { - sess.Where(builder.Eq{"board_id": 0}) - } - } + sess.Where(builder.Eq{"board_id": max(opts.BoardID, 0)}) notes := BoardNoteList{} - if err := sess.Find(¬es); err != nil { + if err := sess.OrderBy("Sorting").Find(¬es); err != nil { return nil, fmt.Errorf("unable to query Notes: %w", err) } @@ -130,24 +131,30 @@ func (note *BoardNote) GetLastEventTimestamp() timeutil.TimeStamp { // GetLastEventLabel returns the localization label for the current note. func (note *BoardNote) GetLastEventLabel() string { if note.UpdatedUnix > note.CreatedUnix { - return "repo.projects.note.edited_by" + return "repo.projects.note.updated_by" } return "repo.projects.note.created_by" } // UpdateBoardNote changes a BoardNote func UpdateBoardNote(ctx context.Context, note *BoardNote) error { - var fieldToUpdate []string - - if note.Title != "" { - fieldToUpdate = append(fieldToUpdate, "title") - } - - fieldToUpdate = append(fieldToUpdate, "content") + _, err := db.GetEngine(ctx).ID(note.ID).Update(note) + return err +} - _, err := db.GetEngine(ctx).ID(note.ID).Cols(fieldToUpdate...).Update(note) +// MoveBoardNoteOnProjectBoard moves or keeps notes in a column and sorts them inside that column +func MoveBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedNoteIDs map[int64]int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + sess := db.GetEngine(ctx) - return err + for sorting, issueID := range sortedNoteIDs { + _, err := sess.Exec("UPDATE `board_note` SET board_id=?, sorting=? WHERE id=?", board.ID, sorting, issueID) + if err != nil { + return err + } + } + return nil + }) } // DeleteBoardNote removes the BoardNote from the project board. diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index c2c9ccc410b18..01c6e2c1591dc 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -724,7 +724,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { - ctx.NotFound("", nil) + ctx.NotFound("ProjectNotFound", err) } else { ctx.ServerError("GetProjectByID", err) } @@ -734,7 +734,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode note, err := project_model.GetBoardNoteById(ctx, ctx.ParamsInt64(":noteID")) if err != nil { if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("", nil) + ctx.NotFound("ProjectBoardNoteNotFound", err) } else { ctx.ServerError("GetBoardNoteById", err) } @@ -805,6 +805,9 @@ func EditBoardNote(ctx *context.Context) { func DeleteBoardNote(ctx *context.Context) { _, boardNote := checkProjectBoardNoteChangePermissions(ctx) + if ctx.Written() { + return + } if err := project_model.DeleteBoardNote(ctx, boardNote); err != nil { ctx.ServerError("DeleteBoardNote", err) @@ -815,6 +818,91 @@ func DeleteBoardNote(ctx *context.Context) { } func MoveBoardNote(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only authorized users are allowed to perform this action.", + }) + return + } + + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound("ProjectNotFound", err) + } else { + ctx.ServerError("GetProjectByID", err) + } + return + } + + var board *project_model.Board + + if ctx.ParamsInt64(":boardID") == 0 { + board = &project_model.Board{ + ID: 0, + ProjectID: project.ID, + Title: ctx.Tr("repo.projects.type.uncategorized"), + } + } else { + board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + if err != nil { + if project_model.IsErrProjectBoardNotExist(err) { + ctx.NotFound("ProjectBoardNotExist", nil) + } else { + ctx.ServerError("GetProjectBoard", err) + } + return + } + if board.ProjectID != project.ID { + ctx.NotFound("BoardNotInProject", nil) + return + } + } + + type MovedBoardNotesForm struct { + BoardNotes []struct { + BoardNoteID int64 `json:"boardNoteID"` + Sorting int64 `json:"sorting"` + } `json:"boardNotes"` + } + + form := &MovedBoardNotesForm{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedBoardNotesForm", err) + } + + boardNoteIDs := make([]int64, 0, len(form.BoardNotes)) + sortedBoardNoteIDs := make(map[int64]int64) + for _, boardNote := range form.BoardNotes { + boardNoteIDs = append(boardNoteIDs, boardNote.BoardNoteID) + sortedBoardNoteIDs[boardNote.Sorting] = boardNote.BoardNoteID + } + movedBoardNotes, err := project_model.GetBoardNoteByIds(ctx, boardNoteIDs) + if err != nil { + if project_model.IsErrProjectBoardNoteNotExist(err) { + ctx.NotFound("BoardNoteNotExisting", nil) + } else { + ctx.ServerError("GetBoardNoteByIds", err) + } + return + } + + if len(movedBoardNotes) != len(form.BoardNotes) { + ctx.ServerError("some board-notes do not exist", errors.New("some board-notes do not exist")) + return + } + + if err = project_model.MoveBoardNoteOnProjectBoard(ctx, board, sortedBoardNoteIDs); err != nil { + ctx.ServerError("MoveBoardNoteOnProjectBoard", err) + return + } ctx.JSONOK() } diff --git a/routers/web/web.go b/routers/web/web.go index c2cf620c175f9..1bec85ea05df4 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1344,12 +1344,11 @@ func registerRoutes(m *web.Route) { m.Group("/note", func() { m.Post("", web.Bind(forms.BoardNoteForm{}), repo.AddNoteToBoard) + m.Post("/move", repo.MoveBoardNote) m.Group("/{noteID}", func() { m.Put("", web.Bind(forms.BoardNoteForm{}), repo.EditBoardNote) m.Delete("", repo.DeleteBoardNote) - - m.Post("/move", repo.MoveBoardNote) }) }) }) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index a951356164052..38ce79ebe24c9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -69,7 +69,7 @@ <div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> - <div class="ui small circular grey label project-column-issue-count"> + <div class="ui small circular grey label project-column-issue-note-count"> {{.NumIssuesAndNotes ctx}} </div> {{.Title}} @@ -194,7 +194,7 @@ {{ $columnID := .ID }} {{if or $canWriteProject (index $.NotesMap $columnID)}} - <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> + <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> {{range (index $.NotesMap $columnID)}} <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID))}} diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 3285b151463e4..48de24eec162a 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -64,6 +64,16 @@ flex: unset; } +/* don't show divider, if currently no note-cards are draged and note-cards are empty */ +#project-board:not(:has(.note-cards > .card-ghost)) .project-column > .note-cards:not(:has(> *)) + .divider { + display: none; +} + +/* give empty note-cards a min-height, if there is currently a note-card been draged */ +#project-board:has(.note-cards > .card-ghost) .project-column > .note-cards:not(:has(> *)) { + min-height: 5%; +} + .project-column > .divider { margin: 5px 0; } diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index aa21cfd4074bd..d9baac5a50dec 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -5,10 +5,10 @@ import {createSortable} from '../modules/sortable.js'; const {csrfToken} = window.config; -function updateIssueCount(cards) { +function updateIssueAndNoteCount(cards) { const parent = cards.parentElement; - const cnt = parent.getElementsByClassName('issue-card').length; - parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt; + const cnt = parent.querySelectorAll('.issue-card, .note-card').length; + parent.querySelector('.project-column-issue-note-count').textContent = cnt; } function sendPostRequestAndUnsetDirty(url, form, data) { @@ -28,8 +28,8 @@ function sendPostRequestAndUnsetDirty(url, form, data) { function moveIssue({item, from, to, oldIndex}) { const columnCards = to.getElementsByClassName('issue-card'); - updateIssueCount(from); - updateIssueCount(to); + updateIssueAndNoteCount(from); + updateIssueAndNoteCount(to); const columnSorting = { issues: Array.from(columnCards, (card, i) => ({ @@ -52,6 +52,33 @@ function moveIssue({item, from, to, oldIndex}) { }); } +function moveNote({from, to}) { + const columnCards = to.getElementsByClassName('note-card'); + updateIssueAndNoteCount(from); + updateIssueAndNoteCount(to); + + const columnSorting = { + boardNotes: Array.from(columnCards, (card, i) => ({ + boardNoteID: parseInt($(card).data('note')), + sorting: i, + })), + }; + + $.ajax({ + url: `${to.getAttribute('data-url')}/move`, + data: JSON.stringify(columnSorting), + headers: { + 'X-Csrf-Token': csrfToken, + }, + contentType: 'application/json', + type: 'POST', + success: () => { + // reload, because the relative-time changes from `created` to `updated` + window.location.reload(); + }, + }); +} + async function initRepoProjectSortable() { const els = document.querySelectorAll('#project-board > .board.sortable'); if (!els.length) return; @@ -102,8 +129,8 @@ async function initRepoProjectSortable() { group: 'note-shared', animation: 150, ghostClass: 'card-ghost', - onAdd: () => { console.error('@TODO') }, - onUpdate: () => { console.error('@TODO') }, + onAdd: moveNote, + onUpdate: moveNote, delayOnTouchOnly: true, delay: 500 }); From 90d0b609b5a03225625b842edd3834352721d41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sat, 3 Feb 2024 21:21:16 +0100 Subject: [PATCH 04/46] feat(board-notes): set boardID to 0 on when board gets deleted --- models/project/board.go | 4 ++++ models/project/note.go | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/models/project/board.go b/models/project/board.go index d49d59e8ffd59..791f63a41495b 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -200,6 +200,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { return err } + if err = board.removeBoardNotes(ctx); err != nil { + return err + } + if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil { return err } diff --git a/models/project/note.go b/models/project/note.go index 540dc8a949dd6..0e46fdd46a325 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -164,3 +164,9 @@ func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { } return nil } + +// removeBoardNotes sets the boardID to 0 for the board +func (board *Board) removeBoardNotes(ctx context.Context) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `board_note` SET board_id = 0 WHERE board_id = ?", board.ID) + return err +} From a9924b655191c0c5b065425a9e4f9cd53eb1ec12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sat, 3 Feb 2024 23:03:17 +0100 Subject: [PATCH 05/46] feat(board-notes): add projectID to BoardNote --- models/project/board.go | 1 + models/project/note.go | 26 +++++++++++++++++--------- models/project/project.go | 4 ++++ routers/web/repo/projects.go | 23 +++++++++-------------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/models/project/board.go b/models/project/board.go index 791f63a41495b..2e80cfc6461c3 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -85,6 +85,7 @@ func (b *Board) NumIssues(ctx context.Context) int { // NumNotes return counter of all notes assigned to the board func (b *Board) NumNotes(ctx context.Context) int { c, err := db.GetEngine(ctx).Table("board_note"). + Where("project_id=?", b.ProjectID). And("board_id=?", b.ID). GroupBy("id"). Cols("id"). diff --git a/models/project/note.go b/models/project/note.go index 0e46fdd46a325..e3efe47ccd7f8 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -21,6 +21,7 @@ type BoardNote struct { Content string `xorm:"LONGTEXT"` Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` Creator *user_model.User `xorm:"-"` @@ -34,7 +35,8 @@ type BoardNoteList = []*BoardNote // NotesOptions represents options of an note. type NotesOptions struct { //nolint db.Paginator - BoardID int64 + ProjectID int64 + BoardID int64 } func init() { @@ -66,11 +68,11 @@ func GetBoardNoteByIds(ctx context.Context, noteIDs []int64) (BoardNoteList, err return notes, nil } -// LoadNotesFromBoardList load notes assigned to the boards -func (p *Project) LoadNotesFromBoardList(ctx context.Context, bs BoardList) (map[int64]BoardNoteList, error) { +// LoadBoardNotesFromBoardList load notes assigned to the boards +func (p *Project) LoadBoardNotesFromBoardList(ctx context.Context, bs BoardList) (map[int64]BoardNoteList, error) { notesMap := make(map[int64]BoardNoteList, len(bs)) for i := range bs { - il, err := LoadNotesFromBoard(ctx, bs[i]) + il, err := LoadBoardNotesFromBoard(ctx, bs[i]) if err != nil { return nil, err } @@ -79,10 +81,11 @@ func (p *Project) LoadNotesFromBoardList(ctx context.Context, bs BoardList) (map return notesMap, nil } -// LoadNotesFromBoard load notes assigned to this board -func LoadNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { +// LoadBoardNotesFromBoard load notes assigned to this board +func LoadBoardNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { notes, err := BoardNotes(ctx, &NotesOptions{ - BoardID: board.ID, + ProjectID: board.ProjectID, + BoardID: board.ID, }) if err != nil { return nil, err @@ -95,10 +98,10 @@ func LoadNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) { sess := db.GetEngine(ctx) - sess.Where(builder.Eq{"board_id": max(opts.BoardID, 0)}) + sess.Where(builder.Eq{"board_id": opts.BoardID}).And(builder.Eq{"project_id": opts.ProjectID}) notes := BoardNoteList{} - if err := sess.OrderBy("Sorting").Find(¬es); err != nil { + if err := sess.OrderBy("sorting").Find(¬es); err != nil { return nil, fmt.Errorf("unable to query Notes: %w", err) } @@ -157,6 +160,11 @@ func MoveBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedNoteID }) } +func deleteBoardNoteByProjectID(ctx context.Context, projectID int64) error { + _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&BoardNote{}) + return err +} + // DeleteBoardNote removes the BoardNote from the project board. func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { if _, err := db.GetEngine(ctx).Delete(boardNote); err != nil { diff --git a/models/project/project.go b/models/project/project.go index eca49f85a0d15..c42e8b47b2bef 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -430,6 +430,10 @@ func DeleteProjectByID(ctx context.Context, id int64) error { return err } + if err := deleteBoardNoteByProjectID(ctx, id); err != nil { + return err + } + if err := deleteBoardByProjectID(ctx, id); err != nil { return err } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 01c6e2c1591dc..3314fe4f10c0d 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -324,7 +324,7 @@ func ViewProject(ctx *context.Context) { return } - notesMap, err := project.LoadNotesFromBoardList(ctx, boards) + notesMap, err := project.LoadBoardNotesFromBoardList(ctx, boards) if err != nil { ctx.ServerError("LoadNotesOfBoards", err) return @@ -740,10 +740,9 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode } return nil, nil } - boardID := ctx.ParamsInt64(":boardID") - if note.BoardID != boardID { + if note.ProjectID != project.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Board[%d] as expected", note.ID, boardID), + "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", note.ID, project.ID), }) return nil, nil } @@ -770,10 +769,11 @@ func AddNoteToBoard(ctx *context.Context) { Title: form.Title, Content: form.Content, + ProjectID: ctx.ParamsInt64(":id"), BoardID: ctx.ParamsInt64(":boardID"), CreatorID: ctx.Doer.ID, }); err != nil { - ctx.ServerError("NewProjectBoard", err) + ctx.ServerError("NewProjectBoardNote", err) return } @@ -782,20 +782,15 @@ func AddNoteToBoard(ctx *context.Context) { func EditBoardNote(ctx *context.Context) { form := web.GetForm(ctx).(*forms.BoardNoteForm) - _, note := checkProjectBoardNoteChangePermissions(ctx) + _, boardNote := checkProjectBoardNoteChangePermissions(ctx) if ctx.Written() { return } - if form.Title != "" { - note.Title = form.Title - } - - if form.Content != "" { - note.Content = form.Content - } + boardNote.Title = form.Title + boardNote.Content = form.Content - if err := project_model.UpdateBoardNote(ctx, note); err != nil { + if err := project_model.UpdateBoardNote(ctx, boardNote); err != nil { ctx.ServerError("UpdateProjectBoardNote", err) return } From 423404c04835847f99a31413ab155b94f1636d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:04:31 +0100 Subject: [PATCH 06/46] feat(board-notes): better styles for large cards --- templates/projects/view.tmpl | 38 +++++++++++++++------------- web_src/css/features/projects.css | 27 ++++++++++++-------- web_src/js/features/repo-projects.js | 6 ++--- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 38ce79ebe24c9..80376f0c7b4ad 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -192,27 +192,29 @@ <div class="divider"></div> - {{ $columnID := .ID }} - {{if or $canWriteProject (index $.NotesMap $columnID)}} - <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> - {{range (index $.NotesMap $columnID)}} - <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> - {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID))}} + <div class="cards-wrapper"> + {{ $columnID := .ID }} + {{if or $canWriteProject (index $.NotesMap $columnID)}} + <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> + {{range (index $.NotesMap $columnID)}} + <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> + {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID))}} + </div> + {{end}} + </div> + + {{if or $canWriteProject (index $.IssuesMap $columnID)}} + <div class="divider"></div> + {{end}} + {{end}} + + <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> + {{range (index $.IssuesMap $columnID)}} + <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> + {{template "repo/issue/card" (dict "Issue" . "Page" $)}} </div> {{end}} </div> - - {{if or $canWriteProject (index $.IssuesMap $columnID)}} - <div class="divider"></div> - {{end}} - {{end}} - - <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> - {{range (index $.IssuesMap $columnID)}} - <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> - {{template "repo/issue/card" (dict "Issue" . "Page" $)}} - </div> - {{end}} </div> </div> {{end}} diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 48de24eec162a..23ddec8e88b80 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -14,7 +14,6 @@ width: 320px; height: calc(100vh - 450px); min-height: 60vh; - overflow-y: scroll; flex: 0 0 auto; overflow: visible; display: flex; @@ -48,29 +47,35 @@ line-height: 1.25 !important; } -.project-column > .cards { +.cards-wrapper { flex: 1; display: flex; - align-content: baseline; - margin: 0 !important; - padding: 0 !important; - flex-wrap: nowrap !important; flex-direction: column; overflow-x: auto; +} + +.cards-wrapper > .cards { + flex-grow: 1; + margin: 0 !important; + flex-direction: column; gap: .25rem; } -.project-column > .note-cards { - flex: unset; +.cards-wrapper > .note-cards { + flex-grow: unset; } /* don't show divider, if currently no note-cards are draged and note-cards are empty */ -#project-board:not(:has(.note-cards > .card-ghost)) .project-column > .note-cards:not(:has(> *)) + .divider { +#project-board:not(:has(.note-cards > .card-ghost)) .cards-wrapper > .note-cards:not(:has(> *)) + .divider, +/* don't show divider, if there are no childs in issue-cards */ +.project-column:not(:has(.issue-cards > *)) .note-cards + .divider { display: none; } -/* give empty note-cards a min-height, if there is currently a note-card been draged */ -#project-board:has(.note-cards > .card-ghost) .project-column > .note-cards:not(:has(> *)) { +/* give empty cards a min-height, if there is currently a child-card been draged */ +#project-board:has(.note-cards > .card-ghost) .cards-wrapper > .note-cards:not(:has(> *)), +/* set min-height if issue-cards, this helps when note-cards are heigher then 100% */ +#project-board:has(.card-ghost) .cards-wrapper > .issue-cards:not(:has(> *)) { min-height: 5%; } diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index d9baac5a50dec..c22810f5293ac 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -6,9 +6,9 @@ import {createSortable} from '../modules/sortable.js'; const {csrfToken} = window.config; function updateIssueAndNoteCount(cards) { - const parent = cards.parentElement; - const cnt = parent.querySelectorAll('.issue-card, .note-card').length; - parent.querySelector('.project-column-issue-note-count').textContent = cnt; + const cardsWrapper = $(cards).closest('.cards-wrapper'); + const cnt = cardsWrapper.find('.issue-card, .note-card').length; + cardsWrapper.find('.project-column-issue-note-count').text(cnt); } function sendPostRequestAndUnsetDirty(url, form, data) { From c3698893a3569f355e80abd004027a2940aac711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:26:25 +0100 Subject: [PATCH 07/46] fix: update note content on frontend and backend --- models/project/note.go | 7 ++++++- templates/repo/note.tmpl | 8 +++----- web_src/js/features/repo-projects.js | 15 ++++++++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index e3efe47ccd7f8..17966377d4b69 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -141,7 +141,12 @@ func (note *BoardNote) GetLastEventLabel() string { // UpdateBoardNote changes a BoardNote func UpdateBoardNote(ctx context.Context, note *BoardNote) error { - _, err := db.GetEngine(ctx).ID(note.ID).Update(note) + var fieldToUpdate []string + + fieldToUpdate = append(fieldToUpdate, "title") + fieldToUpdate = append(fieldToUpdate, "content") + + _, err := db.GetEngine(ctx).ID(note.ID).Cols(fieldToUpdate...).Update(note) return err } diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 85fa7a9aff8fe..b86d9c6cdcdb8 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -60,11 +60,9 @@ </div> {{end}} </div> - {{if and .Content (gt (len .Content) 0)}} - <div class="project-column-note-content"> - {{.Content}} - </div> - {{end}} + <div class="project-column-note-content"> + {{.Content}} + </div> <div class="meta"> <span class="text light grey muted-links"> {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index c22810f5293ac..efba6686abbc1 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -6,9 +6,9 @@ import {createSortable} from '../modules/sortable.js'; const {csrfToken} = window.config; function updateIssueAndNoteCount(cards) { - const cardsWrapper = $(cards).closest('.cards-wrapper'); - const cnt = cardsWrapper.find('.issue-card, .note-card').length; - cardsWrapper.find('.project-column-issue-note-count').text(cnt); + const column = $(cards).closest('.project-column'); + const cnt = column.find('.issue-card, .note-card').length; + column.find('.project-column-issue-note-count').text(cnt); } function sendPostRequestAndUnsetDirty(url, form, data) { @@ -72,10 +72,9 @@ function moveNote({from, to}) { }, contentType: 'application/json', type: 'POST', - success: () => { - // reload, because the relative-time changes from `created` to `updated` - window.location.reload(); - }, + }).done(() => { + // reload, because the relative-time changes from `created` to `updated` + window.location.reload(); }); } @@ -278,6 +277,8 @@ export function initRepoProject() { noteContent.text(noteContentTextarea.val()); editModal.removeClass('dirty'); $('.ui.modal').modal('hide'); + // reload, because the relative-time changes from `created` to `updated` + window.location.reload(); }); }); }); From 962ea45818856657dee8d67f8938fb06f2c8826d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:32:23 +0100 Subject: [PATCH 08/46] enhance: don't reload page to remove deleted column and cards --- web_src/js/features/repo-projects.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index efba6686abbc1..2596f374a12d2 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -199,22 +199,25 @@ export function initRepoProject() { }); $('.show-delete-project-column-modal').each(function () { - const deleteColumnModal = $(`${$(this).attr('data-modal')}`); - const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button'); - const deleteUrl = $(this).attr('data-url'); + const deleteColumnModalId = $(this).data('modal'); + const deleteColumnUrl = $(this).data('url'); + + const columnToDelete = $(this).closest('.project-column'); + const deleteColumnModal = $(deleteColumnModalId); + const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button'); deleteColumnButton.on('click', (e) => { e.preventDefault(); $.ajax({ - url: deleteUrl, + url: deleteColumnUrl, headers: { 'X-Csrf-Token': csrfToken, }, contentType: 'application/json', method: 'DELETE', }).done(() => { - window.location.reload(); + columnToDelete.remove(); }); }); }); @@ -285,9 +288,10 @@ export function initRepoProject() { $('.show-delete-project-column-note-modal').each(function () { const deleteNoteUrl = $(this).data('url'); const modalId = $(this).data('modal'); - const deletModal = $(modalId); + const deleteModal = $(modalId); + const noteCard = deleteModal.closest('.note-card'); - const confirmDeleteButton = deletModal.find('.actions > .ok.button'); + const confirmDeleteButton = deleteModal.find('.actions > .ok.button'); confirmDeleteButton.on('click', (e) => { e.preventDefault(); @@ -299,7 +303,9 @@ export function initRepoProject() { contentType: 'application/json', method: 'DELETE', }).done(() => { - window.location.reload(); + const noteCards = noteCard.parent(); + noteCard.remove(); + updateIssueAndNoteCount(noteCards); }); }); }); From ac79a3197f69478aeb9739779f89e989dee79318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:39:34 +0100 Subject: [PATCH 09/46] fix: max-width for cards and attachments (fixes: #29029) --- web_src/css/features/projects.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 23ddec8e88b80..8c4d82684a9c0 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -59,6 +59,7 @@ margin: 0 !important; flex-direction: column; gap: .25rem; + max-width: 100%; } .cards-wrapper > .note-cards { @@ -93,6 +94,7 @@ .card-attachment-images { display: inline-block; + max-width: 100%; white-space: nowrap; overflow: hidden; text-align: center; @@ -101,6 +103,7 @@ .card-attachment-images img { display: inline-block; max-height: 50px; + max-width: 100%; border-radius: var(--border-radius); margin-right: 2px; } From bff8dc6523e44655a68ab8ee5ecbdadd843b0610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:46:28 +0100 Subject: [PATCH 10/46] enhance: can add notes to 'Uncategorized'-Column --- templates/projects/view.tmpl | 138 ++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 80376f0c7b4ad..85b90c4fde4ca 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -74,7 +74,7 @@ </div> {{.Title}} </div> - {{if and $canWriteProject (ne .ID 0)}} + {{if $canWriteProject}} <div class="ui dropdown jump item"> <div class="gt-px-3"> {{svg "octicon-kebab-horizontal"}} @@ -86,35 +86,37 @@ {{svg "octicon-note"}} {{ctx.Locale.Tr "repo.projects.note.add"}} </a> - <a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> - {{svg "octicon-pencil"}} - {{ctx.Locale.Tr "repo.projects.column.edit"}} - </a> - {{if not .Default}} - <a class="item show-modal button default-project-column-show" - data-modal="#default-project-column-modal-{{.ID}}" - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" - data-url="{{$.Link}}/{{.ID}}/default"> - {{svg "octicon-pin"}} - {{ctx.Locale.Tr "repo.projects.column.set_default"}} + {{if (ne .ID 0)}} + <a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> + {{svg "octicon-pencil"}} + {{ctx.Locale.Tr "repo.projects.column.edit"}} </a> - {{else}} - <a class="item show-modal button default-project-column-show" - data-modal="#default-project-column-modal-{{.ID}}" - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" - data-url="{{$.Link}}/{{.ID}}/unsetdefault"> - {{svg "octicon-pin-slash"}} - {{ctx.Locale.Tr "repo.projects.column.unset_default"}} + {{if not .Default}} + <a class="item show-modal button default-project-column-show" + data-modal="#default-project-column-modal-{{.ID}}" + data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" + data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" + data-url="{{$.Link}}/{{.ID}}/default"> + {{svg "octicon-pin"}} + {{ctx.Locale.Tr "repo.projects.column.set_default"}} + </a> + {{else}} + <a class="item show-modal button default-project-column-show" + data-modal="#default-project-column-modal-{{.ID}}" + data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" + data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" + data-url="{{$.Link}}/{{.ID}}/unsetdefault"> + {{svg "octicon-pin-slash"}} + {{ctx.Locale.Tr "repo.projects.column.unset_default"}} + </a> + {{end}} + <a class="item show-modal button show-delete-project-column-modal" + data-modal="#delete-project-column-modal-{{.ID}}" + data-url="{{$.Link}}/{{.ID}}"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "repo.projects.column.delete"}} </a> {{end}} - <a class="item show-modal button show-delete-project-column-modal" - data-modal="#delete-project-column-modal-{{.ID}}" - data-url="{{$.Link}}/{{.ID}}"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "repo.projects.column.delete"}} - </a> <div class="ui small modal" id="add-project-column-note-modal-{{.ID}}"> <div class="header"> @@ -137,54 +139,56 @@ {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.add"))}} </div> - <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> - <div class="header"> - {{ctx.Locale.Tr "repo.projects.column.edit"}} - </div> - <div class="content"> - <form class="ui form"> - <div class="required field"> - <label for="new_project_column_title">{{ctx.Locale.Tr "repo.projects.column.edit_title"}}</label> - <input class="project-column-title-input" id="new_project_column_title" name="title" value="{{.Title}}" required> - </div> + {{if (ne .ID 0)}} + <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.column.edit"}} + </div> + <div class="content"> + <form class="ui form"> + <div class="required field"> + <label for="new_project_column_title">{{ctx.Locale.Tr "repo.projects.column.edit_title"}}</label> + <input class="project-column-title-input" id="new_project_column_title" name="title" value="{{.Title}}" required> + </div> - <div class="field color-field"> - <label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> - <div class="color picker column"> - <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}"> - {{template "repo/issue/label_precolors"}} + <div class="field color-field"> + <label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> + <div class="color picker column"> + <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}"> + {{template "repo/issue/label_precolors"}} + </div> </div> - </div> - <div class="text right actions"> - <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> - <button data-url="{{$.Link}}/{{.ID}}" class="ui primary button edit-project-column-button">{{ctx.Locale.Tr "repo.projects.column.edit"}}</button> - </div> - </form> + <div class="text right actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> + <button data-url="{{$.Link}}/{{.ID}}" class="ui primary button edit-project-column-button">{{ctx.Locale.Tr "repo.projects.column.edit"}}</button> + </div> + </form> + </div> </div> - </div> - <div class="ui g-modal-confirm modal default-project-column-modal" id="default-project-column-modal-{{.ID}}"> - <div class="header"> - <span id="default-project-column-header"></span> - </div> - <div class="content"> - <label id="default-project-column-content"></label> + <div class="ui g-modal-confirm modal default-project-column-modal" id="default-project-column-modal-{{.ID}}"> + <div class="header"> + <span id="default-project-column-header"></span> + </div> + <div class="content"> + <label id="default-project-column-content"></label> + </div> + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} - </div> - <div class="ui g-modal-confirm modal" id="delete-project-column-modal-{{.ID}}"> - <div class="header"> - {{ctx.Locale.Tr "repo.projects.column.delete"}} - </div> - <div class="content"> - <label> - {{ctx.Locale.Tr "repo.projects.column.deletion_desc"}} - </label> + <div class="ui g-modal-confirm modal" id="delete-project-column-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.column.delete"}} + </div> + <div class="content"> + <label> + {{ctx.Locale.Tr "repo.projects.column.deletion_desc"}} + </label> + </div> + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} - </div> + {{end}} </div> </div> {{end}} From 2c63ee1278fa2b5b6dd4ace427be9d8a9372247f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 00:53:22 +0100 Subject: [PATCH 11/46] enhance: don't reload page after moving note --- web_src/js/features/repo-projects.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 2596f374a12d2..e6af2b3477868 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -52,7 +52,7 @@ function moveIssue({item, from, to, oldIndex}) { }); } -function moveNote({from, to}) { +function moveNote({item, from, to, oldIndex}) { const columnCards = to.getElementsByClassName('note-card'); updateIssueAndNoteCount(from); updateIssueAndNoteCount(to); @@ -72,9 +72,9 @@ function moveNote({from, to}) { }, contentType: 'application/json', type: 'POST', - }).done(() => { - // reload, because the relative-time changes from `created` to `updated` - window.location.reload(); + error: () => { + from.insertBefore(item, from.children[oldIndex]); + }, }); } From 696a632ea6115d98451be3a975bb01be7f4ce3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 01:03:03 +0100 Subject: [PATCH 12/46] enhance: new sort-order -> newest BoardNote on top --- models/project/note.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/project/note.go b/models/project/note.go index 17966377d4b69..1f991d20237f7 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -101,7 +101,7 @@ func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) sess.Where(builder.Eq{"board_id": opts.BoardID}).And(builder.Eq{"project_id": opts.ProjectID}) notes := BoardNoteList{} - if err := sess.OrderBy("sorting").Find(¬es); err != nil { + if err := sess.Asc("sorting").Desc("updated_unix").Desc("id").Find(¬es); err != nil { return nil, fmt.Errorf("unable to query Notes: %w", err) } From a29422f00370fd5424c573764fabe6737e4ff182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 02:05:55 +0100 Subject: [PATCH 13/46] refactor: use form-fetch-action instead of own $.ajax --- templates/projects/view.tmpl | 35 ++++++------ templates/repo/note.tmpl | 38 +++++++------ web_src/js/features/repo-projects.js | 80 +--------------------------- 3 files changed, 41 insertions(+), 112 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 85b90c4fde4ca..79808bfa90e89 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -80,63 +80,64 @@ {{svg "octicon-kebab-horizontal"}} </div> <div class="menu user-menu"> - <a class="item show-modal button show-add-project-column-note-modal" + <button class="ui item show-modal button" data-modal="#add-project-column-note-modal-{{.ID}}" - data-url="{{$.Link}}/{{.ID}}/note"> + data-modal-form.action="{{$.Link}}/{{.ID}}/note"> {{svg "octicon-note"}} {{ctx.Locale.Tr "repo.projects.note.add"}} - </a> + </button> {{if (ne .ID 0)}} - <a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> + <button class="ui item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.column.edit"}} - </a> + </button> {{if not .Default}} - <a class="item show-modal button default-project-column-show" + <button class="ui item show-modal button default-project-column-show" data-modal="#default-project-column-modal-{{.ID}}" data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" data-url="{{$.Link}}/{{.ID}}/default"> {{svg "octicon-pin"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}} - </a> + </button> {{else}} - <a class="item show-modal button default-project-column-show" + <button class="ui item show-modal button default-project-column-show" data-modal="#default-project-column-modal-{{.ID}}" data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" data-url="{{$.Link}}/{{.ID}}/unsetdefault"> {{svg "octicon-pin-slash"}} {{ctx.Locale.Tr "repo.projects.column.unset_default"}} - </a> + </button> {{end}} - <a class="item show-modal button show-delete-project-column-modal" + <button class="ui item show-modal button show-delete-project-column-modal" data-modal="#delete-project-column-modal-{{.ID}}" data-url="{{$.Link}}/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.column.delete"}} - </a> + </button> {{end}} <div class="ui small modal" id="add-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.add"}} </div> - <div class="content"> - <div class="ui form"> + <form class="ui form form-fetch-action" method="post"> + <div class="content"> + {{$.CsrfTokenHtml}} <div class="required field"> <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" required> </div> - <div class="field content-field"> + <div class="field"> <label for="new-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> <textarea id="new-project-column-note-content-{{.ID}}" name="content"></textarea> </div> </div> - </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.add"))}} + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.add"))}} + </form> </div> {{if (ne .ID 0)}} @@ -202,7 +203,7 @@ <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> {{range (index $.NotesMap $columnID)}} <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> - {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID))}} + {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID) "CsrfTokenHtml" $.CsrfTokenHtml)}} </div> {{end}} </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index b86d9c6cdcdb8..da1c993f1bf72 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -11,50 +11,54 @@ {{svg "octicon-kebab-horizontal"}} </div> <div class="menu user-menu left"> - <a class="item show-modal button show-edit-project-column-note-modal" + <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-url="{{$.Link}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/note/{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} - </a> - <a class="item show-modal button show-delete-project-column-note-modal" + </button> + <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-url="{{$.Link}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/note/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} - </a> + </button> <div class="ui small modal" id="edit-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.edit"}} </div> - <div class="content"> - <div class="ui form"> + <form class="ui form form-fetch-action" method="put"> + <div class="content"> + {{$.CsrfTokenHtml}} <div class="required field"> <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" required> </div> - <div class="field content-field"> + <div class="field"> <label for="edit-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> <textarea id="edit-project-column-note-content-{{.ID}}" name="content">{{.Content}}</textarea> </div> </div> - </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.edit"))}} + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.edit"))}} + </form> </div> <div class="ui g-modal-confirm modal" id="delete-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.delete"}} </div> - <div class="content"> - <label> - {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} - </label> - </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} + <form class="ui form form-fetch-action" action="delete"> + {{$.CsrfTokenHtml}} + <div class="content"> + <label> + {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} + </label> + </div> + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} + </form> </div> </div> </div> diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index e6af2b3477868..e28029ed5fa65 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -11,7 +11,7 @@ function updateIssueAndNoteCount(cards) { column.find('.project-column-issue-note-count').text(cnt); } -function sendPostRequestAndUnsetDirty(url, form, data) { +function createNewColumn(url, form, data) { $.ajax({ url, data: JSON.stringify(data), @@ -231,83 +231,7 @@ export function initRepoProject() { } const url = $(this).data('url'); const form = columnTitle.closest('form'); - sendPostRequestAndUnsetDirty(url, form, {title: columnTitle.val(), color: projectColorInput.val()}); - }); - - $('.show-add-project-column-note-modal').each(function () { - const addNoteUrl = $(this).data('url'); - const modalId = $(this).data('modal'); - const addModal = $(modalId); - - const confirmAddButton = addModal.find('.actions > .ok.button'); - confirmAddButton.on('click', (e) => { - e.preventDefault(); - const noteTitleInput = addModal.find('[name="title"]'); - const contentTextarea = addModal.find('[name="content"]'); - if (!noteTitleInput.val()) { - return; - } - const form = noteTitleInput.closest('form'); - sendPostRequestAndUnsetDirty(addNoteUrl, form, {title: noteTitleInput.val(), content: contentTextarea.val()}); - }); - }); - $('.show-edit-project-column-note-modal').each(function () { - const editNoteUrl = $(this).data('url'); - const modalId = $(this).data('modal'); - const wrapperCard = $(this).closest('.note-card'); - - const noteTitle = wrapperCard.find('.project-column-note-title'); - const noteContent = wrapperCard.find('.project-column-note-content'); - - const editModal = wrapperCard.find(modalId); - const noteTitleInput = editModal.find('[name="title"]'); - const noteContentTextarea = editModal.find('[name="content"]'); - - const confirmEditButton = editModal.find('.actions > .ok.button'); - confirmEditButton.on('click', (e) => { - e.preventDefault(); - - $.ajax({ - url: editNoteUrl, - data: JSON.stringify({title: noteTitleInput.val(), content: noteContentTextarea.val()}), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'PUT', - }).done(() => { - noteTitle.text(noteTitleInput.val()); - noteContent.text(noteContentTextarea.val()); - editModal.removeClass('dirty'); - $('.ui.modal').modal('hide'); - // reload, because the relative-time changes from `created` to `updated` - window.location.reload(); - }); - }); - }); - $('.show-delete-project-column-note-modal').each(function () { - const deleteNoteUrl = $(this).data('url'); - const modalId = $(this).data('modal'); - const deleteModal = $(modalId); - const noteCard = deleteModal.closest('.note-card'); - - const confirmDeleteButton = deleteModal.find('.actions > .ok.button'); - confirmDeleteButton.on('click', (e) => { - e.preventDefault(); - - $.ajax({ - url: deleteNoteUrl, - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'DELETE', - }).done(() => { - const noteCards = noteCard.parent(); - noteCard.remove(); - updateIssueAndNoteCount(noteCards); - }); - }); + createNewColumn(url, form, {title: columnTitle.val(), color: projectColorInput.val()}); }); } From 0f4b806946d998808d2277f71001edff30c94ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 05:36:14 +0100 Subject: [PATCH 14/46] enhance: content of BoardNotes with MD support --- routers/web/repo/projects.go | 13 +++++++++++++ templates/projects/view.tmpl | 21 ++++++++++++++++----- templates/repo/note.tmpl | 21 +++++++++++++-------- web_src/css/features/projects.css | 6 ++++-- web_src/js/features/repo-projects.js | 17 ++++++++++++++++- 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 3314fe4f10c0d..12f17e145e8fd 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -330,6 +330,19 @@ func ViewProject(ctx *context.Context) { return } + for _, noteList := range notesMap { + for _, note := range noteList { + note.Content, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + }, + Metas: ctx.Repo.Repository.ComposeMetas(), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, note.Content) + } + } + if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) for _, issuesList := range issuesMap { diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 79808bfa90e89..e2f6d9fa72f5e 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -126,13 +126,17 @@ <div class="content"> {{$.CsrfTokenHtml}} <div class="required field"> - <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> - <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" required> + <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.new_title"}}</label> + <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" autofocus required> </div> <div class="field"> - <label for="new-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> - <textarea id="new-project-column-note-content-{{.ID}}" name="content"></textarea> + {{template "shared/combomarkdowneditor" (dict + "MarkdownPreviewUrl" (print $.RepoLink "/markup") + "MarkdownPreviewContext" $.RepoLink + "TextareaName" "content" + "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") + )}} </div> </div> @@ -203,7 +207,14 @@ <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> {{range (index $.NotesMap $columnID)}} <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> - {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" (print $.Link "/" $columnID) "CsrfTokenHtml" $.CsrfTokenHtml)}} + {{template "repo/note" (dict + "BoardNote" . + "CanWriteProjects" $canWriteProject + "Link" $.Link + "RepoLink" $.RepoLink + "columnID" $columnID + "CsrfTokenHtml" $.CsrfTokenHtml) + }} </div> {{end}} </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index da1c993f1bf72..7e1c17fd505ec 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -13,18 +13,18 @@ <div class="menu user-menu left"> <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} </button> - <div class="ui small modal" id="edit-project-column-note-modal-{{.ID}}"> + <div class="ui modal" id="edit-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.edit"}} </div> @@ -33,12 +33,17 @@ {{$.CsrfTokenHtml}} <div class="required field"> <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> - <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" required> + <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" autofocus required> </div> - + <div class="field"> - <label for="edit-project-column-note-content-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.content"}}</label> - <textarea id="edit-project-column-note-content-{{.ID}}" name="content">{{.Content}}</textarea> + {{template "shared/combomarkdowneditor" (dict + "MarkdownPreviewUrl" (print $.RepoLink "/markup") + "MarkdownPreviewContext" $.RepoLink + "TextareaName" "content" + "TextareaContent" .Content + "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") + )}} </div> </div> @@ -65,7 +70,7 @@ {{end}} </div> <div class="project-column-note-content"> - {{.Content}} + {{Str2html .Content}} </div> <div class="meta"> <span class="text light grey muted-links"> diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 8c4d82684a9c0..62881988384f2 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -66,6 +66,10 @@ flex-grow: unset; } +.cards-wrapper > .cards * { + max-width: 100%; +} + /* don't show divider, if currently no note-cards are draged and note-cards are empty */ #project-board:not(:has(.note-cards > .card-ghost)) .cards-wrapper > .note-cards:not(:has(> *)) + .divider, /* don't show divider, if there are no childs in issue-cards */ @@ -94,7 +98,6 @@ .card-attachment-images { display: inline-block; - max-width: 100%; white-space: nowrap; overflow: hidden; text-align: center; @@ -103,7 +106,6 @@ .card-attachment-images img { display: inline-block; max-height: 50px; - max-width: 100%; border-radius: var(--border-radius); margin-right: 2px; } diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index e28029ed5fa65..99f51d572ce91 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import {useLightTextOnBackground} from '../utils/color.js'; import tinycolor from 'tinycolor2'; import {createSortable} from '../modules/sortable.js'; +import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; const {csrfToken} = window.config; @@ -137,10 +138,24 @@ async function initRepoProjectSortable() { } export function initRepoProject() { - if (!$('.repository.projects').length) { + const mainContent = $('.repository.projects'); + if (!mainContent.length) { return; } + const modalButtons = mainContent.find('.project-column button[data-modal]'); + modalButtons.each(function() { + const modalButton = $(this); + const modalId = modalButton.data('modal'); + + const markdownEditor = $(`${modalId} .combo-markdown-editor`); + if (!markdownEditor.length) return; + + modalButton.one('click', () => { + initComboMarkdownEditor(markdownEditor); + }); + }); + const _promise = initRepoProjectSortable(); $('.edit-project-column-modal').each(function () { From 12f8cda0560d2d9b50333759d0b95481112a5230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 05:45:51 +0100 Subject: [PATCH 15/46] fix: whole project-column has cursor grab --- web_src/css/features/projects.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 62881988384f2..7975ec1cf57d0 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -18,6 +18,8 @@ overflow: visible; display: flex; flex-direction: column; + + cursor: grab; } .project-column-header { From 51c8e09826b8741676ca3e515b87bb34ea9715ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 06:24:11 +0100 Subject: [PATCH 16/46] enhance: smaller cards and view-modal --- models/project/note.go | 9 ++- routers/web/repo/projects.go | 2 +- templates/projects/view.tmpl | 2 +- templates/repo/note.tmpl | 111 ++++++++++++++++----------- web_src/css/features/projects.css | 10 +++ web_src/js/features/repo-projects.js | 2 +- 6 files changed, 84 insertions(+), 52 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index 1f991d20237f7..e39c7942bc3f8 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -16,10 +16,11 @@ import ( // BoardNote is used to represent a note on a boards type BoardNote struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"TEXT NOT NULL"` - Content string `xorm:"LONGTEXT"` - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 12f17e145e8fd..6693933a5a7ea 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -332,7 +332,7 @@ func ViewProject(ctx *context.Context) { for _, noteList := range notesMap { for _, note := range noteList { - note.Content, err = markdown.RenderString(&markup.RenderContext{ + note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ Links: markup.Links{ Base: ctx.Repo.RepoLink, }, diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index e2f6d9fa72f5e..5f7986d4d752f 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -118,7 +118,7 @@ </button> {{end}} - <div class="ui small modal" id="add-project-column-note-modal-{{.ID}}"> + <div class="ui modal" id="add-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.add"}} </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 7e1c17fd505ec..1bb973abf58a5 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -5,72 +5,93 @@ {{svg "octicon-note"}} </div> <span class="note-card-title muted project-column-note-title">{{.Title}}</span> - {{ if $.CanWriteProjects }} + {{if or $.CanWriteProjects (gt (len .RenderedContent) 0)}} <div class="ui dropdown jump item"> <div class="gt-px-3"> {{svg "octicon-kebab-horizontal"}} </div> <div class="menu user-menu left"> <button class="ui item show-modal button" - data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> - {{svg "octicon-pencil"}} - {{ctx.Locale.Tr "repo.projects.note.edit"}} - </button> - <button class="ui item show-modal button" - data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "repo.projects.note.delete"}} + data-modal="#view-project-column-note-modal-{{.ID}}"> + {{svg "octicon-eye"}} + {{ctx.Locale.Tr "repo.projects.note.view"}} </button> + {{if $.CanWriteProjects}} + <button class="ui item show-modal button" + data-modal="#edit-project-column-note-modal-{{.ID}}" + data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> + {{svg "octicon-pencil"}} + {{ctx.Locale.Tr "repo.projects.note.edit"}} + </button> + <button class="ui item show-modal button" + data-modal="#delete-project-column-note-modal-{{.ID}}" + data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "repo.projects.note.delete"}} + </button> + {{end}} - <div class="ui modal" id="edit-project-column-note-modal-{{.ID}}"> + <div class="ui modal" id="view-project-column-note-modal-{{.ID}}"> <div class="header"> - {{ctx.Locale.Tr "repo.projects.note.edit"}} + {{ctx.Locale.Tr "repo.projects.note.view"}} </div> - <form class="ui form form-fetch-action" method="put"> - <div class="content"> - {{$.CsrfTokenHtml}} - <div class="required field"> - <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> - <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" autofocus required> - </div> + <div class="content"> + {{Str2html .RenderedContent}} + </div> + <div class="actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "repo.projects.close"}}</button> + </div> + </div> - <div class="field"> - {{template "shared/combomarkdowneditor" (dict - "MarkdownPreviewUrl" (print $.RepoLink "/markup") - "MarkdownPreviewContext" $.RepoLink - "TextareaName" "content" - "TextareaContent" .Content - "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") - )}} - </div> + {{if $.CanWriteProjects}} + <div class="ui modal" id="edit-project-column-note-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.edit"}} </div> + <form class="ui form form-fetch-action" method="put"> + <div class="content"> + {{$.CsrfTokenHtml}} + <div class="required field"> + <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" autofocus required> + </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.edit"))}} - </form> - </div> + <div class="field"> + {{template "shared/combomarkdowneditor" (dict + "MarkdownPreviewUrl" (print $.RepoLink "/markup") + "MarkdownPreviewContext" $.RepoLink + "TextareaName" "content" + "TextareaContent" .Content + "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") + )}} + </div> + </div> - <div class="ui g-modal-confirm modal" id="delete-project-column-note-modal-{{.ID}}"> - <div class="header"> - {{ctx.Locale.Tr "repo.projects.note.delete"}} + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.edit"))}} + </form> </div> - <form class="ui form form-fetch-action" action="delete"> - {{$.CsrfTokenHtml}} - <div class="content"> - <label> - {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} - </label> + + <div class="ui g-modal-confirm modal" id="delete-project-column-note-modal-{{.ID}}"> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.delete"}} </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} - </form> - </div> + <form class="ui form form-fetch-action" action="delete"> + {{$.CsrfTokenHtml}} + <div class="content"> + <label> + {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} + </label> + </div> + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} + </form> + </div> + {{end}} </div> </div> {{end}} </div> <div class="project-column-note-content"> - {{Str2html .Content}} + {{Str2html .RenderedContent}} </div> <div class="meta"> <span class="text light grey muted-links"> diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 7975ec1cf57d0..1fa8dd24cec9e 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -1,3 +1,8 @@ +.ui.modal .content { + max-height: 85vh; + overflow-y: auto; +} + .board { display: flex; flex-direction: row; @@ -98,6 +103,11 @@ margin-right: auto !important; } +.project-column-note-content { + max-height: 250px; + overflow-y: hidden; +} + .card-attachment-images { display: inline-block; white-space: nowrap; diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 99f51d572ce91..fce0301663c9d 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -152,7 +152,7 @@ export function initRepoProject() { if (!markdownEditor.length) return; modalButton.one('click', () => { - initComboMarkdownEditor(markdownEditor); + initComboMarkdownEditor(markdownEditor, {easyMDEOptions: {maxHeight: '50vh', minHeight: '50vh'}}); }); }); From 4e17cf2119950d94664774470c2d6832e7d678c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 07:52:25 +0100 Subject: [PATCH 17/46] enhance: create issue from board-note --- templates/projects/view.tmpl | 1 + templates/repo/note.tmpl | 4 ++++ web_src/js/features/repo-issue-new.js | 28 +++++++++++++++++++++++++++ web_src/js/features/repo-projects.js | 12 ++++++++++++ web_src/js/index.js | 2 ++ 5 files changed, 47 insertions(+) create mode 100644 web_src/js/features/repo-issue-new.js diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 5f7986d4d752f..afdc49a23d984 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -213,6 +213,7 @@ "Link" $.Link "RepoLink" $.RepoLink "columnID" $columnID + "ProjectID" $.Project.ID "CsrfTokenHtml" $.CsrfTokenHtml) }} </div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 1bb973abf58a5..fbafb78920abf 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -23,6 +23,10 @@ {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> + <a class="ui item button create-project-issue-from-note" href="{{$.RepoLink}}/issues/new/choose?project={{$.ProjectID}}"> + {{svg "octicon-issue-opened"}} + {{ctx.Locale.Tr "repo.issues.new"}} + </a> <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> diff --git a/web_src/js/features/repo-issue-new.js b/web_src/js/features/repo-issue-new.js new file mode 100644 index 0000000000000..5aaa2bba21448 --- /dev/null +++ b/web_src/js/features/repo-issue-new.js @@ -0,0 +1,28 @@ +import $ from 'jquery'; +import {getComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; + +export function initRepoIssueNew() { + const newIssuePage = $('.repository.issue.new'); + if (!newIssuePage.length) return; + + // this is set from board-notes in repo-projects.js + const boardNoteTitle = sessionStorage.getItem('board-note-title'); + const boardNoteContent = sessionStorage.getItem('board-note-content'); + + if (boardNoteTitle) { + const issueTitle = newIssuePage.find('#issue_title'); + issueTitle.val(boardNoteTitle); + } + if (boardNoteContent) { + // @TODO: find a better way to get the combobox + const waitForComboMarkdownEditorInterval = setInterval(() => { + const comboMarkdownEditorContainer = newIssuePage.find('.combo-markdown-editor'); + const comboMarkdownEditor = getComboMarkdownEditor(comboMarkdownEditorContainer); + if (!comboMarkdownEditor) return; + + clearInterval(waitForComboMarkdownEditorInterval); + + comboMarkdownEditor.value(boardNoteContent); + }, 100); + } +} diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index fce0301663c9d..c8b68644142a4 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -158,6 +158,18 @@ export function initRepoProject() { const _promise = initRepoProjectSortable(); + $('.create-project-issue-from-note').each(function () { + const anchorLink = $(this); + const wrapperCard = anchorLink.closest('.note-card'); + const titleInput = wrapperCard.find('[name="title"]'); + const contentTextarea = wrapperCard.find('.markdown-text-editor'); + + anchorLink.on('click', () => { + sessionStorage.setItem('board-note-title', titleInput.val()); + sessionStorage.setItem('board-note-content', contentTextarea.val()); + }); + }); + $('.edit-project-column-modal').each(function () { const projectHeader = $(this).closest('.project-column-header'); const projectTitleLabel = projectHeader.find('.project-column-title'); diff --git a/web_src/js/index.js b/web_src/js/index.js index 4713618506b0c..c20f5f0429bbc 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -18,6 +18,7 @@ import {initAdminConfigs} from './features/admin/config.js'; import {initMarkupAnchors} from './markup/anchors.js'; import {initNotificationCount, initNotificationsTable} from './features/notification.js'; import {initRepoIssueContentHistory} from './features/repo-issue-content.js'; +import {initRepoIssueNew} from './features/repo-issue-new.js'; import {initStopwatch} from './features/stopwatch.js'; import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; @@ -149,6 +150,7 @@ onDomReady(() => { initRepoEditor(); initRepoGraphGit(); initRepoIssueContentHistory(); + initRepoIssueNew(); initRepoIssueDue(); initRepoIssueList(); initRepoIssueSidebarList(); From 0d8f8a21753d917474679e65a44f6e92f3d5000b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 08:41:39 +0100 Subject: [PATCH 18/46] i18n: translations for board-notes --- options/locale/locale_de-DE.ini | 13 ++++++++++++- options/locale/locale_en-US.ini | 13 ++++++++++++- templates/projects/view.tmpl | 6 +++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c24d25b1ac6f2..8acece6e79f6e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1327,7 +1327,7 @@ projects.column.set_default_desc=Diese Spalte als Standard für unkategorisierte projects.column.unset_default=Standard entfernen projects.column.unset_default_desc=Diese Spalte als Standard entfernen projects.column.delete=Spalte löschen -projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach 'Nicht kategorisiert' verschoben. Fortfahren? +projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues und Notizen nach 'Nicht kategorisiert' verschoben. Fortfahren? projects.column.color=Farbe projects.open=Öffnen projects.close=Schließen @@ -1336,6 +1336,17 @@ projects.card_type.desc=Kartenvorschau projects.card_type.images_and_text=Bilder und Text projects.card_type.text_only=Nur Text +projects.note.view = Notiz ansehen +projects.note.new = Neue Notiz +projects.note.new_title = Titel +projects.note.content = Schreibe eine Notiz +projects.note.edit = Notiz bearbeiten +projects.note.edit_title = Titel +projects.note.delete = Notiz löschen +projects.note.deletion_desc = Beim Löschen einer Notiz wird diese von der Projektspalte entfernt. Fortfahren? +projects.note.created_by = erstellt %[1]s von <a href="%[2]s">%[3]s</a> +projects.note.updated_by = bearbeitet %[1]s von <a href="%[2]s">%[3]s</a> + issues.desc=Verwalte Bug-Reports, Aufgaben und Meilensteine. issues.filter_assignees=Filter issues.filter_milestones=Meilenstein filtern diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9af4d70171c50..f0d1089ddcfd0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1343,7 +1343,7 @@ projects.column.set_default_desc = "Set this column as default for uncategorized projects.column.unset_default = "Unset Default" projects.column.unset_default_desc = "Unset this column as default" projects.column.delete = "Delete Column" -projects.column.deletion_desc = "Deleting a project column moves all related issues to 'Uncategorized'. Continue?" +projects.column.deletion_desc = "Deleting a project column moves all related issues and notes to 'Uncategorized'. Continue?" projects.column.color = "Color" projects.open = Open projects.close = Close @@ -1352,6 +1352,17 @@ projects.card_type.desc = "Card Previews" projects.card_type.images_and_text = "Images and Text" projects.card_type.text_only = "Text Only" +projects.note.view = View Content +projects.note.new = New Note +projects.note.new_title = Title +projects.note.content = Write a Note +projects.note.edit = Edit Note +projects.note.edit_title = Title +projects.note.delete = Delete Note +projects.note.deletion_desc = Deleting removes the note from the project column. Continue? +projects.note.created_by = created %[1]s by <a href="%[2]s">%[3]s</a> +projects.note.updated_by = updated %[1]s by <a href="%[2]s">%[3]s</a> + issues.desc = Organize bug reports, tasks and milestones. issues.filter_assignees = Filter Assignee issues.filter_milestones = Filter Milestone diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index afdc49a23d984..62c0346c56b84 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -84,7 +84,7 @@ data-modal="#add-project-column-note-modal-{{.ID}}" data-modal-form.action="{{$.Link}}/{{.ID}}/note"> {{svg "octicon-note"}} - {{ctx.Locale.Tr "repo.projects.note.add"}} + {{ctx.Locale.Tr "repo.projects.note.new"}} </button> {{if (ne .ID 0)}} <button class="ui item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> @@ -120,7 +120,7 @@ <div class="ui modal" id="add-project-column-note-modal-{{.ID}}"> <div class="header"> - {{ctx.Locale.Tr "repo.projects.note.add"}} + {{ctx.Locale.Tr "repo.projects.note.new"}} </div> <form class="ui form form-fetch-action" method="post"> <div class="content"> @@ -140,7 +140,7 @@ </div> </div> - {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.add"))}} + {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm" "ModalButtonOkText" (ctx.Locale.Tr "repo.projects.note.new"))}} </form> </div> From 965f70d9830dae8680dff55c999ab74698be0b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 08:06:04 +0100 Subject: [PATCH 19/46] fix: edit the column-title don't remove the count-div (fixes: #29031) --- web_src/js/features/repo-projects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index c8b68644142a4..564b734928869 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -193,7 +193,7 @@ export function initRepoProject() { contentType: 'application/json', method: 'PUT', }).done(() => { - projectTitleLabel.text(projectTitleInput.val()); + projectTitleLabel.contents().last().replaceWith(projectTitleInput.val()); projectTitleInput.closest('form').removeClass('dirty'); if (projectColorInput.val()) { setLabelColor(projectHeader, projectColorInput.val()); From 762f2cec2ff78d42e82b7ab346f064b8d60cd3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 09:05:59 +0100 Subject: [PATCH 20/46] fix: v1.21 to main --- routers/web/repo/projects.go | 2 +- templates/projects/view.tmpl | 2 +- templates/repo/note.tmpl | 2 +- web_src/css/features/projects.css | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 6693933a5a7ea..79fd826756d5f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -336,7 +336,7 @@ func ViewProject(ctx *context.Context) { Links: markup.Links{ Base: ctx.Repo.RepoLink, }, - Metas: ctx.Repo.Repository.ComposeMetas(), + Metas: ctx.Repo.Repository.ComposeMetas(ctx), GitRepo: ctx.Repo.GitRepo, Ctx: ctx, }, note.Content) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 62c0346c56b84..ed33d45f60434 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -202,7 +202,7 @@ <div class="divider"></div> <div class="cards-wrapper"> - {{ $columnID := .ID }} + {{$columnID := .ID}} {{if or $canWriteProject (index $.NotesMap $columnID)}} <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> {{range (index $.NotesMap $columnID)}} diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index fbafb78920abf..50dd89c8836a6 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -1,5 +1,5 @@ {{with .BoardNote}} -<div class="content gt-p-0 gt-w-100"> +<div class="content gt-p-0 gt-w-full"> <div class="gt-df gt-items-start"> <div class="project-column-note-card-icon"> {{svg "octicon-note"}} diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 1fa8dd24cec9e..814334e95b71d 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -50,6 +50,7 @@ } .project-column-title { + white-space: unset !important; background: none !important; line-height: 1.25 !important; } From 76b90eba7a7788c90ee8173c63f4e50cb931788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 09:17:16 +0100 Subject: [PATCH 21/46] chore: lint --- models/project/note.go | 10 +++++----- routers/web/repo/projects.go | 6 +++++- routers/web/web.go | 4 ++-- templates/projects/view.tmpl | 2 -- templates/repo/note.tmpl | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index e39c7942bc3f8..dff21ac88857f 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -34,7 +34,7 @@ type BoardNote struct { type BoardNoteList = []*BoardNote // NotesOptions represents options of an note. -type NotesOptions struct { //nolint +type NotesOptions struct { db.Paginator ProjectID int64 BoardID int64 @@ -44,8 +44,8 @@ func init() { db.RegisterModel(new(BoardNote)) } -// GetBoardNoteById load notes assigned to the boards -func GetBoardNoteById(ctx context.Context, noteID int64) (*BoardNote, error) { +// GetBoardNoteByID load notes assigned to the boards +func GetBoardNoteByID(ctx context.Context, noteID int64) (*BoardNote, error) { note := new(BoardNote) has, err := db.GetEngine(ctx).ID(noteID).Get(note) @@ -180,7 +180,7 @@ func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { } // removeBoardNotes sets the boardID to 0 for the board -func (board *Board) removeBoardNotes(ctx context.Context) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `board_note` SET board_id = 0 WHERE board_id = ?", board.ID) +func (b *Board) removeBoardNotes(ctx context.Context) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `board_note` SET board_id = 0 WHERE board_id = ?", b.ID) return err } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 79fd826756d5f..a3dc4b42b00dc 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -340,6 +340,10 @@ func ViewProject(ctx *context.Context) { GitRepo: ctx.Repo.GitRepo, Ctx: ctx, }, note.Content) + if err != nil { + ctx.ServerError("RenderString", err) + return + } } } @@ -744,7 +748,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return nil, nil } - note, err := project_model.GetBoardNoteById(ctx, ctx.ParamsInt64(":noteID")) + note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { if project_model.IsErrProjectBoardNoteNotExist(err) { ctx.NotFound("ProjectBoardNoteNotFound", err) diff --git a/routers/web/web.go b/routers/web/web.go index 1bec85ea05df4..b4709d55bd29f 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -985,7 +985,7 @@ func registerRoutes(m *web.Route) { m.Get("", org.Projects) m.Get("/{id}", org.ViewProject) }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) - m.Group("", func() { //nolint:dupl + m.Group("", func() { m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { @@ -1323,7 +1323,7 @@ func registerRoutes(m *web.Route) { m.Group("/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) - m.Group("", func() { //nolint:dupl + m.Group("", func() { m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index ed33d45f60434..db6e7a477850b 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -219,12 +219,10 @@ </div> {{end}} </div> - {{if or $canWriteProject (index $.IssuesMap $columnID)}} <div class="divider"></div> {{end}} {{end}} - <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> {{range (index $.IssuesMap $columnID)}} <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 50dd89c8836a6..dd757ab853884 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -104,4 +104,4 @@ </span> </div> </div> -{{end}} \ No newline at end of file +{{end}} From 4815e7a8043eb8e5633bc077a22f287d65643ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 10:04:55 +0100 Subject: [PATCH 22/46] fix: required changes from CI and Bot --- options/locale/locale_de-DE.ini | 13 +------------ web_src/css/features/projects.css | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 8acece6e79f6e..c24d25b1ac6f2 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1327,7 +1327,7 @@ projects.column.set_default_desc=Diese Spalte als Standard für unkategorisierte projects.column.unset_default=Standard entfernen projects.column.unset_default_desc=Diese Spalte als Standard entfernen projects.column.delete=Spalte löschen -projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues und Notizen nach 'Nicht kategorisiert' verschoben. Fortfahren? +projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach 'Nicht kategorisiert' verschoben. Fortfahren? projects.column.color=Farbe projects.open=Öffnen projects.close=Schließen @@ -1336,17 +1336,6 @@ projects.card_type.desc=Kartenvorschau projects.card_type.images_and_text=Bilder und Text projects.card_type.text_only=Nur Text -projects.note.view = Notiz ansehen -projects.note.new = Neue Notiz -projects.note.new_title = Titel -projects.note.content = Schreibe eine Notiz -projects.note.edit = Notiz bearbeiten -projects.note.edit_title = Titel -projects.note.delete = Notiz löschen -projects.note.deletion_desc = Beim Löschen einer Notiz wird diese von der Projektspalte entfernt. Fortfahren? -projects.note.created_by = erstellt %[1]s von <a href="%[2]s">%[3]s</a> -projects.note.updated_by = bearbeitet %[1]s von <a href="%[2]s">%[3]s</a> - issues.desc=Verwalte Bug-Reports, Aufgaben und Meilensteine. issues.filter_assignees=Filter issues.filter_milestones=Meilenstein filtern diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 814334e95b71d..7974dc714d99d 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -87,7 +87,7 @@ /* give empty cards a min-height, if there is currently a child-card been draged */ #project-board:has(.note-cards > .card-ghost) .cards-wrapper > .note-cards:not(:has(> *)), -/* set min-height if issue-cards, this helps when note-cards are heigher then 100% */ +/* set min-height if issue-cards, this helps when note-cards are higher then 100% */ #project-board:has(.card-ghost) .cards-wrapper > .issue-cards:not(:has(> *)) { min-height: 5%; } From 410b286871cd9629bd4974cd1652b2e65a60c698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 13:34:09 +0100 Subject: [PATCH 23/46] enhance: show amount of tasks in board-notes card --- models/issues/issue.go | 11 +++-------- models/project/note.go | 11 +++++++++++ modules/markup/markdown/markdown.go | 6 ++++++ templates/repo/note.tmpl | 7 +++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index 90aad10bb9001..74debee7031e5 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -7,7 +7,6 @@ package issues import ( "context" "fmt" - "regexp" "slices" "code.gitea.io/gitea/models/db" @@ -16,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -141,11 +141,6 @@ type Issue struct { ShowRole RoleDescriptor `xorm:"-"` } -var ( - issueTasksPat = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`) - issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`) -) - // IssueIndex represents the issue index table type IssueIndex db.ResourceIndex @@ -443,12 +438,12 @@ func (issue *Issue) IsPoster(uid int64) bool { // GetTasks returns the amount of tasks in the issues content func (issue *Issue) GetTasks() int { - return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) + return len(markdown.MarkdownTasksRegex.FindAllStringIndex(issue.Content, -1)) } // GetTasksDone returns the amount of completed tasks in the issues content func (issue *Issue) GetTasksDone() int { - return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1)) + return len(markdown.MarkdownTasksDoneRegex.FindAllStringIndex(issue.Content, -1)) } // GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close. diff --git a/models/project/note.go b/models/project/note.go index dff21ac88857f..5d6d1f20f8a9d 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -140,6 +141,16 @@ func (note *BoardNote) GetLastEventLabel() string { return "repo.projects.note.created_by" } +// GetTasks returns the amount of tasks in the board-notes content +func (note *BoardNote) GetTasks() int { + return len(markdown.MarkdownTasksRegex.FindAllStringIndex(note.Content, -1)) +} + +// GetTasksDone returns the amount of completed tasks in the board-notes content +func (note *BoardNote) GetTasksDone() int { + return len(markdown.MarkdownTasksDoneRegex.FindAllStringIndex(note.Content, -1)) +} + // UpdateBoardNote changes a BoardNote func UpdateBoardNote(ctx context.Context, note *BoardNote) error { var fieldToUpdate []string diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 771162b9a3f12..f2732650efc0b 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -7,6 +7,7 @@ package markdown import ( "fmt" "io" + "regexp" "strings" "sync" @@ -38,6 +39,11 @@ var ( renderConfigKey = parser.NewContextKey() ) +var ( + MarkdownTasksRegex = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`) + MarkdownTasksDoneRegex = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`) +) + type limitWriter struct { w io.Writer sum int64 diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index dd757ab853884..bb41c7930e10c 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -103,5 +103,12 @@ {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} </span> </div> + {{$tasks := .GetTasks}} + {{if gt $tasks 0}} + <div class="meta"> + {{svg "octicon-checklist" 16 "gt-mr-2 gt-vm"}} + <span class="gt-vm">{{.GetTasksDone}} / {{$tasks}}</span> + </div> + {{end}} </div> {{end}} From 50841341a3225452c72f600095364a624f6834f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Sun, 4 Feb 2024 21:45:08 +0100 Subject: [PATCH 24/46] enhance: board-notes can now be pinned --- models/project/note.go | 175 ++++++++++++++++++++++++++- modules/setting/repository.go | 10 ++ options/locale/locale_en-US.ini | 1 + routers/web/repo/projects.go | 90 ++++++++++++++ routers/web/web.go | 6 + templates/projects/view.tmpl | 33 +++-- templates/repo/note.tmpl | 18 ++- web_src/css/features/projects.css | 9 +- web_src/js/features/repo-projects.js | 52 ++++++++ 9 files changed, 380 insertions(+), 14 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index 5d6d1f20f8a9d..4825d7bc04c4a 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -10,7 +10,9 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -22,6 +24,7 @@ type BoardNote struct { Content string `xorm:"LONGTEXT"` RenderedContent string `xorm:"-"` Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` @@ -107,6 +110,7 @@ func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) return nil, fmt.Errorf("unable to query Notes: %w", err) } + // @TODO: same code in `GetPinnedBoardNotes()` and should be used with `LoadAttributes()` for _, note := range notes { creator := new(user_model.User) has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) @@ -128,7 +132,176 @@ func NewBoardNote(ctx context.Context, note *BoardNote) error { return err } -// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close. +/* @TODO: make it work - markdown.RenderString should also be at this function +// IsPinned returns if a BoardNote is pinned +func (notes BoardNoteList) LoadAttributes() error { + return nil +} */ + +var ErrBoardNoteMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned board-notes has been readched") + +// IsPinned returns if a BoardNote is pinned +func (note *BoardNote) IsPinned() bool { + return note.PinOrder != 0 +} + +// IsPinned returns if a BoardNote is pinned +func (note *BoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { + var maxPin int64 + _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM board_note WHERE project_id = ?", note.ProjectID).Get(&maxPin) + if err != nil { + return -1, err + } + return maxPin, nil +} + +// IsPinned returns if a BoardNote is pinned +func (note *BoardNote) IsNewPinAllowed(ctx context.Context) bool { + maxPin, err := note.GetMaxPinOrder(ctx) + if err != nil { + return false + } + + // Check if the maximum allowed Pins reached + return maxPin < setting.Repository.Project.MaxPinned +} + +// Pin pins a BoardNote +func (note *BoardNote) Pin(ctx context.Context) error { + // If the BoardNote is already pinned, we don't need to pin it twice + if note.IsPinned() { + return nil + } + + maxPin, err := note.GetMaxPinOrder(ctx) + if err != nil { + return err + } + + // Check if the maximum allowed Pins reached + if maxPin >= setting.Repository.Project.MaxPinned { + return ErrBoardNoteMaxPinReached + } + + _, err = db.GetEngine(ctx).Table("board_note"). + Where("id = ?", note.ID). + Update(map[string]any{ + "pin_order": maxPin + 1, + }) + if err != nil { + return err + } + + return nil +} + +// Unpin unpins a BoardNote +func (note *BoardNote) Unpin(ctx context.Context) error { + // If the BoardNote is not pinned, we don't need to unpin it + if !note.IsPinned() { + return nil + } + + // This sets the Pin for all BoardNotes that come after the unpined BoardNote to the correct value + _, err := db.GetEngine(ctx).Exec("UPDATE board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", note.ProjectID, note.PinOrder) + if err != nil { + return err + } + + _, err = db.GetEngine(ctx).Table("board_note"). + Where("id = ?", note.ID). + Update(map[string]any{ + "pin_order": 0, + }) + if err != nil { + return err + } + + return nil +} + +// MovePin moves a Pinned BoardNote to a new Position +func (note *BoardNote) MovePin(ctx context.Context, newPosition int64) error { + // If the BoardNote is not pinned, we can't move them + if !note.IsPinned() { + return nil + } + + if newPosition < 1 { + return fmt.Errorf("The Position can't be lower than 1") + } + + dbctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + maxPin, err := note.GetMaxPinOrder(ctx) + if err != nil { + return err + } + + // If the new Position bigger than the current Maximum, set it to the Maximum + if newPosition > maxPin+1 { + newPosition = maxPin + 1 + } + + // Lower the Position of all Pinned BoardNotes that came after the current Position + _, err = db.GetEngine(dbctx).Exec("UPDATE board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", note.ProjectID, note.PinOrder) + if err != nil { + return err + } + + // Higher the Position of all Pinned BoardNotes that comes after the new Position + _, err = db.GetEngine(dbctx).Exec("UPDATE board_note SET pin_order = pin_order + 1 WHERE project_id = ? AND pin_order >= ?", note.ProjectID, newPosition) + if err != nil { + return err + } + + _, err = db.GetEngine(dbctx).Table("board_note"). + Where("id = ?", note.ID). + Update(map[string]any{ + "pin_order": newPosition, + }) + if err != nil { + return err + } + + return committer.Commit() +} + +// GetPinnedBoardNotes returns the pinned BaordNotes for the given Project +func GetPinnedBoardNotes(ctx context.Context, projectID int64) (BoardNoteList, error) { + notes := make(BoardNoteList, 0) + + err := db.GetEngine(ctx). + Table("board_note"). + Where("project_id = ?", projectID). + And("pin_order > 0"). + OrderBy("pin_order"). + Find(¬es) + if err != nil { + return nil, err + } + + // @TODO: same code in `BoardNotes()` and should be used with `LoadAttributes()` + for _, note := range notes { + creator := new(user_model.User) + has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) + if err != nil { + return nil, err + } + if !has { + return nil, user_model.ErrUserNotExist{UID: note.CreatorID} + } + note.Creator = creator + } + + return notes, nil +} + +// GetLastEventTimestamp returns the last user visible event timestamp, either the creation or the update. func (note *BoardNote) GetLastEventTimestamp() timeutil.TimeStamp { return max(note.CreatedUnix, note.UpdatedUnix) } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a6f0ed8833589..6a950e6b86e20 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -94,6 +94,10 @@ var ( MaxPinned int } `ini:"repository.issue"` + Project struct { + MaxPinned int64 + } `ini:"repository.project"` + Release struct { AllowedTypes string DefaultPagingNum int @@ -237,6 +241,12 @@ var ( MaxPinned: 3, }, + Project: struct { + MaxPinned int64 + }{ + MaxPinned: 3, + }, + Release: struct { AllowedTypes string DefaultPagingNum int diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f0d1089ddcfd0..e972e1a7d4575 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1362,6 +1362,7 @@ projects.note.delete = Delete Note projects.note.deletion_desc = Deleting removes the note from the project column. Continue? projects.note.created_by = created %[1]s by <a href="%[2]s">%[3]s</a> projects.note.updated_by = updated %[1]s by <a href="%[2]s">%[3]s</a> +projects.note.max_pinned = You can't pin more notes issues.desc = Organize bug reports, tasks and milestones. issues.filter_assignees = Filter Assignee diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index a3dc4b42b00dc..2da39b04b4bbe 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -330,6 +330,7 @@ func ViewProject(ctx *context.Context) { return } + // @TODO: maybe should be in BoardNote for _, noteList := range notesMap { for _, note := range noteList { note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ @@ -394,11 +395,34 @@ func ViewProject(ctx *context.Context) { return } + pinnedBoardNotes, err := project_model.GetPinnedBoardNotes(ctx, project.ID) + if err != nil { + ctx.ServerError("GetPinnedBoardNotes", err) + return + } + + // @TODO: maybe should be in BoardNote + for _, note := range pinnedBoardNotes { + note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, note.Content) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + } + ctx.Data["IsProjectsPage"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend + ctx.Data["PinnedNotes"] = pinnedBoardNotes ctx.Data["NotesMap"] = notesMap ctx.HTML(http.StatusOK, tplProjectsView) @@ -918,3 +942,69 @@ func MoveBoardNote(ctx *context.Context) { ctx.JSONOK() } + +// PinBoardNote pins the BoardNote +func PinBoardNote(ctx *context.Context) { + note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetBoardNoteByID", err) + return + } + + err = note.Pin(ctx) + if err != nil { + ctx.ServerError("Pin", err) + return + } + + ctx.JSONOK() +} + +// PinBoardNote unpins the BoardNote +func UnPinBoardNote(ctx *context.Context) { + note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetBoardNoteByID", err) + return + } + + err = note.Unpin(ctx) + if err != nil { + ctx.ServerError("Unpin", err) + return + } + + ctx.JSONOK() +} + +// PinBoardNote moves a pined the BoardNote +func PinMoveBoardNote(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, "Only signed in users are allowed to perform this action.") + return + } + + type movePinBoardNoteForm struct { + Position int64 `json:"position"` + } + + form := &movePinBoardNoteForm{} + if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("Decode movePinBoardNoteForm", err) + return + } + + note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetIssueByID", err) + return + } + + err = note.MovePin(ctx, form.Position) + if err != nil { + ctx.ServerError("MovePin", err) + return + } + + ctx.JSONOK() +} diff --git a/routers/web/web.go b/routers/web/web.go index b4709d55bd29f..711e38af71698 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1349,6 +1349,12 @@ func registerRoutes(m *web.Route) { m.Group("/{noteID}", func() { m.Put("", web.Bind(forms.BoardNoteForm{}), repo.EditBoardNote) m.Delete("", repo.DeleteBoardNote) + + m.Group("/pin", func() { + m.Post("", web.Bind(forms.BoardNoteForm{}), repo.PinBoardNote) + m.Delete("", repo.UnPinBoardNote) + m.Post("/move", repo.PinMoveBoardNote) + }) }) }) }) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index db6e7a477850b..314bb692136be 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,5 +1,22 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} +{{if gt (len .PinnedNotes) 0}} +<div id="pinned-notes"> + {{range .PinnedNotes}} + <div class="note-card pinned-card {{if $canWriteProject}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-note="{{.ID}}"> + {{template "repo/note" (dict + "BoardNote" . + "CanWriteProjects" $canWriteProject + "Link" $.Link + "RepoLink" $.RepoLink + "ProjectID" $.Project.ID + "CsrfTokenHtml" $.CsrfTokenHtml) + }} + </div> + {{end}} +</div> +{{end}} + <div class="ui container"> <div class="gt-df gt-sb gt-ac gt-mb-4"> <h2 class="gt-mb-0">{{.Project.Title}}</h2> @@ -66,7 +83,7 @@ <div id="project-board"> <div class="board {{if .CanWriteProjects}}sortable{{end}}"> {{range .Columns}} - <div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> + <div class="ui segment project-column {{if $canWriteProject}}gt-cursor-grab{{end}}" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> <div class="ui small circular grey label project-column-issue-note-count"> @@ -202,30 +219,28 @@ <div class="divider"></div> <div class="cards-wrapper"> - {{$columnID := .ID}} - {{if or $canWriteProject (index $.NotesMap $columnID)}} + {{if or $canWriteProject (index $.NotesMap .ID)}} <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> - {{range (index $.NotesMap $columnID)}} - <div class="note-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-note="{{.ID}}"> + {{range (index $.NotesMap .ID)}} + <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . "CanWriteProjects" $canWriteProject "Link" $.Link "RepoLink" $.RepoLink - "columnID" $columnID "ProjectID" $.Project.ID "CsrfTokenHtml" $.CsrfTokenHtml) }} </div> {{end}} </div> - {{if or $canWriteProject (index $.IssuesMap $columnID)}} + {{if or $canWriteProject (index $.IssuesMap .ID)}} <div class="divider"></div> {{end}} {{end}} <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> - {{range (index $.IssuesMap $columnID)}} - <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> + {{range (index $.IssuesMap .ID)}} + <div class="issue-card" data-issue="{{.ID}}"> {{template "repo/issue/card" (dict "Issue" . "Page" $)}} </div> {{end}} diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index bb41c7930e10c..585a77ce9260c 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -19,7 +19,7 @@ {{if $.CanWriteProjects}} <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> @@ -27,9 +27,23 @@ {{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.new"}} </a> + {{$notPinnedAndNotAllowedToPin := and (not .IsPinned) (not (.IsNewPinAllowed ctx))}} + <button class="ui item button {{if $notPinnedAndNotAllowedToPin}}disabled{{end}} pin-project-column-note" + {{if $notPinnedAndNotAllowedToPin}}style="pointer-events: unset !important"{{end}} + data-method="{{if .IsPinned}}delete{{else}}post{{end}}" + data-url="{{$.Link}}/{{.BoardID}}/note/{{.ID}}/pin" + {{if $notPinnedAndNotAllowedToPin}}data-tooltip-content="{{ctx.Locale.Tr "repo.projects.note.max_pinned"}}"{{end}}> + {{if .IsPinned}} + {{svg "octicon-pin-slash"}} + {{ctx.Locale.Tr "unpin"}} + {{else}} + {{svg "octicon-pin"}} + {{ctx.Locale.Tr "pin"}} + {{end}} + </button> <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{$.columnID}}/note/{{.ID}}"> + data-modal-form.action="{{$.Link}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} </button> diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 7974dc714d99d..eb9a27c3aa8a4 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -3,6 +3,13 @@ overflow-y: auto; } +#pinned-notes { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-bottom: 8px; +} + .board { display: flex; flex-direction: row; @@ -23,8 +30,6 @@ overflow: visible; display: flex; flex-direction: column; - - cursor: grab; } .project-column-header { diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 564b734928869..04a67a625ec98 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -79,7 +79,36 @@ function moveNote({item, from, to, oldIndex}) { }); } +function movePinned({newIndex, item}) { + const newPosition = newIndex + 1; + const id = $(item).data('note'); + const url = `${$(item).data('url')}/${id}/pin/move`; + + $.ajax({ + url, + data: JSON.stringify({position: newPosition}), + headers: { + 'X-Csrf-Token': csrfToken, + }, + contentType: 'application/json', + type: 'POST', + }); +} + async function initRepoProjectSortable() { + const pinnedNotesCards = document.querySelector('#pinned-notes'); + if (pinnedNotesCards) { + createSortable(pinnedNotesCards, { + group: 'pinned-shared', + animation: 150, + ghostClass: 'card-ghost', + onAdd: movePinned, + onUpdate: movePinned, + delayOnTouchOnly: true, + delay: 500 + }); + } + const els = document.querySelectorAll('#project-board > .board.sortable'); if (!els.length) return; @@ -260,6 +289,29 @@ export function initRepoProject() { const form = columnTitle.closest('form'); createNewColumn(url, form, {title: columnTitle.val(), color: projectColorInput.val()}); }); + + $('.pin-project-column-note').each(function () { + const pinButton = $(this); + const pinUrl = pinButton.data('url'); + const ajaxMethod = pinButton.data('method'); + + pinButton.on('click', (e) => { + e.preventDefault(); + + if (pinButton.hasClass('disabled')) return; + + $.ajax({ + url: pinUrl, + headers: { + 'X-Csrf-Token': csrfToken, + }, + contentType: 'application/json', + method: ajaxMethod, + }).done(() => { + window.location.reload(); + }); + }); + }); } function setLabelColor(label, color) { From abb5152c68575ee5b0827837a5eb731a745b550d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Tue, 6 Feb 2024 20:33:06 +0100 Subject: [PATCH 25/46] chore: rename BoardNote to ProjectBoardNote --- models/project/board.go | 16 ++-- models/project/note.go | 180 +++++++++++++++++------------------ models/project/project.go | 4 +- routers/web/repo/projects.go | 120 +++++++++++------------ routers/web/web.go | 14 +-- services/forms/repo_form.go | 4 +- templates/projects/view.tmpl | 10 +- 7 files changed, 174 insertions(+), 174 deletions(-) diff --git a/models/project/board.go b/models/project/board.go index 2e80cfc6461c3..d759336cc44db 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -82,9 +82,9 @@ func (b *Board) NumIssues(ctx context.Context) int { return int(c) } -// NumNotes return counter of all notes assigned to the board -func (b *Board) NumNotes(ctx context.Context) int { - c, err := db.GetEngine(ctx).Table("board_note"). +// NumProjectBoardNotes return counter of all notes assigned to the board +func (b *Board) NumProjectBoardNotes(ctx context.Context) int { + c, err := db.GetEngine(ctx).Table("project_board_note"). Where("project_id=?", b.ProjectID). And("board_id=?", b.ID). GroupBy("id"). @@ -96,11 +96,11 @@ func (b *Board) NumNotes(ctx context.Context) int { return int(c) } -// NumIssuesAndNotes return counter of all issues and notes assigned to the board -func (b *Board) NumIssuesAndNotes(ctx context.Context) int { +// NumIssuesAndProjectBoardNotes return counter of all issues and notes assigned to the board +func (b *Board) NumIssuesAndProjectBoardNotes(ctx context.Context) int { numIssues := b.NumIssues(ctx) - numNotes := b.NumNotes(ctx) - return numIssues + numNotes + numProjectBoardNotes := b.NumProjectBoardNotes(ctx) + return numIssues + numProjectBoardNotes } func init() { @@ -201,7 +201,7 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { return err } - if err = board.removeBoardNotes(ctx); err != nil { + if err = board.removeProjectBoardNotes(ctx); err != nil { return err } diff --git a/models/project/note.go b/models/project/note.go index 4825d7bc04c4a..e66aa45372860 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -17,8 +17,8 @@ import ( "xorm.io/builder" ) -// BoardNote is used to represent a note on a boards -type BoardNote struct { +// ProjectBoardNote is used to represent a note on a boards +type ProjectBoardNote struct { ID int64 `xorm:"pk autoincr"` Title string `xorm:"TEXT NOT NULL"` Content string `xorm:"LONGTEXT"` @@ -35,60 +35,60 @@ type BoardNote struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } -type BoardNoteList = []*BoardNote +type ProjectBoardNoteList []*ProjectBoardNote -// NotesOptions represents options of an note. -type NotesOptions struct { +// ProjectBoardNotesOptions represents options of an note. +type ProjectBoardNotesOptions struct { db.Paginator ProjectID int64 BoardID int64 } func init() { - db.RegisterModel(new(BoardNote)) + db.RegisterModel(new(ProjectBoardNote)) } -// GetBoardNoteByID load notes assigned to the boards -func GetBoardNoteByID(ctx context.Context, noteID int64) (*BoardNote, error) { - note := new(BoardNote) +// GetProjectBoardNoteByID load notes assigned to the boards +func GetProjectBoardNoteByID(ctx context.Context, projectBoardNoteID int64) (*ProjectBoardNote, error) { + projectBoardNote := new(ProjectBoardNote) - has, err := db.GetEngine(ctx).ID(noteID).Get(note) + has, err := db.GetEngine(ctx).ID(projectBoardNoteID).Get(projectBoardNote) if err != nil { return nil, err } else if !has { - return nil, ErrProjectBoardNoteNotExist{BoardNoteID: noteID} + return nil, ErrProjectBoardNoteNotExist{BoardNoteID: projectBoardNoteID} } - return note, nil + return projectBoardNote, nil } -// GetBoardNoteByIds return notes with the given IDs. -func GetBoardNoteByIds(ctx context.Context, noteIDs []int64) (BoardNoteList, error) { - notes := make(BoardNoteList, 0, len(noteIDs)) +// GetProjectBoardNotesByIds return notes with the given IDs. +func GetProjectBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) (ProjectBoardNoteList, error) { + projectBoardNoteList := make(ProjectBoardNoteList, 0, len(projectBoardNoteIDs)) - if err := db.GetEngine(ctx).In("id", noteIDs).Find(¬es); err != nil { + if err := db.GetEngine(ctx).In("id", projectBoardNoteIDs).Find(&projectBoardNoteList); err != nil { return nil, err } - return notes, nil + return projectBoardNoteList, nil } -// LoadBoardNotesFromBoardList load notes assigned to the boards -func (p *Project) LoadBoardNotesFromBoardList(ctx context.Context, bs BoardList) (map[int64]BoardNoteList, error) { - notesMap := make(map[int64]BoardNoteList, len(bs)) - for i := range bs { - il, err := LoadBoardNotesFromBoard(ctx, bs[i]) +// LoadProjectBoardNotesFromBoardList load notes assigned to the boards +func (p *Project) LoadProjectBoardNotesFromBoardList(ctx context.Context, boardList BoardList) (map[int64]ProjectBoardNoteList, error) { + projectBoardNoteListMap := make(map[int64]ProjectBoardNoteList, len(boardList)) + for i := range boardList { + il, err := LoadProjectBoardNotesFromBoard(ctx, boardList[i]) if err != nil { return nil, err } - notesMap[bs[i].ID] = il + projectBoardNoteListMap[boardList[i].ID] = il } - return notesMap, nil + return projectBoardNoteListMap, nil } -// LoadBoardNotesFromBoard load notes assigned to this board -func LoadBoardNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { - notes, err := BoardNotes(ctx, &NotesOptions{ +// LoadProjectBoardNotesFromBoard load notes assigned to this board +func LoadProjectBoardNotesFromBoard(ctx context.Context, board *Board) (ProjectBoardNoteList, error) { + projectBoardNoteList, err := ProjectBoardNotes(ctx, &ProjectBoardNotesOptions{ ProjectID: board.ProjectID, BoardID: board.ID, }) @@ -96,22 +96,22 @@ func LoadBoardNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, return nil, err } - return notes, nil + return projectBoardNoteList, nil } -// BoardNotes returns a list of notes by given conditions. -func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) { +// ProjectBoardNotes returns a list of notes by given conditions. +func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (ProjectBoardNoteList, error) { sess := db.GetEngine(ctx) sess.Where(builder.Eq{"board_id": opts.BoardID}).And(builder.Eq{"project_id": opts.ProjectID}) - notes := BoardNoteList{} - if err := sess.Asc("sorting").Desc("updated_unix").Desc("id").Find(¬es); err != nil { - return nil, fmt.Errorf("unable to query Notes: %w", err) + projectBoardNoteList := ProjectBoardNoteList{} + if err := sess.Asc("sorting").Desc("updated_unix").Desc("id").Find(&projectBoardNoteList); err != nil { + return nil, fmt.Errorf("unable to query project-board-notes: %w", err) } // @TODO: same code in `GetPinnedBoardNotes()` and should be used with `LoadAttributes()` - for _, note := range notes { + for _, note := range projectBoardNoteList { creator := new(user_model.User) has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) if err != nil { @@ -123,12 +123,12 @@ func BoardNotes(ctx context.Context, opts *NotesOptions) (BoardNoteList, error) note.Creator = creator } - return notes, nil + return projectBoardNoteList, nil } -// NewBoardNote adds a new note to a given board -func NewBoardNote(ctx context.Context, note *BoardNote) error { - _, err := db.GetEngine(ctx).Insert(note) +// NewProjectBoardNote adds a new note to a given board +func NewProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardNote) error { + _, err := db.GetEngine(ctx).Insert(projectBoardNote) return err } @@ -138,17 +138,17 @@ func (notes BoardNoteList) LoadAttributes() error { return nil } */ -var ErrBoardNoteMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned board-notes has been readched") +var ErrBoardNoteMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned project-board-notes has been readched") // IsPinned returns if a BoardNote is pinned -func (note *BoardNote) IsPinned() bool { - return note.PinOrder != 0 +func (projectBoardNote *ProjectBoardNote) IsPinned() bool { + return projectBoardNote.PinOrder != 0 } // IsPinned returns if a BoardNote is pinned -func (note *BoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { +func (projectBoardNote *ProjectBoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { var maxPin int64 - _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM board_note WHERE project_id = ?", note.ProjectID).Get(&maxPin) + _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM project_board_note WHERE project_id = ?", projectBoardNote.ProjectID).Get(&maxPin) if err != nil { return -1, err } @@ -156,8 +156,8 @@ func (note *BoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { } // IsPinned returns if a BoardNote is pinned -func (note *BoardNote) IsNewPinAllowed(ctx context.Context) bool { - maxPin, err := note.GetMaxPinOrder(ctx) +func (projectBoardNote *ProjectBoardNote) IsNewPinAllowed(ctx context.Context) bool { + maxPin, err := projectBoardNote.GetMaxPinOrder(ctx) if err != nil { return false } @@ -167,13 +167,13 @@ func (note *BoardNote) IsNewPinAllowed(ctx context.Context) bool { } // Pin pins a BoardNote -func (note *BoardNote) Pin(ctx context.Context) error { +func (projectBoardNote *ProjectBoardNote) Pin(ctx context.Context) error { // If the BoardNote is already pinned, we don't need to pin it twice - if note.IsPinned() { + if projectBoardNote.IsPinned() { return nil } - maxPin, err := note.GetMaxPinOrder(ctx) + maxPin, err := projectBoardNote.GetMaxPinOrder(ctx) if err != nil { return err } @@ -183,8 +183,8 @@ func (note *BoardNote) Pin(ctx context.Context) error { return ErrBoardNoteMaxPinReached } - _, err = db.GetEngine(ctx).Table("board_note"). - Where("id = ?", note.ID). + _, err = db.GetEngine(ctx).Table("project_board_note"). + Where("id = ?", projectBoardNote.ID). Update(map[string]any{ "pin_order": maxPin + 1, }) @@ -196,20 +196,20 @@ func (note *BoardNote) Pin(ctx context.Context) error { } // Unpin unpins a BoardNote -func (note *BoardNote) Unpin(ctx context.Context) error { +func (projectBoardNote *ProjectBoardNote) Unpin(ctx context.Context) error { // If the BoardNote is not pinned, we don't need to unpin it - if !note.IsPinned() { + if !projectBoardNote.IsPinned() { return nil } // This sets the Pin for all BoardNotes that come after the unpined BoardNote to the correct value - _, err := db.GetEngine(ctx).Exec("UPDATE board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", note.ProjectID, note.PinOrder) + _, err := db.GetEngine(ctx).Exec("UPDATE project_board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", projectBoardNote.ProjectID, projectBoardNote.PinOrder) if err != nil { return err } - _, err = db.GetEngine(ctx).Table("board_note"). - Where("id = ?", note.ID). + _, err = db.GetEngine(ctx).Table("project_board_note"). + Where("id = ?", projectBoardNote.ID). Update(map[string]any{ "pin_order": 0, }) @@ -221,9 +221,9 @@ func (note *BoardNote) Unpin(ctx context.Context) error { } // MovePin moves a Pinned BoardNote to a new Position -func (note *BoardNote) MovePin(ctx context.Context, newPosition int64) error { +func (projectBoardNote *ProjectBoardNote) MovePin(ctx context.Context, newPosition int64) error { // If the BoardNote is not pinned, we can't move them - if !note.IsPinned() { + if !projectBoardNote.IsPinned() { return nil } @@ -237,7 +237,7 @@ func (note *BoardNote) MovePin(ctx context.Context, newPosition int64) error { } defer committer.Close() - maxPin, err := note.GetMaxPinOrder(ctx) + maxPin, err := projectBoardNote.GetMaxPinOrder(ctx) if err != nil { return err } @@ -248,19 +248,19 @@ func (note *BoardNote) MovePin(ctx context.Context, newPosition int64) error { } // Lower the Position of all Pinned BoardNotes that came after the current Position - _, err = db.GetEngine(dbctx).Exec("UPDATE board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", note.ProjectID, note.PinOrder) + _, err = db.GetEngine(dbctx).Exec("UPDATE project_board_note SET pin_order = pin_order - 1 WHERE project_id = ? AND pin_order > ?", projectBoardNote.ProjectID, projectBoardNote.PinOrder) if err != nil { return err } // Higher the Position of all Pinned BoardNotes that comes after the new Position - _, err = db.GetEngine(dbctx).Exec("UPDATE board_note SET pin_order = pin_order + 1 WHERE project_id = ? AND pin_order >= ?", note.ProjectID, newPosition) + _, err = db.GetEngine(dbctx).Exec("UPDATE project_board_note SET pin_order = pin_order + 1 WHERE project_id = ? AND pin_order >= ?", projectBoardNote.ProjectID, newPosition) if err != nil { return err } - _, err = db.GetEngine(dbctx).Table("board_note"). - Where("id = ?", note.ID). + _, err = db.GetEngine(dbctx).Table("project_board_note"). + Where("id = ?", projectBoardNote.ID). Update(map[string]any{ "pin_order": newPosition, }) @@ -271,12 +271,12 @@ func (note *BoardNote) MovePin(ctx context.Context, newPosition int64) error { return committer.Commit() } -// GetPinnedBoardNotes returns the pinned BaordNotes for the given Project -func GetPinnedBoardNotes(ctx context.Context, projectID int64) (BoardNoteList, error) { - notes := make(BoardNoteList, 0) +// GetPinnedProjectBoardNotes returns the pinned BaordNotes for the given Project +func GetPinnedProjectBoardNotes(ctx context.Context, projectID int64) (ProjectBoardNoteList, error) { + notes := make(ProjectBoardNoteList, 0) err := db.GetEngine(ctx). - Table("board_note"). + Table("project_board_note"). Where("project_id = ?", projectID). And("pin_order > 0"). OrderBy("pin_order"). @@ -302,46 +302,46 @@ func GetPinnedBoardNotes(ctx context.Context, projectID int64) (BoardNoteList, e } // GetLastEventTimestamp returns the last user visible event timestamp, either the creation or the update. -func (note *BoardNote) GetLastEventTimestamp() timeutil.TimeStamp { - return max(note.CreatedUnix, note.UpdatedUnix) +func (projectBoardNote *ProjectBoardNote) GetLastEventTimestamp() timeutil.TimeStamp { + return max(projectBoardNote.CreatedUnix, projectBoardNote.UpdatedUnix) } // GetLastEventLabel returns the localization label for the current note. -func (note *BoardNote) GetLastEventLabel() string { - if note.UpdatedUnix > note.CreatedUnix { +func (projectBoardNote *ProjectBoardNote) GetLastEventLabel() string { + if projectBoardNote.UpdatedUnix > projectBoardNote.CreatedUnix { return "repo.projects.note.updated_by" } return "repo.projects.note.created_by" } -// GetTasks returns the amount of tasks in the board-notes content -func (note *BoardNote) GetTasks() int { - return len(markdown.MarkdownTasksRegex.FindAllStringIndex(note.Content, -1)) +// GetTasks returns the amount of tasks in the project-board-notes content +func (projectBoardNote *ProjectBoardNote) GetTasks() int { + return len(markdown.MarkdownTasksRegex.FindAllStringIndex(projectBoardNote.Content, -1)) } -// GetTasksDone returns the amount of completed tasks in the board-notes content -func (note *BoardNote) GetTasksDone() int { - return len(markdown.MarkdownTasksDoneRegex.FindAllStringIndex(note.Content, -1)) +// GetTasksDone returns the amount of completed tasks in the project-board-notes content +func (projectBoardNote *ProjectBoardNote) GetTasksDone() int { + return len(markdown.MarkdownTasksDoneRegex.FindAllStringIndex(projectBoardNote.Content, -1)) } -// UpdateBoardNote changes a BoardNote -func UpdateBoardNote(ctx context.Context, note *BoardNote) error { +// UpdateProjectBoardNote changes a BoardNote +func UpdateProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardNote) error { var fieldToUpdate []string fieldToUpdate = append(fieldToUpdate, "title") fieldToUpdate = append(fieldToUpdate, "content") - _, err := db.GetEngine(ctx).ID(note.ID).Cols(fieldToUpdate...).Update(note) + _, err := db.GetEngine(ctx).ID(projectBoardNote.ID).Cols(fieldToUpdate...).Update(projectBoardNote) return err } -// MoveBoardNoteOnProjectBoard moves or keeps notes in a column and sorts them inside that column -func MoveBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedNoteIDs map[int64]int64) error { +// MoveProjectBoardNoteOnProjectBoard moves or keeps notes in a column and sorts them inside that column +func MoveProjectBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedProjectBoardNoteIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) - for sorting, issueID := range sortedNoteIDs { - _, err := sess.Exec("UPDATE `board_note` SET board_id=?, sorting=? WHERE id=?", board.ID, sorting, issueID) + for sorting, issueID := range sortedProjectBoardNoteIDs { + _, err := sess.Exec("UPDATE `project_board_note` SET board_id=?, sorting=? WHERE id=?", board.ID, sorting, issueID) if err != nil { return err } @@ -350,21 +350,21 @@ func MoveBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedNoteID }) } -func deleteBoardNoteByProjectID(ctx context.Context, projectID int64) error { - _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&BoardNote{}) +func deleteProjectBoardNoteByProjectID(ctx context.Context, projectID int64) error { + _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&ProjectBoardNote{}) return err } -// DeleteBoardNote removes the BoardNote from the project board. -func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { +// DeleteProjectBoardNote removes the BoardNote from the project board. +func DeleteProjectBoardNote(ctx context.Context, boardNote *ProjectBoardNote) error { if _, err := db.GetEngine(ctx).Delete(boardNote); err != nil { return err } return nil } -// removeBoardNotes sets the boardID to 0 for the board -func (b *Board) removeBoardNotes(ctx context.Context) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `board_note` SET board_id = 0 WHERE board_id = ?", b.ID) +// removeProjectBoardNotes sets the boardID to 0 for the board +func (b *Board) removeProjectBoardNotes(ctx context.Context) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `project_board_note` SET board_id = 0 WHERE board_id = ?", b.ID) return err } diff --git a/models/project/project.go b/models/project/project.go index c42e8b47b2bef..a9fe317537f09 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -97,7 +97,7 @@ func IsErrProjectBoardNoteNotExist(err error) bool { } func (err ErrProjectBoardNoteNotExist) Error() string { - return fmt.Sprintf("project board-note does not exist [id: %d]", err.BoardNoteID) + return fmt.Sprintf("project-board-note does not exist [id: %d]", err.BoardNoteID) } func (err ErrProjectBoardNoteNotExist) Unwrap() error { @@ -430,7 +430,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error { return err } - if err := deleteBoardNoteByProjectID(ctx, id); err != nil { + if err := deleteProjectBoardNoteByProjectID(ctx, id); err != nil { return err } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 2da39b04b4bbe..47e4e7bcc08ae 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -324,7 +324,7 @@ func ViewProject(ctx *context.Context) { return } - notesMap, err := project.LoadBoardNotesFromBoardList(ctx, boards) + notesMap, err := project.LoadProjectBoardNotesFromBoardList(ctx, boards) if err != nil { ctx.ServerError("LoadNotesOfBoards", err) return @@ -395,7 +395,7 @@ func ViewProject(ctx *context.Context) { return } - pinnedBoardNotes, err := project_model.GetPinnedBoardNotes(ctx, project.ID) + pinnedBoardNotes, err := project_model.GetPinnedProjectBoardNotes(ctx, project.ID) if err != nil { ctx.ServerError("GetPinnedBoardNotes", err) return @@ -422,8 +422,8 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend - ctx.Data["PinnedNotes"] = pinnedBoardNotes - ctx.Data["NotesMap"] = notesMap + ctx.Data["PinnedProjectBoardNotes"] = pinnedBoardNotes + ctx.Data["ProjectBoardNotesMap"] = notesMap ctx.HTML(http.StatusOK, tplProjectsView) } @@ -747,7 +747,7 @@ func MoveIssues(ctx *context.Context) { ctx.JSONOK() } -func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.BoardNote) { +func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.ProjectBoardNote) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -772,33 +772,33 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return nil, nil } - note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { if project_model.IsErrProjectBoardNoteNotExist(err) { ctx.NotFound("ProjectBoardNoteNotFound", err) } else { - ctx.ServerError("GetBoardNoteById", err) + ctx.ServerError("GetProjectBoardNoteById", err) } return nil, nil } - if note.ProjectID != project.ID { + if projectBoardNote.ProjectID != project.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", note.ID, project.ID), + "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), }) return nil, nil } if project.RepoID != ctx.Repo.Repository.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", note.ID, ctx.Repo.Repository.ID), + "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", projectBoardNote.ID, ctx.Repo.Repository.ID), }) return nil, nil } - return project, note + return project, projectBoardNote } -func AddNoteToBoard(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.BoardNoteForm) +func AddProjectBoardNoteToBoard(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", @@ -806,7 +806,7 @@ func AddNoteToBoard(ctx *context.Context) { return } - if err := project_model.NewBoardNote(ctx, &project_model.BoardNote{ + if err := project_model.NewProjectBoardNote(ctx, &project_model.ProjectBoardNote{ Title: form.Title, Content: form.Content, @@ -821,17 +821,17 @@ func AddNoteToBoard(ctx *context.Context) { ctx.JSONOK() } -func EditBoardNote(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.BoardNoteForm) - _, boardNote := checkProjectBoardNoteChangePermissions(ctx) +func EditProjectBoardNote(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) + _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) if ctx.Written() { return } - boardNote.Title = form.Title - boardNote.Content = form.Content + projectBoardNote.Title = form.Title + projectBoardNote.Content = form.Content - if err := project_model.UpdateBoardNote(ctx, boardNote); err != nil { + if err := project_model.UpdateProjectBoardNote(ctx, projectBoardNote); err != nil { ctx.ServerError("UpdateProjectBoardNote", err) return } @@ -839,21 +839,21 @@ func EditBoardNote(ctx *context.Context) { ctx.JSONOK() } -func DeleteBoardNote(ctx *context.Context) { - _, boardNote := checkProjectBoardNoteChangePermissions(ctx) +func DeleteProjectBoardNote(ctx *context.Context) { + _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) if ctx.Written() { return } - if err := project_model.DeleteBoardNote(ctx, boardNote); err != nil { - ctx.ServerError("DeleteBoardNote", err) + if err := project_model.DeleteProjectBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("DeleteProjectBoardNote", err) return } ctx.JSONOK() } -func MoveBoardNote(ctx *context.Context) { +func MoveProjectBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -902,58 +902,58 @@ func MoveBoardNote(ctx *context.Context) { } } - type MovedBoardNotesForm struct { - BoardNotes []struct { - BoardNoteID int64 `json:"boardNoteID"` - Sorting int64 `json:"sorting"` - } `json:"boardNotes"` + type MovedProjectBoardNotesForm struct { + ProjectBoardNotes []struct { + ProjectBoardNoteID int64 `json:"projectBoardNoteID"` + Sorting int64 `json:"sorting"` + } `json:"projectBoardNotes"` } - form := &MovedBoardNotesForm{} + form := &MovedProjectBoardNotesForm{} if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("DecodeMovedBoardNotesForm", err) + ctx.ServerError("DecodeMovedProjectBoardNotesForm", err) } - boardNoteIDs := make([]int64, 0, len(form.BoardNotes)) - sortedBoardNoteIDs := make(map[int64]int64) - for _, boardNote := range form.BoardNotes { - boardNoteIDs = append(boardNoteIDs, boardNote.BoardNoteID) - sortedBoardNoteIDs[boardNote.Sorting] = boardNote.BoardNoteID + projectBoardNoteIDs := make([]int64, 0, len(form.ProjectBoardNotes)) + sortedProjectBoardNoteIDs := make(map[int64]int64) + for _, boardNote := range form.ProjectBoardNotes { + projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.ProjectBoardNoteID) + sortedProjectBoardNoteIDs[boardNote.Sorting] = boardNote.ProjectBoardNoteID } - movedBoardNotes, err := project_model.GetBoardNoteByIds(ctx, boardNoteIDs) + movedProjectBoardNotes, err := project_model.GetProjectBoardNotesByIds(ctx, projectBoardNoteIDs) if err != nil { if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("BoardNoteNotExisting", nil) + ctx.NotFound("ProjectBoardNoteNotExisting", nil) } else { - ctx.ServerError("GetBoardNoteByIds", err) + ctx.ServerError("GetProjectBoardNoteByIds", err) } return } - if len(movedBoardNotes) != len(form.BoardNotes) { - ctx.ServerError("some board-notes do not exist", errors.New("some board-notes do not exist")) + if len(movedProjectBoardNotes) != len(form.ProjectBoardNotes) { + ctx.ServerError("some project-board-notes do not exist", errors.New("some project-board-notes do not exist")) return } - if err = project_model.MoveBoardNoteOnProjectBoard(ctx, board, sortedBoardNoteIDs); err != nil { - ctx.ServerError("MoveBoardNoteOnProjectBoard", err) + if err = project_model.MoveProjectBoardNoteOnProjectBoard(ctx, board, sortedProjectBoardNoteIDs); err != nil { + ctx.ServerError("MoveProjectBoardNoteOnProjectBoard", err) return } ctx.JSONOK() } -// PinBoardNote pins the BoardNote -func PinBoardNote(ctx *context.Context) { - note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +// PinProjectBoardNote pins the BoardNote +func PinProjectBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetBoardNoteByID", err) + ctx.ServerError("GetProjectBoardNoteByID", err) return } - err = note.Pin(ctx) + err = projectBoardNote.Pin(ctx) if err != nil { - ctx.ServerError("Pin", err) + ctx.ServerError("PinProjectBoardNote", err) return } @@ -961,8 +961,8 @@ func PinBoardNote(ctx *context.Context) { } // PinBoardNote unpins the BoardNote -func UnPinBoardNote(ctx *context.Context) { - note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +func UnPinProjectBoardNote(ctx *context.Context) { + note, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { ctx.ServerError("GetBoardNoteByID", err) return @@ -970,7 +970,7 @@ func UnPinBoardNote(ctx *context.Context) { err = note.Unpin(ctx) if err != nil { - ctx.ServerError("Unpin", err) + ctx.ServerError("UnpinProjectBoardNote", err) return } @@ -978,31 +978,31 @@ func UnPinBoardNote(ctx *context.Context) { } // PinBoardNote moves a pined the BoardNote -func PinMoveBoardNote(ctx *context.Context) { +func PinMoveProjectBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, "Only signed in users are allowed to perform this action.") return } - type movePinBoardNoteForm struct { + type MovePinProjectBoardNoteForm struct { Position int64 `json:"position"` } - form := &movePinBoardNoteForm{} + form := &MovePinProjectBoardNoteForm{} if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("Decode movePinBoardNoteForm", err) + ctx.ServerError("Decode MovePinProjectBoardNoteForm", err) return } - note, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + note, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetIssueByID", err) + ctx.ServerError("GetProjectBoardNoteByID", err) return } err = note.MovePin(ctx, form.Position) if err != nil { - ctx.ServerError("MovePin", err) + ctx.ServerError("MovePinProjectBoardNote", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 711e38af71698..f16bab10069cf 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1343,17 +1343,17 @@ func registerRoutes(m *web.Route) { m.Post("/move", repo.MoveIssues) m.Group("/note", func() { - m.Post("", web.Bind(forms.BoardNoteForm{}), repo.AddNoteToBoard) - m.Post("/move", repo.MoveBoardNote) + m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), repo.AddProjectBoardNoteToBoard) + m.Post("/move", repo.MoveProjectBoardNote) m.Group("/{noteID}", func() { - m.Put("", web.Bind(forms.BoardNoteForm{}), repo.EditBoardNote) - m.Delete("", repo.DeleteBoardNote) + m.Put("", web.Bind(forms.ProjectBoardNoteForm{}), repo.EditProjectBoardNote) + m.Delete("", repo.DeleteProjectBoardNote) m.Group("/pin", func() { - m.Post("", web.Bind(forms.BoardNoteForm{}), repo.PinBoardNote) - m.Delete("", repo.UnPinBoardNote) - m.Post("/move", repo.PinMoveBoardNote) + m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), repo.PinProjectBoardNote) + m.Delete("", repo.UnPinProjectBoardNote) + m.Post("/move", repo.PinMoveProjectBoardNote) }) }) }) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index c412ada440379..2f286f1a2116c 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -533,8 +533,8 @@ type EditProjectBoardForm struct { Color string `binding:"MaxSize(7)"` } -// BoardNoteForm is a form for editing/creating a note to a board -type BoardNoteForm struct { +// ProjectBoardNoteForm is a form for editing/creating a note to a board +type ProjectBoardNoteForm struct { Title string `binding:"Required;MaxSize(255)"` Content string } diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 314bb692136be..1da3dbae7ea43 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,8 +1,8 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} -{{if gt (len .PinnedNotes) 0}} +{{if gt (len .PinnedProjectBoardNotes) 0}} <div id="pinned-notes"> - {{range .PinnedNotes}} + {{range .PinnedProjectBoardNotes}} <div class="note-card pinned-card {{if $canWriteProject}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . @@ -87,7 +87,7 @@ <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> <div class="ui small circular grey label project-column-issue-note-count"> - {{.NumIssuesAndNotes ctx}} + {{.NumIssuesAndProjectBoardNotes ctx}} </div> {{.Title}} </div> @@ -219,9 +219,9 @@ <div class="divider"></div> <div class="cards-wrapper"> - {{if or $canWriteProject (index $.NotesMap .ID)}} + {{if or $canWriteProject (index $.ProjectBoardNotesMap .ID)}} <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> - {{range (index $.NotesMap .ID)}} + {{range (index $.ProjectBoardNotesMap .ID)}} <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . From 1c12d878e3144917744268b3f044b45c1464ff63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Tue, 6 Feb 2024 20:52:21 +0100 Subject: [PATCH 26/46] refactor: LoadAttributes function --- models/project/note.go | 55 +++++++++++++++++------------------- routers/web/repo/projects.go | 6 ++-- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index e66aa45372860..9a627807f5a0e 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -70,6 +70,10 @@ func GetProjectBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) return nil, err } + if err := projectBoardNoteList.LoadAttributes(ctx); err != nil { + return nil, err + } + return projectBoardNoteList, nil } @@ -110,17 +114,8 @@ func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (Pro return nil, fmt.Errorf("unable to query project-board-notes: %w", err) } - // @TODO: same code in `GetPinnedBoardNotes()` and should be used with `LoadAttributes()` - for _, note := range projectBoardNoteList { - creator := new(user_model.User) - has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) - if err != nil { - return nil, err - } - if !has { - return nil, user_model.ErrUserNotExist{UID: note.CreatorID} - } - note.Creator = creator + if err := projectBoardNoteList.LoadAttributes(ctx); err != nil { + return nil, err } return projectBoardNoteList, nil @@ -132,11 +127,22 @@ func NewProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardNote return err } -/* @TODO: make it work - markdown.RenderString should also be at this function -// IsPinned returns if a BoardNote is pinned -func (notes BoardNoteList) LoadAttributes() error { +// LoadAttributes prerenders the markdown content and sets the creator +func (projectBoardNoteList ProjectBoardNoteList) LoadAttributes(ctx context.Context) error { + for _, projectBoardNote := range projectBoardNoteList { + creator := new(user_model.User) + has, err := db.GetEngine(ctx).ID(projectBoardNote.CreatorID).Get(creator) + if err != nil { + return err + } + if !has { + return user_model.ErrUserNotExist{UID: projectBoardNote.CreatorID} + } + projectBoardNote.Creator = creator + } + return nil -} */ +} var ErrBoardNoteMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned project-board-notes has been readched") @@ -273,32 +279,23 @@ func (projectBoardNote *ProjectBoardNote) MovePin(ctx context.Context, newPositi // GetPinnedProjectBoardNotes returns the pinned BaordNotes for the given Project func GetPinnedProjectBoardNotes(ctx context.Context, projectID int64) (ProjectBoardNoteList, error) { - notes := make(ProjectBoardNoteList, 0) + projectBoardNoteList := make(ProjectBoardNoteList, 0) err := db.GetEngine(ctx). Table("project_board_note"). Where("project_id = ?", projectID). And("pin_order > 0"). OrderBy("pin_order"). - Find(¬es) + Find(&projectBoardNoteList) if err != nil { return nil, err } - // @TODO: same code in `BoardNotes()` and should be used with `LoadAttributes()` - for _, note := range notes { - creator := new(user_model.User) - has, err := db.GetEngine(ctx).ID(note.CreatorID).Get(creator) - if err != nil { - return nil, err - } - if !has { - return nil, user_model.ErrUserNotExist{UID: note.CreatorID} - } - note.Creator = creator + if err := projectBoardNoteList.LoadAttributes(ctx); err != nil { + return nil, err } - return notes, nil + return projectBoardNoteList, nil } // GetLastEventTimestamp returns the last user visible event timestamp, either the creation or the update. diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 47e4e7bcc08ae..405c8ba305bb6 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -326,11 +326,10 @@ func ViewProject(ctx *context.Context) { notesMap, err := project.LoadProjectBoardNotesFromBoardList(ctx, boards) if err != nil { - ctx.ServerError("LoadNotesOfBoards", err) + ctx.ServerError("LoadProjectBoardNotesOfBoards", err) return } - // @TODO: maybe should be in BoardNote for _, noteList := range notesMap { for _, note := range noteList { note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ @@ -397,11 +396,10 @@ func ViewProject(ctx *context.Context) { pinnedBoardNotes, err := project_model.GetPinnedProjectBoardNotes(ctx, project.ID) if err != nil { - ctx.ServerError("GetPinnedBoardNotes", err) + ctx.ServerError("GetPinnedProjectBoardNotes", err) return } - // @TODO: maybe should be in BoardNote for _, note := range pinnedBoardNotes { note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ Links: markup.Links{ From 7b28107636b96671f2c8293211051664e8051270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 15:14:05 +0100 Subject: [PATCH 27/46] fix: missed translations for form --- routers/web/repo/projects.go | 11 +++++++---- web_src/js/features/repo-projects.js | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 405c8ba305bb6..036c8819f4b0b 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -707,6 +707,7 @@ func MoveIssues(ctx *context.Context) { form := &movedIssuesForm{} if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { ctx.ServerError("DecodeMovedIssuesForm", err) + return } issueIDs := make([]int64, 0, len(form.Issues)) @@ -792,6 +793,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode }) return nil, nil } + return project, projectBoardNote } @@ -910,6 +912,7 @@ func MoveProjectBoardNote(ctx *context.Context) { form := &MovedProjectBoardNotesForm{} if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { ctx.ServerError("DecodeMovedProjectBoardNotesForm", err) + return } projectBoardNoteIDs := make([]int64, 0, len(form.ProjectBoardNotes)) @@ -960,13 +963,13 @@ func PinProjectBoardNote(ctx *context.Context) { // PinBoardNote unpins the BoardNote func UnPinProjectBoardNote(ctx *context.Context) { - note, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { ctx.ServerError("GetBoardNoteByID", err) return } - err = note.Unpin(ctx) + err = projectBoardNote.Unpin(ctx) if err != nil { ctx.ServerError("UnpinProjectBoardNote", err) return @@ -992,13 +995,13 @@ func PinMoveProjectBoardNote(ctx *context.Context) { return } - note, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { ctx.ServerError("GetProjectBoardNoteByID", err) return } - err = note.MovePin(ctx, form.Position) + err = projectBoardNote.MovePin(ctx, form.Position) if err != nil { ctx.ServerError("MovePinProjectBoardNote", err) return diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 04a67a625ec98..c67d8ff4a6766 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -59,8 +59,8 @@ function moveNote({item, from, to, oldIndex}) { updateIssueAndNoteCount(to); const columnSorting = { - boardNotes: Array.from(columnCards, (card, i) => ({ - boardNoteID: parseInt($(card).data('note')), + projectBoardNotes: Array.from(columnCards, (card, i) => ({ + projectBoardNoteID: parseInt($(card).data('note')), sorting: i, })), }; From f3ce6317938d3e1c329efb5a894714edad97a06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 15:15:50 +0100 Subject: [PATCH 28/46] fix: projectBoardNote delete --- templates/repo/note.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 585a77ce9260c..1b0705bb5e45f 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -93,7 +93,7 @@ <div class="header"> {{ctx.Locale.Tr "repo.projects.note.delete"}} </div> - <form class="ui form form-fetch-action" action="delete"> + <form class="ui form form-fetch-action" method="delete"> {{$.CsrfTokenHtml}} <div class="content"> <label> From 99cf2d110cf190f0a1e4c4ec08d9b2f669bc99c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 21:09:32 +0100 Subject: [PATCH 29/46] enhance: add tags to project-board-notes --- models/project/note.go | 20 +++++--- models/project/note_label.go | 65 ++++++++++++++++++++++++ models/project/project.go | 23 ++++++++- modules/templates/helper.go | 1 + modules/templates/util_render.go | 20 ++++++++ options/locale/locale_en-US.ini | 11 ++-- routers/web/repo/projects.go | 83 +++++++++++++++++++++++++++++++ templates/projects/view.tmpl | 16 +++--- templates/repo/note.tmpl | 65 ++++++++++++++++++------ web_src/css/features/projects.css | 8 +++ 10 files changed, 274 insertions(+), 38 deletions(-) create mode 100644 models/project/note_label.go diff --git a/models/project/note.go b/models/project/note.go index 9a627807f5a0e..084a17c7020a9 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -19,12 +19,13 @@ import ( // ProjectBoardNote is used to represent a note on a boards type ProjectBoardNote struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"TEXT NOT NULL"` - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` - PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + RenderedContent string `xorm:"-"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` + LabelIDs []int64 `xorm:"-"` // can't be []*Label because of 'import cycle not allowed' ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` @@ -46,6 +47,7 @@ type ProjectBoardNotesOptions struct { func init() { db.RegisterModel(new(ProjectBoardNote)) + db.RegisterModel(new(ProjectBoardNoteLabel)) } // GetProjectBoardNoteByID load notes assigned to the boards @@ -56,7 +58,7 @@ func GetProjectBoardNoteByID(ctx context.Context, projectBoardNoteID int64) (*Pr if err != nil { return nil, err } else if !has { - return nil, ErrProjectBoardNoteNotExist{BoardNoteID: projectBoardNoteID} + return nil, ErrProjectBoardNoteNotExist{ProjectBoardNoteID: projectBoardNoteID} } return projectBoardNote, nil @@ -139,6 +141,10 @@ func (projectBoardNoteList ProjectBoardNoteList) LoadAttributes(ctx context.Cont return user_model.ErrUserNotExist{UID: projectBoardNote.CreatorID} } projectBoardNote.Creator = creator + + if err := projectBoardNote.LoadLabelIDs(ctx); err != nil { + return err + } } return nil diff --git a/models/project/note_label.go b/models/project/note_label.go new file mode 100644 index 0000000000000..71a2defd35703 --- /dev/null +++ b/models/project/note_label.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" +) + +// ProjectBoardNoteLabel represents an project-baord-note-label relation. +type ProjectBoardNoteLabel struct { + ID int64 `xorm:"pk autoincr"` + ProjectBoardNoteID int64 `xorm:"INDEX NOT NULL"` + LabelID int64 `xorm:"INDEX NOT NULL"` +} + +// LoadLabels loads labels +func (projectBoardNote *ProjectBoardNote) LoadLabelIDs(ctx context.Context) (err error) { + if projectBoardNote.LabelIDs != nil || len(projectBoardNote.LabelIDs) == 0 { + projectBoardNote.LabelIDs, err = GetLabelsByProjectBoardNoteID(ctx, projectBoardNote.ID) + if err != nil { + return fmt.Errorf("GetLabelsByProjectBoardNoteID [%d]: %w", projectBoardNote.ID, err) + } + } + return nil +} + +// LoadLabels removes all labels from project-board-note +func (projectBoardNote *ProjectBoardNote) RemoveAllLabels(ctx context.Context) error { + _, err := db.GetEngine(ctx).Where("project_board_note_id = ?", projectBoardNote.ID).Delete(ProjectBoardNoteLabel{}) + return err +} + +// LoadLabels add a label to project-board-note -> requires a valid labelID +func (projectBoardNote *ProjectBoardNote) AddLabel(ctx context.Context, labelID int64) error { + _, err := db.GetEngine(ctx).Insert(ProjectBoardNoteLabel{ + ProjectBoardNoteID: projectBoardNote.ID, + LabelID: labelID, + }) + return err +} + +// LoadLabels removes a label from project-board-note +func (projectBoardNote *ProjectBoardNote) RemoveLabelByID(ctx context.Context, labelID int64) error { + _, err := db.GetEngine(ctx).Delete(ProjectBoardNoteLabel{ + ProjectBoardNoteID: projectBoardNote.ID, + LabelID: labelID, + }) + return err +} + +// GetLabelsByProjectBoardNoteID returns all labelIDs that belong to given projectBoardNote by ID. +func GetLabelsByProjectBoardNoteID(ctx context.Context, projectBoardNoteID int64) ([]int64, error) { + var labelIDs []int64 + return labelIDs, db.GetEngine(ctx). + Table("label"). + Cols("label.id"). + Asc("label.name"). + Where("project_board_note_label.project_board_note_id = ?", projectBoardNoteID). + Join("INNER", "project_board_note_label", "project_board_note_label.label_id = label.id"). + Find(&labelIDs) +} diff --git a/models/project/project.go b/models/project/project.go index a9fe317537f09..38621f449472a 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -87,7 +87,7 @@ func (err ErrProjectBoardNotExist) Unwrap() error { // ErrProjectBoardNoteNotExist represents a "ProjectBoardNotExist" kind of error. type ErrProjectBoardNoteNotExist struct { - BoardNoteID int64 + ProjectBoardNoteID int64 } // IsErrProjectBoardNoteNotExist checks if an error is a ErrProjectBoardNoteNotExist @@ -97,13 +97,32 @@ func IsErrProjectBoardNoteNotExist(err error) bool { } func (err ErrProjectBoardNoteNotExist) Error() string { - return fmt.Sprintf("project-board-note does not exist [id: %d]", err.BoardNoteID) + return fmt.Sprintf("project-board-note does not exist [id: %d]", err.ProjectBoardNoteID) } func (err ErrProjectBoardNoteNotExist) Unwrap() error { return util.ErrNotExist } +// ErrProjectBoardNoteLabelNotExist represents a "ProjectBoardNotExist" kind of error. +type ErrProjectBoardNoteLabelNotExist struct { + ProjectBoardNoteLabelID int64 +} + +// IsErrProjectBoardNoteLabelNotExist checks if an error is a ErrProjectBoardNoteLabelNotExist +func IsErrProjectBoardNoteLabelNotExist(err error) bool { + _, ok := err.(ErrProjectBoardNoteLabelNotExist) + return ok +} + +func (err ErrProjectBoardNoteLabelNotExist) Error() string { + return fmt.Sprintf("project-board-note-label does not exist [id: %d]", err.ProjectBoardNoteLabelID) +} + +func (err ErrProjectBoardNoteLabelNotExist) Unwrap() error { + return util.ErrNotExist +} + // Project represents a project board type Project struct { ID int64 `xorm:"pk autoincr"` diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 96cdd9ca46c0c..36c5f74eb9d7e 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -165,6 +165,7 @@ func NewFuncMap() template.FuncMap { "RenderMarkdownToHtml": RenderMarkdownToHtml, "RenderLabel": RenderLabel, "RenderLabels": RenderLabels, + "RenderLabelsFromIDs": RenderLabelsFromIDs, // ----------------------------------------------------------------- // misc diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 1d9635410b370..6c88a009cf6cd 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -224,3 +224,23 @@ func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink st htmlCode += "</span>" return template.HTML(htmlCode) } + +func RenderLabelsFromIDs(ctx context.Context, labelIDs []int64, repoLink string) template.HTML { + labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs) + if err != nil { + log.Error("GetLabelsByIDs", err) + return "" + } + + htmlCode := `<span class="labels-list">` + for _, label := range labels { + // Protect against nil value in labels - shouldn't happen but would cause a panic if so + if label == nil { + continue + } + htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ", + repoLink, label.ID, RenderLabel(ctx, label)) + } + htmlCode += "</span>" + return template.HTML(htmlCode) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e972e1a7d4575..073d7a1337e18 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1352,14 +1352,15 @@ projects.card_type.desc = "Card Previews" projects.card_type.images_and_text = "Images and Text" projects.card_type.text_only = "Text Only" -projects.note.view = View Content +projects.note.view = View Note projects.note.new = New Note -projects.note.new_title = Title -projects.note.content = Write a Note +projects.note.title = Title +projects.note.labels = Labels +projects.note.no_label = No Label +projects.note.description = Description projects.note.edit = Edit Note -projects.note.edit_title = Title projects.note.delete = Delete Note -projects.note.deletion_desc = Deleting removes the note from the project column. Continue? +projects.note.deletion_description = Deleting removes the note from the project column. Continue? projects.note.created_by = created %[1]s by <a href="%[2]s">%[3]s</a> projects.note.updated_by = updated %[1]s by <a href="%[2]s">%[3]s</a> projects.note.max_pinned = You can't pin more notes diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 036c8819f4b0b..925e75f9dd149 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -415,6 +416,13 @@ func ViewProject(ctx *context.Context) { } } + labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByRepoID", err) + return + } + + ctx.Data["Labels"] = labels ctx.Data["IsProjectsPage"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["Project"] = project @@ -821,6 +829,31 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { ctx.JSONOK() } +func findNewAndRemovedIDs(original []int64, updated []int64) (newValues, removedValues []int64) { + if original == nil { + return updated, nil + } + if updated == nil { + return nil, nil + } + + for _, v := range updated { + if !slices.Contains(original, v) { + // If the value is not in the original map, it's new + newValues = append(newValues, v) + } + } + + for _, v := range original { + if !slices.Contains(updated, v) { + // If the value is not in the updated map, it's removed + removedValues = append(removedValues, v) + } + } + + return newValues, removedValues +} + func EditProjectBoardNote(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) @@ -831,11 +864,61 @@ func EditProjectBoardNote(ctx *context.Context) { projectBoardNote.Title = form.Title projectBoardNote.Content = form.Content + err := projectBoardNote.LoadLabelIDs(ctx) + if err != nil { + ctx.ServerError("LoadLabelIDs", err) + } + if err := project_model.UpdateProjectBoardNote(ctx, projectBoardNote); err != nil { ctx.ServerError("UpdateProjectBoardNote", err) return } + // LabelIDs is send without parentheses - maybe because of multipart/form-data + labelIdsString := "[" + ctx.Req.FormValue("labelIds") + "]" + var labelIDs []int64 + if err := json.Unmarshal([]byte(labelIdsString), &labelIDs); err != nil { + ctx.ServerError("Unmarshal", err) + } + + newLabelIDs, removedLabelIDs := findNewAndRemovedIDs(projectBoardNote.LabelIDs, labelIDs) + + for _, labelID := range newLabelIDs { + label, err := issues_model.GetLabelByID(ctx, labelID) + if err != nil { + if issues_model.IsErrLabelNotExist(err) { + ctx.Error(http.StatusNotFound, "GetLabelByID") + } else { + ctx.ServerError("GetLabelByID", err) + } + return + } + + err = projectBoardNote.AddLabel(ctx, label.ID) + if err != nil { + ctx.ServerError("AddLabel", err) + return + } + } + + for _, labelID := range removedLabelIDs { + label, err := issues_model.GetLabelByID(ctx, labelID) + if err != nil { + if issues_model.IsErrLabelNotExist(err) { + ctx.Error(http.StatusNotFound, "GetLabelByID") + } else { + ctx.ServerError("GetLabelByID", err) + } + return + } + + err = projectBoardNote.RemoveLabelByID(ctx, label.ID) + if err != nil { + ctx.ServerError("RemoveLabelByID", err) + return + } + } + ctx.JSONOK() } diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 1da3dbae7ea43..d9db070a634bc 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -6,11 +6,9 @@ <div class="note-card pinned-card {{if $canWriteProject}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . + "root" $ "CanWriteProjects" $canWriteProject - "Link" $.Link - "RepoLink" $.RepoLink - "ProjectID" $.Project.ID - "CsrfTokenHtml" $.CsrfTokenHtml) + "RenderModals" false) }} </div> {{end}} @@ -143,7 +141,7 @@ <div class="content"> {{$.CsrfTokenHtml}} <div class="required field"> - <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.new_title"}}</label> + <label for="new-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.title"}}</label> <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" autofocus required> </div> @@ -152,7 +150,7 @@ "MarkdownPreviewUrl" (print $.RepoLink "/markup") "MarkdownPreviewContext" $.RepoLink "TextareaName" "content" - "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") + "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.description") )}} </div> </div> @@ -225,11 +223,9 @@ <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict "BoardNote" . + "root" $ "CanWriteProjects" $canWriteProject - "Link" $.Link - "RepoLink" $.RepoLink - "ProjectID" $.Project.ID - "CsrfTokenHtml" $.CsrfTokenHtml) + "RenderModals" true) }} </div> {{end}} diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 1b0705bb5e45f..daf04f8a4bd8b 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -19,11 +19,11 @@ {{if $.CanWriteProjects}} <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> - <a class="ui item button create-project-issue-from-note" href="{{$.RepoLink}}/issues/new/choose?project={{$.ProjectID}}"> + <a class="ui item button create-project-issue-from-note" href="{{$.root.RepoLink}}/issues/new/choose?project={{$.root.Project.ID}}"> {{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.new"}} </a> @@ -31,7 +31,7 @@ <button class="ui item button {{if $notPinnedAndNotAllowedToPin}}disabled{{end}} pin-project-column-note" {{if $notPinnedAndNotAllowedToPin}}style="pointer-events: unset !important"{{end}} data-method="{{if .IsPinned}}delete{{else}}post{{end}}" - data-url="{{$.Link}}/{{.BoardID}}/note/{{.ID}}/pin" + data-url="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}/pin" {{if $notPinnedAndNotAllowedToPin}}data-tooltip-content="{{ctx.Locale.Tr "repo.projects.note.max_pinned"}}"{{end}}> {{if .IsPinned}} {{svg "octicon-pin-slash"}} @@ -43,7 +43,7 @@ </button> <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.Link}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} </button> @@ -53,34 +53,68 @@ <div class="header"> {{ctx.Locale.Tr "repo.projects.note.view"}} </div> - <div class="content"> - {{Str2html .RenderedContent}} + <div class="content ui form"> + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.title"}}</label> + {{.Title}} + </div> + + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + {{if .LabelIDs}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.Link}} + {{else}} + {{ctx.Locale.Tr "repo.projects.note.no_label"}} + {{end}} + </div> + + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.description"}}</label> + {{Str2html .RenderedContent}} + </div> </div> <div class="actions"> <button class="ui cancel button">{{ctx.Locale.Tr "repo.projects.close"}}</button> </div> </div> - {{if $.CanWriteProjects}} + {{if and $.RenderModals $.CanWriteProjects}} <div class="ui modal" id="edit-project-column-note-modal-{{.ID}}"> <div class="header"> {{ctx.Locale.Tr "repo.projects.note.edit"}} </div> <form class="ui form form-fetch-action" method="put"> <div class="content"> - {{$.CsrfTokenHtml}} + {{$.root.CsrfTokenHtml}} <div class="required field"> - <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.edit_title"}}</label> + <label for="edit-project-column-note-title-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.title"}}</label> <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" autofocus required> </div> + <div class="field"> + <label for="edit-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + <div class="ui fluid selection dropdown multiple clearable"> + <input type="hidden" id="edit-project-column-note-label-{{.ID}}" name="labelIds" value="{{range $index, $val := .LabelIDs}}{{if ne $index 0}},{{end}}{{$val}}{{end}}"> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="default text"></div> + <div class="menu"> + {{range $.root.Labels}} + <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> + <small class="desc">{{.Description | RenderEmoji ctx}}</small> + <span>{{RenderLabel ctx .}}</span> + </div> + {{end}} + </div> + </div> + </div> + <div class="field"> {{template "shared/combomarkdowneditor" (dict - "MarkdownPreviewUrl" (print $.RepoLink "/markup") - "MarkdownPreviewContext" $.RepoLink + "MarkdownPreviewUrl" (print $.root.RepoLink "/markup") + "MarkdownPreviewContext" $.root.RepoLink "TextareaName" "content" "TextareaContent" .Content - "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.content") + "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.description") )}} </div> </div> @@ -94,10 +128,10 @@ {{ctx.Locale.Tr "repo.projects.note.delete"}} </div> <form class="ui form form-fetch-action" method="delete"> - {{$.CsrfTokenHtml}} + {{$.root.CsrfTokenHtml}} <div class="content"> <label> - {{ctx.Locale.Tr "repo.projects.note.deletion_desc"}} + {{ctx.Locale.Tr "repo.projects.note.deletion_description"}} </label> </div> {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} @@ -124,5 +158,8 @@ <span class="gt-vm">{{.GetTasksDone}} / {{$tasks}}</span> </div> {{end}} + {{if .LabelIDs}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.Link}} + {{end}} </div> {{end}} diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index eb9a27c3aa8a4..7ff3ff6dc3299 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -3,6 +3,14 @@ overflow-y: auto; } +.ui.modal .ui.selection { + flex-wrap: wrap; +} + +.ui.modal .ui.selection > a > .desc { + display: none; +} + #pinned-notes { display: grid; grid-template-columns: repeat(3, 1fr); From 1bd15a477a02c7d539fd65decde0429501e5594b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 22:39:35 +0100 Subject: [PATCH 30/46] fix: init markdownEditor on pinned notes --- web_src/js/features/repo-projects.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index c67d8ff4a6766..402549b7c9fca 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import {useLightTextOnBackground} from '../utils/color.js'; import tinycolor from 'tinycolor2'; import {createSortable} from '../modules/sortable.js'; -import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; +import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; const {csrfToken} = window.config; @@ -172,7 +172,7 @@ export function initRepoProject() { return; } - const modalButtons = mainContent.find('.project-column button[data-modal]'); + const modalButtons = mainContent.find(':where(.project-column, #pinned-notes) button[data-modal]'); modalButtons.each(function() { const modalButton = $(this); const modalId = modalButton.data('modal'); @@ -181,6 +181,8 @@ export function initRepoProject() { if (!markdownEditor.length) return; modalButton.one('click', () => { + const comboMarkdownEditor = getComboMarkdownEditor(markdownEditor); + if (comboMarkdownEditor) return; // only init once initComboMarkdownEditor(markdownEditor, {easyMDEOptions: {maxHeight: '50vh', minHeight: '50vh'}}); }); }); From dd7cb774dc0e9fee57b08a87fc89c4c61498f6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 22:40:48 +0100 Subject: [PATCH 31/46] enhance: select labels while creating note --- routers/web/repo/projects.go | 36 ++++++++++++++++++++++++++++++++++-- templates/projects/view.tmpl | 17 +++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 925e75f9dd149..fe302a0e4d455 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -814,18 +814,50 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { return } - if err := project_model.NewProjectBoardNote(ctx, &project_model.ProjectBoardNote{ + // LabelIDs is send without parentheses - maybe because of multipart/form-data + labelIdsString := "[" + ctx.Req.FormValue("labelIds") + "]" + var labelIDs []int64 + if err := json.Unmarshal([]byte(labelIdsString), &labelIDs); err != nil { + ctx.ServerError("Unmarshal", err) + } + + // check that all LabelsIDs are valid + for _, labelID := range labelIDs { + _, err := issues_model.GetLabelByID(ctx, labelID) + if err != nil { + if issues_model.IsErrLabelNotExist(err) { + ctx.Error(http.StatusNotFound, "GetLabelByID") + } else { + ctx.ServerError("GetLabelByID", err) + } + return + } + } + + projectBoardNote := project_model.ProjectBoardNote{ Title: form.Title, Content: form.Content, ProjectID: ctx.ParamsInt64(":id"), BoardID: ctx.ParamsInt64(":boardID"), CreatorID: ctx.Doer.ID, - }); err != nil { + } + err := project_model.NewProjectBoardNote(ctx, &projectBoardNote) + if err != nil { ctx.ServerError("NewProjectBoardNote", err) return } + if len(labelIDs) > 0 { + for _, labelID := range labelIDs { + err := projectBoardNote.AddLabel(ctx, labelID) + if err != nil { + ctx.ServerError("AddLabel", err) + return + } + } + } + ctx.JSONOK() } diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index d9db070a634bc..f85cecc48bf41 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -145,6 +145,23 @@ <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" autofocus required> </div> + <div class="field"> + <label for="new-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + <div class="ui fluid selection dropdown multiple clearable"> + <input type="hidden" id="new-project-column-note-label-{{.ID}}" name="labelIds"> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="default text"></div> + <div class="menu"> + {{range $.Labels}} + <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> + <small class="desc">{{.Description | RenderEmoji ctx}}</small> + <span>{{RenderLabel ctx .}}</span> + </div> + {{end}} + </div> + </div> + </div> + <div class="field"> {{template "shared/combomarkdowneditor" (dict "MarkdownPreviewUrl" (print $.RepoLink "/markup") From 0c770143cb893f6b1c58e0b9264550cb43efac41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 22:41:36 +0100 Subject: [PATCH 32/46] fix: unique note_label --- models/project/note_label.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/project/note_label.go b/models/project/note_label.go index 71a2defd35703..562d45670eaa6 100644 --- a/models/project/note_label.go +++ b/models/project/note_label.go @@ -13,8 +13,8 @@ import ( // ProjectBoardNoteLabel represents an project-baord-note-label relation. type ProjectBoardNoteLabel struct { ID int64 `xorm:"pk autoincr"` - ProjectBoardNoteID int64 `xorm:"INDEX NOT NULL"` - LabelID int64 `xorm:"INDEX NOT NULL"` + ProjectBoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` + LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` } // LoadLabels loads labels From 313016ebb097f9e243c3f8dd2efa8787d1bd8349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 22:46:01 +0100 Subject: [PATCH 33/46] style: break note card title --- web_src/css/repo/note-card.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/css/repo/note-card.css b/web_src/css/repo/note-card.css index 5cf2b8dc3fc16..b3bca02179733 100644 --- a/web_src/css/repo/note-card.css +++ b/web_src/css/repo/note-card.css @@ -21,6 +21,7 @@ } .note-card-title { + word-break: break-word; flex: 1; font-size: 18px; margin-left: 4px; From c87b630ac038fcde010179d7974b4a44bcd2bceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Wed, 7 Feb 2024 23:04:48 +0100 Subject: [PATCH 34/46] fix: issue content was set with old data --- web_src/js/features/repo-issue-new.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web_src/js/features/repo-issue-new.js b/web_src/js/features/repo-issue-new.js index 5aaa2bba21448..48047129a99f9 100644 --- a/web_src/js/features/repo-issue-new.js +++ b/web_src/js/features/repo-issue-new.js @@ -9,6 +9,9 @@ export function initRepoIssueNew() { const boardNoteTitle = sessionStorage.getItem('board-note-title'); const boardNoteContent = sessionStorage.getItem('board-note-content'); + sessionStorage.removeItem('board-note-title'); + sessionStorage.removeItem('board-note-content'); + if (boardNoteTitle) { const issueTitle = newIssuePage.find('#issue_title'); issueTitle.val(boardNoteTitle); From be88dedfd44faec79f904e9359bff23df7683ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Thu, 8 Feb 2024 01:32:44 +0100 Subject: [PATCH 35/46] enhance: link milestone to note --- models/project/note.go | 2 ++ modules/templates/helper.go | 2 ++ modules/templates/util_render.go | 22 +++++++++++++ routers/web/repo/projects.go | 27 ++++++++++++++-- services/forms/repo_form.go | 5 +-- templates/projects/milestone_selection.tmpl | 36 +++++++++++++++++++++ templates/projects/view.tmpl | 5 +++ templates/repo/issue/card.tmpl | 9 ++---- templates/repo/note.tmpl | 10 ++++++ 9 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 templates/projects/milestone_selection.tmpl diff --git a/models/project/note.go b/models/project/note.go index 084a17c7020a9..d6ee985e69cce 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -26,6 +26,7 @@ type ProjectBoardNote struct { Sorting int64 `xorm:"NOT NULL DEFAULT 0"` PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` LabelIDs []int64 `xorm:"-"` // can't be []*Label because of 'import cycle not allowed' + MilestoneID int64 `xorm:"INDEX"` ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` @@ -333,6 +334,7 @@ func UpdateProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardN fieldToUpdate = append(fieldToUpdate, "title") fieldToUpdate = append(fieldToUpdate, "content") + fieldToUpdate = append(fieldToUpdate, "milestone_id") _, err := db.GetEngine(ctx).ID(projectBoardNote.ID).Cols(fieldToUpdate...).Update(projectBoardNote) return err diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 36c5f74eb9d7e..7b8edc4849d73 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -167,6 +167,8 @@ func NewFuncMap() template.FuncMap { "RenderLabels": RenderLabels, "RenderLabelsFromIDs": RenderLabelsFromIDs, + "RenderMilestone": RenderMilestone, + // ----------------------------------------------------------------- // misc "ShortSha": base.ShortSha, diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 6c88a009cf6cd..e7208116f55f7 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -15,11 +15,13 @@ import ( "unicode" issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/util" ) @@ -244,3 +246,23 @@ func RenderLabelsFromIDs(ctx context.Context, labelIDs []int64, repoLink string) htmlCode += "</span>" return template.HTML(htmlCode) } + +func RenderMilestone(ctx context.Context, milestoneID int64, repoID int64, classes ...string) template.HTML { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + log.Error("GetRepositoryByID", err) + return "" + } + + milestone, err := issues_model.GetMilestoneByRepoID(ctx, repo.ID, milestoneID) + if err != nil { + log.Error("GetMilestoneByRepoID", err) + return "" + } + + htmlCode := fmt.Sprintf("<a class='milestone %s' href='%s/milestone/%d'>", strings.Join(classes, " "), repo.Link(), milestone.ID) + htmlCode += string(svg.RenderHTML("octicon-milestone", 16, "gt-mr-2 gt-vm")) + htmlCode += fmt.Sprintf("<span class'gt-vm'>%s</span>", milestone.Name) + htmlCode += "</a>" + return template.HTML(htmlCode) +} diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index fe302a0e4d455..b6ccd3dd39946 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -421,8 +421,27 @@ func ViewProject(ctx *context.Context) { ctx.ServerError("GetLabelsByRepoID", err) return } - ctx.Data["Labels"] = labels + + milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ + RepoID: ctx.Repo.Repository.ID, + }) + if err != nil { + ctx.ServerError("GetAllRepoMilestones", err) + return + } + + openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{} + for _, milestone := range milestones { + if milestone.IsClosed { + closedMilestones = append(closedMilestones, milestone) + } else { + openMilestones = append(openMilestones, milestone) + } + } + ctx.Data["OpenMilestones"] = openMilestones + ctx.Data["ClosedMilestones"] = closedMilestones + ctx.Data["IsProjectsPage"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["Project"] = project @@ -835,8 +854,9 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { } projectBoardNote := project_model.ProjectBoardNote{ - Title: form.Title, - Content: form.Content, + Title: form.Title, + Content: form.Content, + MilestoneID: form.MilestoneID, ProjectID: ctx.ParamsInt64(":id"), BoardID: ctx.ParamsInt64(":boardID"), @@ -895,6 +915,7 @@ func EditProjectBoardNote(ctx *context.Context) { projectBoardNote.Title = form.Title projectBoardNote.Content = form.Content + projectBoardNote.MilestoneID = form.MilestoneID err := projectBoardNote.LoadLabelIDs(ctx) if err != nil { diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 2f286f1a2116c..4db0306643870 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -535,8 +535,9 @@ type EditProjectBoardForm struct { // ProjectBoardNoteForm is a form for editing/creating a note to a board type ProjectBoardNoteForm struct { - Title string `binding:"Required;MaxSize(255)"` - Content string + Title string `binding:"Required;MaxSize(255)"` + Content string + MilestoneID int64 `form:"milestoneId"` } // _____ .__.__ __ diff --git a/templates/projects/milestone_selection.tmpl b/templates/projects/milestone_selection.tmpl new file mode 100644 index 0000000000000..be9314244e3bf --- /dev/null +++ b/templates/projects/milestone_selection.tmpl @@ -0,0 +1,36 @@ +<div class="ui fluid selection dropdown clearable"> + <input type="hidden" id="edit-project-column-note-milestone-{{.ID}}" name="milestoneId" {{if and .MilestoneID (gt .MilestoneID 0)}}value="{{.MilestoneID}}"{{end}}> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + {{svg "octicon-x" 14 "remove icon"}} + <div class="default text"></div> + <div class="menu"> + {{if and (not .OpenMilestones) (not .ClosedMilestones)}} + <div class="disabled item"> + {{ctx.Locale.Tr "repo.projects.note.no_items"}} + </div> + {{else}} + {{if .OpenMilestones}} + <div class="divider"></div> + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.open_milestone"}} + </div> + {{range .OpenMilestones}} + <div class="item" data-value="{{.ID}}"> + {{RenderMilestone ctx .ID $.Repository.ID}} + </div> + {{end}} + {{end}} + {{if .ClosedMilestones}} + {{if gt (len .OpenMilestones) 0}}<div class="divider"></div>{{end}} + <div class="header"> + {{ctx.Locale.Tr "repo.projects.note.closed_milestone"}} + </div> + {{range .ClosedMilestones}} + <div class="item" data-value="{{.ID}}"> + {{RenderMilestone ctx .ID $.Repository.ID}} + </div> + {{end}} + {{end}} + {{end}} + </div> +</div> \ No newline at end of file diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index f85cecc48bf41..a315f09618629 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -162,6 +162,11 @@ </div> </div> + <div class="field"> + <label for="new-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{template "projects/milestone_selection" $}} + </div> + <div class="field"> {{template "shared/combomarkdowneditor" (dict "MarkdownPreviewUrl" (print $.RepoLink "/markup") diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 14d08fc0ef9cf..beaff28d1d7d4 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -32,12 +32,9 @@ </span> </div> {{if .MilestoneID}} - <div class="meta gt-my-2"> - <a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}"> - {{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}} - <span class="gt-vm">{{.Milestone.Name}}</span> - </a> - </div> + <div class="meta gt-my-2"> + {{RenderMilestone ctx .MilestoneID .Repo.ID}} + </div> {{end}} {{if $.Page.LinkedPRs}} {{range index $.Page.LinkedPRs .ID}} diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index daf04f8a4bd8b..610b66242ea4a 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -108,6 +108,11 @@ </div> </div> + <div class="field"> + <label for="edit-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{template "projects/milestone_selection" (dict "." $.root "MilestoneID" .MilestoneID)}} + </div> + <div class="field"> {{template "shared/combomarkdowneditor" (dict "MarkdownPreviewUrl" (print $.root.RepoLink "/markup") @@ -151,6 +156,11 @@ {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} </span> </div> + {{if .MilestoneID}} + <div class="meta gt-my-2"> + {{RenderMilestone ctx .MilestoneID $.root.Repository.ID}} + </div> + {{end}} {{$tasks := .GetTasks}} {{if gt $tasks 0}} <div class="meta"> From 1f1c9971c9c8f243bda1445ca318ad41cdee24e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Thu, 8 Feb 2024 15:01:25 +0100 Subject: [PATCH 36/46] permissions: only admin and creator can edit notes --- models/project/note.go | 13 ------------- routers/web/repo/projects.go | 8 ++++++++ templates/repo/note.tmpl | 34 +++++++++++++++++++--------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index d6ee985e69cce..7154e143863c0 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -305,19 +305,6 @@ func GetPinnedProjectBoardNotes(ctx context.Context, projectID int64) (ProjectBo return projectBoardNoteList, nil } -// GetLastEventTimestamp returns the last user visible event timestamp, either the creation or the update. -func (projectBoardNote *ProjectBoardNote) GetLastEventTimestamp() timeutil.TimeStamp { - return max(projectBoardNote.CreatedUnix, projectBoardNote.UpdatedUnix) -} - -// GetLastEventLabel returns the localization label for the current note. -func (projectBoardNote *ProjectBoardNote) GetLastEventLabel() string { - if projectBoardNote.UpdatedUnix > projectBoardNote.CreatedUnix { - return "repo.projects.note.updated_by" - } - return "repo.projects.note.created_by" -} - // GetTasks returns the amount of tasks in the project-board-notes content func (projectBoardNote *ProjectBoardNote) GetTasks() int { return len(markdown.MarkdownTasksRegex.FindAllStringIndex(projectBoardNote.Content, -1)) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index b6ccd3dd39946..7b54383d1b970 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -807,6 +807,14 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode } return nil, nil } + + if !ctx.Doer.IsAdmin && ctx.Doer.ID != projectBoardNote.CreatorID { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only the creator or an admin can perform this action.", + }) + return nil, nil + } + if projectBoardNote.ProjectID != project.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 610b66242ea4a..31a97f1d445e7 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -17,17 +17,19 @@ {{ctx.Locale.Tr "repo.projects.note.view"}} </button> {{if $.CanWriteProjects}} - <button class="ui item show-modal button" - data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> - {{svg "octicon-pencil"}} - {{ctx.Locale.Tr "repo.projects.note.edit"}} - </button> + {{if eq $.root.Context.Doer.ID .CreatorID}} + <button class="ui item show-modal button" + data-modal="#edit-project-column-note-modal-{{.ID}}" + data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> + {{svg "octicon-pencil"}} + {{ctx.Locale.Tr "repo.projects.note.edit"}} + </button> + {{end}} <a class="ui item button create-project-issue-from-note" href="{{$.root.RepoLink}}/issues/new/choose?project={{$.root.Project.ID}}"> {{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.new"}} </a> - {{$notPinnedAndNotAllowedToPin := and (not .IsPinned) (not (.IsNewPinAllowed ctx))}} + {{$notPinnedAndNotAllowedToPin := and (not .IsPinned) (not (.IsNewPinAllowed ctx)) (or $.root.Context.Doer.IsAdmin (eq $.root.Context.Doer.ID .CreatorID))}} <button class="ui item button {{if $notPinnedAndNotAllowedToPin}}disabled{{end}} pin-project-column-note" {{if $notPinnedAndNotAllowedToPin}}style="pointer-events: unset !important"{{end}} data-method="{{if .IsPinned}}delete{{else}}post{{end}}" @@ -41,12 +43,14 @@ {{ctx.Locale.Tr "pin"}} {{end}} </button> - <button class="ui item show-modal button" - data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "repo.projects.note.delete"}} - </button> + {{if eq $.root.Context.Doer.ID .CreatorID}} + <button class="ui item show-modal button" + data-modal="#delete-project-column-note-modal-{{.ID}}" + data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "repo.projects.note.delete"}} + </button> + {{end}} {{end}} <div class="ui modal" id="view-project-column-note-modal-{{.ID}}"> @@ -152,8 +156,8 @@ </div> <div class="meta"> <span class="text light grey muted-links"> - {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} - {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} + {{$timeStr := TimeSinceUnix .CreatedUnix ctx.Locale}} + {{ctx.Locale.Tr "repo.projects.note.created_by" $timeStr (.Creator.HomeLink | Escape) (.Creator.GetDisplayName | Escape) | Safe}} </span> </div> {{if .MilestoneID}} From 67e5a866202dc9b6dfe8c55d42ba484606b7c260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Thu, 8 Feb 2024 22:06:23 +0100 Subject: [PATCH 37/46] fix: cursor-grap only on columns and cards if column-id != 0 and use can edit --- templates/projects/view.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index a315f09618629..61966434cbe93 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -81,7 +81,7 @@ <div id="project-board"> <div class="board {{if .CanWriteProjects}}sortable{{end}}"> {{range .Columns}} - <div class="ui segment project-column {{if $canWriteProject}}gt-cursor-grab{{end}}" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> + <div class="ui segment project-column {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> <div class="ui small circular grey label project-column-issue-note-count"> @@ -240,7 +240,7 @@ <div class="cards-wrapper"> {{if or $canWriteProject (index $.ProjectBoardNotesMap .ID)}} - <div class="ui cards note-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> + <div class="ui cards note-cards {{if $canWriteProject}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> {{range (index $.ProjectBoardNotesMap .ID)}} <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict @@ -256,7 +256,7 @@ <div class="divider"></div> {{end}} {{end}} - <div class="ui cards issue-cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> + <div class="ui cards issue-cards {{if $canWriteProject}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="issue-board-{{.ID}}"> {{range (index $.IssuesMap .ID)}} <div class="issue-card" data-issue="{{.ID}}"> {{template "repo/issue/card" (dict "Issue" . "Page" $)}} From 1c50683c5ed90bfbef3af4dd3b7ff3d16fd00d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Thu, 8 Feb 2024 22:17:31 +0100 Subject: [PATCH 38/46] fix: moving pinned cards need permissions --- templates/projects/view.tmpl | 2 +- web_src/js/features/repo-projects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 61966434cbe93..72f8d1cc31c90 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,7 +1,7 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} {{if gt (len .PinnedProjectBoardNotes) 0}} -<div id="pinned-notes"> +<div id="pinned-notes" {{if .CanWriteProjects}}class="sortable"{{end}}> {{range .PinnedProjectBoardNotes}} <div class="note-card pinned-card {{if $canWriteProject}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-note="{{.ID}}"> {{template "repo/note" (dict diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 402549b7c9fca..5770da55d6fff 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -96,7 +96,7 @@ function movePinned({newIndex, item}) { } async function initRepoProjectSortable() { - const pinnedNotesCards = document.querySelector('#pinned-notes'); + const pinnedNotesCards = document.querySelector('#pinned-notes.sortable'); if (pinnedNotesCards) { createSortable(pinnedNotesCards, { group: 'pinned-shared', From f1387ca0ad205f5e607cb74eff51a53f0e52bebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 00:38:10 +0100 Subject: [PATCH 39/46] enhance: show pinned note in list --- models/project/note.go | 32 ++++++++++++++-- models/project/project.go | 3 +- routers/web/repo/projects.go | 23 +++++++++++ templates/projects/list.tmpl | 65 +++++++++++++++++++------------- templates/repo/note.tmpl | 10 ++--- web_src/css/shared/milestone.css | 10 +++++ 6 files changed, 107 insertions(+), 36 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index 7154e143863c0..82b853c6f2c8a 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -41,9 +41,9 @@ type ProjectBoardNoteList []*ProjectBoardNote // ProjectBoardNotesOptions represents options of an note. type ProjectBoardNotesOptions struct { - db.Paginator ProjectID int64 BoardID int64 + IsPinned util.OptionalBool } func init() { @@ -51,7 +51,7 @@ func init() { db.RegisterModel(new(ProjectBoardNoteLabel)) } -// GetProjectBoardNoteByID load notes assigned to the boards +// GetProjectBoardNoteByID load note by ID func GetProjectBoardNoteByID(ctx context.Context, projectBoardNoteID int64) (*ProjectBoardNote, error) { projectBoardNote := new(ProjectBoardNote) @@ -80,6 +80,20 @@ func GetProjectBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) return projectBoardNoteList, nil } +// GetProjectBoardNotesByProjectID load pinned notes assigned to the project +func GetProjectBoardNotesByProjectID(ctx context.Context, projectID int64, isPinned bool) (ProjectBoardNoteList, error) { + projectBoardNoteList, err := ProjectBoardNotes(ctx, &ProjectBoardNotesOptions{ + ProjectID: projectID, + BoardID: -1, + IsPinned: util.OptionalBoolOf(isPinned), + }) + if err != nil { + return nil, err + } + + return projectBoardNoteList, nil +} + // LoadProjectBoardNotesFromBoardList load notes assigned to the boards func (p *Project) LoadProjectBoardNotesFromBoardList(ctx context.Context, boardList BoardList) (map[int64]ProjectBoardNoteList, error) { projectBoardNoteListMap := make(map[int64]ProjectBoardNoteList, len(boardList)) @@ -110,7 +124,19 @@ func LoadProjectBoardNotesFromBoard(ctx context.Context, board *Board) (ProjectB func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (ProjectBoardNoteList, error) { sess := db.GetEngine(ctx) - sess.Where(builder.Eq{"board_id": opts.BoardID}).And(builder.Eq{"project_id": opts.ProjectID}) + if opts.BoardID >= 0 { + sess.Where(builder.Eq{"board_id": opts.BoardID}) + } + if opts.ProjectID >= 0 { + sess.Where(builder.Eq{"project_id": opts.ProjectID}) + } + if !opts.IsPinned.IsNone() { + if opts.IsPinned.IsTrue() { + sess.Where(builder.NotNull{"pin_order"}).And(builder.Gt{"pin_order": 0}) + } else { + sess.Where(builder.IsNull{"pin_order"}).Or(builder.Eq{"pin_order": 0}) + } + } projectBoardNoteList := ProjectBoardNoteList{} if err := sess.Asc("sorting").Desc("updated_unix").Desc("id").Find(&projectBoardNoteList); err != nil { diff --git a/models/project/project.go b/models/project/project.go index 38621f449472a..d613e3c297e78 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -138,7 +138,8 @@ type Project struct { CardType CardType Type Type - RenderedContent string `xorm:"-"` + RenderedContent string `xorm:"-"` + FirstPinnedProjectBoardNote *ProjectBoardNote `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 7b54383d1b970..e196aca9769ca 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -102,6 +102,29 @@ func Projects(ctx *context.Context) { ctx.ServerError("RenderString", err) return } + + pinnedProjectBoardNotes, err := project_model.GetProjectBoardNotesByProjectID(ctx, projects[i].ID, true) + if err != nil { + ctx.ServerError("GetProjectBoardNotesByProjectID", err) + return + } + if len(pinnedProjectBoardNotes) > 0 { + firstPinnedProjectBoardNote := pinnedProjectBoardNotes[0] + + firstPinnedProjectBoardNote.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, firstPinnedProjectBoardNote.Content) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + projects[i].FirstPinnedProjectBoardNote = firstPinnedProjectBoardNote + } } ctx.Data["Projects"] = projects diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index cbff82dd702f3..e694c76116921 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -45,39 +45,50 @@ <div class="milestone-list"> {{range .Projects}} - <li class="milestone-card"> - <h3 class="flex-text-block gt-m-0"> - {{svg .IconName 16}} - <a class="muted" href="{{.Link ctx}}">{{.Title}}</a> - </h3> - <div class="milestone-toolbar"> - <div class="group"> - <div class="flex-text-block"> - {{svg "octicon-issue-opened" 14}} - {{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}} + <li class="milestone-card {{if .FirstPinnedProjectBoardNote}}with-pinned-note{{end}}"> + {{if .FirstPinnedProjectBoardNote}} + <div class="note-card" data-note="{{.ID}}"> + {{template "repo/note" (dict + "BoardNote" .FirstPinnedProjectBoardNote + "root" $ + "CanWriteProjects" false) + }} + </div> + {{end}} + <div> + <h3 class="flex-text-block gt-m-0"> + {{svg .IconName 16}} + <a class="muted" href="{{.Link ctx}}">{{.Title}}</a> + </h3> + <div class="milestone-toolbar"> + <div class="group"> + <div class="flex-text-block"> + {{svg "octicon-issue-opened" 14}} + {{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}} + </div> + <div class="flex-text-block"> + {{svg "octicon-check" 14}} + {{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}} + </div> </div> - <div class="flex-text-block"> - {{svg "octicon-check" 14}} - {{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}} + {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} + <div class="group"> + <a class="flex-text-inline" href="{{.Link ctx}}/edit">{{svg "octicon-pencil" 14}}{{ctx.Locale.Tr "repo.issues.label_edit"}}</a> + {{if .IsClosed}} + <a class="link-action flex-text-inline" href data-url="{{.Link ctx}}/open">{{svg "octicon-check" 14}}{{ctx.Locale.Tr "repo.projects.open"}}</a> + {{else}} + <a class="link-action flex-text-inline" href data-url="{{.Link ctx}}/close">{{svg "octicon-skip" 14}}{{ctx.Locale.Tr "repo.projects.close"}}</a> + {{end}} + <a class="delete-button flex-text-inline" href="#" data-url="{{.Link ctx}}/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.issues.label_delete"}}</a> </div> - </div> - {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} - <div class="group"> - <a class="flex-text-inline" href="{{.Link ctx}}/edit">{{svg "octicon-pencil" 14}}{{ctx.Locale.Tr "repo.issues.label_edit"}}</a> - {{if .IsClosed}} - <a class="link-action flex-text-inline" href data-url="{{.Link ctx}}/open">{{svg "octicon-check" 14}}{{ctx.Locale.Tr "repo.projects.open"}}</a> - {{else}} - <a class="link-action flex-text-inline" href data-url="{{.Link ctx}}/close">{{svg "octicon-skip" 14}}{{ctx.Locale.Tr "repo.projects.close"}}</a> {{end}} - <a class="delete-button flex-text-inline" href="#" data-url="{{.Link ctx}}/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.issues.label_delete"}}</a> + </div> + {{if .Description}} + <div class="content"> + {{.RenderedContent|Str2html}} </div> {{end}} </div> - {{if .Description}} - <div class="content"> - {{.RenderedContent|Str2html}} - </div> - {{end}} </li> {{end}} diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 31a97f1d445e7..11870cbab7851 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -20,7 +20,7 @@ {{if eq $.root.Context.Doer.ID .CreatorID}} <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> @@ -33,7 +33,7 @@ <button class="ui item button {{if $notPinnedAndNotAllowedToPin}}disabled{{end}} pin-project-column-note" {{if $notPinnedAndNotAllowedToPin}}style="pointer-events: unset !important"{{end}} data-method="{{if .IsPinned}}delete{{else}}post{{end}}" - data-url="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}/pin" + data-url="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}/pin" {{if $notPinnedAndNotAllowedToPin}}data-tooltip-content="{{ctx.Locale.Tr "repo.projects.note.max_pinned"}}"{{end}}> {{if .IsPinned}} {{svg "octicon-pin-slash"}} @@ -46,7 +46,7 @@ {{if eq $.root.Context.Doer.ID .CreatorID}} <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.Link}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} </button> @@ -66,7 +66,7 @@ <div class="field"> <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> {{if .LabelIDs}} - {{RenderLabelsFromIDs ctx .LabelIDs $.root.Link}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.RepoLink}} {{else}} {{ctx.Locale.Tr "repo.projects.note.no_label"}} {{end}} @@ -173,7 +173,7 @@ </div> {{end}} {{if .LabelIDs}} - {{RenderLabelsFromIDs ctx .LabelIDs $.root.Link}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.RepoLink}} {{end}} </div> {{end}} diff --git a/web_src/css/shared/milestone.css b/web_src/css/shared/milestone.css index 91e6b5e387c4f..ee7d3ab31cf22 100644 --- a/web_src/css/shared/milestone.css +++ b/web_src/css/shared/milestone.css @@ -2,12 +2,22 @@ list-style: none; } +.milestone-list .note-card { + height: fit-content; +} + .milestone-card { width: 100%; padding-top: 10px; padding-bottom: 10px; } +.milestone-card.with-pinned-note { + gap: 1rem; + display: grid; + grid-template-columns: minmax(30%, min-content) 1fr; +} + .milestone-card + .milestone-card { border-top: 1px solid var(--color-secondary); } From 0aa0c5cbac67e91ab6bd6ffdc7840189c8432bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 00:51:21 +0100 Subject: [PATCH 40/46] fix: show milestone in view-modal of note --- templates/repo/note.tmpl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 11870cbab7851..8b73fa29c5730 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -72,6 +72,15 @@ {{end}} </div> + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{if .MilestoneID}} + {{RenderMilestone ctx .MilestoneID $.root.Repository.ID}} + {{else}} + {{ctx.Locale.Tr "repo.projects.note.no_milestone"}} + {{end}} + </div> + <div class="field"> <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.description"}}</label> {{Str2html .RenderedContent}} From 50e8545c9024cb0ddcde6b448e8b60a218e4c70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 02:55:00 +0100 Subject: [PATCH 41/46] enhance: notes are now available in orgs --- models/project/note.go | 15 +- routers/web/org/projects.go | 311 +++++++++++++++++++++++++++++++++++ routers/web/repo/projects.go | 48 +----- routers/web/web.go | 16 ++ templates/projects/view.tmpl | 40 +++-- templates/repo/note.tmpl | 108 ++++++------ 6 files changed, 418 insertions(+), 120 deletions(-) diff --git a/models/project/note.go b/models/project/note.go index 82b853c6f2c8a..01ec0c03633e5 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -19,14 +19,13 @@ import ( // ProjectBoardNote is used to represent a note on a boards type ProjectBoardNote struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"TEXT NOT NULL"` - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` - PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` - LabelIDs []int64 `xorm:"-"` // can't be []*Label because of 'import cycle not allowed' - MilestoneID int64 `xorm:"INDEX"` + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` + LabelIDs []int64 `xorm:"-"` // can't be []*Label because of 'import cycle not allowed' + MilestoneID int64 `xorm:"INDEX"` ProjectID int64 `xorm:"INDEX NOT NULL"` BoardID int64 `xorm:"INDEX NOT NULL"` diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 03798a712c2ee..02f9a4c958cea 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -105,6 +105,15 @@ func Projects(ctx *context.Context) { for _, project := range projects { project.RenderedContent = project.Description + + pinnedProjectBoardNotes, err := project_model.GetProjectBoardNotesByProjectID(ctx, project.ID, true) + if err != nil { + ctx.ServerError("GetProjectBoardNotesByProjectID", err) + return + } + if len(pinnedProjectBoardNotes) > 0 { + project.FirstPinnedProjectBoardNote = pinnedProjectBoardNotes[0] + } } err = shared_user.LoadHeaderCount(ctx) @@ -362,6 +371,12 @@ func ViewProject(ctx *context.Context) { return } + notesMap, err := project.LoadProjectBoardNotesFromBoardList(ctx, boards) + if err != nil { + ctx.ServerError("LoadProjectBoardNotesOfBoards", err) + return + } + if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) for _, issuesList := range issuesMap { @@ -395,6 +410,12 @@ func ViewProject(ctx *context.Context) { } } + pinnedBoardNotes, err := project_model.GetPinnedProjectBoardNotes(ctx, project.ID) + if err != nil { + ctx.ServerError("GetPinnedProjectBoardNotes", err) + return + } + project.RenderedContent = project.Description ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true @@ -402,6 +423,8 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend + ctx.Data["PinnedProjectBoardNotes"] = pinnedBoardNotes + ctx.Data["ProjectBoardNotesMap"] = notesMap shared_user.RenderUserHeader(ctx) err = shared_user.LoadHeaderCount(ctx) @@ -749,3 +772,291 @@ func MoveIssues(ctx *context.Context) { ctx.JSONOK() } + +func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.ProjectBoardNote) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return nil, nil + } + + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound("ProjectNotFound", err) + } else { + ctx.ServerError("GetProjectByID", err) + } + return nil, nil + } + if project.OwnerID != ctx.ContextUser.ID { + ctx.NotFound("InvalidRepoID", nil) + return nil, nil + } + + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + if project_model.IsErrProjectBoardNoteNotExist(err) { + ctx.NotFound("ProjectBoardNoteNotFound", err) + } else { + ctx.ServerError("GetProjectBoardNoteById", err) + } + return nil, nil + } + + if !ctx.Doer.IsAdmin && ctx.Doer.ID != projectBoardNote.CreatorID { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only the creator or an admin can perform this action.", + }) + return nil, nil + } + + if projectBoardNote.ProjectID != project.ID { + ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ + "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), + }) + return nil, nil + } + + return project, projectBoardNote +} + +func AddProjectBoardNoteToBoard(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) + + // LabelIDs is send without parentheses - maybe because of multipart/form-data + labelIdsString := "[" + ctx.Req.FormValue("labelIds") + "]" + var labelIDs []int64 + if err := json.Unmarshal([]byte(labelIdsString), &labelIDs); err != nil { + ctx.ServerError("Unmarshal", err) + } + + // check that all LabelsIDs are valid + for _, labelID := range labelIDs { + _, err := issues_model.GetLabelByID(ctx, labelID) + if err != nil { + if issues_model.IsErrLabelNotExist(err) { + ctx.Error(http.StatusNotFound, "GetLabelByID") + } else { + ctx.ServerError("GetLabelByID", err) + } + return + } + } + + projectBoardNote := project_model.ProjectBoardNote{ + Title: form.Title, + Content: form.Content, + + ProjectID: ctx.ParamsInt64(":id"), + BoardID: ctx.ParamsInt64(":boardID"), + CreatorID: ctx.Doer.ID, + } + err := project_model.NewProjectBoardNote(ctx, &projectBoardNote) + if err != nil { + ctx.ServerError("NewProjectBoardNote", err) + return + } + + if len(labelIDs) > 0 { + for _, labelID := range labelIDs { + err := projectBoardNote.AddLabel(ctx, labelID) + if err != nil { + ctx.ServerError("AddLabel", err) + return + } + } + } + + ctx.JSONOK() +} + +func EditProjectBoardNote(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) + _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) + if ctx.Written() { + return + } + + projectBoardNote.Title = form.Title + projectBoardNote.Content = form.Content + + if err := project_model.UpdateProjectBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("UpdateProjectBoardNote", err) + return + } + + ctx.JSONOK() +} + +func DeleteProjectBoardNote(ctx *context.Context) { + _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) + if ctx.Written() { + return + } + + if err := project_model.DeleteProjectBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("DeleteProjectBoardNote", err) + return + } + + ctx.JSONOK() +} + +func MoveProjectBoardNote(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, map[string]string{ + "message": "Only signed in users are allowed to perform this action.", + }) + return + } + + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound("ProjectNotFound", err) + } else { + ctx.ServerError("GetProjectByID", err) + } + return + } + + var board *project_model.Board + + if ctx.ParamsInt64(":boardID") == 0 { + board = &project_model.Board{ + ID: 0, + ProjectID: project.ID, + Title: ctx.Tr("repo.projects.type.uncategorized"), + } + } else { + board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) + if err != nil { + if project_model.IsErrProjectBoardNotExist(err) { + ctx.NotFound("ProjectBoardNotExist", nil) + } else { + ctx.ServerError("GetProjectBoard", err) + } + return + } + if board.ProjectID != project.ID { + ctx.NotFound("BoardNotInProject", nil) + return + } + } + + type MovedProjectBoardNotesForm struct { + ProjectBoardNotes []struct { + ProjectBoardNoteID int64 `json:"projectBoardNoteID"` + Sorting int64 `json:"sorting"` + } `json:"projectBoardNotes"` + } + + form := &MovedProjectBoardNotesForm{} + if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("DecodeMovedProjectBoardNotesForm", err) + return + } + + projectBoardNoteIDs := make([]int64, 0, len(form.ProjectBoardNotes)) + sortedProjectBoardNoteIDs := make(map[int64]int64) + for _, boardNote := range form.ProjectBoardNotes { + projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.ProjectBoardNoteID) + sortedProjectBoardNoteIDs[boardNote.Sorting] = boardNote.ProjectBoardNoteID + } + movedProjectBoardNotes, err := project_model.GetProjectBoardNotesByIds(ctx, projectBoardNoteIDs) + if err != nil { + if project_model.IsErrProjectBoardNoteNotExist(err) { + ctx.NotFound("ProjectBoardNoteNotExisting", nil) + } else { + ctx.ServerError("GetProjectBoardNoteByIds", err) + } + return + } + + if len(movedProjectBoardNotes) != len(form.ProjectBoardNotes) { + ctx.ServerError("some project-board-notes do not exist", errors.New("some project-board-notes do not exist")) + return + } + + if err = project_model.MoveProjectBoardNoteOnProjectBoard(ctx, board, sortedProjectBoardNoteIDs); err != nil { + ctx.ServerError("MoveProjectBoardNoteOnProjectBoard", err) + return + } + + ctx.JSONOK() +} + +// PinProjectBoardNote pins the BoardNote +func PinProjectBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetProjectBoardNoteByID", err) + return + } + + err = projectBoardNote.Pin(ctx) + if err != nil { + ctx.ServerError("PinProjectBoardNote", err) + return + } + + ctx.JSONOK() +} + +// PinBoardNote unpins the BoardNote +func UnPinProjectBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetBoardNoteByID", err) + return + } + + err = projectBoardNote.Unpin(ctx) + if err != nil { + ctx.ServerError("UnpinProjectBoardNote", err) + return + } + + ctx.JSONOK() +} + +// PinBoardNote moves a pined the BoardNote +func PinMoveProjectBoardNote(ctx *context.Context) { + if ctx.Doer == nil { + ctx.JSON(http.StatusForbidden, "Only signed in users are allowed to perform this action.") + return + } + + type MovePinProjectBoardNoteForm struct { + Position int64 `json:"position"` + } + + form := &MovePinProjectBoardNoteForm{} + if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { + ctx.ServerError("Decode MovePinProjectBoardNoteForm", err) + return + } + + projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + if err != nil { + ctx.ServerError("GetProjectBoardNoteByID", err) + return + } + + err = projectBoardNote.MovePin(ctx, form.Position) + if err != nil { + ctx.ServerError("MovePinProjectBoardNote", err) + return + } + + ctx.JSONOK() +} diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index e196aca9769ca..25e8dc93251bc 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -109,21 +109,7 @@ func Projects(ctx *context.Context) { return } if len(pinnedProjectBoardNotes) > 0 { - firstPinnedProjectBoardNote := pinnedProjectBoardNotes[0] - - firstPinnedProjectBoardNote.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, firstPinnedProjectBoardNote.Content) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - projects[i].FirstPinnedProjectBoardNote = firstPinnedProjectBoardNote + projects[i].FirstPinnedProjectBoardNote = pinnedProjectBoardNotes[0] } } @@ -354,23 +340,6 @@ func ViewProject(ctx *context.Context) { return } - for _, noteList := range notesMap { - for _, note := range noteList { - note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, note.Content) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - } - } - if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) for _, issuesList := range issuesMap { @@ -424,21 +393,6 @@ func ViewProject(ctx *context.Context) { return } - for _, note := range pinnedBoardNotes { - note.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, note.Content) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - } - labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) diff --git a/routers/web/web.go b/routers/web/web.go index f16bab10069cf..59dbd597971d5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1003,6 +1003,22 @@ func registerRoutes(m *web.Route) { m.Post("/unsetdefault", org.UnsetDefaultProjectBoard) m.Post("/move", org.MoveIssues) + + m.Group("/note", func() { + m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), org.AddProjectBoardNoteToBoard) + m.Post("/move", org.MoveProjectBoardNote) + + m.Group("/{noteID}", func() { + m.Put("", web.Bind(forms.ProjectBoardNoteForm{}), org.EditProjectBoardNote) + m.Delete("", org.DeleteProjectBoardNote) + + m.Group("/pin", func() { + m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), org.PinProjectBoardNote) + m.Delete("", org.UnPinProjectBoardNote) + m.Post("/move", org.PinMoveProjectBoardNote) + }) + }) + }) }) }) }, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), func(ctx *context.Context) { diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 72f8d1cc31c90..0e29fa2d82da8 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -145,27 +145,31 @@ <input id="new-project-column-note-title-{{.ID}}" name="title" maxlength="255" autofocus required> </div> - <div class="field"> - <label for="new-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> - <div class="ui fluid selection dropdown multiple clearable"> - <input type="hidden" id="new-project-column-note-label-{{.ID}}" name="labelIds"> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="default text"></div> - <div class="menu"> - {{range $.Labels}} - <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> - <small class="desc">{{.Description | RenderEmoji ctx}}</small> - <span>{{RenderLabel ctx .}}</span> - </div> - {{end}} + {{if and $.Labels (gt (len $.Labels) 0)}} + <div class="field"> + <label for="new-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + <div class="ui fluid selection dropdown multiple clearable"> + <input type="hidden" id="new-project-column-note-label-{{.ID}}" name="labelIds"> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="default text"></div> + <div class="menu"> + {{range $.Labels}} + <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> + <small class="desc">{{.Description | RenderEmoji ctx}}</small> + <span>{{RenderLabel ctx .}}</span> + </div> + {{end}} + </div> </div> </div> - </div> + {{end}} - <div class="field"> - <label for="new-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> - {{template "projects/milestone_selection" $}} - </div> + {{if or $.OpenMilestones $.ClosedMilestones}} + <div class="field"> + <label for="new-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{template "projects/milestone_selection" $}} + </div> + {{end}} <div class="field"> {{template "shared/combomarkdowneditor" (dict diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 8b73fa29c5730..4c73273e0c524 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -1,11 +1,15 @@ {{with .BoardNote}} +{{$backendURL := (print $.root.FeedURL "/projects/" .ProjectID "/" .BoardID "/note/")}} +{{if not $.root.Repository}} + {{$backendURL = (print $.root.FeedURL "/-/projects/" .ProjectID "/" .BoardID "/note/")}} +{{end}} <div class="content gt-p-0 gt-w-full"> <div class="gt-df gt-items-start"> <div class="project-column-note-card-icon"> {{svg "octicon-note"}} </div> <span class="note-card-title muted project-column-note-title">{{.Title}}</span> - {{if or $.CanWriteProjects (gt (len .RenderedContent) 0)}} + {{if or $.CanWriteProjects (gt (len .Content) 0)}} <div class="ui dropdown jump item"> <div class="gt-px-3"> {{svg "octicon-kebab-horizontal"}} @@ -20,20 +24,22 @@ {{if eq $.root.Context.Doer.ID .CreatorID}} <button class="ui item show-modal button" data-modal="#edit-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$backendURL}}{{.ID}}"> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.note.edit"}} </button> {{end}} - <a class="ui item button create-project-issue-from-note" href="{{$.root.RepoLink}}/issues/new/choose?project={{$.root.Project.ID}}"> - {{svg "octicon-issue-opened"}} - {{ctx.Locale.Tr "repo.issues.new"}} - </a> + {{if $.root.Repository}} + <a class="ui item button create-project-issue-from-note" href="{{$.root.FeedURL}}/issues/new/choose?project={{$.root.Project.ID}}"> + {{svg "octicon-issue-opened"}} + {{ctx.Locale.Tr "repo.issues.new"}} + </a> + {{end}} {{$notPinnedAndNotAllowedToPin := and (not .IsPinned) (not (.IsNewPinAllowed ctx)) (or $.root.Context.Doer.IsAdmin (eq $.root.Context.Doer.ID .CreatorID))}} <button class="ui item button {{if $notPinnedAndNotAllowedToPin}}disabled{{end}} pin-project-column-note" {{if $notPinnedAndNotAllowedToPin}}style="pointer-events: unset !important"{{end}} data-method="{{if .IsPinned}}delete{{else}}post{{end}}" - data-url="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}/pin" + data-url="{{$backendURL}}{{.ID}}/pin" {{if $notPinnedAndNotAllowedToPin}}data-tooltip-content="{{ctx.Locale.Tr "repo.projects.note.max_pinned"}}"{{end}}> {{if .IsPinned}} {{svg "octicon-pin-slash"}} @@ -46,7 +52,7 @@ {{if eq $.root.Context.Doer.ID .CreatorID}} <button class="ui item show-modal button" data-modal="#delete-project-column-note-modal-{{.ID}}" - data-modal-form.action="{{$.root.RepoLink}}/projects/{{.ProjectID}}/{{.BoardID}}/note/{{.ID}}"> + data-modal-form.action="{{$backendURL}}{{.ID}}"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.note.delete"}} </button> @@ -63,27 +69,31 @@ {{.Title}} </div> - <div class="field"> - <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> - {{if .LabelIDs}} - {{RenderLabelsFromIDs ctx .LabelIDs $.root.RepoLink}} - {{else}} - {{ctx.Locale.Tr "repo.projects.note.no_label"}} - {{end}} - </div> + {{if and $.root.Labels (gt (len $.root.Labels) 0)}} + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + {{if .LabelIDs}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.FeedURL}} + {{else}} + {{ctx.Locale.Tr "repo.projects.note.no_label"}} + {{end}} + </div> + {{end}} - <div class="field"> - <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> - {{if .MilestoneID}} - {{RenderMilestone ctx .MilestoneID $.root.Repository.ID}} - {{else}} - {{ctx.Locale.Tr "repo.projects.note.no_milestone"}} - {{end}} - </div> + {{if or $.root.OpenMilestones $.root.ClosedMilestones}} + <div class="field"> + <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{if .MilestoneID}} + {{RenderMilestone ctx .MilestoneID $.root.Repository.ID}} + {{else}} + {{ctx.Locale.Tr "repo.projects.note.no_milestone"}} + {{end}} + </div> + {{end}} <div class="field"> <label class="gt-font-16">{{ctx.Locale.Tr "repo.projects.note.description"}}</label> - {{Str2html .RenderedContent}} + {{RenderMarkdownToHtml ctx .Content}} </div> </div> <div class="actions"> @@ -104,32 +114,36 @@ <input id="edit-project-column-note-title-{{.ID}}" name="title" value="{{.Title}}" maxlength="255" autofocus required> </div> - <div class="field"> - <label for="edit-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> - <div class="ui fluid selection dropdown multiple clearable"> - <input type="hidden" id="edit-project-column-note-label-{{.ID}}" name="labelIds" value="{{range $index, $val := .LabelIDs}}{{if ne $index 0}},{{end}}{{$val}}{{end}}"> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="default text"></div> - <div class="menu"> - {{range $.root.Labels}} - <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> - <small class="desc">{{.Description | RenderEmoji ctx}}</small> - <span>{{RenderLabel ctx .}}</span> - </div> - {{end}} + {{if and $.root.Labels (gt (len $.root.Labels) 0)}} + <div class="field"> + <label for="edit-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> + <div class="ui fluid selection dropdown multiple clearable"> + <input type="hidden" id="edit-project-column-note-label-{{.ID}}" name="labelIds" value="{{range $index, $val := .LabelIDs}}{{if ne $index 0}},{{end}}{{$val}}{{end}}"> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="default text"></div> + <div class="menu"> + {{range $.root.Labels}} + <div class="item vertical gt-gap-y-2" data-value="{{.ID}}"> + <small class="desc">{{.Description | RenderEmoji ctx}}</small> + <span>{{RenderLabel ctx .}}</span> + </div> + {{end}} + </div> </div> </div> - </div> + {{end}} - <div class="field"> - <label for="edit-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> - {{template "projects/milestone_selection" (dict "." $.root "MilestoneID" .MilestoneID)}} - </div> + {{if or $.root.OpenMilestones $.root.ClosedMilestones}} + <div class="field"> + <label for="edit-project-column-note-milestone-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.milestone"}}</label> + {{template "projects/milestone_selection" (dict "." $.root "MilestoneID" .MilestoneID)}} + </div> + {{end}} <div class="field"> {{template "shared/combomarkdowneditor" (dict - "MarkdownPreviewUrl" (print $.root.RepoLink "/markup") - "MarkdownPreviewContext" $.root.RepoLink + "MarkdownPreviewUrl" (print $.root.FeedURL "/markup") + "MarkdownPreviewContext" $.root.FeedURL "TextareaName" "content" "TextareaContent" .Content "TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.note.description") @@ -161,7 +175,7 @@ {{end}} </div> <div class="project-column-note-content"> - {{Str2html .RenderedContent}} + {{RenderMarkdownToHtml ctx .Content}} </div> <div class="meta"> <span class="text light grey muted-links"> @@ -182,7 +196,7 @@ </div> {{end}} {{if .LabelIDs}} - {{RenderLabelsFromIDs ctx .LabelIDs $.root.RepoLink}} + {{RenderLabelsFromIDs ctx .LabelIDs $.root.FeedURL}} {{end}} </div> {{end}} From 2f4631acf5649120c208302e9fc4d52f23dd94e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 03:31:17 +0100 Subject: [PATCH 42/46] enhance: milestone and label selectors with search --- templates/projects/milestone_selection.tmpl | 2 +- templates/projects/view.tmpl | 2 +- templates/repo/note.tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/projects/milestone_selection.tmpl b/templates/projects/milestone_selection.tmpl index be9314244e3bf..85b06226d32a4 100644 --- a/templates/projects/milestone_selection.tmpl +++ b/templates/projects/milestone_selection.tmpl @@ -1,4 +1,4 @@ -<div class="ui fluid selection dropdown clearable"> +<div class="ui fluid selection search normal dropdown clearable"> <input type="hidden" id="edit-project-column-note-milestone-{{.ID}}" name="milestoneId" {{if and .MilestoneID (gt .MilestoneID 0)}}value="{{.MilestoneID}}"{{end}}> {{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-x" 14 "remove icon"}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 0e29fa2d82da8..d77559cc0e3a3 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -148,7 +148,7 @@ {{if and $.Labels (gt (len $.Labels) 0)}} <div class="field"> <label for="new-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> - <div class="ui fluid selection dropdown multiple clearable"> + <div class="ui fluid selection search normal dropdown multiple clearable"> <input type="hidden" id="new-project-column-note-label-{{.ID}}" name="labelIds"> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="default text"></div> diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 4c73273e0c524..1c183a1f21b1d 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -117,7 +117,7 @@ {{if and $.root.Labels (gt (len $.root.Labels) 0)}} <div class="field"> <label for="edit-project-column-note-label-{{.ID}}">{{ctx.Locale.Tr "repo.projects.note.labels"}}</label> - <div class="ui fluid selection dropdown multiple clearable"> + <div class="ui fluid selection search normal dropdown multiple clearable"> <input type="hidden" id="edit-project-column-note-label-{{.ID}}" name="labelIds" value="{{range $index, $val := .LabelIDs}}{{if ne $index 0}},{{end}}{{$val}}{{end}}"> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="default text"></div> From 6222c42dd9e80802b25bb39aecfdafce9b959484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 03:32:30 +0100 Subject: [PATCH 43/46] chore: add migration --- models/migrations/v1_22/v287.go | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 models/migrations/v1_22/v287.go diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go new file mode 100644 index 0000000000000..53f58025aaa49 --- /dev/null +++ b/models/migrations/v1_22/v287.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateTablesForProjectBoardNotes(x *xorm.Engine) error { + type ProjectBoardNote struct { + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` + MilestoneID int64 `xorm:"INDEX"` + + ProjectID int64 `xorm:"INDEX NOT NULL"` + BoardID int64 `xorm:"INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + type ProjectBoardNoteLabel struct { + ID int64 `xorm:"pk autoincr"` + ProjectBoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` + LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` + } + + err := x.Sync(new(ProjectBoardNote)) + if err != nil { + return err + } + + return x.Sync(new(ProjectBoardNoteLabel)) +} From 98cb5c036a501eac9a2aa9f0c090d593d99d791e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 03:41:54 +0100 Subject: [PATCH 44/46] i18n: translations for milestone --- options/locale/locale_en-US.ini | 5 +++++ templates/projects/milestone_selection.tmpl | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 073d7a1337e18..201da4f7d875c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1357,6 +1357,11 @@ projects.note.new = New Note projects.note.title = Title projects.note.labels = Labels projects.note.no_label = No Label +projects.note.milestone = Milestone +projects.note.no_milestone = No Milestone +projects.note.no_milestones = No Milestones +projects.note.open_milestones = Open Milestones +projects.note.closed_milestones = Closed Milestones projects.note.description = Description projects.note.edit = Edit Note projects.note.delete = Delete Note diff --git a/templates/projects/milestone_selection.tmpl b/templates/projects/milestone_selection.tmpl index 85b06226d32a4..86639e3ab1e17 100644 --- a/templates/projects/milestone_selection.tmpl +++ b/templates/projects/milestone_selection.tmpl @@ -6,13 +6,13 @@ <div class="menu"> {{if and (not .OpenMilestones) (not .ClosedMilestones)}} <div class="disabled item"> - {{ctx.Locale.Tr "repo.projects.note.no_items"}} + {{ctx.Locale.Tr "repo.projects.note.no_milestones"}} </div> {{else}} {{if .OpenMilestones}} <div class="divider"></div> <div class="header"> - {{ctx.Locale.Tr "repo.projects.note.open_milestone"}} + {{ctx.Locale.Tr "repo.projects.note.open_milestones"}} </div> {{range .OpenMilestones}} <div class="item" data-value="{{.ID}}"> @@ -23,7 +23,7 @@ {{if .ClosedMilestones}} {{if gt (len .OpenMilestones) 0}}<div class="divider"></div>{{end}} <div class="header"> - {{ctx.Locale.Tr "repo.projects.note.closed_milestone"}} + {{ctx.Locale.Tr "repo.projects.note.closed_milestones"}} </div> {{range .ClosedMilestones}} <div class="item" data-value="{{.ID}}"> From 0f74de61dd8b5178fcd4d2b929827fd622324997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 04:28:54 +0100 Subject: [PATCH 45/46] chore: fmt and lint --- models/migrations/v1_22/v287.go | 56 +++++---- models/project/board.go | 14 +-- models/project/note.go | 109 ++++++++-------- models/project/note_label.go | 47 +++---- models/project/project.go | 42 +++---- modules/templates/util_render.go | 2 +- routers/web/org/projects.go | 128 +++++++++---------- routers/web/repo/projects.go | 130 ++++++++++---------- routers/web/web.go | 32 ++--- services/forms/repo_form.go | 4 +- templates/projects/list.tmpl | 6 +- templates/projects/milestone_selection.tmpl | 2 +- templates/projects/view.tmpl | 14 +-- templates/repo/note.tmpl | 2 +- 14 files changed, 304 insertions(+), 284 deletions(-) diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go index 53f58025aaa49..9fd2e693edca8 100644 --- a/models/migrations/v1_22/v287.go +++ b/models/migrations/v1_22/v287.go @@ -9,33 +9,43 @@ import ( "xorm.io/xorm" ) -func CreateTablesForProjectBoardNotes(x *xorm.Engine) error { - type ProjectBoardNote struct { - ID int64 `xorm:"pk autoincr"` - Title string `xorm:"TEXT NOT NULL"` - Content string `xorm:"LONGTEXT"` - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` - PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` - MilestoneID int64 `xorm:"INDEX"` - - ProjectID int64 `xorm:"INDEX NOT NULL"` - BoardID int64 `xorm:"INDEX NOT NULL"` - CreatorID int64 `xorm:"NOT NULL"` - - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` - } +type BoardNote struct { + ID int64 `xorm:"pk autoincr"` + Title string `xorm:"TEXT NOT NULL"` + Content string `xorm:"LONGTEXT"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + PinOrder int64 `xorm:"NOT NULL DEFAULT 0"` + MilestoneID int64 `xorm:"INDEX"` + + ProjectID int64 `xorm:"INDEX NOT NULL"` + BoardID int64 `xorm:"INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} - type ProjectBoardNoteLabel struct { - ID int64 `xorm:"pk autoincr"` - ProjectBoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` - LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` - } +// TableName xorm will read the table name from this method +func (BoardNote) TableName() string { + return "project_board_note" +} + +type BoardNoteLabel struct { + ID int64 `xorm:"pk autoincr"` + BoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` + LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` +} + +// TableName xorm will read the table name from this method +func (BoardNoteLabel) TableName() string { + return "project_board_note_label" +} - err := x.Sync(new(ProjectBoardNote)) +func CreateTablesForBoardNotes(x *xorm.Engine) error { + err := x.Sync(new(BoardNote)) if err != nil { return err } - return x.Sync(new(ProjectBoardNoteLabel)) + return x.Sync(new(BoardNoteLabel)) } diff --git a/models/project/board.go b/models/project/board.go index d759336cc44db..04265dbb19f93 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -82,8 +82,8 @@ func (b *Board) NumIssues(ctx context.Context) int { return int(c) } -// NumProjectBoardNotes return counter of all notes assigned to the board -func (b *Board) NumProjectBoardNotes(ctx context.Context) int { +// NumBoardNotes return counter of all notes assigned to the board +func (b *Board) NumBoardNotes(ctx context.Context) int { c, err := db.GetEngine(ctx).Table("project_board_note"). Where("project_id=?", b.ProjectID). And("board_id=?", b.ID). @@ -96,11 +96,11 @@ func (b *Board) NumProjectBoardNotes(ctx context.Context) int { return int(c) } -// NumIssuesAndProjectBoardNotes return counter of all issues and notes assigned to the board -func (b *Board) NumIssuesAndProjectBoardNotes(ctx context.Context) int { +// NumIssuesAndNotes return counter of all issues and notes assigned to the board +func (b *Board) NumIssuesAndNotes(ctx context.Context) int { numIssues := b.NumIssues(ctx) - numProjectBoardNotes := b.NumProjectBoardNotes(ctx) - return numIssues + numProjectBoardNotes + numBoardNotes := b.NumBoardNotes(ctx) + return numIssues + numBoardNotes } func init() { @@ -201,7 +201,7 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { return err } - if err = board.removeProjectBoardNotes(ctx); err != nil { + if err = board.removeBoardNotes(ctx); err != nil { return err } diff --git a/models/project/note.go b/models/project/note.go index 01ec0c03633e5..43600b4d6d01a 100644 --- a/models/project/note.go +++ b/models/project/note.go @@ -17,8 +17,8 @@ import ( "xorm.io/builder" ) -// ProjectBoardNote is used to represent a note on a boards -type ProjectBoardNote struct { +// BoardNote is used to represent a note on a boards +type BoardNote struct { ID int64 `xorm:"pk autoincr"` Title string `xorm:"TEXT NOT NULL"` Content string `xorm:"LONGTEXT"` @@ -36,37 +36,42 @@ type ProjectBoardNote struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } -type ProjectBoardNoteList []*ProjectBoardNote +// TableName xorm will read the table name from this method +func (*BoardNote) TableName() string { + return "project_board_note" +} + +type BoardNoteList []*BoardNote -// ProjectBoardNotesOptions represents options of an note. -type ProjectBoardNotesOptions struct { +// BoardNotesOptions represents options of an note. +type BoardNotesOptions struct { ProjectID int64 BoardID int64 IsPinned util.OptionalBool } func init() { - db.RegisterModel(new(ProjectBoardNote)) - db.RegisterModel(new(ProjectBoardNoteLabel)) + db.RegisterModel(new(BoardNote)) + db.RegisterModel(new(BoardNoteLabel)) } -// GetProjectBoardNoteByID load note by ID -func GetProjectBoardNoteByID(ctx context.Context, projectBoardNoteID int64) (*ProjectBoardNote, error) { - projectBoardNote := new(ProjectBoardNote) +// GetBoardNoteByID load note by ID +func GetBoardNoteByID(ctx context.Context, projectBoardNoteID int64) (*BoardNote, error) { + projectBoardNote := new(BoardNote) has, err := db.GetEngine(ctx).ID(projectBoardNoteID).Get(projectBoardNote) if err != nil { return nil, err } else if !has { - return nil, ErrProjectBoardNoteNotExist{ProjectBoardNoteID: projectBoardNoteID} + return nil, ErrBoardNoteNotExist{BoardNoteID: projectBoardNoteID} } return projectBoardNote, nil } -// GetProjectBoardNotesByIds return notes with the given IDs. -func GetProjectBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) (ProjectBoardNoteList, error) { - projectBoardNoteList := make(ProjectBoardNoteList, 0, len(projectBoardNoteIDs)) +// GetBoardNotesByIds return notes with the given IDs. +func GetBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) (BoardNoteList, error) { + projectBoardNoteList := make(BoardNoteList, 0, len(projectBoardNoteIDs)) if err := db.GetEngine(ctx).In("id", projectBoardNoteIDs).Find(&projectBoardNoteList); err != nil { return nil, err @@ -79,9 +84,9 @@ func GetProjectBoardNotesByIds(ctx context.Context, projectBoardNoteIDs []int64) return projectBoardNoteList, nil } -// GetProjectBoardNotesByProjectID load pinned notes assigned to the project -func GetProjectBoardNotesByProjectID(ctx context.Context, projectID int64, isPinned bool) (ProjectBoardNoteList, error) { - projectBoardNoteList, err := ProjectBoardNotes(ctx, &ProjectBoardNotesOptions{ +// GetBoardNotesByProjectID load pinned notes assigned to the project +func GetBoardNotesByProjectID(ctx context.Context, projectID int64, isPinned bool) (BoardNoteList, error) { + projectBoardNoteList, err := BoardNotes(ctx, &BoardNotesOptions{ ProjectID: projectID, BoardID: -1, IsPinned: util.OptionalBoolOf(isPinned), @@ -93,11 +98,11 @@ func GetProjectBoardNotesByProjectID(ctx context.Context, projectID int64, isPin return projectBoardNoteList, nil } -// LoadProjectBoardNotesFromBoardList load notes assigned to the boards -func (p *Project) LoadProjectBoardNotesFromBoardList(ctx context.Context, boardList BoardList) (map[int64]ProjectBoardNoteList, error) { - projectBoardNoteListMap := make(map[int64]ProjectBoardNoteList, len(boardList)) +// LoadBoardNotesFromBoardList load notes assigned to the boards +func (p *Project) LoadBoardNotesFromBoardList(ctx context.Context, boardList BoardList) (map[int64]BoardNoteList, error) { + projectBoardNoteListMap := make(map[int64]BoardNoteList, len(boardList)) for i := range boardList { - il, err := LoadProjectBoardNotesFromBoard(ctx, boardList[i]) + il, err := LoadBoardNotesFromBoard(ctx, boardList[i]) if err != nil { return nil, err } @@ -106,9 +111,9 @@ func (p *Project) LoadProjectBoardNotesFromBoardList(ctx context.Context, boardL return projectBoardNoteListMap, nil } -// LoadProjectBoardNotesFromBoard load notes assigned to this board -func LoadProjectBoardNotesFromBoard(ctx context.Context, board *Board) (ProjectBoardNoteList, error) { - projectBoardNoteList, err := ProjectBoardNotes(ctx, &ProjectBoardNotesOptions{ +// LoadBoardNotesFromBoard load notes assigned to this board +func LoadBoardNotesFromBoard(ctx context.Context, board *Board) (BoardNoteList, error) { + projectBoardNoteList, err := BoardNotes(ctx, &BoardNotesOptions{ ProjectID: board.ProjectID, BoardID: board.ID, }) @@ -119,8 +124,8 @@ func LoadProjectBoardNotesFromBoard(ctx context.Context, board *Board) (ProjectB return projectBoardNoteList, nil } -// ProjectBoardNotes returns a list of notes by given conditions. -func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (ProjectBoardNoteList, error) { +// BoardNotes returns a list of notes by given conditions. +func BoardNotes(ctx context.Context, opts *BoardNotesOptions) (BoardNoteList, error) { sess := db.GetEngine(ctx) if opts.BoardID >= 0 { @@ -137,7 +142,7 @@ func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (Pro } } - projectBoardNoteList := ProjectBoardNoteList{} + projectBoardNoteList := BoardNoteList{} if err := sess.Asc("sorting").Desc("updated_unix").Desc("id").Find(&projectBoardNoteList); err != nil { return nil, fmt.Errorf("unable to query project-board-notes: %w", err) } @@ -149,14 +154,14 @@ func ProjectBoardNotes(ctx context.Context, opts *ProjectBoardNotesOptions) (Pro return projectBoardNoteList, nil } -// NewProjectBoardNote adds a new note to a given board -func NewProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardNote) error { +// NewBoardNote adds a new note to a given board +func NewBoardNote(ctx context.Context, projectBoardNote *BoardNote) error { _, err := db.GetEngine(ctx).Insert(projectBoardNote) return err } // LoadAttributes prerenders the markdown content and sets the creator -func (projectBoardNoteList ProjectBoardNoteList) LoadAttributes(ctx context.Context) error { +func (projectBoardNoteList BoardNoteList) LoadAttributes(ctx context.Context) error { for _, projectBoardNote := range projectBoardNoteList { creator := new(user_model.User) has, err := db.GetEngine(ctx).ID(projectBoardNote.CreatorID).Get(creator) @@ -179,12 +184,12 @@ func (projectBoardNoteList ProjectBoardNoteList) LoadAttributes(ctx context.Cont var ErrBoardNoteMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned project-board-notes has been readched") // IsPinned returns if a BoardNote is pinned -func (projectBoardNote *ProjectBoardNote) IsPinned() bool { +func (projectBoardNote *BoardNote) IsPinned() bool { return projectBoardNote.PinOrder != 0 } // IsPinned returns if a BoardNote is pinned -func (projectBoardNote *ProjectBoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { +func (projectBoardNote *BoardNote) GetMaxPinOrder(ctx context.Context) (int64, error) { var maxPin int64 _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM project_board_note WHERE project_id = ?", projectBoardNote.ProjectID).Get(&maxPin) if err != nil { @@ -194,7 +199,7 @@ func (projectBoardNote *ProjectBoardNote) GetMaxPinOrder(ctx context.Context) (i } // IsPinned returns if a BoardNote is pinned -func (projectBoardNote *ProjectBoardNote) IsNewPinAllowed(ctx context.Context) bool { +func (projectBoardNote *BoardNote) IsNewPinAllowed(ctx context.Context) bool { maxPin, err := projectBoardNote.GetMaxPinOrder(ctx) if err != nil { return false @@ -205,7 +210,7 @@ func (projectBoardNote *ProjectBoardNote) IsNewPinAllowed(ctx context.Context) b } // Pin pins a BoardNote -func (projectBoardNote *ProjectBoardNote) Pin(ctx context.Context) error { +func (projectBoardNote *BoardNote) Pin(ctx context.Context) error { // If the BoardNote is already pinned, we don't need to pin it twice if projectBoardNote.IsPinned() { return nil @@ -234,7 +239,7 @@ func (projectBoardNote *ProjectBoardNote) Pin(ctx context.Context) error { } // Unpin unpins a BoardNote -func (projectBoardNote *ProjectBoardNote) Unpin(ctx context.Context) error { +func (projectBoardNote *BoardNote) Unpin(ctx context.Context) error { // If the BoardNote is not pinned, we don't need to unpin it if !projectBoardNote.IsPinned() { return nil @@ -259,7 +264,7 @@ func (projectBoardNote *ProjectBoardNote) Unpin(ctx context.Context) error { } // MovePin moves a Pinned BoardNote to a new Position -func (projectBoardNote *ProjectBoardNote) MovePin(ctx context.Context, newPosition int64) error { +func (projectBoardNote *BoardNote) MovePin(ctx context.Context, newPosition int64) error { // If the BoardNote is not pinned, we can't move them if !projectBoardNote.IsPinned() { return nil @@ -309,9 +314,9 @@ func (projectBoardNote *ProjectBoardNote) MovePin(ctx context.Context, newPositi return committer.Commit() } -// GetPinnedProjectBoardNotes returns the pinned BaordNotes for the given Project -func GetPinnedProjectBoardNotes(ctx context.Context, projectID int64) (ProjectBoardNoteList, error) { - projectBoardNoteList := make(ProjectBoardNoteList, 0) +// GetPinnedBoardNotes returns the pinned BaordNotes for the given Project +func GetPinnedBoardNotes(ctx context.Context, projectID int64) (BoardNoteList, error) { + projectBoardNoteList := make(BoardNoteList, 0) err := db.GetEngine(ctx). Table("project_board_note"). @@ -331,17 +336,17 @@ func GetPinnedProjectBoardNotes(ctx context.Context, projectID int64) (ProjectBo } // GetTasks returns the amount of tasks in the project-board-notes content -func (projectBoardNote *ProjectBoardNote) GetTasks() int { +func (projectBoardNote *BoardNote) GetTasks() int { return len(markdown.MarkdownTasksRegex.FindAllStringIndex(projectBoardNote.Content, -1)) } // GetTasksDone returns the amount of completed tasks in the project-board-notes content -func (projectBoardNote *ProjectBoardNote) GetTasksDone() int { +func (projectBoardNote *BoardNote) GetTasksDone() int { return len(markdown.MarkdownTasksDoneRegex.FindAllStringIndex(projectBoardNote.Content, -1)) } -// UpdateProjectBoardNote changes a BoardNote -func UpdateProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardNote) error { +// UpdateBoardNote changes a BoardNote +func UpdateBoardNote(ctx context.Context, projectBoardNote *BoardNote) error { var fieldToUpdate []string fieldToUpdate = append(fieldToUpdate, "title") @@ -352,12 +357,12 @@ func UpdateProjectBoardNote(ctx context.Context, projectBoardNote *ProjectBoardN return err } -// MoveProjectBoardNoteOnProjectBoard moves or keeps notes in a column and sorts them inside that column -func MoveProjectBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedProjectBoardNoteIDs map[int64]int64) error { +// MoveBoardNoteOnProjectBoard moves or keeps notes in a column and sorts them inside that column +func MoveBoardNoteOnProjectBoard(ctx context.Context, board *Board, sortedBoardNoteIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) - for sorting, issueID := range sortedProjectBoardNoteIDs { + for sorting, issueID := range sortedBoardNoteIDs { _, err := sess.Exec("UPDATE `project_board_note` SET board_id=?, sorting=? WHERE id=?", board.ID, sorting, issueID) if err != nil { return err @@ -367,21 +372,21 @@ func MoveProjectBoardNoteOnProjectBoard(ctx context.Context, board *Board, sorte }) } -func deleteProjectBoardNoteByProjectID(ctx context.Context, projectID int64) error { - _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&ProjectBoardNote{}) +func deleteBoardNoteByProjectID(ctx context.Context, projectID int64) error { + _, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&BoardNote{}) return err } -// DeleteProjectBoardNote removes the BoardNote from the project board. -func DeleteProjectBoardNote(ctx context.Context, boardNote *ProjectBoardNote) error { +// DeleteBoardNote removes the BoardNote from the project board. +func DeleteBoardNote(ctx context.Context, boardNote *BoardNote) error { if _, err := db.GetEngine(ctx).Delete(boardNote); err != nil { return err } return nil } -// removeProjectBoardNotes sets the boardID to 0 for the board -func (b *Board) removeProjectBoardNotes(ctx context.Context) error { +// removeBoardNotes sets the boardID to 0 for the board +func (b *Board) removeBoardNotes(ctx context.Context) error { _, err := db.GetEngine(ctx).Exec("UPDATE `project_board_note` SET board_id = 0 WHERE board_id = ?", b.ID) return err } diff --git a/models/project/note_label.go b/models/project/note_label.go index 562d45670eaa6..48bbb61c7e60d 100644 --- a/models/project/note_label.go +++ b/models/project/note_label.go @@ -10,56 +10,61 @@ import ( "code.gitea.io/gitea/models/db" ) -// ProjectBoardNoteLabel represents an project-baord-note-label relation. -type ProjectBoardNoteLabel struct { - ID int64 `xorm:"pk autoincr"` - ProjectBoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` - LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` +// BoardNoteLabel represents an project-baord-note-label relation. +type BoardNoteLabel struct { + ID int64 `xorm:"pk autoincr"` + BoardNoteID int64 `xorm:"UNIQUE(s) NOT NULL"` + LabelID int64 `xorm:"UNIQUE(s) NOT NULL"` +} + +// TableName xorm will read the table name from this method +func (*BoardNoteLabel) TableName() string { + return "project_board_note_label" } // LoadLabels loads labels -func (projectBoardNote *ProjectBoardNote) LoadLabelIDs(ctx context.Context) (err error) { +func (projectBoardNote *BoardNote) LoadLabelIDs(ctx context.Context) (err error) { if projectBoardNote.LabelIDs != nil || len(projectBoardNote.LabelIDs) == 0 { - projectBoardNote.LabelIDs, err = GetLabelsByProjectBoardNoteID(ctx, projectBoardNote.ID) + projectBoardNote.LabelIDs, err = GetLabelsByBoardNoteID(ctx, projectBoardNote.ID) if err != nil { - return fmt.Errorf("GetLabelsByProjectBoardNoteID [%d]: %w", projectBoardNote.ID, err) + return fmt.Errorf("GetLabelsByBoardNoteID [%d]: %w", projectBoardNote.ID, err) } } return nil } // LoadLabels removes all labels from project-board-note -func (projectBoardNote *ProjectBoardNote) RemoveAllLabels(ctx context.Context) error { - _, err := db.GetEngine(ctx).Where("project_board_note_id = ?", projectBoardNote.ID).Delete(ProjectBoardNoteLabel{}) +func (projectBoardNote *BoardNote) RemoveAllLabels(ctx context.Context) error { + _, err := db.GetEngine(ctx).Where("board_note_id = ?", projectBoardNote.ID).Delete(BoardNoteLabel{}) return err } // LoadLabels add a label to project-board-note -> requires a valid labelID -func (projectBoardNote *ProjectBoardNote) AddLabel(ctx context.Context, labelID int64) error { - _, err := db.GetEngine(ctx).Insert(ProjectBoardNoteLabel{ - ProjectBoardNoteID: projectBoardNote.ID, - LabelID: labelID, +func (projectBoardNote *BoardNote) AddLabel(ctx context.Context, labelID int64) error { + _, err := db.GetEngine(ctx).Insert(BoardNoteLabel{ + BoardNoteID: projectBoardNote.ID, + LabelID: labelID, }) return err } // LoadLabels removes a label from project-board-note -func (projectBoardNote *ProjectBoardNote) RemoveLabelByID(ctx context.Context, labelID int64) error { - _, err := db.GetEngine(ctx).Delete(ProjectBoardNoteLabel{ - ProjectBoardNoteID: projectBoardNote.ID, - LabelID: labelID, +func (projectBoardNote *BoardNote) RemoveLabelByID(ctx context.Context, labelID int64) error { + _, err := db.GetEngine(ctx).Delete(BoardNoteLabel{ + BoardNoteID: projectBoardNote.ID, + LabelID: labelID, }) return err } -// GetLabelsByProjectBoardNoteID returns all labelIDs that belong to given projectBoardNote by ID. -func GetLabelsByProjectBoardNoteID(ctx context.Context, projectBoardNoteID int64) ([]int64, error) { +// GetLabelsByBoardNoteID returns all labelIDs that belong to given projectBoardNote by ID. +func GetLabelsByBoardNoteID(ctx context.Context, projectBoardNoteID int64) ([]int64, error) { var labelIDs []int64 return labelIDs, db.GetEngine(ctx). Table("label"). Cols("label.id"). Asc("label.name"). - Where("project_board_note_label.project_board_note_id = ?", projectBoardNoteID). + Where("project_board_note_label.board_note_id = ?", projectBoardNoteID). Join("INNER", "project_board_note_label", "project_board_note_label.label_id = label.id"). Find(&labelIDs) } diff --git a/models/project/project.go b/models/project/project.go index d613e3c297e78..9ceb6979ea125 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -85,41 +85,41 @@ func (err ErrProjectBoardNotExist) Unwrap() error { return util.ErrNotExist } -// ErrProjectBoardNoteNotExist represents a "ProjectBoardNotExist" kind of error. -type ErrProjectBoardNoteNotExist struct { - ProjectBoardNoteID int64 +// ErrBoardNoteNotExist represents a "ProjectBoardNotExist" kind of error. +type ErrBoardNoteNotExist struct { + BoardNoteID int64 } -// IsErrProjectBoardNoteNotExist checks if an error is a ErrProjectBoardNoteNotExist -func IsErrProjectBoardNoteNotExist(err error) bool { - _, ok := err.(ErrProjectBoardNoteNotExist) +// IsErrBoardNoteNotExist checks if an error is a ErrBoardNoteNotExist +func IsErrBoardNoteNotExist(err error) bool { + _, ok := err.(ErrBoardNoteNotExist) return ok } -func (err ErrProjectBoardNoteNotExist) Error() string { - return fmt.Sprintf("project-board-note does not exist [id: %d]", err.ProjectBoardNoteID) +func (err ErrBoardNoteNotExist) Error() string { + return fmt.Sprintf("project-board-note does not exist [id: %d]", err.BoardNoteID) } -func (err ErrProjectBoardNoteNotExist) Unwrap() error { +func (err ErrBoardNoteNotExist) Unwrap() error { return util.ErrNotExist } -// ErrProjectBoardNoteLabelNotExist represents a "ProjectBoardNotExist" kind of error. -type ErrProjectBoardNoteLabelNotExist struct { - ProjectBoardNoteLabelID int64 +// ErrBoardNoteLabelNotExist represents a "ProjectBoardNotExist" kind of error. +type ErrBoardNoteLabelNotExist struct { + BoardNoteLabelID int64 } -// IsErrProjectBoardNoteLabelNotExist checks if an error is a ErrProjectBoardNoteLabelNotExist -func IsErrProjectBoardNoteLabelNotExist(err error) bool { - _, ok := err.(ErrProjectBoardNoteLabelNotExist) +// IsErrBoardNoteLabelNotExist checks if an error is a ErrBoardNoteLabelNotExist +func IsErrBoardNoteLabelNotExist(err error) bool { + _, ok := err.(ErrBoardNoteLabelNotExist) return ok } -func (err ErrProjectBoardNoteLabelNotExist) Error() string { - return fmt.Sprintf("project-board-note-label does not exist [id: %d]", err.ProjectBoardNoteLabelID) +func (err ErrBoardNoteLabelNotExist) Error() string { + return fmt.Sprintf("project-board-note-label does not exist [id: %d]", err.BoardNoteLabelID) } -func (err ErrProjectBoardNoteLabelNotExist) Unwrap() error { +func (err ErrBoardNoteLabelNotExist) Unwrap() error { return util.ErrNotExist } @@ -138,8 +138,8 @@ type Project struct { CardType CardType Type Type - RenderedContent string `xorm:"-"` - FirstPinnedProjectBoardNote *ProjectBoardNote `xorm:"-"` + RenderedContent string `xorm:"-"` + FirstPinnedBoardNote *BoardNote `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -450,7 +450,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error { return err } - if err := deleteProjectBoardNoteByProjectID(ctx, id); err != nil { + if err := deleteBoardNoteByProjectID(ctx, id); err != nil { return err } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index e7208116f55f7..e680fdbdebadf 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -247,7 +247,7 @@ func RenderLabelsFromIDs(ctx context.Context, labelIDs []int64, repoLink string) return template.HTML(htmlCode) } -func RenderMilestone(ctx context.Context, milestoneID int64, repoID int64, classes ...string) template.HTML { +func RenderMilestone(ctx context.Context, milestoneID, repoID int64, classes ...string) template.HTML { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if err != nil { log.Error("GetRepositoryByID", err) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 02f9a4c958cea..341c7845ee091 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -106,13 +106,13 @@ func Projects(ctx *context.Context) { for _, project := range projects { project.RenderedContent = project.Description - pinnedProjectBoardNotes, err := project_model.GetProjectBoardNotesByProjectID(ctx, project.ID, true) + pinnedBoardNotes, err := project_model.GetBoardNotesByProjectID(ctx, project.ID, true) if err != nil { - ctx.ServerError("GetProjectBoardNotesByProjectID", err) + ctx.ServerError("GetBoardNotesByProjectID", err) return } - if len(pinnedProjectBoardNotes) > 0 { - project.FirstPinnedProjectBoardNote = pinnedProjectBoardNotes[0] + if len(pinnedBoardNotes) > 0 { + project.FirstPinnedBoardNote = pinnedBoardNotes[0] } } @@ -371,9 +371,9 @@ func ViewProject(ctx *context.Context) { return } - notesMap, err := project.LoadProjectBoardNotesFromBoardList(ctx, boards) + notesMap, err := project.LoadBoardNotesFromBoardList(ctx, boards) if err != nil { - ctx.ServerError("LoadProjectBoardNotesOfBoards", err) + ctx.ServerError("LoadBoardNotesOfBoards", err) return } @@ -410,9 +410,9 @@ func ViewProject(ctx *context.Context) { } } - pinnedBoardNotes, err := project_model.GetPinnedProjectBoardNotes(ctx, project.ID) + pinnedBoardNotes, err := project_model.GetPinnedBoardNotes(ctx, project.ID) if err != nil { - ctx.ServerError("GetPinnedProjectBoardNotes", err) + ctx.ServerError("GetPinnedBoardNotes", err) return } @@ -423,8 +423,8 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend - ctx.Data["PinnedProjectBoardNotes"] = pinnedBoardNotes - ctx.Data["ProjectBoardNotesMap"] = notesMap + ctx.Data["PinnedColumnNotes"] = pinnedBoardNotes + ctx.Data["ColumnNotesMap"] = notesMap shared_user.RenderUserHeader(ctx) err = shared_user.LoadHeaderCount(ctx) @@ -773,7 +773,7 @@ func MoveIssues(ctx *context.Context) { ctx.JSONOK() } -func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.ProjectBoardNote) { +func checkBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.BoardNote) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -795,12 +795,12 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return nil, nil } - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("ProjectBoardNoteNotFound", err) + if project_model.IsErrBoardNoteNotExist(err) { + ctx.NotFound("BoardNoteNotFound", err) } else { - ctx.ServerError("GetProjectBoardNoteById", err) + ctx.ServerError("GetBoardNoteById", err) } return nil, nil } @@ -814,7 +814,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode if projectBoardNote.ProjectID != project.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), + "message": fmt.Sprintf("BoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), }) return nil, nil } @@ -822,7 +822,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return project, projectBoardNote } -func AddProjectBoardNoteToBoard(ctx *context.Context) { +func AddBoardNoteToBoard(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -830,7 +830,7 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { return } - form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) + form := web.GetForm(ctx).(*forms.BoardNoteForm) // LabelIDs is send without parentheses - maybe because of multipart/form-data labelIdsString := "[" + ctx.Req.FormValue("labelIds") + "]" @@ -852,7 +852,7 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { } } - projectBoardNote := project_model.ProjectBoardNote{ + projectBoardNote := project_model.BoardNote{ Title: form.Title, Content: form.Content, @@ -860,9 +860,9 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { BoardID: ctx.ParamsInt64(":boardID"), CreatorID: ctx.Doer.ID, } - err := project_model.NewProjectBoardNote(ctx, &projectBoardNote) + err := project_model.NewBoardNote(ctx, &projectBoardNote) if err != nil { - ctx.ServerError("NewProjectBoardNote", err) + ctx.ServerError("NewBoardNote", err) return } @@ -879,9 +879,9 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { ctx.JSONOK() } -func EditProjectBoardNote(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) - _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) +func EditBoardNote(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.BoardNoteForm) + _, projectBoardNote := checkBoardNoteChangePermissions(ctx) if ctx.Written() { return } @@ -889,29 +889,29 @@ func EditProjectBoardNote(ctx *context.Context) { projectBoardNote.Title = form.Title projectBoardNote.Content = form.Content - if err := project_model.UpdateProjectBoardNote(ctx, projectBoardNote); err != nil { - ctx.ServerError("UpdateProjectBoardNote", err) + if err := project_model.UpdateBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("UpdateBoardNote", err) return } ctx.JSONOK() } -func DeleteProjectBoardNote(ctx *context.Context) { - _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) +func DeleteBoardNote(ctx *context.Context) { + _, projectBoardNote := checkBoardNoteChangePermissions(ctx) if ctx.Written() { return } - if err := project_model.DeleteProjectBoardNote(ctx, projectBoardNote); err != nil { - ctx.ServerError("DeleteProjectBoardNote", err) + if err := project_model.DeleteBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("DeleteBoardNote", err) return } ctx.JSONOK() } -func MoveProjectBoardNote(ctx *context.Context) { +func MoveBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -953,59 +953,59 @@ func MoveProjectBoardNote(ctx *context.Context) { } } - type MovedProjectBoardNotesForm struct { - ProjectBoardNotes []struct { - ProjectBoardNoteID int64 `json:"projectBoardNoteID"` - Sorting int64 `json:"sorting"` + type MovedBoardNotesForm struct { + BoardNotes []struct { + BoardNoteID int64 `json:"projectBoardNoteID"` + Sorting int64 `json:"sorting"` } `json:"projectBoardNotes"` } - form := &MovedProjectBoardNotesForm{} + form := &MovedBoardNotesForm{} if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("DecodeMovedProjectBoardNotesForm", err) + ctx.ServerError("DecodeMovedBoardNotesForm", err) return } - projectBoardNoteIDs := make([]int64, 0, len(form.ProjectBoardNotes)) - sortedProjectBoardNoteIDs := make(map[int64]int64) - for _, boardNote := range form.ProjectBoardNotes { - projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.ProjectBoardNoteID) - sortedProjectBoardNoteIDs[boardNote.Sorting] = boardNote.ProjectBoardNoteID + projectBoardNoteIDs := make([]int64, 0, len(form.BoardNotes)) + sortedBoardNoteIDs := make(map[int64]int64) + for _, boardNote := range form.BoardNotes { + projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.BoardNoteID) + sortedBoardNoteIDs[boardNote.Sorting] = boardNote.BoardNoteID } - movedProjectBoardNotes, err := project_model.GetProjectBoardNotesByIds(ctx, projectBoardNoteIDs) + movedBoardNotes, err := project_model.GetBoardNotesByIds(ctx, projectBoardNoteIDs) if err != nil { - if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("ProjectBoardNoteNotExisting", nil) + if project_model.IsErrBoardNoteNotExist(err) { + ctx.NotFound("BoardNoteNotExisting", nil) } else { - ctx.ServerError("GetProjectBoardNoteByIds", err) + ctx.ServerError("GetBoardNoteByIds", err) } return } - if len(movedProjectBoardNotes) != len(form.ProjectBoardNotes) { + if len(movedBoardNotes) != len(form.BoardNotes) { ctx.ServerError("some project-board-notes do not exist", errors.New("some project-board-notes do not exist")) return } - if err = project_model.MoveProjectBoardNoteOnProjectBoard(ctx, board, sortedProjectBoardNoteIDs); err != nil { - ctx.ServerError("MoveProjectBoardNoteOnProjectBoard", err) + if err = project_model.MoveBoardNoteOnProjectBoard(ctx, board, sortedBoardNoteIDs); err != nil { + ctx.ServerError("MoveBoardNoteOnProjectBoard", err) return } ctx.JSONOK() } -// PinProjectBoardNote pins the BoardNote -func PinProjectBoardNote(ctx *context.Context) { - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +// PinBoardNote pins the BoardNote +func PinBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetProjectBoardNoteByID", err) + ctx.ServerError("GetBoardNoteByID", err) return } err = projectBoardNote.Pin(ctx) if err != nil { - ctx.ServerError("PinProjectBoardNote", err) + ctx.ServerError("PinBoardNote", err) return } @@ -1013,8 +1013,8 @@ func PinProjectBoardNote(ctx *context.Context) { } // PinBoardNote unpins the BoardNote -func UnPinProjectBoardNote(ctx *context.Context) { - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +func UnPinBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { ctx.ServerError("GetBoardNoteByID", err) return @@ -1022,7 +1022,7 @@ func UnPinProjectBoardNote(ctx *context.Context) { err = projectBoardNote.Unpin(ctx) if err != nil { - ctx.ServerError("UnpinProjectBoardNote", err) + ctx.ServerError("UnpinBoardNote", err) return } @@ -1030,31 +1030,31 @@ func UnPinProjectBoardNote(ctx *context.Context) { } // PinBoardNote moves a pined the BoardNote -func PinMoveProjectBoardNote(ctx *context.Context) { +func PinMoveBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, "Only signed in users are allowed to perform this action.") return } - type MovePinProjectBoardNoteForm struct { + type MovePinBoardNoteForm struct { Position int64 `json:"position"` } - form := &MovePinProjectBoardNoteForm{} + form := &MovePinBoardNoteForm{} if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("Decode MovePinProjectBoardNoteForm", err) + ctx.ServerError("Decode MovePinBoardNoteForm", err) return } - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetProjectBoardNoteByID", err) + ctx.ServerError("GetBoardNoteByID", err) return } err = projectBoardNote.MovePin(ctx, form.Position) if err != nil { - ctx.ServerError("MovePinProjectBoardNote", err) + ctx.ServerError("MovePinBoardNote", err) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 25e8dc93251bc..0f8638c20bca5 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -103,13 +103,13 @@ func Projects(ctx *context.Context) { return } - pinnedProjectBoardNotes, err := project_model.GetProjectBoardNotesByProjectID(ctx, projects[i].ID, true) + pinnedBoardNotes, err := project_model.GetBoardNotesByProjectID(ctx, projects[i].ID, true) if err != nil { - ctx.ServerError("GetProjectBoardNotesByProjectID", err) + ctx.ServerError("GetBoardNotesByProjectID", err) return } - if len(pinnedProjectBoardNotes) > 0 { - projects[i].FirstPinnedProjectBoardNote = pinnedProjectBoardNotes[0] + if len(pinnedBoardNotes) > 0 { + projects[i].FirstPinnedBoardNote = pinnedBoardNotes[0] } } @@ -334,9 +334,9 @@ func ViewProject(ctx *context.Context) { return } - notesMap, err := project.LoadProjectBoardNotesFromBoardList(ctx, boards) + notesMap, err := project.LoadBoardNotesFromBoardList(ctx, boards) if err != nil { - ctx.ServerError("LoadProjectBoardNotesOfBoards", err) + ctx.ServerError("LoadBoardNotesOfBoards", err) return } @@ -387,9 +387,9 @@ func ViewProject(ctx *context.Context) { return } - pinnedBoardNotes, err := project_model.GetPinnedProjectBoardNotes(ctx, project.ID) + pinnedBoardNotes, err := project_model.GetPinnedBoardNotes(ctx, project.ID) if err != nil { - ctx.ServerError("GetPinnedProjectBoardNotes", err) + ctx.ServerError("GetPinnedBoardNotes", err) return } @@ -424,8 +424,8 @@ func ViewProject(ctx *context.Context) { ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend - ctx.Data["PinnedProjectBoardNotes"] = pinnedBoardNotes - ctx.Data["ProjectBoardNotesMap"] = notesMap + ctx.Data["PinnedColumnNotes"] = pinnedBoardNotes + ctx.Data["ColumnNotesMap"] = notesMap ctx.HTML(http.StatusOK, tplProjectsView) } @@ -750,7 +750,7 @@ func MoveIssues(ctx *context.Context) { ctx.JSONOK() } -func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.ProjectBoardNote) { +func checkBoardNoteChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.BoardNote) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -775,12 +775,12 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return nil, nil } - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("ProjectBoardNoteNotFound", err) + if project_model.IsErrBoardNoteNotExist(err) { + ctx.NotFound("BoardNoteNotFound", err) } else { - ctx.ServerError("GetProjectBoardNoteById", err) + ctx.ServerError("GetBoardNoteById", err) } return nil, nil } @@ -794,7 +794,7 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode if projectBoardNote.ProjectID != project.ID { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ - "message": fmt.Sprintf("ProjectBoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), + "message": fmt.Sprintf("BoardNote[%d] is not in Project[%d] as expected", projectBoardNote.ID, project.ID), }) return nil, nil } @@ -809,8 +809,8 @@ func checkProjectBoardNoteChangePermissions(ctx *context.Context) (*project_mode return project, projectBoardNote } -func AddProjectBoardNoteToBoard(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) +func AddBoardNoteToBoard(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.BoardNoteForm) if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only authorized users are allowed to perform this action.", @@ -838,7 +838,7 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { } } - projectBoardNote := project_model.ProjectBoardNote{ + projectBoardNote := project_model.BoardNote{ Title: form.Title, Content: form.Content, MilestoneID: form.MilestoneID, @@ -847,9 +847,9 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { BoardID: ctx.ParamsInt64(":boardID"), CreatorID: ctx.Doer.ID, } - err := project_model.NewProjectBoardNote(ctx, &projectBoardNote) + err := project_model.NewBoardNote(ctx, &projectBoardNote) if err != nil { - ctx.ServerError("NewProjectBoardNote", err) + ctx.ServerError("NewBoardNote", err) return } @@ -866,7 +866,7 @@ func AddProjectBoardNoteToBoard(ctx *context.Context) { ctx.JSONOK() } -func findNewAndRemovedIDs(original []int64, updated []int64) (newValues, removedValues []int64) { +func findNewAndRemovedIDs(original, updated []int64) (newValues, removedValues []int64) { if original == nil { return updated, nil } @@ -891,9 +891,9 @@ func findNewAndRemovedIDs(original []int64, updated []int64) (newValues, removed return newValues, removedValues } -func EditProjectBoardNote(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.ProjectBoardNoteForm) - _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) +func EditBoardNote(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.BoardNoteForm) + _, projectBoardNote := checkBoardNoteChangePermissions(ctx) if ctx.Written() { return } @@ -907,8 +907,8 @@ func EditProjectBoardNote(ctx *context.Context) { ctx.ServerError("LoadLabelIDs", err) } - if err := project_model.UpdateProjectBoardNote(ctx, projectBoardNote); err != nil { - ctx.ServerError("UpdateProjectBoardNote", err) + if err := project_model.UpdateBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("UpdateBoardNote", err) return } @@ -960,21 +960,21 @@ func EditProjectBoardNote(ctx *context.Context) { ctx.JSONOK() } -func DeleteProjectBoardNote(ctx *context.Context) { - _, projectBoardNote := checkProjectBoardNoteChangePermissions(ctx) +func DeleteBoardNote(ctx *context.Context) { + _, projectBoardNote := checkBoardNoteChangePermissions(ctx) if ctx.Written() { return } - if err := project_model.DeleteProjectBoardNote(ctx, projectBoardNote); err != nil { - ctx.ServerError("DeleteProjectBoardNote", err) + if err := project_model.DeleteBoardNote(ctx, projectBoardNote); err != nil { + ctx.ServerError("DeleteBoardNote", err) return } ctx.JSONOK() } -func MoveProjectBoardNote(ctx *context.Context) { +func MoveBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, map[string]string{ "message": "Only signed in users are allowed to perform this action.", @@ -1023,59 +1023,59 @@ func MoveProjectBoardNote(ctx *context.Context) { } } - type MovedProjectBoardNotesForm struct { - ProjectBoardNotes []struct { - ProjectBoardNoteID int64 `json:"projectBoardNoteID"` - Sorting int64 `json:"sorting"` + type MovedBoardNotesForm struct { + BoardNotes []struct { + BoardNoteID int64 `json:"projectBoardNoteID"` + Sorting int64 `json:"sorting"` } `json:"projectBoardNotes"` } - form := &MovedProjectBoardNotesForm{} + form := &MovedBoardNotesForm{} if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("DecodeMovedProjectBoardNotesForm", err) + ctx.ServerError("DecodeMovedBoardNotesForm", err) return } - projectBoardNoteIDs := make([]int64, 0, len(form.ProjectBoardNotes)) - sortedProjectBoardNoteIDs := make(map[int64]int64) - for _, boardNote := range form.ProjectBoardNotes { - projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.ProjectBoardNoteID) - sortedProjectBoardNoteIDs[boardNote.Sorting] = boardNote.ProjectBoardNoteID + projectBoardNoteIDs := make([]int64, 0, len(form.BoardNotes)) + sortedBoardNoteIDs := make(map[int64]int64) + for _, boardNote := range form.BoardNotes { + projectBoardNoteIDs = append(projectBoardNoteIDs, boardNote.BoardNoteID) + sortedBoardNoteIDs[boardNote.Sorting] = boardNote.BoardNoteID } - movedProjectBoardNotes, err := project_model.GetProjectBoardNotesByIds(ctx, projectBoardNoteIDs) + movedBoardNotes, err := project_model.GetBoardNotesByIds(ctx, projectBoardNoteIDs) if err != nil { - if project_model.IsErrProjectBoardNoteNotExist(err) { - ctx.NotFound("ProjectBoardNoteNotExisting", nil) + if project_model.IsErrBoardNoteNotExist(err) { + ctx.NotFound("BoardNoteNotExisting", nil) } else { - ctx.ServerError("GetProjectBoardNoteByIds", err) + ctx.ServerError("GetBoardNoteByIds", err) } return } - if len(movedProjectBoardNotes) != len(form.ProjectBoardNotes) { + if len(movedBoardNotes) != len(form.BoardNotes) { ctx.ServerError("some project-board-notes do not exist", errors.New("some project-board-notes do not exist")) return } - if err = project_model.MoveProjectBoardNoteOnProjectBoard(ctx, board, sortedProjectBoardNoteIDs); err != nil { - ctx.ServerError("MoveProjectBoardNoteOnProjectBoard", err) + if err = project_model.MoveBoardNoteOnProjectBoard(ctx, board, sortedBoardNoteIDs); err != nil { + ctx.ServerError("MoveBoardNoteOnProjectBoard", err) return } ctx.JSONOK() } -// PinProjectBoardNote pins the BoardNote -func PinProjectBoardNote(ctx *context.Context) { - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +// PinBoardNote pins the BoardNote +func PinBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetProjectBoardNoteByID", err) + ctx.ServerError("GetBoardNoteByID", err) return } err = projectBoardNote.Pin(ctx) if err != nil { - ctx.ServerError("PinProjectBoardNote", err) + ctx.ServerError("PinBoardNote", err) return } @@ -1083,8 +1083,8 @@ func PinProjectBoardNote(ctx *context.Context) { } // PinBoardNote unpins the BoardNote -func UnPinProjectBoardNote(ctx *context.Context) { - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) +func UnPinBoardNote(ctx *context.Context) { + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { ctx.ServerError("GetBoardNoteByID", err) return @@ -1092,7 +1092,7 @@ func UnPinProjectBoardNote(ctx *context.Context) { err = projectBoardNote.Unpin(ctx) if err != nil { - ctx.ServerError("UnpinProjectBoardNote", err) + ctx.ServerError("UnpinBoardNote", err) return } @@ -1100,31 +1100,31 @@ func UnPinProjectBoardNote(ctx *context.Context) { } // PinBoardNote moves a pined the BoardNote -func PinMoveProjectBoardNote(ctx *context.Context) { +func PinMoveBoardNote(ctx *context.Context) { if ctx.Doer == nil { ctx.JSON(http.StatusForbidden, "Only signed in users are allowed to perform this action.") return } - type MovePinProjectBoardNoteForm struct { + type MovePinBoardNoteForm struct { Position int64 `json:"position"` } - form := &MovePinProjectBoardNoteForm{} + form := &MovePinBoardNoteForm{} if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { - ctx.ServerError("Decode MovePinProjectBoardNoteForm", err) + ctx.ServerError("Decode MovePinBoardNoteForm", err) return } - projectBoardNote, err := project_model.GetProjectBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) + projectBoardNote, err := project_model.GetBoardNoteByID(ctx, ctx.ParamsInt64(":noteID")) if err != nil { - ctx.ServerError("GetProjectBoardNoteByID", err) + ctx.ServerError("GetBoardNoteByID", err) return } err = projectBoardNote.MovePin(ctx, form.Position) if err != nil { - ctx.ServerError("MovePinProjectBoardNote", err) + ctx.ServerError("MovePinBoardNote", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 59dbd597971d5..ba4b8f40c6502 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -985,7 +985,7 @@ func registerRoutes(m *web.Route) { m.Get("", org.Projects) m.Get("/{id}", org.ViewProject) }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) - m.Group("", func() { + m.Group("", func() { //nolint:dupl m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { @@ -1005,17 +1005,17 @@ func registerRoutes(m *web.Route) { m.Post("/move", org.MoveIssues) m.Group("/note", func() { - m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), org.AddProjectBoardNoteToBoard) - m.Post("/move", org.MoveProjectBoardNote) + m.Post("", web.Bind(forms.BoardNoteForm{}), org.AddBoardNoteToBoard) + m.Post("/move", org.MoveBoardNote) m.Group("/{noteID}", func() { - m.Put("", web.Bind(forms.ProjectBoardNoteForm{}), org.EditProjectBoardNote) - m.Delete("", org.DeleteProjectBoardNote) + m.Put("", web.Bind(forms.BoardNoteForm{}), org.EditBoardNote) + m.Delete("", org.DeleteBoardNote) m.Group("/pin", func() { - m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), org.PinProjectBoardNote) - m.Delete("", org.UnPinProjectBoardNote) - m.Post("/move", org.PinMoveProjectBoardNote) + m.Post("", web.Bind(forms.BoardNoteForm{}), org.PinBoardNote) + m.Delete("", org.UnPinBoardNote) + m.Post("/move", org.PinMoveBoardNote) }) }) }) @@ -1339,7 +1339,7 @@ func registerRoutes(m *web.Route) { m.Group("/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) - m.Group("", func() { + m.Group("", func() { //nolint:dupl m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { @@ -1359,17 +1359,17 @@ func registerRoutes(m *web.Route) { m.Post("/move", repo.MoveIssues) m.Group("/note", func() { - m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), repo.AddProjectBoardNoteToBoard) - m.Post("/move", repo.MoveProjectBoardNote) + m.Post("", web.Bind(forms.BoardNoteForm{}), repo.AddBoardNoteToBoard) + m.Post("/move", repo.MoveBoardNote) m.Group("/{noteID}", func() { - m.Put("", web.Bind(forms.ProjectBoardNoteForm{}), repo.EditProjectBoardNote) - m.Delete("", repo.DeleteProjectBoardNote) + m.Put("", web.Bind(forms.BoardNoteForm{}), repo.EditBoardNote) + m.Delete("", repo.DeleteBoardNote) m.Group("/pin", func() { - m.Post("", web.Bind(forms.ProjectBoardNoteForm{}), repo.PinProjectBoardNote) - m.Delete("", repo.UnPinProjectBoardNote) - m.Post("/move", repo.PinMoveProjectBoardNote) + m.Post("", web.Bind(forms.BoardNoteForm{}), repo.PinBoardNote) + m.Delete("", repo.UnPinBoardNote) + m.Post("/move", repo.PinMoveBoardNote) }) }) }) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 4db0306643870..da87f7402f10b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -533,8 +533,8 @@ type EditProjectBoardForm struct { Color string `binding:"MaxSize(7)"` } -// ProjectBoardNoteForm is a form for editing/creating a note to a board -type ProjectBoardNoteForm struct { +// BoardNoteForm is a form for editing/creating a note to a board +type BoardNoteForm struct { Title string `binding:"Required;MaxSize(255)"` Content string MilestoneID int64 `form:"milestoneId"` diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index e694c76116921..21c03b58dbea6 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -45,11 +45,11 @@ <div class="milestone-list"> {{range .Projects}} - <li class="milestone-card {{if .FirstPinnedProjectBoardNote}}with-pinned-note{{end}}"> - {{if .FirstPinnedProjectBoardNote}} + <li class="milestone-card {{if .FirstPinnedBoardNote}}with-pinned-note{{end}}"> + {{if .FirstPinnedBoardNote}} <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict - "BoardNote" .FirstPinnedProjectBoardNote + "ColumnNote" .FirstPinnedBoardNote "root" $ "CanWriteProjects" false) }} diff --git a/templates/projects/milestone_selection.tmpl b/templates/projects/milestone_selection.tmpl index 86639e3ab1e17..0c63f8e44e301 100644 --- a/templates/projects/milestone_selection.tmpl +++ b/templates/projects/milestone_selection.tmpl @@ -33,4 +33,4 @@ {{end}} {{end}} </div> -</div> \ No newline at end of file +</div> diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index d77559cc0e3a3..ae2aaf7fd25ba 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,11 +1,11 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} -{{if gt (len .PinnedProjectBoardNotes) 0}} +{{if gt (len .PinnedColumnNotes) 0}} <div id="pinned-notes" {{if .CanWriteProjects}}class="sortable"{{end}}> - {{range .PinnedProjectBoardNotes}} + {{range .PinnedColumnNotes}} <div class="note-card pinned-card {{if $canWriteProject}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-note="{{.ID}}"> {{template "repo/note" (dict - "BoardNote" . + "ColumnNote" . "root" $ "CanWriteProjects" $canWriteProject "RenderModals" false) @@ -85,7 +85,7 @@ <div class="project-column-header"> <div class="ui large label project-column-title gt-py-2"> <div class="ui small circular grey label project-column-issue-note-count"> - {{.NumIssuesAndProjectBoardNotes ctx}} + {{.NumIssuesAndNotes ctx}} </div> {{.Title}} </div> @@ -243,12 +243,12 @@ <div class="divider"></div> <div class="cards-wrapper"> - {{if or $canWriteProject (index $.ProjectBoardNotesMap .ID)}} + {{if or $canWriteProject (index $.ColumnNotesMap .ID)}} <div class="ui cards note-cards {{if $canWriteProject}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}/note" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="note-board-{{.ID}}"> - {{range (index $.ProjectBoardNotesMap .ID)}} + {{range (index $.ColumnNotesMap .ID)}} <div class="note-card" data-note="{{.ID}}"> {{template "repo/note" (dict - "BoardNote" . + "ColumnNote" . "root" $ "CanWriteProjects" $canWriteProject "RenderModals" true) diff --git a/templates/repo/note.tmpl b/templates/repo/note.tmpl index 1c183a1f21b1d..b27157f00b9c5 100644 --- a/templates/repo/note.tmpl +++ b/templates/repo/note.tmpl @@ -1,4 +1,4 @@ -{{with .BoardNote}} +{{with .ColumnNote}} {{$backendURL := (print $.root.FeedURL "/projects/" .ProjectID "/" .BoardID "/note/")}} {{if not $.root.Repository}} {{$backendURL = (print $.root.FeedURL "/-/projects/" .ProjectID "/" .BoardID "/note/")}} From afcd5d805396bb520bdeb6148f77a06366864830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim-Niclas=20Oelschl=C3=A4ger?= <zokki.softwareschmiede@gmail.com> Date: Fri, 9 Feb 2024 04:29:37 +0100 Subject: [PATCH 46/46] fix: prefill of issue form is now from pinned notes possible --- web_src/js/features/repo-projects.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 5770da55d6fff..41a518cc0d445 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -192,8 +192,11 @@ export function initRepoProject() { $('.create-project-issue-from-note').each(function () { const anchorLink = $(this); const wrapperCard = anchorLink.closest('.note-card'); - const titleInput = wrapperCard.find('[name="title"]'); - const contentTextarea = wrapperCard.find('.markdown-text-editor'); + const showEditModalButton = wrapperCard.find('button[data-modal^="#edit-project-column-note-modal-"]'); + const editModalId = showEditModalButton.data('modal'); + const editModal = $(editModalId); + const titleInput = editModal.find('[name="title"]'); + const contentTextarea = editModal.find('.markdown-text-editor'); anchorLink.on('click', () => { sessionStorage.setItem('board-note-title', titleInput.val());