fix: #373 missing send file to bbb menu, use vuejs for dialog

Signed-off-by: Sebastien Marinier <sebastien.marinier@arawa.fr>
pull/408/head
Sebastien Marinier 2025-12-19 14:50:51 +01:00
parent c5a0129d9a
commit e248a12ec1
6 changed files with 403 additions and 429 deletions

View File

@ -1,12 +1,15 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
extraFileExtensions: ['.vue'],
},
plugins: [
'vue',
'@typescript-eslint',
],
settings: {
@ -15,6 +18,7 @@ module.exports = {
},
},
extends: [
'plugin:vue/recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
@ -31,5 +35,18 @@ module.exports = {
indent: ['warn', 'tab'],
semi: ['error', 'always'],
'@typescript-eslint/ban-types': 'off',
'vue/script-setup-uses-vars': 'error',
'vue/html-indent': ['warn', 'tab', {
attribute: 1,
baseIndent: 1,
closeBracket: 0,
alignAttributesVertically: true,
ignores: []
}],
'vue/first-attribute-linebreak': 'off',
'vue/max-attributes-per-line': ['warn', {
singleline: 5,
multiline: 1
}]
},
}

481
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,8 @@
"test:php:integration": "dotenv ./vendor/bin/phpunit -- -c phpunit.integration.xml",
"fix": "run-p --continue-on-error --print-label lint:fix:*",
"lint": "run-p --continue-on-error --print-label lint:*",
"lint:script": "eslint --ext .tsx,.ts ts",
"lint:fix:script": "eslint --ext .tsx,.ts ts --fix",
"lint:script": "eslint --ext .tsx,.ts,.vue ts",
"lint:fix:script": "eslint --ext .tsx,.ts,.vue ts --fix",
"lint:style": "stylelint 'ts/**/*.scss'",
"lint:fix:style": "stylelint 'ts/**/*.scss' --fix",
"lint:php": "./vendor/bin/php-cs-fixer fix --dry-run",
@ -41,6 +41,7 @@
"@nextcloud/dialogs": "^6.0.1",
"@nextcloud/files": "^3.12.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.22.0",
"@octokit/rest": "^18.0.4",
"archiver": "^5.0.0",
"colors": "^1.4.0",
@ -52,7 +53,8 @@
"qrcode.react": "^2.0.0",
"react-copy-to-clipboard": "^5.0.2",
"sass": "^1.89.0",
"simple-git": "^3.16.0"
"simple-git": "^3.16.0",
"vue": "^2.7.16"
},
"husky": {
"hooks": {
@ -114,7 +116,7 @@
"stylelint-config-recommended-scss": "^5.0.2",
"stylelint-scss": "^4.2.0",
"ts-loader": "^9.2.8",
"typescript": "^4.9.3",
"typescript": "^5.9.2",
"url-loader": "^4.0.0",
"webpack": "^5.70.0",
"webpack-cli": "^5.1.4",

View File

@ -1,13 +1,17 @@
import axios from '@nextcloud/axios';
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
import { showSuccess, showWarning, showError } from '@nextcloud/dialogs';
// import * as Files from '@nextcloud/files';
import { FileAction, registerFileAction } from '@nextcloud/files';
import { api } from './Common/Api';
import './filelist.scss';
import Vue from 'vue';
import SendFileDialog from './views/SendFileDialog.vue';
import iconBBBInline from '../img/app-dark.svg?raw';
type OC_Dialogs_Message = (content: string, title: string, dialogType: 'notice' | 'alert' | 'warn' | 'none', buttons?: number, callback?: () => void, modal?: boolean, allowHtml?: boolean) => Promise<void>;
type ExtendedDialogs = typeof OC.dialogs & { message: OC_Dialogs_Message };
type NCNode = any;
const mimeTypes = [
const mimeTypes: readonly string[] = [
'application/pdf',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.text',
@ -23,9 +27,7 @@ const mimeTypes = [
'image/png',
'text/plain',
'text/rtf',
] as const;
type MimeTypes = typeof mimeTypes[number];
];
async function createDirectShare(fileId: number): Promise<string> {
const url = generateOcsUrl('apps/dav/api/v1/', undefined, {
@ -54,7 +56,7 @@ function insertDocumentToRoom(shareUrl: string, filename: string, roomUid: strin
return api.insertDocument(roomUid, shareUrl, filename);
}
async function sendFile(fileId: number, filename: string, roomUid: string) {
export async function sendFileToBBB(fileId: number, filename: string, roomUid: string) {
const shareUrl = await createDirectShare(fileId);
const isRunning = await api.isRunning(roomUid);
@ -75,80 +77,58 @@ async function sendFile(fileId: number, filename: string, roomUid: string) {
}
}
async function openDialog(fileId: number, filename: string) {
const initContent = '<div id="bbb-file-action"><span className="icon icon-loading-small icon-visible"></span></div>';
const title = t('bbb', 'Send file to BBB');
/**
* Create a DOM component to mount the dialog Vue component
*
* @param fileId number
* @param filename string
*/
export function showSendFileDialog(fileId: number, filename: string ) {
const mount = document.createElement('div');
mount.id = 'bbb-widget-container';
document.body.appendChild(mount);
const exDialogs = OC.dialogs as ExtendedDialogs;
await exDialogs.message(initContent, title, 'none', -1, undefined, true, true);
const rooms = await api.getRooms();
const container = $('#bbb-file-action').empty();
const table = $('<table>').appendTo(container);
table.attr('style', 'margin-top: 1em; width: 100%;');
for (const room of rooms) {
const row = $('<tr>');
const button = $('<button>');
button.text(room.running ? t('bbb', 'Send to') : t('bbb', 'Start with'));
button.addClass(room.running ? 'success' : 'primary');
button.attr('type', 'button');
button.on('click', (ev) => {
ev.preventDefault();
table.find('button').prop('disabled', true);
$(ev.target).addClass('icon-loading-small');
sendFile(fileId, filename, room.uid).then(() => {
container.parents('.oc-dialog').find('.oc-dialog-close').trigger('click');
});
});
row.append($('<td>').append(button));
row.append($('<td>').attr('style', 'width: 100%;').text(room.name));
row.appendTo(table);
}
if (rooms.length > 0) {
const description = t('bbb', 'Please select the room in which you like to use the file "{filename}".', { filename });
container.append(description);
container.append(table);
} else {
container.append($('p').text(t('bbb', 'No rooms available!')));
}
}
function registerFileAction(fileActions: any, mime: MimeTypes) {
fileActions.registerAction({
name: 'bbb',
displayName: t('bbb', 'Send to BBB'),
mime,
permissions: OC.PERMISSION_SHARE,
icon: OC.imagePath('bbb', 'app-dark.svg'),
actionHandler: (fileName, context) => {
console.log('Action handler');
openDialog(context.fileInfoModel.id, fileName);
},
const vm = new Vue({
el: '#bbb-widget-container',
render: h => h(SendFileDialog, {
props: {
fileId,
filename,
},
on: {
// listen to 'close' event emitted by the dialog component, to clean up
close: () => {
vm.$destroy();
mount.remove();
},
},
}),
});
}
const BBBFileListPlugin = {
ignoreLists: [
'trashbin',
],
attach(fileList) {
if (this.ignoreLists.includes(fileList.id) || !OC.currentUser) {
return;
}
mimeTypes.forEach(mime => registerFileAction(fileList.fileActions, mime));
/**
* Register the file action "Send to BBB"
*/
registerFileAction( new FileAction({
id: 'bbb-send-file',
displayName: () => {
return t('bbb', 'Send to BBB');
},
};
enabled: (nodes) => {
// only files with the mime type allowed
if (!Array.isArray(nodes) || nodes.length === 0) return false;
OC.Plugins.register('OCA.Files.FileList', BBBFileListPlugin);
return nodes.every((node): boolean | null => {
const mime = node.mime;
if (!mime) return false;
// enable only for allowed mime types
return mimeTypes.includes(mime);
});
},
iconSvgInline: () => iconBBBInline,
exec: async (node: NCNode) : Promise<boolean|null> => {
showSendFileDialog(node.fileid, node.displayname);
return null;
},
order: 20,
}));

10
ts/types.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module '*.svg?raw' {
const content: string;
export default content;
}
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

172
ts/views/SendFileDialog.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<NcDialog
dialog-classes="bbb-send-file-dialog"
size="normal"
:visible="opened"
:name="title"
@closing="onClose"
>
<NcLoadingIcon v-if="loading"
:size="32"
:name="t('bbb', 'Loading…')"
appearance="dark"
/>
<div v-else class="send-file__content">
<slot name="body">
<p>
{{ t('bbb', 'Please select the room in which you like to use the file "{filename}".', { filename }) }}
</p>
<p class="note">
{{ t('bbb', 'Only rooms for which you are one of the administrators will be displayed.') }}
</p>
<div class="send-file__content__container">
<table v-if="rooms.length > 0" class="send-file__content__table">
<tr v-for="room in rooms" :key="room.id">
<td>
<NcButton
:variant="room.running ? 'success' : 'primary'"
@click="sendFileToRoom(room.uid)"
>
{{ room.running ? t('bbb', 'Send to') : t('bbb', 'Start with') }}
</NcButton>
</td>
<td>{{ room.name }}</td>
</tr>
</table>
<p v-else>
{{ t('bbb', 'No rooms available!') }}
</p>
</div>
</slot>
</div>
<template #actions>
<button class="nc-btn nc-btn--tertiary" @click="onClose">
{{ t('bbb','Close') }}
</button>
</template>
</NcDialog>
</template>
<script>
import NcDialog from '@nextcloud/vue/components/NcDialog';
import NcButton from '@nextcloud/vue/components/NcButton';
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon';
import { showError } from '@nextcloud/dialogs';
import { translate as t } from '@nextcloud/l10n';
import { api, Permission } from '../Common/Api';
import { sendFileToBBB } from '../filelist';
export default {
name: 'SendFileDialog',
components: {
NcDialog,
NcLoadingIcon,
NcButton,
},
props: {
filename: { type: String, required: true },
fileId: { type: Number, required: true },
title: { type: String, default: t('bbb', 'Send to BBB') },
},
data() {
return {
opened: false,
loading: true,
rooms: [],
};
},
created() {
api.getRooms().then((rooms) => {
this.rooms = rooms.filter(room => room.permission === Permission.Admin);
this.loading = false;
});
},
methods: {
t,
onClose() {
this.opened = false;
this.$emit('close');
},
/*
La confirmation déclenche un événement 'confirm'.
Si la logique d'envoi est asynchrone côté parent, le parent peut gérer le flag `modelValue`/un loader.
Ici on active un loader visuel court pour l'UX puis on ferme la dialog.
*/
onConfirm() {
this.loading = true;
try {
this.$emit('confirm');
} finally {
// fermeture et reset loader (simulé court délai si nécessaire)
this.loading = false;
this.$emit('update:modelValue', false);
}
},
sendFileToRoom(roomUid) {
this.loading = true;
sendFileToBBB(this.fileId, this.filename, roomUid).then(() => {
this.loading = false;
this.$emit('sent', roomUid);
this.onClose();
}).catch(() => {
this.loading = false;
showError(t('bbb', 'An error occurred while sending the file to the room.'));
});
},
},
};
</script>
<style scoped lang="scss">
.send-file__title {
margin: 0;
}
.send-file__info {
margin: 0 0 0.5rem 0;
word-break: break-all;
}
.nc-btn {
min-width: 96px;
}
.send-file__content {
margin-top: 1rem;
&__table {
width: 100%;
margin-top: 1em;
td {
padding: 0.2rem;
}
}
&__container {
width: 80%;
margin-right: auto;
margin-left: auto;
max-height: 400px;
overflow-y: scroll auto;
}
}
button.success {
background-color: var(--color-success);
border-color: var(--color-success-hover);
color: var(--color-primary-text);
&:hover {
background-color: var(--color-success-hover);
}
}
.bbb-send-file-dialog {
min-width: 400px;
}
.send-file__content .note {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--color-secondary-text);
font-style: italic;
}
</style>