| @@ -467,12 +467,17 @@ func runWeb(ctx *cli.Context) { | |||||
| m.Combo("/:index/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment) | m.Combo("/:index/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment) | ||||
| m.Group("/:index", func() { | m.Group("/:index", func() { | ||||
| m.Post("/title", repo.UpdateIssueTitle) | |||||
| m.Post("/label", repo.UpdateIssueLabel) | m.Post("/label", repo.UpdateIssueLabel) | ||||
| m.Post("/milestone", repo.UpdateIssueMilestone) | m.Post("/milestone", repo.UpdateIssueMilestone) | ||||
| m.Post("/assignee", repo.UpdateIssueAssignee) | m.Post("/assignee", repo.UpdateIssueAssignee) | ||||
| }, reqRepoAdmin) | }, reqRepoAdmin) | ||||
| m.Group("/:index", func() { | |||||
| m.Post("/title", repo.UpdateIssueTitle) | |||||
| m.Post("/content", repo.UpdateIssueContent) | |||||
| }) | |||||
| }) | }) | ||||
| m.Post("/comments/:id", repo.UpdateCommentContent) | |||||
| m.Group("/labels", func() { | m.Group("/labels", func() { | ||||
| m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | ||||
| m.Post("/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) | m.Post("/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) | ||||
| @@ -17,7 +17,7 @@ import ( | |||||
| "github.com/gogits/gogs/modules/setting" | "github.com/gogits/gogs/modules/setting" | ||||
| ) | ) | ||||
| const APP_VER = "0.6.5.0819 Beta" | |||||
| const APP_VER = "0.6.5.0820 Beta" | |||||
| func init() { | func init() { | ||||
| runtime.GOMAXPROCS(runtime.NumCPU()) | runtime.GOMAXPROCS(runtime.NumCPU()) | ||||
| @@ -258,7 +258,27 @@ func IsErrIssueNotExist(err error) bool { | |||||
| } | } | ||||
| func (err ErrIssueNotExist) Error() string { | func (err ErrIssueNotExist) Error() string { | ||||
| return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %4]", err.ID, err.RepoID, err.Index) | |||||
| return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) | |||||
| } | |||||
| // _________ __ | |||||
| // \_ ___ \ ____ _____ _____ ____ _____/ |_ | |||||
| // / \ \/ / _ \ / \ / \_/ __ \ / \ __\ | |||||
| // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ | | |||||
| // \______ /\____/|__|_| /__|_| /\___ >___| /__| | |||||
| // \/ \/ \/ \/ \/ | |||||
| type ErrCommentNotExist struct { | |||||
| ID int64 | |||||
| } | |||||
| func IsErrCommentNotExist(err error) bool { | |||||
| _, ok := err.(ErrCommentNotExist) | |||||
| return ok | |||||
| } | |||||
| func (err ErrCommentNotExist) Error() string { | |||||
| return fmt.Sprintf("comment does not exist [id: %d]", err.ID) | |||||
| } | } | ||||
| // .____ ___. .__ | // .____ ___. .__ | ||||
| @@ -1363,6 +1363,14 @@ func (c *Comment) AfterSet(colName string, _ xorm.Cell) { | |||||
| } | } | ||||
| } | } | ||||
| func (c *Comment) AfterDelete() { | |||||
| _, err := DeleteAttachmentsByComment(c.ID, true) | |||||
| if err != nil { | |||||
| log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) | |||||
| } | |||||
| } | |||||
| // HashTag returns unique hash tag for comment. | // HashTag returns unique hash tag for comment. | ||||
| func (c *Comment) HashTag() string { | func (c *Comment) HashTag() string { | ||||
| return "issuecomment-" + com.ToStr(c.ID) | return "issuecomment-" + com.ToStr(c.ID) | ||||
| @@ -1473,11 +1481,16 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri | |||||
| return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, attachments) | return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, attachments) | ||||
| } | } | ||||
| // GetCommentById returns the comment with the given id | |||||
| func GetCommentById(id int64) (*Comment, error) { | |||||
| // GetCommentByID returns the comment by given ID. | |||||
| func GetCommentByID(id int64) (*Comment, error) { | |||||
| c := new(Comment) | c := new(Comment) | ||||
| _, err := x.Id(id).Get(c) | |||||
| return c, err | |||||
| has, err := x.Id(id).Get(c) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } else if !has { | |||||
| return nil, ErrCommentNotExist{id} | |||||
| } | |||||
| return c, nil | |||||
| } | } | ||||
| // GetCommentsByIssueID returns all comments of issue by given ID. | // GetCommentsByIssueID returns all comments of issue by given ID. | ||||
| @@ -1486,12 +1499,10 @@ func GetCommentsByIssueID(issueID int64) ([]*Comment, error) { | |||||
| return comments, x.Where("issue_id=?", issueID).Asc("created").Find(&comments) | return comments, x.Where("issue_id=?", issueID).Asc("created").Find(&comments) | ||||
| } | } | ||||
| func (c *Comment) AfterDelete() { | |||||
| _, err := DeleteAttachmentsByComment(c.ID, true) | |||||
| if err != nil { | |||||
| log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) | |||||
| } | |||||
| // UpdateComment updates information of comment. | |||||
| func UpdateComment(c *Comment) error { | |||||
| _, err := x.Id(c.ID).AllCols().Update(c) | |||||
| return err | |||||
| } | } | ||||
| // Attachment represent a attachment of issue/comment/release. | // Attachment represent a attachment of issue/comment/release. | ||||
| @@ -2,26 +2,30 @@ | |||||
| var csrf; | var csrf; | ||||
| function initCommentForm() { | |||||
| if ($('.comment.form').length == 0) { | |||||
| return | |||||
| } | |||||
| var $form = $('.comment.form'); | |||||
| $form.find('.tabular.menu .item').tab(); | |||||
| $form.find('.tabular.menu .item[data-tab="preview"]').click(function () { | |||||
| function initCommentPreviewTab($form) { | |||||
| var $tab_menu = $form.find('.tabular.menu'); | |||||
| $tab_menu.find('.item').tab(); | |||||
| $tab_menu.find('.item[data-tab="' + $tab_menu.data('preview') + '"]').click(function () { | |||||
| var $this = $(this); | var $this = $(this); | ||||
| $.post($this.data('url'), { | $.post($this.data('url'), { | ||||
| "_csrf": csrf, | "_csrf": csrf, | ||||
| "mode": "gfm", | "mode": "gfm", | ||||
| "context": $this.data('context'), | "context": $this.data('context'), | ||||
| "text": $form.find('.tab.segment[data-tab="write"] textarea').val() | |||||
| "text": $form.find('.tab.segment[data-tab="' + $tab_menu.data('write') + '"] textarea').val() | |||||
| }, | }, | ||||
| function (data) { | function (data) { | ||||
| $form.find('.tab.segment[data-tab="preview"]').html(data); | |||||
| $form.find('.tab.segment[data-tab="' + $tab_menu.data('preview') + '"]').html(data); | |||||
| } | } | ||||
| ); | ); | ||||
| }); | }); | ||||
| } | |||||
| function initCommentForm() { | |||||
| if ($('.comment.form').length == 0) { | |||||
| return | |||||
| } | |||||
| initCommentPreviewTab($('.comment.form')); | |||||
| // Labels | // Labels | ||||
| var $list = $('.ui.labels.list'); | var $list = $('.ui.labels.list'); | ||||
| @@ -260,6 +264,66 @@ function initRepository() { | |||||
| return false; | return false; | ||||
| }); | }); | ||||
| // Edit issue or comment content | |||||
| $('.edit-content').click(function () { | |||||
| var $segment = $(this).parent().parent().next(); | |||||
| var $edit_content_zone = $segment.find('.edit-content-zone'); | |||||
| var $render_content = $segment.find('.render-content'); | |||||
| var $raw_content = $segment.find('.raw-content'); | |||||
| var $textarea; | |||||
| // Setup new form | |||||
| if ($edit_content_zone.html().length == 0) { | |||||
| $edit_content_zone.html($('#edit-content-form').html()); | |||||
| $textarea = $segment.find('textarea'); | |||||
| // Give new write/preview data-tab name to distinguish from others | |||||
| var $edit_content_form = $edit_content_zone.find('.ui.comment.form'); | |||||
| var $tabular_menu = $edit_content_form.find('.tabular.menu'); | |||||
| $tabular_menu.attr('data-write', $edit_content_zone.data('write')); | |||||
| $tabular_menu.attr('data-preview', $edit_content_zone.data('preview')); | |||||
| $tabular_menu.find('.write.item').attr('data-tab', $edit_content_zone.data('write')); | |||||
| $tabular_menu.find('.preview.item').attr('data-tab', $edit_content_zone.data('preview')); | |||||
| $edit_content_form.find('.write.segment').attr('data-tab', $edit_content_zone.data('write')); | |||||
| $edit_content_form.find('.preview.segment').attr('data-tab', $edit_content_zone.data('preview')); | |||||
| initCommentPreviewTab($edit_content_form); | |||||
| $edit_content_zone.find('.cancel.button').click(function () { | |||||
| $render_content.show(); | |||||
| $edit_content_zone.hide(); | |||||
| }); | |||||
| $edit_content_zone.find('.save.button').click(function () { | |||||
| $render_content.show(); | |||||
| $edit_content_zone.hide(); | |||||
| $.post($edit_content_zone.data('update-url'), { | |||||
| "_csrf": csrf, | |||||
| "content": $textarea.val(), | |||||
| "context": $edit_content_zone.data('context') | |||||
| }, | |||||
| function (data) { | |||||
| if (data.length == 0) { | |||||
| $render_content.html($('#no-content').html()); | |||||
| } else { | |||||
| $render_content.html(data.content); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| } else { | |||||
| $textarea = $segment.find('textarea'); | |||||
| } | |||||
| // Show write/preview tab and copy raw content as needed | |||||
| $edit_content_zone.show(); | |||||
| $render_content.hide(); | |||||
| if ($textarea.val().length == 0) { | |||||
| $textarea.val($raw_content.text()); | |||||
| } | |||||
| $textarea.focus(); | |||||
| return false; | |||||
| }); | |||||
| // Change status | // Change status | ||||
| var $status_btn = $('#status-button'); | var $status_btn = $('#status-button'); | ||||
| $('#content').keyup(function () { | $('#content').keyup(function () { | ||||
| @@ -253,6 +253,10 @@ | |||||
| height: 200px; | height: 200px; | ||||
| } | } | ||||
| } | } | ||||
| .edit.buttons { | |||||
| margin-top: 10px; | |||||
| } | |||||
| } | } | ||||
| .event { | .event { | ||||
| position: relative; | position: relative; | ||||
| @@ -543,19 +543,16 @@ func UpdateIssueTitle(ctx *middleware.Context) { | |||||
| return | return | ||||
| } | } | ||||
| if !ctx.IsSigned || ctx.User.Id != issue.PosterID || !ctx.Repo.IsAdmin() { | |||||
| if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) { | |||||
| ctx.Error(403) | ctx.Error(403) | ||||
| return | return | ||||
| } | } | ||||
| title := ctx.Query("title") | |||||
| if len(title) == 0 { | |||||
| ctx.JSON(200, map[string]interface{}{ | |||||
| "title": issue.Name, | |||||
| }) | |||||
| issue.Name = ctx.Query("title") | |||||
| if len(issue.Name) == 0 { | |||||
| ctx.Error(204) | |||||
| return | return | ||||
| } | } | ||||
| issue.Name = title | |||||
| if err := models.UpdateIssue(issue); err != nil { | if err := models.UpdateIssue(issue); err != nil { | ||||
| ctx.Handle(500, "UpdateIssue", err) | ctx.Handle(500, "UpdateIssue", err) | ||||
| @@ -567,6 +564,28 @@ func UpdateIssueTitle(ctx *middleware.Context) { | |||||
| }) | }) | ||||
| } | } | ||||
| func UpdateIssueContent(ctx *middleware.Context) { | |||||
| issue := getActionIssue(ctx) | |||||
| if ctx.Written() { | |||||
| return | |||||
| } | |||||
| if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) { | |||||
| ctx.Error(403) | |||||
| return | |||||
| } | |||||
| issue.Content = ctx.Query("content") | |||||
| if err := models.UpdateIssue(issue); err != nil { | |||||
| ctx.Handle(500, "UpdateIssue", err) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]interface{}{ | |||||
| "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Query("context"))), | |||||
| }) | |||||
| } | |||||
| func UpdateIssueLabel(ctx *middleware.Context) { | func UpdateIssueLabel(ctx *middleware.Context) { | ||||
| issue := getActionIssue(ctx) | issue := getActionIssue(ctx) | ||||
| if ctx.Written() { | if ctx.Written() { | ||||
| @@ -748,6 +767,42 @@ func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) { | |||||
| ctx.Redirect(fmt.Sprintf("%s/issues/%d#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag())) | ctx.Redirect(fmt.Sprintf("%s/issues/%d#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag())) | ||||
| } | } | ||||
| func UpdateCommentContent(ctx *middleware.Context) { | |||||
| comment, err := models.GetCommentByID(ctx.ParamsInt64(":id")) | |||||
| if err != nil { | |||||
| if models.IsErrCommentNotExist(err) { | |||||
| ctx.Error(404, "GetCommentByID") | |||||
| } else { | |||||
| ctx.Handle(500, "GetCommentByID", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| if !ctx.IsSigned || (ctx.User.Id != comment.PosterID && !ctx.Repo.IsAdmin()) { | |||||
| ctx.Error(403) | |||||
| return | |||||
| } else if comment.Type != models.COMMENT_TYPE_COMMENT { | |||||
| ctx.Error(204) | |||||
| return | |||||
| } | |||||
| comment.Content = ctx.Query("content") | |||||
| if len(comment.Content) == 0 { | |||||
| ctx.JSON(200, map[string]interface{}{ | |||||
| "content": "", | |||||
| }) | |||||
| return | |||||
| } | |||||
| if err := models.UpdateComment(comment); err != nil { | |||||
| ctx.Handle(500, "UpdateComment", err) | |||||
| return | |||||
| } | |||||
| ctx.JSON(200, map[string]interface{}{ | |||||
| "content": string(base.RenderMarkdown([]byte(comment.Content), ctx.Query("context"))), | |||||
| }) | |||||
| } | |||||
| func Labels(ctx *middleware.Context) { | func Labels(ctx *middleware.Context) { | ||||
| ctx.Data["Title"] = ctx.Tr("repo.labels") | ctx.Data["Title"] = ctx.Tr("repo.labels") | ||||
| ctx.Data["PageIsLabels"] = true | ctx.Data["PageIsLabels"] = true | ||||
| @@ -1 +1 @@ | |||||
| 0.6.5.0819 Beta | |||||
| 0.6.5.0820 Beta | |||||
| @@ -1,7 +1,7 @@ | |||||
| <div class="field"> | <div class="field"> | ||||
| <div class="ui top attached tabular menu"> | |||||
| <div class="ui top attached tabular menu" data-write="write" data-preview="preview"> | |||||
| <a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</a> | <a class="active item" data-tab="write">{{.i18n.Tr "repo.release.write"}}</a> | ||||
| <a class="item" data-tab="preview" data-url="/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a> | |||||
| <a class="item" data-tab="preview" data-url="/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a> | |||||
| </div> | </div> | ||||
| <div class="ui bottom attached active tab segment" data-tab="write"> | <div class="ui bottom attached active tab segment" data-tab="write"> | ||||
| <textarea id="content" name="content"></textarea> | <textarea id="content" name="content"></textarea> | ||||
| @@ -50,16 +50,20 @@ | |||||
| <span class="text grey"><a {{if gt .Issue.Poster.Id 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.Name}}</a> {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}}</span> | <span class="text grey"><a {{if gt .Issue.Poster.Id 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.Name}}</a> {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}}</span> | ||||
| <div class="ui right actions"> | <div class="ui right actions"> | ||||
| {{if .IsIssueOwner}} | {{if .IsIssueOwner}} | ||||
| <a class="item" href="#"><i class="octicon octicon-pencil"></i></a> | |||||
| <a class="edit-content item" href="#"><i class="octicon octicon-pencil"></i></a> | |||||
| {{end}} | {{end}} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="ui attached segment markdown"> | |||||
| {{if .Issue.RenderedContent}} | |||||
| {{.Issue.RenderedContent|Str2html}} | |||||
| {{else}} | |||||
| <span class="no-content">{{.i18n.Tr "repo.issues.no_content"}}</span> | |||||
| {{end}} | |||||
| <div class="ui attached segment"> | |||||
| <div class="render-content markdown"> | |||||
| {{if .Issue.RenderedContent}} | |||||
| {{.Issue.RenderedContent|Str2html}} | |||||
| {{else}} | |||||
| <span class="no-content">{{.i18n.Tr "repo.issues.no_content"}}</span> | |||||
| {{end}} | |||||
| </div> | |||||
| <div class="raw-content hide">{{.Issue.Content}}</div> | |||||
| <div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{.Link}}/content" data-context="{{.RepoLink}}"></div> | |||||
| </div> | </div> | ||||
| {{if .Issue.Attachments}} | {{if .Issue.Attachments}} | ||||
| <div class="ui bottom attached segment"> | <div class="ui bottom attached segment"> | ||||
| @@ -98,16 +102,20 @@ | |||||
| </div> | </div> | ||||
| {{end}} | {{end}} | ||||
| {{if or $.IsRepositoryAdmin (eq .Poster.Id $.SignedUserID)}} | {{if or $.IsRepositoryAdmin (eq .Poster.Id $.SignedUserID)}} | ||||
| <a class="item" href="#"><i class="octicon octicon-pencil"></i></a> | |||||
| <a class="edit-content item" href="#" data-type="comment"><i class="octicon octicon-pencil"></i></a> | |||||
| {{end}} | {{end}} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="ui attached segment markdown"> | |||||
| {{if .RenderedContent}} | |||||
| {{.RenderedContent|Str2html}} | |||||
| {{else}} | |||||
| <span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span> | |||||
| {{end}} | |||||
| <div class="ui attached segment"> | |||||
| <div class="render-content markdown"> | |||||
| {{if .RenderedContent}} | |||||
| {{.RenderedContent|Str2html}} | |||||
| {{else}} | |||||
| <span class="no-content">{{$.i18n.Tr "repo.issues.no_content"}}</span> | |||||
| {{end}} | |||||
| </div> | |||||
| <div class="raw-content hide">{{.Content}}</div> | |||||
| <div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}"></div> | |||||
| </div> | </div> | ||||
| {{if .Attachments}} | {{if .Attachments}} | ||||
| <div class="ui bottom attached segment"> | <div class="ui bottom attached segment"> | ||||
| @@ -264,4 +272,27 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="hide" id="edit-content-form"> | |||||
| <div class="ui comment form"> | |||||
| <div class="ui top attached tabular menu"> | |||||
| <a class="active write item">{{$.i18n.Tr "repo.release.write"}}</a> | |||||
| <a class="preview item" data-url="/api/v1/markdown" data-context="{{$.RepoLink}}">{{$.i18n.Tr "repo.release.preview"}}</a> | |||||
| </div> | |||||
| <div class="ui bottom attached active write tab segment"> | |||||
| <textarea id="content" name="content"></textarea> | |||||
| </div> | |||||
| <div class="ui bottom attached tab preview segment markdown"> | |||||
| {{$.i18n.Tr "repo.release.loading"}} | |||||
| </div> | |||||
| <div class="text right edit buttons"> | |||||
| <div class="ui basic blue cancel button">{{.i18n.Tr "repo.issues.cancel"}}</div> | |||||
| <div class="ui green save button">{{.i18n.Tr "repo.issues.save"}}</div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="hide" id="no-content"> | |||||
| <span class="no-content">{{.i18n.Tr "repo.issues.no_content"}}</span> | |||||
| </div> | </div> | ||||