Compare commits

...

36 Commits

Author SHA1 Message Date
Louis Chmn 67c31584e2 HACK: limit the amount of files returned by getFolderContentsById
Signed-off-by: Louis Chmn <louis@chmn.me>
2026-02-11 15:49:09 +01:00
Louis d21351701a Merge pull request #58140 from nextcloud/artonge/fix/ignore_abort_error
fix(files): Do not show abort error to the user
2026-02-11 15:03:02 +01:00
nextcloud-command 6c04307e13 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-11 13:42:45 +00:00
Louis Chmn a4f396e648 fix(files): Do not show abort error to the user
Signed-off-by: Louis Chmn <louis@chmn.me>
2026-02-11 13:40:33 +00:00
Kate 615d343d96 Merge pull request #58256 from nextcloud/fix/iresult/fetch-all-conditional-return-type 2026-02-11 09:26:55 +01:00
provokateurin 83fbc64c99 fix(IResult): Use more accurate conditional return type for fetchAll
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-11 08:50:13 +01:00
Nextcloud bot 4a9e04962c fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-02-11 00:20:55 +00:00
Anna feaebeb97e Merge pull request #58223 from nextcloud/fix/oracle-truncate-table
fix: quote tablenames for truncating in oracle
2026-02-10 23:29:34 +01:00
grnd-alt bc5771b0ff fix: correctly quote tablenames for truncating with oracle
Signed-off-by: grnd-alt <git@belakkaf.net>
2026-02-10 22:48:02 +01:00
Sebastian Krupinski 63eb9679c2 Merge pull request #58228 from nextcloud/fix/group-shares-cleanup
fix: delete CalDav and CardDav shares upon group deletion
2026-02-10 11:49:15 -05:00
Kate 98cb8b6155 Merge pull request #55632 from nextcloud/feat/preview/expire-previews
feat(preview): Expire previews
2026-02-10 17:35:28 +01:00
Maximilian Martin 6e5baa6928 fix: delete CalDav and CardDav shares upon group deletion
Signed-off-by: Maximilian Martin <maximilian_martin@gmx.de>
2026-02-10 11:14:40 -05:00
Maksim Sukharev a096c89c66 Merge pull request #58218 from nextcloud/fix/57804/clear-status-this-week 2026-02-10 16:56:23 +01:00
nextcloud-command 6563214204 chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-10 15:36:54 +00:00
Cristian Scheid 2ddf73f89f fix(user_status): use getFirstDay() from @nextcloud/l10n
Signed-off-by: Cristian Scheid <cristianscheid@gmail.com>
2026-02-10 16:27:51 +01:00
provokateurin fe9e43c165 feat(preview): Expire previews
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-10 15:35:38 +01:00
Ferdinand Thiessen 1a5679b176 Merge pull request #58208 from nextcloud/chore/update-files-4rc3
chore: update `@nextcloud/files` to v4.0.0-rc.3
2026-02-10 15:22:34 +01:00
Ferdinand Thiessen 7b7d74fda2 chore: compile assets
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:53:19 +01:00
Ferdinand Thiessen f075051f4a test: remove testing internal of libraries and test only app code
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen 0e361550f1 fix(files): add legacy wrapper for upload library
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen 38644873f2 refactor(files): migrate to files registry for reactive file actions
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen 643a815557 refactor(files): migrate file list actions to new files registry
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen c73b85aecb refactor(files): port file list headers to new files registry
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen 4a284f61e6 refactor(files): port filters store to new files registry
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen e088473929 refactor(files_sharing): adjust note to recipient files header
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:41 +01:00
Ferdinand Thiessen 29b47c93ab chore(deps): update @nextcloud/files to v4.0.0-rc.3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-02-10 14:50:40 +01:00
github-actions[bot] 935cd2910f Merge pull request #58213 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/axios-1.13.5
chore(deps): Bump axios from 1.12.2 to 1.13.5 in /build/frontend-legacy
2026-02-10 13:21:20 +00:00
nextcloud-command 422bca31bf chore(assets): Recompile assets
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
2026-02-10 12:39:26 +00:00
dependabot[bot] adce834b4f chore(deps): Bump axios from 1.12.2 to 1.13.5 in /build/frontend-legacy
Bumps [axios](https://github.com/axios/axios) from 1.12.2 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.2...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 13:23:36 +01:00
Andy Scherzinger 7da7f50203 Merge pull request #58209 from nextcloud/carl/local-preview-fix
fix(preview): Fix scanning preview
2026-02-10 12:38:58 +01:00
Anna 8c01737a63 Merge pull request #58206 from nextcloud/fix/preview-637/cast-snowflake-id-to-int
fix(snowflake): cast lastId to string
2026-02-10 11:46:07 +01:00
Kate e0c282d531 Merge pull request #58195 from nextcloud/ci/rector-apply
ci: Add workflow to apply rector changes weekly
2026-02-10 10:36:25 +01:00
Nextcloud bot d65aa0b7c3 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-02-10 00:21:48 +00:00
Carl Schwan 543b46f3aa fix(preview): Fix scanning preview
Make sure we set the mimetype string representation and not int
representation.

Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-02-09 22:49:59 +01:00
Anna Larch 99a1150ec2 fix(snowflake): cast lastId to int
Signed-off-by: Anna Larch <anna@nextcloud.com>
2026-02-09 21:36:20 +01:00
provokateurin 0469f57a3a ci: Add workflow to apply rector changes weekly
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-09 14:55:54 +01:00
321 changed files with 1526 additions and 1120 deletions
+68
View File
@@ -0,0 +1,68 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
#
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: Apply rector changes
on:
workflow_dispatch:
schedule:
# At 14:30 on Sundays
- cron: '30 14 * * 0'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
name: rector-apply
steps:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.event.repository.default_branch }}
- name: Get php version
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
- name: Set up php${{ steps.versions.outputs.php-min }}
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2.36.0
with:
php-version: ${{ steps.versions.outputs.php-min }}
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
composer remove nextcloud/ocp --dev --no-scripts
composer i
- name: Rector
run: composer run rector
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
commit-message: 'refactor: Apply rector changes'
committer: GitHub <noreply@github.com>
author: nextcloud-command <nextcloud-command@users.noreply.github.com>
signoff: true
branch: automated/noid/rector-changes
title: 'Apply rector changes'
labels: |
technical debt
3. to review
+2 -2
View File
@@ -1,8 +1,8 @@
OC.L10N.register(
"cloud_federation_api",
{
"Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.",
"Cloud Federation API" : "Jamg'armaning bulutli APIsi",
"Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi",
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Jamoada API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
},
"nplurals=1; plural=0;");
+2 -2
View File
@@ -1,6 +1,6 @@
{ "translations": {
"Cloud Federation API" : "Asl faylni o'chirishda kutilmagan xatolik yuz berdi.",
"Cloud Federation API" : "Jamg'armaning bulutli APIsi",
"Enable clouds to communicate with each other and exchange data" : "Bulutlar bir-biri bilan aloqa qilish va ma'lumot almashish imkonini beradi",
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Federation API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "Cloud Jamoada API turli xil Nextcloud misollariga bir-biri bilan muloqot qilish va ma'lumotlarni almashish imkonini beradi."
},"pluralForm" :"nplurals=1; plural=0;"
}
+37
View File
@@ -0,0 +1,37 @@
OC.L10N.register(
"comments",
{
"Comments" : "Izohlar",
"You commented" : "Siz fikr bildirgansiz",
"{author} commented" : "{author} izoh qoldirdi",
"You commented on %1$s" : "Siz %1$s haqida fikr bildirdingiz",
"You commented on {file}" : "Siz {file} ga izoh qoldirdingiz",
"%1$s commented on %2$s" : "%1$s %2$s haqida fikr bildirdi",
"{author} commented on {file}" : "{author} {file} ga izoh qoldirdi",
"<strong>Comments</strong> for files" : "Fayllar uchun <strong>Izohlar</strong>",
"Files" : "Fayllar",
"You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Siz \"{file}\"da, keyinchalik o'chirilgan hisob tomonidan izohda tilga olingansiz",
"{user} mentioned you in a comment on \"{file}\"" : "{user} sizni \"{file}\" dagi izohda tilga oldi",
"Files app plugin to add comments to files" : "Fayllarga izohlar qo'shish ilova plagini",
"Edit comment" : "Izohni tahrirlash",
"Delete comment" : "Izohni o'chirish",
"Cancel edit" : "Tahrirni bekor qilish",
"New comment" : "Yangi izoh",
"Write a comment …" : "Fikr yozing…",
"Post comment" : "Fikr qoldirish",
"@ for mentions, : for emoji, / for smart picker" : "@ eslatmalar uchun, : emojilar uchun, / aqlli tanlovclar uchun",
"Could not reload comments" : "Izohlarni qayta yuklab bo'lmadi",
"Failed to mark comments as read" : "Izohlarni o'qilgan deb belgilashda xatolik yuz berdi",
"Unable to load the comments list" : "Izohlar ro'yxatini yuklab bo'lmadi",
"No comments yet, start the conversation!" : "Hali izohlar yo'q, suhbatni boshlang!",
"No more messages" : "Boshqa xabarlar yo'q",
"Retry" : "Qayta urinish",
"_1 new comment_::_{unread} new comments_" : ["{unread} ta yangi izoh"],
"Comment" : "Izoh",
"An error occurred while trying to edit the comment" : "Izohni tahrirlashda xatolik yuz berdi",
"Comment deleted" : "Izoh o'chirildi",
"An error occurred while trying to delete the comment" : "Izohni o'chirishda xatolik yuz berdi",
"An error occurred while trying to create the comment" : "Izoh yaratishda xatolik yuz berdi",
"Write a comment …" : "Izoh yozing..."
},
"nplurals=1; plural=0;");
+35
View File
@@ -0,0 +1,35 @@
{ "translations": {
"Comments" : "Izohlar",
"You commented" : "Siz fikr bildirgansiz",
"{author} commented" : "{author} izoh qoldirdi",
"You commented on %1$s" : "Siz %1$s haqida fikr bildirdingiz",
"You commented on {file}" : "Siz {file} ga izoh qoldirdingiz",
"%1$s commented on %2$s" : "%1$s %2$s haqida fikr bildirdi",
"{author} commented on {file}" : "{author} {file} ga izoh qoldirdi",
"<strong>Comments</strong> for files" : "Fayllar uchun <strong>Izohlar</strong>",
"Files" : "Fayllar",
"You were mentioned on \"{file}\", in a comment by an account that has since been deleted" : "Siz \"{file}\"da, keyinchalik o'chirilgan hisob tomonidan izohda tilga olingansiz",
"{user} mentioned you in a comment on \"{file}\"" : "{user} sizni \"{file}\" dagi izohda tilga oldi",
"Files app plugin to add comments to files" : "Fayllarga izohlar qo'shish ilova plagini",
"Edit comment" : "Izohni tahrirlash",
"Delete comment" : "Izohni o'chirish",
"Cancel edit" : "Tahrirni bekor qilish",
"New comment" : "Yangi izoh",
"Write a comment …" : "Fikr yozing…",
"Post comment" : "Fikr qoldirish",
"@ for mentions, : for emoji, / for smart picker" : "@ eslatmalar uchun, : emojilar uchun, / aqlli tanlovclar uchun",
"Could not reload comments" : "Izohlarni qayta yuklab bo'lmadi",
"Failed to mark comments as read" : "Izohlarni o'qilgan deb belgilashda xatolik yuz berdi",
"Unable to load the comments list" : "Izohlar ro'yxatini yuklab bo'lmadi",
"No comments yet, start the conversation!" : "Hali izohlar yo'q, suhbatni boshlang!",
"No more messages" : "Boshqa xabarlar yo'q",
"Retry" : "Qayta urinish",
"_1 new comment_::_{unread} new comments_" : ["{unread} ta yangi izoh"],
"Comment" : "Izoh",
"An error occurred while trying to edit the comment" : "Izohni tahrirlashda xatolik yuz berdi",
"Comment deleted" : "Izoh o'chirildi",
"An error occurred while trying to delete the comment" : "Izohni o'chirishda xatolik yuz berdi",
"An error occurred while trying to create the comment" : "Izoh yaratishda xatolik yuz berdi",
"Write a comment …" : "Izoh yozing..."
},"pluralForm" :"nplurals=1; plural=0;"
}
+2
View File
@@ -89,6 +89,7 @@ use OCP\Contacts\IManager as IContactsManager;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\Federation\Events\TrustedServerRemovedEvent;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Server;
@@ -205,6 +206,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(UserCreatedEvent::class, UserEventsListener::class);
$context->registerEventListener(UserChangedEvent::class, UserEventsListener::class);
$context->registerEventListener(UserUpdatedEvent::class, UserEventsListener::class);
$context->registerEventListener(GroupDeletedEvent::class, UserEventsListener::class);
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
+11 -1
View File
@@ -20,6 +20,8 @@ use OCP\BackgroundJob\IJobList;
use OCP\Defaults;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Group\Events\BeforeGroupDeletedEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\IUser;
use OCP\IUserManager;
use OCP\User\Events\BeforeUserDeletedEvent;
@@ -32,7 +34,7 @@ use OCP\User\Events\UserIdAssignedEvent;
use OCP\User\Events\UserIdUnassignedEvent;
use Psr\Log\LoggerInterface;
/** @template-implements IEventListener<UserFirstTimeLoggedInEvent|UserIdAssignedEvent|BeforeUserIdUnassignedEvent|UserIdUnassignedEvent|BeforeUserDeletedEvent|UserDeletedEvent|UserCreatedEvent|UserChangedEvent|UserUpdatedEvent> */
/** @template-implements IEventListener<UserFirstTimeLoggedInEvent|UserIdAssignedEvent|BeforeUserIdUnassignedEvent|UserIdUnassignedEvent|BeforeUserDeletedEvent|UserDeletedEvent|UserCreatedEvent|UserChangedEvent|UserUpdatedEvent|BeforeGroupDeletedEvent|GroupDeletedEvent> */
class UserEventsListener implements IEventListener {
/** @var IUser[] */
@@ -77,6 +79,8 @@ class UserEventsListener implements IEventListener {
$this->firstLogin($event->getUser());
} elseif ($event instanceof UserUpdatedEvent) {
$this->updateUser($event->getUser());
} elseif ($event instanceof GroupDeletedEvent) {
$this->postDeleteGroup($event->getGroup()->getGID());
}
}
@@ -135,6 +139,12 @@ class UserEventsListener implements IEventListener {
unset($this->addressBooksToDelete[$uid]);
}
public function postDeleteGroup(string $gid): void {
$encodedGid = urlencode($gid);
$this->calDav->deleteAllSharesByUser('principals/groups/' . $encodedGid);
$this->cardDav->deleteAllSharesByUser('principals/groups/' . $encodedGid);
}
public function changeUser(IUser $user, string $feature): void {
// This case is already covered by the account manager firing up a signal
// later on
@@ -182,4 +182,15 @@ class UserEventsListenerTest extends TestCase {
$this->userEventsListener->preDeleteUser($user);
$this->userEventsListener->postDeleteUser('newUser');
}
public function testDeleteGroup(): void {
$this->calDavBackend->expects($this->once())
->method('deleteAllSharesByUser')
->with('principals/groups/testGroup');
$this->cardDavBackend->expects($this->once())
->method('deleteAllSharesByUser')
->with('principals/groups/testGroup');
$this->userEventsListener->postDeleteGroup('testGroup');
}
}
+9
View File
@@ -79,6 +79,7 @@ OC.L10N.register(
"Go to the \"{dir}\" directory" : "Ir a la carpeta \"{dir}\"",
"Current directory path" : "Dirección de la carpeta actual",
"Share" : "Compartir",
"Reload content" : "Recargar contenido",
"Your have used your space quota and cannot upload files anymore" : "Ha utilizado su cuota de espacio y ya no puede subir más archivos",
"You do not have permission to upload or create files here." : "No tiene permiso para subir o crear archivos aquí",
"Drag and drop files here to upload" : "Arrastre y suelte archivos aquí para subirlos",
@@ -109,6 +110,7 @@ OC.L10N.register(
"Last 30 days" : "Últimos 30 días",
"This year ({year})" : "Este año ({year})",
"Last year ({year})" : "El año pasado ({year})",
"Custom range" : "Rango personalizado",
"Custom date range" : "Rango de fechas personalizado",
"Search everywhere" : "Buscar en todas partes",
"Documents" : "Documentos",
@@ -120,6 +122,7 @@ OC.L10N.register(
"Images" : "Imágenes",
"Videos" : "Vídeos",
"Filters" : "Filtros",
"Back to filters" : "Volver a filtros",
"Appearance" : "Apariencia",
"Show hidden files" : "Mostrar archivos ocultos",
"Show file type column" : "Mostrar la columna de tipo de archivo",
@@ -230,6 +233,9 @@ OC.L10N.register(
"Removing the file extension \"{old}\" may render the file unreadable." : "Quitar la extensión \"{old}\" podría hacer a el archivo ilegible.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Añadir la extensión \"{new}\" podría hacer a el archivo ilegible.",
"Do not show this dialog again." : "No mostrar este diálogo de nuevo.",
"Rename file to hidden" : "Cambiar el nombre del archivo para ocultarlo",
"Prefixing a filename with a dot may render the file hidden." : "Si antepones un punto al nombre de un archivo, éste podría quedar oculto.",
"Are you sure you want to rename the file to \"{filename}\"?" : "¿Estás seguro que quieres cambiar el nombre del archivo a \"{filename}\"?",
"Cancel" : "Cancelar",
"Rename" : "Renombrar",
"Select file or folder to link to" : "Selecciona archivo o carpeta a enlazar",
@@ -244,6 +250,7 @@ OC.L10N.register(
"Error during upload: {message}" : "Error durante la subida: {message}",
"Error during upload, status code {status}" : "Error durante la subida, código de estado {status}",
"Unknown error during upload" : "Error desconocido durante la subida",
"File list is reloading" : "La lista de archivos se está recargando",
"Loading current folder" : "Cargando carpeta actual",
"Retry" : "Reintentar",
"No files in here" : "Aquí no hay archivos",
@@ -312,7 +319,9 @@ OC.L10N.register(
"The files are locked" : "Los archivos están bloqueados",
"The file does not exist anymore" : "El archivo ya no existe",
"Moving \"{source}\" to \"{destination}\" …" : "Moviendo \"{source}\" a \"{destination}\" …",
"Moving {count} files to \"{destination}\" …" : "Moviendo{count} archivos a \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copiando \"{source}\" a \"{destination}\" …",
"Copying {count} files to \"{destination}\" …" : "Copiando {count} archivos a \"{destination}\" …",
"Choose destination" : "Elegir destino",
"Copy to {target}" : "Copiar a {target}",
"Move to {target}" : "Mover a {target}",
+9
View File
@@ -77,6 +77,7 @@
"Go to the \"{dir}\" directory" : "Ir a la carpeta \"{dir}\"",
"Current directory path" : "Dirección de la carpeta actual",
"Share" : "Compartir",
"Reload content" : "Recargar contenido",
"Your have used your space quota and cannot upload files anymore" : "Ha utilizado su cuota de espacio y ya no puede subir más archivos",
"You do not have permission to upload or create files here." : "No tiene permiso para subir o crear archivos aquí",
"Drag and drop files here to upload" : "Arrastre y suelte archivos aquí para subirlos",
@@ -107,6 +108,7 @@
"Last 30 days" : "Últimos 30 días",
"This year ({year})" : "Este año ({year})",
"Last year ({year})" : "El año pasado ({year})",
"Custom range" : "Rango personalizado",
"Custom date range" : "Rango de fechas personalizado",
"Search everywhere" : "Buscar en todas partes",
"Documents" : "Documentos",
@@ -118,6 +120,7 @@
"Images" : "Imágenes",
"Videos" : "Vídeos",
"Filters" : "Filtros",
"Back to filters" : "Volver a filtros",
"Appearance" : "Apariencia",
"Show hidden files" : "Mostrar archivos ocultos",
"Show file type column" : "Mostrar la columna de tipo de archivo",
@@ -228,6 +231,9 @@
"Removing the file extension \"{old}\" may render the file unreadable." : "Quitar la extensión \"{old}\" podría hacer a el archivo ilegible.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Añadir la extensión \"{new}\" podría hacer a el archivo ilegible.",
"Do not show this dialog again." : "No mostrar este diálogo de nuevo.",
"Rename file to hidden" : "Cambiar el nombre del archivo para ocultarlo",
"Prefixing a filename with a dot may render the file hidden." : "Si antepones un punto al nombre de un archivo, éste podría quedar oculto.",
"Are you sure you want to rename the file to \"{filename}\"?" : "¿Estás seguro que quieres cambiar el nombre del archivo a \"{filename}\"?",
"Cancel" : "Cancelar",
"Rename" : "Renombrar",
"Select file or folder to link to" : "Selecciona archivo o carpeta a enlazar",
@@ -242,6 +248,7 @@
"Error during upload: {message}" : "Error durante la subida: {message}",
"Error during upload, status code {status}" : "Error durante la subida, código de estado {status}",
"Unknown error during upload" : "Error desconocido durante la subida",
"File list is reloading" : "La lista de archivos se está recargando",
"Loading current folder" : "Cargando carpeta actual",
"Retry" : "Reintentar",
"No files in here" : "Aquí no hay archivos",
@@ -310,7 +317,9 @@
"The files are locked" : "Los archivos están bloqueados",
"The file does not exist anymore" : "El archivo ya no existe",
"Moving \"{source}\" to \"{destination}\" …" : "Moviendo \"{source}\" a \"{destination}\" …",
"Moving {count} files to \"{destination}\" …" : "Moviendo{count} archivos a \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copiando \"{source}\" a \"{destination}\" …",
"Copying {count} files to \"{destination}\" …" : "Copiando {count} archivos a \"{destination}\" …",
"Choose destination" : "Elegir destino",
"Copy to {target}" : "Copiar a {target}",
"Move to {target}" : "Mover a {target}",
+5
View File
@@ -233,6 +233,9 @@ OC.L10N.register(
"Removing the file extension \"{old}\" may render the file unreadable." : "Retirer l'extension de fichier \"{old}\" peut rendre le fichier illisible.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Ajouter l'extension de fichier \"{new}\" peut rendre le fichier illisible.",
"Do not show this dialog again." : "Ne plus montrer cette boite de dialogue.",
"Rename file to hidden" : "Renommer le fichier en caché",
"Prefixing a filename with a dot may render the file hidden." : "Le fait de préfixer un nom de fichier avec un point peut rendre le fichier caché.",
"Are you sure you want to rename the file to \"{filename}\"?" : "Voulez-vous vraiment renommer le fichier en \"{filename}\" ?",
"Cancel" : "Annuler",
"Rename" : "Renommer",
"Select file or folder to link to" : "Sélection d'un fichier ou d'un dossier à lier",
@@ -316,7 +319,9 @@ OC.L10N.register(
"The files are locked" : "Les fichiers sont verrouillés",
"The file does not exist anymore" : "Le fichier n'existe plus",
"Moving \"{source}\" to \"{destination}\" …" : "Déplacement de \"{source}\" vers \"{destination}\" …",
"Moving {count} files to \"{destination}\" …" : "Déplacement de {count} fichiers vers \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copie de \"{source}\" vers \"{destination}\" …",
"Copying {count} files to \"{destination}\" …" : "Copie de {count} fichiers vers \"{destination}\" …",
"Choose destination" : "Choisir la destination",
"Copy to {target}" : "Copier vers {target}",
"Move to {target}" : "Déplacer vers {target}",
+5
View File
@@ -231,6 +231,9 @@
"Removing the file extension \"{old}\" may render the file unreadable." : "Retirer l'extension de fichier \"{old}\" peut rendre le fichier illisible.",
"Adding the file extension \"{new}\" may render the file unreadable." : "Ajouter l'extension de fichier \"{new}\" peut rendre le fichier illisible.",
"Do not show this dialog again." : "Ne plus montrer cette boite de dialogue.",
"Rename file to hidden" : "Renommer le fichier en caché",
"Prefixing a filename with a dot may render the file hidden." : "Le fait de préfixer un nom de fichier avec un point peut rendre le fichier caché.",
"Are you sure you want to rename the file to \"{filename}\"?" : "Voulez-vous vraiment renommer le fichier en \"{filename}\" ?",
"Cancel" : "Annuler",
"Rename" : "Renommer",
"Select file or folder to link to" : "Sélection d'un fichier ou d'un dossier à lier",
@@ -314,7 +317,9 @@
"The files are locked" : "Les fichiers sont verrouillés",
"The file does not exist anymore" : "Le fichier n'existe plus",
"Moving \"{source}\" to \"{destination}\" …" : "Déplacement de \"{source}\" vers \"{destination}\" …",
"Moving {count} files to \"{destination}\" …" : "Déplacement de {count} fichiers vers \"{destination}\" …",
"Copying \"{source}\" to \"{destination}\" …" : "Copie de \"{source}\" vers \"{destination}\" …",
"Copying {count} files to \"{destination}\" …" : "Copie de {count} fichiers vers \"{destination}\" …",
"Choose destination" : "Choisir la destination",
"Copy to {target}" : "Copier vers {target}",
"Move to {target}" : "Déplacer vers {target}",
+5
View File
@@ -233,6 +233,9 @@ OC.L10N.register(
"Removing the file extension \"{old}\" may render the file unreadable." : "移除檔案副檔名「{old}」可能會導致檔案無法讀取。",
"Adding the file extension \"{new}\" may render the file unreadable." : "加入檔案副檔名「{new}」可能會導致檔案無法讀取。",
"Do not show this dialog again." : "不要再次顯示此對話方塊。",
"Rename file to hidden" : "將檔案重新命名為隱藏檔案",
"Prefixing a filename with a dot may render the file hidden." : "在檔案名稱前加點號可能會導致檔案隱藏。",
"Are you sure you want to rename the file to \"{filename}\"?" : "您確定您想要重新命名檔案為「{filename}」嗎?",
"Cancel" : "取消",
"Rename" : "重新命名",
"Select file or folder to link to" : "選取要連結的檔案或資料夾",
@@ -316,7 +319,9 @@ OC.L10N.register(
"The files are locked" : "檔案已鎖定",
"The file does not exist anymore" : "檔案已不存在",
"Moving \"{source}\" to \"{destination}\" …" : "正在移動「{source}」至「{destination}」 ……",
"Moving {count} files to \"{destination}\" …" : "正在將 {count} 個檔案移動到「{destination}」……",
"Copying \"{source}\" to \"{destination}\" …" : "正在複製「{source}」至「{destination}」 ……",
"Copying {count} files to \"{destination}\" …" : "正在將 {count} 個檔案複製到「{destination}」……",
"Choose destination" : "選擇目的地",
"Copy to {target}" : "複製到 {target}",
"Move to {target}" : "移動到 {target}",
+5
View File
@@ -231,6 +231,9 @@
"Removing the file extension \"{old}\" may render the file unreadable." : "移除檔案副檔名「{old}」可能會導致檔案無法讀取。",
"Adding the file extension \"{new}\" may render the file unreadable." : "加入檔案副檔名「{new}」可能會導致檔案無法讀取。",
"Do not show this dialog again." : "不要再次顯示此對話方塊。",
"Rename file to hidden" : "將檔案重新命名為隱藏檔案",
"Prefixing a filename with a dot may render the file hidden." : "在檔案名稱前加點號可能會導致檔案隱藏。",
"Are you sure you want to rename the file to \"{filename}\"?" : "您確定您想要重新命名檔案為「{filename}」嗎?",
"Cancel" : "取消",
"Rename" : "重新命名",
"Select file or folder to link to" : "選取要連結的檔案或資料夾",
@@ -314,7 +317,9 @@
"The files are locked" : "檔案已鎖定",
"The file does not exist anymore" : "檔案已不存在",
"Moving \"{source}\" to \"{destination}\" …" : "正在移動「{source}」至「{destination}」 ……",
"Moving {count} files to \"{destination}\" …" : "正在將 {count} 個檔案移動到「{destination}」……",
"Copying \"{source}\" to \"{destination}\" …" : "正在複製「{source}」至「{destination}」 ……",
"Copying {count} files to \"{destination}\" …" : "正在將 {count} 個檔案複製到「{destination}」……",
"Choose destination" : "選擇目的地",
"Copy to {target}" : "複製到 {target}",
"Move to {target}" : "移動到 {target}",
+4
View File
@@ -115,6 +115,7 @@ import FileEntryActions from './FileEntry/FileEntryActions.vue'
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
import FileEntryName from './FileEntry/FileEntryName.vue'
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
import { useFileActions } from '../composables/useFileActions.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
@@ -170,7 +171,10 @@ export default defineComponent({
activeView,
} = useActiveStore()
const actions = useFileActions()
return {
actions,
actionsMenuStore,
activeFolder,
activeNode,
@@ -77,6 +77,7 @@ import FileEntryActions from './FileEntry/FileEntryActions.vue'
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
import FileEntryName from './FileEntry/FileEntryName.vue'
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
import { useFileActions } from '../composables/useFileActions.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
@@ -122,7 +123,10 @@ export default defineComponent({
activeView,
} = useActiveStore()
const actions = useFileActions()
return {
actions,
actionsMenuStore,
activeFolder,
activeNode,
+2 -4
View File
@@ -8,7 +8,7 @@ import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
import { showError } from '@nextcloud/dialogs'
import { FileType, Folder, getFileActions, File as NcFile, Node, NodeStatus, Permission } from '@nextcloud/files'
import { FileType, Folder, File as NcFile, Node, NodeStatus, Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { isPublicShare } from '@nextcloud/sharing/public'
@@ -24,8 +24,6 @@ import { isDownloadable } from '../utils/permissions.ts'
Vue.directive('onClickOutside', vOnClickOutside)
const actions = getFileActions()
export default defineComponent({
props: {
source: {
@@ -233,7 +231,7 @@ export default defineComponent({
return []
}
return actions
return this.actions
.filter((action: IFileAction) => {
if (!action.enabled) {
return true
@@ -6,20 +6,22 @@
<script setup lang="ts">
import type { IHotkeyConfig } from '@nextcloud/files'
import { getFileActions } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { computed } from 'vue'
import NcAppSettingsShortcutsSection from '@nextcloud/vue/components/NcAppSettingsShortcutsSection'
import NcHotkey from '@nextcloud/vue/components/NcHotkey'
import NcHotkeyList from '@nextcloud/vue/components/NcHotkeyList'
import { useFileActions } from '../../composables/useFileActions.ts'
const actionHotkeys = getFileActions()
const actions = useFileActions()
const actionHotkeys = computed(() => actions.value
.filter((action) => !!action.hotkey)
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map((action) => ({
id: action.id,
label: action.hotkey!.description,
hotkey: hotkeyToString(action.hotkey!),
}))
})))
/**
* Convert a hotkey configuration to a hotkey string.
@@ -9,7 +9,7 @@
</template>
<script lang="ts">
import type { Folder, Header, View } from '@nextcloud/files'
import type { Folder, IFileListHeader, View } from '@nextcloud/files'
import type { PropType } from 'vue'
import PQueue from 'p-queue'
@@ -25,7 +25,7 @@ export default {
name: 'FilesListHeader',
props: {
header: {
type: Object as PropType<Header>,
type: Object as PropType<IFileListHeader>,
required: true,
},
@@ -12,7 +12,7 @@
:disabled="!!loading || areSomeNodesLoading"
:force-name="true"
:inline="enabledInlineActions.length"
:menu-name="enabledInlineActions.length <= 1 ? t('files', 'Actions') : null"
:menu-name="enabledInlineActions.length <= 1 ? t('files', 'Actions') : undefined"
@close="openedSubmenu = null">
<!-- Default actions list-->
<NcActionButton
@@ -75,7 +75,7 @@ import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { DefaultType, getFileActions, NodeStatus } from '@nextcloud/files'
import { DefaultType, NodeStatus } from '@nextcloud/files'
import { translate } from '@nextcloud/l10n'
import { computed, defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
@@ -84,6 +84,7 @@ import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
import { useFileActions } from '../composables/useFileActions.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import logger from '../logger.ts'
import actionsMixins from '../mixins/actionsMixin.ts'
@@ -92,9 +93,6 @@ import { useActiveStore } from '../store/active.ts'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
// The registered actions list
const actions = getFileActions()
export default defineComponent({
name: 'FilesListTableHeaderActions',
@@ -128,7 +126,7 @@ export default defineComponent({
const selectionStore = useSelectionStore()
const { isMedium, isNarrow } = useFileListWidth()
const boundariesElement = document.getElementById('app-content-vue')
const boundariesElement = document.getElementById('app-content-vue') as HTMLElement
const inlineActions = computed(() => {
if (isNarrow.value) {
@@ -140,7 +138,10 @@ export default defineComponent({
return 3
})
const actions = useFileActions()
return {
actions,
actionsMenuStore,
activeFolder,
filesStore,
@@ -159,7 +160,7 @@ export default defineComponent({
computed: {
enabledFileActions(): IFileAction[] {
return actions
return this.actions
// We don't handle renderInline actions in this component
.filter((action) => !action.renderInline)
// We don't handle actions that are not visible
+9 -16
View File
@@ -74,7 +74,7 @@ import type { ComponentPublicInstance, PropType } from 'vue'
import type { UserConfig } from '../types.ts'
import { showError } from '@nextcloud/dialogs'
import { FileType, Folder, getFileActions, getSidebar, Permission, View } from '@nextcloud/files'
import { FileType, Folder, getSidebar, Permission, View } from '@nextcloud/files'
import { n, t } from '@nextcloud/l10n'
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { computed, defineComponent } from 'vue'
@@ -87,6 +87,7 @@ import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
import VirtualList from './VirtualList.vue'
import { useEnabledFileActions } from '../composables/useFileActions.ts'
import { useFileListHeaders } from '../composables/useFileListHeaders.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
@@ -362,21 +363,13 @@ export default defineComponent({
}
if (node.type === FileType.File) {
const defaultAction = getFileActions()
// Get only default actions (visible and hidden)
.filter((action) => !!action?.default)
// Find actions that are either always enabled or enabled for the current node
.filter((action) => (!action.enabled || action.enabled({
nodes: [node],
view: this.currentView,
folder: this.currentFolder,
contents: this.nodes,
})))
.filter((action) => action.id !== 'download')
// Sort enabled default actions by order
.sort((a, b) => (a.order || 0) - (b.order || 0))
// Get the first one
.at(0)
const actions = useEnabledFileActions({
nodes: [node],
view: this.currentView,
folder: this.currentFolder,
contents: this.nodes,
})
const defaultAction = actions.value.find((action) => action.id !== 'download' && !!action.default)
// Some file types do not have a default action (e.g. they can only be downloaded)
// So if there is an enabled default action, so execute it
@@ -0,0 +1,124 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction, INode, registerFileAction } from '@nextcloud/files'
import type * as composable from './useFileActions.ts'
import { Folder, View } from '@nextcloud/files'
import { defaultRemoteURL, defaultRootPath } from '@nextcloud/files/dav'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
interface Context {
useFileActions: typeof composable.useFileActions
useEnabledFileActions: typeof composable.useEnabledFileActions
registerFileAction: typeof registerFileAction
}
describe('useFileActions', () => {
beforeEach(async (context: Context) => {
delete globalThis._nc_files_scope
// reset modules to reset internal variables (the headers ref) of the composable and the library (the scoped globals)
vi.resetModules()
context.useFileActions = (await import('./useFileActions.ts')).useFileActions
context.useEnabledFileActions = (await import('./useFileActions.ts')).useEnabledFileActions
context.registerFileAction = (await import('@nextcloud/files')).registerFileAction
})
it<Context>('gets the actions', ({ useFileActions, registerFileAction }) => {
const action: IFileAction = { id: '1', order: 5, displayName: () => 'Action', iconSvgInline: vi.fn(), exec: vi.fn() }
registerFileAction(action)
const actions = useFileActions()
expect(actions.value).toEqual([action])
})
it<Context>('composable is reactive', async ({ useFileActions, registerFileAction }) => {
const action: IFileAction = { id: '1', order: 5, displayName: () => 'Action', iconSvgInline: vi.fn(), exec: vi.fn() }
registerFileAction(action)
await nextTick()
const actions = useFileActions()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1'])
// now add a new action
const action2: IFileAction = { id: '2', order: 9, displayName: () => 'Action', iconSvgInline: vi.fn(), exec: vi.fn() }
registerFileAction(action2)
// reactive update, lower order first
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '2'])
})
})
describe('useEnabledFileActions', () => {
beforeEach(async (context: Context) => {
delete globalThis._nc_files_scope
// reset modules to reset internal variables (the headers ref) of the composable and the library (the scoped globals)
vi.resetModules()
context.useFileActions = (await import('./useFileActions.ts')).useFileActions
context.useEnabledFileActions = (await import('./useFileActions.ts')).useEnabledFileActions
context.registerFileAction = (await import('@nextcloud/files')).registerFileAction
})
it<Context>('gets the actions', ({ useEnabledFileActions, registerFileAction }) => {
registerFileAction({ id: '1', order: 0, displayName: () => 'Action 1', iconSvgInline: vi.fn(), exec: vi.fn() })
registerFileAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: () => false, iconSvgInline: vi.fn(), exec: vi.fn() })
registerFileAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: () => true, iconSvgInline: vi.fn(), exec: vi.fn() })
const folder = new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
const view = new View({ id: 'view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
const contents = []
const actions = useEnabledFileActions({ folder, contents, view })
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
it<Context>('composable is reactive', async ({ useEnabledFileActions, registerFileAction }) => {
registerFileAction({ id: '1', order: 0, displayName: () => 'Action 1', iconSvgInline: vi.fn(), exec: vi.fn() })
registerFileAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: () => false, iconSvgInline: vi.fn(), exec: vi.fn() })
const folder = new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
const view = new View({ id: 'view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
const contents = []
const actions = useEnabledFileActions({ folder, contents, view })
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1'])
registerFileAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: () => true, iconSvgInline: vi.fn(), exec: vi.fn() })
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
it<Context>('composable is reactive to context changes', async ({ useEnabledFileActions, registerFileAction }) => {
// only enabled if view id === 'enabled-view'
registerFileAction({ id: '1', order: 0, displayName: () => 'Action 1', enabled: ({ view }) => view.id === 'enabled-view', iconSvgInline: vi.fn(), exec: vi.fn() })
// only enabled if contents has items
registerFileAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: ({ contents }) => contents.length > 0, iconSvgInline: vi.fn(), exec: vi.fn() })
// only enabled if folder owner is 'owner2'
registerFileAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: ({ folder }) => folder.owner === 'owner2', iconSvgInline: vi.fn(), exec: vi.fn() })
const context = ref({
folder: new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath }),
view: new View({ id: 'disabled-view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' }),
contents: ref<INode[]>([(new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath }))]),
})
const actions = useEnabledFileActions(context)
// we have contents but wrong folder and view so only 2 is enabled
expect(actions.value.map(({ id }) => id)).toStrictEqual(['2'])
// no contents so nothing is enabled
context.value.contents = []
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual([])
// correct owner for action 3
context.value.folder = new Folder({ owner: 'owner2', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['3'])
// correct view for action 1
context.value.view = new View({ id: 'enabled-view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
})
@@ -0,0 +1,42 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ActionContext, IFileAction } from '@nextcloud/files'
import type { MaybeRefOrGetter } from '@vueuse/core'
import type { Ref } from 'vue'
import { getFileActions, getFilesRegistry } from '@nextcloud/files'
import { toValue } from '@vueuse/core'
import { computed, readonly, ref } from 'vue'
const actions = ref<IFileAction[] | undefined>()
/**
* Get the registered and sorted file actions.
*/
export function useFileActions() {
if (!actions.value) {
// if not initialized by other component yet, initialize and subscribe to registry changes
actions.value = getFileActions()
getFilesRegistry().addEventListener('register:action', () => {
actions.value = getFileActions()
})
}
return readonly(actions as Ref<IFileAction[]>)
}
/**
* Get the enabled file actions for the given context.
*
* @param context - The context to check the enabled state of the actions against
*/
export function useEnabledFileActions(context: MaybeRefOrGetter<ActionContext>) {
const actions = useFileActions()
return computed(() => actions.value
.filter((action) => action.enabled === undefined
|| action.enabled(toValue(context)!))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)))
}
@@ -0,0 +1,133 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileListAction, INode, registerFileListAction } from '@nextcloud/files'
import type * as composable from './useFileListActions.ts'
import { Folder, View } from '@nextcloud/files'
import { defaultRemoteURL, defaultRootPath } from '@nextcloud/files/dav'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref, shallowRef } from 'vue'
interface Context {
useFileListActions: typeof composable.useFileListActions
useEnabledFileListActions: typeof composable.useEnabledFileListActions
registerFileListAction: typeof registerFileListAction
}
describe('useFileListActions', () => {
beforeEach(async (context: Context) => {
delete globalThis._nc_files_scope
// reset modules to reset internal variables (the headers ref) of the composable and the library (the scoped globals)
vi.resetModules()
context.useFileListActions = (await import('./useFileListActions.ts')).useFileListActions
context.useEnabledFileListActions = (await import('./useFileListActions.ts')).useEnabledFileListActions
context.registerFileListAction = (await import('@nextcloud/files')).registerFileListAction
})
it<Context>('gets the actions', ({ useFileListActions, registerFileListAction }) => {
const action: IFileListAction = { id: '1', order: 5, displayName: () => 'Action', exec: vi.fn() }
registerFileListAction(action)
const actions = useFileListActions()
expect(actions.value).toEqual([action])
})
it<Context>('actions are sorted', ({ useFileListActions, registerFileListAction }) => {
const action: IFileListAction = { id: '1', order: 5, displayName: () => 'Action 1', exec: vi.fn() }
const action2: IFileListAction = { id: '2', order: 0, displayName: () => 'Action 2', exec: vi.fn() }
registerFileListAction(action)
registerFileListAction(action2)
const actions = useFileListActions()
// lower order first
expect(actions.value.map(({ id }) => id)).toStrictEqual(['2', '1'])
})
it<Context>('composable is reactive', async ({ useFileListActions, registerFileListAction }) => {
const action: IFileListAction = { id: '1', order: 5, displayName: () => 'Action', exec: vi.fn() }
registerFileListAction(action)
await nextTick()
const actions = useFileListActions()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1'])
// now add a new action
const action2: IFileListAction = { id: '2', order: 0, displayName: () => 'Action', exec: vi.fn() }
registerFileListAction(action2)
// reactive update, lower order first
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['2', '1'])
})
})
describe('useEnabledFileListActions', () => {
beforeEach(async (context: Context) => {
delete globalThis._nc_files_scope
// reset modules to reset internal variables (the headers ref) of the composable and the library (the scoped globals)
vi.resetModules()
context.useFileListActions = (await import('./useFileListActions.ts')).useFileListActions
context.useEnabledFileListActions = (await import('./useFileListActions.ts')).useEnabledFileListActions
context.registerFileListAction = (await import('@nextcloud/files')).registerFileListAction
})
it<Context>('gets the actions sorted', ({ useEnabledFileListActions, registerFileListAction }) => {
registerFileListAction({ id: '1', order: 0, displayName: () => 'Action 1', exec: vi.fn() })
registerFileListAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: () => false, exec: vi.fn() })
registerFileListAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: () => true, exec: vi.fn() })
const folder = new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
const view = new View({ id: 'view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
const contents = []
const actions = useEnabledFileListActions(folder, contents, view)
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
it<Context>('composable is reactive', async ({ useEnabledFileListActions, registerFileListAction }) => {
registerFileListAction({ id: '1', order: 0, displayName: () => 'Action 1', exec: vi.fn() })
registerFileListAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: () => false, exec: vi.fn() })
const folder = new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
const view = new View({ id: 'view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
const contents = []
const actions = useEnabledFileListActions(folder, contents, view)
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1'])
registerFileListAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: () => true, exec: vi.fn() })
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
it<Context>('composable is reactive to context changes', async ({ useEnabledFileListActions, registerFileListAction }) => {
// only enabled if view id === 'enabled-view'
registerFileListAction({ id: '1', order: 0, displayName: () => 'Action 1', enabled: ({ view }) => view.id === 'enabled-view', exec: vi.fn() })
// only enabled if contents has items
registerFileListAction({ id: '2', order: 5, displayName: () => 'Action 2', enabled: ({ contents }) => contents.length > 0, exec: vi.fn() })
// only enabled if folder owner is 'owner2'
registerFileListAction({ id: '3', order: 9, displayName: () => 'Action 3', enabled: ({ folder }) => folder.owner === 'owner2', exec: vi.fn() })
const folder = shallowRef(new Folder({ owner: 'owner', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath }))
const view = shallowRef(new View({ id: 'disabled-view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' }))
const contents = ref<INode[]>([folder.value])
const actions = useEnabledFileListActions(folder, contents, view)
// we have contents but wrong folder and view so only 2 is enabled
expect(actions.value.map(({ id }) => id)).toStrictEqual(['2'])
// no contents so nothing is enabled
contents.value = []
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual([])
// correct owner for action 3
folder.value = new Folder({ owner: 'owner2', root: defaultRootPath, source: defaultRemoteURL + defaultRootPath })
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['3'])
// correct view for action 1
view.value = new View({ id: 'enabled-view', getContents: vi.fn(), icon: '<svg></svg>', name: 'View' })
await nextTick()
expect(actions.value.map(({ id }) => id)).toStrictEqual(['1', '3'])
})
})
@@ -0,0 +1,53 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileListAction, IFolder, INode, IView } from '@nextcloud/files'
import type { MaybeRefOrGetter } from '@vueuse/core'
import type { ComputedRef } from 'vue'
import { getFileListActions, getFilesRegistry } from '@nextcloud/files'
import { toValue } from '@vueuse/core'
import { computed, ref } from 'vue'
const actions = ref<IFileListAction[]>()
const sorted = computed(() => [...(actions.value ?? [])].sort((a, b) => a.order - b.order))
/**
* Get the registered and sorted file list actions.
*/
export function useFileListActions(): ComputedRef<IFileListAction[]> {
if (!actions.value) {
// if not initialized by other component yet, initialize and subscribe to registry changes
actions.value = getFileListActions()
getFilesRegistry().addEventListener('register:listAction', () => {
actions.value = getFileListActions()
})
}
return sorted
}
/**
* Get the enabled file list actions for the given folder, contents and view.
*
* @param folder - The current folder
* @param contents - The contents of the current folder
* @param view - The current view
*/
export function useEnabledFileListActions(
folder: MaybeRefOrGetter<IFolder | undefined>,
contents: MaybeRefOrGetter<INode[]>,
view: MaybeRefOrGetter<IView | undefined>,
) {
const actions = useFileListActions()
return computed(() => {
if (toValue(folder) === undefined || toValue(view) === undefined) {
return []
}
return actions.value.filter((action) => action.enabled === undefined
|| action.enabled({ folder: toValue(folder)!, contents: toValue(contents), view: toValue(view)! }))
})
}
@@ -3,39 +3,59 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { Header } from '@nextcloud/files'
import type { IFileListHeader } from '@nextcloud/files'
import type { registerFileListHeader } from '@nextcloud/files'
import type { ComputedRef } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useFileListHeaders } from './useFileListHeaders.ts'
import { nextTick } from 'vue'
const getFileListHeaders = vi.hoisted(() => vi.fn())
vi.mock('@nextcloud/files', async (originalModule) => {
return {
...(await originalModule()),
getFileListHeaders,
}
})
interface Context {
useFileListHeaders: () => ComputedRef<IFileListHeader[]>
registerFileListHeader: typeof registerFileListHeader
}
describe('useFileListHeaders', () => {
beforeEach(() => vi.resetAllMocks())
beforeEach(async (context: Context) => {
delete globalThis._nc_files_scope
// reset modules to reset internal variables (the headers ref) of the composable and the library (the scoped globals)
vi.resetModules()
context.useFileListHeaders = (await import('./useFileListHeaders.ts')).useFileListHeaders
context.registerFileListHeader = (await import('@nextcloud/files')).registerFileListHeader
})
it('gets the headers', () => {
const header = new Header({ id: '1', order: 5, render: vi.fn(), updated: vi.fn() })
getFileListHeaders.mockImplementationOnce(() => [header])
it<Context>('gets the headers', ({ useFileListHeaders, registerFileListHeader }) => {
const header: IFileListHeader = { id: '1', order: 5, render: vi.fn(), updated: vi.fn() }
registerFileListHeader(header)
const headers = useFileListHeaders()
expect(headers.value).toEqual([header])
expect(getFileListHeaders).toHaveBeenCalledOnce()
})
it('headers are sorted', () => {
const header = new Header({ id: '1', order: 10, render: vi.fn(), updated: vi.fn() })
const header2 = new Header({ id: '2', order: 5, render: vi.fn(), updated: vi.fn() })
getFileListHeaders.mockImplementationOnce(() => [header, header2])
it<Context>('headers are sorted', ({ useFileListHeaders, registerFileListHeader }) => {
const header: IFileListHeader = { id: '1', order: 10, render: vi.fn(), updated: vi.fn() }
const header2: IFileListHeader = { id: '2', order: 5, render: vi.fn(), updated: vi.fn() }
registerFileListHeader(header)
registerFileListHeader(header2)
const headers = useFileListHeaders()
// lower order first
expect(headers.value.map(({ id }) => id)).toStrictEqual(['2', '1'])
expect(getFileListHeaders).toHaveBeenCalledOnce()
})
it<Context>('composable is reactive', async ({ useFileListHeaders, registerFileListHeader }) => {
const header: IFileListHeader = { id: 'a', order: 10, render: vi.fn(), updated: vi.fn() }
registerFileListHeader(header)
await nextTick()
const headers = useFileListHeaders()
expect(headers.value.map(({ id }) => id)).toStrictEqual(['a'])
// now add a new header
const header2: IFileListHeader = { id: 'b', order: 5, render: vi.fn(), updated: vi.fn() }
registerFileListHeader(header2)
// reactive update, lower order first
await nextTick()
expect(headers.value.map(({ id }) => id)).toStrictEqual(['b', 'a'])
})
})
@@ -2,18 +2,27 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Header } from '@nextcloud/files'
import type { IFileListHeader } from '@nextcloud/files'
import type { ComputedRef } from 'vue'
import { getFileListHeaders } from '@nextcloud/files'
import { getFileListHeaders, getFilesRegistry } from '@nextcloud/files'
import { computed, ref } from 'vue'
const headers = ref<IFileListHeader[]>()
const sorted = computed(() => [...(headers.value ?? [])].sort((a, b) => a.order - b.order) as IFileListHeader[])
/**
* Get the registered and sorted file list headers.
*/
export function useFileListHeaders(): ComputedRef<Header[]> {
const headers = ref(getFileListHeaders())
const sorted = computed(() => [...headers.value].sort((a, b) => a.order - b.order) as Header[])
export function useFileListHeaders(): ComputedRef<IFileListHeader[]> {
if (!headers.value) {
// if not initialized by other component yet, initialize and subscribe to registry changes
headers.value = getFileListHeaders()
getFilesRegistry().addEventListener('register:listHeader', () => {
headers.value = getFileListHeaders()
})
}
return sorted
}
+12 -1
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { addNewFileMenuEntry, registerFileAction } from '@nextcloud/files'
import { addNewFileMenuEntry, getNewFileMenu, registerFileAction } from '@nextcloud/files'
import { registerDavProperty } from '@nextcloud/files/dav'
import { isPublicShare } from '@nextcloud/sharing/public'
import { registerConvertActions } from './actions/convertAction.ts'
@@ -79,3 +79,14 @@ registerDavProperty('nc:is-mount-root', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:metadata-blurhash', { nc: 'http://nextcloud.org/ns' })
initLivePhotos()
// TODO: REMOVE THIS ONCE THE UPLOAD LIBRARY IS MIGRATED TO THE NEW FILES LIBRARY
window._nc_newfilemenu = new Proxy(getNewFileMenu(), {
get(target, prop) {
return target[prop as keyof typeof target]
},
set(target, prop, value) {
target[prop as keyof typeof target] = value
return true
},
})
+39 -34
View File
@@ -1,8 +1,9 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { NewMenuEntry, Node } from '@nextcloud/files'
import type { IFolder, INode, NewMenuEntry } from '@nextcloud/files'
import FolderPlusSvg from '@mdi/svg/svg/folder-plus-outline.svg?raw'
import { getCurrentUser } from '@nextcloud/auth'
@@ -10,48 +11,24 @@ import axios from '@nextcloud/axios'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { Folder, Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { t } from '@nextcloud/l10n'
import { basename } from 'path'
import logger from '../logger.ts'
import { newNodeName } from '../utils/newNodeDialog.ts'
type createFolderResponse = {
fileid: number
source: string
}
/**
*
* @param root
* @param name
*/
async function createNewFolder(root: Folder, name: string): Promise<createFolderResponse> {
const source = root.source + '/' + name
const encodedSource = root.encodedSource + '/' + encodeURIComponent(name)
const response = await axios({
method: 'MKCOL',
url: encodedSource,
headers: {
Overwrite: 'F',
},
})
return {
fileid: parseInt(response.headers['oc-fileid']),
source,
}
}
export const entry: NewMenuEntry = {
id: 'newFolder',
order: 0,
displayName: t('files', 'New folder'),
enabled: (context: Folder) => Boolean(context.permissions & Permission.CREATE) && Boolean(context.permissions & Permission.READ),
// Make the svg icon color match the primary element color
iconSvgInline: FolderPlusSvg.replace(/viewBox/gi, 'style="color: var(--color-primary-element)" viewBox'),
order: 0,
async handler(context: Folder, content: Node[]) {
enabled(context) {
return Boolean(context.permissions & Permission.CREATE)
&& Boolean(context.permissions & Permission.READ)
},
async handler(context: IFolder, content: INode[]) {
const name = await newNodeName(t('files', 'New folder'), content)
if (name === null) {
return
@@ -92,3 +69,31 @@ export const entry: NewMenuEntry = {
}
},
}
type createFolderResponse = {
fileid: number
source: string
}
/**
* Create a new folder in the given root with the given name
*
* @param root - The folder in which the new folder should be created
* @param name - The name of the new folder
*/
async function createNewFolder(root: IFolder, name: string): Promise<createFolderResponse> {
const source = root.source + '/' + name
const encodedSource = root.encodedSource + '/' + encodeURIComponent(name)
const response = await axios({
method: 'MKCOL',
url: encodedSource,
headers: {
Overwrite: 'F',
},
})
return {
fileid: parseInt(response.headers['oc-fileid']),
source,
}
}
+32 -8
View File
@@ -1,15 +1,30 @@
/**
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction, IFolder, INode, IView } from '@nextcloud/files'
import { getCurrentUser } from '@nextcloud/auth'
import { subscribe } from '@nextcloud/event-bus'
import { getNavigation } from '@nextcloud/files'
import { Folder, getNavigation, Permission } from '@nextcloud/files'
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
import { defineStore } from 'pinia'
import { ref, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
import { useFilesStore } from './files.ts'
// Temporary fake folder to use until we have the first valid folder
// fetched and cached. This allow us to mount the FilesListVirtual
// at all time and avoid unmount/mount and undesired rendering issues.
const dummyFolder = new Folder({
id: 0,
source: getRemoteURL() + getRootPath(),
root: getRootPath(),
owner: getCurrentUser()?.uid || null,
permissions: Permission.NONE,
})
export const useActiveStore = defineStore('active', () => {
/**
@@ -17,11 +32,6 @@ export const useActiveStore = defineStore('active', () => {
*/
const activeAction = shallowRef<IFileAction>()
/**
* The currently active folder
*/
const activeFolder = ref<IFolder>()
/**
* The current active node within the folder
*/
@@ -32,6 +42,20 @@ export const useActiveStore = defineStore('active', () => {
*/
const activeView = shallowRef<IView>()
const filesStore = useFilesStore()
const { directory } = useRouteParameters()
/**
* The currently active folder
*/
const activeFolder = computed<IFolder>(() => {
if (!activeView.value?.id) {
return dummyFolder
}
return filesStore.getDirectoryByPath(activeView.value.id, directory.value)
?? dummyFolder
})
// Set the active node on the router params
watch(activeNode, () => {
if (typeof activeNode.value?.fileid !== 'number' || activeNode.value.fileid === activeFolder.value?.fileid) {
+42 -27
View File
@@ -6,7 +6,7 @@
import type { FilterUpdateChipsEvent, IFileListFilter, IFileListFilterChip, IFileListFilterWithUi } from '@nextcloud/files'
import { emit, subscribe } from '@nextcloud/event-bus'
import { getFileListFilters } from '@nextcloud/files'
import { getFileListFilters, getFilesRegistry } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import logger from '../logger.ts'
@@ -63,8 +63,8 @@ export const useFiltersStore = defineStore('filters', () => {
const index = filters.value.findIndex(({ id }) => id === filterId)
if (index > -1) {
const [filter] = filters.value.splice(index, 1)
filter.removeEventListener('update:chips', onFilterUpdateChips)
filter.removeEventListener('update:filter', onFilterUpdate)
filter!.removeEventListener('update:chips', onFilterUpdateChips)
filter!.removeEventListener('update:filter', onFilterUpdate)
logger.debug('Files list filter unregistered', { id: filterId })
}
}
@@ -92,27 +92,7 @@ export const useFiltersStore = defineStore('filters', () => {
logger.debug('File list filter chips updated', { filter: id, chips: event.detail })
}
/**
* Event handler that resets all filters if the file list view was changed.
*
*/
function onViewChanged() {
logger.debug('Reset all file list filters - view changed')
for (const filter of filters.value) {
if (filter.reset !== undefined) {
filter.reset()
}
}
}
// Initialize the store
subscribe('files:navigation:changed', onViewChanged)
subscribe('files:filter:added', addFilter)
subscribe('files:filter:removed', removeFilter)
for (const filter of getFileListFilters()) {
addFilter(filter)
}
initialize()
return {
// state
@@ -123,9 +103,44 @@ export const useFiltersStore = defineStore('filters', () => {
// getters / computed
activeChips,
sortedFilters,
}
// actions / methods
addFilter,
removeFilter,
/**
* Initialize the store by registering event listeners and loading initial filters.
*
* @internal
*/
function initialize() {
const registry = getFilesRegistry()
const initialFilters = getFileListFilters()
// handle adding and removing filters after initialization
registry.addEventListener('register:listFilter', (event) => {
addFilter(event.detail)
})
registry.addEventListener('unregister:listFilter', (event) => {
removeFilter(event.detail)
})
// register the initial filters
for (const filter of initialFilters) {
addFilter(filter)
}
// subscribe to file list view changes to reset the filters
subscribe('files:navigation:changed', onViewChanged)
}
/**
* Event handler that resets all filters if the file list view was changed.
*
* @internal
*/
function onViewChanged() {
logger.debug('Reset all file list filters - view changed')
for (const filter of filters.value) {
if (filter.reset !== undefined) {
filter.reset()
}
}
}
})
+4
View File
@@ -37,6 +37,10 @@ export function humanizeWebDAVError(error: unknown) {
return t('files', 'Storage is temporarily not available')
}
}
// We don't need to show abortion error to the user as those are expected.
if (error.name === 'AbortError') {
return null
}
return t('files', 'Unexpected error: {error}', { error: error.message })
}
+3 -3
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import NewNodeDialog from '../components/NewNodeDialog.vue'
@@ -27,8 +27,8 @@ interface ILabels {
* @param labels Labels to set on the dialog
* @return string if successful otherwise null if aborted
*/
export function newNodeName(defaultName: string, folderContent: Node[], labels: ILabels = {}) {
const contentNames = folderContent.map((node: Node) => node.basename)
export function newNodeName(defaultName: string, folderContent: INode[], labels: ILabels = {}) {
const contentNames = folderContent.map((node: INode) => node.basename)
return new Promise<string | null>((resolve) => {
spawnDialog(NewNodeDialog, {
+18 -57
View File
@@ -160,11 +160,9 @@ import type { ComponentPublicInstance } from 'vue'
import type { Route } from 'vue-router'
import type { UserConfig } from '../types.ts'
import { getCurrentUser } from '@nextcloud/auth'
import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Folder, getFileListActions, Permission, sortNodes } from '@nextcloud/files'
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
import { Permission, sortNodes } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { dirname, join } from '@nextcloud/paths'
@@ -189,6 +187,7 @@ import BreadCrumbs from '../components/BreadCrumbs.vue'
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
import FileListFilters from '../components/FileListFilter/FileListFilters.vue'
import FilesListVirtual from '../components/FilesListVirtual.vue'
import { useEnabledFileListActions } from '../composables/useFileListActions.ts'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
@@ -258,13 +257,27 @@ export default defineComponent({
const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', [])
const currentView = computed(() => activeStore.activeView)
const currentFolder = computed(() => activeStore.activeFolder)
const dirContents = computed<INode[]>(() => {
const sources = (currentFolder.value as { _children?: string[] })?._children ?? []
return sources.map(filesStore.getNode)
.filter(Boolean) as INode[]
})
const enabledFileListActions = useEnabledFileListActions(
currentFolder,
dirContents,
currentView,
)
return {
currentFolder,
currentView,
dirContents,
directory,
enabledFileListActions,
fileId,
isNarrow,
t,
sidebar,
activeStore,
@@ -280,6 +293,7 @@ export default defineComponent({
enableGridView,
forbiddenCharacters,
ShareType,
t,
}
},
@@ -329,34 +343,6 @@ export default defineComponent({
return `${this.currentFolder.displayname} - ${title}`
},
/**
* The current folder.
*/
currentFolder(): Folder {
// Temporary fake folder to use until we have the first valid folder
// fetched and cached. This allow us to mount the FilesListVirtual
// at all time and avoid unmount/mount and undesired rendering issues.
const dummyFolder = new Folder({
id: 0,
source: getRemoteURL() + getRootPath(),
root: getRootPath(),
owner: getCurrentUser()?.uid || null,
permissions: Permission.NONE,
})
if (!this.currentView?.id) {
return dummyFolder
}
return this.filesStore.getDirectoryByPath(this.currentView.id, this.directory) || dummyFolder
},
dirContents(): Node[] {
return (this.currentFolder?._children || [])
.map(this.filesStore.getNode)
.filter((node: Node) => !!node)
},
/**
* The current directory contents.
*/
@@ -445,27 +431,6 @@ export default defineComponent({
return !this.loading && this.isEmptyDir && this.currentView?.emptyView !== undefined
},
enabledFileListActions() {
if (!this.currentView || !this.currentFolder) {
return []
}
const actions = getFileListActions()
const enabledActions = actions
.filter((action) => {
if (action.enabled === undefined) {
return true
}
return action.enabled({
view: this.currentView!,
folder: this.currentFolder!,
contents: this.dirContents,
})
})
.toSorted((a, b) => a.order - b.order)
return enabledActions
},
/**
* Using the filtered content if filters are active
*/
@@ -495,10 +460,6 @@ export default defineComponent({
}
},
currentFolder() {
this.activeStore.activeFolder = this.currentFolder
},
currentView(newView, oldView) {
if (newView?.id === oldView?.id) {
return
+18 -14
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, Navigation } from '@nextcloud/files'
import type { IFolder } from '@nextcloud/files'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
import { getNavigation, View } from '@nextcloud/files'
@@ -21,13 +21,21 @@ beforeAll(async () => {
await fireEvent.resize(window)
})
const navigation = getNavigation()
beforeEach(() => {
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
expect(navigation.views).toHaveLength(0)
})
describe('Navigation', () => {
beforeEach(cleanup)
beforeEach(async () => {
delete window._nc_navigation
mockWindow()
getNavigation().register(createView('files', 'Files'))
navigation.register(createView('files', 'Files'))
await router.replace({ name: 'filelist', params: { view: 'files' } })
})
@@ -130,11 +138,7 @@ describe('Navigation', () => {
})
describe('Navigation API', () => {
let Navigation: Navigation
beforeEach(async () => {
delete window._nc_navigation
Navigation = getNavigation()
mockWindow()
await router.replace({ name: 'filelist', params: { view: 'files' } })
@@ -144,7 +148,7 @@ describe('Navigation API', () => {
beforeEach(cleanup)
it('Check API entries rendering', async () => {
Navigation.register(createView('files', 'Files'))
navigation.register(createView('files', 'Files'))
const component = render(NavigationView, {
router,
@@ -171,8 +175,8 @@ describe('Navigation API', () => {
})
it('Adds a new entry and render', async () => {
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
navigation.register(createView('files', 'Files'))
navigation.register(createView('sharing', 'Sharing'))
const component = render(NavigationView, {
router,
@@ -198,9 +202,9 @@ describe('Navigation API', () => {
})
it('Adds a new children, render and open menu', async () => {
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
navigation.register(createView('files', 'Files'))
navigation.register(createView('sharing', 'Sharing'))
navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
const component = render(NavigationView, {
router,
@@ -272,7 +276,7 @@ function createView(id: string, name: string, parent?: string) {
return new View({
id,
name,
getContents: async () => ({ folder: {} as Folder, contents: [] }),
getContents: async () => ({ folder: {} as IFolder, contents: [] }),
icon: FolderSvg,
order: 1,
parent,
+39 -51
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, Folder as NcFolder } from '@nextcloud/files'
import type { IFolder } from '@nextcloud/files'
import * as eventBus from '@nextcloud/event-bus'
import * as filesUtils from '@nextcloud/files'
@@ -23,30 +23,26 @@ window.OC = {
TAG_FAVORITE: '_$!<Favorite>!$_',
}
declare global {
interface Window {
_nc_navigation?: Navigation
const navigation = getNavigation()
beforeEach(() => {
vi.resetAllMocks()
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
}
expect(navigation.views).toHaveLength(0)
})
describe('Favorites view definition', () => {
let Navigation
beforeEach(() => {
vi.resetAllMocks()
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Default empty favorite view', async () => {
vi.spyOn(eventBus, 'subscribe')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
const favoritesView = navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
expect(eventBus.subscribe).toHaveBeenCalledTimes(3)
expect(eventBus.subscribe).toHaveBeenNthCalledWith(1, 'files:favorites:added', expect.anything())
@@ -54,7 +50,7 @@ describe('Favorites view definition', () => {
expect(eventBus.subscribe).toHaveBeenNthCalledWith(3, 'files:node:renamed', expect.anything())
// one main view and no children
expect(Navigation.views.length).toBe(1)
expect(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@@ -95,14 +91,14 @@ describe('Favorites view definition', () => {
}),
]
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve(favoriteFolders))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: favoriteFolders }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: favoriteFolders }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
const favoritesView = navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
// one main view and 3 children
expect(Navigation.views.length).toBe(5)
expect(navigation.views.length).toBe(5)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(4)
@@ -129,25 +125,17 @@ describe('Favorites view definition', () => {
})
describe('Dynamic update of favorite folders', () => {
let Navigation
beforeEach(() => {
vi.restoreAllMocks()
delete window._nc_navigation
Navigation = getNavigation()
})
test('Add a favorite folder creates a new entry in the navigation', async () => {
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
const favoritesView = navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
// one main view and no children
expect(Navigation.views.length).toBe(1)
expect(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@@ -162,8 +150,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
@@ -181,14 +169,14 @@ describe('Dynamic update of favorite folders', () => {
})]
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve(favoriteFolders))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: favoriteFolders }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: favoriteFolders }))
await registerFavoritesView()
let favoritesView = Navigation.views.find((view) => view.id === 'favorites')
let favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
let favoritesView = navigation.views.find((view) => view.id === 'favorites')
let favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
// one main view and no children
expect(Navigation.views.length).toBe(2)
expect(navigation.views.length).toBe(2)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(1)
@@ -209,8 +197,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
@@ -219,11 +207,11 @@ describe('Dynamic update of favorite folders', () => {
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
expect(fo).toHaveBeenCalled()
favoritesView = Navigation.views.find((view) => view.id === 'favorites')
favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
favoritesView = navigation.views.find((view) => view.id === 'favorites')
favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
// one main view and no children
expect(Navigation.views.length).toBe(1)
expect(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
})
@@ -231,14 +219,14 @@ describe('Dynamic update of favorite folders', () => {
test('Renaming a favorite folder updates the navigation', async () => {
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
const favoritesView = navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
// one main view and no children
expect(Navigation.views.length).toBe(1)
expect(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@@ -255,8 +243,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
@@ -1,14 +1,15 @@
import type { Folder } from '@nextcloud/files'
/**
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFolder } from '@nextcloud/files'
import type { ComponentPublicInstance, VueConstructor } from 'vue'
import { Header, registerFileListHeaders } from '@nextcloud/files'
import { registerFileListHeader } from '@nextcloud/files'
import Vue from 'vue'
type IFilesHeaderNoteToRecipient = ComponentPublicInstance & { updateFolder: (folder: Folder) => void }
type IFilesHeaderNoteToRecipient = ComponentPublicInstance & { updateFolder: (folder: IFolder) => void }
/**
* Register the "note to recipient" as a files list header
@@ -17,19 +18,19 @@ export default function registerNoteToRecipient() {
let FilesHeaderNoteToRecipient: VueConstructor
let instance: IFilesHeaderNoteToRecipient
registerFileListHeaders(new Header({
registerFileListHeader({
id: 'note-to-recipient',
order: 0,
// Always if there is a note
enabled: (folder: Folder) => Boolean(folder.attributes.note),
enabled: (folder: IFolder) => Boolean(folder.attributes.note),
// Update the root folder if needed
updated: (folder: Folder) => {
updated: (folder: IFolder) => {
if (instance) {
instance.updateFolder(folder)
}
},
// render simply spawns the component
render: async (el: HTMLElement, folder: Folder) => {
render: async (el: HTMLElement, folder: IFolder) => {
if (FilesHeaderNoteToRecipient === undefined) {
const { default: component } = await import('../views/FilesHeaderNoteToRecipient.vue')
FilesHeaderNoteToRecipient = Vue.extend(component)
@@ -37,5 +38,5 @@ export default function registerNoteToRecipient() {
instance = new FilesHeaderNoteToRecipient().$mount(el) as unknown as IFilesHeaderNoteToRecipient
instance.updateFolder(folder)
},
}))
})
}
@@ -3,44 +3,40 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, View } from '@nextcloud/files'
import type { View } from '@nextcloud/files'
import type { OCSResponse } from '@nextcloud/typings/ocs'
import axios from '@nextcloud/axios'
import { Folder, getNavigation } from '@nextcloud/files'
import { getNavigation } from '@nextcloud/files'
import * as ncInitialState from '@nextcloud/initial-state'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import registerSharingViews from './shares.ts'
import '../main.ts'
declare global {
interface Window {
_nc_navigation?: Navigation
const navigation = getNavigation()
beforeEach(() => {
vi.resetAllMocks()
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
}
expect(navigation.views).toHaveLength(0)
})
describe('Sharing views definition', () => {
let Navigation
beforeEach(() => {
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Default values', () => {
vi.spyOn(Navigation, 'register')
expect(Navigation.views.length).toBe(0)
vi.spyOn(navigation, 'register')
registerSharingViews()
const shareOverviewView = Navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = Navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
const shareOverviewView = navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
expect(Navigation.register).toHaveBeenCalledTimes(7)
expect(navigation.register).toHaveBeenCalledTimes(7)
// one main view and no children
expect(Navigation.views.length).toBe(7)
expect(navigation.views.length).toBe(7)
expect(shareOverviewView).toBeDefined()
expect(sharesChildViews.length).toBe(6)
@@ -76,35 +72,28 @@ describe('Sharing views definition', () => {
})
test('Shared with others view is not registered if user has no storage quota', () => {
vi.spyOn(Navigation, 'register')
vi.spyOn(navigation, 'register')
const spy = vi.spyOn(ncInitialState, 'loadState').mockImplementationOnce(() => ({ quota: 0 }))
expect(Navigation.views.length).toBe(0)
expect(navigation.views.length).toBe(0)
registerSharingViews()
expect(Navigation.register).toHaveBeenCalledTimes(6)
expect(Navigation.views.length).toBe(6)
expect(navigation.register).toHaveBeenCalledTimes(6)
expect(navigation.views.length).toBe(6)
const shareOverviewView = Navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = Navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
const shareOverviewView = navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
expect(shareOverviewView).toBeDefined()
expect(sharesChildViews.length).toBe(5)
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledWith('files', 'storageStats', { quota: -1 })
const sharedWithOthersView = Navigation.views.find((view) => view.id === 'sharingout')
const sharedWithOthersView = navigation.views.find((view) => view.id === 'sharingout')
expect(sharedWithOthersView).toBeUndefined()
})
})
describe('Sharing views contents', () => {
let Navigation
beforeEach(() => {
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Sharing overview get contents', async () => {
vi.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
@@ -122,11 +111,11 @@ describe('Sharing views contents', () => {
})
registerSharingViews()
expect(Navigation.views.length).toBe(7)
Navigation.views.forEach(async (view: View) => {
const content = await view.getContents('/')
expect(navigation.views.length).toBe(7)
for (const view of navigation.views) {
const content = await view.getContents('/', { signal: new AbortController().signal })
expect(content.contents).toStrictEqual([])
expect(content.folder).toBeInstanceOf(Folder)
})
expect(content.folder).toBeTypeOf('object')
}
})
})
@@ -170,13 +170,13 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('does not delete node from view if request failed', async () => {
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const emitSpy = vi.spyOn(ncEventBus, 'emit')
axiosMock.request.mockImplementationOnce(() => {
throw new Error()
})
const emitSpy = vi.spyOn(ncEventBus, 'emit')
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
expect(await restoreAction.exec({
nodes: [node],
view: trashbinView,
@@ -189,6 +189,7 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('batch: only returns success if all requests worked', async () => {
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
expect(await restoreAction.execBatch!({
@@ -201,6 +202,7 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('batch: only returns success if all requests worked - one failed', async () => {
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
axiosMock.request.mockImplementationOnce(() => {
+4
View File
@@ -330,6 +330,10 @@ OC.L10N.register(
"Database transaction isolation level" : "Nivel de aislamiento de transacciones en la base de datos",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Su base de datos no funciona con el nivel de aislamiento de transacciones \"READ COMMITTED\". Esto puede causar problemas cuando se ejecutan en paralelo varias acciones.",
"Was not able to get transaction isolation level: %s" : "No se pudo obtener el nivel de aislamiento de transacciones: %s",
"Second factor configuration" : "Configuración del segundo factor",
"This instance has no second factor provider available." : "Esta instancia no dispone de un proveedor de segundo factor.",
"Second factor providers are available but two-factor authentication is not enforced." : "Hay proveedores de segundo factor disponibles, pero la autenticación de dos factores no se está requiriendo.",
"Second factor providers are available and enforced: %s." : "Los proveedores del segundo factor están disponibles y se aplican: %s.",
".well-known URLs" : "URLs .well-known ",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "`check_for_working_wellknown_setup` está configurado como falso en su configuración, por lo que se omitió esta verificación.",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "No se pudo verificar que su servidor web sirve correctamente `.well-known`. Por favor, revise manualmente.",
+4
View File
@@ -328,6 +328,10 @@
"Database transaction isolation level" : "Nivel de aislamiento de transacciones en la base de datos",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "Su base de datos no funciona con el nivel de aislamiento de transacciones \"READ COMMITTED\". Esto puede causar problemas cuando se ejecutan en paralelo varias acciones.",
"Was not able to get transaction isolation level: %s" : "No se pudo obtener el nivel de aislamiento de transacciones: %s",
"Second factor configuration" : "Configuración del segundo factor",
"This instance has no second factor provider available." : "Esta instancia no dispone de un proveedor de segundo factor.",
"Second factor providers are available but two-factor authentication is not enforced." : "Hay proveedores de segundo factor disponibles, pero la autenticación de dos factores no se está requiriendo.",
"Second factor providers are available and enforced: %s." : "Los proveedores del segundo factor están disponibles y se aplican: %s.",
".well-known URLs" : "URLs .well-known ",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "`check_for_working_wellknown_setup` está configurado como falso en su configuración, por lo que se omitió esta verificación.",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "No se pudo verificar que su servidor web sirve correctamente `.well-known`. Por favor, revise manualmente.",
+22
View File
@@ -330,6 +330,10 @@ OC.L10N.register(
"Database transaction isolation level" : "資料庫交易隔離層級",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "您的資料庫並未使用「READ COMMITTED」的交易隔離等級。當有多個行為平行進行時,這可能會造成問題。",
"Was not able to get transaction isolation level: %s" : "無法取得交易隔離層級:%s",
"Second factor configuration" : "第二因素設定",
"This instance has no second factor provider available." : "此站台沒有可用的第二因素提供者。",
"Second factor providers are available but two-factor authentication is not enforced." : "有第二因素提供者,但並未強制啟用兩階段驗證。",
"Second factor providers are available and enforced: %s." : "已啟用並強制執行第二因素提供者:%s。",
".well-known URLs" : ".well-known URL",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "`check_for_working_wellknown_setup` 在您的設定中被設定為 false,因此略過此檢查。",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "無法檢查您的網路伺服器是否正確提供 `.well-known`。請手動檢查。",
@@ -438,9 +442,16 @@ OC.L10N.register(
"This app is supported via your current Nextcloud subscription." : "您目前的 Nextcloud 訂閱方案支援此應用程式。",
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "精選應用程式是由社群開發。它們提供了相當重要的功能,並已準備好在正式環境使用。",
"Community rating: {score}/5" : "社群評分:{score}/5",
"Office suite switching is managed through the Nextcloud All-in-One interface." : "辦公室套裝軟體切換透過 Nextcloud All-in-One 介面管理。",
"Please use the AIO interface to switch between office suites." : "請使用 AIO 介面在辦公室套裝軟體間切換。",
"Select your preferred office suite. Please note that installing requires manual server setup." : "選取您偏好的辦公室套裝軟體。請注意,安裝需要手動設定伺服器。",
"installed" : "已安裝",
"Learn more" : "瞭解更多",
"Disable office suites" : "停用辦公室套裝軟體",
"Disable all" : "全部停用",
"Download and enable all" : "下載並全部啟用",
"All office suites disabled" : "所有辦公室套裝軟體均已停用",
"{name} enabled" : "已啟用 {name}",
"All apps are up-to-date." : "所有應用程式都是最新狀態。",
"Icon" : "圖示",
"Name" : "名稱",
@@ -900,6 +911,17 @@ OC.L10N.register(
"App bundles" : "應用程式套組",
"Featured apps" : "精選應用程式",
"Supported apps" : "支援的應用程式",
"Best Nextcloud integration" : "最佳 Nextcloud 整合",
"Open source" : "開放原始碼",
"Good performance" : "效能不錯",
"Best security: documents never leave your server" : "最佳安全性:文件永遠不會離開您的伺服器",
"Best ODF compatibility" : "最佳 ODF 相容性",
"Best support for legacy files" : "對舊版檔案的最佳支援",
"Good Nextcloud integration" : "Nextcloud 整合不錯",
"Open core" : "開放核心",
"Best performance" : "最佳效能",
"Limited ODF compatibility" : "有限的 ODF 相容性",
"Best Microsoft compatibility" : "最佳微軟相容性",
"Show to everyone" : "對所有人顯示",
"Show to logged in accounts only" : "僅對已登入的帳號顯示",
"Hide" : "隱藏",
+22
View File
@@ -328,6 +328,10 @@
"Database transaction isolation level" : "資料庫交易隔離層級",
"Your database does not run with \"READ COMMITTED\" transaction isolation level. This can cause problems when multiple actions are executed in parallel." : "您的資料庫並未使用「READ COMMITTED」的交易隔離等級。當有多個行為平行進行時,這可能會造成問題。",
"Was not able to get transaction isolation level: %s" : "無法取得交易隔離層級:%s",
"Second factor configuration" : "第二因素設定",
"This instance has no second factor provider available." : "此站台沒有可用的第二因素提供者。",
"Second factor providers are available but two-factor authentication is not enforced." : "有第二因素提供者,但並未強制啟用兩階段驗證。",
"Second factor providers are available and enforced: %s." : "已啟用並強制執行第二因素提供者:%s。",
".well-known URLs" : ".well-known URL",
"`check_for_working_wellknown_setup` is set to false in your configuration, so this check was skipped." : "`check_for_working_wellknown_setup` 在您的設定中被設定為 false,因此略過此檢查。",
"Could not check that your web server serves `.well-known` correctly. Please check manually." : "無法檢查您的網路伺服器是否正確提供 `.well-known`。請手動檢查。",
@@ -436,9 +440,16 @@
"This app is supported via your current Nextcloud subscription." : "您目前的 Nextcloud 訂閱方案支援此應用程式。",
"Featured apps are developed by and within the community. They offer central functionality and are ready for production use." : "精選應用程式是由社群開發。它們提供了相當重要的功能,並已準備好在正式環境使用。",
"Community rating: {score}/5" : "社群評分:{score}/5",
"Office suite switching is managed through the Nextcloud All-in-One interface." : "辦公室套裝軟體切換透過 Nextcloud All-in-One 介面管理。",
"Please use the AIO interface to switch between office suites." : "請使用 AIO 介面在辦公室套裝軟體間切換。",
"Select your preferred office suite. Please note that installing requires manual server setup." : "選取您偏好的辦公室套裝軟體。請注意,安裝需要手動設定伺服器。",
"installed" : "已安裝",
"Learn more" : "瞭解更多",
"Disable office suites" : "停用辦公室套裝軟體",
"Disable all" : "全部停用",
"Download and enable all" : "下載並全部啟用",
"All office suites disabled" : "所有辦公室套裝軟體均已停用",
"{name} enabled" : "已啟用 {name}",
"All apps are up-to-date." : "所有應用程式都是最新狀態。",
"Icon" : "圖示",
"Name" : "名稱",
@@ -898,6 +909,17 @@
"App bundles" : "應用程式套組",
"Featured apps" : "精選應用程式",
"Supported apps" : "支援的應用程式",
"Best Nextcloud integration" : "最佳 Nextcloud 整合",
"Open source" : "開放原始碼",
"Good performance" : "效能不錯",
"Best security: documents never leave your server" : "最佳安全性:文件永遠不會離開您的伺服器",
"Best ODF compatibility" : "最佳 ODF 相容性",
"Best support for legacy files" : "對舊版檔案的最佳支援",
"Good Nextcloud integration" : "Nextcloud 整合不錯",
"Open core" : "開放核心",
"Best performance" : "最佳效能",
"Limited ODF compatibility" : "有限的 ODF 相容性",
"Best Microsoft compatibility" : "最佳微軟相容性",
"Show to everyone" : "對所有人顯示",
"Show to logged in accounts only" : "僅對已登入的帳號顯示",
"Hide" : "隱藏",
+10
View File
@@ -68,14 +68,21 @@ OC.L10N.register(
"Privacy policy link" : "Lien vers la politique de confidentialité",
"Background and color" : "Arrière-plan et couleur",
"Primary color" : "Couleur principale",
"Set the default primary color, used to highlight important elements." : "Définissez la couleur primaire par défaut, utilisée pour mettre en évidence les éléments importants.",
"The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements." : "La couleur utilisée pour les éléments tels que les boutons principaux peut varier légèrement, car elle est ajustée afin de répondre aux exigences en matière d'accessibilité.",
"Background color" : "Couleur d'arrière-plan",
"When no background image is set the background color will be used." : "Si aucune image d'arrière-plan n'est définie, la couleur d'arrière-plan sera utilisée.",
"Otherwise the background color is by default generated from the background image, but can be adjusted to fine tune the color of the navigation icons." : "Sinon, la couleur d'arrière-plan est générée par défaut à partir de l'image d'arrière-plan, mais peut être ajustée pour affiner la couleur des icônes de navigation. ",
"Use a plain background color instead of a background image." : "Utiliser une couleur unie d'arrière-plan plutôt qu'une image d'arrière-plan.",
"Remove background image" : "Retirer l'image d'arrière-plan",
"Background image" : "Image d'arrière-plan",
"Favicon" : "Favicon",
"Logo" : "Logo",
"Navigation bar logo" : "Logo de la barre de navigation",
"Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on." : "Bien que vous puissiez sélectionner et personnaliser votre instance, les utilisateurs peuvent modifier leur arrière-plan et leurs couleurs. Si vous voulez imposer votre personnalisation, vous pouvez activer cette option.",
"Disable user theming" : "Désactiver la gestion du thème par l'utilisateur",
"Current selected app: {app}, position {position} of {total}" : "L'application sélectionnée actuelle : {app}, position {position} sur {total}",
"Navigation bar app order" : "Ordre des applications de la barre de navigation",
"Move up" : "Déplacer vers le haut",
"Move down" : "Déplacer vers le bas",
"Theme selection is enforced" : "La sélection du thème est imposée",
@@ -91,6 +98,7 @@ OC.L10N.register(
"Custom background" : "Arrière-plan personnalisé",
"Plain background" : "Arrière-plan uni",
"Default background" : "Arrière-plan par défaut",
"Default shipped background images" : "Images d'arrière-plan fournies par défaut",
"Keyboard shortcuts" : "Raccourcis clavier",
"In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps." : "Dans certains cas, les raccourcis clavier peuvent interférer avec les outils d'accessibilité. Afin de vous permettre de vous concentrer correctement sur votre outil, vous pouvez désactiver tous les raccourcis clavier ici. Cela désactivera également tous les raccourcis disponibles dans les applications.",
"Disable all keyboard shortcuts" : "Désactiver tous les raccourcis clavier",
@@ -98,6 +106,8 @@ OC.L10N.register(
"Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements." : "Définissez une couleur principale pour mettre en évidence les éléments importants. La couleur utilisée pour les éléments tels que les boutons primaires peut varier légèrement en fonction des exigences d'accessibilité.",
"Reset primary color" : "Réinitialiser la couleur principale",
"Reset to default" : "Restaurer les valeurs par défaut",
"Non image file selected" : "Fichier non image sélectionné",
"Preview of the selected image" : "Aperçu de l'image sélectionnée",
"Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "Laccès universel est très important pour nous. Nous suivons les standards du web et nous assurons que tout soit également utilisable sans souris et avec des logiciels dassistance technique tels que les lecteurs d’écran. Nous visons à respecter les {linkstart}Règles pour laccessibilité des contenus Web{linkend} 2.1 de niveau AA et même de niveau AAA avec le thème à fort contraste.",
"If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si vous rencontrez des problèmes, n'hésitez pas à les signaler sur {issuetracker}notre outil de suivi des problèmes{linkend}. Et si vous voulez vous impliquer, venez rejoindre {designteam}notre équipe de design{linkend} !",
"Unable to apply the setting." : "Impossible d'appliquer le réglage.",
+10
View File
@@ -66,14 +66,21 @@
"Privacy policy link" : "Lien vers la politique de confidentialité",
"Background and color" : "Arrière-plan et couleur",
"Primary color" : "Couleur principale",
"Set the default primary color, used to highlight important elements." : "Définissez la couleur primaire par défaut, utilisée pour mettre en évidence les éléments importants.",
"The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements." : "La couleur utilisée pour les éléments tels que les boutons principaux peut varier légèrement, car elle est ajustée afin de répondre aux exigences en matière d'accessibilité.",
"Background color" : "Couleur d'arrière-plan",
"When no background image is set the background color will be used." : "Si aucune image d'arrière-plan n'est définie, la couleur d'arrière-plan sera utilisée.",
"Otherwise the background color is by default generated from the background image, but can be adjusted to fine tune the color of the navigation icons." : "Sinon, la couleur d'arrière-plan est générée par défaut à partir de l'image d'arrière-plan, mais peut être ajustée pour affiner la couleur des icônes de navigation. ",
"Use a plain background color instead of a background image." : "Utiliser une couleur unie d'arrière-plan plutôt qu'une image d'arrière-plan.",
"Remove background image" : "Retirer l'image d'arrière-plan",
"Background image" : "Image d'arrière-plan",
"Favicon" : "Favicon",
"Logo" : "Logo",
"Navigation bar logo" : "Logo de la barre de navigation",
"Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on." : "Bien que vous puissiez sélectionner et personnaliser votre instance, les utilisateurs peuvent modifier leur arrière-plan et leurs couleurs. Si vous voulez imposer votre personnalisation, vous pouvez activer cette option.",
"Disable user theming" : "Désactiver la gestion du thème par l'utilisateur",
"Current selected app: {app}, position {position} of {total}" : "L'application sélectionnée actuelle : {app}, position {position} sur {total}",
"Navigation bar app order" : "Ordre des applications de la barre de navigation",
"Move up" : "Déplacer vers le haut",
"Move down" : "Déplacer vers le bas",
"Theme selection is enforced" : "La sélection du thème est imposée",
@@ -89,6 +96,7 @@
"Custom background" : "Arrière-plan personnalisé",
"Plain background" : "Arrière-plan uni",
"Default background" : "Arrière-plan par défaut",
"Default shipped background images" : "Images d'arrière-plan fournies par défaut",
"Keyboard shortcuts" : "Raccourcis clavier",
"In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps." : "Dans certains cas, les raccourcis clavier peuvent interférer avec les outils d'accessibilité. Afin de vous permettre de vous concentrer correctement sur votre outil, vous pouvez désactiver tous les raccourcis clavier ici. Cela désactivera également tous les raccourcis disponibles dans les applications.",
"Disable all keyboard shortcuts" : "Désactiver tous les raccourcis clavier",
@@ -96,6 +104,8 @@
"Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements." : "Définissez une couleur principale pour mettre en évidence les éléments importants. La couleur utilisée pour les éléments tels que les boutons primaires peut varier légèrement en fonction des exigences d'accessibilité.",
"Reset primary color" : "Réinitialiser la couleur principale",
"Reset to default" : "Restaurer les valeurs par défaut",
"Non image file selected" : "Fichier non image sélectionné",
"Preview of the selected image" : "Aperçu de l'image sélectionnée",
"Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "Laccès universel est très important pour nous. Nous suivons les standards du web et nous assurons que tout soit également utilisable sans souris et avec des logiciels dassistance technique tels que les lecteurs d’écran. Nous visons à respecter les {linkstart}Règles pour laccessibilité des contenus Web{linkend} 2.1 de niveau AA et même de niveau AAA avec le thème à fort contraste.",
"If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si vous rencontrez des problèmes, n'hésitez pas à les signaler sur {issuetracker}notre outil de suivi des problèmes{linkend}. Et si vous voulez vous impliquer, venez rejoindre {designteam}notre équipe de design{linkend} !",
"Unable to apply the setting." : "Impossible d'appliquer le réglage.",
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { formatRelativeTime, t } from '@nextcloud/l10n'
import { formatRelativeTime, getFirstDay, t } from '@nextcloud/l10n'
import { dateFactory } from './dateService.js'
/**
@@ -94,6 +94,6 @@ function getEndOfDay(date) {
*/
function getEndOfWeek(date) {
const endOfWeek = getEndOfDay(date)
endOfWeek.setDate(date.getDate() + ((endOfWeek.getFirstDay() - 1 - endOfWeek.getDay() + 7) % 7))
endOfWeek.setDate(date.getDate() + ((getFirstDay() - 1 - endOfWeek.getDay() + 7) % 7))
return endOfWeek
}
+92 -270
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -35,7 +35,7 @@
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/dialogs": "^7.2.0",
"@nextcloud/event-bus": "^3.3.3",
"@nextcloud/files": "^4.0.0-rc.1",
"@nextcloud/files": "^4.0.0-rc.3",
"@nextcloud/initial-state": "^3.0.0",
"@nextcloud/l10n": "^3.4.1",
"@nextcloud/logger": "^3.0.3",
+1 -1
View File
@@ -10,7 +10,7 @@ $nextcloudDir = dirname(__DIR__);
return (require __DIR__ . '/rector-shared.php')
->withPaths([
$nextcloudDir . '/build/rector-strict.php',
// TODO: Add more files. The entry above is just there to stop rector from complaining about the fact that it ran without checking any files.
$nextcloudDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
])
->withPreparedSets(
deadCode: true,
+8
View File
@@ -2917,4 +2917,12 @@ $CONFIG = [
'fe80::/10',
'10.0.0.1',
],
/**
* Delete previews older than a certain number of days to reduce storage usage.
* Less than one day is not allowed, so set it to 0 to disable the deletion.
*
* Defaults to ``0``.
*/
'preview_expiration_days' => 0,
];
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\BackgroundJobs;
use OC\Preview\PreviewService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
class ExpirePreviewsJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly IConfig $config,
private readonly PreviewService $service,
) {
parent::__construct($time);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
$this->setInterval(60 * 60 * 24);
}
protected function run(mixed $argument): void {
$days = $this->config->getSystemValueInt('preview_expiration_days');
if ($days <= 0) {
return;
}
$this->service->deleteExpiredPreviews($days);
}
}
+2
View File
@@ -45,6 +45,8 @@ OC.L10N.register(
"Input text is too long" : "El texto introducido es demasiado largo",
"Requested task type does not exist" : "El tipo de tarea solicitada no existe",
"Necessary language model provider is not available" : "El proveedor de modelo de lenguaje necesario no está disponible",
"Cannot generate more than 12 images" : "No se puede generar más de 12 imágenes",
"Cannot generate less than 1 image" : "No se puede generar menos de 1 imágen",
"No text to image provider is available" : "No hay proveedores de texto a imagen disponible",
"Image not found" : "Imagen no encontrada",
"No translation provider available" : "No hay proveedores de traducción disponibles",
+2
View File
@@ -43,6 +43,8 @@
"Input text is too long" : "El texto introducido es demasiado largo",
"Requested task type does not exist" : "El tipo de tarea solicitada no existe",
"Necessary language model provider is not available" : "El proveedor de modelo de lenguaje necesario no está disponible",
"Cannot generate more than 12 images" : "No se puede generar más de 12 imágenes",
"Cannot generate less than 1 image" : "No se puede generar menos de 1 imágen",
"No text to image provider is available" : "No hay proveedores de texto a imagen disponible",
"Image not found" : "Imagen no encontrada",
"No translation provider available" : "No hay proveedores de traducción disponibles",
+2
View File
@@ -304,7 +304,9 @@ OC.L10N.register(
"Loading your contacts …" : "Chargement de vos contacts...",
"Looking for {term} …" : "Recherche de {term}...",
"Search contacts" : "Rechercher des contacts",
"Filter by team" : "Filtrer par équipe",
"All teams" : "Toutes les équipes",
"Search contacts in team {team}" : "Rechercher des contacts dans l'équipe {team}",
"Search contacts …" : "Rechercher un contact...",
"Reset search" : "Réinitialiser la recherche",
"Could not load your contacts" : "Impossible de charger vos contacts",
+2
View File
@@ -302,7 +302,9 @@
"Loading your contacts …" : "Chargement de vos contacts...",
"Looking for {term} …" : "Recherche de {term}...",
"Search contacts" : "Rechercher des contacts",
"Filter by team" : "Filtrer par équipe",
"All teams" : "Toutes les équipes",
"Search contacts in team {team}" : "Rechercher des contacts dans l'équipe {team}",
"Search contacts …" : "Rechercher un contact...",
"Reset search" : "Réinitialiser la recherche",
"Could not load your contacts" : "Impossible de charger vos contacts",
+1
View File
@@ -45,6 +45,7 @@ OC.L10N.register(
"Input text is too long" : "輸入文字太長",
"Requested task type does not exist" : "請求的任務類型不存在",
"Necessary language model provider is not available" : "沒有可用的語言模型程式",
"Cannot generate more than 12 images" : "無法產生超過 12 張影像",
"Cannot generate less than 1 image" : "無法產生少於 1 張影像",
"No text to image provider is available" : "沒有可用的文字轉影像提供者",
"Image not found" : "找不到影像",
+1
View File
@@ -43,6 +43,7 @@
"Input text is too long" : "輸入文字太長",
"Requested task type does not exist" : "請求的任務類型不存在",
"Necessary language model provider is not available" : "沒有可用的語言模型程式",
"Cannot generate more than 12 images" : "無法產生超過 12 張影像",
"Cannot generate less than 1 image" : "無法產生少於 1 張影像",
"No text to image provider is available" : "沒有可用的文字轉影像提供者",
"Image not found" : "找不到影像",
-200
View File
@@ -4,7 +4,6 @@
*/
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { IFileAction } from '@nextcloud/files'
import { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSelectionActionButton, getSelectionActionEntry, selectRowForFile } from './FilesUtils.ts'
@@ -12,12 +11,6 @@ const ACTION_DELETE = 'delete'
const ACTION_COPY_MOVE = 'move-copy'
const ACTION_DETAILS = 'details'
declare global {
interface Window {
_nc_fileactions: IFileAction[]
}
}
// Those two arrays doesn't represent the full list of actions
// the goal is to test a few, we're not trying to match the full feature set
const expectedDefaultActionsIDs = [
@@ -57,77 +50,6 @@ describe('Files: Actions', { testIsolation: true }, () => {
})
})
it('Show some nested actions', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
// Open the menu
getActionButtonForFileId(fileId)
.scrollIntoView()
.click({ force: true })
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
getActionEntryForFileId(fileId, 'menu-back').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-1').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-2').should('not.exist')
// Click on the parent action
getActionEntryForFileId(fileId, 'nested-action')
.should('be.visible')
.click()
// Check we have the children and the back button but not the parent
getActionEntryForFileId(fileId, 'nested-action').should('not.exist')
getActionEntryForFileId(fileId, 'menu-back').should('be.visible')
getActionEntryForFileId(fileId, 'nested-child-1').should('be.visible')
getActionEntryForFileId(fileId, 'nested-child-2').should('be.visible')
// Click on the back button
getActionEntryForFileId(fileId, 'menu-back')
.should('be.visible')
.click()
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
getActionEntryForFileId(fileId, 'menu-back').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-1').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-2').should('not.exist')
})
it('Show some actions for a selection', () => {
cy.visit('/apps/files')
getRowForFile('image.jpg').should('be.visible')
@@ -145,126 +67,4 @@ describe('Files: Actions', { testIsolation: true }, () => {
getSelectionActionEntry(actionId).should('be.visible')
})
})
it('Show some nested actions for a selection', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
selectRowForFile('image.jpg')
// Open the menu
getSelectionActionButton().click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('be.visible')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
// Click on the parent action
getSelectionActionEntry('nested-action')
.find('button').last()
.should('exist').click({ force: true })
// Check we have the children and the back button but not the parent
getSelectionActionEntry('nested-action').should('not.exist')
getSelectionActionEntry('menu-back').should('be.visible')
getSelectionActionEntry('nested-child-1').should('be.visible')
getSelectionActionEntry('nested-child-2').should('be.visible')
// Click on the back button
getSelectionActionEntry('menu-back')
.find('button').last()
.should('exist').click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('be.visible')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
})
it('Do not show parent if nested action has no batch support', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
selectRowForFile('image.jpg')
// Open the menu
getSelectionActionButton().click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('not.exist')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
})
})
+4 -1
View File
@@ -56,7 +56,7 @@ This file is generated from multiple sources. Included packages:
- version: 3.3.3
- license: GPL-3.0-or-later
- @nextcloud/files
- version: 4.0.0-rc.1
- version: 4.0.0-rc.3
- license: AGPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
@@ -157,6 +157,9 @@ This file is generated from multiple sources. Included packages:
- vue-loader
- version: 15.11.1
- license: MIT
- vue-router
- version: 3.6.5
- license: MIT
- vue
- version: 2.7.16
- license: MIT
+1 -1
View File
@@ -57,7 +57,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -71,7 +71,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+2 -2
View File
@@ -91,7 +91,7 @@ This file is generated from multiple sources. Included packages:
- version: 3.3.3
- license: GPL-3.0-or-later
- @nextcloud/files
- version: 4.0.0-rc.1
- version: 4.0.0-rc.3
- license: AGPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
@@ -145,7 +145,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -64,7 +64,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -90,7 +90,7 @@ This file is generated from multiple sources. Included packages:
- version: 6.6.4
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+2 -2
View File
@@ -84,7 +84,7 @@ This file is generated from multiple sources. Included packages:
- version: 3.3.3
- license: GPL-3.0-or-later
- @nextcloud/files
- version: 4.0.0-rc.1
- version: 4.0.0-rc.3
- license: AGPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
@@ -129,7 +129,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -69,7 +69,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -156,7 +156,7 @@ This file is generated from multiple sources. Included packages:
- version: 1.0.7
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- balanced-match
- version: 1.0.2
+1 -1
View File
@@ -115,7 +115,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- bail
- version: 2.0.2
+1 -1
View File
@@ -64,7 +64,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -156,7 +156,7 @@ This file is generated from multiple sources. Included packages:
- version: 1.0.7
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- balanced-match
- version: 1.0.2
+1 -1
View File
@@ -123,7 +123,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -122,7 +122,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- bail
- version: 2.0.2
+1 -1
View File
@@ -111,7 +111,7 @@ This file is generated from multiple sources. Included packages:
- version: 4.5.0
- license: Apache-2.0
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -156,7 +156,7 @@ This file is generated from multiple sources. Included packages:
- version: 1.0.7
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- balanced-match
- version: 1.0.2
+1 -1
View File
@@ -127,7 +127,7 @@ This file is generated from multiple sources. Included packages:
- version: 3.5.26
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -156,7 +156,7 @@ This file is generated from multiple sources. Included packages:
- version: 1.0.7
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- balanced-match
- version: 1.0.2
+1 -1
View File
@@ -111,7 +111,7 @@ This file is generated from multiple sources. Included packages:
- version: 4.5.0
- license: Apache-2.0
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -108,7 +108,7 @@ This file is generated from multiple sources. Included packages:
- version: 3.3.3
- license: GPL-3.0-or-later
- @nextcloud/files
- version: 4.0.0-rc.1
- version: 4.0.0-rc.3
- license: AGPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
@@ -168,7 +168,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -111,7 +111,7 @@ This file is generated from multiple sources. Included packages:
- version: 4.5.0
- license: Apache-2.0
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -144,7 +144,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- bail
- version: 2.0.2
+1 -1
View File
@@ -63,7 +63,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
+1 -1
View File
@@ -146,7 +146,7 @@ This file is generated from multiple sources. Included packages:
- version: 11.3.0
- license: MIT
- axios
- version: 1.12.2
- version: 1.13.5
- license: MIT
- base64-js
- version: 1.5.1
@@ -1,2 +1,2 @@
import{b as g,q as y,s as v,c as p,u as o,o as n,L as h,w as _,g as V,t as b,v as x,r as M,j as d,e as f,F as q,C as w,E as K,G as U}from"./runtime-dom.esm-bundler-DSTOTAEf.chunk.mjs";import{c as j}from"./index-D9L8KHF3.chunk.mjs";import{a as C}from"./index-JpgrUA2Z-DPCs44Lo.chunk.mjs";import{t as s}from"./translation-DoG5ZELJ-2ffMJaM4.chunk.mjs";import{g as E}from"./createElementId-DhjFt1I9-Bjk2333q.chunk.mjs";import{c as L}from"./logger-D3RVzcfQ-iUjwSNGe.chunk.mjs";import{N as S}from"./NcSelect-Czzsi3P_-wYuKB0zM.chunk.mjs";import{N as A}from"./NcCheckboxRadioSwitch-BCSKF7Tk-BDM2s1GW.chunk.mjs";import{N}from"./NcPasswordField-djttkA5Q-DvTgf1Bu.chunk.mjs";import{_ as z}from"./TrashCanOutline-DKx7CxBb.chunk.mjs";import{C as c,a as k}from"./types-DD622x-I.chunk.mjs";import{l as B}from"./logger-CrDakPzW.chunk.mjs";const P=g({__name:"ConfigurationEntry",props:y({configKey:{},configOption:{}},{modelValue:{type:[String,Boolean],default:""},modelModifiers:{}}),emits:["update:modelValue"],setup(e){const a=v(e,"modelValue");return(t,i)=>e.configOption.type!==o(c).Boolean?(n(),p(h(e.configOption.type===o(c).Password?o(N):o(z)),{key:0,modelValue:a.value,"onUpdate:modelValue":i[0]||(i[0]=l=>a.value=l),name:e.configKey,required:!(e.configOption.flags&o(k).Optional),label:e.configOption.value,title:e.configOption.tooltip},null,8,["modelValue","name","required","label","title"])):(n(),p(o(A),{key:1,modelValue:a.value,"onUpdate:modelValue":i[1]||(i[1]=l=>a.value=l),type:"switch",title:e.configOption.tooltip},{default:_(()=>[V(b(e.configOption.value),1)]),_:1},8,["modelValue","title"]))}}),R=g({__name:"AuthMechanismRsa",props:y({authMechanism:{}},{modelValue:{required:!0},modelModifiers:{}}),emits:["update:modelValue"],setup(e){const a=v(e,"modelValue"),t=M();x(t,()=>{t.value&&(a.value.private_key="",a.value.public_key="")});async function i(){try{const{data:l}=await j.post(E("/apps/files_external/ajax/public_key.php"),{keyLength:t.value});a.value.private_key=l.data.private_key,a.value.public_key=l.data.public_key}catch(l){B.error("Error generating RSA key pair",{error:l}),C(s("files_external","Error generating key pair"))}}return(l,m)=>(n(),d("div",null,[(n(!0),d(q,null,w(e.authMechanism.configuration,(r,u)=>K((n(),p(P,{key:r.value,modelValue:a.value[u],"onUpdate:modelValue":O=>a.value[u]=O,configKey:u,configOption:r},null,8,["modelValue","onUpdate:modelValue","configKey","configOption"])),[[U,!(r.flags&o(k).Hidden)]])),128)),f(o(S),{modelValue:t.value,"onUpdate:modelValue":m[0]||(m[0]=r=>t.value=r),clearable:!1,inputLabel:o(s)("files_external","Key size"),options:[1024,2048,4096],required:""},null,8,["modelValue","inputLabel"]),f(o(L),{disabled:!t.value,wide:"",onClick:i},{default:_(()=>[V(b(o(s)("files_external","Generate keys")),1)]),_:1},8,["disabled"])]))}}),$=Object.freeze(Object.defineProperty({__proto__:null,default:R},Symbol.toStringTag,{value:"Module"}));export{$ as A,P as _};
//# sourceMappingURL=AuthMechanismRsa-Dn6adkfl.chunk.mjs.map
import{b as g,q as y,s as v,c as p,u as o,o as n,L as h,w as _,g as V,t as b,v as x,r as M,j as d,e as f,F as q,C as w,E as K,G as U}from"./runtime-dom.esm-bundler-DSTOTAEf.chunk.mjs";import{c as j}from"./index-D9L8KHF3.chunk.mjs";import{a as C}from"./index-JpgrUA2Z-D2EPRmnZ.chunk.mjs";import{t as s}from"./translation-DoG5ZELJ-2ffMJaM4.chunk.mjs";import{g as E}from"./createElementId-DhjFt1I9-Bjk2333q.chunk.mjs";import{c as L}from"./logger-D3RVzcfQ-iUjwSNGe.chunk.mjs";import{N as S}from"./NcSelect-Czzsi3P_-7VXJuGva.chunk.mjs";import{N as A}from"./NcCheckboxRadioSwitch-BCSKF7Tk-BDM2s1GW.chunk.mjs";import{N}from"./NcPasswordField-djttkA5Q-DvTgf1Bu.chunk.mjs";import{_ as z}from"./TrashCanOutline-DKx7CxBb.chunk.mjs";import{C as c,a as k}from"./types-DD622x-I.chunk.mjs";import{l as B}from"./logger-CrDakPzW.chunk.mjs";const P=g({__name:"ConfigurationEntry",props:y({configKey:{},configOption:{}},{modelValue:{type:[String,Boolean],default:""},modelModifiers:{}}),emits:["update:modelValue"],setup(e){const a=v(e,"modelValue");return(t,i)=>e.configOption.type!==o(c).Boolean?(n(),p(h(e.configOption.type===o(c).Password?o(N):o(z)),{key:0,modelValue:a.value,"onUpdate:modelValue":i[0]||(i[0]=l=>a.value=l),name:e.configKey,required:!(e.configOption.flags&o(k).Optional),label:e.configOption.value,title:e.configOption.tooltip},null,8,["modelValue","name","required","label","title"])):(n(),p(o(A),{key:1,modelValue:a.value,"onUpdate:modelValue":i[1]||(i[1]=l=>a.value=l),type:"switch",title:e.configOption.tooltip},{default:_(()=>[V(b(e.configOption.value),1)]),_:1},8,["modelValue","title"]))}}),R=g({__name:"AuthMechanismRsa",props:y({authMechanism:{}},{modelValue:{required:!0},modelModifiers:{}}),emits:["update:modelValue"],setup(e){const a=v(e,"modelValue"),t=M();x(t,()=>{t.value&&(a.value.private_key="",a.value.public_key="")});async function i(){try{const{data:l}=await j.post(E("/apps/files_external/ajax/public_key.php"),{keyLength:t.value});a.value.private_key=l.data.private_key,a.value.public_key=l.data.public_key}catch(l){B.error("Error generating RSA key pair",{error:l}),C(s("files_external","Error generating key pair"))}}return(l,m)=>(n(),d("div",null,[(n(!0),d(q,null,w(e.authMechanism.configuration,(r,u)=>K((n(),p(P,{key:r.value,modelValue:a.value[u],"onUpdate:modelValue":O=>a.value[u]=O,configKey:u,configOption:r},null,8,["modelValue","onUpdate:modelValue","configKey","configOption"])),[[U,!(r.flags&o(k).Hidden)]])),128)),f(o(S),{modelValue:t.value,"onUpdate:modelValue":m[0]||(m[0]=r=>t.value=r),clearable:!1,inputLabel:o(s)("files_external","Key size"),options:[1024,2048,4096],required:""},null,8,["modelValue","inputLabel"]),f(o(L),{disabled:!t.value,wide:"",onClick:i},{default:_(()=>[V(b(o(s)("files_external","Generate keys")),1)]),_:1},8,["disabled"])]))}}),$=Object.freeze(Object.defineProperty({__proto__:null,default:R},Symbol.toStringTag,{value:"Module"}));export{$ as A,P as _};
//# sourceMappingURL=AuthMechanismRsa-TqhTMDlx.chunk.mjs.map
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{t}from"./translation-DoG5ZELJ-2ffMJaM4.chunk.mjs";import{N as u}from"./index-Dzo4H_NA.chunk.mjs";import{N as d}from"./NcNoteCard-CVhtNL04-CdF6Qoal.chunk.mjs";import{N as p}from"./NcPasswordField-djttkA5Q-DvTgf1Bu.chunk.mjs";import{_ as c}from"./TrashCanOutline-DKx7CxBb.chunk.mjs";import{b as g,c as f,o as h,w as x,e as s,u as e,r as n}from"./runtime-dom.esm-bundler-DSTOTAEf.chunk.mjs";import"./index-6_gsQFyp.chunk.mjs";import"./createElementId-DhjFt1I9-Bjk2333q.chunk.mjs";import"./logger-D3RVzcfQ-iUjwSNGe.chunk.mjs";import"./mdi-kAZc0JKn.chunk.mjs";import"./index-D9L8KHF3.chunk.mjs";import"./string_decoder-BO00msnV.chunk.mjs";import"./index-xFugdZPW.chunk.mjs";import"./NcInputField-Bwsh2aHY-Bf_22pmD.chunk.mjs";const k=g({__name:"CredentialsDialog",emits:["close"],setup(_){const o=n(""),r=n(""),m=[{label:t("files_external","Confirm"),type:"submit",variant:"primary"}];return(i,a)=>(h(),f(e(u),{buttons:m,class:"external-storage-auth",closeOnClickOutside:"","data-cy-external-storage-auth":"",isForm:"",name:e(t)("files_external","Storage credentials"),outTransition:"",onSubmit:a[2]||(a[2]=l=>i.$emit("close",{login:o.value,password:r.value})),"onUpdate:open":a[3]||(a[3]=l=>i.$emit("close"))},{default:x(()=>[s(e(d),{class:"external-storage-auth__header",text:e(t)("files_external","To access the storage, you need to provide the authentication credentials."),type:"info"},null,8,["text"]),s(e(c),{modelValue:o.value,"onUpdate:modelValue":a[0]||(a[0]=l=>o.value=l),autofocus:"",class:"external-storage-auth__login","data-cy-external-storage-auth-dialog-login":"",label:e(t)("files_external","Login"),placeholder:e(t)("files_external","Enter the storage login"),minlength:"2",name:"login",required:""},null,8,["modelValue","label","placeholder"]),s(e(p),{modelValue:r.value,"onUpdate:modelValue":a[1]||(a[1]=l=>r.value=l),class:"external-storage-auth__password","data-cy-external-storage-auth-dialog-password":"",label:e(t)("files_external","Password"),placeholder:e(t)("files_external","Enter the storage password"),name:"password",required:""},null,8,["modelValue","label","placeholder"])]),_:1},8,["name"]))}});export{k as default};
//# sourceMappingURL=CredentialsDialog-DwWjxtIL.chunk.mjs.map
import{t}from"./translation-DoG5ZELJ-2ffMJaM4.chunk.mjs";import{N as u}from"./index-dSOqvlAc.chunk.mjs";import{N as d}from"./NcNoteCard-CVhtNL04-CdF6Qoal.chunk.mjs";import{N as p}from"./NcPasswordField-djttkA5Q-DvTgf1Bu.chunk.mjs";import{_ as c}from"./TrashCanOutline-DKx7CxBb.chunk.mjs";import{b as g,c as f,o as h,w as x,e as s,u as e,r as n}from"./runtime-dom.esm-bundler-DSTOTAEf.chunk.mjs";import"./index-6_gsQFyp.chunk.mjs";import"./createElementId-DhjFt1I9-Bjk2333q.chunk.mjs";import"./logger-D3RVzcfQ-iUjwSNGe.chunk.mjs";import"./mdi-kAZc0JKn.chunk.mjs";import"./index-D9L8KHF3.chunk.mjs";import"./string_decoder-BO00msnV.chunk.mjs";import"./index-xFugdZPW.chunk.mjs";import"./NcInputField-Bwsh2aHY-Bf_22pmD.chunk.mjs";const k=g({__name:"CredentialsDialog",emits:["close"],setup(_){const o=n(""),r=n(""),m=[{label:t("files_external","Confirm"),type:"submit",variant:"primary"}];return(i,a)=>(h(),f(e(u),{buttons:m,class:"external-storage-auth",closeOnClickOutside:"","data-cy-external-storage-auth":"",isForm:"",name:e(t)("files_external","Storage credentials"),outTransition:"",onSubmit:a[2]||(a[2]=l=>i.$emit("close",{login:o.value,password:r.value})),"onUpdate:open":a[3]||(a[3]=l=>i.$emit("close"))},{default:x(()=>[s(e(d),{class:"external-storage-auth__header",text:e(t)("files_external","To access the storage, you need to provide the authentication credentials."),type:"info"},null,8,["text"]),s(e(c),{modelValue:o.value,"onUpdate:modelValue":a[0]||(a[0]=l=>o.value=l),autofocus:"",class:"external-storage-auth__login","data-cy-external-storage-auth-dialog-login":"",label:e(t)("files_external","Login"),placeholder:e(t)("files_external","Enter the storage login"),minlength:"2",name:"login",required:""},null,8,["modelValue","label","placeholder"]),s(e(p),{modelValue:r.value,"onUpdate:modelValue":a[1]||(a[1]=l=>r.value=l),class:"external-storage-auth__password","data-cy-external-storage-auth-dialog-password":"",label:e(t)("files_external","Password"),placeholder:e(t)("files_external","Enter the storage password"),name:"password",required:""},null,8,["modelValue","label","placeholder"])]),_:1},8,["name"]))}});export{k as default};
//# sourceMappingURL=CredentialsDialog-BDoVqYk8.chunk.mjs.map
@@ -1 +1 @@
{"version":3,"file":"CredentialsDialog-DwWjxtIL.chunk.mjs","sources":["../build/frontend/apps/files_external/src/views/CredentialsDialog.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n - SPDX-License-Identifier: AGPL-3.0-or-later\n-->\n\n<script setup lang=\"ts\">\nimport { t } from '@nextcloud/l10n'\nimport { ref } from 'vue'\nimport NcDialog from '@nextcloud/vue/components/NcDialog'\nimport NcNoteCard from '@nextcloud/vue/components/NcNoteCard'\nimport NcPasswordField from '@nextcloud/vue/components/NcPasswordField'\nimport NcTextField from '@nextcloud/vue/components/NcTextField'\n\ndefineEmits<{\n\tclose: [payload?: { login: string, password: string }]\n}>()\n\nconst login = ref('')\nconst password = ref('')\n\nconst dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{\n\tlabel: t('files_external', 'Confirm'),\n\ttype: 'submit',\n\tvariant: 'primary',\n}]\n</script>\n\n<template>\n\t<NcDialog\n\t\t:buttons=\"dialogButtons\"\n\t\tclass=\"external-storage-auth\"\n\t\tcloseOnClickOutside\n\t\tdata-cy-external-storage-auth\n\t\tisForm\n\t\t:name=\"t('files_external', 'Storage credentials')\"\n\t\toutTransition\n\t\t@submit=\"$emit('close', { login, password })\"\n\t\t@update:open=\"$emit('close')\">\n\t\t<!-- Header -->\n\t\t<NcNoteCard\n\t\t\tclass=\"external-storage-auth__header\"\n\t\t\t:text=\"t('files_external', 'To access the storage, you need to provide the authentication credentials.')\"\n\t\t\ttype=\"info\" />\n\n\t\t<!-- Login -->\n\t\t<NcTextField\n\t\t\tv-model=\"login\"\n\t\t\tautofocus\n\t\t\tclass=\"external-storage-auth__login\"\n\t\t\tdata-cy-external-storage-auth-dialog-login\n\t\t\t:label=\"t('files_external', 'Login')\"\n\t\t\t:placeholder=\"t('files_external', 'Enter the storage login')\"\n\t\t\tminlength=\"2\"\n\t\t\tname=\"login\"\n\t\t\trequired />\n\n\t\t<!-- Password -->\n\t\t<NcPasswordField\n\t\t\tv-model=\"password\"\n\t\t\tclass=\"external-storage-auth__password\"\n\t\t\tdata-cy-external-storage-auth-dialog-password\n\t\t\t:label=\"t('files_external', 'Password')\"\n\t\t\t:placeholder=\"t('files_external', 'Enter the storage password')\"\n\t\t\tname=\"password\"\n\t\t\trequired />\n\t</NcDialog>\n</template>\n"],"names":["login","ref","password","dialogButtons","t","_createBlock","_unref","NcDialog","_cache","$event","$emit","_createVNode","NcNoteCard","NcTextField","NcPasswordField"],"mappings":"sxBAiBA,MAAMA,EAAQC,EAAI,EAAE,EACdC,EAAWD,EAAI,EAAE,EAEjBE,EAA0D,CAAC,CAChE,MAAOC,EAAE,iBAAkB,SAAS,EACpC,KAAM,SACN,QAAS,SAAA,CACT,oBAIAC,EAqCWC,EAAAC,CAAA,EAAA,CApCT,QAASJ,EACV,MAAM,wBACN,oBAAA,GACA,gCAAA,GACA,OAAA,GACC,KAAMG,EAAAF,CAAA,EAAC,iBAAA,qBAAA,EACR,cAAA,GACC,SAAMI,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAC,GAAEC,EAAAA,MAAK,QAAA,CAAA,MAAYV,EAAA,eAAOE,EAAA,MAAQ,GACxC,+BAAaQ,EAAAA,MAAK,OAAA,EAAA,aAEnB,IAGe,CAHfC,EAGeL,EAAAM,CAAA,EAAA,CAFd,MAAM,gCACL,KAAMN,EAAAF,CAAA,EAAC,iBAAA,4EAAA,EACR,KAAK,MAAA,mBAGNO,EASYL,EAAAO,CAAA,EAAA,YARFb,EAAA,2CAAAA,EAAK,MAAAS,GACd,UAAA,GACA,MAAM,+BACN,6CAAA,GACC,MAAOH,EAAAF,CAAA,EAAC,iBAAA,OAAA,EACR,YAAaE,EAAAF,CAAA,EAAC,iBAAA,yBAAA,EACf,UAAU,IACV,KAAK,QACL,SAAA,EAAA,+CAGDO,EAOYL,EAAAQ,CAAA,EAAA,YANFZ,EAAA,2CAAAA,EAAQ,MAAAO,GACjB,MAAM,kCACN,gDAAA,GACC,MAAOH,EAAAF,CAAA,EAAC,iBAAA,UAAA,EACR,YAAaE,EAAAF,CAAA,EAAC,iBAAA,4BAAA,EACf,KAAK,WACL,SAAA,EAAA"}
{"version":3,"file":"CredentialsDialog-BDoVqYk8.chunk.mjs","sources":["../build/frontend/apps/files_external/src/views/CredentialsDialog.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n - SPDX-License-Identifier: AGPL-3.0-or-later\n-->\n\n<script setup lang=\"ts\">\nimport { t } from '@nextcloud/l10n'\nimport { ref } from 'vue'\nimport NcDialog from '@nextcloud/vue/components/NcDialog'\nimport NcNoteCard from '@nextcloud/vue/components/NcNoteCard'\nimport NcPasswordField from '@nextcloud/vue/components/NcPasswordField'\nimport NcTextField from '@nextcloud/vue/components/NcTextField'\n\ndefineEmits<{\n\tclose: [payload?: { login: string, password: string }]\n}>()\n\nconst login = ref('')\nconst password = ref('')\n\nconst dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{\n\tlabel: t('files_external', 'Confirm'),\n\ttype: 'submit',\n\tvariant: 'primary',\n}]\n</script>\n\n<template>\n\t<NcDialog\n\t\t:buttons=\"dialogButtons\"\n\t\tclass=\"external-storage-auth\"\n\t\tcloseOnClickOutside\n\t\tdata-cy-external-storage-auth\n\t\tisForm\n\t\t:name=\"t('files_external', 'Storage credentials')\"\n\t\toutTransition\n\t\t@submit=\"$emit('close', { login, password })\"\n\t\t@update:open=\"$emit('close')\">\n\t\t<!-- Header -->\n\t\t<NcNoteCard\n\t\t\tclass=\"external-storage-auth__header\"\n\t\t\t:text=\"t('files_external', 'To access the storage, you need to provide the authentication credentials.')\"\n\t\t\ttype=\"info\" />\n\n\t\t<!-- Login -->\n\t\t<NcTextField\n\t\t\tv-model=\"login\"\n\t\t\tautofocus\n\t\t\tclass=\"external-storage-auth__login\"\n\t\t\tdata-cy-external-storage-auth-dialog-login\n\t\t\t:label=\"t('files_external', 'Login')\"\n\t\t\t:placeholder=\"t('files_external', 'Enter the storage login')\"\n\t\t\tminlength=\"2\"\n\t\t\tname=\"login\"\n\t\t\trequired />\n\n\t\t<!-- Password -->\n\t\t<NcPasswordField\n\t\t\tv-model=\"password\"\n\t\t\tclass=\"external-storage-auth__password\"\n\t\t\tdata-cy-external-storage-auth-dialog-password\n\t\t\t:label=\"t('files_external', 'Password')\"\n\t\t\t:placeholder=\"t('files_external', 'Enter the storage password')\"\n\t\t\tname=\"password\"\n\t\t\trequired />\n\t</NcDialog>\n</template>\n"],"names":["login","ref","password","dialogButtons","t","_createBlock","_unref","NcDialog","_cache","$event","$emit","_createVNode","NcNoteCard","NcTextField","NcPasswordField"],"mappings":"sxBAiBA,MAAMA,EAAQC,EAAI,EAAE,EACdC,EAAWD,EAAI,EAAE,EAEjBE,EAA0D,CAAC,CAChE,MAAOC,EAAE,iBAAkB,SAAS,EACpC,KAAM,SACN,QAAS,SAAA,CACT,oBAIAC,EAqCWC,EAAAC,CAAA,EAAA,CApCT,QAASJ,EACV,MAAM,wBACN,oBAAA,GACA,gCAAA,GACA,OAAA,GACC,KAAMG,EAAAF,CAAA,EAAC,iBAAA,qBAAA,EACR,cAAA,GACC,SAAMI,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAC,GAAEC,EAAAA,MAAK,QAAA,CAAA,MAAYV,EAAA,eAAOE,EAAA,MAAQ,GACxC,+BAAaQ,EAAAA,MAAK,OAAA,EAAA,aAEnB,IAGe,CAHfC,EAGeL,EAAAM,CAAA,EAAA,CAFd,MAAM,gCACL,KAAMN,EAAAF,CAAA,EAAC,iBAAA,4EAAA,EACR,KAAK,MAAA,mBAGNO,EASYL,EAAAO,CAAA,EAAA,YARFb,EAAA,2CAAAA,EAAK,MAAAS,GACd,UAAA,GACA,MAAM,+BACN,6CAAA,GACC,MAAOH,EAAAF,CAAA,EAAC,iBAAA,OAAA,EACR,YAAaE,EAAAF,CAAA,EAAC,iBAAA,yBAAA,EACf,UAAU,IACV,KAAK,QACL,SAAA,EAAA,+CAGDO,EAOYL,EAAAQ,CAAA,EAAA,YANFZ,EAAA,2CAAAA,EAAQ,MAAAO,GACjB,MAAM,kCACN,gDAAA,GACC,MAAOH,EAAAF,CAAA,EAAC,iBAAA,UAAA,EACR,YAAaE,EAAAF,CAAA,EAAC,iBAAA,4BAAA,EACf,KAAK,WACL,SAAA,EAAA"}
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

Some files were not shown because too many files have changed in this diff Show More