Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSES/CLA-signed-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ C/ My company has custom contribution contract with Lutra Consulting Ltd. or I a
* lavor, 26th April 2023
* luxusko, 25th August 2023
* jozef-budac, 30th January 2024
* fernandinand, 13th March 2025
5 changes: 2 additions & 3 deletions server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

from .sync.utils import get_blacklisted_dirs, get_blacklisted_files
from .config import Configuration
from .commands import add_commands
from .commands import add_commands as server_commands

convention = {
"ix": "ix_%(column_0_label)s",
Expand Down Expand Up @@ -139,8 +139,6 @@ def create_simple_app() -> Flask:
if Configuration.GEVENT_WORKER:
flask_app.wsgi_app = GeventTimeoutMiddleware(flask_app.wsgi_app)

add_commands(flask_app)

return flask_app


Expand Down Expand Up @@ -189,6 +187,7 @@ def create_app(public_keys: List[str] = None) -> Flask:
login_manager.init_app(app.app)
# register auth blueprint
register_auth(app.app)
server_commands(app.app)

# adjust login manager
@login_manager.user_loader
Expand Down
19 changes: 19 additions & 0 deletions server/mergin/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from flask import Flask
import random
import string
import os
from datetime import datetime, timezone


Expand Down Expand Up @@ -82,6 +83,16 @@ def _send_email(email: str):
f"Error sending email: {e}",
)

def _check_permissions(path):
"""Check for write permission on working folders"""

if not os.access(path, os.W_OK):
_echo_error(
f"Permissions for {path} folder not set correctly. Please review these settings.",
)
else:
click.secho(f"Permissions granted for {path} folder", fg="green")

def _check_server(): # pylint: disable=W0612
"""Check server configuration."""

Expand Down Expand Up @@ -117,6 +128,8 @@ def _check_server(): # pylint: disable=W0612
else:
click.secho("Database initialized properly", fg="green")

_check_permissions(app.config.get("LOCAL_PROJECTS"))

_check_celery()

def _init_db():
Expand Down Expand Up @@ -209,3 +222,9 @@ def send_check_email(email: str): # pylint: disable=W0612
def check():
"""Check server configuration."""
_check_server()

