feat(repo): enable delete directory

brymut 2025-11-11 07:27:01 +03:00
parent e0206bc9dd
commit 10ddd2a1b0
No known key found for this signature in database
GPG Key ID: 25AB059F26A42251
7 changed files with 109 additions and 70 deletions

View File

@ -111,6 +111,8 @@ copy_error = Copy failed
copy_type_unsupported = This file type cannot be copied
copy_filename = Copy filename
repo.more_operations = More Operations
write = Write
preview = Preview
loading = Loading…
@ -1358,6 +1360,8 @@ editor.delete_this_file = Delete File
editor.delete_this_directory = Delete Directory
editor.must_have_write_access = You must have write access to make or propose changes to this file.
editor.file_delete_success = File "%s" has been deleted.
editor.directory_delete_success = Directory "%s" has been deleted.
editor.delete_directory = Delete directory '%s'
editor.name_your_file = Name your file…
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.
editor.or = or

View File

@ -384,7 +384,7 @@ func DeleteFile(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplDeleteFile)
}
// DeleteFilePost response for deleting file
// DeleteFilePost response for deleting file or directory
func DeleteFilePost(ctx *context.Context) {
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
if ctx.Written() {
@ -392,27 +392,73 @@ func DeleteFilePost(ctx *context.Context) {
}
treePath := ctx.Repo.TreePath
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Files: []*files_service.ChangeRepoFile{
// Check if the path is a directory
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil {
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
return
}
var filesToDelete []*files_service.ChangeRepoFile
var commitMessage string
if entry.IsDir() {
// Get all files in the directory recursively
tree, err := ctx.Repo.Commit.SubTree(treePath)
if err != nil {
ctx.ServerError("SubTree", err)
return
}
entries, err := tree.ListEntriesRecursiveFast()
if err != nil {
ctx.ServerError("ListEntriesRecursiveFast", err)
return
}
// Create delete operations for all files in the directory
for _, e := range entries {
if !e.IsDir() && !e.IsSubModule() {
filesToDelete = append(filesToDelete, &files_service.ChangeRepoFile{
Operation: "delete",
TreePath: treePath + "/" + e.Name(),
})
}
}
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
} else {
// Single file deletion
filesToDelete = []*files_service.ChangeRepoFile{
{
Operation: "delete",
TreePath: treePath,
},
},
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
Signoff: parsed.form.Signoff,
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
}
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath))
}
_, err = files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit,
OldBranch: parsed.OldBranchName,
NewBranch: parsed.NewBranchName,
Files: filesToDelete,
Message: commitMessage,
Signoff: parsed.form.Signoff,
Author: parsed.GitCommitter,
Committer: parsed.GitCommitter,
})
if err != nil {
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
if entry.IsDir() {
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
} else {
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
}
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
redirectForCommitChoice(ctx, parsed, redirectTreePath)
}

View File

