Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67c31584e2 | |||
| d21351701a | |||
| 6c04307e13 | |||
| a4f396e648 | |||
| 615d343d96 | |||
| 83fbc64c99 | |||
| 4a9e04962c | |||
| feaebeb97e | |||
| bc5771b0ff | |||
| 63eb9679c2 | |||
| 98cb8b6155 | |||
| 6e5baa6928 | |||
| a096c89c66 | |||
| 6563214204 | |||
| 2ddf73f89f | |||
| fe9e43c165 | |||
| 1a5679b176 | |||
| 7b7d74fda2 | |||
| f075051f4a | |||
| 0e361550f1 | |||
| 38644873f2 | |||
| 643a815557 | |||
| c73b85aecb | |||
| 4a284f61e6 | |||
| e088473929 | |||
| 29b47c93ab | |||
| 935cd2910f | |||
| 422bca31bf | |||
| adce834b4f | |||
| 7da7f50203 | |||
| 8c01737a63 | |||
| e0c282d531 | |||
| d65aa0b7c3 | |||
| 543b46f3aa | |||
| 99a1150ec2 | |||
| 0469f57a3a |
@@ -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
|
||||
@@ -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;");
|
||||
|
||||
@@ -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;"
|
||||
}
|
||||
@@ -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;");
|
||||
@@ -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;"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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" : "隱藏",
|
||||
|
||||
@@ -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" : "隱藏",
|
||||
|
||||
@@ -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." : "L’accè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 d’assistance technique tels que les lecteurs d’écran. Nous visons à respecter les {linkstart}Règles pour l’accessibilité 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.",
|
||||
|
||||
@@ -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." : "L’accè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 d’assistance technique tels que les lecteurs d’écran. Nous visons à respecter les {linkstart}Règles pour l’accessibilité 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
|
||||
}
|
||||
|
||||
Generated
+92
-270
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" : "找不到影像",
|
||||
|
||||
@@ -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" : "找不到影像",
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Vendored
+4
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+2
-2
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+2
-2
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
Vendored
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -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
|
||||
Vendored
+1
-1
@@ -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"}
|
||||
+5
-5
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
Reference in New Issue
Block a user