@server.command()
@click.option("--path", required=False, default=app.config.get("LOCAL_PROJECTS"))
def permissions(path: str):
"""Check for specific path write permission"""
_check_permissions(path)
2 changes: 1 addition & 1 deletion web-app/packages/admin-app/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const createRouter = (pinia: Pinia) => {
props: true
},
{
path: 'tree/:location?',
path: 'tree/:location(.*)?',
name: AdminRoutes.ProjectTree,
component: ProjectFilesView,
props: true
Expand Down
6 changes: 3 additions & 3 deletions web-app/packages/app/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ export const createRouter = (pinia: Pinia) => {
meta: {
breadcrump: [{ title: 'Projects', path: '/projects' }]
},
redirect: { name: 'project-tree' },
redirect: { name: ProjectRouteName.ProjectTree },

children: [
{
path: 'tree/:location?',
name: 'project-tree',
path: 'tree/:location(.*)?',
name: ProjectRouteName.ProjectTree,
component: FileBrowserView,
props: true,
meta: { public: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,6 @@ $colors: (
--rose-color: #{map-get($colors, "rose")};
--dark-green-color: #{map-get($colors, "dark-green")};
--negative-light-color: #{map-get($colors, "negative-light")};
--earth-color: #{map-get($colors, "earth")};
color-scheme: light;
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ $actionIconBorderRadius: 50%;

/// Scale factor of small component size
/// @group general
$scaleSM:0.875;
$scaleSM:0.750;

/// Scale factor of small large size
/// @group general
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
</ul>
</nav>
</div>
<footer class="flex flex-column row-gap-3 p-3">
<footer class="flex flex-column row-gap-3 p-4">
<slot name="footer">
<!-- footer content -->
<template v-if="userStore.isSuperUser">
Expand Down
209 changes: 126 additions & 83 deletions web-app/packages/lib/src/modules/project/components/FilesTable.vue
Original file line number Diff line number Diff line change
@@ -1,69 +1,110 @@
<template>
<PDataView
:value="items"
:data-key="'path'"
:paginator="items.length > itemPerPage"
:sort-field="options.sortBy"
:sort-order="options.sortDesc"
:rows="itemPerPage"
:paginator-template="'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'"
data-cy="file-browser-table"
>
<template #header>
<div class="grid grid-nogutter">
<div
v-for="col in columns"
:class="[`col-${col.cols ?? 3}`, 'paragraph-p6 hidden lg:flex']"
:key="col.text"
<div>
<PBreadcrumb
:model="breadcrumps"
:pt="{
root: {
style: {
backgroundColor: 'transparent'
},
class: 'border-none p-4 w-full overflow-x-auto'
}
}"
>
<template #separator>
<i class="ti ti-slash" />
</template>
<template #item="{ item, props }">
<router-link
v-if="item.path"
v-slot="{ href, navigate }"
:to="{
path: item.path
}"
custom
>
{{ col.text }}
<a :href="href" v-bind="props.action" @click="navigate">
<span :class="[item.icon, 'paragraph-p6']" />
{{ '&nbsp;' }}
<span
:class="[
'opacity-80 paragraph-p6',
item.active ? 'text-color-forest font-semibold' : 'text-color'
]"
>{{ item.label }}</span
>
</a>
</router-link>
</template>
</PBreadcrumb>
<PDataView
:value="items"
:data-key="'path'"
:paginator="items.length > ITEMS_PER_PAGE"
:sort-field="options.sortBy"
:sort-order="options.sortDesc"
:rows="ITEMS_PER_PAGE"
:paginator-template="'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'"
data-cy="file-browser-table"
>
<template #header>
<div class="grid grid-nogutter">
<div
v-for="col in columns"
:class="[`col-${col.cols ?? 2}`, 'paragraph-p6 hidden lg:flex']"
:key="col.text"
>
{{ col.text }}
</div>
<div class="col-12 flex lg:hidden">Files</div>
</div>
<div class="col-12 flex lg:hidden">Files</div>
</div>
</template>
<template #list="slotProps">
<div
v-for="item in slotProps.items"
:key="item.id"
class="grid grid-nogutter px-4 py-3 mt-0 border-bottom-1 border-gray-200 paragraph-p6 hover:bg-gray-50 cursor-pointer row-gap-2"
@click.prevent="rowClick(item)"
>
</template>
<template #list="slotProps">
<div
v-for="col in columns"
:class="[`lg:col-${col.cols ?? 3}`, 'flex align-items-center col-12']"
:key="col.key"
v-for="item in slotProps.items"
:key="item.id"
class="grid grid-nogutter px-4 py-3 mt-0 border-bottom-1 border-gray-200 paragraph-p6 hover:bg-gray-50 cursor-pointer row-gap-2"
@click.prevent="rowClick(item)"
>
<span
v-if="col.key === 'name'"
class="font-semibold mb-2 lg:mb-0 m-0"
<div
v-for="col in columns"
:class="[
`lg:col-${col.cols ?? 2}`,
'flex align-items-center col-12'
]"
:key="col.key"
>
<file-icon :file="item" />{{ item.name }}
</span>
<span
v-else-if="col.key === 'mtime'"
v-tooltip.bottom="{ value: $filters.datetime(item.mtime) }"
class="opacity-80"
>
{{ $filters.timediff(item.mtime) }}
</span>
<span v-else class="opacity-80">{{
item.size !== undefined ? $filters.filesize(item.size) : ''
}}</span>
<span
v-if="col.key === 'name'"
class="font-semibold mb-2 lg:mb-0 m-0"
>
<file-icon :file="item" />{{ item.name }}
</span>
<span
v-else-if="col.key === 'mtime'"
v-tooltip.bottom="{ value: $filters.datetime(item.mtime) }"
class="opacity-80"
>
{{ item.mtime ? $filters.timediff(item.mtime) : '' }}
</span>
<span v-else class="opacity-80">{{
item.size !== undefined ? $filters.filesize(item.size) : ''
}}</span>
</div>
</div>
</template>
<template #empty>
<div class="w-full text-center p-4">
<span>No files found.</span>
</div>
</div>
</template>
<template #empty>
<div class="w-full text-center p-4">
<span>No files found.</span>
</div>
</template>
</PDataView>
</template>
</PDataView>
</div>
</template>

<script setup lang="ts">
import escapeRegExp from 'lodash/escapeRegExp'
import max from 'lodash/max'
import orderBy from 'lodash/orderBy'
import Path from 'path'
import { computed } from 'vue'

Expand All @@ -81,7 +122,7 @@ interface Column {
interface FileItem {
name?: string
path: string
type?: string
type?: 'folder' | 'file'
link?: string
size?: number
mtime?: string
Expand All @@ -108,14 +149,37 @@ const emit = defineEmits<{

const projectStore = useProjectStore()

const itemPerPage = 50
const ITEMS_PER_PAGE = 100

const columns: Column[] = [
{ text: 'Name', key: 'name', cols: 6 },
{ text: 'Name', key: 'name', cols: 8 },
{ text: 'Modified', key: 'mtime' },
{ text: 'Size', key: 'size' }
]

const breadcrumps = computed(() => {
const location = props.location
const parts = location.split('/').filter(Boolean)
let path = ''
const result = parts.map((part, index) => {
path = Path.join(path, part)
return {
label: part,
path: folderLink(path),
active: index === parts.length - 1
}
})
return [
{
icon: 'ti ti-folder',
label: 'Files',
path: folderLink(''),
active: parts.length === 0
},
...result
]
})

const projectFiles = computed(() => {
let files = projectStore.project.files
if (projectStore.uploads[projectStore.project.path] && diff.value) {
Expand All @@ -133,7 +197,7 @@ const directoryFiles = computed(() => {
)
})

const folders = computed(() => {
const folders = computed<FileItem[]>(() => {
const folders: { [key: string]: FileItem[] } = {}
const prefix = props.location ? escapeRegExp(props.location + '/') : ''
const regex = new RegExp(`^${prefix}([^/]+)/`)
Expand Down Expand Up @@ -190,34 +254,13 @@ function filterByLocation(files) {
}

const items = computed(() => {
const result: FileItem[] = [...folders.value, ...directoryFiles.value]
if (props.search) {
const regex = new RegExp(escapeRegExp(removeAccents(props.search)), 'i')
// TODO: Replace with DataView sorting instead this order_by with lodash
return orderBy(
filterByLocation(projectFiles.value).filter(
(f) => f.path.search(regex) !== -1
),
props.options.sortBy,
props.options.sortDesc < 0 ? 'desc' : 'asc'
)
return filterByLocation(result).filter((f) => f.path.search(regex) !== -1)
}

let result: FileItem[] = []
if (props.location) {
result.push({
name: '..',
type: 'folder',
link: Path.normalize(Path.join(props.location, '..')),
mtime: '',
path: ''
})
}
result = result.concat(folders.value, directoryFiles.value)
return orderBy(
result,
props.options?.sortBy,
props.options.sortDesc < 0 ? 'desc' : 'asc'
)
return result
})

function fileFlags(file: FileItem) {
Expand Down Expand Up @@ -250,7 +293,7 @@ function folderDiffStats(files: FileItem[]) {
}
}

function fileTreeView(file: FileItem) {
function fileTreeView(file): FileItem {
const filename = Path.basename(file.path)
return {
...file,
Expand Down
Loading
Loading