feat: improved conflict resolution when uploading/copying/moving files (#5765)
This commit is contained in:
@@ -177,9 +177,12 @@ function moveCopy(
|
|||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const from = item.from;
|
const from = item.from;
|
||||||
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
||||||
|
const finalOverwrite =
|
||||||
|
item.overwrite == undefined ? overwrite : item.overwrite;
|
||||||
|
const finalRename = item.rename == undefined ? rename : item.rename;
|
||||||
const url = `${from}?action=${
|
const url = `${from}?action=${
|
||||||
copy ? "copy" : "rename"
|
copy ? "copy" : "rename"
|
||||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
}&destination=${to}&override=${finalOverwrite}&rename=${finalRename}`;
|
||||||
promises.push(resourceAction(url, "PATCH"));
|
promises.push(resourceAction(url, "PATCH"));
|
||||||
}
|
}
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ const drop = async (event: Event) => {
|
|||||||
from: fileStore.req?.items[i].url,
|
from: fileStore.req?.items[i].url,
|
||||||
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||||
name: fileStore.req?.items[i].name,
|
name: fileStore.req?.items[i].name,
|
||||||
|
size: fileStore.req?.items[i].size,
|
||||||
|
modified: fileStore.req?.items[i].modified,
|
||||||
|
overwrite: false,
|
||||||
|
rename: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,7 @@ const drop = async (event: Event) => {
|
|||||||
const path = el.__vue__.url;
|
const path = el.__vue__.url;
|
||||||
const baseItems = (await api.fetch(path)).items;
|
const baseItems = (await api.fetch(path)).items;
|
||||||
|
|
||||||
const action = (overwrite: boolean, rename: boolean) => {
|
const action = (overwrite?: boolean, rename?: boolean) => {
|
||||||
api
|
api
|
||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -200,26 +204,35 @@ const drop = async (event: Event) => {
|
|||||||
|
|
||||||
const conflict = upload.checkConflict(items, baseItems);
|
const conflict = upload.checkConflict(items, baseItems);
|
||||||
|
|
||||||
let overwrite = false;
|
if (conflict.length > 0) {
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
if (conflict) {
|
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "resolve-conflict",
|
||||||
confirm: (event: Event, option: any) => {
|
props: {
|
||||||
overwrite = option == "overwrite";
|
conflict: conflict,
|
||||||
rename = option == "rename";
|
},
|
||||||
|
confirm: (event: Event, result: Array<ConflictingResource>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
action(overwrite, rename);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
items[item.index].rename = true;
|
||||||
|
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
|
||||||
|
items[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
items.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length > 0) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(false, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemClick = (event: Event | KeyboardEvent) => {
|
const itemClick = (event: Event | KeyboardEvent) => {
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export default {
|
|||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: this.req.items[item].name,
|
||||||
|
size: this.req.items[item].size,
|
||||||
|
modified: this.req.items[item].modified,
|
||||||
|
overwrite: false,
|
||||||
|
rename: this.$route.path === this.dest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,36 +122,41 @@ export default {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
|
||||||
this.closeHovers();
|
|
||||||
action(false, true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(this.dest)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
if (conflict.length > 0) {
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
if (conflict) {
|
|
||||||
this.showHover({
|
this.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "resolve-conflict",
|
||||||
confirm: (event, option) => {
|
props: {
|
||||||
overwrite = option == "overwrite";
|
conflict: conflict,
|
||||||
rename = option == "rename";
|
},
|
||||||
|
confirm: (event, result) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
action(overwrite, rename);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
items[item.index].rename = true;
|
||||||
|
} else if (
|
||||||
|
item.checked.length == 1 &&
|
||||||
|
item.checked[0] == "origin"
|
||||||
|
) {
|
||||||
|
items[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
items.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length > 0) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(false, false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ export default {
|
|||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: this.req.items[item].name,
|
||||||
|
size: this.req.items[item].size,
|
||||||
|
modified: this.req.items[item].modified,
|
||||||
|
overwrite: false,
|
||||||
|
rename: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,26 +125,39 @@ export default {
|
|||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(this.dest)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
if (conflict.length > 0) {
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
if (conflict) {
|
|
||||||
this.showHover({
|
this.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "resolve-conflict",
|
||||||
confirm: (event, option) => {
|
props: {
|
||||||
overwrite = option == "overwrite";
|
conflict: conflict,
|
||||||
rename = option == "rename";
|
files: items,
|
||||||
|
},
|
||||||
|
confirm: (event, result) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
action(overwrite, rename);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
items[item.index].rename = true;
|
||||||
|
} else if (
|
||||||
|
item.checked.length == 1 &&
|
||||||
|
item.checked[0] == "origin"
|
||||||
|
) {
|
||||||
|
items[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
items.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length > 0) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(false, false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import Copy from "./Copy.vue";
|
|||||||
import NewFile from "./NewFile.vue";
|
import NewFile from "./NewFile.vue";
|
||||||
import NewDir from "./NewDir.vue";
|
import NewDir from "./NewDir.vue";
|
||||||
import Replace from "./Replace.vue";
|
import Replace from "./Replace.vue";
|
||||||
import ReplaceRename from "./ReplaceRename.vue";
|
|
||||||
import Share from "./Share.vue";
|
import Share from "./Share.vue";
|
||||||
import ShareDelete from "./ShareDelete.vue";
|
import ShareDelete from "./ShareDelete.vue";
|
||||||
import Upload from "./Upload.vue";
|
import Upload from "./Upload.vue";
|
||||||
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
|
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
|
||||||
|
import ResolveConflict from "./ResolveConflict.vue";
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
@@ -44,12 +44,12 @@ const components = new Map<string, any>([
|
|||||||
["newDir", NewDir],
|
["newDir", NewDir],
|
||||||
["download", Download],
|
["download", Download],
|
||||||
["replace", Replace],
|
["replace", Replace],
|
||||||
["replace-rename", ReplaceRename],
|
|
||||||
["share", Share],
|
["share", Share],
|
||||||
["upload", Upload],
|
["upload", Upload],
|
||||||
["share-delete", ShareDelete],
|
["share-delete", ShareDelete],
|
||||||
["deleteUser", DeleteUser],
|
["deleteUser", DeleteUser],
|
||||||
["discardEditorChanges", DiscardEditorChanges],
|
["discardEditorChanges", DiscardEditorChanges],
|
||||||
|
["resolve-conflict", ResolveConflict],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const modal = computed(() => {
|
const modal = computed(() => {
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="card floating">
|
|
||||||
<div class="card-title">
|
|
||||||
<h2>{{ $t("prompts.replace") }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-action">
|
|
||||||
<button
|
|
||||||
class="button button--flat button--grey"
|
|
||||||
@click="closeHovers"
|
|
||||||
:aria-label="$t('buttons.cancel')"
|
|
||||||
:title="$t('buttons.cancel')"
|
|
||||||
tabindex="3"
|
|
||||||
>
|
|
||||||
{{ $t("buttons.cancel") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="button button--flat button--blue"
|
|
||||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
|
||||||
:aria-label="$t('buttons.rename')"
|
|
||||||
:title="$t('buttons.rename')"
|
|
||||||
tabindex="2"
|
|
||||||
>
|
|
||||||
{{ $t("buttons.rename") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="focus-prompt"
|
|
||||||
class="button button--flat button--red"
|
|
||||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
|
||||||
:aria-label="$t('buttons.replace')"
|
|
||||||
:title="$t('buttons.replace')"
|
|
||||||
tabindex="1"
|
|
||||||
>
|
|
||||||
{{ $t("buttons.replace") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapActions, mapState } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "replace-rename",
|
|
||||||
computed: {
|
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
307
frontend/src/components/prompts/ResolveConflict.vue
Normal file
307
frontend/src/components/prompts/ResolveConflict.vue
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>
|
||||||
|
{{
|
||||||
|
personalized
|
||||||
|
? $t("prompts.resolveConflict")
|
||||||
|
: $t("prompts.replaceOrSkip")
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<template v-if="personalized">
|
||||||
|
<p v-if="isUploadAction != true">
|
||||||
|
{{ $t("prompts.singleConflictResolve") }}
|
||||||
|
</p>
|
||||||
|
<div class="conflict-list-container">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
@change="toogleCheckAll"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="originAllChecked"
|
||||||
|
value="origin"
|
||||||
|
/>
|
||||||
|
{{
|
||||||
|
isUploadAction != true
|
||||||
|
? $t("prompts.filesInOrigin")
|
||||||
|
: $t("prompts.uploadingFiles")
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
@change="toogleCheckAll"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="destAllChecked"
|
||||||
|
value="dest"
|
||||||
|
/>
|
||||||
|
{{ $t("prompts.filesInDest") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<template v-for="(item, index) in conflict" :key="index">
|
||||||
|
<div class="conflict-file-name">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
|
||||||
|
<template v-if="item.checked.length == 2">
|
||||||
|
<span v-if="isUploadAction != true" class="result-rename">
|
||||||
|
{{ $t("prompts.rename") }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="result-error">
|
||||||
|
{{ $t("prompts.forbiddenError") }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
item.checked.length == 1 && item.checked[0] == 'origin'
|
||||||
|
"
|
||||||
|
class="result-override"
|
||||||
|
>
|
||||||
|
{{ $t("prompts.override") }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="result-skip">
|
||||||
|
{{ $t("prompts.skip") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input v-model="item.checked" type="checkbox" value="origin" />
|
||||||
|
<div>
|
||||||
|
<p class="conflict-file-value">
|
||||||
|
{{ humanTime(item.origin.lastModified) }}
|
||||||
|
</p>
|
||||||
|
<p class="conflict-file-value">
|
||||||
|
{{ humanSize(item.origin.size) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input v-model="item.checked" type="checkbox" value="dest" />
|
||||||
|
<div>
|
||||||
|
<p class="conflict-file-value">
|
||||||
|
{{ humanTime(item.dest.lastModified) }}
|
||||||
|
</p>
|
||||||
|
<p class="conflict-file-value">
|
||||||
|
{{ humanSize(item.dest.size) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>
|
||||||
|
{{ $t("prompts.fastConflictResolve", { count: conflict.length }) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="result-buttons">
|
||||||
|
<button @click="(e) => resolve(e, ['origin'])">
|
||||||
|
<i class="material-icons">done_all</i>
|
||||||
|
{{ $t("buttons.overrideAll") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isUploadAction != true"
|
||||||
|
@click="(e) => resolve(e, ['origin', 'dest'])"
|
||||||
|
>
|
||||||
|
<i class="material-icons">folder_copy</i>
|
||||||
|
{{ $t("buttons.renameAll") }}
|
||||||
|
</button>
|
||||||
|
<button @click="(e) => resolve(e, ['dest'])">
|
||||||
|
<i class="material-icons">undo</i>
|
||||||
|
{{ $t("buttons.skipAll") }}
|
||||||
|
</button>
|
||||||
|
<button @click="personalized = true">
|
||||||
|
<i class="material-icons">checklist</i>
|
||||||
|
{{ $t("buttons.singleDecision") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action" style="display: flex; justify-content: end">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="button button--flat button--grey"
|
||||||
|
@click="close"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')"
|
||||||
|
tabindex="4"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="personalized"
|
||||||
|
id="focus-prompt"
|
||||||
|
class="button button--flat"
|
||||||
|
@click="(event) => currentPrompt?.confirm(event, conflict)"
|
||||||
|
:aria-label="$t('buttons.ok')"
|
||||||
|
:title="$t('buttons.ok')"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.ok") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import { filesize } from "@/utils";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const { currentPrompt } = layoutStore;
|
||||||
|
|
||||||
|
const conflict = ref<ConflictingResource[]>(currentPrompt?.props.conflict);
|
||||||
|
|
||||||
|
const isUploadAction = ref<boolean | undefined>(
|
||||||
|
currentPrompt?.props.isUploadAction
|
||||||
|
);
|
||||||
|
|
||||||
|
const personalized = ref(false);
|
||||||
|
|
||||||
|
const originAllChecked = computed(() => {
|
||||||
|
for (const item of conflict.value) {
|
||||||
|
if (!item.checked.includes("origin")) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const destAllChecked = computed(() => {
|
||||||
|
for (const item of conflict.value) {
|
||||||
|
if (!item.checked.includes("dest")) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
layoutStore.closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const humanSize = (size: number | undefined) => {
|
||||||
|
return size == undefined ? "Unknown size" : filesize(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const humanTime = (modified: string | number | undefined) => {
|
||||||
|
if (modified == undefined) return "Unknown date";
|
||||||
|
|
||||||
|
return dayjs(modified).format("L LT");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolve = (event: Event, result: Array<"origin" | "dest">) => {
|
||||||
|
for (const item of conflict.value) {
|
||||||
|
item.checked = result;
|
||||||
|
}
|
||||||
|
currentPrompt?.confirm(event, conflict.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toogleCheckAll = (e: Event) => {
|
||||||
|
const target = e.currentTarget as HTMLInputElement;
|
||||||
|
const value = target.value as "origin" | "dest" | "both";
|
||||||
|
const checked = target.checked;
|
||||||
|
|
||||||
|
for (const item of conflict.value) {
|
||||||
|
if (value == "both") {
|
||||||
|
item.checked = ["origin", "dest"];
|
||||||
|
} else {
|
||||||
|
if (!item.checked.includes(value)) {
|
||||||
|
if (checked) {
|
||||||
|
item.checked.push(value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!checked) {
|
||||||
|
item.checked = value == "dest" ? ["origin"] : ["dest"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.conflict-list-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-list-container > div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
border-bottom: solid 1px var(--textPrimary);
|
||||||
|
gap: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-list-container > div:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-list-container > div > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-name {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-file-value {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-rename,
|
||||||
|
.result-override,
|
||||||
|
.result-error,
|
||||||
|
.result-skip {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 0.75rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-override {
|
||||||
|
background-color: var(--input-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
background-color: var(--icon-red);
|
||||||
|
}
|
||||||
|
.result-rename {
|
||||||
|
background-color: var(--icon-orange);
|
||||||
|
}
|
||||||
|
.result-skip {
|
||||||
|
background-color: var(--icon-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-buttons > button {
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: solid 1px transparent;
|
||||||
|
width: 100%;
|
||||||
|
transition: all ease-in-out 200ms;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-buttons > button:hover {
|
||||||
|
border: solid 1px var(--icon-blue);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -69,18 +69,29 @@ const uploadInput = (event: Event) => {
|
|||||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||||
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict.length > 0) {
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace",
|
prompt: "resolve-conflict",
|
||||||
action: (event: Event) => {
|
props: {
|
||||||
event.preventDefault();
|
conflict: conflict,
|
||||||
layoutStore.closeHovers();
|
isUploadAction: true,
|
||||||
upload.handleFiles(uploadFiles, path, false);
|
|
||||||
},
|
},
|
||||||
confirm: (event: Event) => {
|
confirm: (event: Event, result: Array<ConflictingResource>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
upload.handleFiles(uploadFiles, path, true);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
continue;
|
||||||
|
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
|
||||||
|
uploadFiles[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
uploadFiles.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uploadFiles.length > 0) {
|
||||||
|
upload.handleFiles(uploadFiles, path);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,11 @@
|
|||||||
"saveChanges": "Save changes",
|
"saveChanges": "Save changes",
|
||||||
"editAsText": "Edit as Text",
|
"editAsText": "Edit as Text",
|
||||||
"increaseFontSize": "Increase font size",
|
"increaseFontSize": "Increase font size",
|
||||||
"decreaseFontSize": "Decrease font size"
|
"decreaseFontSize": "Decrease font size",
|
||||||
|
"overrideAll": "Replace all files in destination folder",
|
||||||
|
"skipAll": "Skip all conflicting files",
|
||||||
|
"renameAll": "Rename all files (create a copy)",
|
||||||
|
"singleDecision": "Decide for each conflicting file"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download File",
|
"downloadFile": "Download File",
|
||||||
@@ -161,7 +165,17 @@
|
|||||||
"uploadMessage": "Select an option to upload.",
|
"uploadMessage": "Select an option to upload.",
|
||||||
"optionalPassword": "Optional password",
|
"optionalPassword": "Optional password",
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?"
|
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?",
|
||||||
|
"replaceOrSkip": "Replace or skip files",
|
||||||
|
"resolveConflict": "Which files do you want to keep?",
|
||||||
|
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
|
||||||
|
"fastConflictResolve": "The destination folder there are {count} files with same name.",
|
||||||
|
"uploadingFiles": "Uploading files",
|
||||||
|
"filesInOrigin": "Files in origin",
|
||||||
|
"filesInDest": "Files in destination",
|
||||||
|
"override": "Overwrite",
|
||||||
|
"skip": "Skip",
|
||||||
|
"forbiddenError": "Forbidden Error"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
|
|||||||
15
frontend/src/types/file.d.ts
vendored
15
frontend/src/types/file.d.ts
vendored
@@ -52,6 +52,8 @@ type DownloadFormat =
|
|||||||
interface ClipItem {
|
interface ClipItem {
|
||||||
from: string;
|
from: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
modified?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreadCrumb {
|
interface BreadCrumb {
|
||||||
@@ -59,6 +61,19 @@ interface BreadCrumb {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConflictingItem {
|
||||||
|
lastModified: number | string | undefined;
|
||||||
|
size: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConflictingResource {
|
||||||
|
index: number;
|
||||||
|
name: string;
|
||||||
|
origin: ConflictingItem;
|
||||||
|
dest: ConflictingItem;
|
||||||
|
checked: Array<"origin" | "dest">;
|
||||||
|
}
|
||||||
|
|
||||||
interface CsvData {
|
interface CsvData {
|
||||||
headers: string[];
|
headers: string[];
|
||||||
rows: string[][];
|
rows: string[][];
|
||||||
|
|||||||
1
frontend/src/types/upload.d.ts
vendored
1
frontend/src/types/upload.d.ts
vendored
@@ -17,6 +17,7 @@ interface UploadEntry {
|
|||||||
isDir: boolean;
|
isDir: boolean;
|
||||||
fullPath?: string;
|
fullPath?: string;
|
||||||
file?: File;
|
file?: File;
|
||||||
|
overwrite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadList = UploadEntry[];
|
type UploadList = UploadEntry[];
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ import { useUploadStore } from "@/stores/upload";
|
|||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
|
|
||||||
export function checkConflict(
|
export function checkConflict(
|
||||||
files: UploadList,
|
files: UploadList | Array<any>,
|
||||||
dest: ResourceItem[]
|
dest: ResourceItem[]
|
||||||
): boolean {
|
): ConflictingResource[] {
|
||||||
if (typeof dest === "undefined" || dest === null) {
|
if (typeof dest === "undefined" || dest === null) {
|
||||||
dest = [];
|
dest = [];
|
||||||
}
|
}
|
||||||
|
const conflictingFiles: ConflictingResource[] = [];
|
||||||
|
|
||||||
const folder_upload = files[0].fullPath !== undefined;
|
const folder_upload = files[0].fullPath !== undefined;
|
||||||
|
|
||||||
const names = new Set<string>();
|
function getFile(name: string): ResourceItem | null {
|
||||||
|
for (const item of dest) {
|
||||||
|
if (item.name == name) return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
let name = file.name;
|
let name = file.name;
|
||||||
@@ -24,10 +32,25 @@ export function checkConflict(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
names.add(name);
|
const item = getFile(name);
|
||||||
|
if (item != null) {
|
||||||
|
conflictingFiles.push({
|
||||||
|
index: i,
|
||||||
|
name: item.path,
|
||||||
|
origin: {
|
||||||
|
lastModified: file.modified || file.file?.lastModified,
|
||||||
|
size: file.size,
|
||||||
|
},
|
||||||
|
dest: {
|
||||||
|
lastModified: item.modified,
|
||||||
|
size: item.size,
|
||||||
|
},
|
||||||
|
checked: ["origin"],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dest.some((d) => names.has(d.name));
|
return conflictingFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
|
export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
|
||||||
@@ -146,6 +169,12 @@ export function handleFiles(
|
|||||||
|
|
||||||
const type = file.isDir ? "dir" : detectType((file.file as File).type);
|
const type = file.isDir ? "dir" : detectType((file.file as File).type);
|
||||||
|
|
||||||
uploadStore.upload(path, file.name, file.file ?? null, overwrite, type);
|
uploadStore.upload(
|
||||||
|
path,
|
||||||
|
file.name,
|
||||||
|
file.file ?? null,
|
||||||
|
file.overwrite || overwrite,
|
||||||
|
type
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,6 +628,8 @@ const copyCut = (event: Event | KeyboardEvent): void => {
|
|||||||
items.push({
|
items.push({
|
||||||
from: fileStore.req.items[i].url,
|
from: fileStore.req.items[i].url,
|
||||||
name: fileStore.req.items[i].name,
|
name: fileStore.req.items[i].name,
|
||||||
|
size: fileStore.req.items[i].size,
|
||||||
|
modified: fileStore.req.items[i].modified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +653,15 @@ const paste = (event: Event) => {
|
|||||||
for (const item of clipboardStore.items) {
|
for (const item of clipboardStore.items) {
|
||||||
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
|
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
|
||||||
const to = route.path + encodeURIComponent(item.name);
|
const to = route.path + encodeURIComponent(item.name);
|
||||||
items.push({ from, to, name: item.name });
|
items.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
name: item.name,
|
||||||
|
size: item.size,
|
||||||
|
modified: item.modified,
|
||||||
|
overwrite: false,
|
||||||
|
rename: clipboardStore.path == route.path,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -660,7 +670,7 @@ const paste = (event: Event) => {
|
|||||||
|
|
||||||
const preselect = removePrefix(route.path) + items[0].name;
|
const preselect = removePrefix(route.path) + items[0].name;
|
||||||
|
|
||||||
let action = (overwrite: boolean, rename: boolean) => {
|
let action = (overwrite?: boolean, rename?: boolean) => {
|
||||||
api
|
api
|
||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -683,34 +693,37 @@ const paste = (event: Event) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clipboardStore.path == route.path) {
|
|
||||||
action(false, true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const conflict = upload.checkConflict(items, fileStore.req!.items);
|
const conflict = upload.checkConflict(items, fileStore.req!.items);
|
||||||
|
|
||||||
let overwrite = false;
|
if (conflict.length > 0) {
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
if (conflict) {
|
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "resolve-conflict",
|
||||||
confirm: (event: Event, option: string) => {
|
props: {
|
||||||
overwrite = option == "overwrite";
|
conflict: conflict,
|
||||||
rename = option == "rename";
|
},
|
||||||
|
confirm: (event: Event, result: Array<ConflictingResource>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
action(overwrite, rename);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
items[item.index].rename = true;
|
||||||
|
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
|
||||||
|
items[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
items.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length > 0) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(false, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columnsResize = () => {
|
const columnsResize = () => {
|
||||||
@@ -806,20 +819,30 @@ const drop = async (event: DragEvent) => {
|
|||||||
|
|
||||||
const preselect = removePrefix(path) + (files[0].fullPath || files[0].name);
|
const preselect = removePrefix(path) + (files[0].fullPath || files[0].name);
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict.length > 0) {
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace",
|
prompt: "resolve-conflict",
|
||||||
action: (event: Event) => {
|
props: {
|
||||||
event.preventDefault();
|
conflict: conflict,
|
||||||
layoutStore.closeHovers();
|
isUploadAction: true,
|
||||||
upload.handleFiles(files, path, false);
|
|
||||||
fileStore.preselect = preselect;
|
|
||||||
},
|
},
|
||||||
confirm: (event: Event) => {
|
confirm: (event: Event, result: Array<ConflictingResource>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
upload.handleFiles(files, path, true);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
fileStore.preselect = preselect;
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
continue;
|
||||||
|
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
|
||||||
|
files[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
files.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (files.length > 0) {
|
||||||
|
upload.handleFiles(files, path, true);
|
||||||
|
fileStore.preselect = preselect;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -852,18 +875,29 @@ const uploadInput = (event: Event) => {
|
|||||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||||
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict.length > 0) {
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace",
|
prompt: "resolve-conflict",
|
||||||
action: (event: Event) => {
|
props: {
|
||||||
event.preventDefault();
|
conflict: conflict,
|
||||||
layoutStore.closeHovers();
|
isUploadAction: true,
|
||||||
upload.handleFiles(uploadFiles, path, false);
|
|
||||||
},
|
},
|
||||||
confirm: (event: Event) => {
|
confirm: (event: Event, result: Array<ConflictingResource>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
upload.handleFiles(uploadFiles, path, true);
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
const item = result[i];
|
||||||
|
if (item.checked.length == 2) {
|
||||||
|
continue;
|
||||||
|
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
|
||||||
|
uploadFiles[item.index].overwrite = true;
|
||||||
|
} else {
|
||||||
|
uploadFiles.splice(item.index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uploadFiles.length > 0) {
|
||||||
|
upload.handleFiles(uploadFiles, path, true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user