{{template "user/dashboard/navbar" .}} -
+
{{template "base/alert" .}} -
-
- {{template "user/heatmap" .}} - {{template "user/dashboard/feeds" .}} -
- {{template "user/dashboard/repolist" .}} +
+ {{template "user/heatmap" .}} + {{template "user/dashboard/feeds" .}}
+ {{template "user/dashboard/repolist" .}}
{{template "base/footer" .}} diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 34f9b67f8e..2781f710ed 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -52,4 +52,4 @@ data.organizationId = {{.ContextUser.ID}}; window.config.pageData.dashboardRepoList = data; -
+
diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go index b6f3d3bc81..d4368d51fe 100644 --- a/tests/integration/api_comment_attachment_test.go +++ b/tests/integration/api_comment_attachment_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go index 375fe9ced8..b6a0cca6d5 100644 --- a/tests/integration/api_issue_attachment_test.go +++ b/tests/integration/api_issue_attachment_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_packages_cargo_test.go b/tests/integration/api_packages_cargo_test.go index 869d90066a..55cce50c7b 100644 --- a/tests/integration/api_packages_cargo_test.go +++ b/tests/integration/api_packages_cargo_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go new file mode 100644 index 0000000000..f3188eb49f --- /dev/null +++ b/tests/integration/api_repo_compare_test.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPICompareBranches(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + // Login as User2. + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + repoName := "repo20" + + req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var apiResp *api.Compare + DecodeJSON(t, resp, &apiResp) + + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) +} diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go index f776b35325..0e01b504cc 100644 --- a/tests/integration/api_user_search_test.go +++ b/tests/integration/api_user_search_test.go @@ -10,7 +10,9 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -57,6 +59,25 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) { } } +func TestAPIUserSearchPaged(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.API.DefaultPagingNum, 5)() + + req := NewRequest(t, "GET", "/api/v1/users/search?limit=1") + resp := MakeRequest(t, req, http.StatusOK) + + var limitedResults SearchResults + DecodeJSON(t, resp, &limitedResults) + assert.Len(t, limitedResults.Data, 1) + + req = NewRequest(t, "GET", "/api/v1/users/search") + resp = MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.Len(t, results.Data, 5) +} + func TestAPIUserSearchSystemUsers(t *testing.T) { defer tests.PrepareTestEnv(t)() for _, systemUser := range []*user_model.User{ diff --git a/tests/integration/incoming_email_test.go b/tests/integration/incoming_email_test.go index 1284833864..543e620dbf 100644 --- a/tests/integration/incoming_email_test.go +++ b/tests/integration/incoming_email_test.go @@ -76,14 +76,11 @@ func TestIncomingEmail(t *testing.T) { t.Run("Handler", func(t *testing.T) { t.Run("Reply", func(t *testing.T) { - t.Run("Comment", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + checkReply := func(t *testing.T, payload []byte, issue *issues_model.Issue, commentType issues_model.CommentType) { + t.Helper() handler := &incoming.ReplyHandler{} - payload, err := incoming_payload.CreateReferencePayload(issue) - assert.NoError(t, err) - assert.Error(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, nil, payload)) assert.NoError(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, user, payload)) @@ -101,7 +98,7 @@ func TestIncomingEmail(t *testing.T) { comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{ IssueID: issue.ID, - Type: issues_model.CommentTypeComment, + Type: commentType, }) assert.NoError(t, err) assert.NotEmpty(t, comments) @@ -113,6 +110,14 @@ func TestIncomingEmail(t *testing.T) { attachment := comment.Attachments[0] assert.Equal(t, content.Attachments[0].Name, attachment.Name) assert.EqualValues(t, 4, attachment.Size) + } + t.Run("Issue", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + payload, err := incoming_payload.CreateReferencePayload(issue) + assert.NoError(t, err) + + checkReply(t, payload, issue, issues_model.CommentTypeComment) }) t.Run("CodeComment", func(t *testing.T) { @@ -121,33 +126,22 @@ func TestIncomingEmail(t *testing.T) { comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 6}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) - handler := &incoming.ReplyHandler{} - content := &incoming.MailContent{ - Content: "code reply by mail", - Attachments: []*incoming.Attachment{ - { - Name: "attachment.txt", - Content: []byte("test"), - }, - }, - } + payload, err := incoming_payload.CreateReferencePayload(comment) + assert.NoError(t, err) + + checkReply(t, payload, issue, issues_model.CommentTypeCode) + }) + + t.Run("Comment", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) payload, err := incoming_payload.CreateReferencePayload(comment) assert.NoError(t, err) - assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload)) - - comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{ - IssueID: issue.ID, - Type: issues_model.CommentTypeCode, - }) - assert.NoError(t, err) - assert.NotEmpty(t, comments) - comment = comments[len(comments)-1] - assert.Equal(t, user.ID, comment.PosterID) - assert.Equal(t, content.Content, comment.Content) - assert.NoError(t, comment.LoadAttachments(db.DefaultContext)) - assert.Empty(t, comment.Attachments) + checkReply(t, payload, issue, issues_model.CommentTypeComment) }) }) diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 5f102f8d62..e50f5c1356 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -1,6 +1,5 @@ // Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/repo_mergecommit_revert_test.go b/tests/integration/repo_mergecommit_revert_test.go index 7041861f11..eb75d45c15 100644 --- a/tests/integration/repo_mergecommit_revert_test.go +++ b/tests/integration/repo_mergecommit_revert_test.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package integration import ( diff --git a/web_src/css/actions.css b/web_src/css/actions.css index 1d5bea2395..0ab09f537a 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -44,9 +44,10 @@ } .run-list-item-right { - flex: 0 0 min(20%, 130px); + width: 130px; display: flex; flex-direction: column; + flex-shrink: 0; gap: 3px; color: var(--color-text-light); } @@ -57,3 +58,26 @@ gap: .25rem; align-items: center; } + +.run-list .flex-item-trailing { + flex-wrap: nowrap; + width: 280px; + flex: 0 0 280px; +} + +.run-list-ref { + display: inline-block !important; +} + +@media (max-width: 767.98px) { + .run-list .flex-item-trailing { + flex-direction: column; + align-items: flex-end; + width: auto; + flex-basis: auto; + } + .run-list-item-right, + .run-list-ref { + max-width: 110px; + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index 06542c652e..c571280ee0 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -453,6 +453,7 @@ a.label, .ui.selection.dropdown .menu > .item { border-color: var(--color-secondary); + white-space: nowrap; } .ui.selection.visible.dropdown > .text:not(.default) { @@ -1562,6 +1563,7 @@ table th[data-sortt-desc] .svg { align-items: center; gap: .25rem; vertical-align: middle; + min-width: 0; } .ui.ui.button { @@ -1582,4 +1584,5 @@ table th[data-sortt-desc] .svg { display: flex; align-items: center; gap: .25rem; + min-width: 0; } diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 4bb9fa38bf..2ee2399d73 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -7,7 +7,6 @@ .dashboard.feeds .context.user.menu .ui.header, .dashboard.issues .context.user.menu .ui.header { font-size: 1rem; - text-transform: none; } .dashboard.feeds .filter.menu, diff --git a/web_src/css/install.css b/web_src/css/install.css index 4ac294e902..ee2395e6c5 100644 --- a/web_src/css/install.css +++ b/web_src/css/install.css @@ -18,7 +18,8 @@ width: auto; } -.page-content.install form.ui.form input { +.page-content.install form.ui.form input:not([type="checkbox"],[type="radio"]), +.page-content.install form.ui.form .ui.selection.dropdown { width: 60%; } diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css index d3e45714a4..8d73573bfa 100644 --- a/web_src/css/modules/checkbox.css +++ b/web_src/css/modules/checkbox.css @@ -66,7 +66,7 @@ input[type="radio"] { } .ui.toggle.checkbox input { width: 3.5rem; - height: 1.5rem; + height: 21px; opacity: 0; z-index: 3; } @@ -81,29 +81,30 @@ input[type="radio"] { content: ""; z-index: 1; top: 0; - width: 3.5rem; - height: 1.5rem; + width: 49px; + height: 21px; border-radius: 500rem; left: 0; } .ui.toggle.checkbox label::after { background: var(--color-white); + box-shadow: 1px 1px 4px 1px var(--color-shadow); position: absolute; content: ""; opacity: 1; z-index: 2; - width: 1.5rem; - height: 1.5rem; - top: 0; - left: 0; + width: 18px; + height: 18px; + top: 1.5px; + left: 1.5px; border-radius: 500rem; transition: background 0.3s ease, left 0.3s ease; } .ui.toggle.checkbox input ~ label::after { - left: -0.05rem; + left: 1.5px; } .ui.toggle.checkbox input:checked ~ label::after { - left: 2.15rem; + left: 29px; } .ui.toggle.checkbox input:focus ~ label::before, .ui.toggle.checkbox label::before { diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css index 48560bd3d9..acc8408f37 100644 --- a/web_src/css/modules/divider.css +++ b/web_src/css/modules/divider.css @@ -2,12 +2,16 @@ margin: 10px 0; height: 0; font-weight: var(--font-weight-medium); - text-transform: uppercase; color: var(--color-text); font-size: 1rem; width: 100%; } +h4.divider { + margin-top: 1.25rem; + margin-bottom: 1.25rem; +} + .divider:not(.divider-text) { border-top: 1px solid var(--color-secondary); } diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css index 1ca513687f..5d4e12cc12 100644 --- a/web_src/css/modules/flexcontainer.css +++ b/web_src/css/modules/flexcontainer.css @@ -6,10 +6,16 @@ margin-top: var(--page-spacing); } +/* small options menu on the left, used in settings/admin pages */ .flex-container-nav { width: 240px; } +/* wide sidebar on the right, used in frontpage */ +.flex-container-sidebar { + width: 35%; +} + .flex-container-main { flex: 1; min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ @@ -19,7 +25,9 @@ .flex-container { flex-direction: column; } - .flex-container-nav { + .flex-container-nav, + .flex-container-sidebar { + order: -1; width: auto; } } diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css index 05381e1185..9cec5fcbe6 100644 --- a/web_src/css/modules/header.css +++ b/web_src/css/modules/header.css @@ -9,7 +9,6 @@ font-family: var(--fonts-regular); font-weight: var(--font-weight-medium); line-height: 1.28571429; - text-transform: none; } .ui.header:first-child { diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css index 0512c5fddb..2032b2c84b 100644 --- a/web_src/css/modules/label.css +++ b/web_src/css/modules/label.css @@ -5,12 +5,12 @@ display: inline-flex; align-items: center; gap: .25rem; + min-width: 0; vertical-align: middle; line-height: 1; background: var(--color-label-bg); color: var(--color-label-text); padding: 0.3em 0.5em; - text-transform: none; font-size: 0.85714286rem; font-weight: var(--font-weight-medium); border: 0 solid transparent; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 3cb22bb00f..87dbeb5bba 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -435,7 +435,6 @@ td .commit-summary { padding: 0 !important; } -.non-diff-file-content .attached.segment, .non-diff-file-content .pdfobject { border-radius: 0 0 var(--border-radius) var(--border-radius); } @@ -836,55 +835,53 @@ td .commit-summary { margin-right: 0.25em; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit { - line-height: 34px; /* this must be same as .badge height, to avoid overflow */ - clear: both; /* reset the "float right shabox", in the future, use flexbox instead */ +.singular-commit { + display: flex; + align-items: center; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > img.avatar, -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > .avatar img { - position: relative; - top: -2px; +.singular-commit .badge { + height: 30px !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label { +.singular-commit .shabox .sha.label { margin: 0; border: 1px solid var(--color-light-border); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning { +.singular-commit .shabox .sha.label.isSigned.isWarning { border: 1px solid var(--color-red-badge); background: var(--color-red-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning:hover { +.singular-commit .shabox .sha.label.isSigned.isWarning:hover { background: var(--color-red-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified { +.singular-commit .shabox .sha.label.isSigned.isVerified { border: 1px solid var(--color-green-badge); background: var(--color-green-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified:hover { +.singular-commit .shabox .sha.label.isSigned.isVerified:hover { background: var(--color-green-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { border: 1px solid var(--color-yellow-badge); background: var(--color-yellow-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { border: 1px solid var(--color-orange-badge); background: var(--color-orange-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } @@ -1079,6 +1076,12 @@ td .commit-summary { margin-left: 15px; } +.repository.view.issue .comment-list .event .detail .text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .repository.view.issue .comment-list .event .segments { box-shadow: none; } @@ -2339,6 +2342,8 @@ td .commit-summary { .stats-table { display: table; width: 100%; + margin: 6px 0; + border-spacing: 2px; } .stats-table .table-cell { @@ -2346,7 +2351,17 @@ td .commit-summary { } .stats-table .table-cell.tiny { - height: 0.5em; + height: 8px; +} + +.stats-table .table-cell:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.stats-table .table-cell:last-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } .labels-list { @@ -2429,6 +2444,7 @@ tbody.commit-list { .author-wrapper { max-width: 180px; align-self: center; + white-space: nowrap; } /* in the commit list, messages can wrap so we can use inline */ diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js new file mode 100644 index 0000000000..8c4e1506be --- /dev/null +++ b/web_src/js/components/RepoActionView.test.js @@ -0,0 +1,105 @@ +import {mount, flushPromises} from '@vue/test-utils'; +import RepoActionView from './RepoActionView.vue'; + +test('processes ##[group] and ##[endgroup]', async () => { + Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); + vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { + const artifacts_value = { + artifacts: [], + }; + const stepsLog_value = [ + { + step: 0, + cursor: 0, + lines: [ + {index: 1, message: '##[group]Test group', timestamp: 0}, + {index: 2, message: 'A test line', timestamp: 0}, + {index: 3, message: '##[endgroup]', timestamp: 0}, + {index: 4, message: 'A line outside the group', timestamp: 0}, + ], + }, + ]; + const jobs_value = { + state: { + run: { + status: 'success', + commit: { + pusher: {}, + }, + }, + currentJob: { + steps: [ + { + summary: 'Test Job', + duration: '1s', + status: 'success', + }, + ], + }, + }, + logs: { + stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], + }, + }; + + return Promise.resolve({ + ok: true, + json: vi.fn().mockResolvedValue( + url.endsWith('/artifacts') ? artifacts_value : jobs_value, + ), + }); + }); + + const wrapper = mount(RepoActionView, { + props: { + jobIndex: '1', + locale: { + approve: '', + cancel: '', + rerun: '', + artifactsTitle: '', + areYouSure: '', + confirmDeleteArtifact: '', + rerun_all: '', + showTimeStamps: '', + showLogSeconds: '', + showFullScreen: '', + downloadLogs: '', + status: { + unknown: '', + waiting: '', + running: '', + success: '', + failure: '', + cancelled: '', + skipped: '', + blocked: '', + }, + }, + }, + }); + await flushPromises(); + await wrapper.get('.job-step-summary').trigger('click'); + await flushPromises(); + + // Test if header was loaded correctly + expect(wrapper.get('.step-summary-msg').text()).toEqual('Test Job'); + + // Check if 3 lines where rendered + expect(wrapper.findAll('.job-log-line').length).toEqual(3); + + // Check if line 1 contains the group header + expect(wrapper.get('.job-log-line:nth-of-type(1) > details.log-msg').text()).toEqual('Test group'); + + // Check if right after the header line exists a log list + expect(wrapper.find('.job-log-line:nth-of-type(1) + .job-log-list.hidden').exists()).toBe(true); + + // Check if inside the loglist exist exactly one log line + expect(wrapper.findAll('.job-log-list > .job-log-line').length).toEqual(1); + + // Check if inside the loglist is an logline with our second logline + expect(wrapper.get('.job-log-list > .job-log-line > .log-msg').text()).toEqual('A test line'); + + // Check if after the log list exists another log line + expect(wrapper.get('.job-log-list + .job-log-line > .log-msg').text()).toEqual('A line outside the group'); +}); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 378f726688..4f2af3ac6d 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -110,34 +110,6 @@ const sfc = { }, methods: { - // get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group` - getLogsContainer(idx) { - const el = this.$refs.logs[idx]; - return el._stepLogsActiveContainer ?? el; - }, - // begin a log group - beginLogGroup(idx) { - const el = this.$refs.logs[idx]; - - const elJobLogGroup = document.createElement('div'); - elJobLogGroup.classList.add('job-log-group'); - - const elJobLogGroupSummary = document.createElement('div'); - elJobLogGroupSummary.classList.add('job-log-group-summary'); - - const elJobLogList = document.createElement('div'); - elJobLogList.classList.add('job-log-list'); - - elJobLogGroup.append(elJobLogGroupSummary); - elJobLogGroup.append(elJobLogList); - el._stepLogsActiveContainer = elJobLogList; - }, - // end a log group - endLogGroup(idx) { - const el = this.$refs.logs[idx]; - el._stepLogsActiveContainer = null; - }, - // show/hide the step logs for a step toggleStepLogs(idx) { this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded; @@ -153,8 +125,18 @@ const sfc = { approveRun() { POST(`${this.run.link}/approve`); }, + // show/hide the step logs for a group + toggleGroupLogs(event) { + const line = event.target.parentElement; + const list = line.nextSibling; + if (event.newState === 'open') { + list.classList.remove('hidden'); + } else { + list.classList.add('hidden'); + } + }, - createLogLine(line, startTime, stepIndex) { + createLogLine(line, startTime, stepIndex, group) { const div = document.createElement('div'); div.classList.add('job-log-line'); div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`); @@ -180,9 +162,19 @@ const sfc = { logTimeSeconds.textContent = `${seconds}s`; toggleElem(logTimeSeconds, this.timeVisible['log-time-seconds']); - const logMessage = document.createElement('span'); - logMessage.className = 'log-msg'; + let logMessage = document.createElement('span'); logMessage.innerHTML = renderAnsi(line.message); + if (group.isHeader) { + const details = document.createElement('details'); + details.addEventListener('toggle', this.toggleGroupLogs); + const summary = document.createElement('summary'); + summary.append(logMessage); + details.append(summary); + logMessage = details; + } + logMessage.className = 'log-msg'; + logMessage.style.paddingLeft = `${group.depth}em`; + div.append(logTimeStamp); div.append(logMessage); div.append(logTimeSeconds); @@ -191,10 +183,38 @@ const sfc = { }, appendLogs(stepIndex, logLines, startTime) { + const groupStack = []; + const container = this.$refs.logs[stepIndex]; for (const line of logLines) { - // TODO: group support: ##[group]GroupTitle , ##[endgroup] - const el = this.getLogsContainer(stepIndex); - el.append(this.createLogLine(line, startTime, stepIndex)); + const el = groupStack.length > 0 ? groupStack[groupStack.length - 1] : container; + const group = { + depth: groupStack.length, + isHeader: false, + }; + if (line.message.startsWith('##[group]')) { + group.isHeader = true; + + const logLine = this.createLogLine( + { + ...line, + message: line.message.substring(9), + }, + startTime, stepIndex, group, + ); + logLine.setAttribute('data-group', group.index); + el.append(logLine); + + const list = document.createElement('div'); + list.classList.add('job-log-list'); + list.classList.add('hidden'); + list.setAttribute('data-group', group.index); + groupStack.push(list); + el.append(list); + } else if (line.message.startsWith('##[endgroup]')) { + groupStack.pop(); + } else { + el.append(this.createLogLine(line, startTime, stepIndex, group)); + } } }, @@ -382,7 +402,7 @@ export function initRepositoryActionView() { -
@@ -391,8 +411,8 @@ export function initRepositoryActionView() { {{ run.commit.shortSHA }} {{ run.commit.localePushedBy }} {{ run.commit.pusher.displayName }} - - {{ run.commit.branch.name }} + + {{ run.commit.branch.name }}