feat: filemanager - plná cesta na disku, podpora symlinků, oprava stylů
- Backend: listFiles vrací full_path, detekce symlinků/junctions - Backend: resolveProjectPath povoluje navigaci přes symlinky - Frontend: zobrazení plné cesty pod breadcrumbem - Frontend: ikona odkazu u symlinků s tooltipem cíle - Fix: underline jen na názvu složky, ne na počtu položek Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ class NasFileManager
|
|||||||
/**
|
/**
|
||||||
* Seznam souboru a slozek v dane ceste
|
* Seznam souboru a slozek v dane ceste
|
||||||
*
|
*
|
||||||
* @return array{path: string, items: list<array<string, mixed>>}|null
|
* @return array{path: string, items: list<array<string, mixed>>, full_path: string}|null
|
||||||
*/
|
*/
|
||||||
public function listFiles(string $projectNumber, string $subPath = ''): ?array
|
public function listFiles(string $projectNumber, string $subPath = ''): ?array
|
||||||
{
|
{
|
||||||
@@ -115,14 +115,23 @@ class NasFileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fullPath = $dirPath . '/' . $entry;
|
$fullPath = $dirPath . '/' . $entry;
|
||||||
|
$isLink = is_link($fullPath) || $this->isJunction($fullPath);
|
||||||
$isDir = is_dir($fullPath);
|
$isDir = is_dir($fullPath);
|
||||||
|
|
||||||
$item = [
|
$item = [
|
||||||
'name' => $entry,
|
'name' => $entry,
|
||||||
'type' => $isDir ? 'folder' : 'file',
|
'type' => $isDir ? 'folder' : 'file',
|
||||||
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
|
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
|
||||||
|
'is_symlink' => $isLink,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($isLink) {
|
||||||
|
$target = $this->readLinkTarget($fullPath);
|
||||||
|
if ($target !== null) {
|
||||||
|
$item['link_target'] = $target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($isDir) {
|
if ($isDir) {
|
||||||
$item['item_count'] = $this->countItems($fullPath);
|
$item['item_count'] = $this->countItems($fullPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -151,10 +160,14 @@ class NasFileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Realna cesta na disku (pro zobrazeni v UI)
|
||||||
|
$realDirPath = realpath($dirPath);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'path' => $subPath,
|
'path' => $subPath,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
'breadcrumb' => $breadcrumb,
|
'breadcrumb' => $breadcrumb,
|
||||||
|
'full_path' => str_replace('/', '\\', $realDirPath ?: $dirPath),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +400,9 @@ class NasFileManager
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a project sub-path to a safe absolute path
|
* Resolve a project sub-path to a safe absolute path
|
||||||
* Returns null if path traversal detected
|
* Returns null if path traversal detected.
|
||||||
|
* Symlinky/junctions jsou povoleny - realpath kontrola se preskoci
|
||||||
|
* pokud je v ceste symlink.
|
||||||
*/
|
*/
|
||||||
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
|
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
|
||||||
{
|
{
|
||||||
@@ -410,31 +425,68 @@ class NasFileManager
|
|||||||
|
|
||||||
$candidate = $folderPath . '/' . $subPath;
|
$candidate = $folderPath . '/' . $subPath;
|
||||||
|
|
||||||
$normalBase = str_replace('\\', '/', $folderPath);
|
if (!file_exists($candidate)) {
|
||||||
|
|
||||||
// realpath kontrola - soubor/slozka musi existovat pro existujici cesty
|
|
||||||
if (file_exists($candidate)) {
|
|
||||||
$real = realpath($candidate);
|
|
||||||
$normalReal = str_replace('\\', '/', (string) $real);
|
|
||||||
if ($real === false || !str_starts_with($normalReal, $normalBase)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $normalReal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pro nove soubory/slozky - kontrola rodice
|
// Pro nove soubory/slozky - kontrola rodice
|
||||||
$parentDir = dirname($candidate);
|
$parentDir = dirname($candidate);
|
||||||
if (file_exists($parentDir)) {
|
if (file_exists($parentDir)) {
|
||||||
$realParent = realpath($parentDir);
|
$normalBase = str_replace('\\', '/', $folderPath);
|
||||||
$normalParent = str_replace('\\', '/', (string) $realParent);
|
// Rodic musi byt v projektu nebo dosazitelny pres symlink z projektu
|
||||||
if ($realParent === false || !str_starts_with($normalParent, $normalBase)) {
|
if (!$this->isPathReachable($parentDir, $normalBase)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $candidate;
|
return $candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Existujici cesta - overi ze je dosazitelna z projektove slozky
|
||||||
|
// (bud primo uvnitr, nebo pres symlink ktery je v projektu)
|
||||||
|
$normalBase = str_replace('\\', '/', $folderPath);
|
||||||
|
if (!$this->isPathReachable($candidate, $normalBase)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('\\', '/', $candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overi ze cesta je dosazitelna z projektove slozky.
|
||||||
|
* Projde cestu segment po segmentu - pokud narazi na symlink
|
||||||
|
* v projektove slozce, povoli ho.
|
||||||
|
*/
|
||||||
|
private function isPathReachable(string $path, string $projectBase): bool
|
||||||
|
{
|
||||||
|
$normalPath = str_replace('\\', '/', $path);
|
||||||
|
|
||||||
|
// Primo v projektove slozce (bez symlinku)
|
||||||
|
$real = realpath($path);
|
||||||
|
if ($real !== false) {
|
||||||
|
$normalReal = str_replace('\\', '/', $real);
|
||||||
|
if (str_starts_with($normalReal, $projectBase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cesta muze vest pres symlink - overime ze symlink samotny
|
||||||
|
// je uvnitr projektove slozky
|
||||||
|
$parts = explode('/', str_replace('\\', '/', $normalPath));
|
||||||
|
$baseParts = explode('/', $projectBase);
|
||||||
|
$baseLen = count($baseParts);
|
||||||
|
|
||||||
|
$current = '';
|
||||||
|
foreach ($parts as $i => $part) {
|
||||||
|
$current .= ($i > 0 ? '/' : '') . $part;
|
||||||
|
if ($i < $baseLen) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Segment za projektovou slozkou - pokud je symlink, je to OK
|
||||||
|
if (file_exists($current) && (is_link($current) || $this->isJunction($current))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function deleteRecursive(string $path): bool
|
private function deleteRecursive(string $path): bool
|
||||||
{
|
{
|
||||||
if (is_file($path)) {
|
if (is_file($path)) {
|
||||||
@@ -495,6 +547,55 @@ class NasFileManager
|
|||||||
return round($bytes / 1073741824, 1) . ' GB';
|
return round($bytes / 1073741824, 1) . ' GB';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detekce NTFS junction pointu (Windows)
|
||||||
|
*/
|
||||||
|
private function isJunction(string $path): bool
|
||||||
|
{
|
||||||
|
if (PHP_OS_FAMILY !== 'Windows') {
|
||||||
|
return is_link($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
|
||||||
|
if ($attr !== false && $attr !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback - realpath se lisi od puvodniho path u junction
|
||||||
|
$real = realpath($path);
|
||||||
|
$normalized = str_replace('\\', '/', $path);
|
||||||
|
$normalReal = str_replace('\\', '/', (string) $real);
|
||||||
|
return $real !== false && strtolower($normalized) !== strtolower($normalReal) && is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precte cil symlinku/junction
|
||||||
|
*/
|
||||||
|
private function readLinkTarget(string $path): ?string
|
||||||
|
{
|
||||||
|
// PHP is_link + readlink
|
||||||
|
if (is_link($path)) {
|
||||||
|
$target = readlink($path);
|
||||||
|
return $target !== false ? str_replace('/', '\\', $target) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows junction - realpath ukazuje na cil
|
||||||
|
$real = realpath($path);
|
||||||
|
if ($real !== false) {
|
||||||
|
$normalized = str_replace('\\', '/', $path);
|
||||||
|
$normalReal = str_replace('\\', '/', $real);
|
||||||
|
if (strtolower($normalized) !== strtolower($normalReal)) {
|
||||||
|
return str_replace('/', '\\', $real);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
|
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
|
||||||
*/
|
*/
|
||||||
|
|||||||
137
dist/api/includes/NasFileManager.php
vendored
137
dist/api/includes/NasFileManager.php
vendored
@@ -94,7 +94,7 @@ class NasFileManager
|
|||||||
/**
|
/**
|
||||||
* Seznam souboru a slozek v dane ceste
|
* Seznam souboru a slozek v dane ceste
|
||||||
*
|
*
|
||||||
* @return array{path: string, items: list<array<string, mixed>>}|null
|
* @return array{path: string, items: list<array<string, mixed>>, full_path: string}|null
|
||||||
*/
|
*/
|
||||||
public function listFiles(string $projectNumber, string $subPath = ''): ?array
|
public function listFiles(string $projectNumber, string $subPath = ''): ?array
|
||||||
{
|
{
|
||||||
@@ -115,14 +115,23 @@ class NasFileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$fullPath = $dirPath . '/' . $entry;
|
$fullPath = $dirPath . '/' . $entry;
|
||||||
|
$isLink = is_link($fullPath) || $this->isJunction($fullPath);
|
||||||
$isDir = is_dir($fullPath);
|
$isDir = is_dir($fullPath);
|
||||||
|
|
||||||
$item = [
|
$item = [
|
||||||
'name' => $entry,
|
'name' => $entry,
|
||||||
'type' => $isDir ? 'folder' : 'file',
|
'type' => $isDir ? 'folder' : 'file',
|
||||||
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
|
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
|
||||||
|
'is_symlink' => $isLink,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($isLink) {
|
||||||
|
$target = $this->readLinkTarget($fullPath);
|
||||||
|
if ($target !== null) {
|
||||||
|
$item['link_target'] = $target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($isDir) {
|
if ($isDir) {
|
||||||
$item['item_count'] = $this->countItems($fullPath);
|
$item['item_count'] = $this->countItems($fullPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -151,10 +160,14 @@ class NasFileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Realna cesta na disku (pro zobrazeni v UI)
|
||||||
|
$realDirPath = realpath($dirPath);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'path' => $subPath,
|
'path' => $subPath,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
'breadcrumb' => $breadcrumb,
|
'breadcrumb' => $breadcrumb,
|
||||||
|
'full_path' => str_replace('/', '\\', $realDirPath ?: $dirPath),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +400,9 @@ class NasFileManager
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a project sub-path to a safe absolute path
|
* Resolve a project sub-path to a safe absolute path
|
||||||
* Returns null if path traversal detected
|
* Returns null if path traversal detected.
|
||||||
|
* Symlinky/junctions jsou povoleny - realpath kontrola se preskoci
|
||||||
|
* pokud je v ceste symlink.
|
||||||
*/
|
*/
|
||||||
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
|
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
|
||||||
{
|
{
|
||||||
@@ -410,31 +425,68 @@ class NasFileManager
|
|||||||
|
|
||||||
$candidate = $folderPath . '/' . $subPath;
|
$candidate = $folderPath . '/' . $subPath;
|
||||||
|
|
||||||
$normalBase = str_replace('\\', '/', $folderPath);
|
if (!file_exists($candidate)) {
|
||||||
|
|
||||||
// realpath kontrola - soubor/slozka musi existovat pro existujici cesty
|
|
||||||
if (file_exists($candidate)) {
|
|
||||||
$real = realpath($candidate);
|
|
||||||
$normalReal = str_replace('\\', '/', (string) $real);
|
|
||||||
if ($real === false || !str_starts_with($normalReal, $normalBase)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $normalReal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pro nove soubory/slozky - kontrola rodice
|
// Pro nove soubory/slozky - kontrola rodice
|
||||||
$parentDir = dirname($candidate);
|
$parentDir = dirname($candidate);
|
||||||
if (file_exists($parentDir)) {
|
if (file_exists($parentDir)) {
|
||||||
$realParent = realpath($parentDir);
|
$normalBase = str_replace('\\', '/', $folderPath);
|
||||||
$normalParent = str_replace('\\', '/', (string) $realParent);
|
// Rodic musi byt v projektu nebo dosazitelny pres symlink z projektu
|
||||||
if ($realParent === false || !str_starts_with($normalParent, $normalBase)) {
|
if (!$this->isPathReachable($parentDir, $normalBase)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $candidate;
|
return $candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Existujici cesta - overi ze je dosazitelna z projektove slozky
|
||||||
|
// (bud primo uvnitr, nebo pres symlink ktery je v projektu)
|
||||||
|
$normalBase = str_replace('\\', '/', $folderPath);
|
||||||
|
if (!$this->isPathReachable($candidate, $normalBase)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_replace('\\', '/', $candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overi ze cesta je dosazitelna z projektove slozky.
|
||||||
|
* Projde cestu segment po segmentu - pokud narazi na symlink
|
||||||
|
* v projektove slozce, povoli ho.
|
||||||
|
*/
|
||||||
|
private function isPathReachable(string $path, string $projectBase): bool
|
||||||
|
{
|
||||||
|
$normalPath = str_replace('\\', '/', $path);
|
||||||
|
|
||||||
|
// Primo v projektove slozce (bez symlinku)
|
||||||
|
$real = realpath($path);
|
||||||
|
if ($real !== false) {
|
||||||
|
$normalReal = str_replace('\\', '/', $real);
|
||||||
|
if (str_starts_with($normalReal, $projectBase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cesta muze vest pres symlink - overime ze symlink samotny
|
||||||
|
// je uvnitr projektove slozky
|
||||||
|
$parts = explode('/', str_replace('\\', '/', $normalPath));
|
||||||
|
$baseParts = explode('/', $projectBase);
|
||||||
|
$baseLen = count($baseParts);
|
||||||
|
|
||||||
|
$current = '';
|
||||||
|
foreach ($parts as $i => $part) {
|
||||||
|
$current .= ($i > 0 ? '/' : '') . $part;
|
||||||
|
if ($i < $baseLen) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Segment za projektovou slozkou - pokud je symlink, je to OK
|
||||||
|
if (file_exists($current) && (is_link($current) || $this->isJunction($current))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function deleteRecursive(string $path): bool
|
private function deleteRecursive(string $path): bool
|
||||||
{
|
{
|
||||||
if (is_file($path)) {
|
if (is_file($path)) {
|
||||||
@@ -495,6 +547,55 @@ class NasFileManager
|
|||||||
return round($bytes / 1073741824, 1) . ' GB';
|
return round($bytes / 1073741824, 1) . ' GB';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detekce NTFS junction pointu (Windows)
|
||||||
|
*/
|
||||||
|
private function isJunction(string $path): bool
|
||||||
|
{
|
||||||
|
if (PHP_OS_FAMILY !== 'Windows') {
|
||||||
|
return is_link($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attr = @exec('fsutil reparsepoint query "' . str_replace('/', '\\', $path) . '" 2>NUL');
|
||||||
|
if ($attr !== false && $attr !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback - realpath se lisi od puvodniho path u junction
|
||||||
|
$real = realpath($path);
|
||||||
|
$normalized = str_replace('\\', '/', $path);
|
||||||
|
$normalReal = str_replace('\\', '/', (string) $real);
|
||||||
|
return $real !== false && strtolower($normalized) !== strtolower($normalReal) && is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precte cil symlinku/junction
|
||||||
|
*/
|
||||||
|
private function readLinkTarget(string $path): ?string
|
||||||
|
{
|
||||||
|
// PHP is_link + readlink
|
||||||
|
if (is_link($path)) {
|
||||||
|
$target = readlink($path);
|
||||||
|
return $target !== false ? str_replace('/', '\\', $target) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows junction - realpath ukazuje na cil
|
||||||
|
$real = realpath($path);
|
||||||
|
if ($real !== false) {
|
||||||
|
$normalized = str_replace('\\', '/', $path);
|
||||||
|
$normalReal = str_replace('\\', '/', $real);
|
||||||
|
if (strtolower($normalized) !== strtolower($normalReal)) {
|
||||||
|
return str_replace('/', '\\', $real);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
|
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"window_start":1773405014,"count":4}
|
{"window_start":1773406850,"count":6}
|
||||||
@@ -1 +1 @@
|
|||||||
{"window_start":1773404939,"count":1}
|
{"window_start":1773406619,"count":1}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-Bay45BGf.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["−",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
|
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-c9Us0bor.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["−",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectDetail-B_QNS7Ph.js
vendored
1
dist/assets/ProjectDetail-B_QNS7Ph.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectDetail-DBLvWP4x.js
vendored
Normal file
1
dist/assets/ProjectDetail-DBLvWP4x.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-Bay45BGf.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
|
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-c9Us0bor.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-Bay45BGf.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
|
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-c9Us0bor.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
|
||||||
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -29,11 +29,11 @@
|
|||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Urbanist:wght@400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Urbanist:wght@400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
<script type="module" crossorigin src="/assets/index-Bay45BGf.js"></script>
|
<script type="module" crossorigin src="/assets/index-c9Us0bor.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BVs3cwbi.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BVs3cwbi.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-animation-0s3FMHwK.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-animation-0s3FMHwK.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-Dyr8OjFr.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-Dyr8OjFr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-S7b0Xjr1.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-eC-aMCY3.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="background-color: var(--bg-primary, #12121a);">
|
<body style="background-color: var(--bg-primary, #12121a);">
|
||||||
|
|||||||
4
dist/vendor/composer/installed.php
vendored
4
dist/vendor/composer/installed.php
vendored
@@ -3,7 +3,7 @@
|
|||||||
'name' => 'boha/website',
|
'name' => 'boha/website',
|
||||||
'pretty_version' => 'dev-master',
|
'pretty_version' => 'dev-master',
|
||||||
'version' => 'dev-master',
|
'version' => 'dev-master',
|
||||||
'reference' => '45fd930f76debb906f6a51b10aab2cb0937ac200',
|
'reference' => '019a8355dc85203c5fc76591ec9c01a01068edc9',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
'boha/website' => array(
|
'boha/website' => array(
|
||||||
'pretty_version' => 'dev-master',
|
'pretty_version' => 'dev-master',
|
||||||
'version' => 'dev-master',
|
'version' => 'dev-master',
|
||||||
'reference' => '45fd930f76debb906f6a51b10aab2cb0937ac200',
|
'reference' => '019a8355dc85203c5fc76591ec9c01a01068edc9',
|
||||||
'type' => 'project',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
@@ -2514,6 +2514,17 @@ img {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fm-full-path {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
user-select: all;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.fm-toolbar-actions {
|
.fm-toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -2621,9 +2632,6 @@ img {
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fm-folder-link:hover {
|
.fm-folder-link:hover {
|
||||||
@@ -2652,3 +2660,16 @@ img {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fm-name-cell {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fm-symlink-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,22 +46,41 @@ function getFileIcon(type, extension) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SymlinkBadge({ target }) {
|
||||||
|
return (
|
||||||
|
<span className="fm-symlink-badge" title={target || 'Odkaz'}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function FileNameCell({ item, onFolderClick }) {
|
function FileNameCell({ item, onFolderClick }) {
|
||||||
if (item.type === 'folder') {
|
if (item.type === 'folder') {
|
||||||
return (
|
return (
|
||||||
|
<span className="fm-name-cell">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="fm-folder-link"
|
className="fm-folder-link"
|
||||||
onClick={() => onFolderClick(item.name)}
|
onClick={() => onFolderClick(item.name)}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
</button>
|
||||||
|
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
|
||||||
{item.item_count !== undefined && (
|
{item.item_count !== undefined && (
|
||||||
<span className="fm-item-count">{item.item_count}</span>
|
<span className="fm-item-count">{item.item_count}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <span className="fm-file-name">{item.name}</span>
|
return (
|
||||||
|
<span className="fm-name-cell">
|
||||||
|
<span className="fm-file-name">{item.name}</span>
|
||||||
|
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectFileManager({ projectId, projectNumber, hasPermission, hasNasFolder }) {
|
export default function ProjectFileManager({ projectId, projectNumber, hasPermission, hasNasFolder }) {
|
||||||
@@ -72,6 +91,7 @@ export default function ProjectFileManager({ projectId, projectNumber, hasPermis
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [currentPath, setCurrentPath] = useState('')
|
const [currentPath, setCurrentPath] = useState('')
|
||||||
const [breadcrumb, setBreadcrumb] = useState([''])
|
const [breadcrumb, setBreadcrumb] = useState([''])
|
||||||
|
const [fullPath, setFullPath] = useState('')
|
||||||
|
|
||||||
const [dragOver, setDragOver] = useState(false)
|
const [dragOver, setDragOver] = useState(false)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
@@ -105,6 +125,7 @@ export default function ProjectFileManager({ projectId, projectNumber, hasPermis
|
|||||||
setItems(data.data.items || [])
|
setItems(data.data.items || [])
|
||||||
setBreadcrumb(data.data.breadcrumb || [''])
|
setBreadcrumb(data.data.breadcrumb || [''])
|
||||||
setCurrentPath(data.data.path || '')
|
setCurrentPath(data.data.path || '')
|
||||||
|
setFullPath(data.data.full_path || '')
|
||||||
} else if (res.status === 404) {
|
} else if (res.status === 404) {
|
||||||
setItems([])
|
setItems([])
|
||||||
setBreadcrumb([''])
|
setBreadcrumb([''])
|
||||||
@@ -403,6 +424,10 @@ export default function ProjectFileManager({ projectId, projectNumber, hasPermis
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{fullPath && (
|
||||||
|
<span className="fm-full-path" title={fullPath}>{fullPath}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<div className="fm-toolbar-actions">
|
<div className="fm-toolbar-actions">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user