@ -84,7 +84,7 @@
</a>
</div>
</button>
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "more_operations"}}">
<button class="ui dropdown basic compact jump button icon repo-file-actions-dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
{{svg "octicon-kebab-horizontal"}}
<div class="menu">
<a class="item" data-clipboard-text="{{.TreePath}}">
@ -93,19 +93,12 @@
<a class="item" data-clipboard-text="{{AppUrl}}{{StringUtils.TrimPrefix .Repository.Link "/"}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
{{svg "octicon-link" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_copy_permalink"}}
</a>
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived)}}
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .Repository.IsArchived) (not $isTreePathRoot)}}
<div class="divider"></div>
<a class="item" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
<a class="item danger" href="{{.RepoLink}}/_delete/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">
{{svg "octicon-trash" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.delete_this_directory"}}
</a>
{{end}}
<div class="divider"></div>
<div class="item">
<div class="ui checkbox" id="center-content-toggle">
<input type="checkbox" id="center-content-checkbox">
<label for="center-content-checkbox">{{ctx.Locale.Tr "repo.center_content"}}</label>
</div>
</div>
</div>
</button>
{{end}}

View File

@ -16,9 +16,13 @@
margin: 0.5rem 0;
}
/* Center content option */
.repo-content-centered {
max-width: 980px;
margin-left: auto !important;
margin-right: auto !important;
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger,
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger svg {
color: var(--color-red) !important;
}
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover,
.ui.dropdown.repo-file-actions-dropdown > .menu > .item.danger:hover svg {
color: var(--color-red) !important;
background: var(--color-red-badge-hover-bg) !important;
}

View File

@ -36,6 +36,12 @@ const handleSearchInput = (e: Event) => {
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && searchQuery.value) {
e.preventDefault();
clearSearch();
return;
}
if (!searchQuery.value || filteredFiles.value.length === 0) return;
if (e.key === 'ArrowDown') {
@ -52,12 +58,14 @@ const handleKeyDown = (e: KeyboardEvent) => {
if (selectedFile) {
handleSearchResultClick(selectedFile.matchResult.join(''));
}
} else if (e.key === 'Escape') {
searchQuery.value = '';
if (searchInputElement) searchInputElement.value = '';
}
};
const clearSearch = () => {
searchQuery.value = '';
if (searchInputElement) searchInputElement.value = '';
};
const scrollSelectedIntoView = () => {
nextTick(() => {
const resultsEl = searchResults.value;
@ -79,8 +87,7 @@ const handleClickOutside = (e: MouseEvent) => {
// Check if click is outside search input and results
if (searchInputElement && !searchInputElement.contains(target) &&
resultsEl && !resultsEl.contains(target)) {
searchQuery.value = '';
if (searchInputElement) searchInputElement.value = '';
clearSearch();
}
};
@ -133,8 +140,7 @@ onUnmounted(() => {
});
function handleSearchResultClick(filePath: string) {
searchQuery.value = '';
if (searchInputElement) searchInputElement.value = '';
clearSearch();
window.location.href = `${treeLink.value}/${pathEscapeSegments(filePath)}`;
}
</script>
@ -151,7 +157,8 @@ function handleSearchResultClick(filePath: string) {
@mouseenter="selectedIndex = idx"
:title="result.matchResult.join('')"
>
<span v-html="svg('octicon-file', 16)"></span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="svg('octicon-file', 16)"/>
<span class="file-tree-search-result-path">
<span
v-for="(part, index) in result.matchResult"
@ -161,8 +168,12 @@ function handleSearchResultClick(filePath: string) {
</span>
</div>
</div>
<div v-if="searchQuery && filteredFiles.length === 0" class="file-tree-search-no-results">
No matching file found
<div v-if="searchQuery && filteredFiles.length === 0" ref="searchResults" class="file-tree-search-results file-tree-search-no-results">
<div class="file-tree-no-results-content">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="svg('octicon-search', 24)"/>
<span>No matching files found</span>
</div>
</div>
</Teleport>
<div class="view-file-tree-items">
@ -209,7 +220,7 @@ function handleSearchResultClick(filePath: string) {
border-bottom: 1px solid var(--color-secondary);
}
.file-tree-search-result-item svg {
.file-tree-search-result-item > span:first-child {
flex-shrink: 0;
margin-top: 0.125rem;
}
@ -236,9 +247,20 @@ function handleSearchResultClick(filePath: string) {
}
.file-tree-search-no-results {
padding: 1rem;
text-align: center;
padding: 0;
}
.file-tree-no-results-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1.5rem;
color: var(--color-text-light-2);
font-size: 14px;
}
.file-tree-no-results-content > span:first-child {
opacity: 0.5;
}
</style>

View File

@ -1,28 +0,0 @@
// Handle repository file/directory actions dropdown
export function initRepoFileActions() {
const centerContentCheckbox = document.querySelector<HTMLInputElement>('#center-content-checkbox');
if (!centerContentCheckbox) return;
// Load saved preference
const isCentered = localStorage.getItem('repo-content-centered') === 'true';
centerContentCheckbox.checked = isCentered;
applyCenterContent(isCentered);
// Handle checkbox change
centerContentCheckbox.addEventListener('change', () => {
const centered = centerContentCheckbox.checked;
localStorage.setItem('repo-content-centered', String(centered));
applyCenterContent(centered);
});
}
function applyCenterContent(centered: boolean) {
const container = document.querySelector('.ui.container');
if (!container) return;
if (centered) {
container.classList.add('repo-content-centered');
} else {
container.classList.remove('repo-content-centered');
}
}

View File

@ -64,7 +64,6 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton}
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
import {callInitFunctions} from './modules/init.ts';
import {initRepoViewFileTree} from './features/repo-view-file-tree.ts';
import {initRepoFileActions} from './features/repo-file-actions.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
@ -138,7 +137,6 @@ const initPerformanceTracer = callInitFunctions([
initRepoReleaseNew,
initRepoTopicBar,
initRepoViewFileTree,
initRepoFileActions,
initRepoWikiForm,
initRepository,
initRepositoryActionView,