Merge pull request #39196 from nextcloud/feat/f2v/sharing

This commit is contained in:
John Molakvoæ 2023-07-11 15:06:03 +02:00 committed by GitHub
commit 2cf8d6d965
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2229 additions and 1330 deletions

View file

@ -20,6 +20,7 @@
*
*/
export default {
get: async () => ({ status: 200, data: {} }),
delete: async () => ({ status: 200, data: {} }),
post: async () => ({ status: 200, data: {} }),
}

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["files.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAWA,SAEC,YACA,YACA,qBACA,WAED,oEACA,8BACA,kDAEC,+CAED,0BACC,oDAGD,mBACC,kBACA,aACA,SACA,4CACC,iBAIF,gBACC,aAGD,OACC,iBACA,YACA,aACA,aACA,mBAGD,6EAGC,yBACA,gCAID,kBACC,kBACA,WACA,gBACA,cACA,sBAEA,6CACC,aAGD,wBACC,wBACA,gBAEA,SAEA,WACA,cACA,0DAMD,wBACC,cACA,WAEA,mGAEC,8CAEA,6KACC,oCAKF,8DACC,oBAKH,yBACC,aAID,uCACC,cACA,WAGD,wBAGC,yBAEA,qBAGD,6FACC,+DAGD,iCACC,yDAGD,kFACC,0CAGD,4EACC,+DAID,gBCxEC,yCD2ED,iBC3EC,yCD8ED,oBC9EC,0CDiFD,yFCjFC,wCDuFD,uBCvFC,yCD0FD,2BC1FC,2CD6FD,mBC7FC,yCDgGD,2BChGC,4CDmGD,wBCnGC,0CDsGD,4BCtGC,4CD0GD,4CACC,WAGD,iCACC,WACA,YACA,eACA,SACA,eAGD,wCACC,aAGD,0CACC,WAGD,2BACC,YAED,4KAKC,+CAED,wMAKC,oDAGD,qCAEA,yDACC,oCAED,kCACC,iCACA,8BACA,4BACA,yBACA,mBAED,wGAIC,UACA,oCAGD,oBACC,oCAED,uBACC,6BAED,sBACC,cACA,aACA,YACA,sBACA,2BACA,sBACA,oCACC,kBAGF,kCACC,qBACA,mBAED,2BACC,eACA,iBAGD,uCACC,cAGD,yBACC,WACA,WACA,gBACA,qBACA,2BACA,WAED,wJAIC,kBAED,2CACC,eAED,4EAEC,mBAGD,kBAEC,4CACA,gBACA,mBAED,SACC,eACA,kBACA,+BACA,4BAED,qBACC,kBACA,aACA,UAGD,uBACC,kBACA,YAGD,0BACC,gBAED,uCACC,iBAED,8EAEC,2BACA,sBACA,kBAEA,gBAGD,qMAQC,gBACA,qPACC,MAIF,2BACC,0DACA,iBAGD,sDACC,iBAGD,+BACC,kBACA,aAED,kCACC,aAGD,0DAGC,WACA,kBAED,kDAEC,aACA,kBACA,2BACA,sBACA,YACA,iBACA,UAED,qCAEC,QACA,eACA,eACA,YAGA,8DACC,WAED,mEACC,WAGF,6BACC,qBACA,WACA,YACA,wBACA,2BACA,4BACA,gBACA,eACA,mCACA,eACA,kBACA,UAED,oCACC,eAID,2CACC,qCAGD,iDACC,qBACA,4BACA,YAED,uBACC,iBACA,kBACA,SAGD,6IACA,8FAEA,wCACC,kBACA,gBACA,uBACA,YAKA,kBACC,YACA,4BACC,QACA,YACA,aACA,gBACA,mBACA,uBACA,YACA,WACA,mBAID,+BACC,iBACA,aACA,uBACA,mBACA,kCACA,gBAEA,iDACC,iBACA,iBACA,wCACA,iCACA,oCACA,uBACA,mBACA,gBACA,uBACA,iBACA,kBAEA,uDACC,iBACA,sBAID,mEACC,gBAOL,iJAEC,wBAGD,mCACC,iCACA,8BACA,4BACA,yBAED,4BACC,WAGD,2CACC,uBACA,gBACA,kBACA,mBAKD,8BACC,kBACA,mBAEA,iBACA,OACA,SACA,YACA,cAEA,iBACA,eAEA,iBACA,oCACA,uBACA,mBAGD,mBACC,UAID,6DACC,WACA,eAID,iRAIC,UAID,0EACC,WAMA,wEACC,aAGD,oGACC,+CACA,wCACA,wBACA,yDACA,aAIF,oGAEC,mBAGD,+BACC,kBACA,WACA,eACA,gBACA,wJAGD,wFAEC,kBACA,UACA,YAGD,yCACC,qBACA,WAED,8CACC,kBACA,cACA,SACA,WACA,iBACA,kBACA,wDAEC,8CACA,8CACA,oBAEA,WACA,YACA,aACA,qBACA,uBAGF,8DACC,+CAGD,iDAGA,aACC,WAGD,iCACC,kBAID,mDAEC,gBAID,oCACC,qBACA,0BAGD,8EACC,0BAOA,kCACC,eAGD,sEACC,eAGD,sCACC,gBAIF,aACC,YACA,WACA,2BAGD,qCACC,wCAID,iBACI,kBACA,qBACA,sBAEJ,wBACI,aAEJ,mBACC,eACA,iBACA,iBAGD,0BACC,aAED,uBACC,kBACA,2BACA,mBAGD,8CACC,gBAIA,8BACC,eACA,iBACA,iBACA,WACA,2CACC,kBACA,0FAGC,kBACA,cACA,SACA,UACA,WACA,gBAED,mDACC,qBACA,sBAGF,0CACC,iBACA,oBACA,kBACA,mBAGA,oGACC,WAID,qIAEC,WAED,uDACC,WACA,0HACC,WAIH,wEACC,UAED,oCACC,+CACA,wCAGF,uGACC,WAED,wDACC,UAKF,4EACC,qBACA,eACA,gBACA,uBACA,sBACA,gBAGD,2CACC,yBAGD,yCACC,UAGD,kNAKC,UAGD,qCACC,gBAGD,0FAEC,WAGD,mDACC,eAGD,SACC,oCAGA,aAED,wCACC,WAEA,mBAKD,sBACC,aAED,2DAIC,+BAED,YACC,mBACA,mBACA,iBAED,wBACC,UAED,YACC,qBAGD,iBACC,WACA,aAED,6BACC,kBACA,mBACA,YAGA,gBAED,yBACC,kBAED,MACC,WACA,kBACA,MACA,OACA,QACA,SACA,8CACA,sCACA,wBACA,WACA,yBACA,8BACA,4BACA,6BACA,iCAED,kBACC,UAGD,aACC,gBACA,SACA,sBACA,eACA,gBACA,aAGA,oBACC,qBAKF,gBACC,sBACA,wBACA,gBACA,YACA,UACA,SACA,0DACA,WACA,yBACA,sBACA,qBACA,iBACA,aACA,MACA,kBAKE,0IACC,sBACA,qBACA,aACA,YACA,WACA,YACA,mBACA,uBAED,oFACC,aAQJ,0DACC,OAGD,6KAIC,qBACA,sBACA,0BAMA,sDACC,sBAED,yDACC,uDAIF,iJAGC,aAGD,oJAGC,WACA,YAGD,gCACC,kBAGD,YACC,mBAEA,uBACC,mCAIF,0DAEC,oCAED,qBACC,oCACA,4BACC,2BAIF,cACC,iBACA,kBACA,gBACA,6BACA,cACA,gBACA,YAEA,2BACC,aAGD,kCACC,UACA,kBACA,iBAIF,uBACC,oBACA,YACA,gBACA,+BACA,UACA,YACA,wBACA,sBAEA,6BACC,YAKA,oEACC,0BAIF,kCACC,WACA,mCAWA,kDACC,cACA,4CACA,0DACA,qDACC,WACA,YAMH,+CACC,aACA,+CACA,6BACA,aACA,cAGA,+DACC,cACA,kBACA,aACA,mCAEA,0fAKC,+BAEA,oxDAGC,+CAKH,kDACC,eACA,mBAGC,8EACC,YACA,eACA,kBACA,MAvDQ,MAwDR,OAxDQ,MAyDR,QAxDO,KAyDP,MACA,OACA,WAEA,yFACC,4BACA,6BACA,wBACA,SACA,mCACA,4BACA,2BAKA,wGACC,UACA,UACA,YAKH,uEACC,WACA,SACA,MACA,YAEA,YACA,gBAEA,kBAGD,iEACC,YACA,mCAIA,gBAKA,0BAEA,2EACC,aACA,YACA,iBACA,kBACA,iBACA,UAEA,0FACC,qBACA,kBACA,gBACA,uBACA,mBAED,kFACC,WACA,OACA,eAED,iFACC,WACA,OACA,eAID,sFACC,aAKF,8EACC,aAGD,8EACC,eACA,iBACA,aACA,mBACA,kBACA,QAEA,sFACC,QAxJK,KAyJL,WACA,YACA,aACA,mBACA,uBAGA,wGACC,aAQH,2GACC,yBAEA,6HACC,YACA,kBAIF,6GACC,yBAGD,6GACC,yBAIF,gEACC,iBACA,mCAEA,+EACC,WACA,cACA,YAMH,kHAEC,aAGD,sIAEC,kBACA,SACA,UACA,aACA,WAEA,kJACC,WACA,YACA,oBACA,QAzNO,KA0NP,kKACC,SACA,MA5NM,KA6NN,OA7NM,KAmOT,+DACC,OACA,YACA,aAGA,yFACC,gBACA,uBAMJ,+FACC,cAID,+CACC,aAEA,qEACC,qBACA,cAEA,aAEA,wEACC,iBAEA,iKAEC,aAGD,8EACI,cAQR,aACC,0DACA,YACA,SACA,aACA,WACA,YACA,mCACA,iCACA,YACA,gBAEA,uEAGC,UAGD,oEAEC,mEASF,cACC,eACA,MAOC,uGACC,gBAID,4EACC,WAKF,0BACC,kBACA,QACA,MAKF,gBACC,aAGD,8BACC,gBACA,sBACA,kBACA,kBACA,aACA,eACA,mBAEA,iCACC,WACA,eAGD,6DACC,aACA,YACA","file":"files.css"}
{"version":3,"sourceRoot":"","sources":["files.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAWA,SAEC,YACA,YACA,qBACA,WAED,oEACA,8BACA,kDAEC,+CAED,0BACC,oDAGD,mBACC,kBACA,aACA,SACA,4CACC,iBAIF,gBACC,aAGD,OACC,iBACA,YACA,aACA,aACA,mBAGD,6EAGC,yBACA,gCAID,kBACC,kBACA,WACA,gBACA,cACA,sBAEA,6CACC,aAGD,wBACC,wBACA,gBAEA,SAEA,WACA,cACA,0DAMD,wBACC,cACA,WAEA,mGAEC,8CAEA,6KACC,oCAKF,8DACC,oBAKH,yBACC,aAID,uCACC,cACA,WAGD,wBAGC,yBAEA,qBAGD,6FACC,+DAGD,iCACC,yDAGD,kFACC,0CAGD,4EACC,+DAID,gBCxEC,yCD2ED,iBC3EC,yCD8ED,oBC9EC,0CDiFD,qGCjFC,wCDuFD,0BCvFC,yCD0FD,2BC1FC,2CD6FD,mBC7FC,yCDgGD,2BChGC,4CDmGD,2BCnGC,0CDsGD,4BCtGC,4CD0GD,4CACC,WAGD,iCACC,WACA,YACA,eACA,SACA,eAGD,wCACC,aAGD,0CACC,WAGD,2BACC,YAED,4KAKC,+CAED,wMAKC,oDAGD,qCAEA,yDACC,oCAED,kCACC,iCACA,8BACA,4BACA,yBACA,mBAED,wGAIC,UACA,oCAGD,oBACC,oCAED,uBACC,6BAED,sBACC,cACA,aACA,YACA,sBACA,2BACA,sBACA,oCACC,kBAGF,kCACC,qBACA,mBAED,2BACC,eACA,iBAGD,uCACC,cAGD,yBACC,WACA,WACA,gBACA,qBACA,2BACA,WAED,wJAIC,kBAED,2CACC,eAED,4EAEC,mBAGD,kBAEC,4CACA,gBACA,mBAED,SACC,eACA,kBACA,+BACA,4BAED,qBACC,kBACA,aACA,UAGD,uBACC,kBACA,YAGD,0BACC,gBAED,uCACC,iBAED,8EAEC,2BACA,sBACA,kBAEA,gBAGD,qMAQC,gBACA,qPACC,MAIF,2BACC,0DACA,iBAGD,sDACC,iBAGD,+BACC,kBACA,aAED,kCACC,aAGD,0DAGC,WACA,kBAED,kDAEC,aACA,kBACA,2BACA,sBACA,YACA,iBACA,UAED,qCAEC,QACA,eACA,eACA,YAGA,8DACC,WAED,mEACC,WAGF,6BACC,qBACA,WACA,YACA,wBACA,2BACA,4BACA,gBACA,eACA,mCACA,eACA,kBACA,UAED,oCACC,eAID,2CACC,qCAGD,iDACC,qBACA,4BACA,YAED,uBACC,iBACA,kBACA,SAGD,6IACA,8FAEA,wCACC,kBACA,gBACA,uBACA,YAKA,kBACC,YACA,4BACC,QACA,YACA,aACA,gBACA,mBACA,uBACA,YACA,WACA,mBAID,+BACC,iBACA,aACA,uBACA,mBACA,kCACA,gBAEA,iDACC,iBACA,iBACA,wCACA,iCACA,oCACA,uBACA,mBACA,gBACA,uBACA,iBACA,kBAEA,uDACC,iBACA,sBAID,mEACC,gBAOL,iJAEC,wBAGD,mCACC,iCACA,8BACA,4BACA,yBAED,4BACC,WAGD,2CACC,uBACA,gBACA,kBACA,mBAKD,8BACC,kBACA,mBAEA,iBACA,OACA,SACA,YACA,cAEA,iBACA,eAEA,iBACA,oCACA,uBACA,mBAGD,mBACC,UAID,6DACC,WACA,eAID,iRAIC,UAID,0EACC,WAMA,wEACC,aAGD,oGACC,+CACA,wCACA,wBACA,yDACA,aAIF,oGAEC,mBAGD,+BACC,kBACA,WACA,eACA,gBACA,wJAGD,wFAEC,kBACA,UACA,YAGD,yCACC,qBACA,WAED,8CACC,kBACA,cACA,SACA,WACA,iBACA,kBACA,wDAEC,8CACA,8CACA,oBAEA,WACA,YACA,aACA,qBACA,uBAGF,8DACC,+CAGD,iDAGA,aACC,WAGD,iCACC,kBAID,mDAEC,gBAID,oCACC,qBACA,0BAGD,8EACC,0BAOA,kCACC,eAGD,sEACC,eAGD,sCACC,gBAIF,aACC,YACA,WACA,2BAGD,qCACC,wCAID,iBACI,kBACA,qBACA,sBAEJ,wBACI,aAEJ,mBACC,eACA,iBACA,iBAGD,0BACC,aAED,uBACC,kBACA,2BACA,mBAGD,8CACC,gBAIA,8BACC,eACA,iBACA,iBACA,WACA,2CACC,kBACA,0FAGC,kBACA,cACA,SACA,UACA,WACA,gBAED,mDACC,qBACA,sBAGF,0CACC,iBACA,oBACA,kBACA,mBAGA,oGACC,WAID,qIAEC,WAED,uDACC,WACA,0HACC,WAIH,wEACC,UAED,oCACC,+CACA,wCAGF,uGACC,WAED,wDACC,UAKF,4EACC,qBACA,eACA,gBACA,uBACA,sBACA,gBAGD,2CACC,yBAGD,yCACC,UAGD,kNAKC,UAGD,qCACC,gBAGD,0FAEC,WAGD,mDACC,eAGD,SACC,oCAGA,aAED,wCACC,WAEA,mBAKD,sBACC,aAED,2DAIC,+BAED,YACC,mBACA,mBACA,iBAED,wBACC,UAED,YACC,qBAGD,iBACC,WACA,aAED,6BACC,kBACA,mBACA,YAGA,gBAED,yBACC,kBAED,MACC,WACA,kBACA,MACA,OACA,QACA,SACA,8CACA,sCACA,wBACA,WACA,yBACA,8BACA,4BACA,6BACA,iCAED,kBACC,UAGD,aACC,gBACA,SACA,sBACA,eACA,gBACA,aAGA,oBACC,qBAKF,gBACC,sBACA,wBACA,gBACA,YACA,UACA,SACA,0DACA,WACA,yBACA,sBACA,qBACA,iBACA,aACA,MACA,kBAKE,0IACC,sBACA,qBACA,aACA,YACA,WACA,YACA,mBACA,uBAED,oFACC,aAQJ,0DACC,OAGD,6KAIC,qBACA,sBACA,0BAMA,sDACC,sBAED,yDACC,uDAIF,iJAGC,aAGD,oJAGC,WACA,YAGD,gCACC,kBAGD,YACC,mBAEA,uBACC,mCAIF,0DAEC,oCAED,qBACC,oCACA,4BACC,2BAIF,cACC,iBACA,kBACA,gBACA,6BACA,cACA,gBACA,YAEA,2BACC,aAGD,kCACC,UACA,kBACA,iBAIF,uBACC,oBACA,YACA,gBACA,+BACA,UACA,YACA,wBACA,sBAEA,6BACC,YAKA,oEACC,0BAIF,kCACC,WACA,mCAWA,kDACC,cACA,4CACA,0DACA,qDACC,WACA,YAMH,+CACC,aACA,+CACA,6BACA,aACA,cAGA,+DACC,cACA,kBACA,aACA,mCAEA,0fAKC,+BAEA,oxDAGC,+CAKH,kDACC,eACA,mBAGC,8EACC,YACA,eACA,kBACA,MAvDQ,MAwDR,OAxDQ,MAyDR,QAxDO,KAyDP,MACA,OACA,WAEA,yFACC,4BACA,6BACA,wBACA,SACA,mCACA,4BACA,2BAKA,wGACC,UACA,UACA,YAKH,uEACC,WACA,SACA,MACA,YAEA,YACA,gBAEA,kBAGD,iEACC,YACA,mCAIA,gBAKA,0BAEA,2EACC,aACA,YACA,iBACA,kBACA,iBACA,UAEA,0FACC,qBACA,kBACA,gBACA,uBACA,mBAED,kFACC,WACA,OACA,eAED,iFACC,WACA,OACA,eAID,sFACC,aAKF,8EACC,aAGD,8EACC,eACA,iBACA,aACA,mBACA,kBACA,QAEA,sFACC,QAxJK,KAyJL,WACA,YACA,aACA,mBACA,uBAGA,wGACC,aAQH,2GACC,yBAEA,6HACC,YACA,kBAIF,6GACC,yBAGD,6GACC,yBAIF,gEACC,iBACA,mCAEA,+EACC,WACA,cACA,YAMH,kHAEC,aAGD,sIAEC,kBACA,SACA,UACA,aACA,WAEA,kJACC,WACA,YACA,oBACA,QAzNO,KA0NP,kKACC,SACA,MA5NM,KA6NN,OA7NM,KAmOT,+DACC,OACA,YACA,aAGA,yFACC,gBACA,uBAMJ,+FACC,cAID,+CACC,aAEA,qEACC,qBACA,cAEA,aAEA,wEACC,iBAEA,iKAEC,aAGD,8EACI,cAQR,aACC,0DACA,YACA,SACA,aACA,WACA,YACA,mCACA,iCACA,YACA,gBAEA,uEAGC,UAGD,oEAEC,mEASF,cACC,eACA,MAOC,uGACC,gBAID,4EACC,WAKF,0BACC,kBACA,QACA,MAKF,gBACC,aAGD,8BACC,gBACA,sBACA,kBACA,kBACA,aACA,eACA,mBAEA,iCACC,WACA,eAGD,6DACC,aACA,YACA","file":"files.css"}

View file

@ -144,13 +144,13 @@
.nav-icon-favorites {
@include icon-color('starred', 'actions', variables.$color-black, 2, true);
}
.nav-icon-sharingin,
.nav-icon-sharingout,
.nav-icon-pendingshares,
.nav-icon-shareoverview {
.nav-icon-sharinginOld,
.nav-icon-sharingoutOld,
.nav-icon-pendingsharesOld,
.nav-icon-shareoverviewOld {
@include icon-color('share', 'files', variables.$color-black);
}
.nav-icon-sharinglinks {
.nav-icon-sharinglinksOld {
@include icon-color('public', 'files', variables.$color-black);
}
.nav-icon-extstoragemounts {
@ -162,7 +162,7 @@
.nav-icon-trashbin-starred {
@include icon-color('delete', 'files', #ff0000);
}
.nav-icon-deletedshares {
.nav-icon-deletedsharesOld {
@include icon-color('unshare', 'files', variables.$color-black);
}
.nav-icon-favorites-starred {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -111,6 +111,7 @@
var fileInfo = context.fileList.files[$file.index()];
var dir = context.dir || context.fileList.getCurrentDirectory();
var tags = $file.attr('data-tags');
var isFile = $file.attr('data-type') === 'file';
if (_.isUndefined(tags)) {
tags = '';
@ -121,7 +122,7 @@
// Fake Node object for vue compatibility
const node = {
type: 'folder',
type: isFile ? 'file' : 'folder',
path: (dir + '/' + fileName).replace(/\/\/+/g, '/'),
root: '/files/' + OC.getCurrentUser().uid
}

View file

@ -19,12 +19,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, FileType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
import { registerFileAction, FileAction } from '../services/FileAction'
import { registerFileAction, FileAction, DefaultType } from '../services/FileAction'
import { generateUrl } from '@nextcloud/router'
import type { Navigation } from '../services/Navigation'

View file

@ -22,7 +22,7 @@
import * as favoriteAction from './favoriteAction'
import { action } from './favoriteAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission } from '@nextcloud/files'
import { File, Permission } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
@ -120,6 +120,7 @@ describe('Favorite action enabled tests', () => {
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()

View file

@ -20,13 +20,15 @@
*
*/
import { emit } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router'
import { Permission, type Node } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import StarSvg from '@mdi/svg/svg/star.svg?raw'
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
import type { Node } from '@nextcloud/files'
import Vue from 'vue'
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
import StarSvg from '@mdi/svg/svg/star.svg?raw'
import { generateUrl } from '@nextcloud/router'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
import type { Navigation } from '../services/Navigation'
@ -54,7 +56,7 @@ export const favoriteNode = async (node: Node, view: Navigation, willFavorite: b
}
// Update the node webdav attribute
node.attributes.favorite = willFavorite ? 1 : 0
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
// Dispatch event to whoever is interested
if (willFavorite) {
@ -85,8 +87,9 @@ export const action = new FileAction({
},
enabled(nodes: Node[]) {
// We can only favorite nodes within files
// We can only favorite nodes within files and with permissions
return !nodes.some(node => !node.root?.startsWith?.('/files'))
&& nodes.every(node => node.permissions !== Permission.NONE)
},
async exec(node: Node, view: Navigation) {

View file

@ -21,7 +21,7 @@
*/
import { action } from './sidebarAction'
import { expect } from '@jest/globals'
import { File } from '@nextcloud/files'
import { File, Permission } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import type { Navigation } from '../services/Navigation'
import logger from '../logger'
@ -51,12 +51,29 @@ describe('Open sidebar action enabled tests', () => {
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true)
})
test('Disabled without permissions', () => {
window.OCA = { Files: { Sidebar: {} } }
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled if more than one node', () => {
window.OCA = { Files: { Sidebar: {} } }

View file

@ -21,9 +21,9 @@
*/
import { translate as t } from '@nextcloud/l10n'
import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'
import type { Node } from '@nextcloud/files'
import { Permission, type Node } from '@nextcloud/files'
import { registerFileAction, FileAction, DefaultType } from '../services/FileAction'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
export const ACTION_DETAILS = 'details'
@ -45,7 +45,7 @@ export const action = new FileAction({
return false
}
return nodes[0].root?.startsWith('/files/') ?? false
return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false
},
async exec(node: Node) {

View file

@ -48,12 +48,26 @@ describe('View in folder action enabled tests', () => {
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true)
})
test('Disabled without permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled for non-dav ressources', () => {
const file = new File({
id: 1,
@ -107,13 +121,14 @@ describe('View in folder action execute tests', () => {
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const exec = await action.exec(file, view, '/')
// Silent action
expect(exec).toBe(null)
expect(goToRouteMock).toBeCalledTimes(1)
expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/' })
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/' })
})
test('View in (sub) folder', async () => {
@ -126,13 +141,14 @@ describe('View in folder action execute tests', () => {
root: '/files/admin',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const exec = await action.exec(file, view, '/')
// Silent action
expect(exec).toBe(null)
expect(goToRouteMock).toBeCalledTimes(1)
expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/Foo/Bar' })
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar' })
})
test('View in folder fails without node', async () => {

View file

@ -19,7 +19,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { Node, FileType } from '@nextcloud/files'
import { Node, FileType, Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw'
@ -46,6 +46,10 @@ export const action = new FileAction({
return false
}
if (node.permissions === Permission.NONE) {
return false
}
return node.type === FileType.File
},

View file

@ -150,7 +150,7 @@
<script lang='ts'>
import { debounce } from 'debounce'
import { emit } from '@nextcloud/event-bus'
import { formatFileSize } from '@nextcloud/files'
import { formatFileSize, Permission } from '@nextcloud/files'
import { Fragment } from 'vue-frag'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate } from '@nextcloud/l10n'
@ -166,6 +166,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import StarIcon from 'vue-material-design-icons/Star.vue'
import Vue from 'vue'
import type moment from 'moment'
import { ACTION_DETAILS } from '../actions/sidebarAction.ts'
import { getFileActions, DefaultType } from '../services/FileAction.ts'
@ -173,7 +174,6 @@ import { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useFilesStore } from '../store/files.ts'
import type moment from 'moment'
import { useKeyboardStore } from '../store/keyboard.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
@ -336,11 +336,16 @@ export default Vue.extend({
}
}
if (this.source?.permissions & Permission.READ) {
return {
download: this.source.basename,
href: this.source.source,
title: this.t('files', 'Download file {name}', { name: this.displayName }),
}
}
return {
download: this.source.basename,
href: this.source.source,
// TODO: Use first action title ?
title: this.t('files', 'Download file {name}', { name: this.displayName }),
is: 'span',
}
},
@ -398,7 +403,15 @@ export default Vue.extend({
// Actions shown in the menu
enabledMenuActions() {
return this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN)
return [
// Showing inline first for the NcActions inline prop
...this.enabledInlineActions,
// Then the rest
...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN),
].filter((value, index, self) => {
// Then we filter duplicates to prevent inline actions to be shown twice
return index === self.findIndex(action => action.id === value.id)
})
},
openedMenu: {
get() {
@ -602,7 +615,7 @@ export default Vue.extend({
},
openDetailsIfAvailable(event) {
const detailsAction = this.enabledDefaultActions.find(action => action.id === ACTION_DETAILS)
const detailsAction = this.enabledActions.find(action => action.id === ACTION_DETAILS)
if (detailsAction) {
event.preventDefault()
event.stopPropagation()

View file

@ -322,6 +322,7 @@ export default Vue.extend({
}
.files-list__row-name-text {
color: var(--color-main-text);
// Make some space for the outline
padding: 5px 10px;
margin-left: -10px;

View file

@ -58,7 +58,7 @@ export const useFilesStore = function(...args) {
// Update the store all at once
const files = nodes.reduce((acc, node) => {
if (!node.fileid) {
logger.warn('Trying to update/set a node without fileid', node)
logger.error('Trying to update/set a node without fileid', node)
return acc
}
acc[node.fileid] = node

View file

@ -129,9 +129,16 @@ export default () => {
// Add a folder to the favorites paths array and update the views
const addPathToFavorites = function(path: string) {
const view = generateFolderView(path)
// Skip if already exists
if (favoriteFolders.find(folder => folder === path)) {
return
}
// Update arrays
favoriteFolders.push(path)
favoriteFoldersViews.push(view)
// Update and sort views
updateAndSortViews()
Navigation.register(view)
@ -140,10 +147,18 @@ export default () => {
// Remove a folder from the favorites paths array and update the views
const removePathFromFavorites = function(path: string) {
const id = generateIdFromPath(path)
const index = favoriteFolders.findIndex(f => f === path)
const index = favoriteFolders.findIndex(folder => folder === path)
// Skip if not exists
if (index === -1) {
return
}
// Update arrays
favoriteFolders.splice(index, 1)
favoriteFoldersViews.splice(index, 1)
// Update and sort views
Navigation.remove(id)
updateAndSortViews()
}

View file

@ -212,56 +212,6 @@ class ViewControllerTest extends TestCase {
'expanded' => false,
'unread' => 0,
],
'shareoverview' => [
'id' => 'shareoverview',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 18,
'name' => \OC::$server->getL10N('files_sharing')->t('Shares'),
'classes' => 'collapsible',
'sublist' => [
[
'id' => 'sharingout',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 16,
'name' => \OC::$server->getL10N('files_sharing')->t('Shared with others'),
],
[
'id' => 'sharingin',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 15,
'name' => \OC::$server->getL10N('files_sharing')->t('Shared with you'),
],
[
'id' => 'sharinglinks',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 17,
'name' => \OC::$server->getL10N('files_sharing')->t('Shared by link', []),
],
[
'id' => 'deletedshares',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 19,
'name' => \OC::$server->getL10N('files_sharing')->t('Deleted shares'),
],
[
'id' => 'pendingshares',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 19,
'name' => \OC::$server->getL10N('files_sharing')->t('Pending shares'),
],
],
'active' => false,
'icon' => '',
'type' => 'link',
'expanded' => false,
'unread' => 0,
]
]);
$expected = new Http\TemplateResponse(
@ -292,30 +242,6 @@ class ViewControllerTest extends TestCase {
'id' => 'systemtagsfilter',
'content' => null,
],
'sharingout' => [
'id' => 'sharingout',
'content' => null,
],
'sharingin' => [
'id' => 'sharingin',
'content' => null,
],
'sharinglinks' => [
'id' => 'sharinglinks',
'content' => null,
],
'deletedshares' => [
'id' => 'deletedshares',
'content' => null,
],
'pendingshares' => [
'id' => 'pendingshares',
'content' => null
],
'shareoverview' => [
'id' => 'shareoverview',
'content' => null,
],
],
'hiddenFields' => [],
'showgridview' => null

View file

@ -1,409 +0,0 @@
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
if (!OCA.Sharing) {
/**
* @namespace OCA.Sharing
*/
OCA.Sharing = {}
}
/**
* @namespace
*/
OCA.Sharing.App = {
_inFileList: null,
_outFileList: null,
_overviewFileList: null,
_pendingFileList: null,
initSharingIn($el) {
if (this._inFileList) {
return this._inFileList
}
this._inFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.self',
sharedWithUser: true,
fileActions: this._createFileActions(),
config: OCA.Files.App.getFilesConfig(),
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._inFileList)
this._inFileList.appName = t('files_sharing', 'Shared with you')
this._inFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-shared"></div>'
+ '<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>'
+ '<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>')
return this._inFileList
},
initSharingOut($el) {
if (this._outFileList) {
return this._outFileList
}
this._outFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.others',
sharedWithUser: false,
fileActions: this._createFileActions(),
config: OCA.Files.App.getFilesConfig(),
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._outFileList)
this._outFileList.appName = t('files_sharing', 'Shared with others')
this._outFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-shared"></div>'
+ '<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>'
+ '<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>')
return this._outFileList
},
initSharingLinks($el) {
if (this._linkFileList) {
return this._linkFileList
}
this._linkFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.link',
linksOnly: true,
fileActions: this._createFileActions(),
config: OCA.Files.App.getFilesConfig(),
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._linkFileList)
this._linkFileList.appName = t('files_sharing', 'Shared by link')
this._linkFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-public"></div>'
+ '<h2>' + t('files_sharing', 'No shared links') + '</h2>'
+ '<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>')
return this._linkFileList
},
initSharingDeleted($el) {
if (this._deletedFileList) {
return this._deletedFileList
}
this._deletedFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.deleted',
defaultFileActionsDisabled: true,
showDeleted: true,
sharedWithUser: true,
fileActions: this._restoreShareAction(),
config: OCA.Files.App.getFilesConfig(),
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._deletedFileList)
this._deletedFileList.appName = t('files_sharing', 'Deleted shares')
this._deletedFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-share"></div>'
+ '<h2>' + t('files_sharing', 'No deleted shares') + '</h2>'
+ '<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>')
return this._deletedFileList
},
initSharingPening($el) {
if (this._pendingFileList) {
return this._pendingFileList
}
this._pendingFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.pending',
showPending: true,
detailsViewEnabled: false,
defaultFileActionsDisabled: true,
sharedWithUser: true,
fileActions: this._acceptShareAction(),
config: OCA.Files.App.getFilesConfig(),
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._pendingFileList)
this._pendingFileList.appName = t('files_sharing', 'Pending shares')
this._pendingFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-share"></div>'
+ '<h2>' + t('files_sharing', 'No pending shares') + '</h2>'
+ '<p>' + t('files_sharing', 'Shares you have received but not confirmed will show up here') + '</p>')
return this._pendingFileList
},
initShareingOverview($el) {
if (this._overviewFileList) {
return this._overviewFileList
}
this._overviewFileList = new OCA.Sharing.FileList(
$el,
{
id: 'shares.overview',
fileActions: this._createFileActions(),
config: OCA.Files.App.getFilesConfig(),
isOverview: true,
// The file list is created when a "show" event is handled, so
// it should be marked as "shown" like it would have been done
// if handling the event with the file list already created.
shown: true,
}
)
this._extendFileList(this._overviewFileList)
this._overviewFileList.appName = t('files_sharing', 'Shares')
this._overviewFileList.$el.find('.emptyfilelist.emptycontent').html('<div class="icon-share"></div>'
+ '<h2>' + t('files_sharing', 'No shares') + '</h2>'
+ '<p>' + t('files_sharing', 'Shares will show up here') + '</p>')
return this._overviewFileList
},
removeSharingIn() {
if (this._inFileList) {
this._inFileList.$fileList.empty()
}
},
removeSharingOut() {
if (this._outFileList) {
this._outFileList.$fileList.empty()
}
},
removeSharingLinks() {
if (this._linkFileList) {
this._linkFileList.$fileList.empty()
}
},
removeSharingDeleted() {
if (this._deletedFileList) {
this._deletedFileList.$fileList.empty()
}
},
removeSharingPending() {
if (this._pendingFileList) {
this._pendingFileList.$fileList.empty()
}
},
removeSharingOverview() {
if (this._overviewFileList) {
this._overviewFileList.$fileList.empty()
}
},
/**
* Destroy the app
*/
destroy() {
OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated)
OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated)
this.removeSharingIn()
this.removeSharingOut()
this.removeSharingLinks()
this._inFileList = null
this._outFileList = null
this._linkFileList = null
this._overviewFileList = null
delete this._globalActionsInitialized
},
_createFileActions() {
// inherit file actions from the files app
const fileActions = new OCA.Files.FileActions()
// note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions()
fileActions.merge(OCA.Files.fileActions)
if (!this._globalActionsInitialized) {
// in case actions are registered later
this._onActionsUpdated = _.bind(this._onActionsUpdated, this)
OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated)
OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated)
this._globalActionsInitialized = true
}
// when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
OCA.Files.App.setActiveView('files', { silent: true })
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true)
})
fileActions.setDefault('dir', 'Open')
return fileActions
},
_restoreShareAction() {
const fileActions = new OCA.Files.FileActions()
fileActions.registerAction({
name: 'Restore',
displayName: t('files_sharing', 'Restore'),
altText: t('files_sharing', 'Restore share'),
mime: 'all',
permissions: OC.PERMISSION_ALL,
iconClass: 'icon-history',
type: OCA.Files.FileActions.TYPE_INLINE,
actionHandler(fileName, context) {
const shareId = context.$file.data('shareId')
$.post(OC.linkToOCS('apps/files_sharing/api/v1/deletedshares', 2) + shareId)
.success(function(result) {
context.fileList.remove(context.fileInfoModel.attributes.name)
}).fail(function() {
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'))
})
},
})
return fileActions
},
_acceptShareAction() {
const fileActions = new OCA.Files.FileActions()
fileActions.registerAction({
name: 'Accept share',
displayName: t('files_sharing', 'Accept share'),
mime: 'all',
permissions: OC.PERMISSION_ALL,
iconClass: 'icon-checkmark',
type: OCA.Files.FileActions.TYPE_INLINE,
actionHandler(fileName, context) {
const shareId = context.$file.data('shareId')
let shareBase = 'shares/pending'
if (context.$file.attr('data-remote-id')) {
shareBase = 'remote_shares/pending'
}
$.post(OC.linkToOCS('apps/files_sharing/api/v1/' + shareBase, 2) + shareId)
.success(function(result) {
context.fileList.remove(context.fileInfoModel.attributes.name)
}).fail(function() {
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to accept the share.'))
})
},
})
fileActions.registerAction({
name: 'Reject share',
displayName: t('files_sharing', 'Reject share'),
mime: 'all',
permissions: OC.PERMISSION_ALL,
iconClass: 'icon-close',
type: OCA.Files.FileActions.TYPE_INLINE,
shouldRender(context) {
// disable rejecting group shares from the pending list because they anyway
// land back into that same list
if (context.$file.attr('data-remote-id') && parseInt(context.$file.attr('data-share-type'), 10) === OC.Share.SHARE_TYPE_REMOTE_GROUP) {
return false
}
return true
},
actionHandler(fileName, context) {
const shareId = context.$file.data('shareId')
let shareBase = 'shares'
if (context.$file.attr('data-remote-id')) {
shareBase = 'remote_shares'
}
$.ajax({
url: OC.linkToOCS('apps/files_sharing/api/v1/' + shareBase, 2) + shareId,
type: 'DELETE',
}).success(function(result) {
context.fileList.remove(context.fileInfoModel.attributes.name)
}).fail(function() {
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to reject the share.'))
})
},
})
return fileActions
},
_onActionsUpdated(ev) {
_.each([this._inFileList, this._outFileList, this._linkFileList], function(list) {
if (!list) {
return
}
if (ev.action) {
list.fileActions.registerAction(ev.action)
} else if (ev.defaultAction) {
list.fileActions.setDefault(
ev.defaultAction.mime,
ev.defaultAction.name
)
}
})
},
_extendFileList(fileList) {
// remove size column from summary
fileList.fileSummary.$el.find('.filesize').remove()
},
}
window.addEventListener('DOMContentLoaded', function() {
$('#app-content-sharingin').on('show', function(e) {
OCA.Sharing.App.initSharingIn($(e.target))
})
$('#app-content-sharingin').on('hide', function() {
OCA.Sharing.App.removeSharingIn()
})
$('#app-content-sharingout').on('show', function(e) {
OCA.Sharing.App.initSharingOut($(e.target))
})
$('#app-content-sharingout').on('hide', function() {
OCA.Sharing.App.removeSharingOut()
})
$('#app-content-sharinglinks').on('show', function(e) {
OCA.Sharing.App.initSharingLinks($(e.target))
})
$('#app-content-sharinglinks').on('hide', function() {
OCA.Sharing.App.removeSharingLinks()
})
$('#app-content-deletedshares').on('show', function(e) {
OCA.Sharing.App.initSharingDeleted($(e.target))
})
$('#app-content-deletedshares').on('hide', function() {
OCA.Sharing.App.removeSharingDeleted()
})
$('#app-content-pendingshares').on('show', function(e) {
OCA.Sharing.App.initSharingPening($(e.target))
})
$('#app-content-pendingshares').on('hide', function() {
OCA.Sharing.App.removeSharingPending()
})
$('#app-content-shareoverview').on('show', function(e) {
OCA.Sharing.App.initShareingOverview($(e.target))
})
$('#app-content-shareoverview').on('hide', function() {
OCA.Sharing.App.removeSharingOverview()
})
})

View file

@ -1,541 +0,0 @@
/* eslint-disable */
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function() {
/**
* @class OCA.Sharing.FileList
* @augments OCA.Files.FileList
*
* @classdesc Sharing file list.
* Contains both "shared with others" and "shared with you" modes.
*
* @param $el container element with existing markup for the .files-controls
* and a table
* @param [options] map of options, see other parameters
* @param {boolean} [options.sharedWithUser] true to return files shared with
* the current user, false to return files that the user shared with others.
* Defaults to false.
* @param {boolean} [options.linksOnly] true to return only link shares
*/
var FileList = function($el, options) {
this.initialize($el, options)
}
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Sharing.FileList.prototype */ {
appName: 'Shares',
/**
* Whether the list shows the files shared with the user (true) or
* the files that the user shared with others (false).
*/
_sharedWithUser: false,
_linksOnly: false,
_showDeleted: false,
_showPending: false,
_clientSideSort: true,
_allowSelection: false,
_isOverview: false,
/**
* @private
*/
initialize: function($el, options) {
OCA.Files.FileList.prototype.initialize.apply(this, arguments)
if (this.initialized) {
return
}
// TODO: consolidate both options
if (options && options.sharedWithUser) {
this._sharedWithUser = true
}
if (options && options.linksOnly) {
this._linksOnly = true
}
if (options && options.showDeleted) {
this._showDeleted = true
}
if (options && options.showPending) {
this._showPending = true
}
if (options && options.isOverview) {
this._isOverview = true
}
},
_renderRow: function() {
// HACK: needed to call the overridden _renderRow
// this is because at the time this class is created
// the overriding hasn't been done yet...
return OCA.Files.FileList.prototype._renderRow.apply(this, arguments)
},
_createRow: function(fileData) {
// TODO: hook earlier and render the whole row here
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
$tr.find('.filesize').remove()
$tr.find('td.date').before($tr.children('td:first'))
$tr.find('td.filename input:checkbox').remove()
$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','))
if (this._sharedWithUser) {
$tr.attr('data-share-owner', fileData.shareOwner)
$tr.attr('data-mounttype', 'shared-root')
var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE
$tr.attr('data-permissions', permission)
}
if (this._showDeleted || this._showPending) {
var permission = fileData.permissions
$tr.attr('data-share-permissions', permission)
}
if (fileData.remoteId) {
$tr.attr('data-remote-id', fileData.remoteId)
}
if (fileData.shareType) {
$tr.attr('data-share-type', fileData.shareType)
}
// add row with expiration date for link only shares - influenced by _createRow of filelist
if (this._linksOnly) {
var expirationTimestamp = 0
if (fileData.shares && fileData.shares[0].expiration !== null) {
expirationTimestamp = moment(fileData.shares[0].expiration).valueOf()
}
$tr.attr('data-expiration', expirationTimestamp)
// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
// difference in days multiplied by 5 - brightest shade for expiry dates in more than 32 days (160/5)
var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5)
// ensure that the brightest color is still readable
if (modifiedColor >= 160) {
modifiedColor = 160
}
var formatted
var text
if (expirationTimestamp > 0) {
formatted = OC.Util.formatDate(expirationTimestamp)
text = OC.Util.relativeModifiedDate(expirationTimestamp)
} else {
formatted = t('files_sharing', 'No expiration date set')
text = ''
modifiedColor = 160
}
td = $('<td></td>').attr({ 'class': 'date' })
td.append($('<span></span>').attr({
'class': 'modified',
'title': formatted,
'style': 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')'
}).text(text))
$tr.append(td)
}
return $tr
},
/**
* Set whether the list should contain outgoing shares
* or incoming shares.
*
* @param state true for incoming shares, false otherwise
*/
setSharedWithUser: function(state) {
this._sharedWithUser = !!state
},
updateEmptyContent: function() {
var dir = this.getCurrentDirectory()
if (dir === '/') {
// root has special permissions
this.$el.find('.emptyfilelist.emptycontent').toggleClass('hidden', !this.isEmpty)
this.$el.find('.files-filestable thead th').toggleClass('hidden', this.isEmpty)
// hide expiration date header for non link only shares
if (!this._linksOnly) {
this.$el.find('th.column-expiration').addClass('hidden')
}
} else {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments)
}
},
getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE
},
updateStorageStatistics: function() {
// no op because it doesn't have
// storage info like free space / used space
},
reload: function() {
this.showMask()
if (this._reloadCall?.abort) {
this._reloadCall.abort()
}
// there is only root
this._setCurrentDir('/', false)
var promises = []
var deletedShares = {
url: OC.linkToOCS('apps/files_sharing/api/v1', 2) + 'deletedshares',
/* jshint camelcase: false */
data: {
format: 'json',
include_tags: true
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
var pendingShares = {
url: OC.linkToOCS('apps/files_sharing/api/v1/shares', 2) + 'pending',
/* jshint camelcase: false */
data: {
format: 'json'
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
var pendingRemoteShares = {
url: OC.linkToOCS('apps/files_sharing/api/v1/remote_shares', 2) + 'pending',
/* jshint camelcase: false */
data: {
format: 'json'
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
var shares = {
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
/* jshint camelcase: false */
data: {
format: 'json',
shared_with_me: this._sharedWithUser !== false,
include_tags: true
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
var remoteShares = {
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
/* jshint camelcase: false */
data: {
format: 'json',
include_tags: true
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
// Add the proper ajax requests to the list and run them
// and make sure we have 2 promises
if (this._showDeleted) {
promises.push($.ajax(deletedShares))
} else if (this._showPending) {
promises.push($.ajax(pendingShares))
promises.push($.ajax(pendingRemoteShares))
} else {
promises.push($.ajax(shares))
if (this._sharedWithUser !== false || this._isOverview) {
promises.push($.ajax(remoteShares))
}
if (this._isOverview) {
shares.data.shared_with_me = !shares.data.shared_with_me
promises.push($.ajax(shares))
}
}
this._reloadCall = $.when.apply($, promises)
var callBack = this.reloadCallback.bind(this)
return this._reloadCall.then(callBack, callBack)
},
reloadCallback: function(shares, remoteShares, additionalShares) {
delete this._reloadCall
this.hideMask()
this.$el.find('#headerSharedWith').text(
t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
)
var files = []
// make sure to use the same format
if (shares[0] && shares[0].ocs) {
shares = shares[0]
}
if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
remoteShares = remoteShares[0]
}
if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
additionalShares = additionalShares[0]
}
if (shares.ocs && shares.ocs.data) {
files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser))
}
if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data))
}
if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) {
if (this._showPending) {
// in this case the second callback is about pending remote shares
files = files.concat(this._makeFilesFromRemoteShares(additionalShares.ocs.data))
} else {
files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser))
}
}
this.setFiles(files)
return true
},
_makeFilesFromRemoteShares: function(data) {
var files = data
files = _.chain(files)
// convert share data to file data
.map(function(share) {
var file = {
shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ''),
name: OC.basename(share.mountpoint),
mtime: share.mtime * 1000,
mimetype: share.mimetype,
type: share.type,
// remote share types are different and need to be mapped
shareType: (parseInt(share.share_type, 10) === 1) ? OC.Share.SHARE_TYPE_REMOTE_GROUP : OC.Share.SHARE_TYPE_REMOTE,
id: share.file_id,
path: OC.dirname(share.mountpoint),
permissions: share.permissions,
tags: share.tags || []
}
if (share.remote_id) {
// remote share
if (share.accepted !== '1') {
file.name = OC.basename(share.name)
file.path = '/'
}
file.remoteId = share.remote_id
file.shareOwnerId = share.owner
}
if (!file.mimetype) {
// pending shares usually have no type, so default to showing a directory icon
file.mimetype = 'dir-shared'
}
file.shares = [{
id: share.id,
type: OC.Share.SHARE_TYPE_REMOTE
}]
return file
})
.value()
return files
},
/**
* Converts the OCS API share response data to a file info
* list
* @param {Array} data OCS API share array
* @param {boolean} sharedWithUser
* @returns {Array.<OCA.Sharing.SharedFileInfo>} array of shared file info
*/
_makeFilesFromShares: function(data, sharedWithUser) {
/* jshint camelcase: false */
var files = data
if (this._linksOnly) {
files = _.filter(data, function(share) {
return share.share_type === OC.Share.SHARE_TYPE_LINK
})
}
// OCS API uses non-camelcased names
files = _.chain(files)
// convert share data to file data
.map(function(share) {
// TODO: use OC.Files.FileInfo
var file = {
id: share.file_source,
icon: OC.MimeType.getIconUrl(share.mimetype),
mimetype: share.mimetype,
hasPreview: share.has_preview,
tags: share.tags || []
}
if (share.item_type === 'folder') {
file.type = 'dir'
file.mimetype = 'httpd/unix-directory'
} else {
file.type = 'file'
}
file.share = {
id: share.id,
type: share.share_type,
target: share.share_with,
stime: share.stime * 1000,
expiration: share.expiration
}
if (sharedWithUser) {
file.shareOwner = share.displayname_owner
file.shareOwnerId = share.uid_owner
file.name = OC.basename(share.file_target)
file.path = OC.dirname(share.file_target)
file.permissions = share.permissions
if (file.path) {
file.extraData = share.file_target
}
} else {
if (share.share_type !== OC.Share.SHARE_TYPE_LINK) {
file.share.targetDisplayName = share.share_with_displayname
file.share.targetShareWithId = share.share_with
}
file.name = OC.basename(share.path)
file.path = OC.dirname(share.path)
file.permissions = OC.PERMISSION_ALL
if (file.path) {
file.extraData = share.path
}
}
return file
})
// Group all files and have a "shares" array with
// the share info for each file.
//
// This uses a hash memo to cumulate share information
// inside the same file object (by file id).
.reduce(function(memo, file) {
var data = memo[file.id]
var recipient = file.share.targetDisplayName
var recipientId = file.share.targetShareWithId
if (!data) {
data = memo[file.id] = file
data.shares = [file.share]
// using a hash to make them unique,
// this is only a list to be displayed
data.recipients = {}
data.recipientData = {}
// share types
data.shareTypes = {}
// counter is cheaper than calling _.keys().length
data.recipientsCount = 0
data.mtime = file.share.stime
} else {
// always take the most recent stime
if (file.share.stime > data.mtime) {
data.mtime = file.share.stime
}
data.shares.push(file.share)
}
if (recipient) {
// limit counterparts for output
if (data.recipientsCount < 4) {
// only store the first ones, they will be the only ones
// displayed
data.recipients[recipient] = true
data.recipientData[data.recipientsCount] = {
'shareWith': recipientId,
'shareWithDisplayName': recipient
}
}
data.recipientsCount++
}
data.shareTypes[file.share.type] = true
delete file.share
return memo
}, {})
// Retrieve only the values of the returned hash
.values()
// Clean up
.each(function(data) {
// convert the recipients map to a flat
// array of sorted names
data.mountType = 'shared'
delete data.recipientsCount
if (sharedWithUser) {
// only for outgoing shares
delete data.shareTypes
} else {
data.shareTypes = _.keys(data.shareTypes)
}
})
// Finish the chain by getting the result
.value()
// Sort by expected sort comparator
return files.sort(this._sortComparator)
}
})
/**
* Share info attributes.
*
* @typedef {Object} OCA.Sharing.ShareInfo
*
* @property {number} id share ID
* @property {number} type share type
* @property {String} target share target, either user name or group name
* @property {number} stime share timestamp in milliseconds
* @property {String} [targetDisplayName] display name of the recipient
* (only when shared with others)
* @property {String} [targetShareWithId] id of the recipient
*
*/
/**
* Recipient attributes
*
* @typedef {Object} OCA.Sharing.RecipientInfo
* @property {String} shareWith the id of the recipient
* @property {String} shareWithDisplayName the display name of the recipient
*/
/**
* Shared file info attributes.
*
* @typedef {OCA.Files.FileInfo} OCA.Sharing.SharedFileInfo
*
* @property {Array.<OCA.Sharing.ShareInfo>} shares array of shares for
* this file
* @property {number} mtime most recent share time (if multiple shares)
* @property {String} shareOwner name of the share owner
* @property {Array.<String>} recipients name of the first 4 recipients
* (this is mostly for display purposes)
* @property {Object.<OCA.Sharing.RecipientInfo>} recipientData (as object for easier
* passing to HTML data attributes with jQuery)
*/
OCA.Sharing.FileList = FileList
})()

View file

@ -117,7 +117,6 @@ class Application extends App implements IBootstrap {
$context->injectFn([$this, 'registerMountProviders']);
$context->injectFn([$this, 'registerEventsScripts']);
$context->injectFn([$this, 'registerDownloadEvents']);
$context->injectFn([$this, 'setupSharingMenus']);
Helper::registerHooks();
@ -214,77 +213,4 @@ class Application extends App implements IBootstrap {
}
);
}
public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession): void {
if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) {
return;
}
$navigationManager = \OCA\Files\App::getNavigationManager();
// show_Quick_Access stored as string
$navigationManager->add(function () use ($shareManager, $l10nFactory, $userSession) {
$l = $l10nFactory->get('files_sharing');
$user = $userSession->getUser();
$userId = $user ? $user->getUID() : null;
$sharingSublistArray = [];
if ($shareManager->sharingDisabledForUser($userId) === false) {
$sharingSublistArray[] = [
'id' => 'sharingout',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 16,
'name' => $l->t('Shared with others'),
];
}
$sharingSublistArray[] = [
'id' => 'sharingin',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 15,
'name' => $l->t('Shared with you'),
];
if ($shareManager->sharingDisabledForUser($userId) === false) {
// Check if sharing by link is enabled
if ($shareManager->shareApiAllowLinks()) {
$sharingSublistArray[] = [
'id' => 'sharinglinks',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 17,
'name' => $l->t('Shared by link'),
];
}
}
$sharingSublistArray[] = [
'id' => 'deletedshares',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 19,
'name' => $l->t('Deleted shares'),
];
$sharingSublistArray[] = [
'id' => 'pendingshares',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 19,
'name' => $l->t('Pending shares'),
];
return [
'id' => 'shareoverview',
'appname' => 'files_sharing',
'script' => 'list.php',
'order' => 18,
'name' => $l->t('Shares'),
'classes' => 'collapsible',
'sublist' => $sharingSublistArray,
];
});
}
}

View file

@ -133,6 +133,8 @@ class DeletedShareAPIController extends OCSController {
$result['file_source'] = $node->getId();
$result['file_parent'] = $node->getParent()->getId();
$result['file_target'] = $share->getTarget();
$result['item_size'] = $node->getSize();
$result['item_mtime'] = $node->getMTime();
$expiration = $share->getExpirationDate();
if ($expiration !== null) {

View file

@ -182,6 +182,11 @@ class ShareAPIController extends OCSController {
$sharedBy = $this->userManager->get($share->getSharedBy());
$shareOwner = $this->userManager->get($share->getShareOwner());
$isOwnShare = false;
if ($shareOwner !== null) {
$isOwnShare = $shareOwner->getUID() === $this->currentUser;
}
$result = [
'id' => $share->getId(),
'share_type' => $share->getShareType(),
@ -225,6 +230,11 @@ class ShareAPIController extends OCSController {
$result['item_type'] = 'file';
}
// Get the original node permission if the share owner is the current user
if ($isOwnShare) {
$result['item_permissions'] = $node->getPermissions();
}
$result['mimetype'] = $node->getMimetype();
$result['has_preview'] = $this->previewManager->isAvailable($node);
$result['storage_id'] = $node->getStorage()->getId();
@ -233,6 +243,8 @@ class ShareAPIController extends OCSController {
$result['file_source'] = $node->getId();
$result['file_parent'] = $node->getParent()->getId();
$result['file_target'] = $share->getTarget();
$result['item_size'] = $node->getSize();
$result['item_mtime'] = $node->getMTime();
$expiration = $share->getExpirationDate();
if ($expiration !== null) {
@ -1423,7 +1435,7 @@ class ShareAPIController extends OCSController {
try {
$formattedShare = $this->formatShare($share, $node);
$formattedShare['status'] = $share->getStatus();
$formattedShare['path'] = $share->getNode()->getName();
$formattedShare['path'] = '/' . $share->getNode()->getName();
$formattedShare['permissions'] = 0;
return $formattedShare;
} catch (NotFoundException $e) {

View file

@ -29,6 +29,7 @@ use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Share\IManager;
use OCP\Util;
class LoadAdditionalListener implements IEventListener {
@ -37,10 +38,13 @@ class LoadAdditionalListener implements IEventListener {
return;
}
// After files for the files list shared content
Util::addScript(Application::APP_ID, 'files_sharing', 'files');
// After files for the breadcrumb share indicator
Util::addScript(Application::APP_ID, 'additionalScripts', 'files');
Util::addStyle(Application::APP_ID, 'icons');
$shareManager = \OC::$server->get(IManager::class);
if ($shareManager->shareApiEnabled() && class_exists('\OCA\Files\App')) {
Util::addScript(Application::APP_ID, 'files_sharing', 'files');
}
}
}

View file

@ -0,0 +1,223 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { action } from './acceptShareAction'
import { expect } from '@jest/globals'
import { File, Permission } from '@nextcloud/files'
import { FileAction } from '../../../files/src/services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../../../files/src/services/Navigation'
import '../main'
const view = {
id: 'files',
name: 'Files',
} as Navigation
const pendingShareView = {
id: 'pendingshares',
name: 'Pending shares',
} as Navigation
describe('Accept share action conditions tests', () => {
test('Default values', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('accept-share')
expect(action.displayName([file], pendingShareView)).toBe('Accept share')
expect(action.iconSvgInline([file], pendingShareView)).toBe('<svg>SvgMock</svg>')
expect(action.default).toBeUndefined()
expect(action.order).toBe(1)
expect(action.inline).toBeDefined()
expect(action.inline!(file, pendingShareView)).toBe(true)
})
test('Default values for multiple files', () => {
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.displayName([file1, file2], pendingShareView)).toBe('Accept shares')
})
})
describe('Accept share action enabled tests', () => {
test('Enabled with on pending shares view', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], pendingShareView)).toBe(true)
})
test('Disabled on wrong view', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
test('Disabled without nodes', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], pendingShareView)).toBe(false)
})
})
describe('Accept share action execute tests', () => {
test('Accept share action', async () => {
jest.spyOn(axios, 'post')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(true)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Accept remote share action', async () => {
jest.spyOn(axios, 'post')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
remote: 3,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(true)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/123')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Accept share action batch', async () => {
jest.spyOn(axios, 'post')
jest.spyOn(eventBus, 'emit')
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 456,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.execBatch!([file1, file2], pendingShareView, '/')
expect(exec).toStrictEqual([true, true])
expect(axios.post).toBeCalledTimes(2)
expect(axios.post).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123')
expect(axios.post).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/456')
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Accept fails', async () => {
jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') })
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(false)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123')
expect(eventBus.emit).toBeCalledTimes(0)
})
})

View file

@ -0,0 +1,66 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node } from '@nextcloud/files'
import type { Navigation } from '../../../files/src/services/Navigation'
import { emit } from '@nextcloud/event-bus'
import { generateOcsUrl } from '@nextcloud/router'
import { translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import CheckSvg from '@mdi/svg/svg/check.svg?raw'
import { FileAction, registerFileAction } from '../../../files/src/services/FileAction'
import { pendingSharesViewId } from '../views/shares'
export const action = new FileAction({
id: 'accept-share',
displayName: (nodes: Node[]) => n('files_sharing', 'Accept share', 'Accept shares', nodes.length),
iconSvgInline: () => CheckSvg,
enabled: (nodes, view) => nodes.length > 0 && view.id === pendingSharesViewId,
async exec(node: Node) {
try {
const isRemote = !!node.attributes.remote
const url = generateOcsUrl('apps/files_sharing/api/v1/{shareBase}/pending/{id}', {
shareBase: isRemote ? 'remote_shares' : 'shares',
id: node.attributes.id,
})
await axios.post(url)
// Remove from current view
emit('files:node:deleted', node)
return true
} catch (error) {
return false
}
},
async execBatch(nodes: Node[], view: Navigation, dir: string) {
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 1,
inline: () => true,
})
registerFileAction(action)

View file

@ -0,0 +1,97 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { action } from './openInFilesAction'
import { expect } from '@jest/globals'
import { File, Permission } from '@nextcloud/files'
import { DefaultType, FileAction } from '../../../files/src/services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../../../files/src/services/Navigation'
import '../main'
import { deletedSharesViewId, pendingSharesViewId, sharedWithOthersViewId, sharedWithYouViewId, sharesViewId, sharingByLinksViewId } from '../views/shares'
const view = {
id: 'files',
name: 'Files',
} as Navigation
const validViews = [
sharesViewId,
sharedWithYouViewId,
sharedWithOthersViewId,
sharingByLinksViewId,
].map(id => ({ id, name: id })) as Navigation[]
const invalidViews = [
deletedSharesViewId,
pendingSharesViewId,
].map(id => ({ id, name: id })) as Navigation[]
describe('Open in files action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('open-in-files')
expect(action.displayName([], validViews[0])).toBe('Open in files')
expect(action.iconSvgInline([], validViews[0])).toBe('')
expect(action.default).toBe(DefaultType.HIDDEN)
expect(action.order).toBe(-1000)
expect(action.inline).toBeUndefined()
})
})
describe('Open in files action enabled tests', () => {
test('Enabled with on valid view', () => {
validViews.forEach(view => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(true)
})
})
test('Disabled on wrong view', () => {
invalidViews.forEach(view => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
})
})
describe('Open in files action execute tests', () => {
test('Open in files', async () => {
const goToRouteMock = jest.fn()
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/foobar.txt',
owner: 'admin',
mime: 'text/plain',
root: '/files/admin',
permissions: Permission.READ,
})
const exec = await action.exec(file, view, '/')
// Silent action
expect(exec).toBe(null)
expect(goToRouteMock).toBeCalledTimes(1)
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo' })
})
})

