Mass changes
This commit is contained in:
@@ -1,198 +1,394 @@
|
||||
<script setup>
|
||||
import { FwbTable, FwbTableHead, FwbTableHeadCell, FwbTableBody, FwbTableRow, FwbTableCell, FwbBadge } from 'flowbite-vue'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faFilePdf, faFileWord, faFileExcel, faFileLines, faFileImage, faFile, faCircleInfo, faEllipsisVertical, faDownload } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ref } from 'vue'
|
||||
import Dropdown from '@/Components/Dropdown.vue'
|
||||
import {
|
||||
FwbTable,
|
||||
FwbTableHead,
|
||||
FwbTableHeadCell,
|
||||
FwbTableBody,
|
||||
FwbTableRow,
|
||||
FwbTableCell,
|
||||
FwbBadge,
|
||||
} from "flowbite-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faFilePdf,
|
||||
faFileWord,
|
||||
faFileExcel,
|
||||
faFileLines,
|
||||
faFileImage,
|
||||
faFile,
|
||||
faCircleInfo,
|
||||
faEllipsisVertical,
|
||||
faDownload,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ref } from "vue";
|
||||
import { router } from "@inertiajs/vue3";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
|
||||
import SecondaryButton from "./SecondaryButton.vue";
|
||||
import DangerButton from "./DangerButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
documents: { type: Array, default: () => [] },
|
||||
viewUrlBuilder: { type: Function, default: null },
|
||||
// Optional: build a direct download URL for a document; if not provided, a 'download' event will be emitted
|
||||
// Optional: direct download URL builder; if absent we emit 'download'
|
||||
downloadUrlBuilder: { type: Function, default: null },
|
||||
})
|
||||
// Optional: direct delete URL builder; if absent we emit 'delete'
|
||||
deleteUrlBuilder: { type: Function, default: null },
|
||||
});
|
||||
// Derive a human-friendly source for a document: Case or Contract reference
|
||||
const sourceLabel = (doc) => {
|
||||
// Server can include optional documentable meta; fall back to type
|
||||
if (doc.documentable_type?.toLowerCase?.().includes('contract')) {
|
||||
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : 'Pogodba'
|
||||
if (doc.documentable_type?.toLowerCase?.().includes("contract")) {
|
||||
return doc.contract_reference ? `Pogodba ${doc.contract_reference}` : "Pogodba";
|
||||
}
|
||||
return 'Primer'
|
||||
}
|
||||
return "Primer";
|
||||
};
|
||||
|
||||
const emit = defineEmits(['view', 'download'])
|
||||
const emit = defineEmits(["view", "download", "delete"]);
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes == null) return '-'
|
||||
const thresh = 1024
|
||||
if (Math.abs(bytes) < thresh) return bytes + ' B'
|
||||
const units = ['KB', 'MB', 'GB', 'TB']
|
||||
let u = -1
|
||||
do { bytes /= thresh; ++u } while (Math.abs(bytes) >= thresh && u < units.length - 1)
|
||||
return bytes.toFixed(1) + ' ' + units[u]
|
||||
}
|
||||
if (bytes == null) return "-";
|
||||
const thresh = 1024;
|
||||
if (Math.abs(bytes) < thresh) return bytes + " B";
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
|
||||
return bytes.toFixed(1) + " " + units[u];
|
||||
};
|
||||
|
||||
const extFrom = (doc) => {
|
||||
let ext = (doc?.extension || '').toLowerCase()
|
||||
let ext = (doc?.extension || "").toLowerCase();
|
||||
if (!ext && doc?.original_name) {
|
||||
const parts = String(doc.original_name).toLowerCase().split('.')
|
||||
if (parts.length > 1) ext = parts.pop()
|
||||
const parts = String(doc.original_name).toLowerCase().split(".");
|
||||
if (parts.length > 1) ext = parts.pop();
|
||||
}
|
||||
// derive from mime
|
||||
if (!ext && doc?.mime_type) {
|
||||
const mime = String(doc.mime_type).toLowerCase()
|
||||
if (mime.includes('pdf')) ext = 'pdf'
|
||||
else if (mime.includes('word') || mime.includes('msword') || mime.includes('doc')) ext = 'docx'
|
||||
else if (mime.includes('excel') || mime.includes('sheet')) ext = 'xlsx'
|
||||
else if (mime.includes('csv')) ext = 'csv'
|
||||
else if (mime.startsWith('image/')) ext = 'img'
|
||||
else if (mime.includes('text')) ext = 'txt'
|
||||
const mime = String(doc.mime_type).toLowerCase();
|
||||
if (mime.includes("pdf")) ext = "pdf";
|
||||
else if (mime.includes("word") || mime.includes("msword") || mime.includes("doc"))
|
||||
ext = "docx";
|
||||
else if (mime.includes("excel") || mime.includes("sheet")) ext = "xlsx";
|
||||
else if (mime.includes("csv")) ext = "csv";
|
||||
else if (mime.startsWith("image/")) ext = "img";
|
||||
else if (mime.includes("text")) ext = "txt";
|
||||
}
|
||||
return ext
|
||||
}
|
||||
return ext;
|
||||
};
|
||||
|
||||
const fileTypeInfo = (doc) => {
|
||||
const ext = extFrom(doc)
|
||||
const mime = (doc?.mime_type || '').toLowerCase()
|
||||
const ext = extFrom(doc);
|
||||
const mime = (doc?.mime_type || "").toLowerCase();
|
||||
switch (ext) {
|
||||
case 'pdf':
|
||||
return { icon: faFilePdf, color: 'text-red-600', label: 'PDF' }
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return { icon: faFileWord, color: 'text-blue-600', label: (ext || 'DOCX').toUpperCase() }
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return { icon: faFileExcel, color: 'text-green-600', label: (ext || 'XLSX').toUpperCase() }
|
||||
case 'csv':
|
||||
case "pdf":
|
||||
return { icon: faFilePdf, color: "text-red-600", label: "PDF" };
|
||||
case "doc":
|
||||
case "docx":
|
||||
return {
|
||||
icon: faFileWord,
|
||||
color: "text-blue-600",
|
||||
label: (ext || "DOCX").toUpperCase(),
|
||||
};
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
return {
|
||||
icon: faFileExcel,
|
||||
color: "text-green-600",
|
||||
label: (ext || "XLSX").toUpperCase(),
|
||||
};
|
||||
case "csv":
|
||||
// treat CSV as spreadsheet-like
|
||||
return { icon: faFileExcel, color: 'text-emerald-600', label: 'CSV' }
|
||||
case 'txt':
|
||||
return { icon: faFileLines, color: 'text-slate-600', label: 'TXT' }
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'img':
|
||||
return { icon: faFileImage, color: 'text-fuchsia-600', label: (ext === 'img' ? 'IMG' : (ext || 'IMG').toUpperCase()) }
|
||||
return { icon: faFileExcel, color: "text-emerald-600", label: "CSV" };
|
||||
case "txt":
|
||||
return { icon: faFileLines, color: "text-slate-600", label: "TXT" };
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "img":
|
||||
return {
|
||||
icon: faFileImage,
|
||||
color: "text-fuchsia-600",
|
||||
label: ext === "img" ? "IMG" : (ext || "IMG").toUpperCase(),
|
||||
};
|
||||
default:
|
||||
if (mime.startsWith('image/')) return { icon: faFileImage, color: 'text-fuchsia-600', label: 'IMG' }
|
||||
return { icon: faFile, color: 'text-gray-600', label: (ext || 'FILE').toUpperCase() }
|
||||
if (mime.startsWith("image/"))
|
||||
return { icon: faFileImage, color: "text-fuchsia-600", label: "IMG" };
|
||||
return {
|
||||
icon: faFile,
|
||||
color: "text-gray-600",
|
||||
label: (ext || "FILE").toUpperCase(),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasDesc = (doc) => {
|
||||
const d = doc?.description
|
||||
return typeof d === 'string' && d.trim().length > 0
|
||||
}
|
||||
const d = doc?.description;
|
||||
return typeof d === "string" && d.trim().length > 0;
|
||||
};
|
||||
|
||||
const expandedDescKey = ref(null)
|
||||
const rowKey = (doc, i) => doc?.uuid ?? i
|
||||
const expandedDescKey = ref(null);
|
||||
const rowKey = (doc, i) => doc?.uuid ?? i;
|
||||
const toggleDesc = (doc, i) => {
|
||||
const key = rowKey(doc, i)
|
||||
expandedDescKey.value = expandedDescKey.value === key ? null : key
|
||||
}
|
||||
|
||||
const key = rowKey(doc, i);
|
||||
expandedDescKey.value = expandedDescKey.value === key ? null : key;
|
||||
};
|
||||
|
||||
const resolveDownloadUrl = (doc) => {
|
||||
if (typeof props.downloadUrlBuilder === 'function') return props.downloadUrlBuilder(doc)
|
||||
if (typeof props.downloadUrlBuilder === "function")
|
||||
return props.downloadUrlBuilder(doc);
|
||||
// If no builder provided, parent can handle via emitted event
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleDownload = (doc) => {
|
||||
const url = resolveDownloadUrl(doc)
|
||||
const url = resolveDownloadUrl(doc);
|
||||
if (url) {
|
||||
// Trigger a navigation to a direct-download endpoint; server should set Content-Disposition: attachment
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.target = '_self'
|
||||
a.rel = 'noopener'
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_self";
|
||||
a.rel = "noopener";
|
||||
// In many browsers, simply setting href is enough
|
||||
a.click()
|
||||
a.click();
|
||||
} else {
|
||||
emit('download', doc)
|
||||
emit("download", doc);
|
||||
}
|
||||
closeActions()
|
||||
closeActions();
|
||||
};
|
||||
|
||||
// ---------------- Delete logic ----------------
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
const docToDelete = ref(null);
|
||||
|
||||
const resolveDeleteUrl = (doc) => {
|
||||
// 1. Explicit builder via prop takes precedence
|
||||
if (typeof props.deleteUrlBuilder === "function") {
|
||||
return props.deleteUrlBuilder(doc);
|
||||
}
|
||||
// 2. Attempt automatic route resolution (requires Ziggy's global `route` helper)
|
||||
try {
|
||||
const type = (doc?.documentable_type || "").toLowerCase();
|
||||
// Contract document
|
||||
if (type.includes("contract") && doc?.contract_uuid && doc?.uuid) {
|
||||
if (typeof route === "function") {
|
||||
return route("contract.document.delete", {
|
||||
contract: doc.contract_uuid,
|
||||
document: doc.uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Case document
|
||||
if (doc?.client_case_uuid && doc?.uuid) {
|
||||
if (typeof route === "function") {
|
||||
return route("clientCase.document.delete", {
|
||||
client_case: doc.client_case_uuid,
|
||||
document: doc.uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// swallow – fallback to emit path
|
||||
}
|
||||
// 3. Fallback: no URL, caller must handle emitted event
|
||||
return null;
|
||||
};
|
||||
|
||||
const requestDelete = async () => {
|
||||
if (!docToDelete.value) {
|
||||
return;
|
||||
}
|
||||
const url = resolveDeleteUrl(docToDelete.value);
|
||||
deleting.value = true;
|
||||
try {
|
||||
if (url) {
|
||||
await router.delete(url, { preserveScroll: true });
|
||||
} else {
|
||||
emit("delete", docToDelete.value);
|
||||
}
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
confirmDelete.value = false;
|
||||
docToDelete.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const askDelete = (doc) => {
|
||||
docToDelete.value = doc;
|
||||
confirmDelete.value = true;
|
||||
};
|
||||
|
||||
function closeActions() {
|
||||
/* noop placeholder for symmetry; Dropdown auto-closes */
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div
|
||||
class="relative overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
>
|
||||
<FwbTable hoverable striped class="text-sm">
|
||||
<FwbTableHead class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm">
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Naziv</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vrsta</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Velikost</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Dodano</FwbTableHeadCell>
|
||||
<FwbTableHeadCell class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3">Vir</FwbTableHeadCell>
|
||||
<FwbTableHead
|
||||
class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur border-b border-gray-200 shadow-sm"
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Naziv</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Vrsta</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Velikost</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Dodano</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Vir</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell class="w-px" />
|
||||
</FwbTableHead>
|
||||
<FwbTableBody>
|
||||
<template v-for="(doc, i) in documents" :key="doc.uuid || i">
|
||||
<FwbTableRow>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="text-indigo-600 hover:underline" @click="$emit('view', doc)">{{ doc.name }}</button>
|
||||
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<FontAwesomeIcon :icon="fileTypeInfo(doc).icon" :class="['h-5 w-5', fileTypeInfo(doc).color]" />
|
||||
<span class="text-gray-700">{{ fileTypeInfo(doc).label }}</span>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
|
||||
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-center">
|
||||
<button
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:disabled="!hasDesc(doc)"
|
||||
:title="hasDesc(doc) ? 'Pokaži opis' : 'Ni opisa'"
|
||||
type="button"
|
||||
@click="toggleDesc(doc, i)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
|
||||
</button>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-right whitespace-nowrap">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<FwbTableRow>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||
:title="'Actions'"
|
||||
class="text-indigo-600 hover:underline"
|
||||
@click="$emit('view', doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4 text-gray-700" />
|
||||
{{ doc.name }}
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||
@click="handleDownload(doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
|
||||
<span>Download file</span>
|
||||
</button>
|
||||
<!-- future actions can be slotted here -->
|
||||
</template>
|
||||
</Dropdown>
|
||||
</FwbTableCell>
|
||||
</FwbTableRow>
|
||||
<!-- Expanded description row directly below the item -->
|
||||
<FwbTableRow :key="'desc-' + (doc.uuid || i)" v-if="expandedDescKey === rowKey(doc, i)">
|
||||
<FwbTableCell :colspan="6" class="bg-gray-50">
|
||||
<div class="px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400">
|
||||
{{ doc.description }}
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
</FwbTableRow>
|
||||
<FwbBadge v-if="doc.is_public" type="green">Public</FwbBadge>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
:icon="fileTypeInfo(doc).icon"
|
||||
:class="['h-5 w-5', fileTypeInfo(doc).color]"
|
||||
/>
|
||||
<span class="text-gray-700">{{ fileTypeInfo(doc).label }}</span>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell>{{ formatSize(doc.size) }}</FwbTableCell>
|
||||
<FwbTableCell>{{ new Date(doc.created_at).toLocaleString() }}</FwbTableCell>
|
||||
<FwbTableCell>
|
||||
<FwbBadge type="purple">{{ sourceLabel(doc) }}</FwbBadge>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-center">
|
||||
<button
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:disabled="!hasDesc(doc)"
|
||||
:title="hasDesc(doc) ? 'Pokaži opis' : 'Ni opisa'"
|
||||
type="button"
|
||||
@click="toggleDesc(doc, i)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faCircleInfo" class="h-4 w-4 text-gray-700" />
|
||||
</button>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-right whitespace-nowrap">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||
:title="'Actions'"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="faEllipsisVertical"
|
||||
class="h-4 w-4 text-gray-700"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
|
||||
@click="handleDownload(doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
|
||||
<span>Download file</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
|
||||
@click="askDelete(doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
<!-- future actions can be slotted here -->
|
||||
</template>
|
||||
</Dropdown>
|
||||
</FwbTableCell>
|
||||
</FwbTableRow>
|
||||
<!-- Expanded description row directly below the item -->
|
||||
<FwbTableRow
|
||||
:key="'desc-' + (doc.uuid || i)"
|
||||
v-if="expandedDescKey === rowKey(doc, i)"
|
||||
>
|
||||
<FwbTableCell :colspan="6" class="bg-gray-50">
|
||||
<div
|
||||
class="px-4 py-3 text-sm text-gray-700 whitespace-pre-wrap border-l-2 border-indigo-400"
|
||||
>
|
||||
{{ doc.description }}
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
</FwbTableRow>
|
||||
</template>
|
||||
</FwbTableBody>
|
||||
</FwbTable>
|
||||
<div v-if="!documents || documents.length === 0" class="p-6 text-center text-sm text-gray-500">No documents.</div>
|
||||
<div
|
||||
v-if="!documents || documents.length === 0"
|
||||
class="p-6 text-center text-sm text-gray-500"
|
||||
>
|
||||
No documents.
|
||||
</div>
|
||||
<!-- Delete confirmation modal using shared component -->
|
||||
<ConfirmationModal
|
||||
:show="confirmDelete"
|
||||
:closeable="!deleting"
|
||||
max-width="md"
|
||||
@close="
|
||||
confirmDelete = false;
|
||||
docToDelete = null;
|
||||
"
|
||||
>
|
||||
<template #title>Potrditev</template>
|
||||
<template #content>
|
||||
Ali res želite izbrisati dokument
|
||||
<span class="font-medium">{{ docToDelete?.name }}</span
|
||||
>? Tega dejanja ni mogoče razveljaviti.
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
@click="
|
||||
confirmDelete = false;
|
||||
docToDelete = null;
|
||||
"
|
||||
:disabled="deleting"
|
||||
>Prekliči</SecondaryButton
|
||||
>
|
||||
<DangerButton
|
||||
:disabled="deleting"
|
||||
type="button"
|
||||
class="ml-2"
|
||||
@click="requestDelete"
|
||||
>{{ deleting ? "Brisanje…" : "Izbriši" }}</DangerButton
|
||||
>
|
||||
</template>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,6 +14,10 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => ['py-1', 'bg-white'],
|
||||
},
|
||||
closeOnContentClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
let open = ref(false);
|
||||
@@ -77,9 +81,16 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
const widthClass = computed(() => {
|
||||
return {
|
||||
'48': 'w-48',
|
||||
}[props.width.toString()];
|
||||
const map = {
|
||||
'48': 'w-48', // 12rem
|
||||
'64': 'w-64', // 16rem
|
||||
'72': 'w-72', // 18rem
|
||||
'80': 'w-80', // 20rem
|
||||
'96': 'w-96', // 24rem
|
||||
'wide': 'w-[34rem] max-w-[90vw]',
|
||||
'auto': '',
|
||||
};
|
||||
return map[props.width.toString()] ?? '';
|
||||
});
|
||||
|
||||
const alignmentClasses = computed(() => {
|
||||
@@ -93,6 +104,11 @@ const alignmentClasses = computed(() => {
|
||||
|
||||
return 'origin-top';
|
||||
});
|
||||
const onContentClick = () => {
|
||||
if (props.closeOnContentClick) {
|
||||
open.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -120,7 +136,7 @@ const alignmentClasses = computed(() => {
|
||||
:class="[widthClass]"
|
||||
:style="[panelStyle]"
|
||||
>
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses" @click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses" @click="onContentClick">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,101 +1,112 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '2xl',
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: "2xl",
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const emit = defineEmits(["close"]);
|
||||
const dialog = ref();
|
||||
const showSlot = ref(props.show);
|
||||
|
||||
watch(() => props.show, () => {
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
if (props.show) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
showSlot.value = true;
|
||||
dialog.value?.showModal();
|
||||
document.body.style.overflow = "hidden";
|
||||
showSlot.value = true;
|
||||
dialog.value?.showModal();
|
||||
} else {
|
||||
document.body.style.overflow = null;
|
||||
setTimeout(() => {
|
||||
dialog.value?.close();
|
||||
showSlot.value = false;
|
||||
}, 200);
|
||||
document.body.style.overflow = null;
|
||||
setTimeout(() => {
|
||||
dialog.value?.close();
|
||||
showSlot.value = false;
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
if (props.closeable) {
|
||||
emit('close');
|
||||
}
|
||||
if (props.closeable) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
|
||||
if (props.show) {
|
||||
close();
|
||||
}
|
||||
if (props.show) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||
onMounted(() => document.addEventListener("keydown", closeOnEscape));
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
document.body.style.overflow = null;
|
||||
document.removeEventListener("keydown", closeOnEscape);
|
||||
document.body.style.overflow = null;
|
||||
});
|
||||
|
||||
const maxWidthClass = computed(() => {
|
||||
return {
|
||||
'sm': 'sm:max-w-sm',
|
||||
'md': 'sm:max-w-md',
|
||||
'lg': 'sm:max-w-lg',
|
||||
'xl': 'sm:max-w-xl',
|
||||
'2xl': 'sm:max-w-2xl',
|
||||
}[props.maxWidth];
|
||||
return {
|
||||
sm: "sm:max-w-sm",
|
||||
md: "sm:max-w-md",
|
||||
lg: "sm:max-w-lg",
|
||||
xl: "sm:max-w-xl",
|
||||
"2xl": "sm:max-w-2xl",
|
||||
wide: "sm:max-w-[1200px] w-full",
|
||||
}[props.maxWidth];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent" ref="dialog">
|
||||
<div class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75" />
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div v-show="show" class="mb-6 bg-white rounded-lg overflow-visible shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
|
||||
<slot v-if="showSlot"/>
|
||||
</div>
|
||||
</transition>
|
||||
<dialog
|
||||
class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent"
|
||||
ref="dialog"
|
||||
>
|
||||
<div class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75" />
|
||||
</div>
|
||||
</dialog>
|
||||
</transition>
|
||||
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="mb-6 bg-white rounded-lg overflow-visible shadow-xl transform transition-all sm:w-full sm:mx-auto"
|
||||
:class="maxWidthClass"
|
||||
>
|
||||
<slot v-if="showSlot" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user