mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Merge 5576b10604c39c2323521edf36160e4885c84bd8 into 21af8150b7ba315a9f75264ab77813b0b7c697a8
This commit is contained in:
commit
f8da60f218
@ -435,3 +435,56 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
||||
}
|
||||
|
||||
type ActionRunIndex db.ResourceIndex
|
||||
|
||||
// DeleteActionRunAndChild delete action_task_step, action_task_output, action_task, action_run and action_run_job.
|
||||
func DeleteActionRunAndChild(ctx context.Context, runIDs, jobIDs, taskIDs []int64) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
|
||||
Delete(ActionTaskStep{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).In("task_id", taskIDs).
|
||||
Delete(ActionTaskOutput{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).In("id", taskIDs).Delete(ActionTask{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).In("id", jobIDs).Delete(ActionRunJob{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).In("id", runIDs).Delete(ActionRun{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// GetRunsByIDsAndTriggerUserID -- get all action run by trigger user with selected ids
|
||||
func GetRunsByIDsAndTriggerUserID(ctx context.Context, ids []int64, triggerUserID int64) ([]*ActionRun, error) {
|
||||
var runs []*ActionRun
|
||||
err := db.GetEngine(ctx).Where("trigger_user_id=?", triggerUserID).
|
||||
In("id", ids).Find(&runs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(runs) < 1 {
|
||||
return nil, fmt.Errorf("run with ids %d: %w", ids, util.ErrNotExist)
|
||||
}
|
||||
|
||||
return runs, nil
|
||||
}
|
||||
|
@ -184,3 +184,19 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
return StatusUnknown // it shouldn't happen
|
||||
}
|
||||
}
|
||||
|
||||
func GetRunJobsByRunIDs(ctx context.Context, runIDs []int64) ([]*ActionRunJob, error) {
|
||||
var jobs []*ActionRunJob
|
||||
if err := db.GetEngine(ctx).In("run_id", runIDs).Find(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func GetRunTasksByJobIDs(ctx context.Context, jobIDs []int64) ([]*ActionTask, error) {
|
||||
var tasks []*ActionTask
|
||||
if err := db.GetEngine(ctx).In("job_id", jobIDs).Find(&tasks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"bytes"
|
||||
stdCtx "context"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@ -18,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -28,6 +31,7 @@ import (
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -312,6 +316,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
|
||||
}
|
||||
|
||||
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
||||
@ -424,3 +429,94 @@ func decodeNode(node yaml.Node, out any) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func DeleteRuns(ctx *context.Context) {
|
||||
rd := ctx.Req.Body
|
||||
defer rd.Close()
|
||||
|
||||
req := DeleteRunsRequest{}
|
||||
if err := json.NewDecoder(rd).Decode(&req); err != nil {
|
||||
ctx.ServerError("failed to decode request body into delte runs request", err)
|
||||
return
|
||||
}
|
||||
if len(req.ActionIDs) < 1 {
|
||||
ctx.ServerError("missing action_run.id for delete action run", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
eg = new(errgroup.Group)
|
||||
actionRuns []*actions_model.ActionRun
|
||||
jobIDs, taskIDs []int64
|
||||
taskLogFileNames []string
|
||||
)
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
actionRuns, err = actions_model.GetRunsByIDsAndTriggerUserID(ctx, req.ActionIDs, ctx.Doer.ID)
|
||||
return err
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
actionRunJobs, err := actions_model.GetRunJobsByRunIDs(ctx, req.ActionIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, actionRunJob := range actionRunJobs {
|
||||
jobIDs = append(jobIDs, actionRunJob.ID)
|
||||
}
|
||||
actionTasks, err := actions_model.GetRunTasksByJobIDs(ctx, jobIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, actionTask := range actionTasks {
|
||||
taskIDs = append(taskIDs, actionTask.ID)
|
||||
taskLogFileNames = append(taskLogFileNames, actionTask.LogFilename)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := eg.Wait()
|
||||
if err != nil {
|
||||
ctx.ServerError("failed to get action runs and action run jobs", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(actionRuns) != len(req.ActionIDs) {
|
||||
ctx.ServerError("action ids not match with request", nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = actions_model.DeleteActionRunAndChild(ctx, req.ActionIDs, jobIDs, taskIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("failed to delete action_run", err)
|
||||
return
|
||||
}
|
||||
|
||||
removeActionTaskLogFilenames(taskLogFileNames)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type DeleteRunsRequest struct {
|
||||
ActionIDs []int64 `json:"actionIds"`
|
||||
}
|
||||
|
||||
func removeActionTaskLogFilenames(taskLogFileNames []string) {
|
||||
dirNameActionLog := "actions_log"
|
||||
go func() {
|
||||
for _, taskLogFileName := range taskLogFileNames {
|
||||
var fileName string
|
||||
if filepath.IsAbs(setting.AppDataPath) {
|
||||
fileName = filepath.Join(setting.AppDataPath, dirNameActionLog, taskLogFileName)
|
||||
} else {
|
||||
fileName = filepath.Join(setting.AppWorkPath, setting.AppDataPath, dirNameActionLog, taskLogFileName)
|
||||
}
|
||||
|
||||
if err := os.Remove(fileName); err != nil {
|
||||
log.Error("failed to remove actions_log file %s: %v", fileName, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -1447,6 +1447,8 @@ func registerRoutes(m *web.Router) {
|
||||
m.Group("/workflows/{workflow_name}", func() {
|
||||
m.Get("/badge.svg", actions.GetWorkflowBadge)
|
||||
})
|
||||
|
||||
m.Post("/runs/delete", reqRepoActionsWriter, actions.DeleteRuns)
|
||||
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions)
|
||||
// end "/{username}/{reponame}/actions"
|
||||
|
||||
|
@ -25,7 +25,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="twelve wide column content">
|
||||
<div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center">
|
||||
<div id="action-filter" class="ui secondary filter menu tw-flex tw-items-center tw-justify-between">
|
||||
<!-- Checkbox -->
|
||||
<div class="action-list-toolbar-left">
|
||||
<input type="checkbox" autocomplete="off" class="action-checkbox-all tw-mr-4 tw-ml-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center">
|
||||
{{if $.IsRepoAdmin}}
|
||||
<div id="action-delete" class="ui jump item tw-hidden">
|
||||
<button class="ui red button action-action" data-action="delete" data-url="{{$.RepoLink}}/actions/runs/delete" data-action-delete-confirm="{{ctx.Locale.Tr "confirm_delete_selected"}}">
|
||||
{{ctx.Locale.Tr "repo.issues.delete"}}
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
<!-- Actor -->
|
||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
||||
@ -64,6 +76,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .AllowDisableOrEnableWorkflow}}
|
||||
<button class="ui jump dropdown btn interact-bg tw-p-2">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="flex-list run-list">
|
||||
<div id="action-actions" class="flex-list run-list">
|
||||
{{if not .Runs}}
|
||||
<div class="empty-placeholder">
|
||||
{{svg "octicon-no-entry" 48}}
|
||||
@ -7,6 +7,7 @@
|
||||
{{end}}
|
||||
{{range .Runs}}
|
||||
<div class="flex-item tw-items-center">
|
||||
<input type="checkbox" autocomplete="off" class="action-checkbox tw-mr-4 tw-ml-4" data-action-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""{{if or (eq .Status 6) (eq .Status 5)}}disabled{{end}}>
|
||||
<div class="flex-item-leading">
|
||||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
||||
</div>
|
||||
|
235
tests/integration/api_repo_action_delete_test.go
Normal file
235
tests/integration/api_repo_action_delete_test.go
Normal file
@ -0,0 +1,235 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
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/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoActionDelete(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "2",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "2",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix-failure
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_FAILURE,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||
|
||||
for i := 0; i < len(tc.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := tc.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
}
|
||||
|
||||
task := runner.fetchTask(t)
|
||||
actualTaskNeeds := task.Needs
|
||||
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
|
||||
for jobID, tn := range tc.expectedTaskNeeds {
|
||||
actualNeed := actualTaskNeeds[jobID]
|
||||
assert.Equal(t, tn.Result, actualNeed.Result)
|
||||
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
|
||||
for outputKey, outputValue := range tn.Outputs {
|
||||
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
// check result
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions", httpContext.Username, httpContext.Reponame))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
var runIDs []int64
|
||||
list := htmlDoc.doc.Find("#action-actions input.action-checkbox:not(:disabled)")
|
||||
list.Each(func(i int, s *goquery.Selection) {
|
||||
idStr, exists := s.Attr("data-action-id")
|
||||
if exists {
|
||||
runID, err := strconv.ParseInt(idStr, 10, 64)
|
||||
assert.NoError(t, err)
|
||||
runIDs = append(runIDs, runID)
|
||||
}
|
||||
})
|
||||
|
||||
assert.NotEmpty(t, runIDs)
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
reqDelete := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/%s/%s/actions/runs/delete", httpContext.Username, httpContext.Reponame), map[string]any{
|
||||
"actionIds": runIDs,
|
||||
"_csrf": csrf,
|
||||
}).AddTokenAuth(token).
|
||||
SetHeader("X-Csrf-Token", csrf)
|
||||
session.MakeRequest(t, reqDelete, http.StatusNoContent)
|
||||
|
||||
// should not found
|
||||
_, err := actions_model.GetRunsByIDsAndTriggerUserID(context.Background(), runIDs, user2.ID)
|
||||
assert.EqualError(t, err, fmt.Errorf("run with ids %d: %w", runIDs, util.ErrNotExist).Error())
|
||||
doAPIDeleteRepository(httpContext)
|
||||
})
|
||||
}
|
97
web_src/js/features/repo-action-list.ts
Normal file
97
web_src/js/features/repo-action-list.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
|
||||
function initRepoActionListCheckboxes() {
|
||||
const actionListSelectAll = document.querySelector<HTMLInputElement>('.action-checkbox-all');
|
||||
if (!actionListSelectAll) return; // logged out state
|
||||
const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.action-checkbox:not([disabled])');
|
||||
const actionDelete = document.querySelector('#action-delete');
|
||||
const syncIssueSelectionState = () => {
|
||||
const enabledCheckboxes = Array.from(issueCheckboxes).filter((el) => !el.disabled);
|
||||
const checkedCheckboxes = enabledCheckboxes.filter((el) => el.checked);
|
||||
const anyChecked = Boolean(checkedCheckboxes.length);
|
||||
const allChecked = anyChecked && checkedCheckboxes.length === enabledCheckboxes.length;
|
||||
|
||||
if (allChecked) {
|
||||
actionListSelectAll.checked = true;
|
||||
actionListSelectAll.indeterminate = false;
|
||||
} else if (anyChecked) {
|
||||
actionListSelectAll.checked = false;
|
||||
actionListSelectAll.indeterminate = true;
|
||||
} else {
|
||||
actionListSelectAll.checked = false;
|
||||
actionListSelectAll.indeterminate = false;
|
||||
}
|
||||
if (actionDelete) {
|
||||
toggleElem('#action-delete', anyChecked);
|
||||
}
|
||||
};
|
||||
|
||||
for (const el of issueCheckboxes) {
|
||||
el.addEventListener('change', syncIssueSelectionState);
|
||||
}
|
||||
|
||||
actionListSelectAll.addEventListener('change', () => {
|
||||
for (const el of issueCheckboxes) {
|
||||
if (!el.disabled) {
|
||||
el.checked = actionListSelectAll.checked;
|
||||
}
|
||||
}
|
||||
syncIssueSelectionState();
|
||||
});
|
||||
|
||||
queryElems(document, '.action-action', (el) => el.addEventListener('click',
|
||||
async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const action = el.getAttribute('data-action');
|
||||
const url = el.getAttribute('data-url');
|
||||
const actionIDList: number[] = [];
|
||||
const radix = 10;
|
||||
for (const el of document.querySelectorAll<HTMLInputElement>('.action-checkbox:checked:not([disabled])')) {
|
||||
const id = el.getAttribute('data-action-id');
|
||||
if (id) {
|
||||
actionIDList.push(parseInt(id, radix));
|
||||
}
|
||||
}
|
||||
if (actionIDList.length < 1) return;
|
||||
|
||||
// for delete
|
||||
if (action === 'delete') {
|
||||
const confirmText = el.getAttribute('data-action-delete-confirm');
|
||||
if (!await confirmModal({content: confirmText, confirmButtonColor: 'red'})) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteActions(url, actionIDList);
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showErrorToast(err.responseJSON?.error ?? err.message);
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
async function deleteActions(url: string, actionIds: number[]) {
|
||||
try {
|
||||
const response = await POST(url, {
|
||||
data: {
|
||||
actionIds,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to delete actions');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
export function initRepoActionList() {
|
||||
if (document.querySelector('.page-content.repository.actions')) {
|
||||
initRepoActionListCheckboxes();
|
||||
}
|
||||
}
|
@ -85,6 +85,7 @@ import {
|
||||
initGlobalEnterQuickSubmit,
|
||||
initGlobalFormDirtyLeaveConfirm,
|
||||
} from './features/common-form.ts';
|
||||
import {initRepoActionList} from './features/repo-action-list.ts';
|
||||
|
||||
initGiteaFomantic();
|
||||
initDirAuto();
|
||||
@ -213,5 +214,6 @@ onDomReady(() => {
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
initRepoActionList,
|
||||
]);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user