View file

@ -0,0 +1,56 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { translate as t } from '@nextcloud/l10n'
import type { Node } from '@nextcloud/files'
import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction'
import { sharesViewId, sharedWithYouViewId, sharedWithOthersViewId, sharingByLinksViewId } from '../views/shares'
export const action = new FileAction({
id: 'open-in-files',
displayName: () => t('files', 'Open in files'),
iconSvgInline: () => '',
enabled: (nodes, view) => [
sharesViewId,
sharedWithYouViewId,
sharedWithOthersViewId,
sharingByLinksViewId,
// Deleted and pending shares are not
// accessible in the files app.
].includes(view.id),
async exec(node: Node) {
window.OCP.Files.Router.goToRoute(
null, // use default route
{ view: 'files', fileid: node.fileid },
{ dir: node.dirname, fileid: node.fileid },
)
return null
},
default: DefaultType.HIDDEN,
// Before openFolderAction
order: -1000,
})
registerFileAction(action)

View file

@ -0,0 +1,250 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { action } from './rejectShareAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission } from '@nextcloud/files'
import { FileAction } from '../../../files/src/services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../../../files/src/services/Navigation'
import '../main'
const view = {
id: 'files',
name: 'Files',
} as Navigation
const pendingShareView = {
id: 'pendingshares',
name: 'Pending shares',
} as Navigation
describe('Reject share action conditions tests', () => {
test('Default values', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('reject-share')
expect(action.displayName([file], pendingShareView)).toBe('Reject share')
expect(action.iconSvgInline([file], pendingShareView)).toBe('<svg>SvgMock</svg>')
expect(action.default).toBeUndefined()
expect(action.order).toBe(2)
expect(action.inline).toBeDefined()
expect(action.inline!(file, pendingShareView)).toBe(true)
})
test('Default values for multiple files', () => {
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.displayName([file1, file2], pendingShareView)).toBe('Reject shares')
})
})
describe('Reject share action enabled tests', () => {
test('Enabled with on pending shares view', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], pendingShareView)).toBe(true)
})
test('Disabled on wrong view', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
test('Disabled without nodes', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], pendingShareView)).toBe(false)
})
test('Disabled if some nodes are remote group shares', () => {
const folder1 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
permissions: Permission.READ,
attributes: {
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const folder2 = new Folder({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
owner: 'admin',
permissions: Permission.READ,
attributes: {
remote_id: 1,
share_type: window.OC.Share.SHARE_TYPE_REMOTE_GROUP,
},
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder1], pendingShareView)).toBe(true)
expect(action.enabled!([folder2], pendingShareView)).toBe(false)
expect(action.enabled!([folder1, folder2], pendingShareView)).toBe(false)
})
})
describe('Reject share action execute tests', () => {
test('Reject share action', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(true)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Reject remote share action', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
remote: 3,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(true)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/123')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Reject share action batch', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 456,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.execBatch!([file1, file2], pendingShareView, '/')
expect(exec).toStrictEqual([true, true])
expect(axios.delete).toBeCalledTimes(2)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/456')
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Reject fails', async () => {
jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, pendingShareView, '/')
expect(exec).toBe(false)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123')
expect(eventBus.emit).toBeCalledTimes(0)
})
})

