mirror of https://github.com/go-gitea/gitea.git
feat(repo): enable delete directory
parent
e0206bc9dd
commit
10ddd2a1b0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue