Skip to content

Commit 10ddd2a

Browse files
committed
feat(repo): enable delete directory
1 parent e0206bc commit 10ddd2a

File tree

7 files changed

+109
-70
lines changed

7 files changed

+109
-70
lines changed

options/locale/locale_en-US.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ copy_error = Copy failed
111111
copy_type_unsupported = This file type cannot be copied
112112
copy_filename = Copy filename
113113

114+
repo.more_operations = More Operations
115+
114116
write = Write
115117
preview = Preview
116118
loading = Loading…
@@ -1358,6 +1360,8 @@ editor.delete_this_file = Delete File
13581360
editor.delete_this_directory = Delete Directory
13591361
editor.must_have_write_access = You must have write access to make or propose changes to this file.
13601362
editor.file_delete_success = File "%s" has been deleted.
1363+
editor.directory_delete_success = Directory "%s" has been deleted.
1364+
editor.delete_directory = Delete directory '%s'
13611365
editor.name_your_file = Name your file…
13621366
editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field.
13631367
editor.or = or

routers/web/repo/editor.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -384,35 +384,81 @@ func DeleteFile(ctx *context.Context) {
384384
ctx.HTML(http.StatusOK, tplDeleteFile)
385385
}
386386

387-
// DeleteFilePost response for deleting file
387+
// DeleteFilePost response for deleting file or directory
388388
func DeleteFilePost(ctx *context.Context) {
389389
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
390390
if ctx.Written() {
391391
return
392392
}
393393

394394
treePath := ctx.Repo.TreePath
395-
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
396-
LastCommitID: parsed.form.LastCommit,
397-
OldBranch: parsed.OldBranchName,
398-
NewBranch: parsed.NewBranchName,
399-
Files: []*files_service.ChangeRepoFile{
395+
396+
// Check if the path is a directory
397+
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
398+
if err != nil {
399+
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
400+
return
401+
}
402+
403+
var filesToDelete []*files_service.ChangeRepoFile
404+
var commitMessage string
405+
406+
if entry.IsDir() {
407+
// Get all files in the directory recursively
408+
tree, err := ctx.Repo.Commit.SubTree(treePath)
409+
if err != nil {
410+
ctx.ServerError("SubTree", err)
411+
return
412+
}
413+
414+
entries, err := tree.ListEntriesRecursiveFast()
415+
if err != nil {
416+
ctx.ServerError("ListEntriesRecursiveFast", err)
417+
return
418+
}
419+
420+
// Create delete operations for all files in the directory
421+
for _, e := range entries {
422+
if !e.IsDir() && !e.IsSubModule() {
423+
filesToDelete = append(filesToDelete, &files_service.ChangeRepoFile{
424+
Operation: "delete",
425+
TreePath: treePath + "/" + e.Name(),
426+
})
427+
}
428+
}
429+
430+
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
431+
} else {
432+
// Single file deletion
433+
filesToDelete = []*files_service.ChangeRepoFile{
400434
{
401435
Operation: "delete",
402436
TreePath: treePath,
403437
},
404-
},
405-
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
406-
Signoff: parsed.form.Signoff,
407-
Author: parsed.GitCommitter,
408-
Committer: parsed.GitCommitter,
438+
}
439+
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath))
440+
}
441+
442+
_, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
443+
LastCommitID: parsed.form.LastCommit,
444+
OldBranch: parsed.OldBranchName,
445+
NewBranch: parsed.NewBranchName,
446+
Files: filesToDelete,
447+
Message: commitMessage,
448+
Signoff: parsed.form.Signoff,
449+
Author: parsed.GitCommitter,
450+
Committer: parsed.GitCommitter,
409451
})
410452
if err != nil {
411453
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
412454
return
413455
}
414456

415-
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
457+
if entry.IsDir() {
458+
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
459+
} else {
460+
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
461+
}
416462
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
417463
redirectForCommitChoice(ctx, parsed, redirectTreePath)
418464
}