View file

@ -0,0 +1,83 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node } from '@nextcloud/files'
import type { Navigation } from '../../../files/src/services/Navigation'
import { emit } from '@nextcloud/event-bus'
import { generateOcsUrl } from '@nextcloud/router'
import { translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
import { FileAction, registerFileAction } from '../../../files/src/services/FileAction'
import { pendingSharesViewId } from '../views/shares'
export const action = new FileAction({
id: 'reject-share',
displayName: (nodes: Node[]) => n('files_sharing', 'Reject share', 'Reject shares', nodes.length),
iconSvgInline: () => CloseSvg,
enabled: (nodes, view) => {
if (view.id !== pendingSharesViewId) {
return false
}
if (nodes.length === 0) {
return false
}
// disable rejecting group shares from the pending list because they anyway
// land back into that same list after rejecting them
if (nodes.some(node => node.attributes.remote_id
&& node.attributes.share_type === window.OC.Share.SHARE_TYPE_REMOTE_GROUP)) {
return false
}
return true
},
async exec(node: Node) {
try {
const isRemote = !!node.attributes.remote
const url = generateOcsUrl('apps/files_sharing/api/v1/{shareBase}/{id}', {
shareBase: isRemote ? 'remote_shares' : 'shares',
id: node.attributes.id,
})
await axios.delete(url)
// Remove from current view
emit('files:node:deleted', node)
return true
} catch (error) {
return false
}
},
async execBatch(nodes: Node[], view: Navigation, dir: string) {
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 2,
inline: () => true,
})
registerFileAction(action)

View file

@ -0,0 +1,196 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { action } from './restoreShareAction'
import { expect } from '@jest/globals'
import { File, Permission } from '@nextcloud/files'
import { FileAction } from '../../../files/src/services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../../../files/src/services/Navigation'
import '../main'
const view = {
id: 'files',
name: 'Files',
} as Navigation
const deletedShareView = {
id: 'deletedshares',
name: 'Deleted shares',
} as Navigation
describe('Restore share action conditions tests', () => {
test('Default values', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('restore-share')
expect(action.displayName([file], deletedShareView)).toBe('Restore share')
expect(action.iconSvgInline([file], deletedShareView)).toBe('<svg>SvgMock</svg>')
expect(action.default).toBeUndefined()
expect(action.order).toBe(1)
expect(action.inline).toBeDefined()
expect(action.inline!(file, deletedShareView)).toBe(true)
})
test('Default values for multiple files', () => {
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.displayName([file1, file2], deletedShareView)).toBe('Restore shares')
})
})
describe('Restore share action enabled tests', () => {
test('Enabled with on pending shares view', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], deletedShareView)).toBe(true)
})
test('Disabled on wrong view', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
test('Disabled without nodes', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], deletedShareView)).toBe(false)
})
})
describe('Restore share action execute tests', () => {
test('Restore share action', async () => {
jest.spyOn(axios, 'post')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, deletedShareView, '/')
expect(exec).toBe(true)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Restore share action batch', async () => {
jest.spyOn(axios, 'post')
jest.spyOn(eventBus, 'emit')
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 456,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.execBatch!([file1, file2], deletedShareView, '/')
expect(exec).toStrictEqual([true, true])
expect(axios.post).toBeCalledTimes(2)
expect(axios.post).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123')
expect(axios.post).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/456')
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Restore fails', async () => {
jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') })
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
attributes: {
id: 123,
share_type: window.OC.Share.SHARE_TYPE_USER,
},
})
const exec = await action.exec(file, deletedShareView, '/')
expect(exec).toBe(false)
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123')
expect(eventBus.emit).toBeCalledTimes(0)
})
})

View file

@ -0,0 +1,65 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node } from '@nextcloud/files'
import type { Navigation } from '../../../files/src/services/Navigation'
import { emit } from '@nextcloud/event-bus'
import { generateOcsUrl } from '@nextcloud/router'
import { translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import ArrowULeftTopSvg from '@mdi/svg/svg/arrow-u-left-top.svg?raw'
import { FileAction, registerFileAction } from '../../../files/src/services/FileAction'
import { deletedSharesViewId } from '../views/shares'
export const action = new FileAction({
id: 'restore-share',
displayName: (nodes: Node[]) => n('files_sharing', 'Restore share', 'Restore shares', nodes.length),
iconSvgInline: () => ArrowULeftTopSvg,
enabled: (nodes, view) => nodes.length > 0 && view.id === deletedSharesViewId,
async exec(node: Node) {
try {
const url = generateOcsUrl('apps/files_sharing/api/v1/deletedshares/{id}', {
id: node.attributes.id,
})
await axios.post(url)
// Remove from current view
emit('files:node:deleted', node)
return true
} catch (error) {
return false
}
},
async execBatch(nodes: Node[], view: Navigation, dir: string) {
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
order: 1,
inline: () => true,
})
registerFileAction(action)

View file

@ -0,0 +1,30 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import registerSharingViews from './views/shares'
import './actions/acceptShareAction'
import './actions/openInFilesAction'
import './actions/rejectShareAction'
import './actions/restoreShareAction'
registerSharingViews()

View file

@ -22,7 +22,11 @@
*/
// register default shares types
Object.assign(OC, {
if (!window.OC) {
window.OC = {}
}
Object.assign(window.OC, {
Share: {
SHARE_TYPE_USER: 0,
SHARE_TYPE_GROUP: 1,

View file

@ -0,0 +1,364 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { expect } from '@jest/globals'
import axios from '@nextcloud/axios'
import { Type } from '@nextcloud/sharing'
import * as auth from '@nextcloud/auth'
import { getContents, type OCSResponse } from './SharingService'
import { File, Folder } from '@nextcloud/files'
import logger from './logger'
global.window.OC = {
TAG_FAVORITE: '_$!<Favorite>!$_',
}
describe('SharingService methods definitions', () => {
beforeAll(() => {
jest.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
data: {
ocs: {
meta: {
status: 'ok',
statuscode: 200,
message: 'OK',
},
data: [],
},
} as OCSResponse,
}
})
})
afterAll(() => {
jest.restoreAllMocks()
})
test('Shared with you', async () => {
await getContents(true, false, false, false, [])
expect(axios.get).toHaveBeenCalledTimes(2)
expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', {
headers: {
'Content-Type': 'application/json',
},
params: {
shared_with_me: true,
include_tags: true,
},
})
expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares', {
headers: {
'Content-Type': 'application/json',
},
params: {
include_tags: true,
},
})
})
test('Shared with others', async () => {
await getContents(false, true, false, false, [])
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toHaveBeenCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', {
headers: {
'Content-Type': 'application/json',
},
params: {
shared_with_me: false,
include_tags: true,
},
})
})
test('Pending shares', async () => {
await getContents(false, false, true, false, [])
expect(axios.get).toHaveBeenCalledTimes(2)
expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending', {
headers: {
'Content-Type': 'application/json',
},
params: {
include_tags: true,
},
})
expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending', {
headers: {
'Content-Type': 'application/json',
},
params: {
include_tags: true,
},
})
})
test('Deleted shares', async () => {
await getContents(false, true, false, false, [])
expect(axios.get).toHaveBeenCalledTimes(1)
expect(axios.get).toHaveBeenCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares', {
headers: {
'Content-Type': 'application/json',
},
params: {
shared_with_me: false,
include_tags: true,
},
})
})
test('Unknown owner', async () => {
jest.spyOn(auth, 'getCurrentUser').mockReturnValue(null)
const results = await getContents(false, true, false, false, [])
expect(results.folder.owner).toEqual(null)
})
})
describe('SharingService filtering', () => {
beforeAll(() => {
jest.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
data: {
ocs: {
meta: {
status: 'ok',
statuscode: 200,
message: 'OK',
},
data: [
{
id: '62',
share_type: Type.SHARE_TYPE_USER,
uid_owner: 'test',
displayname_owner: 'test',
permissions: 31,
stime: 1688666292,
expiration: '2023-07-13 00:00:00',
token: null,
path: '/Collaborators',
item_type: 'folder',
item_permissions: 31,
mimetype: 'httpd/unix-directory',
storage: 224,
item_source: 419413,
file_source: 419413,
file_parent: 419336,
file_target: '/Collaborators',
item_size: 41434,
item_mtime: 1688662980,
},
],
},
},
}
})
})
afterAll(() => {
jest.restoreAllMocks()
})
test('Shared with others filtering', async () => {
const shares = await getContents(false, true, false, false, [Type.SHARE_TYPE_USER])
expect(axios.get).toHaveBeenCalledTimes(1)
expect(shares.contents).toHaveLength(1)
expect(shares.contents[0].fileid).toBe(419413)
expect(shares.contents[0]).toBeInstanceOf(Folder)
})
test('Shared with others filtering empty', async () => {
const shares = await getContents(false, true, false, false, [Type.SHARE_TYPE_LINK])
expect(axios.get).toHaveBeenCalledTimes(1)
expect(shares.contents).toHaveLength(0)
})
})
describe('SharingService share to Node mapping', () => {
const shareFile = {
id: '66',
share_type: 0,
uid_owner: 'test',
displayname_owner: 'test',
permissions: 19,
can_edit: true,
can_delete: true,
stime: 1688721609,
parent: null,
expiration: '2023-07-14 00:00:00',
token: null,
uid_file_owner: 'test',
note: '',
label: null,
displayname_file_owner: 'test',
path: '/document.md',
item_type: 'file',
item_permissions: 27,
mimetype: 'text/markdown',
has_preview: true,
storage_id: 'home::test',
storage: 224,
item_source: 530936,
file_source: 530936,
file_parent: 419336,
file_target: '/document.md',
item_size: 123,
item_mtime: 1688721600,
share_with: 'user00',
share_with_displayname: 'User00',
share_with_displayname_unique: 'user00@domain.com',
status: {
status: 'away',
message: null,
icon: null,
clearAt: null,
},
mail_send: 0,
hide_download: 0,
attributes: null,
tags: [],
}
const shareFolder = {
id: '67',
share_type: 0,
uid_owner: 'test',
displayname_owner: 'test',
permissions: 31,
can_edit: true,
can_delete: true,
stime: 1688721629,
parent: null,
expiration: '2023-07-14 00:00:00',
token: null,
uid_file_owner: 'test',
note: '',
label: null,
displayname_file_owner: 'test',
path: '/Folder',
item_type: 'folder',
item_permissions: 31,
mimetype: 'httpd/unix-directory',
has_preview: false,
storage_id: 'home::test',
storage: 224,
item_source: 531080,
file_source: 531080,
file_parent: 419336,
file_target: '/Folder',
item_size: 0,
item_mtime: 1688721623,
share_with: 'user00',
share_with_displayname: 'User00',
share_with_displayname_unique: 'user00@domain.com',
status: {
status: 'away',
message: null,
icon: null,
clearAt: null,
},
mail_send: 0,
hide_download: 0,
attributes: null,
tags: [window.OC.TAG_FAVORITE],
}
test('File', async () => {
jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({
data: {
ocs: {
data: [shareFile],
},
},
}))
const shares = await getContents(false, true, false, false)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(shares.contents).toHaveLength(1)
const file = shares.contents[0] as File
expect(file).toBeInstanceOf(File)
expect(file.fileid).toBe(530936)
expect(file.source).toBe('http://localhost/remote.php/dav/files/test/document.md')
expect(file.owner).toBe('test')
expect(file.mime).toBe('text/markdown')
expect(file.mtime).toBeInstanceOf(Date)
expect(file.size).toBe(123)
expect(file.permissions).toBe(27)
expect(file.root).toBe('/files/test')
expect(file.attributes).toBeInstanceOf(Object)
expect(file.attributes['has-preview']).toBe(true)
expect(file.attributes.previewUrl).toBe('/index.php/core/preview?fileId=530936&x=32&y=32&forceIcon=0')
expect(file.attributes.favorite).toBe(0)
})
test('Folder', async () => {
jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({
data: {
ocs: {
data: [shareFolder],
},
},
}))
const shares = await getContents(false, true, false, false)
expect(axios.get).toHaveBeenCalledTimes(1)
expect(shares.contents).toHaveLength(1)
const folder = shares.contents[0] as Folder
expect(folder).toBeInstanceOf(Folder)
expect(folder.fileid).toBe(531080)
expect(folder.source).toBe('http://localhost/remote.php/dav/files/test/Folder')
expect(folder.owner).toBe('test')
expect(folder.mime).toBe('httpd/unix-directory')
expect(folder.mtime).toBeInstanceOf(Date)
expect(folder.size).toBe(0)
expect(folder.permissions).toBe(31)
expect(folder.root).toBe('/files/test')
expect(folder.attributes).toBeInstanceOf(Object)
expect(folder.attributes['has-preview']).toBe(false)
expect(folder.attributes.previewUrl).toBeUndefined()
expect(folder.attributes.favorite).toBe(1)
})
test('Error', async () => {
jest.spyOn(logger, 'error').mockImplementationOnce(() => {})
jest.spyOn(axios, 'get').mockReturnValueOnce(Promise.resolve({
data: {
ocs: {
data: [{}],
},
},
}))
const shares = await getContents(false, true, false, false)
expect(shares.contents).toHaveLength(0)
expect(logger.error).toHaveBeenCalledTimes(1)
})
})

View file

@ -0,0 +1,181 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* eslint-disable camelcase, n/no-extraneous-import */
import type { AxiosPromise } from 'axios'
import type { ContentsWithRoot } from '../../../files/src/services/Navigation'
import { Folder, File } from '@nextcloud/files'
import { generateOcsUrl, generateRemoteUrl, generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import logger from './logger'
export const rootPath = `/files/${getCurrentUser()?.uid}`
export type OCSResponse = {
ocs: {
meta: {
status: string
statuscode: number
message: string
},
data: []
}
}
const headers = {
'Content-Type': 'application/json',
}
const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
try {
const isFolder = ocsEntry?.item_type === 'folder'
const hasPreview = ocsEntry?.has_preview === true
const Node = isFolder ? Folder : File
const fileid = ocsEntry.file_source
const previewUrl = hasPreview ? generateUrl('/core/preview?fileId={fileid}&x=32&y=32&forceIcon=0', { fileid }) : undefined
// Generate path and strip double slashes
const path = ocsEntry?.path || ocsEntry.file_target
const source = generateRemoteUrl(`dav/${rootPath}/${path}`.replaceAll(/\/\//gm, '/'))
// Prefer share time if more recent than item mtime
let mtime = ocsEntry?.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined
if (ocsEntry?.stime > (ocsEntry?.item_mtime || 0)) {
mtime = new Date((ocsEntry.stime) * 1000)
}
return new Node({
id: fileid,
source,
owner: ocsEntry?.uid_owner,
mime: ocsEntry?.mimetype,
mtime,
size: ocsEntry?.item_size,
permissions: ocsEntry?.item_permissions || ocsEntry?.permissions,
root: rootPath,
attributes: {
...ocsEntry,
previewUrl,
'has-preview': hasPreview,
favorite: ocsEntry?.tags?.includes(window.OC.TAG_FAVORITE) ? 1 : 0,
},
})
} catch (error) {
logger.error('Error while parsing OCS entry', { error })
return null
}
}
const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse> {
const url = generateOcsUrl('apps/files_sharing/api/v1/shares')
return axios.get(url, {
headers,
params: {
shared_with_me,
include_tags: true,
},
})
}
const getSharedWithYou = function(): AxiosPromise<OCSResponse> {
return getShares(true)
}
const getSharedWithOthers = function(): AxiosPromise<OCSResponse> {
return getShares()
}
const getRemoteShares = function(): AxiosPromise<OCSResponse> {
const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares')
return axios.get(url, {
headers,
params: {
include_tags: true,
},
})
}
const getPendingShares = function(): AxiosPromise<OCSResponse> {
const url = generateOcsUrl('apps/files_sharing/api/v1/shares/pending')
return axios.get(url, {
headers,
params: {
include_tags: true,
},
})
}
const getRemotePendingShares = function(): AxiosPromise<OCSResponse> {
const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares/pending')
return axios.get(url, {
headers,
params: {
include_tags: true,
},
})
}
const getDeletedShares = function(): AxiosPromise<OCSResponse> {
const url = generateOcsUrl('apps/files_sharing/api/v1/deletedshares')
return axios.get(url, {
headers,
params: {
include_tags: true,
},
})
}
export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
const promises = [] as AxiosPromise<OCSResponse>[]
if (sharedWithYou) {
promises.push(getSharedWithYou(), getRemoteShares())
}
if (sharedWithOthers) {
promises.push(getSharedWithOthers())
}
if (pendingShares) {
promises.push(getPendingShares(), getRemotePendingShares())
}
if (deletedshares) {
promises.push(getDeletedShares())
}
const responses = await Promise.all(promises)
const data = responses.map((response) => response.data.ocs.data).flat()
let contents = data.map(ocsEntryToNode).filter((node) => node !== null) as (Folder | File)[]
if (filterTypes.length > 0) {
contents = contents.filter((node) => filterTypes.includes(node.attributes?.share_type))
}
return {
folder: new Folder({
id: 0,
source: generateRemoteUrl('dav' + rootPath),
owner: getCurrentUser()?.uid || null,
}),
contents,
}
}

View file

@ -1,8 +1,7 @@
/**
* @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license AGPL-3.0-or-later
*
@ -20,6 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getLoggerBuilder } from '@nextcloud/logger'
import '../js/app.js'
import '../js/sharedfilelist.js'
export default getLoggerBuilder()
.setApp('files_sharing')
.detectUser()
.build()

View file

@ -0,0 +1,125 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* eslint-disable n/no-extraneous-import */
import { expect } from '@jest/globals'
import axios from '@nextcloud/axios'
import { type Navigation } from '../../../files/src/services/Navigation'
import { type OCSResponse } from '../services/SharingService'
import NavigationService from '../../../files/src/services/Navigation'
import registerSharingViews from './shares'
import '../main'
import { Folder } from '@nextcloud/files'
describe('Sharing views definition', () => {
let Navigation
beforeEach(() => {
Navigation = new NavigationService()
window.OCP = { Files: { Navigation } }
})
afterAll(() => {
delete window.OCP
})
test('Default values', () => {
jest.spyOn(Navigation, 'register')
expect(Navigation.views.length).toBe(0)
registerSharingViews()
const shareOverviewView = Navigation.views.find(view => view.id === 'shareoverview') as Navigation
const sharesChildViews = Navigation.views.filter(view => view.parent === 'shareoverview') as Navigation[]
expect(Navigation.register).toHaveBeenCalledTimes(6)
// one main view and no children
expect(Navigation.views.length).toBe(6)
expect(shareOverviewView).toBeDefined()
expect(sharesChildViews.length).toBe(5)
expect(shareOverviewView?.id).toBe('shareoverview')
expect(shareOverviewView?.name).toBe('Shares')
expect(shareOverviewView?.caption).toBe('Overview of shared files.')
expect(shareOverviewView?.icon).toBe('<svg>SvgMock</svg>')
expect(shareOverviewView?.order).toBe(20)
expect(shareOverviewView?.columns).toStrictEqual([])
expect(shareOverviewView?.getContents).toBeDefined()
const dataProvider = [
{ id: 'sharingin', name: 'Shared with you', caption: 'List of files that are shared with you.' },
{ id: 'sharingout', name: 'Shared with others', caption: 'List of files that you shared with others.' },
{ id: 'sharinglinks', name: 'Shared by link', caption: 'List of files that are shared by link.' },
{ id: 'deletedshares', name: 'Deleted shares', caption: 'List of shares that you removed yourself from.' },
{ id: 'pendingshares', name: 'Pending shares', caption: 'List of unapproved shares.' },
]
sharesChildViews.forEach((view, index) => {
expect(view?.id).toBe(dataProvider[index].id)
expect(view?.parent).toBe('shareoverview')
expect(view?.name).toBe(dataProvider[index].name)
expect(view?.caption).toBe(dataProvider[index].caption)
expect(view?.icon).toBe('<svg>SvgMock</svg>')
expect(view?.order).toBe(index + 1)
expect(view?.columns).toStrictEqual([])
expect(view?.getContents).toBeDefined()
})
})
})
describe('Sharing views contents', () => {
let Navigation
beforeEach(() => {
Navigation = new NavigationService()
window.OCP = { Files: { Navigation } }
})
afterAll(() => {
delete window.OCP
})
test('Sharing overview get contents', async () => {
jest.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
data: {
ocs: {
meta: {
status: 'ok',
statuscode: 200,
message: 'OK',
},
data: [],
},
} as OCSResponse,
}
})
registerSharingViews()
expect(Navigation.views.length).toBe(6)
Navigation.views.forEach(async (view: Navigation) => {
const content = await view.getContents('/')
expect(content.contents).toStrictEqual([])
expect(content.folder).toBeInstanceOf(Folder)
})
})
})

