mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-06 03:53:59 +02:00
feat: introduce a copy menu into the file view
This commit is contained in:
parent
2162e5ff64
commit
c7012528fc
@ -17,3 +17,26 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
|||||||
NewGitCmd("diff").Arg("--submodule", "--no-ext-diff", "--color").Arg(diffArgs...).ToArgv(),
|
NewGitCmd("diff").Arg("--submodule", "--no-ext-diff", "--color").Arg(diffArgs...).ToArgv(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *DiffCommands) internalDiffCmdObj(diffArgs ...string) *GitCommandBuilder {
|
||||||
|
return NewGitCmd("diff").
|
||||||
|
Arg("--no-ext-diff", "--no-color").
|
||||||
|
Arg(diffArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DiffCommands) GetPathDiff(path string, staged bool) (string, error) {
|
||||||
|
return self.cmd.New(
|
||||||
|
self.internalDiffCmdObj().
|
||||||
|
ArgIf(staged, "--staged").
|
||||||
|
Arg(path).
|
||||||
|
ToArgv(),
|
||||||
|
).RunWithOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *DiffCommands) GetAllDiff(staged bool) (string, error) {
|
||||||
|
return self.cmd.New(
|
||||||
|
self.internalDiffCmdObj().
|
||||||
|
ArgIf(staged, "--staged").
|
||||||
|
ToArgv(),
|
||||||
|
).RunWithOutput()
|
||||||
|
}
|
||||||
|
@ -371,6 +371,7 @@ type KeybindingFilesConfig struct {
|
|||||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||||
OpenMergeTool string `yaml:"openMergeTool"`
|
OpenMergeTool string `yaml:"openMergeTool"`
|
||||||
OpenStatusFilter string `yaml:"openStatusFilter"`
|
OpenStatusFilter string `yaml:"openStatusFilter"`
|
||||||
|
CopyFileInfoToClipboard string `yaml:"copyFileInfoToClipboard"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingBranchesConfig struct {
|
type KeybindingBranchesConfig struct {
|
||||||
@ -763,6 +764,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
OpenMergeTool: "M",
|
OpenMergeTool: "M",
|
||||||
OpenStatusFilter: "<c-b>",
|
OpenStatusFilter: "<c-b>",
|
||||||
ConfirmDiscard: "x",
|
ConfirmDiscard: "x",
|
||||||
|
CopyFileInfoToClipboard: "y",
|
||||||
},
|
},
|
||||||
Branches: KeybindingBranchesConfig{
|
Branches: KeybindingBranchesConfig{
|
||||||
CopyPullRequestURL: "<c-y>",
|
CopyPullRequestURL: "<c-y>",
|
||||||
|
@ -37,6 +37,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||||||
Handler: self.handleStatusFilterPressed,
|
Handler: self.handleStatusFilterPressed,
|
||||||
Description: self.c.Tr.FileFilter,
|
Description: self.c.Tr.FileFilter,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: opts.GetKey(opts.Config.Files.CopyFileInfoToClipboard),
|
||||||
|
Handler: self.openCopyMenu,
|
||||||
|
Description: self.c.Tr.CopyToClipboardMenu,
|
||||||
|
OpensMenu: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Key: opts.GetKey(opts.Config.Files.CommitChanges),
|
Key: opts.GetKey(opts.Config.Files.CommitChanges),
|
||||||
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
|
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
|
||||||
@ -748,6 +754,103 @@ func (self *FilesController) createStashMenu() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *FilesController) openCopyMenu() error {
|
||||||
|
node := self.context().GetSelected()
|
||||||
|
|
||||||
|
copyNameItem := &types.MenuItem{
|
||||||
|
Label: self.c.Tr.CopyFileName,
|
||||||
|
OnPress: func() error {
|
||||||
|
if err := self.c.OS().CopyToClipboard(node.Name()); err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
self.c.Toast(self.c.Tr.FileNameCopiedToast)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Key: 'n',
|
||||||
|
}
|
||||||
|
copyPathItem := &types.MenuItem{
|
||||||
|
Label: self.c.Tr.CopyFilePath,
|
||||||
|
OnPress: func() error {
|
||||||
|
if err := self.c.OS().CopyToClipboard(node.Path); err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
self.c.Toast(self.c.Tr.FilePathCopiedToast)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Key: 'p',
|
||||||
|
}
|
||||||
|
copyFileDiffItem := &types.MenuItem{
|
||||||
|
Label: self.c.Tr.CopySelectedDiff,
|
||||||
|
Tooltip: self.c.Tr.CopyFileDiffTooltip,
|
||||||
|
OnPress: func() error {
|
||||||
|
path := self.context().GetSelectedPath()
|
||||||
|
hasStaged := self.hasPathStagedChanges(node)
|
||||||
|
diff, err := self.c.Git().Diff.GetPathDiff(path, hasStaged)
|
||||||
|
if err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
if err := self.c.OS().CopyToClipboard(diff); err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
self.c.Toast(self.c.Tr.FileDiffCopiedToast)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Key: 's',
|
||||||
|
}
|
||||||
|
copyAllDiff := &types.MenuItem{
|
||||||
|
Label: self.c.Tr.CopyAllFilesDiff,
|
||||||
|
Tooltip: self.c.Tr.CopyFileDiffTooltip,
|
||||||
|
OnPress: func() error {
|
||||||
|
hasStaged := self.c.Helpers().WorkingTree.AnyStagedFiles()
|
||||||
|
diff, err := self.c.Git().Diff.GetAllDiff(hasStaged)
|
||||||
|
if err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
if err := self.c.OS().CopyToClipboard(diff); err != nil {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
self.c.Toast(self.c.Tr.AllFilesDiffCopiedToast)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Key: 'a',
|
||||||
|
}
|
||||||
|
|
||||||
|
if node == nil {
|
||||||
|
copyNameItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||||
|
copyPathItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||||
|
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||||
|
}
|
||||||
|
if node != nil && !node.GetHasStagedOrTrackedChanges() {
|
||||||
|
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||||
|
}
|
||||||
|
if !self.anyStagedOrTrackedFile() {
|
||||||
|
copyAllDiff.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
|
Title: self.c.Tr.CopyToClipboardMenu,
|
||||||
|
Items: []*types.MenuItem{
|
||||||
|
copyNameItem,
|
||||||
|
copyPathItem,
|
||||||
|
copyFileDiffItem,
|
||||||
|
copyAllDiff,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilesController) anyStagedOrTrackedFile() bool {
|
||||||
|
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
|
||||||
|
return self.c.Helpers().WorkingTree.AnyTrackedFiles()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilesController) hasPathStagedChanges(node *filetree.FileNode) bool {
|
||||||
|
return node.SomeFile(func(t *models.File) bool {
|
||||||
|
return t.HasStagedChanges
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (self *FilesController) stash() error {
|
func (self *FilesController) stash() error {
|
||||||
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
|
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,15 @@ func (self *FileNode) GetHasUnstagedChanges() bool {
|
|||||||
return self.SomeFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
return self.SomeFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *FileNode) GetHasStagedOrTrackedChanges() bool {
|
||||||
|
if !self.GetHasStagedChanges() {
|
||||||
|
return self.SomeFile(func(t *models.File) bool {
|
||||||
|
return t.Tracked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (self *FileNode) GetHasStagedChanges() bool {
|
func (self *FileNode) GetHasStagedChanges() bool {
|
||||||
return self.SomeFile(func(file *models.File) bool { return file.HasStagedChanges })
|
return self.SomeFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package filetree
|
package filetree
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -300,3 +302,7 @@ func (self *Node[T]) ID() string {
|
|||||||
func (self *Node[T]) Description() string {
|
func (self *Node[T]) Description() string {
|
||||||
return self.GetPath()
|
return self.GetPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Node[T]) Name() string {
|
||||||
|
return path.Base(self.Path)
|
||||||
|
}
|
||||||
|
@ -52,6 +52,17 @@ type TranslationSet struct {
|
|||||||
Pull string
|
Pull string
|
||||||
Scroll string
|
Scroll string
|
||||||
FileFilter string
|
FileFilter string
|
||||||
|
CopyToClipboardMenu string
|
||||||
|
CopyFileName string
|
||||||
|
CopyFilePath string
|
||||||
|
CopyFileDiffTooltip string
|
||||||
|
CopySelectedDiff string
|
||||||
|
CopyAllFilesDiff string
|
||||||
|
NoContentToCopyError string
|
||||||
|
FileNameCopiedToast string
|
||||||
|
FilePathCopiedToast string
|
||||||
|
FileDiffCopiedToast string
|
||||||
|
AllFilesDiffCopiedToast string
|
||||||
FilterStagedFiles string
|
FilterStagedFiles string
|
||||||
FilterUnstagedFiles string
|
FilterUnstagedFiles string
|
||||||
ResetFilter string
|
ResetFilter string
|
||||||
@ -851,6 +862,17 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch",
|
CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch",
|
||||||
CantPullOrPushSameBranchTwice: "You cannot push or pull a branch while it is already being pushed or pulled",
|
CantPullOrPushSameBranchTwice: "You cannot push or pull a branch while it is already being pushed or pulled",
|
||||||
FileFilter: "Filter files by status",
|
FileFilter: "Filter files by status",
|
||||||
|
CopyToClipboardMenu: "Copy to clipboard",
|
||||||
|
CopyFileName: "File name",
|
||||||
|
CopyFilePath: "Path",
|
||||||
|
CopyFileDiffTooltip: "If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.",
|
||||||
|
CopySelectedDiff: "Diff of selected file",
|
||||||
|
CopyAllFilesDiff: "Diff of all files",
|
||||||
|
NoContentToCopyError: "Nothing to copy",
|
||||||
|
FileNameCopiedToast: "File name copied to clipboard",
|
||||||
|
FilePathCopiedToast: "File path copied to clipboard",
|
||||||
|
FileDiffCopiedToast: "File diff copied to clipboard",
|
||||||
|
AllFilesDiffCopiedToast: "All files diff copied to clipboard",
|
||||||
FilterStagedFiles: "Show only staged files",
|
FilterStagedFiles: "Show only staged files",
|
||||||
FilterUnstagedFiles: "Show only unstaged files",
|
FilterUnstagedFiles: "Show only unstaged files",
|
||||||
ResetFilter: "Reset filter",
|
ResetFilter: "Reset filter",
|
||||||
|
185
pkg/integration/tests/file/copy_menu.go
Normal file
185
pkg/integration/tests/file/copy_menu.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
// note: this is required to simulate the clipboard during CI
|
||||||
|
func expectClipboard(t *TestDriver, matcher *TextMatcher) {
|
||||||
|
defer t.Shell().DeleteFile("clipboard")
|
||||||
|
|
||||||
|
t.FileSystem().FileContent("clipboard", matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
|
Description: "The copy menu allows to copy name and diff of selected/all files",
|
||||||
|
ExtraCmdArgs: []string{},
|
||||||
|
Skip: false,
|
||||||
|
SetupConfig: func(config *config.AppConfig) {
|
||||||
|
config.UserConfig.OS.CopyToClipboardCmd = "echo {{text}} > clipboard"
|
||||||
|
},
|
||||||
|
SetupRepo: func(shell *Shell) {},
|
||||||
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
|
// Disabled item
|
||||||
|
t.Views().Files().
|
||||||
|
IsEmpty().
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("File name")).
|
||||||
|
Tooltip(Equals("Disabled: Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
t.ExpectPopup().Alert().
|
||||||
|
Title(Equals("Error")).
|
||||||
|
Content(Equals("Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Shell().
|
||||||
|
CreateDir("dir").
|
||||||
|
CreateFile("dir/1-unstaged_file", "unstaged content")
|
||||||
|
|
||||||
|
// Empty content (new file)
|
||||||
|
t.Views().Files().
|
||||||
|
Press(keys.Universal.Refresh).
|
||||||
|
Lines(
|
||||||
|
Contains("dir").IsSelected(),
|
||||||
|
Contains("unstaged_file"),
|
||||||
|
).
|
||||||
|
SelectNextItem().
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of selected file")).
|
||||||
|
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
t.ExpectPopup().Alert().
|
||||||
|
Title(Equals("Error")).
|
||||||
|
Content(Equals("Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
}).
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of all files")).
|
||||||
|
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
t.ExpectPopup().Alert().
|
||||||
|
Title(Equals("Error")).
|
||||||
|
Content(Equals("Nothing to copy")).
|
||||||
|
Confirm()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Shell().
|
||||||
|
GitAdd("dir/1-unstaged_file").
|
||||||
|
Commit("commit-unstaged").
|
||||||
|
UpdateFile("dir/1-unstaged_file", "unstaged content (new)").
|
||||||
|
CreateFileAndAdd("dir/2-staged_file", "staged content").
|
||||||
|
Commit("commit-staged").
|
||||||
|
UpdateFile("dir/2-staged_file", "staged content (new)").
|
||||||
|
GitAdd("dir/2-staged_file")
|
||||||
|
|
||||||
|
// Copy file name
|
||||||
|
t.Views().Files().
|
||||||
|
Press(keys.Universal.Refresh).
|
||||||
|
Lines(
|
||||||
|
Contains("dir"),
|
||||||
|
Contains("unstaged_file").IsSelected(),
|
||||||
|
Contains("staged_file"),
|
||||||
|
).
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("File name")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("unstaged_file"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Copy file path
|
||||||
|
t.Views().Files().
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Path")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("dir/1-unstaged_file"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Selected path diff on a single (unstaged) file
|
||||||
|
t.Views().Files().
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of selected file")).
|
||||||
|
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("+unstaged content (new)"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Selected path diff with staged and unstaged files
|
||||||
|
t.Views().Files().
|
||||||
|
SelectPreviousItem().
|
||||||
|
Lines(
|
||||||
|
Contains("dir").IsSelected(),
|
||||||
|
Contains("unstaged_file"),
|
||||||
|
Contains("staged_file"),
|
||||||
|
).
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of selected file")).
|
||||||
|
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("+staged content (new)"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// All files diff with staged files
|
||||||
|
t.Views().Files().
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of all files")).
|
||||||
|
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("+staged content (new)"))
|
||||||
|
})
|
||||||
|
|
||||||
|
// All files diff with no staged files
|
||||||
|
t.Views().Files().
|
||||||
|
SelectNextItem().
|
||||||
|
SelectNextItem().
|
||||||
|
Lines(
|
||||||
|
Contains("dir"),
|
||||||
|
Contains("unstaged_file"),
|
||||||
|
Contains("staged_file").IsSelected(),
|
||||||
|
).
|
||||||
|
Press(keys.Universal.Select).
|
||||||
|
Press(keys.Files.CopyFileInfoToClipboard).
|
||||||
|
Tap(func() {
|
||||||
|
t.ExpectPopup().Menu().
|
||||||
|
Title(Equals("Copy to clipboard")).
|
||||||
|
Select(Contains("Diff of all files")).
|
||||||
|
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||||
|
Confirm()
|
||||||
|
|
||||||
|
expectClipboard(t, Contains("+staged content (new)").Contains("+unstaged content (new)"))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
@ -117,6 +117,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
diff.DiffAndApplyPatch,
|
diff.DiffAndApplyPatch,
|
||||||
diff.DiffCommits,
|
diff.DiffCommits,
|
||||||
diff.IgnoreWhitespace,
|
diff.IgnoreWhitespace,
|
||||||
|
file.CopyMenu,
|
||||||
file.DirWithUntrackedFile,
|
file.DirWithUntrackedFile,
|
||||||
file.DiscardAllDirChanges,
|
file.DiscardAllDirChanges,
|
||||||
file.DiscardChanges,
|
file.DiscardChanges,
|
||||||
|
Loading…
Reference in New Issue
Block a user