Compare commits

...

12 Commits

Author SHA1 Message Date
Zettat123 1b574ee75d
Merge 802efa342d into 8de2992ffb 2024-04-27 17:03:33 +02:00
wxiaoguang 8de2992ffb
Make Ctrl+Enter work for issue/comment edit (#30720)
Fix #30710
2024-04-27 14:32:00 +00:00
wxiaoguang 6d2a307ad8
Rename migration package name for 1.22-rc1 (#30730)
Ref: Propose to restart 1.22 release #30501
2024-04-27 14:02:07 +00:00
silverwind b93c87b6fe
Issue card improvements (#30687)
Fixes https://github.com/go-gitea/gitea/issues/30682 and does a few
improvements:

- Use gap instead of margin/padding
- Don't render empty image div
- Remove `right floated` class that did nothing

<img width="406" alt="Screenshot 2024-04-24 at 20 21 20"
src="https://github.com/go-gitea/gitea/assets/115237/2fa88707-c2c4-40df-aee7-a684c3097ed0">

---------

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
2024-04-27 13:35:26 +00:00
Yarden Shoham 51c28d9683
Don't show loading indicators when refreshing the system status (#30712)
Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2024-04-27 13:05:06 +00:00
wxiaoguang d3cdef88ad
Add some tests to clarify the "must-change-password" behavior (#30693)
Follow  #30472:

When a user is created by command line `./gitea admin user create`:

Old behavior before #30472: the first user (admin or non-admin) doesn't
need to change password.

Revert to the old behavior before #30472
2024-04-27 12:23:37 +00:00
Kemal Zebari dd301cae1c
Prevent allow/reject reviews on merged/closed PRs (#30686)
Resolves #30675.
2024-04-27 11:55:03 +00:00
silverwind 238eb3ff9f
Update JS dependencies (#30713)
- Update all JS dependencies
- Remove
[now-unnecessary](https://github.com/microsoft/monaco-editor/issues/4325)
monaco workaround
- Update stylelint config for new rule
- Tested Monaco, Swagger UI, Mermaid
2024-04-27 11:28:28 +00:00
silverwind b2abac5e5f
Improve diff stats bar (#30669)
Minor tweaks:

- Remove unnecessary `item` class which was causing unwanted padding to
be added.
- Add some padding and prevent wrapping so it looks better on mobile.
- Increase width by 4px.

<img width="116" alt="Screenshot 2024-04-24 at 00 15 07"
src="https://github.com/go-gitea/gitea/assets/115237/1f1cf54c-8053-4297-b309-71d9c2ceb9ee">
<img width="441" alt="Screenshot 2024-04-24 at 00 14 57"
src="https://github.com/go-gitea/gitea/assets/115237/2f3a33dc-edad-4b97-b64c-6812aae513cb">
2024-04-27 11:22:55 +00:00
Chongyi Zheng 4ae6b1a553
Remove unused parameter for some functions in `services/mirror` (#30724)
Suggested by gopls `unusedparams`
2024-04-27 10:44:49 +00:00
Zettat123 802efa342d remove stopTimerIfAvailable 2024-04-26 17:07:09 +08:00
Zettat123 078d120ac5 call notifier after merging a pr 2024-04-26 16:47:06 +08:00
48 changed files with 628 additions and 471 deletions

View File

@ -35,7 +35,7 @@ var microcmdUserChangePassword = &cli.Command{
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password",
Usage: "User must change password (can be disabled by --must-change-password=false)",
Value: true,
},
},

View File

@ -4,6 +4,7 @@
package cmd
import (
"context"
"errors"
"fmt"
@ -48,7 +49,7 @@ var microcmdUserCreate = &cli.Command{
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "Set to false to prevent forcing the user to change their password after initial login",
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
DisableDefaultText: true,
},
&cli.IntFlag{
@ -91,11 +92,16 @@ func runCreateUser(c *cli.Context) error {
_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
}
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
ctx := c.Context
if !setting.IsInTesting {
// FIXME: need to refactor the "installSignals/initDB" related code later
// it doesn't make sense to call it in (almost) every command action function
var cancel context.CancelFunc
ctx, cancel = installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
}
var password string
@ -123,8 +129,8 @@ func runCreateUser(c *cli.Context) error {
if err != nil {
return fmt.Errorf("IsTableNotEmpty: %w", err)
}
if !hasUserRecord && isAdmin {
// if this is the first admin being created, don't force to change password (keep the old behavior)
if !hasUserRecord {
// if this is the first one being created, don't force to change password (keep the old behavior)
mustChangePassword = false
}
}

View File

@ -0,0 +1,44 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{})
reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
}
type createCheck struct{ IsAdmin, MustChangePassword bool }
createUser := func(name, args string) createCheck {
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return createCheck{u.IsAdmin, u.MustChangePassword}
}
reset()
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
}

View File

@ -112,13 +112,18 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
}
}
func NewMainApp(version, versionExtra string) *cli.App {
type AppVersion struct {
Version string
Extra string
}
func NewMainApp(appVer AppVersion) *cli.App {
app := cli.NewApp()
app.Name = "Gitea"
app.HelpName = "gitea"
app.Usage = "A painless self-hosted Git service"
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = version + versionExtra
app.Version = appVer.Version + appVer.Extra
app.EnableBashCompletion = true
// these sub-commands need to use config file

View File

@ -28,7 +28,7 @@ func makePathOutput(workPath, customPath, customConf string) string {
}
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
app := NewMainApp("version", "version-extra")
app := NewMainApp(AppVersion{})
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
prepareSubcommandWithConfig(testCmd, appGlobalFlags())
app.Commands = append(app.Commands, testCmd)

View File

@ -42,7 +42,7 @@ func main() {
log.GetManager().Close()
os.Exit(code)
}
app := cmd.NewMainApp(Version, formatBuiltWith())
app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()})
_ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp
log.GetManager().Close()
}

View File

@ -49,7 +49,7 @@ func TestCreateIssueDependency(t *testing.T) {
assert.False(t, left)
// Close #2 and check again
_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true)
_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true, false)
assert.NoError(t, err)
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)

View File

@ -119,7 +119,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
}
// ChangeIssueStatus changes issue status to open or closed.
func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) {
func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
if err := issue.LoadRepo(ctx); err != nil {
return nil, err
}
@ -127,7 +127,7 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User,
return nil, err
}
return changeIssueStatus(ctx, issue, doer, isClosed, false)
return changeIssueStatus(ctx, issue, doer, isClosed, isMergePull)
}
// ChangeIssueTitle changes the title of this issue, as the given user.

View File

@ -98,7 +98,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) {
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true)
_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true, false)
assert.NoError(t, err)
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))

View File

@ -499,10 +499,6 @@ func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
return false, err
}
if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil {
return false, fmt.Errorf("Issue.changeStatus: %w", err)
}
// reset the conflicted files as there cannot be any if we're merged
pr.ConflictedFiles = []string{}

View File

@ -21,7 +21,6 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -574,18 +573,20 @@ var migrations = []Migration{
// v293 -> v294
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
// Gitea 1.22.0 ends at 294
// Gitea 1.22.0-rc0 ends at 294
// v294 -> v295
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
NewMigration("Add unique index for project issue table", v1_22.AddUniqueIndexForProjectIssue),
// v295 -> v296
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary),
NewMigration("Add commit status summary table", v1_22.AddCommitStatusSummary),
// v296 -> v297
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2),
NewMigration("Add missing field of commit status summary table", v1_22.AddCommitStatusSummary2),
// v297 -> v298
NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode),
NewMigration("Add everyone_access_mode for repo_unit", v1_22.AddRepoUnitEveryoneAccessMode),
// v298 -> v299
NewMigration("Drop wrongly created table o_auth2_application", v1_23.DropWronglyCreatedTable),
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
}
// GetCurrentDBVersion returns the current db version

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import (
"fmt"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import (
"slices"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import "xorm.io/xorm"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import "xorm.io/xorm"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import (
"code.gitea.io/gitea/models/perm"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
package v1_22 //nolint
import "xorm.io/xorm"

View File

@ -6,7 +6,6 @@ package unittest
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
@ -18,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage"
@ -46,6 +46,14 @@ func fatalTestError(fmtStr string, args ...any) {
// InitSettings initializes config provider and load common settings for tests
func InitSettings() {
setting.IsInTesting = true
log.OsExiter = func(code int) {
if code != 0 {
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
panic(fmt.Errorf("non-zero exit code during testing: %d", code))
}
os.Exit(0)
}
if setting.CustomConf == "" {
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
_ = os.Remove(setting.CustomConf)
@ -54,7 +62,7 @@ func InitSettings() {
setting.LoadCommonSettings()
if err := setting.PrepareAppDataPath(); err != nil {
log.Fatalf("Can not prepare APP_DATA_PATH: %v", err)
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
}
// register the dummy hash algorithm function used in the test fixtures
_ = hash.Register("dummy", hash.NewDummyHasher)

View File

@ -57,11 +57,13 @@ func Critical(format string, v ...any) {
Log(1, ERROR, format, v...)
}
var OsExiter = os.Exit
// Fatal records fatal log and exit process
func Fatal(format string, v ...any) {
Log(1, FATAL, format, v...)
GetManager().Close()
os.Exit(1)
OsExiter(1)
}
func GetLogger(name string) Logger {

665
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
"node": ">= 18.0.0"
},
"dependencies": {
"@citation-js/core": "0.7.9",
"@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.9",
"@citation-js/core": "0.7.11",
"@citation-js/plugin-bibtex": "0.7.11",
"@citation-js/plugin-csl": "0.7.11",
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.0",
@ -33,17 +33,17 @@
"katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.9.0",
"mini-css-extract-plugin": "2.8.1",
"mini-css-extract-plugin": "2.9.0",
"minimatch": "9.0.4",
"monaco-editor": "0.47.0",
"monaco-editor": "0.48.0",
"monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0",
"postcss": "8.4.38",
"postcss-loader": "8.1.1",
"postcss-nesting": "12.1.1",
"postcss-nesting": "12.1.2",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2",
"swagger-ui-dist": "5.15.1",
"swagger-ui-dist": "5.17.2",
"tailwindcss": "3.4.3",
"temporal-polyfill": "0.2.4",
"throttle-debounce": "5.0.0",
@ -53,7 +53,7 @@
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2",
"vue": "3.4.21",
"vue": "3.4.25",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1",
"vue-loader": "17.4.2",
@ -66,7 +66,7 @@
"@eslint-community/eslint-plugin-eslint-comments": "4.3.0",
"@playwright/test": "1.43.1",
"@stoplight/spectral-cli": "6.11.1",
"@stylistic/eslint-plugin-js": "1.7.0",
"@stylistic/eslint-plugin-js": "1.7.2",
"@stylistic/stylelint-plugin": "2.1.1",
"@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0",
@ -81,20 +81,20 @@
"eslint-plugin-unicorn": "52.0.0",
"eslint-plugin-vitest": "0.4.1",
"eslint-plugin-vitest-globals": "1.5.0",
"eslint-plugin-vue": "9.24.1",
"eslint-plugin-vue": "9.25.0",
"eslint-plugin-vue-scoped-css": "2.8.0",
"eslint-plugin-wc": "2.1.0",
"happy-dom": "14.7.1",
"markdownlint-cli": "0.39.0",
"postcss-html": "1.6.0",
"stylelint": "16.3.1",
"stylelint": "16.4.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0",
"updates": "16.0.1",
"vite-string-plugin": "1.1.5",
"vitest": "1.5.0"
"vite-string-plugin": "1.2.0",
"vitest": "1.5.2"
},
"browserslist": [
"defaults"

View File

@ -720,7 +720,7 @@ func CreateIssue(ctx *context.APIContext) {
}
if form.Closed {
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil {
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true, false); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
return

View File

@ -4,6 +4,7 @@
package repo
import (
"errors"
"fmt"
"net/http"
"strings"
@ -372,7 +373,11 @@ func CreatePullReview(ctx *context.APIContext) {
// create review and associate all pending review comments
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
if err != nil {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
}
return
}
@ -460,7 +465,11 @@ func SubmitPullReview(ctx *context.APIContext) {
// create review and associate all pending review comments
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
if err != nil {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
}
return
}

View File

@ -242,7 +242,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
}
var isShowClosed optional.Option[bool]
switch ctx.FormString("state") {
stateVal := ctx.FormString("state")
switch stateVal {
case "closed":
isShowClosed = optional.Some(true)
case "all":
@ -2924,7 +2925,7 @@ func UpdateIssueStatus(ctx *context.Context) {
continue
}
if issue.IsClosed != isClosed {
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed, false); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.JSON(http.StatusPreconditionFailed, map[string]any{
"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
@ -3068,7 +3069,7 @@ func NewComment(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
} else {
isClosed := form.Status == "close"
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed, false); err != nil {
log.Error("ChangeStatus: %v", err)
if issues_model.IsErrDependenciesLeft(err) {
@ -3079,13 +3080,6 @@ func NewComment(ctx *context.Context) {
}
return
}
} else {
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
ctx.ServerError("CreateOrStopIssueStopwatch", err)
return
}
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
}
}
}

View File

@ -1153,13 +1153,6 @@ func MergePullRequest(ctx *context.Context) {
}
log.Trace("Pull request merged: %d", pr.ID)
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
ctx.ServerError("stopTimerIfAvailable", err)
return
}
log.Trace("Pull request merged: %d", pr.ID)
if form.DeleteBranchAfterMerge {
// Don't cleanup when other pr use this branch as head branch
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
@ -1209,16 +1202,6 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
}
func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error {
if issues_model.StopwatchExists(ctx, user.ID, issue.ID) {
if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil {
return err
}
}
return nil
}
// CompareAndPullRequestPost response for creating pull request
func CompareAndPullRequestPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm)

View File

@ -264,6 +264,8 @@ func SubmitReview(ctx *context.Context) {
if issues_model.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
} else if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Status(http.StatusUnprocessableEntity)
} else {
ctx.ServerError("SubmitReview", err)
}

View File

@ -196,7 +196,7 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
}
if isClosed != refIssue.IsClosed {
refIssue.Repo = refRepo
if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil {
if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed, false); err != nil {
return err
}
}

View File

@ -13,8 +13,8 @@ import (
)
// ChangeStatus changes issue status to open or closed.
func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error {
comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed)
func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed, isMergePull bool) error {
comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed, isMergePull)
if err != nil {
if issues_model.IsErrDependenciesLeft(err) && closed {
if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {

View File

@ -40,7 +40,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
}
log.Trace("Doing: Update")
handler := func(idx int, bean any) error {
handler := func(bean any) error {
var repo *repo_model.Repository
var mirrorType SyncType
var referenceID int64
@ -91,7 +91,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
pullMirrorsRequested := 0
if pullLimit != 0 {
if err := repo_model.MirrorsIterate(ctx, pullLimit, func(idx int, bean any) error {
if err := handler(idx, bean); err != nil {
if err := handler(bean); err != nil {
return err
}
pullMirrorsRequested++
@ -105,7 +105,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
pushMirrorsRequested := 0
if pushLimit != 0 {
if err := repo_model.PushMirrorsIterate(ctx, pushLimit, func(idx int, bean any) error {
if err := handler(idx, bean); err != nil {
if err := handler(bean); err != nil {
return err
}
pushMirrorsRequested++

View File

@ -466,7 +466,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
if len(results) > 0 {
if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok {
if ok := checkAndUpdateEmptyRepository(ctx, m, results); !ok {
log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err)
return false
}
@ -564,7 +564,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
return true
}
func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool {
func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, results []*mirrorSyncResult) bool {
if !m.Repo.IsEmpty {
return true
}

View File

@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/timeutil"
asymkey_service "code.gitea.io/gitea/services/asymkey"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
)
@ -296,6 +297,10 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
return false
} else if !merged {
return false
} else {
if err = issue_service.ChangeStatus(ctx, pr.Issue, pr.Merger, pr.MergedCommitID, true, true); err != nil {
log.Error("ChangeStatus %-v: %v", pr, err)
}
}
notify_service.MergePullRequest(ctx, merger, pr)

View File

@ -193,8 +193,12 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
pr.Merger = doer
pr.MergerID = doer.ID
if _, err := pr.SetMerged(ctx); err != nil {
if merged, err := pr.SetMerged(ctx); err != nil {
log.Error("SetMerged %-v: %v", pr, err)
} else if merged {
if err = issue_service.ChangeStatus(ctx, pr.Issue, pr.Merger, pr.MergedCommitID, true, true); err != nil {
log.Error("ChangeStatus %-v: %v", pr, err)
}
}
if err := pr.LoadIssue(ctx); err != nil {
@ -233,7 +237,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
}
isClosed := ref.RefAction == references.XRefActionCloses
if isClosed != ref.Issue.IsClosed {
if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil {
if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed, false); err != nil {
// Allow ErrDependenciesLeft
if !issues_model.IsErrDependenciesLeft(err) {
return err
@ -530,6 +534,10 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
return err
} else if !merged {
return fmt.Errorf("SetMerged failed")
} else {
if err = issue_service.ChangeStatus(ctx, pr.Issue, pr.Merger, pr.MergedCommitID, true, true); err != nil {
log.Error("ChangeStatus %-v: %v", pr, err)
}
}
return nil
}); err != nil {

View File

@ -633,7 +633,7 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64,
var errs errlist
for _, pr := range prs {
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true, false); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
errs = append(errs, err)
}
}
@ -667,7 +667,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
if pr.BaseRepoID == repo.ID {
continue
}
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) {
if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true, false); err != nil && !issues_model.IsErrPullWasClosed(err) {
errs = append(errs, err)
}
}

View File

@ -6,6 +6,7 @@ package pull
import (
"context"
"errors"
"fmt"
"io"
"regexp"
@ -43,6 +44,9 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
// checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@ -293,6 +297,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
stale = false
} else {
if issue.IsClosed {
return nil, nil, ErrSubmitReviewOnClosedPR
}
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
return nil, nil, err

View File

@ -191,8 +191,9 @@ export default {
'no-invalid-double-slash-comments': true,
'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
'no-irregular-whitespace': true,
'no-unknown-animations': null,
'no-unknown-custom-properties': null,
'no-unknown-animations': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-media': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-properties': null, // disabled until stylelint supports multi-file linting
'number-max-precision': null,
'plugin/declaration-block-no-ignored-properties': true,
'property-allowed-list': null,

View File

@ -76,7 +76,8 @@
{{ctx.Locale.Tr "admin.dashboard.system_status"}}
</h4>
{{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".divider" class="ui attached table segment">
<div class="no-loading-indicator tw-hidden"></div>
<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".no-loading-indicator" class="ui attached table segment">
{{template "admin/system_status" .}}
</div>
</div>

View File

@ -235,7 +235,7 @@
{{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}}
<template id="issue-comment-editor-template">
<div class="ui comment form">
<div class="ui form comment">
{{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print $.Repository.Link "/markup")
"MarkdownPreviewContext" $.RepoLink
@ -249,7 +249,7 @@
{{end}}
<div class="text right edit buttons">
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui primary save button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>
</div>
</template>

View File

@ -30,20 +30,24 @@
{{end}}
<div class="divider"></div>
{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
{{if $showSelfTooltip}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
</span>
{{else}}
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
{{if not $.Issue.IsClosed}}
{{if $showSelfTooltip}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
</span>
{{else}}
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
{{end}}
{{end}}
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
{{if $showSelfTooltip}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
</span>
{{else}}
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
{{if not $.Issue.IsClosed}}
{{if $showSelfTooltip}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
</span>
{{else}}
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
{{end}}
{{end}}
</form>
</div>

View File

@ -1,13 +1,16 @@
{{with .Issue}}
{{if eq $.Page.Project.CardType 1}}{{/* Images and Text*/}}
{{$attachments := index $.Page.issuesAttachmentMap .ID}}
{{if $attachments}}
<div class="card-attachment-images">
{{range (index $.Page.issuesAttachmentMap .ID)}}
{{range $attachments}}
<img src="{{.DownloadURL}}" alt="{{.Name}}" />
{{end}}
</div>
{{end}}
{{end}}
<div class="content tw-p-0 tw-w-full">
<div class="tw-flex tw-items-start">
<div class="content tw-w-full">
<div class="tw-flex tw-items-start tw-gap-[5px]">
<div class="issue-card-icon">
{{template "shared/issueicon" .}}
</div>
@ -18,7 +21,7 @@
</a>
{{end}}
</div>
<div class="meta tw-my-1">
<div class="meta">
<span class="text light grey muted-links">
{{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}}
{{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}}
@ -59,13 +62,15 @@
</div>
{{if or .Labels .Assignees}}
<div class="extra content labels-list tw-p-0 tw-pt-1">
{{range .Labels}}
<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
{{end}}
<div class="right floated">
<div class="tw-flex tw-justify-between">
<div class="labels-list tw-flex-1">
{{range .Labels}}
<a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a>
{{end}}
</div>
<div class="tw-flex tw-flex-wrap tw-content-start tw-gap-1">
{{range .Assignees}}
<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini tw-mr-2"}}</a>
<a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28}}</a>
{{end}}
</div>
</div>

View File

@ -146,7 +146,7 @@
</div>
<template id="issue-comment-editor-template">
<div class="ui comment form">
<div class="ui form comment">
<div class="field">
{{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup")
@ -164,8 +164,8 @@
<div class="field">
<div class="text right edit">
<button class="ui basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui primary save button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div>
</div>
</div>

View File

@ -16,7 +16,7 @@
<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
</a>
{{if or .Diff.TotalAddition .Diff.TotalDeletion}}
<span class="item tw-ml-auto tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
<span class="tw-ml-auto tw-pl-3 tw-whitespace-nowrap tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
<span class="diff-stats-bar">
<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>

View File

@ -5,12 +5,15 @@ package integration
import (
"net/http"
"net/http/httptest"
"net/url"
"path"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@ -176,3 +179,82 @@ func TestPullView_CodeOwner(t *testing.T) {
})
})
}
func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
user1Session := loginUser(t, "user1")
user2Session := loginUser(t, "user2")
// Have user1 create a fork of repo1.
testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
// Create a merged PR (made by user1) in the upstream repo1.
testEditFile(t, user1Session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
// Grab the CSRF token.
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Submit an approve review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
// Submit a reject review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
})
t.Run("Submit approve/reject review on closed PR", func(t *testing.T) {
// Created a closed PR (made by user1) in the upstream repo1.
testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n")
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
// Grab the CSRF token.
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Submit an approve review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
// Submit a reject review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
})
})
}
func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
options := map[string]string{
"_csrf": csrf,
"commit_id": "",
"content": "test",
"type": reviewType,
}
submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit")
req := NewRequestWithValues(t, "POST", submitURL, options)
return session.MakeRequest(t, req, expectedSubmitStatus)
}
func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
closeURL := path.Join(owner, repo, "issues", issueNumber, "comments")
options := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"status": "close",
}
req = NewRequestWithValues(t, "POST", closeURL, options)
return session.MakeRequest(t, req, http.StatusOK)
}

View File

@ -46,7 +46,6 @@ func InitTest(requireGitea bool) {
// TODO: Speedup tests that rely on the event source ticker, confirm whether there is any bug or failure.
// setting.UI.Notification.EventSourceUpdateTime = time.Second
setting.IsInTesting = true
setting.AppWorkPath = giteaRoot
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
if requireGitea {

View File

@ -2520,7 +2520,7 @@ tbody.commit-list {
display: inline-block;
background-color: var(--color-red);
height: 12px;
width: 40px;
width: 44px;
}
.diff-stats-bar .diff-stats-add-bar {

View File

@ -1,6 +1,7 @@
.issue-card {
display: flex;
flex-direction: column;
gap: 4px;
align-items: start;
border-radius: var(--border-radius);
padding: 8px 10px;
@ -17,7 +18,6 @@
.issue-card-title {
flex: 1;
font-size: 14px;
margin-left: 4px;
}
.issue-card.sortable-chosen .issue-card-title {

View File

@ -6,18 +6,10 @@
// This file must be imported before any lazy-loading is being attempted.
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
const filteredErrors = new Set([
'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325
]);
export function showGlobalErrorMessage(msg) {
const pageContent = document.querySelector('.page-content');
if (!pageContent) return;
for (const filteredError of filteredErrors) {
if (msg.includes(filteredError)) return;
}
// compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = msg.replace(/\W/g, '').trim();
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);

View File

@ -1,5 +1,5 @@
export function handleGlobalEnterQuickSubmit(target) {
const form = target.closest('form');
let form = target.closest('form');
if (form) {
if (!form.checkValidity()) {
form.reportValidity();
@ -9,5 +9,10 @@ export function handleGlobalEnterQuickSubmit(target) {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
return;
}
form = target.closest('.ui.form');
if (form) {
form.querySelector('.ui.primary.button')?.click();
}
}

View File

@ -162,8 +162,8 @@ async function onEditContent(event) {
editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML;
comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone'));
editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset);
editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh);
editContentZone.querySelector('.ui.cancel.button').addEventListener('click', cancelAndReset);
editContentZone.querySelector('.ui.primary.button').addEventListener('click', saveAndRefresh);
}
// Show write/preview tab and copy raw content as needed