View file

@ -0,0 +1,126 @@
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type NavigationService from '../../../files/src/services/Navigation'
import type { Navigation } from '../../../files/src/services/Navigation'
import { translate as t } from '@nextcloud/l10n'
import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw'
import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
import AccountSvg from '@mdi/svg/svg/account.svg?raw'
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import ShareVariantSvg from '@mdi/svg/svg/share-variant.svg?raw'
import { getContents } from '../services/SharingService'
export const sharesViewId = 'shareoverview'
export const sharedWithYouViewId = 'sharingin'
export const sharedWithOthersViewId = 'sharingout'
export const sharingByLinksViewId = 'sharinglinks'
export const deletedSharesViewId = 'deletedshares'
export const pendingSharesViewId = 'pendingshares'
export default () => {
const Navigation = window.OCP.Files.Navigation as NavigationService
Navigation.register({
id: sharesViewId,
name: t('files_sharing', 'Shares'),
caption: t('files_sharing', 'Overview of shared files.'),
icon: ShareVariantSvg,
order: 20,
columns: [],
getContents: () => getContents(),
} as Navigation)
Navigation.register({
id: sharedWithYouViewId,
name: t('files_sharing', 'Shared with you'),
caption: t('files_sharing', 'List of files that are shared with you.'),
icon: AccountSvg,
order: 1,
parent: sharesViewId,
columns: [],
getContents: () => getContents(true, false, false, false),
} as Navigation)
Navigation.register({
id: sharedWithOthersViewId,
name: t('files_sharing', 'Shared with others'),
caption: t('files_sharing', 'List of files that you shared with others.'),
icon: AccountGroupSvg,
order: 2,
parent: sharesViewId,
columns: [],
getContents: () => getContents(false, true, false, false),
} as Navigation)
Navigation.register({
id: sharingByLinksViewId,
name: t('files_sharing', 'Shared by link'),
caption: t('files_sharing', 'List of files that are shared by link.'),
icon: LinkSvg,
order: 3,
parent: sharesViewId,
columns: [],
getContents: () => getContents(false, true, false, false, [window.OC.Share.SHARE_TYPE_LINK]),
} as Navigation)
Navigation.register({
id: deletedSharesViewId,
name: t('files_sharing', 'Deleted shares'),
caption: t('files_sharing', 'List of shares that you removed yourself from.'),
icon: DeleteSvg,
order: 4,
parent: sharesViewId,
columns: [],
getContents: () => getContents(false, false, false, true),
} as Navigation)
Navigation.register({
id: pendingSharesViewId,
name: t('files_sharing', 'Pending shares'),
caption: t('files_sharing', 'List of unapproved shares.'),
icon: AccountClockSvg,
order: 5,
parent: sharesViewId,
columns: [],
getContents: () => getContents(false, false, true, false),
} as Navigation)
}

