Rework prompts to use vue-final-modal for better accessibility

This commit is contained in:
Kloon ImKloon 2023-11-03 18:33:40 +01:00
parent bc5c9b92f3
commit 6034de7bd4
No known key found for this signature in database
GPG Key ID: CCF1C86A995C5B6A
30 changed files with 724 additions and 354 deletions

View File

@ -12,7 +12,6 @@
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"ace-builds": "^1.31.1",
"clipboard": "^2.0.11",
"core-js": "^3.33.2",
"dayjs": "^1.11.10",
"filesize": "^10.1.0",
@ -30,6 +29,7 @@
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.3.4",
"vue-final-modal": "^4.4.5",
"vue-i18n": "^9.6.4",
"vue-lazyload": "^3.0.0",
"vue-router": "^4.2.5",
@ -3677,16 +3677,6 @@
"node": ">= 6"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -3936,11 +3926,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -4551,8 +4536,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz",
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
"optional": true,
"peer": true,
"dependencies": {
"tabbable": "^6.2.0"
}
@ -4704,14 +4687,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -6353,11 +6328,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@ -6544,9 +6514,7 @@
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"optional": true,
"peer": true
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/terser": {
"version": "5.24.0",
@ -6572,11 +6540,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/titleize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
@ -7059,6 +7022,197 @@
"eslint": ">=6.0.0"
}
},
"node_modules/vue-final-modal": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/vue-final-modal/-/vue-final-modal-4.4.5.tgz",
"integrity": "sha512-5O1twpVcOuvxc8fJppNYtdZ4M0GGeoSMcHZTFUoie3BmtzSx3EE6Lvz8YgnpQbDkxVMmRKEjhsSBywqqWfoQVw==",
"dependencies": {
"@vueuse/core": "^9.13.0",
"@vueuse/integrations": "^9.13.0",
"focus-trap": "^7.4.0",
"vue": "^3.3.4"
},
"peerDependencies": {
"@vueuse/core": ">=9.11.1",
"@vueuse/integrations": ">=9.11.1",
"focus-trap": ">=7.2.0",
"vue": ">=3.2.0"
}
},
"node_modules/vue-final-modal/node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
},
"node_modules/vue-final-modal/node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/integrations": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-9.13.0.tgz",
"integrity": "sha512-I1kX/tsfcvWWLZD7HZaP0LsSfchK13YxReLfharXhk72SFXp87doLbRaTfIF5w8m/gr/vPtcNyQPAXW7Ubpuww==",
"dependencies": {
"@vueuse/core": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"async-validator": "*",
"axios": "*",
"change-case": "*",
"drauu": "*",
"focus-trap": "*",
"fuse.js": "*",
"idb-keyval": "*",
"jwt-decode": "*",
"nprogress": "*",
"qrcode": "*",
"universal-cookie": "*"
},
"peerDependenciesMeta": {
"async-validator": {
"optional": true
},
"axios": {
"optional": true
},
"change-case": {
"optional": true
},
"drauu": {
"optional": true
},
"focus-trap": {
"optional": true
},
"fuse.js": {
"optional": true
},
"idb-keyval": {
"optional": true
},
"jwt-decode": {
"optional": true
},
"nprogress": {
"optional": true
},
"qrcode": {
"optional": true
},
"universal-cookie": {
"optional": true
}
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/integrations/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/vue-final-modal/node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-i18n": {
"version": "9.6.4",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.6.4.tgz",

View File

@ -22,7 +22,6 @@
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"ace-builds": "^1.31.1",
"clipboard": "^2.0.11",
"core-js": "^3.33.2",
"dayjs": "^1.11.10",
"filesize": "^10.1.0",
@ -40,6 +39,7 @@
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.3.4",
"vue-final-modal": "^4.4.5",
"vue-i18n": "^9.6.4",
"vue-lazyload": "^3.0.0",
"vue-router": "^4.2.5",

View File

@ -0,0 +1,20 @@
<template>
<VueFinalModal
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
>
<slot />
</VueFinalModal>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
</script>

View File

@ -6,7 +6,7 @@
<div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list>
<file-list @update:selected="(val) => (dest = val)" tabindex="1" />
</div>
<div class="card-action">
@ -15,14 +15,17 @@
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
tabindex="2"
>
{{ $t("buttons.copy") }}
</button>

View File

@ -14,14 +14,17 @@
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
tabindex="1"
>
{{ $t("buttons.delete") }}
</button>

View File

@ -0,0 +1,40 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ t("prompts.deleteUser") }}</p>
</div>
<div class="card-action">
<button
id="focus-prompt"
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="1"
>
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="layoutStore.showConfirm"
tabindex="2"
>
{{ t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>

View File

@ -1,18 +1,18 @@
<template>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t("prompts.download") }}</h2>
<h2>{{ t("prompts.download") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.downloadMessage") }}</p>
<p>{{ t("prompts.downloadMessage") }}</p>
<button
id="focus-prompt"
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="showConfirm(format)"
v-focus
@click="layoutStore.showConfirm(format)"
>
{{ ext }}
</button>
@ -20,25 +20,21 @@
</div>
</template>
<script>
import { mapState } from "pinia";
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "download",
data: function () {
return {
formats: {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
},
};
},
computed: mapState(useLayoutStore, ["showConfirm"]),
const layoutStore = useLayoutStore();
const { t } = useI18n();
const formats = {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
};
</script>

View File

@ -20,11 +20,13 @@
<div class="card-action">
<button
id="focus-prompt"
type="submit"
@click="closeHovers"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
tabindex="1"
>
{{ $t("buttons.ok") }}
</button>

View File

@ -33,33 +33,45 @@
<p>
<strong>MD5: </strong
><code
><a @click="checksum($event, 'md5')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'md5')"
@keypress.enter="checksum($event, 'md5')"
tabindex="2"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA1: </strong
><code
><a @click="checksum($event, 'sha1')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha1')"
@keypress.enter="checksum($event, 'sha1')"
tabindex="3"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA256: </strong
><code
><a @click="checksum($event, 'sha256')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha256')"
@keypress.enter="checksum($event, 'sha256')"
tabindex="4"
>{{ $t("prompts.show") }}</a
></code
>
</p>
<p>
<strong>SHA512: </strong
><code
><a @click="checksum($event, 'sha512')">{{
$t("prompts.show")
}}</a></code
><a
@click="checksum($event, 'sha512')"
@keypress.enter="checksum($event, 'sha512')"
tabindex="5"
>{{ $t("prompts.show") }}</a
></code
>
</p>
</template>
@ -67,6 +79,7 @@
<div class="card-action">
<button
id="focus-prompt"
type="submit"
@click="closeHovers"
class="button button--flat"
@ -149,8 +162,7 @@ export default {
try {
const hash = await api.checksum(link, algo);
// eslint-disable-next-line
event.target.innerHTML = hash;
event.target.textContent = hash;
} catch (e) {
this.$showError(e);
}

View File

@ -5,7 +5,7 @@
</div>
<div class="card-content">
<file-list @update:selected="(val) => (dest = val)"></file-list>
<file-list @update:selected="(val) => (dest = val)" tabindex="1" />
</div>
<div class="card-action">
@ -14,15 +14,18 @@
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
tabindex="2"
>
{{ $t("buttons.move") }}
</button>

View File

@ -1,85 +1,88 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.newDir") }}</h2>
<h2>{{ t("prompts.newDir") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.newDirMessage") }}</p>
<p>{{ t("prompts.newDirMessage") }}</p>
<input
id="focus-prompt"
class="input input--block"
type="text"
@keyup.enter="submit"
v-model.trim="name"
v-focus
tabindex="1"
/>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
:title="t('buttons.create')"
@click="submit"
tabindex="2"
>
{{ $t("buttons.create") }}
{{ t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from "pinia";
<script setup lang="ts">
import { inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api";
import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
export default {
name: "new-dir",
data: function () {
return {
name: "",
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["isFiles", "isListing"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
const $showError = inject<IToastError>("$showError")!;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/";
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
const name = ref<string>("");
try {
await api.post(uri);
this.$router.push({ path: `${uri}` });
} catch (e) {
this.$showError(e);
}
const submit = async (event: Event) => {
event.preventDefault();
if (name.value === "") return;
this.closeHovers();
},
},
// Build the path of the new directory.
let uri = fileStore.isFiles ? route.path + "/" : "/";
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(name.value) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri);
router.push({ path: `${uri}` });
} catch (e) {
if (e instanceof Error) {
$showError(e);
}
}
layoutStore.closeHovers();
};
</script>

View File

@ -1,14 +1,14 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.newFile") }}</h2>
<h2>{{ t("prompts.newFile") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.newFileMessage") }}</p>
<p>{{ t("prompts.newFileMessage") }}</p>
<input
id="focus-prompt"
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
@ -18,68 +18,68 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
:aria-label="t('buttons.create')"
:title="t('buttons.create')"
>
{{ $t("buttons.create") }}
{{ t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from "pinia";
<script setup lang="ts">
import { inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: "new-file",
data: function () {
return {
name: "",
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["isFiles", "isListing"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
const $showError = inject<IToastError>("$showError")!;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/";
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
const name = ref<string>("");
try {
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e);
}
const submit = async (event: Event) => {
event.preventDefault();
if (name.value === "") return;
this.closeHovers();
},
},
// Build the path of the new directory.
let uri = fileStore.isFiles ? route.path + "/" : "/";
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(name.value);
uri = uri.replace("//", "/");
try {
await api.post(uri);
router.push({ path: uri });
} catch (e) {
if (e instanceof Error) {
$showError(e);
}
}
layoutStore.closeHovers();
};
</script>

View File

@ -1,19 +1,22 @@
<template>
<div>
<component ref="currentComponent" :is="currentComponent"></component>
<div v-show="showOverlay" @click="closeHovers" class="overlay"></div>
<ModalsContainer />
</div>
</template>
<script>
import { mapActions, mapState } from "pinia";
<script setup lang="ts">
import { ref, watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import BaseModal from "./BaseModal.vue";
import Help from "./Help.vue";
import Info from "./Info.vue";
import Delete from "./Delete.vue";
import Rename from "./Rename.vue";
import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue";
import Rename from "./Rename.vue";
import Move from "./Move.vue";
import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue";
@ -21,95 +24,65 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue";
import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue";
import buttons from "@/utils/buttons";
import Upload from "./Upload.vue";
export default {
name: "prompts",
components: {
Info,
Delete,
Rename,
Download,
Move,
Copy,
Share,
NewFile,
NewDir,
Help,
Replace,
ReplaceRename,
Upload,
ShareDelete,
},
data: function () {
return {
pluginData: {
buttons,
},
};
},
created() {
window.addEventListener("keydown", (event) => {
if (this.show == null) return;
const layoutStore = useLayoutStore();
let prompt = this.$refs.currentComponent;
const { show } = storeToRefs(layoutStore);
if (event.key === "Escape") {
event.stopImmediatePropagation();
this.closeHovers();
}
const closeModal = ref<() => Promise<string>>();
if (event.key == "Enter") {
switch (this.show) {
case "delete":
prompt.submit();
break;
case "copy":
prompt.copy(event);
break;
case "move":
prompt.move(event);
break;
case "replace":
prompt.showConfirm(event);
break;
}
}
});
},
computed: {
...mapState(useLayoutStore, ["show", "showConfirm"]),
currentComponent: function () {
const matched =
[
"info",
"help",
"delete",
"rename",
"move",
"copy",
"newFile",
"newDir",
"download",
"replace",
"replace-rename",
"share",
"upload",
"share-delete",
].indexOf(this.show) >= 0;
const components = new Map<string, any>([
["info", Info],
["help", Help],
["delete", Delete],
["rename", Rename],
["move", Move],
["copy", Copy],
["newFile", NewFile],
["newDir", NewDir],
["download", Download],
["replace", Replace],
["replace-rename", ReplaceRename],
["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
]);
return (matched && this.show) || null;
watch(show, (newValue) => {
if (closeModal.value) {
closeModal.value();
closeModal.value = undefined;
}
const modal = components.get(newValue!);
if (!modal) return;
const { open, close } = useModal({
component: BaseModal,
attrs: {
// title: "Hello World!",
// onConfirm() {
// console.log("onConfirm");
// },
},
showOverlay: function () {
return (
this.show !== null && this.show !== "search" && this.show !== "more"
);
slots: {
default: modal,
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
});
closeModal.value = close;
open();
});
window.addEventListener("keydown", (event) => {
if (!layoutStore.show) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
</script>

View File

@ -10,8 +10,8 @@
>:
</p>
<input
id="focus-prompt"
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"

View File

@ -14,6 +14,7 @@
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
@ -22,14 +23,17 @@
@click="showAction"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
tabindex="2"
>
{{ $t("buttons.continue") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--red"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
>
{{ $t("buttons.replace") }}
</button>

View File

@ -14,6 +14,7 @@
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
@ -22,14 +23,17 @@
@click="(event) => showConfirm(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) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
>
{{ $t("buttons.replace") }}
</button>

View File

@ -1,5 +1,5 @@
<template>
<div class="card floating share__promt__card" id="share">
<div class="card floating" id="share">
<div class="card-title">
<h2>{{ $t("buttons.share") }}</h2>
</div>
@ -25,9 +25,9 @@
<td class="small">
<button
class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
>
<i class="material-icons">content_paste</i>
</button>
@ -35,9 +35,9 @@
<td class="small" v-if="hasDownloadLink()">
<button
class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
@ -81,15 +81,21 @@
<p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input">
<vue-number-input
tabindex="1"
center
controls
size="small"
:max="2147483647"
:min="1"
:min="0"
@keyup.enter="submit"
v-model.trim="time"
v-focus
v-model="time"
/>
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
<select
class="right"
v-model="unit"
:aria-label="$t('time.unit')"
tabindex="2"
>
<option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option>
@ -101,6 +107,7 @@
class="input input--block"
type="password"
v-model.trim="password"
tabindex="3"
/>
</div>
@ -110,14 +117,17 @@
@click="() => switchListing()"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="5"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--blue"
@click="submit"
:aria-label="$t('buttons.share')"
:title="$t('buttons.share')"
tabindex="4"
>
{{ $t("buttons.share") }}
</button>
@ -131,14 +141,14 @@ import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api, pub as pub_api } from "@/api";
import dayjs from "dayjs";
import Clipboard from "clipboard";
import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
export default {
name: "share",
data: function () {
return {
time: "",
time: 0,
unit: "hours",
links: [],
clip: null,
@ -180,24 +190,24 @@ export default {
this.$showError(e);
}
},
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeUnmount() {
this.clip.destroy();
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy(text).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
},
submit: async function () {
let isPermanent = !this.time || this.time == 0;
try {
let res = null;
if (isPermanent) {
if (!this.time) {
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
@ -206,7 +216,7 @@ export default {
this.links.push(res);
this.sort();
this.time = "";
this.time = 0;
this.unit = "hours";
this.password = "";

View File

@ -9,14 +9,17 @@
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
tabindex="1"
>
{{ $t("buttons.delete") }}
</button>

View File

@ -1,38 +1,111 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2>
<h2>{{ t("prompts.upload") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p>
<p>{{ t("prompts.uploadMessage") }}</p>
</div>
<div class="card-action full">
<div @click="uploadFile" class="action">
<div
@click="uploadFile"
@keypress.enter="uploadFile"
class="action"
id="focus-prompt"
tabindex="1"
>
<i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div>
<div class="title">{{ t("buttons.file") }}</div>
</div>
<div @click="uploadFolder" class="action">
<div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div>
<div class="title">{{ t("buttons.folder") }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "upload",
methods: {
uploadFile: function () {
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
},
uploadFolder: function () {
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
},
},
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as upload from "@/utils/upload";
const { t } = useI18n();
const route = useRoute();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// TODO: this is a copy of the same function in FileListing.vue
const uploadInput = (event: Event) => {
layoutStore.closeHovers();
let files = (event.currentTarget as HTMLInputElement)?.files;
if (files === null) return;
let folder_upload = !!files[0].webkitRelativePath;
const uploadFiles: UploadList = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folder_upload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file,
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
let path = route.path.endsWith("/") ? route.path : route.path + "/";
let conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
if (conflict) {
layoutStore.showHover({
prompt: "replace",
action: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, false);
},
confirm: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, true);
},
});
return;
}
upload.handleFiles(uploadFiles, path);
};
const openUpload = (isFolder: boolean) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.webkitdirectory = isFolder;
// TODO: call the function in FileListing.vue instead
input.onchange = uploadInput;
input.click();
};
const uploadFile = () => {
openUpload(false);
};
const uploadFolder = () => {
openUpload(true);
};
</script>

View File

@ -174,11 +174,11 @@ table tr > *:last-child {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99999;
max-width: 25em;
width: 90%;
max-height: 95%;
animation: 0.1s show forwards;
/* animation-duration: 0.3s;
animation-fill-mode: forwards; */
}
.card > * > *:first-child {

View File

@ -1,5 +1,6 @@
@import "normalize.css/normalize.css";
@import "vue-toastification/dist/index.css";
@import "vue-final-modal/style.css";
@import "./_variables.css";
@import "./_buttons.css";
@import "./_inputs.css";
@ -15,6 +16,12 @@
@import "./login.css";
@import "./mobile.css";
/* For testing only
:focus {
outline: 2px solid crimson !important;
border-radius: 3px !important;
} */
.link {
color: var(--blue);
}

View File

@ -129,6 +129,7 @@
"deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?",
"deleteMessageSingle": "Are you sure you want to delete this file/folder?",
"deleteMessageShare": "Are you sure you want to delete this share({path})?",
"deleteUser": "Are you sure you want to delete this user?",
"deleteTitle": "Delete files",
"displayName": "Display Name:",
"download": "Download files",

View File

@ -2,6 +2,7 @@ import { disableExternal } from "@/utils/constants";
import { createApp } from "vue";
import VueNumberInput from "@chenfengyuan/vue-number-input";
import VueLazyload from "vue-lazyload";
import { createVfm } from "vue-final-modal";
import Toast, { POSITION, useToast } from "vue-toastification";
import {
ToastOptions,
@ -26,6 +27,7 @@ dayjs.extend(relativeTime);
dayjs.extend(duration);
const pinia = createPinia(router);
const vfm = createVfm();
const app = createApp(App);
@ -37,6 +39,7 @@ app.use(Toast, {
newestOnTop: true,
} satisfies PluginOptions);
app.use(vfm);
app.use(i18n);
app.use(pinia);
app.use(router);
@ -50,7 +53,7 @@ app.mixin({
// provide v-focus for components
app.directive("focus", {
mounted: (el) => {
mounted: async (el) => {
// initiate focus for the element
el.focus();
},

View File

@ -6,7 +6,7 @@ export const useLayoutStore = defineStore("layout", {
// convert to a function
state: (): {
loading: boolean;
show: string | null | boolean;
show: string | null;
showConfirm: any;
showAction: PopupAction | null;
showShell: boolean | null;

View File

@ -0,0 +1,66 @@
// Based on code provided by Amir Fo
// https://stackoverflow.com/a/74528564
export function copy(text: string) {
return new Promise<void>((resolve, reject) => {
if (
typeof navigator !== "undefined" &&
typeof navigator.clipboard !== "undefined" &&
// @ts-ignore
navigator.permissions !== "undefined"
) {
navigator.permissions
// @ts-ignore
.query({ name: "clipboard-write" })
.then((permission) => {
if (permission.state === "granted" || permission.state === "prompt") {
const type = "text/plain";
const blob = new Blob([text], { type });
const data = [new ClipboardItem({ [type]: blob })];
navigator.clipboard.write(data).then(resolve).catch(reject);
} else {
reject(new Error("Permission not granted!"));
}
})
.catch((e) => {
// Firefox doesnt support clipboard-write permission
if (navigator.userAgent.indexOf("Firefox") != -1) {
navigator.clipboard.writeText(text).then(resolve).catch(reject);
} else {
reject(e);
}
});
} else if (
document.queryCommandSupported &&
document.queryCommandSupported("copy")
) {
const textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.setAttribute("readonly", "");
textarea.style.fontSize = "12pt";
textarea.style.position = "fixed";
textarea.style.width = "2em";
textarea.style.height = "2em";
textarea.style.padding = "0";
textarea.style.margin = "0";
textarea.style.border = "none";
textarea.style.outline = "none";
textarea.style.boxShadow = "none";
textarea.style.background = "transparent";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
document.body.removeChild(textarea);
resolve();
} catch (e) {
document.body.removeChild(textarea);
reject(e);
}
} else {
reject(
new Error("None of copying methods are supported by this browser!")
);
}
});
}

View File

@ -141,7 +141,7 @@ export function handleFiles(
path += "/";
}
console.log("File", file);
// console.log("File", file);
const item: UploadItem = {
id,

View File

@ -13,9 +13,9 @@
<button
v-if="isSingleFile()"
class="action copy-clipboard"
:data-clipboard-text="linkSelected()"
:aria-label="t('buttons.copyDownloadLinkToClipboard')"
:data-title="t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(linkSelected())"
>
<i class="material-icons">content_paste</i>
</button>
@ -51,6 +51,7 @@
<div class="card-content">
<input
v-focus
class="input input--block"
type="password"
:placeholder="t('login.password')"
v-model="password"
@ -68,6 +69,7 @@
</button>
</div>
</div>
<div class="overlay" />
</div>
<errors v-else :errorCode="error.status" />
</div>
@ -191,13 +193,13 @@ import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors.vue";
import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils";
import { copy } from "@/utils/clipboard";
const error = ref<StatusError | null>(null);
const showLimit = ref<number>(100);
@ -205,7 +207,6 @@ const password = ref<string>("");
const attemptedPasswordLogin = ref<boolean>(false);
const hash = ref<string>("");
const token = ref<string>("");
const clip = ref<any>(null);
const $showSuccess = inject<IToastSuccess>("$showSuccess")!;
@ -350,20 +351,21 @@ const linkSelected = () => {
: "";
};
const copyToClipboard = (text: string) => {
copy(text).then(
() => {
// clipboard successfully set
$showSuccess(t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
};
onMounted(async () => {
// Created
hash.value = route.params.path[0];
await fetchData();
window.addEventListener("keydown", keyEvent);
clip.value = new Clipboard(".copy-clipboard");
clip.value.on("success", () => {
$showSuccess(t("success.linkCopied"));
});
});
onBeforeUnmount(() => {
window.removeEventListener("keydown", keyEvent);
clip.value.destroy();
});
</script>

View File

@ -74,6 +74,8 @@ onMounted(() => {
if (getTheme() === "dark") {
editor.value!.setTheme("ace/theme/twilight");
}
editor.value.focus();
});
onBeforeUnmount(() => {

View File

@ -43,9 +43,9 @@
<td class="small">
<button
class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="t('buttons.copyToClipboard')"
:title="t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
>
<i class="material-icons">content_paste</i>
</button>
@ -67,11 +67,11 @@ import { useAuthStore } from "@/stores/auth";
import { useLayoutStore } from "@/stores/layout";
import { share as api, users } from "@/api";
import dayjs from "dayjs";
import Clipboard from "clipboard";
import Errors from "@/views/Errors.vue";
import { inject, onBeforeUnmount, ref, onMounted } from "vue";
import { inject, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils";
import { copy } from "@/utils/clipboard";
const $showError = inject<IToastError>("$showError")!;
const $showSuccess = inject<IToastSuccess>("$showSuccess")!;
@ -82,7 +82,6 @@ const authStore = useAuthStore();
const error = ref<StatusError | null>(null);
const links = ref<Share[]>([]);
const clip = ref<Clipboard | null>(null);
onMounted(async () => {
layoutStore.loading = true;
@ -106,13 +105,19 @@ onMounted(async () => {
} finally {
layoutStore.loading = false;
}
clip.value = new Clipboard(".copy-clipboard");
clip.value.on("success", () => {
$showSuccess(t("success.linkCopied"));
});
});
onBeforeUnmount(() => clip.value?.destroy());
const copyToClipboard = (text: string) => {
copy(text).then(
() => {
// clipboard successfully set
$showSuccess(t("success.linkCopied"));
},
() => {
// clipboard write failed
}
);
};
const deleteLink = async (event: Event, link: any) => {
event.preventDefault();

View File

@ -45,27 +45,6 @@
</div>
</form>
</div>
<div v-if="layoutStore.show === 'deleteUser'" class="card floating">
<div class="card-content">
<p>Are you sure you want to delete this user?</p>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
v-focus
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button class="button button--flat" @click="deleteUser">
{{ $t("buttons.delete") }}
</button>
</div>
</div>
</div>
</template>
@ -80,9 +59,9 @@ import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils";
const error = ref<StatusError | null>(null);
const originalUser = ref<IUser | null>(null);
const user = ref<IUser | null>(null);
const error = ref<StatusError>();
const originalUser = ref<IUser>();
const user = ref<IUser>();
const createUserDir = ref<boolean>(false);
const $showError = inject<IToastError>("$showError")!;
@ -136,11 +115,12 @@ const fetchData = async () => {
}
};
const deletePrompt = () => layoutStore.showHover("deleteUser");
const deletePrompt = () =>
layoutStore.showHover({ prompt: "deleteUser", confirm: deleteUser });
const deleteUser = async (e: Event) => {
e.preventDefault();
if (user.value === null) {
if (!user.value) {
return false;
}
try {
@ -157,21 +137,22 @@ const deleteUser = async (e: Event) => {
return true;
};
const save = async (event: Event) => {
event.preventDefault();
if (originalUser.value === null || user.value === null) {
if (!user.value) {
return false;
}
try {
if (isNew.value) {
const newUser: IUser = {
...originalUser.value,
...originalUser?.value,
...user.value,
};
const loc = (await api.create(newUser)) || "/settings/users";
router.push({ path: loc });
const loc = await api.create(newUser);
router.push({ path: loc || "/settings/users" });
$showSuccess(t("settings.userCreated"));
} else {
await api.update(user.value);