templates/repo/view_content.tmpl

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
</a>
8585
</div>
8686
</button>
87-
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "more_operations"}}">
87+
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
8888
{{svg "octicon-kebab-horizontal"}}
8989
<div class="menu">
9090
<a class="item" data-clipboard-text="{{.TreePath}}">
@@ -93,19 +93,12 @@
9393
<a class="item" data-clipboard-text="{{AppUrl}}{{StringUtils.TrimPrefix .Repository.Link "/"}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
9494
{{svg "octicon-link" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_copy_permalink"}}
9595
</a>
96-
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived)}}
96+
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived) (not $isTreePathRoot)}}
9797
<div class="divider"></div>
98-
<a class="item" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
98+
<a class="item danger" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
9999
{{svg "octicon-trash" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.delete_this_directory"}}
100100
</a>
101101
{{end}}
102-
<div class="divider"></div>
103-
<div class="item">
104-
<div class="ui checkbox" id="center-content-toggle">
105-
<input type="checkbox" id="center-content-checkbox">
106-
<label for="center-content-checkbox">{{ctx.Locale.Tr "repo.center_content"}}</label>
107-
</div>
108-
</div>
109102
</div>
110103
</button>
111104
{{end}}

web_src/css/repo/file-actions.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
margin: 0.5rem 0;
1717
}
1818

19-
/* Center content option */
20-
.repo-content-centered {
21-
max-width: 980px;
22-
margin-left: auto !important;
23-
margin-right: auto !important;
19+
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger,
20+
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger svg {
21+
color: var(--color-red) !important;
22+
}
23+
24+
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover,
25+
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover svg {
26+
color: var(--color-red) !important;
27+
background: var(--color-red-badge-hover-bg) !important;
2428
}

web_src/js/components/ViewFileTree.vue

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ const handleSearchInput = (e: Event) => {
3636
};
3737
3838
const handleKeyDown = (e: KeyboardEvent) => {
39+
if (e.key === 'Escape' && searchQuery.value) {
40+
e.preventDefault();
41+
clearSearch();
42+
return;
43+
}
44+
3945
if (!searchQuery.value || filteredFiles.value.length === 0) return;
4046
4147
if (e.key === 'ArrowDown') {
@@ -52,12 +58,14 @@ const handleKeyDown = (e: KeyboardEvent) => {
5258
if (selectedFile) {
5359
handleSearchResultClick(selectedFile.matchResult.join(''));
5460
}
55-
} else if (e.key === 'Escape') {
56-
searchQuery.value = '';
57-
if (searchInputElement) searchInputElement.value = '';
5861
}
5962
};
6063
64+
const clearSearch = () => {
65+
searchQuery.value = '';
66+
if (searchInputElement) searchInputElement.value = '';
67+
};
68+
6169
const scrollSelectedIntoView = () => {
6270
nextTick(() => {
6371
const resultsEl = searchResults.value;
@@ -79,8 +87,7 @@ const handleClickOutside = (e: MouseEvent) => {
7987
// Check if click is outside search input and results
8088
if (searchInputElement && !searchInputElement.contains(target) &&
8189
resultsEl && !resultsEl.contains(target)) {
82-
searchQuery.value = '';
83-
if (searchInputElement) searchInputElement.value = '';
90+
clearSearch();
8491
}
8592
};
8693
@@ -133,8 +140,7 @@ onUnmounted(() => {
133140
});
134141
135142
function handleSearchResultClick(filePath: string) {
136-
searchQuery.value = '';
137-
if (searchInputElement) searchInputElement.value = '';
143+
clearSearch();
138144
window.location.href = `${treeLink.value}/${pathEscapeSegments(filePath)}`;
139145
}
140146
</script>
@@ -151,7 +157,8 @@ function handleSearchResultClick(filePath: string) {
151157
@mouseenter="selectedIndex = idx"
152158
:title="result.matchResult.join('')"
153159
>
154-
<span v-html="svg('octicon-file', 16)"></span>
160+
<!-- eslint-disable-next-line vue/no-v-html -->
161+
<span v-html="svg('octicon-file', 16)"/>
155162
<span class="file-tree-search-result-path">
156163
<span
157164
v-for="(part, index) in result.matchResult"
@@ -161,8 +168,12 @@ function handleSearchResultClick(filePath: string) {
161168
</span>
162169
</div>
163170
</div>
164-
<div v-if="searchQuery && filteredFiles.length === 0" class="file-tree-search-no-results">
165-
No matching file found
171+
<div v-if="searchQuery && filteredFiles.length === 0" ref="searchResults" class="file-tree-search-results file-tree-search-no-results">
172+
<div class="file-tree-no-results-content">
173+
<!-- eslint-disable-next-line vue/no-v-html -->
174+
<span v-html="svg('octicon-search', 24)"/>
175+
<span>No matching files found</span>
176+
</div>
166177
</div>
167178
</Teleport>
168179
<div class="view-file-tree-items">
@@ -209,7 +220,7 @@ function handleSearchResultClick(filePath: string) {
209220
border-bottom: 1px solid var(--color-secondary);
210221
}
211222
212-
.file-tree-search-result-item svg {
223+
.file-tree-search-result-item > span:first-child {
213224
flex-shrink: 0;
214225
margin-top: 0.125rem;
215226
}
@@ -236,9 +247,20 @@ function handleSearchResultClick(filePath: string) {
236247
}
237248
238249
.file-tree-search-no-results {
239-
padding: 1rem;
240-
text-align: center;
250+
padding: 0;
251+
}
252+
253+
.file-tree-no-results-content {
254+
display: flex;
255+
flex-direction: column;
256+
align-items: center;
257+
gap: 0.5rem;
258+
padding: 1.5rem;
241259
color: var(--color-text-light-2);
242260
font-size: 14px;
243261
}
262+
263+
.file-tree-no-results-content > span:first-child {
264+
opacity: 0.5;
265+
}
244266
</style>

web_src/js/features/repo-file-actions.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

web_src/js/index-domready.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
6464
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
6565
import {callInitFunctions} from './modules/init.ts';
6666
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
67-
import {initRepoFileActions} from './features/repo-file-actions.ts';
6867

6968
const initStartTime = performance.now();
7069
const initPerformanceTracer = callInitFunctions([
@@ -138,7 +137,6 @@ const initPerformanceTracer = callInitFunctions([
138137
initRepoReleaseNew,
139138
initRepoTopicBar,
140139
initRepoViewFileTree,
141-
initRepoFileActions,
142140
initRepoWikiForm,
143141
initRepository,
144142
initRepositoryActionView,

0 commit comments

Comments
 (0)