View file

@ -576,6 +576,8 @@ class ShareAPIControllerTest extends TestCase {
$file->method('getPath')->willReturn('file');
$file->method('getStorage')->willReturn($storage);
$file->method('getParent')->willReturn($parentFolder);
$file->method('getSize')->willReturn(123465);
$file->method('getMTime')->willReturn(1234567890);
$file->method('getMimeType')->willReturn('myMimeType');
$folder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
@ -583,6 +585,8 @@ class ShareAPIControllerTest extends TestCase {
$folder->method('getPath')->willReturn('folder');
$folder->method('getStorage')->willReturn($storage);
$folder->method('getParent')->willReturn($parentFolder);
$folder->method('getSize')->willReturn(123465);
$folder->method('getMTime')->willReturn(1234567890);
$folder->method('getMimeType')->willReturn('myFolderMimeType');
[$shareAttributes, $shareAttributesReturnJson] = $this->mockShareAttributes();
@ -637,6 +641,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'item_size' => 123465,
'item_mtime' => 1234567890,
'attributes' => null,
];
$data[] = [$share, $expected];
@ -689,6 +695,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123465,
'item_mtime' => 1234567890,
'attributes' => null,
];
$data[] = [$share, $expected];
@ -747,6 +755,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123465,
'item_mtime' => 1234567890,
'attributes' => null,
];
$data[] = [$share, $expected];
@ -3735,6 +3745,13 @@ class ShareAPIControllerTest extends TestCase {
$folder->method('getParent')->willReturn($parent);
$fileWithPreview->method('getParent')->willReturn($parent);
$file->method('getSize')->willReturn(123456);
$folder->method('getSize')->willReturn(123456);
$fileWithPreview->method('getSize')->willReturn(123456);
$file->method('getMTime')->willReturn(1234567890);
$folder->method('getMTime')->willReturn(1234567890);
$fileWithPreview->method('getMTime')->willReturn(1234567890);
$cache = $this->getMockBuilder('OCP\Files\Cache\ICache')->getMock();
$cache->method('getNumericStorageId')->willReturn(100);
$storage = $this->createMock(Storage::class);
@ -3772,7 +3789,7 @@ class ShareAPIControllerTest extends TestCase {
// User backend down
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_USER,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -3796,7 +3813,7 @@ class ShareAPIControllerTest extends TestCase {
'share_with_displayname' => 'recipient',
'share_with_displayname_unique' => 'recipient',
'note' => 'personal note',
'label' => null,
'label' => '',
'mail_send' => 0,
'mimetype' => 'myMimeType',
'has_preview' => false,
@ -3804,13 +3821,15 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => '[{"scope":"permissions","key":"download","enabled":true}]',
], $share, [], false
];
// User backend up
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_USER,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiatorDN',
@ -3823,7 +3842,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'ownerDN',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'file',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -3842,6 +3861,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => '[{"scope":"permissions","key":"download","enabled":true}]',
], $share, [
['owner', $owner],
@ -3864,7 +3885,7 @@ class ShareAPIControllerTest extends TestCase {
// User backend down
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_USER,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -3877,7 +3898,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'file',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -3896,6 +3917,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -3914,7 +3937,7 @@ class ShareAPIControllerTest extends TestCase {
// User backend down
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_USER,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -3927,7 +3950,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'currentUser',
'displayname_file_owner' => 'currentUser',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'file',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -3946,6 +3969,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => true,
'can_delete' => true,
'status' => [],
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -3966,7 +3991,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_GROUP,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -3979,7 +4004,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'file',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -3996,6 +4021,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4014,7 +4041,7 @@ class ShareAPIControllerTest extends TestCase {
->setId(42);
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_GROUP,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4026,7 +4053,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'file',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -4043,6 +4070,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4064,7 +4093,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_LINK,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4097,6 +4126,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4119,7 +4150,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_LINK,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4151,6 +4182,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4170,7 +4203,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_REMOTE,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4182,7 +4215,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4199,6 +4232,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4218,7 +4253,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_REMOTE_GROUP,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4230,7 +4265,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4247,6 +4282,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4267,7 +4304,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_CIRCLE,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4280,7 +4317,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => '',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4298,6 +4335,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4316,7 +4355,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_CIRCLE,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4328,7 +4367,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => '',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4346,6 +4385,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4364,7 +4405,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_CIRCLE,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4376,7 +4417,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => '',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4394,6 +4435,8 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4427,7 +4470,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_EMAIL,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4439,7 +4482,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => '',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4459,6 +4502,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4478,7 +4523,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_EMAIL,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4490,7 +4535,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'owner',
'displayname_file_owner' => 'owner',
'note' => '',
'label' => null,
'label' => '',
'path' => 'folder',
'item_type' => 'folder',
'storage_id' => 'storageId',
@ -4510,6 +4555,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4529,7 +4576,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_USER,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4541,7 +4588,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_file_owner' => 'currentUser',
'displayname_file_owner' => 'currentUser',
'note' => 'personal note',
'label' => null,
'label' => '',
'path' => 'fileWithPreview',
'item_type' => 'file',
'storage_id' => 'storageId',
@ -4560,6 +4607,8 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => true,
'can_delete' => true,
'status' => [],
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, [], false
];
@ -4659,6 +4708,9 @@ class ShareAPIControllerTest extends TestCase {
$file->method('getParent')->willReturn($parent);
$file->method('getSize')->willReturn(123456);
$file->method('getMTime')->willReturn(1234567890);
$cache = $this->getMockBuilder('OCP\Files\Cache\ICache')->getMock();
$cache->method('getNumericStorageId')->willReturn(100);
$storage = $this->createMock(Storage::class);
@ -4683,7 +4735,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_ROOM,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4712,6 +4764,8 @@ class ShareAPIControllerTest extends TestCase {
'label' => '',
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, false, []
];
@ -4730,7 +4784,7 @@ class ShareAPIControllerTest extends TestCase {
$result[] = [
[
'id' => 42,
'id' => '42',
'share_type' => IShare::TYPE_ROOM,
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
@ -4759,6 +4813,8 @@ class ShareAPIControllerTest extends TestCase {
'label' => '',
'can_edit' => false,
'can_delete' => false,
'item_size' => 123456,
'item_mtime' => 1234567890,
'attributes' => null,
], $share, true, [
'share_with_displayname' => 'recipientRoomName'

View file

@ -1,125 +0,0 @@
/**
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('OCA.Sharing.App tests', function() {
var App = OCA.Sharing.App;
var fileListIn;
var fileListOut;
beforeEach(function() {
$('#testArea').append(
'<div id="app-navigation">' +
'<ul><li data-id="files"><a>Files</a></li>' +
'<li data-id="sharingin"><a></a></li>' +
'<li data-id="sharingout"><a></a></li>' +
'</ul></div>' +
'<div id="app-content">' +
'<div id="app-content-files" class="hidden">' +
'</div>' +
'<div id="app-content-sharingin" class="hidden">' +
'</div>' +
'<div id="app-content-sharingout" class="hidden">' +
'</div>' +
'</div>' +
'</div>'
);
fileListIn = App.initSharingIn($('#app-content-sharingin'));
fileListOut = App.initSharingOut($('#app-content-sharingout'));
});
afterEach(function() {
App.destroy();
});
describe('initialization', function() {
it('inits sharing-in list on show', function() {
expect(fileListIn._sharedWithUser).toEqual(true);
});
it('inits sharing-out list on show', function() {
expect(fileListOut._sharedWithUser).toBeFalsy();
});
});
describe('file actions', function() {
it('provides default file actions', function() {
_.each([fileListIn, fileListOut], function(fileList) {
var fileActions = fileList.fileActions;
expect(fileActions.actions.all).toBeDefined();
expect(fileActions.actions.all.Delete).toBeDefined();
expect(fileActions.actions.all.Rename).toBeDefined();
expect(fileActions.actions.all.Download).toBeDefined();
expect(fileActions.defaults.dir).toEqual('Open');
});
});
it('provides custom file actions', function() {
var actionStub = sinon.stub();
// regular file action
OCA.Files.fileActions.register(
'all',
'RegularTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/shared'),
actionStub
);
App._inFileList = null;
fileListIn = App.initSharingIn($('#app-content-sharingin'));
expect(fileListIn.fileActions.actions.all.RegularTest).toBeDefined();
});
it('redirects to files app when opening a directory', function() {
var oldList = OCA.Files.App.fileList;
// dummy new list to make sure it exists
OCA.Files.App.fileList = new OCA.Files.FileList($('<table><thead></thead><tbody></tbody></table>'));
var setActiveViewStub = sinon.stub(OCA.Files.App, 'setActiveView');
// create dummy table so we can click the dom
var $table = '<table><thead></thead><tbody class="files-fileList"></tbody></table>';
$('#app-content-sharingin').append($table);
App._inFileList = null;
fileListIn = App.initSharingIn($('#app-content-sharingin'));
fileListIn.setFiles([{
name: 'testdir',
type: 'dir',
path: '/somewhere/inside/subdir',
counterParts: ['user2'],
shareOwner: 'user2'
}]);
fileListIn.findFileEl('testdir').find('td .nametext').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
expect(setActiveViewStub.calledOnce).toEqual(true);
expect(setActiveViewStub.calledWith('files')).toEqual(true);
setActiveViewStub.restore();
// restore old list
OCA.Files.App.fileList = oldList;
});
});
});

View file

@ -0,0 +1,41 @@
Feature: sharing
Background:
Given using api version "1"
Given using new dav path
# See sharing-v1-part3.feature
Scenario: Creating a new share of a file shows size and mtime
Given user "user0" exists
And user "user1" exists
And As an "user0"
And parameter "shareapi_default_permissions" of app "core" is set to "7"
When creating a share with
| path | welcome.txt |
| shareWith | user1 |
| shareType | 0 |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And Getting info of last share
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And Share fields of last share match with
| item_size | A_NUMBER |
| item_mtime | A_NUMBER |
Scenario: Creating a new share of a file you own shows the file permissions
Given user "user0" exists
And user "user1" exists
And As an "user0"
And parameter "shareapi_default_permissions" of app "core" is set to "7"
When creating a share with
| path | welcome.txt |
| shareWith | user1 |
| shareType | 0 |
And the OCS status code should be "100"
And the HTTP status code should be "200"
And Getting info of last share
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And Share fields of last share match with
| item_permissions | 27 |

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/**
* @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
@ -20,3 +20,69 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
/*! For license information please see files_sharing-main.js.LICENSE.txt */
Object.assign(OC,{Share:{SHARE_TYPE_USER:0,SHARE_TYPE_GROUP:1,SHARE_TYPE_LINK:3,SHARE_TYPE_EMAIL:4,SHARE_TYPE_REMOTE:6,SHARE_TYPE_CIRCLE:7,SHARE_TYPE_GUEST:8,SHARE_TYPE_REMOTE_GROUP:9,SHARE_TYPE_ROOM:10,SHARE_TYPE_DECK:12,SHARE_TYPE_SCIENCEMESH:15}});
//# sourceMappingURL=files_sharing-main.js.map?v=325eef5d4954bda3055f
(()=>{"use strict";window.OC||(window.OC={}),Object.assign(window.OC,{Share:{SHARE_TYPE_USER:0,SHARE_TYPE_GROUP:1,SHARE_TYPE_LINK:3,SHARE_TYPE_EMAIL:4,SHARE_TYPE_REMOTE:6,SHARE_TYPE_CIRCLE:7,SHARE_TYPE_GUEST:8,SHARE_TYPE_REMOTE_GROUP:9,SHARE_TYPE_ROOM:10,SHARE_TYPE_DECK:12,SHARE_TYPE_SCIENCEMESH:15}})})();
//# sourceMappingURL=files_sharing-main.js.map?v=1bbf90b4b4aba9ea0150

View file

@ -1 +1 @@
{"version":3,"file":"files_sharing-main.js?v=325eef5d4954bda3055f","mappings":";AAwBAA,OAAOC,OAAOC,GAAI,CACjBC,MAAO,CACNC,gBAAiB,EACjBC,iBAAkB,EAClBC,gBAAiB,EACjBC,iBAAkB,EAClBC,kBAAmB,EACnBC,kBAAmB,EACnBC,iBAAkB,EAClBC,wBAAyB,EACzBC,gBAAiB,GACjBC,gBAAiB,GACjBC,uBAAwB","sources":["webpack:///nextcloud/apps/files_sharing/src/index.js"],"sourcesContent":["/**\n * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>\n *\n * @author John Molakvoæ <skjnldsv@protonmail.com>\n * @author Julius Härtl <jus@bitgrid.net>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n// register default shares types\nObject.assign(OC, {\n\tShare: {\n\t\tSHARE_TYPE_USER: 0,\n\t\tSHARE_TYPE_GROUP: 1,\n\t\tSHARE_TYPE_LINK: 3,\n\t\tSHARE_TYPE_EMAIL: 4,\n\t\tSHARE_TYPE_REMOTE: 6,\n\t\tSHARE_TYPE_CIRCLE: 7,\n\t\tSHARE_TYPE_GUEST: 8,\n\t\tSHARE_TYPE_REMOTE_GROUP: 9,\n\t\tSHARE_TYPE_ROOM: 10,\n\t\tSHARE_TYPE_DECK: 12,\n\t\tSHARE_TYPE_SCIENCEMESH: 15,\n\t},\n})\n"],"names":["Object","assign","OC","Share","SHARE_TYPE_USER","SHARE_TYPE_GROUP","SHARE_TYPE_LINK","SHARE_TYPE_EMAIL","SHARE_TYPE_REMOTE","SHARE_TYPE_CIRCLE","SHARE_TYPE_GUEST","SHARE_TYPE_REMOTE_GROUP","SHARE_TYPE_ROOM","SHARE_TYPE_DECK","SHARE_TYPE_SCIENCEMESH"],"sourceRoot":""}
{"version":3,"file":"files_sharing-main.js?v=1bbf90b4b4aba9ea0150","mappings":";mBAwBKA,OAAOC,KACRD,OAAOC,GAAK,CAAC,GAEjBC,OAAOC,OAAOH,OAAOC,GAAI,CACrBG,MAAO,CACHC,gBAAiB,EACjBC,iBAAkB,EAClBC,gBAAiB,EACjBC,iBAAkB,EAClBC,kBAAmB,EACnBC,kBAAmB,EACnBC,iBAAkB,EAClBC,wBAAyB,EACzBC,gBAAiB,GACjBC,gBAAiB,GACjBC,uBAAwB","sources":["webpack:///nextcloud/apps/files_sharing/src/main.ts"],"sourcesContent":["\"use strict\";\n/**\n * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>\n *\n * @author John Molakvoæ <skjnldsv@protonmail.com>\n * @author Julius Härtl <jus@bitgrid.net>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n// register default shares types\nif (!window.OC) {\n window.OC = {};\n}\nObject.assign(window.OC, {\n Share: {\n SHARE_TYPE_USER: 0,\n SHARE_TYPE_GROUP: 1,\n SHARE_TYPE_LINK: 3,\n SHARE_TYPE_EMAIL: 4,\n SHARE_TYPE_REMOTE: 6,\n SHARE_TYPE_CIRCLE: 7,\n SHARE_TYPE_GUEST: 8,\n SHARE_TYPE_REMOTE_GROUP: 9,\n SHARE_TYPE_ROOM: 10,\n SHARE_TYPE_DECK: 12,\n SHARE_TYPE_SCIENCEMESH: 15,\n },\n});\n"],"names":["window","OC","Object","assign","Share","SHARE_TYPE_USER","SHARE_TYPE_GROUP","SHARE_TYPE_LINK","SHARE_TYPE_EMAIL","SHARE_TYPE_REMOTE","SHARE_TYPE_CIRCLE","SHARE_TYPE_GUEST","SHARE_TYPE_REMOTE_GROUP","SHARE_TYPE_ROOM","SHARE_TYPE_DECK","SHARE_TYPE_SCIENCEMESH"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,6 +23,7 @@ import type { Config } from 'jest'
// TODO: find a way to consolidate this in one place, with webpack.common.js
const ignorePatterns = [
'@buttercup/fetch',
'@juliushaertl',
'@mdi/svg',
'@nextcloud/vue',
@ -34,6 +35,7 @@ const ignorePatterns = [
'strip-ansi',
'tributejs',
'vue-material-design-icons',
'webdav',
]
const config: Config = {

18
package-lock.json generated
View file

@ -19,7 +19,7 @@
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.1.0",
"@nextcloud/event-bus": "^3.1.0",
"@nextcloud/files": "^3.0.0-beta.10",
"@nextcloud/files": "^3.0.0-beta.11",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",
"@nextcloud/logger": "^2.5.0",
@ -3711,17 +3711,17 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@nextcloud/files": {
"version": "3.0.0-beta.10",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.10.tgz",
"integrity": "sha512-cAh2HWkFgktub/GW07qx/kYz9nR2E/D+Zk/qXF8JW7BL/+gNy4/wOJ7mfDisUZy0gCZKZTV0v5wtEkIHwNdTyA==",
"version": "3.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.11.tgz",
"integrity": "sha512-eYPtUo+pBAvY8H0pSDyBJrpUKWILIadOmPVoHKpwOFwljNN3xh+AeT1ofT3oJI6ALfrKD/lDTe18BKU8uhLADA==",
"dependencies": {
"@nextcloud/auth": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",
"@nextcloud/logger": "^2.5.0"
},
"engines": {
"node": "^16.0.0",
"npm": "^7.0.0 || ^8.0.0"
"node": "^20.0.0",
"npm": "^9.0.0"
}
},
"node_modules/@nextcloud/initial-state": {
@ -29869,9 +29869,9 @@
}
},
"@nextcloud/files": {
"version": "3.0.0-beta.10",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.10.tgz",
"integrity": "sha512-cAh2HWkFgktub/GW07qx/kYz9nR2E/D+Zk/qXF8JW7BL/+gNy4/wOJ7mfDisUZy0gCZKZTV0v5wtEkIHwNdTyA==",
"version": "3.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.11.tgz",
"integrity": "sha512-eYPtUo+pBAvY8H0pSDyBJrpUKWILIadOmPVoHKpwOFwljNN3xh+AeT1ofT3oJI6ALfrKD/lDTe18BKU8uhLADA==",
"requires": {
"@nextcloud/auth": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",

View file

@ -45,7 +45,7 @@
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.1.0",
"@nextcloud/event-bus": "^3.1.0",
"@nextcloud/files": "^3.0.0-beta.10",
"@nextcloud/files": "^3.0.0-beta.11",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",
"@nextcloud/logger": "^2.5.0",

View file

@ -58,8 +58,8 @@ module.exports = {
additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'),
collaboration: path.join(__dirname, 'apps/files_sharing/src', 'collaborationresourceshandler.js'),
files_sharing_tab: path.join(__dirname, 'apps/files_sharing/src', 'files_sharing_tab.js'),
files_sharing: path.join(__dirname, 'apps/files_sharing/src', 'files_sharing.js'),
main: path.join(__dirname, 'apps/files_sharing/src', 'index.js'),
files_sharing: path.join(__dirname, 'apps/files_sharing/src', 'files_sharing.ts'),
main: path.join(__dirname, 'apps/files_sharing/src', 'main.ts'),
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'),
},
files_trashbin: {